PHP-CLI se trata de la interfaz bajo línea de comandos de PHP y en ciertas ocasiones (podría sorprendernos), nos lo encontraremos como servidor de web de aplicaciones internas o incluso externas (siendo la alternativa a los típicos Web Server Apache o Nginx), un poco más adelante revisaremos un indicio de como detectarlo en nuestras auditorías web revisando algunas cabeceras de las respuestas del servidor.
Lo que vengo a traerles hoy día aquí es una breve revisión práctica del fabuloso post que publicaba la gente de ProjectDiscovery en su blog, que por supuesto os lo recomiendo encarecidamente, sus entradas técnicas son la caña! El post original al que me refiero lleva el título de: «PHP Development Server <= 7.4.21 – Remote Source Disclosure» y fue escrito por los investigadores Harsh Jaiswal (@rootxharsh) y Rahul Maini (@iamnoooob). Recuerdo ver este título del post y para seros sincero, pasar un poco de ello. Fue lo típico de dejarlo marcado en pendiente de leer y profundizar, como cientos de enlaces al día y al final nunca volver al mismo; Hasta que de buenas a primeras un colega de estudio (Freddy J. Fernandez) me insistio en su revisión y por tanto fue cuando le di prioridad y madre mía que estudio, en el se produndiza el por que sucede esta revelación a nivel de código en las versiones 7.4.21 y anteriores de PHP.
Pues básicamente eso, que realizando algunas manipulaciones (lo veremos), es totalmente factible visualizar y leer el código fuente de cualquier fichero PHP (como si de un libro de texto se tratara), que recordemos solo es accesible para el servidor y no para el cliente, ya que éste se interpreta y normalmente no tendríamos que verlo más que en su presentación desde el navegador.
Para montar el laboratorio en Docker y siguiendo las indicaciones de ProjectDiscovery, emplearé el siguiente fichero Dockerfile:
FROM php:7.4.21-zts-buster
COPY index.php /var/www/html/index.php
COPY info.php /var/www/html/info.php
COPY secret.php /var/www/html/secret.php
COPY secret.txt /root/secret.txt
CMD ["php", "-S", "0.0.0.0:8002", "-t", "/var/www/html/"]
Después de tener todos los ficheros que queremos pasar a nuestro contenedor preparados, ejecutaremos:
docker build . -t php-src-diclosure

Arrancamos la instancia:
docker run -p 8002:8002 php-src-diclosure

Desde el navegador observamos la presentación de nuestro primer fichero PHP (index.php):

Llevemos ahora esta petición a Burp y observemos el tipo de respuesta del servidor:

Nótese que la cabecera X-Powered-By
en la respuesta, se muestra bastante reveladora para identificar la tecnología concreta detrás de esta página web.
Por un momento nos ponemos en la situación de ser externos al laboratorio y por tanto tener que «descubrir» los potenciales ficheros PHP que pueda contener el objetivo, para esta misión tiraremos el potente FFUF:
ffuf -w /usr/share/wordlists/dirb/common.txt -u http://127.0.0.1:8002/FUZZ.php

En este caso conseguiremos 3 ficheros. El primero (info.php) nos mostrará la ejecución de la típica función phpinfo()
:

Mientras que el tentador fichero secret.php nos plantea la siguiente cuestión que asumiremos como reto:

Si observamos la respuesta del servidor desde Burp obtendremos esto:

Nótese que por un tema de simplicidad he acortado petición a su expresión mínima necesaria.
Por ahora del código PHP no tenemos ni rastro, como es de esperar.
Bien, ahora yendo a los que nos trae hasta aquí, ¿Cómo aplicamos esta técnica?
Pues bastará con generar una petición de este tipo:
GET /secret.php HTTP/1.1
Host: 127.0.0.1:8002
GET / HTTP/1.1
¿Qué? ¿Doble método/verbo HTTP? ¿Nos hemos vuelto locos?
Probemos y veamos resultados entonces (he dejado visible los caracteres no imprimible para una mejor visualización):

¿Qué pasa? ¿Por qué no logramos leer el PHP?
Pues básicamente por que por defecto nuestro Burp nos añadirá la cabecera Content-Length
«automágicamente» y esto nos condicionará los resultados:

Pero podremos y debemos (para esta prueba) deshabilitar la inclusión de esta cabecera desde las configuraciones en el propio Repeater
:

Para hacerlo en el apartado Proxy
debemos ir a Options
y en la sección Intercept Client Requests
desmarcar el apartado: Automatically update Content-Legth header when the request is edited
Entonces ahora si, vemos como nos va con ya todos los cambios previos asumidos:

Tal y como se evidencia, ahora si logramos ver el código fuente del fichero PHP, que en este caso será el del index.php y ¿Por qué el index.php y no el secret.php que en principio es de nuestro mayor interés?
Esto se explica bastante bien en el post original, pero básicamente nos quedamos que sucede cuando si existe un fichero index.php en el directorio (un poco más adelante aclararé un aspecto, al respecto). Bien, el truco para «saltar» esto se cubre añadiendo la consulta a un fichero estadísticamente inexistente en el servidor, para este caso he usado x.x, veamos el resultado:

Listo, el código fuente del fichero secret.php ha sido revelado y efectivamente lograremos ver la «magia» de dicho recurso que se ejecuta en el servidor.
Para darle un toque de CTF al asunto he creado esta estructura, que básicamente conseguirá leer el contenido del fichero local /root/secret.txt
en cuanto le indiquemos el parámetro correspondiente, obviamente se trata de un valor poco adivinable.
Observemos el resultado que conseguiríamos desde el navegador al realizar la consulta adecuadamente:

Ahora bien, lo he comentado entre paréntesis líneas más arriba, antes de emplear el truco de x.x, realmente no es que veamos el contenido del index.php, sino que seguimos viendo el código fuente representado, por decirlo así, solo el HTML sin presencia del código PHP, como lo veríamos si examinamos el código fuente de la página desde cualquier navegador. Empleando la misma técnica que usamos para leer secret.php ahora la emplearemos para leer index.php y veamos de lo que os hablo:

Efectivamente ahora si que leemos el código fuente del fichero index.php en su totalidad (incluyendo la estructura en PHP).
Ahora de bonus veamos mediante esta misma técnica el fichero info.php:

Como curiosidad observemos los últimos registros del servidor web:

Y ya por último la estructura montada en nuestro contenedor Docker:

Espero que os pueda resultar de interés y ayuda para vuestra cacerías próximas, si es verdad que esta versión de PHP ya tiene su tiempo, sin embargo no siempre nos encontraremos testeando versiones actualizadas, ¿no?