Análisis de vulnerabilidad y exploit para CVE-2011-4130 en ProFTPd < 1.3.3g/1.3.4 – (Parte I)

Introducción

En este artículo vamos a explicar los detalles de la vulnerabilidad CVE-2011-4130 basándonos el advisory oficial.

La finalidad de este artículo es permitir al lector poder reproducir la excepción de violación de segmento mediante la cual es posible tomar el control de la aplicación y ejecutar código tal y como se verá en las siguientes partes de este artículo.

Preparando la mesa de trabajo

La vulnerabilidad afecta a las versiones de ProFTPd anteriores a 1.3.3g ó 1.3.4. Así que vamos a utilizar como servidor de pruebas un Ubuntu Desktop 10.04-LTS, ya que en su repositorio a día de hoy la versión actual es la 1.3.2c afectada por la vulnerabilidad en cuestión.

Para el código fuente de la aplicación, he descargado la 1.3.2e también vulnerable, por lo que nos sirve igualmente.

Explicación de la vulnerabilidad

Tal y como podemos ver en el advisory oficial la vulnerabilidad se produce al manejar incorrectamente el pool de estados para los comandos introducidos por el usuario.

El servidor FTP permite ejecutar acciones solicitada por el cliente mientras atiende nuevas peticiones. Para ello utiliza un pool donde va almacenando la lista de estados sobre los comandos solicitados por el cliente. De esta forma puede llevar un control de las peticiones que le quedan por atender. Cuando recibe un comando, guarda el estado en el pool mientras lo ejecuta y sigue recibiendo comandos del cliente. Una vez finalizan las tareas, el servidor restaura el estado para poder indicar al cliente el resultado y continuar atendiendo más comandos.

El problema sucede al solicitarle una petición, y mientras este la atiende, enviarle otra petición con un comando no esperado, por ejemplo un comando inexistente. En este caso, el servidor sale de una función sin restaurar el estado del pool y la lista de estados queda descompensada, al restaurar el estado anterior a este, la lista esta corrupta y cuando finalmente se intenta acceder al dicho estado se hace sobre un puntero ya liberado, produciéndose el Use-After-Free.

Para más detalles en el código, se puede ver como sucede esto en la función pr_cmd_dispatch_phase():

src/data.c:672

