Para los que no lo conozcan, PHP es un lenguaje de scripting que permite desarrollar páginas web dinámicas. En los últimos años ha ido ganando cada vez mas protagonismo, y sitios como wikipedia, facebook o wordpress están desarrollados utilizando PHP.
En sus inicios, PHP no era mas que un conjunto de scripts en perl que funcionaban a modo de CGI. Hoy en día, PHP es un lenguaje con motor propio, llamado Zend, disponible para gran cantidad de sistemas operativos y plataformas. Uno de los puntos mas interesantes de PHP es las posibilidades que ofrece para crear entornos controlados. PHP permite ser instalado para ejecutarse como módulo de apache y correr con los privilegios de apache los scripts PHP que necesite ejecutar. Existe la posibilidad de configurar el motor de PHP para que no permita a los scripts operar fuera de unos directorios controlados, utilizar ciertas funciones o consumir mas de una cantidad determinada de recursos.
Estas funcionalidades de control sobre lo que hacen o dejan de hacer los scripts PHP han permitido la aparición de numerosos servidores que ofrecen hosting compartido para scripts PHP, pues utilizan estas funcionalidades para controlar que un cliente no afecta ni altera datos de otro cliente.
En la actualidad, estas funcionalidades son imprescindibles, no solo por los hostings compartidos, sino por la gran cantidad de agujeros de seguridad que se encuentran día tras día en scripts PHP populares, como foros, blogs, libros de visitas, scripts de noticias etc. PHP no puede garantizar que cada desarrollador del mundo haga bien su trabajo, y por eso ofrece estas funcionalidades para enjaular al máximo cada script, presuponiendo que alguno estará mal hecho.
Algunas de estas funcionalidades de control son open_basedir y safemode, entre otras. Muchas de estas funcionalidades no se recomiendan desde PHP, debido a que es muy difícil para PHP controlar todo lo que hace un script, para garantizar que no accede a donde no debe. Aún y así, hoy en día es una funcionalidad extensivamente utilizada.
Como consecuencia de estas medidas de control, han surgido una gran cantidad de agujeros de seguridad que desde un script PHP provocan errores en el motor Zend para terminar ejecutando código arbitrario, o provocar errores que dejen offline al servidor, o consuman mas recursos de los autorizados por el motor.
Este tipo de errores están pensados para ser ejecutados una vez se tiene acceso para ejecutar código PHP, y solo sirven para saltarse las restricciones del motor y conseguir acceso al sistema, o bien conseguir su mal funcionamiento. No deben confundirse con errores en los propios scripts PHP que permitan acceso a la base de datos o similares.
Durante el fin de semana pasado estube leyendo el código fuente de PHP con la intención de encontrar alguno de estos errores. PHP está desarrollado en C y sigue un modelo de plugins o extensiones. Al descargar el código tienes el motor en un directorio llamado Zend, y las extensiones base en un directorio llamado ext. Casi todas las funciones de PHP son parte de una extensión y no del core en si mismo, incluidas las funciones fsockopen, o fopen, o explode, todas son parte de extensiones.
Durante el fin de semana que estuve auditando PHP, me centré en las extensiones mas susceptibles de fallos, empecé estudiando GD, pero enseguida me di cuenta que era inmensa, y que atacar por ahí me llevaría demasiado tiempo. Poco después me fijé en shmop.
Shmop es una extensión de php, incluída en PHP por defecto, que permite crear zonas de memoria compartida. La función que llamó mi atención fue shmop_read, que permite dado un identificador de una región de memoria compartida, leer una cantidad de datos especificada, a partir de un desplazamiento especificado.
Es decir, supongamos que creamos una región de memoria compartida con shmop_open. Le damos un tamaño de 5 bytes e introducimos el texto “mundo” utilizando shmop_write. A continuación al utilizar shmop_read con el identificar de esa memoria compatida, podríamos leer “ndo” leyendo 3 bytes, con desplazamiento 2. O bien leer “do” leyendo 2 bytes con desplazamiento 3.
La función shmop_read está declarada así:
string shmop_read ( int $shmid , int $start , int $count )
El primer parámetro es el identificador de la zona de memoria compartida a leer. El segundo, es desde donde leer (el desplazamiento), y el último parámetro es la cantidad de bytes a leer.
Es la típica función de lectura con un parámetro para la cantidad a leer, y otro para “desde donde” leer, contando desde el inicio de la memoria compartida apuntada por el identificador proporcionado. Si pudiésemos especificar una cantidad de bytes a leer superior a la que hay en la región de memoria compartida, podríamos leer memoria arbitraria de PHP. O bien, si pudiésemos especificar un desplazamiento superior al tamaño de la memoria compartida, también podríamos leer memoria arbitraria de PHP.
Sin embargo, a priori la función shmop_read se encarga de comprobar ese tipo de cosas:
if (start < 0 || start > shmop->size) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, “start is out of range”);
RETURN_FALSE;}
if (start + count > shmop->size || count < 0) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, “count is out of range”);
RETURN_FALSE;
}
Primero se comprueba que start no sea menor que 0, que no tendría sentido, luego se comprueba que start no sea mayor que el tamaño de la memoria compartida que pretendemos leer. Por lo que el parámetro start de shmop_read, es seguro. No se puede especificar un valor de desplazamiento mayor que el tamaño de la memoria compartida en si misma.
El segundo parámetro, la cantidad de bytes a leer, es comprobado en el segundo if. Primero se comprueba que start + count no sean mayores que el tamaño de la memoria compartida en si misma. De esta forma, cualquier combinación de desplazamiento y cantidad de datos a leer, devolvería una porción de memoria dentro de la región de memoria compartida. Luego el if comprueba que count no sea negativo, lo cual no tendría sentido.
A priori este código es seguro. Su misión es controlar que los parámetros “start” y “count” de shmop_read no provocan que php lea fuera de la región de memoria compartida apuntada por el identificador proporcionado. Sin embargo, el problema reside en que tanto start como count están declarados así:
long shmid, start, count;
Si el servidor es de 32 bits, en el lenguaje C, long es un entero con signo de 32 bits. Eso quiere decir que el primer bit se reserva para el signo. Eso significa que el valor máximo que puede contener count o start es 2^31, es decir, 31bits todos a 1. Superado este valor, el bit que estaba reservado para el signo, se pondrá a 1, lo cual significa que el número es negativo.
Esto significa que si en count ponemos como valor exactamente 2^31, es decir 2147483647, y en start ponemos 1, la cosa queda así:
if (start < 0 || start > shmop->size) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, “start is out of range”);
RETURN_FALSE;}
Superamos este condicional, pues start = 1, por lo que ni es menor que 0, ni mayor que la memoria compartida que pretendemos leer.
A continuación:
if (start + count > shmop->size || count < 0) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, “count is out of range”);
RETURN_FALSE;
}
start + count = 2^31 + 1, o lo que es lo mismo: 2147483647+1 = -2147483647.
Por que un número negativo? Por que al superar los 31 bits, y convertirse en un número de 32 bits, el bit reservado para el signo se ha puesto a 1, indicando que el número es negativo. Por lo que -2147483647 nunca será mayor que la memoria compartida que pretendemos leer. La segundo parte del condicional comprueba que count por si solo no sea menor que 0. count por si solo es 2^31, un número positivo muy grande, mucho mayor que 0.
Hemos superado el condicional pese a haber especificado un tamaño gigante en count, mucho mayor que el área de memoria compartida que pretendemos leer, gracias a un integer overflow.
Debajo de los condiconales, hay:
startaddr = shmop->addr + start;
bytes = count ? count : shmop->size – start;return_string = emalloc(bytes+1);
memcpy(return_string, startaddr, bytes);
return_string[bytes] = 0;RETURN_STRINGL(return_string, bytes, 0);
Se reserva memoria para leer el tamaño especificado por count, que es 2^31, es decir, 2gb, y luego se hace un memcpy de 2gb empezando por la dirección de memoria de la región de memoria compartida. Obviamente, la región de memoria de PHP es mucho menor que 2gb, y al intentar hacer ese memcpy, se provoca una violación de segmento.
Una llamada a ltrace, revela:
malloc(2147745792) = 0×35cbd008
memcpy(0×35cbd024, “”, 2147483647 <unfinished …>
— SIGSEGV (Segmentation fault) —
+++ killed by SIGSEGV +++
Como vemos, se reservan 2^31 bytes, y se intentan copiar a saco dentro de 0×35cbd024, que es el puntero que luego se devolverá hacia el script php que ha ejecutado shmop_read.
Si por algún motivo, no se saliese de la memoria de PHP, por ejemplo por que previamente se haya reservado esa cantidad de memoria y los planetas se hayan alineado un poco, se recuperaría en el script php 2gb de memoria raw, con cualquier tipo de información.
Para reproducir el fallo, basta con un simple script php:
lol@mickeymouse:~$ cat lol.php
<?php
//PHP <= 5.3.5 Integer overflow (CVE-2011-1092)//discovered by Jose Carlos Norte (jcarlos.norte@gmail.com)
//
$shm_key = ftok(__FILE__, ‘t’);
$shm_id = shmop_open($shm_key, “c”, 0644, 100);
$shm_data = shmop_read($shm_id, 1, 2147483647);
//if there is no segmentation fault past this point, we have 2gb of memory!
echo $shm_data;
?>
lol@mickeymouse:~$ php lol.php
Segmentation fault
lol@mickeymouse:~$
El error termina por provocar una violación de segmento y PHP muere. Este error a priori parece no muy explotable, y su peligrosidad es muy reducida, podría permitir algún memory disclossure en algún entorno muy concreto, o provocar denegaciones de servicio en servidor compartidos, depende de la configuración del servidor web.
Es muy interesante en cambio para ilustrar como se encuentra un bug de seguridad en software real, y lo desapercibido que puede pasar un problema de seguridad como este, que en muchos casos, termina en problemas mas graves y sobretodo mas explotables.
Tras estar en contacto con el equipo de seguridad de PHP, que por cierto son muy rápidos y efectivos, el problema está solucionado en el SVN y en PHP 5.3.6 que será liberada inminentemente.
En principio había decidido no publicar nada hasta la publicación de PHP 5.3.6, pero a partir del commit en el SVN de PHP, y de la petición de un número CVE, la noticia ha corrido como la polvora, y me encuentro con este advisory en bugtraq, increíble.
Menos mal que el error no es totalmente grave, pero esto me da que pensar sobre como gestionar la información de seguridad en proyectos con SVN públicos, donde la gente ve los commits día a día…es difícil, sobretodo es difícil que no salte a bugtraq antes que la release que arregla el problema, pero bueno.