Resolución del Lab – SSRF 2 IntraLink
Reconocimiento
IntraLink es el portal de intranet de TechCorp, y presume de una función que a cualquier pentester le hace levantar la ceja. Genera una vista previa automática de cualquier URL que le pegues, lo que significa que hay un componente en el servidor yendo a buscar URLs por su cuenta. Ahí es donde crece un SSRF.
El reto nos entrega el front-end, así que lo leemos antes de tocar nada. En index.html vive la feature del preview, y login.php es un POST pelado, sin token CSRF a la vista. Un vistazo a las cabeceras nos da el stack, Apache con PHP 8.1.34. A partir de aquí guardamos la URL del reto en $URL y la cookie de sesión de la plataforma en cookies.txt, que reutilizamos con -b en cada petición.
El preview que no pregunta quién eres
Rastreamos rutas lógicas y aparece preview.php, que pide un parámetro url y, lo jugoso, lo hace sin exigir estar logueado en IntraLink. Un control de acceso roto que nos regala el endpoint antes de tener cuenta. Lo apuntamos contra su propio localhost para ver si muerde.
curl -sk "$URL/preview.php?url=http://127.0.0.1:80/" -b cookies.txtEl servidor contesta que no pudo conectar al puerto 80, y esa respuesta, que parece un fracaso, es justo la confirmación que buscábamos, porque significa que el servidor intentó la conexión por nosotros. El SSRF está vivo; solo hay que apuntarlo a algo que sí escuche.
Leer el código con file://
Los fetchers de PHP entienden más esquemas que http. Probamos file:// para leer ficheros del propio servidor, y lo primero es averiguar dónde vive la aplicación, así que le pedimos la configuración de Apache.
curl -sk "$URL/preview.php" --data-urlencode "url=file:///etc/apache2/sites-enabled/000-default.conf" -b cookies.txtEl DocumentRoot no es el de por defecto, es /var/www/html/app. Sin ese dato, cada intento de leer un PHP habría fallado en silencio. Con la ruta correcta, vamos directos al login.
curl -sk "$URL/preview.php" --data-urlencode "url=file:///var/www/html/app/login.php" -b cookies.txtEl código no se anda con rodeos, trae las credenciales incrustadas a fuego, employee con employee123. De paso confirmamos que php:// está en la lista negra del preview, pero file:// se coló sin problema y nos dejó leer el fuente como texto plano.
Dentro de IntraLink
Con las credenciales del propio código entramos por la puerta principal.
curl -sk -X POST "$URL/login.php" --data "username=employee&password=employee123" -b cookies.txtYa como el empleado Carlos Martínez, tenemos el mismo preview pero autenticado. Toca girar el SSRF hacia dentro de la máquina, y la pregunta de rigor en un contenedor es quién es el proceso 1.
curl -sk "$URL/preview.php" --data-urlencode "url=file:///proc/1/cmdline" -b cookies.txtResponde supervisord, que en Docker suele ser quien orquesta varios procesos a la vez. Y su configuración es el mapa de todo lo que corre dentro, incluido lo que no asoma por fuera.
curl -sk "$URL/preview.php" --data-urlencode "url=file:///etc/supervisor/conf.d/supervisord.conf" -b cookies.txtAhí está la sorpresa. Junto a Apache hay un segundo programa levantado, admin_service.py, un servicio que jamás aparecería en un escaneo de puertos desde fuera.
El servicio que solo escuchaba a localhost
Ya que file:// lee lo que le pidamos, le pedimos el código del propio servicio para saber qué expone y por dónde.
curl -sk "$URL/preview.php" --data-urlencode "url=file:///var/www/html/app/admin_service.py" -b cookies.txtEl servicio escucha en 127.0.0.1:9090 con un cartel de solo accesible desde localhost, y sirve un /admin con la flag y un /api/config con los secretos de producción. Esa frontera de localhost es exactamente la que un SSRF derriba.
Puente al panel interno
El preview de IntraLink vive en la misma máquina que el servicio interno, así que lo usamos de puente y le pedimos que consulte el panel de administración por nosotros.
curl -sk "$URL/preview.php" --data-urlencode "url=http://127.0.0.1:9090/admin" -b cookies.txtEl cartel de que solo se accede desde localhost se cumple al pie de la letra, y por eso mismo no protege nada. La petición nace en el propio servidor de IntraLink, así que para el panel interno viene de casa, de 127.0.0.1. Un control basado en la IP de origen se desmorona en cuanto otra pieza de la misma máquina reenvía por ti. IntraLink prometía una vista previa de cualquier URL, y cumplió su palabra hasta enseñarnos el panel que se creía a salvo.