int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int send_response) {


pool *resp_pool = NULL;


resp_pool = pr_response_get_pool(); // XXX: Almacena el pool actual

/* Set the pool used by the Response API for this command. */

pr_response_set_pool(cmd->pool); // XXX: Establece el pool a usar


if (phase == 0) { // XXX: pr_cmd_dispatch lo invoca con phase=0

/* First, dispatch to wildcard PRE_CMD handlers. */

success = _dispatch(cmd, PRE_CMD, FALSE, C_ANY);

if (!success)    /* run other pre_cmd */

success = _dispatch(cmd, PRE_CMD, FALSE, NULL);

if (success < 0) { // XXX: Si ha ocurrido un error

/* Dispatch to POST_CMD_ERR handlers as well. */

_dispatch(cmd, POST_CMD_ERR, FALSE, C_ANY);

_dispatch(cmd, POST_CMD_ERR, FALSE, NULL);

_dispatch(cmd, LOG_CMD_ERR, FALSE, C_ANY);

_dispatch(cmd, LOG_CMD_ERR, FALSE, NULL);

pr_response_flush(&resp_err_list);

return success; // XXX: Sale sin restaurar el pool inicial

}


/* Restore any previous pool to the Response API. */

pr_response_set_pool(resp_pool); // XXX: Restaura el pool inicial

return success;

}

Según el código anterior es posible salir de la función apuntando al pool cuyo comando es uno inexistente introducido por nosotros, en lugar de salir apuntando al pool que tenía antes de invocar dicha función. Esto provoca un error al intentar restaurar el pool anterior a este.

Para poder reproducir la vulnerabilidad es necesario llevar al servidor en un estado donde tenga que atender un nuevo comando mientras realiza otras acciones. En un servidor FTP esto puede provocarse al realizar una transferencia de datos, ya sea subiendo información al servidor o bien descargándola de la misma.

Cuando ejecutamos un comando de transferencia de información para descargar (RETR) o para subir información (STOR) la función encargada de manejar dichos datos es pr_data_xfer() que se muestra a continuación. Esta función es la encargada de transferir los datos y de analizar los comandos que hay en el pool de estados para ver si es capaz de atenderlos sin entrar en conflicto con la transferencia actual tal y como se puede ver en el código siguiente:

src/data.c:875

int pr_data_xfer(char *cl_buf, int cl_size) {


     //XXX: Comandos que no pueden ser atendidos durante la transferencia

     if (strcmp(cmd->argv

[0], C_APPE) == 0 ||

strcmp(cmd->argv[0], C_LIST) == 0 ||

strcmp(cmd->argv[0], C_MLSD) == 0 ||

strcmp(cmd->argv[0], C_NLST) == 0 ||

strcmp(cmd->argv[0], C_RETR) == 0 ||

strcmp(cmd->argv[0], C_STOR) == 0 ||

strcmp(cmd->argv[0], C_STOU) == 0 ||

strcmp(cmd->argv[0], C_RNFR) == 0 ||

strcmp(cmd->argv[0], C_RNTO) == 0 ||

strcmp(cmd->argv[0], C_PORT) == 0 ||

strcmp(cmd->argv[0], C_EPRT) == 0 ||

strcmp(cmd->argv[0], C_PASV) == 0 ||

strcmp(cmd->argv[0], C_EPSV) == 0) {

pool *resp_pool;


} else if (strcmp(cmd->argv[0], C_NOOP) == 0) {


} else { // XXX: Comandos que si pueden ser atendidos

pr_cmd_dispatch(cmd); // XXX: Atiende el comando


destroy_pool(cmd->pool); // XXX: Destruye el pool

Como se puede ver, si durante la transferencia se introduce un comando inexistente, y pr_cmd_dispatch() sale sin restaurar el pool, como hemos visto anteriormente, en este punto se destruye el pool anterior al que acabamos de atender.

Para provocar la excepción solo necesitamos saber dónde se va a utilizar este pool que ha sido liberado inadecuadamente. Para ello vemos como pr_data_close() que es invocado al finalizar la transferencia hace una llamada a pr_response_add():

src/data.c:616

void pr_data_close(int quiet) {


if (!quiet)

pr_response_add(R_226, _(“Transfer complete”)); //XXX

}

es en esta función, encargada de mostrar el mensaje de transferencia finalizada, dónde se hace uso del pool liberado y se produce la vulnerabilidad Use-After-Free.

src/response.c:143

void pr_response_add_err(const char *numeric, const char *fmt, …) {



// XXX: Vemos como se accede al pool (res_pool) ya liberado

resp = (pr_response_t *) pcalloc(resp_pool, sizeof(pr_response_t));

resp->num = (numeric ? pstrdup(resp_pool, numeric) : NULL);

resp->msg = pstrdup(resp_pool, resp_buf);


}

Para provocar el error, debemos enviar un comando incorrecto cuyos argumentos sean matcheados por el filtro. Para ello debemos consultar la expresión regular que filtra los argumentos antes de enviárselos al comando y así poder formar el comando que buscamos. Esto podemos verlo en el fichero de configuración /etc/proftpd.conf dónde podemos ver la siguiente línea:

/etc/proftpd/proftpd.conf:30

DenyFilter            *.*/

Por ello cualquier argumento que cumpla esta condición, servirá para alcanzar el código vulnerable y provocar la excepción.

NOTA: Es curioso ver como en el código fuente, aparece el siguiente comentario del desarrollador. Lo que indica que eran conscientes de que existía un error aquí. Buscar este tipo de comentarios en el código fuente de las aplicaciones es una manera más de descubrir vulnerabilidades ;D

src/main.c:659

int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {


/* Get any previous pool that may be being used by the Response API.

*

* In most cases, this will be NULL. However, if proftpd is in the

* midst of a data transfer when a command comes in on the control

* connection, then the pool in use will be that of the data transfer

* instigating command. We want to stash that pool, so that after this

* command is dispatched, we can return the pool of the old command.

* Otherwise, Bad Things (segfaults) happen.

*/

Estrategia a seguir

Para poder reproducir la excepción necesitaremos conectarnos al servidor, autenticarnos, y solicitarle un fichero de datos. Durante el lapso de tiempo en que recibimos los datos del fichero debemos introducir un comando no esperado por el servidor.

Para poder hacer esto de manera eficiente, deberemos programar un par de scripts que lo hagan por nosotros. El primero pone a escuchar un puerto superior al 1024, tras recibir la conexión lee 2 bytes, espera 5 segundos para permitir al otro script enviar el comando inexistente y tras ese lapso de tiempo continua recibiendo los datos para finalizar la conexión. Obviamente el fichero solicitado debe existir.

Por otro lado otro script que conecte con el servidor, se autentica, le dice al servidor la IP y el puerto al que debe enviar los datos del fichero y le pida que lo envíe. Tras esta acción es cuando puede enviar el comando inexistente durante los 5 segundos que le permite el primer script.

Script que queda a la escucha para recibir los datos del servidor (dataServerExploitFTPd.py):

http://pastebin.com/Uuk9LwjN

Script que envía los comandos al servidor para provocar la excepción (exploitProftpd.py).

http://pastebin.com/iJAFCd8e

Capturando la excepción

En primer lugar vamos a levantar el servidor ProFTPd y vamos a comprobar que está funcionando. Para ello basta con utilizar un cliente FTP cualquiera y acceder al mismo con las credenciales pertinentes. Para poder provocar la excepción, es necesario crear un fichero en el home del usuario FTP con el nombre ‘nada‘ (este será el fichero a descargar por el exploit) con algunos bytes de contenido. Una vez hemos hecho esto podemos pasar a ejecutar los scripts. Una vez finalizado, podemos ver el siguiente mensaje tras ejecutar el comando dmesg:

usuario@ubuntu:~$ dmesg


[86470.833018] proftpd[5106]: segfault at 10 ip 0000000000415efe sp 00007fff3dcde270 error 4 in proftpd[400000+97000]

Ahora vamos a capturar dicha excepción con el debugger. Primero vemos los procesos activos de ProFTPd:

usuario@ubuntu:~$ ps -aux | grep proftpd

Warning: bad ps syntax, perhaps a bogus ‘-‘? See http://procps.sf.net/faq.html

proftpd 2242 0.0 0.1 73968 1932 ? SNs Mar11 0:01 proftpd: (accepting connections)

Cuando se conecta un nuevo cliente, se crea otro nuevo proceso con un id más alto, este es el proceso que nos interesa.

usuario@ubuntu:~$ ps -aux | grep proftpd

Warning: bad ps syntax, perhaps a bogus ‘-‘? See http://procps.sf.net/faq.html

proftpd 5595 0.0 0.1 75680 1996 ? Ss 17:17 0:00 proftpd: (accepting connections)

proftpd 5683 0.0 0.2 75680 2684 ? S 17:38 0:00 proftpd: connected: ::ffff:192.168.1.11 (::ffff:192.168.1.11:40222)

Para que nos dé tiempo de hacer todo rápido, vamos a crear un fichero de comandos para GDB (gdbCommands) que inicialmente no existe, con estos dos únicos comandos:

usuario@ubuntu:~$ echo handle SIGSEGV nopass >> gdbCommands

usuario@ubuntu:~$ echo c >> gdbCommands

Ahora vamos a ejecutar el primer script (dataServerExploitFTPd.py) en una shell, abrimos otra shell y lanzamos el segundo script (exploitProftpd.py) y durante el lapso de tiempo que nos da el primer script, abrimos una tercera shell (como root) y ejecutamos el siguiente comando:

root@ubuntu:/home/usuario# gdb –p `pidof proftpd | cut –c’ ‘ –f 2` -x gdbCommands

Con esto nos attacheamos al proceso creado para atender nuestros comandos ftp, y ejecuta automáticamente los comandos del fichero (gdbCommands), lo único que hace es decirle al debugger que no pase las señales de SIGSEGV al programa, ya que queremos que las capture el debugger para analizarlo y luego le decimos que continúe con el proceso. Tras unos segundos finaliza la transmisión de ficheros y podemos ver lo siguiente:

root@ubuntu:/home/usuario# gdb -p `pidof proftpd | cut -d’ ‘ -f1` -x gdbCommands

GNU gdb (GDB) 7.1-ubuntu

Copyright (C) 2010 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law. Type “show copying”

and “show warranty” for details.

This GDB was configured as “x86_64-linux-gnu”.

Para las instrucciones de informe de errores, vea:

.

Adjuntando a process 5713

Leyendo símbolos desde /usr/sbin/proftpd…(no se encontraron símbolos de depuración)hecho.


0x00007f84881defd3 in select () from /lib/libc.so.6

Program received signal SIGSEGV, Segmentation fault.

0x0000000000415efe in ?? ()

(gdb) bt

#0 0x0000000000415efe in ?? ()

#1 0x000000000041700a in pstrdup ()

#2 0x000000000042d927 in pr_response_add ()

#3 0x0000000000450985 in ?? ()

#4 0x000000000042fbc2 in pr_module_call ()

#5 0x00000000004133a6 in ?? ()

#6 0x00000000004139ab in pr_cmd_dispatch_phase ()

#7 0x0000000000414fdc in ?? ()

#8 0x00000000004107ae in ?? ()

#9 0x0000000000412769 in ?? ()

#10 0x000000000041494d in main ()

(gdb) i r

rax 0x0    0

rbx 0x127e130    19390768

rcx 0x0    0

rdx 0x0    0

rsi 0x4    4

rdi 0x127e130    19390768

rbp 0x127e130    0x127e130

rsp 0x7fff81136690    0x7fff81136690

r8 0x47dc02    4709378

r9 0x101010101010101    72340172838076673

r10 0x7f848847e14c    140207198822732

r11 0x7f8488185962    140207195707746

r12 0x4    4

r13 0x69    105

r14 0x7fff811368bc    140735358920892

r15 0xc    12

rip 0x415efe    0x415efe

eflags 0x10202    [ IF RF ]

cs 0x33    51

ss 0x2b    43

ds 0x0    0

es 0x0    0

fs 0x0    0

gs 0x0    0

(gdb) x/8i $rip

=> 0x415efe:    mov 0x10(%rcx),%rdi

0x415f02:    jle 0x415f23

0x415f04:    lea -0x1(%rsi),%r12d

0x415f08:    and $0xfffffffffffffff8,%r12d

0x415f0c:    add $0x8,%r12d

0x415f10:    movslq %r12d,%rbp

0x415f13:    lea (%rdi,%rbp,1),%rax

0x415f17:    cmp (%rcx),%rax

(gdb) set disassembly-flavor intel

(gdb) x/8i $rip

=> 0x415efe:    mov rdi,QWORD PTR [rcx+0x10]

0x415f02:    jle 0x415f23

0x415f04:    lea r12d,[rsi-0x1]

0x415f08:    and r12d,0xfffffffffffffff8

0x415f0c:    add r12d,0x8

0x415f10:    movsxd rbp,r12d

0x415f13:    lea rax,[rdi+rbp*1]

0x415f17:    cmp rax,QWORD PTR [rcx]

Como se puede ver en el código que ha producido la excepción, se intenta acceder al registro RCX cuyo valor es 0.

En el próximo capítulo

Ahora que ya hemos sido capaces de analizar la vulnerabilidad revisando el código fuente y conseguido reproducir la excepción, pasamos a cosas más interesantes, analizar con el debugger como trabajan las funciones que reservan y liberan los pool y de qué manera podemos aprovechar esto para ejecutar código ;D Así que ir preparando vuestro entorno de trabajo que aún nos queda lo más duro.

Introducción

En este artículo vamos a explicar los detalles de la vulnerabilidad CVE-2011-4130 basándonos el advisory oficial.

La finalidad de este artículo es permitir al lector poder reproducir la excepción de violación de segmento mediante la cual es posible tomar el control de la aplicación y ejecutar código tal y como se verá en las siguientes partes de este artículo.

Preparando la mesa de trabajo

La vulnerabilidad afecta a las versiones de ProFTPd anteriores a 1.3.3g ó 1.3.4. Así que vamos a utilizar como servidor de pruebas un Ubuntu Desktop 10.04-LTS, ya que en su repositorio a día de hoy la versión actual es la 1.3.2c afectada por la vulnerabilidad en cuestión.

Para el código fuente de la aplicación, he descargado la 1.3.2e también vulnerable, por lo que nos sirve igualmente.

Explicación de la vulnerabilidad

Tal y como podemos ver en el advisory oficial la vulnerabilidad se produce al manejar incorrectamente el pool de estados para los comandos introducidos por el usuario.

El servidor FTP permite ejecutar acciones solicitada por el cliente mientras atiende nuevas peticiones. Para ello utiliza un pool donde va almacenando la lista de estados sobre los comandos solicitados por el cliente. De esta forma puede llevar un control de las peticiones que le quedan por atender. Cuando recibe un comando, guarda el estado en el pool mientras lo ejecuta y sigue recibiendo comandos del cliente. Una vez finalizan las tareas, el servidor restaura el estado para poder indicar al cliente el resultado y continuar atendiendo más comandos.

El problema sucede al solicitarle una petición, y mientras este la atiende, enviarle otra petición con un comando no esperado, por ejemplo un comando inexistente. En este caso, el servidor sale de una función sin restaurar el estado del pool y la lista de estados queda descompensada, al restaurar el estado anterior a este, la lista esta corrupta y cuando finalmente se intenta acceder al dicho estado se hace sobre un puntero ya liberado, produciéndose el Use-After-Free.

Para más detalles en el código, se puede ver como sucede esto en la función pr_cmd_dispatch_phase():

src/data.c:672

int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int send_response) {


pool *resp_pool = NULL;


resp_pool = pr_response_get_pool(); // XXX: Almacena el pool actual

/* Set the pool used by the Response API for this command. */

pr_response_set_pool(cmd->pool); // XXX: Establece el pool a usar


if (phase == 0) { // XXX: pr_cmd_dispatch lo invoca con phase=0

/* First, dispatch to wildcard PRE_CMD handlers. */

success = _dispatch(cmd, PRE_CMD, FALSE, C_ANY);

if (!success)    /* run other pre_cmd */

success = _dispatch(cmd, PRE_CMD, FALSE, NULL);

if (success < 0) { // XXX: Si ha ocurrido un error

/* Dispatch to POST_CMD_ERR handlers as well. */

_dispatch(cmd, POST_CMD_ERR, FALSE, C_ANY);

_dispatch(cmd, POST_CMD_ERR, FALSE, NULL);

_dispatch(cmd, LOG_CMD_ERR, FALSE, C_ANY);

_dispatch(cmd, LOG_CMD_ERR, FALSE, NULL);

pr_response_flush(&resp_err_list);

return success; // XXX: Sale sin restaurar el pool inicial

}


/* Restore any previous pool to the Response API. */

pr_response_set_pool(resp_pool); // XXX: Restaura el pool inicial

return success;

}

Según el código anterior es posible salir de la función apuntando al pool cuyo comando es uno inexistente introducido por nosotros, en lugar de salir apuntando al pool que tenía antes de invocar dicha función. Esto provoca un error al intentar restaurar el pool anterior a este.

Para poder reproducir la vulnerabilidad es necesario llevar al servidor en un estado donde tenga que atender un nuevo comando mientras realiza otras acciones. En un servidor FTP esto puede provocarse al realizar una transferencia de datos, ya sea subiendo información al servidor o bien descargándola de la misma.

Cuando ejecutamos un comando de transferencia de información para descargar (RETR) o para subir información (STOR) la función encargada de manejar dichos datos es pr_data_xfer() que se muestra a continuación. Esta función es la encargada de transferir los datos y de analizar los comandos que hay en el pool de estados para ver si es capaz de atenderlos sin entrar en conflicto con la transferencia actual tal y como se puede ver en el código siguiente:

src/data.c:875

int pr_data_xfer(char *cl_buf, int cl_size) {


     //XXX: Comandos que no pueden ser atendidos durante la transferencia

     if (strcmp(cmd->argv[0], C_APPE) == 0 ||

strcmp(cmd->argv[0], C_LIST) == 0 ||

strcmp(cmd->argv[0], C_MLSD) == 0 ||

strcmp(cmd->argv[0], C_NLST) == 0 ||

strcmp(cmd->argv[0], C_RETR) == 0 ||

strcmp(cmd->argv[0], C_STOR) == 0 ||

strcmp(cmd->argv[0], C_STOU) == 0 ||

strcmp(cmd->argv[0], C_RNFR) == 0 ||

strcmp(cmd->argv[0], C_RNTO) == 0 ||

strcmp(cmd->argv[0], C_PORT) == 0 ||

strcmp(cmd->argv[0], C_EPRT) == 0 ||

strcmp(cmd->argv[0], C_PASV) == 0 ||

strcmp(cmd->argv[0], C_EPSV) == 0) {

pool *resp_pool;


} else if (strcmp(cmd->argv[0], C_NOOP) == 0) {


} else { // XXX: Comandos que si pueden ser atendidos

pr_cmd_dispatch(cmd); // XXX: Atiende el comando


destroy_pool(cmd->pool); // XXX: Destruye el pool

Como se puede ver, si durante la transferencia se introduce un comando inexistente, y pr_cmd_dispatch() sale sin restaurar el pool, como hemos visto anteriormente, en este punto se destruye el pool anterior al que acabamos de atender.

Para provocar la excepción solo necesitamos saber dónde se va a utilizar este pool que ha sido liberado inadecuadamente. Para ello vemos como pr_data_close() que es invocado al finalizar la transferencia hace una llamada a pr_response_add():

src/data.c:616

void pr_data_close(int quiet) {


if (!quiet)

pr_response_add(R_226, _(“Transfer complete”)); //XXX

}

es en esta función, encargada de mostrar el mensaje de transferencia finalizada, dónde se hace uso del pool liberado y se produce la vulnerabilidad Use-After-Free.

src/response.c:143

void pr_response_add_err(const char *numeric, const char *fmt, …) {



// XXX: Vemos como se accede al pool (res_pool) ya liberado

resp = (pr_response_t *) pcalloc(resp_pool, sizeof(pr_response_t));

resp->num = (numeric ? pstrdup(resp_pool, numeric) : NULL);

resp->msg = pstrdup(resp_pool, resp_buf);


}

Para provocar el error, debemos enviar un comando incorrecto cuyos argumentos sean matcheados por el filtro. Para ello debemos consultar la expresión regular que filtra los argumentos antes de enviárselos al comando y así poder formar el comando que buscamos. Esto podemos verlo en el fichero de configuración /etc/proftpd.conf dónde podemos ver la siguiente línea:

/etc/proftpd/proftpd.conf:30

DenyFilter            *.*/

Por ello cualquier argumento que cumpla esta condición, servirá para alcanzar el código vulnerable y provocar la excepción.

NOTA: Es curioso ver como en el código fuente, aparece el siguiente comentario del desarrollador. Lo que indica que eran conscientes de que existía un error aquí. Buscar este tipo de comentarios en el código fuente de las aplicaciones es una manera más de descubrir vulnerabilidades ;D

src/main.c:659

int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {


/* Get any previous pool that may be being used by the Response API.

*

* In most cases, this will be NULL. However, if proftpd is in the

* midst of a data transfer when a command comes in on the control

* connection, then the pool in use will be that of the data transfer

* instigating command. We want to stash that pool, so that after this

* command is dispatched, we can return the pool of the old command.

* Otherwise, Bad Things (segfaults) happen.

*/

Estrategia a seguir

Para poder reproducir la excepción necesitaremos conectarnos al servidor, autenticarnos, y solicitarle un fichero de datos. Durante el lapso de tiempo en que recibimos los datos del fichero debemos introducir un comando no esperado por el servidor.

Para poder hacer esto de manera eficiente, deberemos programar un par de scripts que lo hagan por nosotros. El primero pone a escuchar un puerto superior al 1024, tras recibir la conexión lee 2 bytes, espera 5 segundos para permitir al otro script enviar el comando inexistente y tras ese lapso de tiempo continua recibiendo los datos para finalizar la conexión. Obviamente el fichero solicitado debe existir.

Por otro lado otro script que conecte con el servidor, se autentica, le dice al servidor la IP y el puerto al que debe enviar los datos del fichero y le pida que lo envíe. Tras esta acción es cuando puede enviar el comando inexistente durante los 5 segundos que le permite el primer script.

Script que queda a la escucha para recibir los datos del servidor (dataServerExploitFTPd.py):

http://pastebin.com/Uuk9LwjN

Script que envía los comandos al servidor para provocar la excepción (exploitProftpd.py).

http://pastebin.com/iJAFCd8e

Capturando la excepción

En primer lugar vamos a levantar el servidor ProFTPd y vamos a comprobar que está funcionando. Para ello basta con utilizar un cliente FTP cualquiera y acceder al mismo con las credenciales pertinentes. Para poder provocar la excepción, es necesario crear un fichero en el home del usuario FTP con el nombre ‘nada‘ (este será el fichero a descargar por el exploit) con algunos bytes de contenido. Una vez hemos hecho esto podemos pasar a ejecutar los scripts. Una vez finalizado, podemos ver el siguiente mensaje tras ejecutar el comando dmesg:

usuario@ubuntu:~$ dmesg


[86470.833018] proftpd[5106]: segfault at 10 ip 0000000000415efe sp 00007fff3dcde270 error 4 in proftpd[400000+97000]

Ahora vamos a capturar dicha excepción con el debugger. Primero vemos los procesos activos de ProFTPd:

usuario@ubuntu:~$ ps -aux | grep proftpd

Warning: bad ps syntax, perhaps a bogus ‘-‘? See http://procps.sf.net/faq.html

proftpd 2242 0.0 0.1 73968 1932 ? SNs Mar11 0:01 proftpd: (accepting connections)

Cuando se conecta un nuevo cliente, se crea otro nuevo proceso con un id más alto, este es el proceso que nos interesa.

usuario@ubuntu:~$ ps -aux | grep proftpd

Warning: bad ps syntax, perhaps a bogus ‘-‘? See http://procps.sf.net/faq.html

proftpd 5595 0.0 0.1 75680 1996 ? Ss 17:17 0:00 proftpd: (accepting connections)

proftpd 5683 0.0 0.2 75680 2684 ? S 17:38 0:00 proftpd: connected: ::ffff:192.168.1.11 (::ffff:192.168.1.11:40222)

Para que nos dé tiempo de hacer todo rápido, vamos a crear un fichero de comandos para GDB (gdbCommands) que inicialmente no existe, con estos dos únicos comandos:

usuario@ubuntu:~$ echo handle SIGSEGV nopass >> gdbCommands

usuario@ubuntu:~$ echo c >> gdbCommands

Ahora vamos a ejecutar el primer script (dataServerExploitFTPd.py) en una shell, abrimos otra shell y lanzamos el segundo script (exploitProftpd.py) y durante el lapso de tiempo que nos da el primer script, abrimos una tercera shell (como root) y ejecutamos el siguiente comando:

root@ubuntu:/home/usuario# gdb –p `pidof proftpd | cut –c’ ‘ –f 2` -x gdbCommands

Con esto nos attacheamos al proceso creado para atender nuestros comandos ftp, y ejecuta automáticamente los comandos del fichero (gdbCommands), lo único que hace es decirle al debugger que no pase las señales de SIGSEGV al programa, ya que queremos que las capture el debugger para analizarlo y luego le decimos que continúe con el proceso. Tras unos segundos finaliza la transmisión de ficheros y podemos ver lo siguiente:

root@ubuntu:/home/usuario# gdb -p `pidof proftpd | cut -d’ ‘ -f1` -x gdbCommands

GNU gdb (GDB) 7.1-ubuntu

Copyright (C) 2010 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law. Type “show copying”

and “show warranty” for details.

This GDB was configured as “x86_64-linux-gnu”.

Para las instrucciones de informe de errores, vea:

.

Adjuntando a process 5713

Leyendo símbolos desde /usr/sbin/proftpd…(no se encontraron símbolos de depuración)hecho.


0x00007f84881defd3 in select () from /lib/libc.so.6

Program received signal SIGSEGV, Segmentation fault.

0x0000000000415efe in ?? ()

(gdb) bt

#0 0x0000000000415efe in ?? ()

#1 0x000000000041700a in pstrdup ()

#2 0x000000000042d927 in pr_response_add ()

#3 0x0000000000450985 in ?? ()

#4 0x000000000042fbc2 in pr_module_call ()

#5 0x00000000004133a6 in ?? ()

#6 0x00000000004139ab in pr_cmd_dispatch_phase ()

#7 0x0000000000414fdc in ?? ()

#8 0x00000000004107ae in ?? ()

#9 0x0000000000412769 in ?? ()

#10 0x000000000041494d in main ()

(gdb) i r

rax 0x0    0

rbx 0x127e130    19390768

rcx 0x0    0

rdx 0x0    0

rsi 0x4    4

rdi 0x127e130    19390768

rbp 0x127e130    0x127e130

rsp 0x7fff81136690    0x7fff81136690

r8 0x47dc02    4709378

r9 0x101010101010101    72340172838076673

r10 0x7f848847e14c    140207198822732

r11 0x7f8488185962    140207195707746

r12 0x4    4

r13 0x69    105

r14 0x7fff811368bc    140735358920892

r15 0xc    12

rip 0x415efe    0x415efe

eflags 0x10202    [ IF RF ]

cs 0x33    51

ss 0x2b    43

ds 0x0    0

es 0x0    0

fs 0x0    0

gs 0x0    0

(gdb) x/8i $rip

=> 0x415efe:    mov 0x10(%rcx),%rdi

0x415f02:    jle 0x415f23

0x415f04:    lea -0x1(%rsi),%r12d

0x415f08:    and $0xfffffffffffffff8,%r12d

0x415f0c:    add $0x8,%r12d

0x415f10:    movslq %r12d,%rbp

0x415f13:    lea (%rdi,%rbp,1),%rax

0x415f17:    cmp (%rcx),%rax

(gdb) set disassembly-flavor intel

(gdb) x/8i $rip

=> 0x415efe:    mov rdi,QWORD PTR [rcx+0x10]

0x415f02:    jle 0x415f23

0x415f04:    lea r12d,[rsi-0x1]

0x415f08:    and r12d,0xfffffffffffffff8

0x415f0c:    add r12d,0x8

0x415f10:    movsxd rbp,r12d

0x415f13:    lea rax,[rdi+rbp*1]

0x415f17:    cmp rax,QWORD PTR [rcx]

Como se puede ver en el código que ha producido la excepción, se intenta acceder al registro RCX cuyo valor es 0.

En el próximo capítulo

Ahora que ya hemos sido capaces de analizar la vulnerabilidad revisando el código fuente y conseguido reproducir la excepción, pasamos a cosas más interesantes, analizar con el debugger como trabajan las funciones que reservan y liberan los pool y de qué manera podemos aprovechar esto para ejecutar código ;D Así que ir preparando vuestro entorno de trabajo que aún nos queda lo más duro.

Introducción

En este artículo vamos a explicar los detalles de la vulnerabilidad CVE-2011-4130 basándonos el advisory oficial.

La finalidad de este artículo es permitir al lector poder reproducir la excepción de violación de segmento mediante la cual es posible tomar el control de la aplicación y ejecutar código tal y como se verá en las siguientes partes de este artículo.

Preparando la mesa de trabajo

La vulnerabilidad afecta a las versiones de ProFTPd anteriores a 1.3.3g ó 1.3.4. Así que vamos a utilizar como servidor de pruebas un Ubuntu Desktop 10.04-LTS, ya que en su repositorio a día de hoy la versión actual es la 1.3.2c afectada por la vulnerabilidad en cuestión.

Para el código fuente de la aplicación, he descargado la 1.3.2e también vulnerable, por lo que nos sirve igualmente.

Explicación de la vulnerabilidad

Tal y como podemos ver en el advisory oficial la vulnerabilidad se produce al manejar incorrectamente el pool de estados para los comandos introducidos por el usuario.

El servidor FTP permite ejecutar acciones solicitada por el cliente mientras atiende nuevas peticiones. Para ello utiliza un pool donde va almacenando la lista de estados sobre los comandos solicitados por el cliente. De esta forma puede llevar un control de las peticiones que le quedan por atender. Cuando recibe un comando, guarda el estado en el pool mientras lo ejecuta y sigue recibiendo comandos del cliente. Una vez finalizan las tareas, el servidor restaura el estado para poder indicar al cliente el resultado y continuar atendiendo más comandos.

El problema sucede al solicitarle una petición, y mientras este la atiende, enviarle otra petición con un comando no esperado, por ejemplo un comando inexistente. En este caso, el servidor sale de una función sin restaurar el estado del pool y la lista de estados queda descompensada, al restaurar el estado anterior a este, la lista esta corrupta y cuando finalmente se intenta acceder al dicho estado se hace sobre un puntero ya liberado, produciéndose el Use-After-Free.

Para más detalles en el código, se puede ver como sucede esto en la función pr_cmd_dispatch_phase():

src/data.c:672

int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int send_response) {


pool *resp_pool = NULL;


resp_pool = pr_response_get_pool(); // XXX: Almacena el pool actual

/* Set the pool used by the Response API for this command. */

pr_response_set_pool(cmd->pool); // XXX: Establece el pool a usar


if (phase == 0) { // XXX: pr_cmd_dispatch lo invoca con phase=0

/* First, dispatch to wildcard PRE_CMD handlers. */

success = _dispatch(cmd, PRE_CMD, FALSE, C_ANY);

if (!success)    /* run other pre_cmd */

success = _dispatch(cmd, PRE_CMD, FALSE, NULL);

if (success < 0) { // XXX: Si ha ocurrido un error

/* Dispatch to POST_CMD_ERR handlers as well. */

_dispatch(cmd, POST_CMD_ERR, FALSE, C_ANY);

_dispatch(cmd, POST_CMD_ERR, FALSE, NULL);

_dispatch(cmd, LOG_CMD_ERR, FALSE, C_ANY);

_dispatch(cmd, LOG_CMD_ERR, FALSE, NULL);

pr_response_flush(&resp_err_list);

return success; // XXX: Sale sin restaurar el pool inicial

}


/* Restore any previous pool to the Response API. */

pr_response_set_pool(resp_pool); // XXX: Restaura el pool inicial

return success;

}

Según el código anterior es posible salir de la función apuntando al pool cuyo comando es uno inexistente introducido por nosotros, en lugar de salir apuntando al pool que tenía antes de invocar dicha función. Esto provoca un error al intentar restaurar el pool anterior a este.

Para poder reproducir la vulnerabilidad es necesario llevar al servidor en un estado donde tenga que atender un nuevo comando mientras realiza otras acciones. En un servidor FTP esto puede provocarse al realizar una transferencia de datos, ya sea subiendo información al servidor o bien descargándola de la misma.

Cuando ejecutamos un comando de transferencia de información para descargar (RETR) o para subir información (STOR) la función encargada de manejar dichos datos es pr_data_xfer() que se muestra a continuación. Esta función es la encargada de transferir los datos y de analizar los comandos que hay en el pool de estados para ver si es capaz de atenderlos sin entrar en conflicto con la transferencia actual tal y como se puede ver en el código siguiente:

src/data.c:875

int pr_data_xfer(char *cl_buf, int cl_size) {


     //XXX: Comandos que no pueden ser atendidos durante la transferencia

     if (strcmp(cmd->argv[0], C_APPE) == 0 ||

strcmp(cmd->argv[0], C_LIST) == 0 ||

strcmp(cmd->argv[0], C_MLSD) == 0 ||

strcmp(cmd->argv[0], C_NLST) == 0 ||

strcmp(cmd->argv[0], C_RETR) == 0 ||

strcmp(cmd->argv[0], C_STOR) == 0 ||

strcmp(cmd->argv[0], C_STOU) == 0 ||

strcmp(cmd->argv[0], C_RNFR) == 0 ||

strcmp(cmd->argv[0], C_RNTO) == 0 ||

strcmp(cmd->argv[0], C_PORT) == 0 ||

strcmp(cmd->argv[0], C_EPRT) == 0 ||

strcmp(cmd->argv[0], C_PASV) == 0 ||

strcmp(cmd->argv[0], C_EPSV) == 0) {

pool *resp_pool;


} else if (strcmp(cmd->argv[0], C_NOOP) == 0) {


} else { // XXX: Comandos que si pueden ser atendidos

pr_cmd_dispatch(cmd); // XXX: Atiende el comando


destroy_pool(cmd->pool); // XXX: Destruye el pool

Como se puede ver, si durante la transferencia se introduce un comando inexistente, y pr_cmd_dispatch() sale sin restaurar el pool, como hemos visto anteriormente, en este punto se destruye el pool anterior al que acabamos de atender.

Para provocar la excepción solo necesitamos saber dónde se va a utilizar este pool que ha sido liberado inadecuadamente. Para ello vemos como pr_data_close() que es invocado al finalizar la transferencia hace una llamada a pr_response_add():

src/data.c:616

void pr_data_close(int quiet) {


if (!quiet)

pr_response_add(R_226, _(“Transfer complete”)); //XXX

}

es en esta función, encargada de mostrar el mensaje de transferencia finalizada, dónde se hace uso del pool liberado y se produce la vulnerabilidad Use-After-Free.

src/response.c:143

void pr_response_add_err(const char *numeric, const char *fmt, …) {



// XXX: Vemos como se accede al pool (res_pool) ya liberado

resp = (pr_response_t *) pcalloc(resp_pool, sizeof(pr_response_t));

resp->num = (numeric ? pstrdup(resp_pool, numeric) : NULL);

resp->msg = pstrdup(resp_pool, resp_buf);


}

Para provocar el error, debemos enviar un comando incorrecto cuyos argumentos sean matcheados por el filtro. Para ello debemos consultar la expresión regular que filtra los argumentos antes de enviárselos al comando y así poder formar el comando que buscamos. Esto podemos verlo en el fichero de configuración /etc/proftpd.conf dónde podemos ver la siguiente línea:

/etc/proftpd/proftpd.conf:30

DenyFilter            *.*/

Por ello cualquier argumento que cumpla esta condición, servirá para alcanzar el código vulnerable y provocar la excepción.

NOTA: Es curioso ver como en el código fuente, aparece el siguiente comentario del desarrollador. Lo que indica que eran conscientes de que existía un error aquí. Buscar este tipo de comentarios en el código fuente de las aplicaciones es una manera más de descubrir vulnerabilidades ;D

src/main.c:659

int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {


/* Get any previous pool that may be being used by the Response API.

*

* In most cases, this will be NULL. However, if proftpd is in the

* midst of a data transfer when a command comes in on the control

* connection, then the pool in use will be that of the data transfer

* instigating command. We want to stash that pool, so that after this

* command is dispatched, we can return the pool of the old command.

* Otherwise, Bad Things (segfaults) happen.

*/

Estrategia a seguir

Para poder reproducir la excepción necesitaremos conectarnos al servidor, autenticarnos, y solicitarle un fichero de datos. Durante el lapso de tiempo en que recibimos los datos del fichero debemos introducir un comando no esperado por el servidor.

Para poder hacer esto de manera eficiente, deberemos programar un par de scripts que lo hagan por nosotros. El primero pone a escuchar un puerto superior al 1024, tras recibir la conexión lee 2 bytes, espera 5 segundos para permitir al otro script enviar el comando inexistente y tras ese lapso de tiempo continua recibiendo los datos para finalizar la conexión. Obviamente el fichero solicitado debe existir.

Por otro lado otro script que conecte con el servidor, se autentica, le dice al servidor la IP y el puerto al que debe enviar los datos del fichero y le pida que lo envíe. Tras esta acción es cuando puede enviar el comando inexistente durante los 5 segundos que le permite el primer script.

Script que queda a la escucha para recibir los datos del servidor (dataServerExploitFTPd.py):

http://pastebin.com/Uuk9LwjN

Script que envía los comandos al servidor para provocar la excepción (exploitProftpd.py).

http://pastebin.com/iJAFCd8e

Capturando la excepción

En primer lugar vamos a levantar el servidor ProFTPd y vamos a comprobar que está funcionando. Para ello basta con utilizar un cliente FTP cualquiera y acceder al mismo con las credenciales pertinentes. Para poder provocar la excepción, es necesario crear un fichero en el home del usuario FTP con el nombre ‘nada‘ (este será el fichero a descargar por el exploit) con algunos bytes de contenido. Una vez hemos hecho esto podemos pasar a ejecutar los scripts. Una vez finalizado, podemos ver el siguiente mensaje tras ejecutar el comando dmesg:

usuario@ubuntu:~$ dmesg


[86470.833018] proftpd[5106]: segfault at 10 ip 0000000000415efe sp 00007fff3dcde270 error 4 in proftpd[400000+97000]

Ahora vamos a capturar dicha excepción con el debugger. Primero vemos los procesos activos de ProFTPd:

usuario@ubuntu:~$ ps -aux | grep proftpd

Warning: bad ps syntax, perhaps a bogus ‘-‘? See http://procps.sf.net/faq.html

proftpd 2242 0.0 0.1 73968 1932 ? SNs Mar11 0:01 proftpd: (accepting connections)

Cuando se conecta un nuevo cliente, se crea otro nuevo proceso con un id más alto, este es el proceso que nos interesa.

usuario@ubuntu:~$ ps -aux | grep proftpd

Warning: bad ps syntax, perhaps a bogus ‘-‘? See http://procps.sf.net/faq.html

proftpd 5595 0.0 0.1 75680 1996 ? Ss 17:17 0:00 proftpd: (accepting connections)

proftpd 5683 0.0 0.2 75680 2684 ? S 17:38 0:00 proftpd: connected: ::ffff:192.168.1.11 (::ffff:192.168.1.11:40222)

Para que nos dé tiempo de hacer todo rápido, vamos a crear un fichero de comandos para GDB (gdbCommands) que inicialmente no existe, con estos dos únicos comandos:

usuario@ubuntu:~$ echo handle SIGSEGV nopass >> gdbCommands

usuario@ubuntu:~$ echo c >> gdbCommands

Ahora vamos a ejecutar el primer script (dataServerExploitFTPd.py) en una shell, abrimos otra shell y lanzamos el segundo script (exploitProftpd.py) y durante el lapso de tiempo que nos da el primer script, abrimos una tercera shell (como root) y ejecutamos el siguiente comando:

root@ubuntu:/home/usuario# gdb –p `pidof proftpd | cut –c’ ‘ –f 2` -x gdbCommands

Con esto nos attacheamos al proceso creado para atender nuestros comandos ftp, y ejecuta automáticamente los comandos del fichero (gdbCommands), lo único que hace es decirle al debugger que no pase las señales de SIGSEGV al programa, ya que queremos que las capture el debugger para analizarlo y luego le decimos que continúe con el proceso. Tras unos segundos finaliza la transmisión de ficheros y podemos ver lo siguiente:

root@ubuntu:/home/usuario# gdb -p `pidof proftpd | cut -d’ ‘ -f1` -x gdbCommands

GNU gdb (GDB) 7.1-ubuntu

Copyright (C) 2010 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law. Type “show copying”

and “show warranty” for details.

This GDB was configured as “x86_64-linux-gnu”.

Para las instrucciones de informe de errores, vea:

.

Adjuntando a process 5713

Leyendo símbolos desde /usr/sbin/proftpd…(no se encontraron símbolos de depuración)hecho.


0x00007f84881defd3 in select () from /lib/libc.so.6

Program received signal SIGSEGV, Segmentation fault.

0x0000000000415efe in ?? ()

(gdb) bt

#0 0x0000000000415efe in ?? ()

#1 0x000000000041700a in pstrdup ()

#2 0x000000000042d927 in pr_response_add ()

#3 0x0000000000450985 in ?? ()

#4 0x000000000042fbc2 in pr_module_call ()

#5 0x00000000004133a6 in ?? ()

#6 0x00000000004139ab in pr_cmd_dispatch_phase ()

#7 0x0000000000414fdc in ?? ()

#8 0x00000000004107ae in ?? ()

#9 0x0000000000412769 in ?? ()

#10 0x000000000041494d in main ()

(gdb) i r

rax 0x0    0

rbx 0x127e130    19390768

rcx 0x0    0

rdx 0x0    0

rsi 0x4    4

rdi 0x127e130    19390768

rbp 0x127e130    0x127e130

rsp 0x7fff81136690    0x7fff81136690

r8 0x47dc02    4709378

r9 0x101010101010101    72340172838076673

r10 0x7f848847e14c    140207198822732

r11 0x7f8488185962    140207195707746

r12 0x4    4

r13 0x69    105

r14 0x7fff811368bc    140735358920892

r15 0xc    12

rip 0x415efe    0x415efe

eflags 0x10202    [ IF RF ]

cs 0x33    51

ss 0x2b    43

ds 0x0    0

es 0x0    0

fs 0x0    0

gs 0x0    0

(gdb) x/8i $rip

=> 0x415efe:    mov 0x10(%rcx),%rdi

0x415f02:    jle 0x415f23

0x415f04:    lea -0x1(%rsi),%r12d

0x415f08:    and $0xfffffffffffffff8,%r12d

0x415f0c:    add $0x8,%r12d

0x415f10:    movslq %r12d,%rbp

0x415f13:    lea (%rdi,%rbp,1),%rax

0x415f17:    cmp (%rcx),%rax

(gdb) set disassembly-flavor intel

(gdb) x/8i $rip

=> 0x415efe:    mov rdi,QWORD PTR [rcx+0x10]

0x415f02:    jle 0x415f23

0x415f04:    lea r12d,[rsi-0x1]

0x415f08:    and r12d,0xfffffffffffffff8

0x415f0c:    add r12d,0x8

0x415f10:    movsxd rbp,r12d

0x415f13:    lea rax,[rdi+rbp*1]

0x415f17:    cmp rax,QWORD PTR [rcx]

Como se puede ver en el código que ha producido la excepción, se intenta acceder al registro RCX cuyo valor es 0.

En el próximo capítulo

Ahora que ya hemos sido capaces de analizar la vulnerabilidad revisando el código fuente y conseguido reproducir la excepción, pasamos a cosas más interesantes, analizar con el debugger como trabajan las funciones que reservan y liberan los pool y de qué manera podemos aprovechar esto para ejecutar código ;D Así que ir preparando vuestro entorno de trabajo que aún nos queda lo más duro.

Follow me on Twitter: @Boken_

Tags: |

Leave A Comment

Utilizamos cookies propias y de terceros para ofrecer nuestros servicios y publicidad basada en tus intereses.
Al usar nuestros servicios, aceptas el uso que hacemos de las cookies, según se describe en nuestra Política de Cookies. Más información

Las opciones de cookie en este sitio web están configuradas para "permitir cookies" para ofrecerte una mejor experiencia de navegación. Si sigues utilizando este sitio web sin cambiar tus opciones o haces clic en "Aceptar" estarás consintiendo las cookies de este sitio.

Close