22 May 2023

GreyCTF Quals 2023 Writeup

Looks like my main category is misc?

Overview

Team Name: ItzyBitzySpider
Position: 18 (Singapore: 3)
Score: 3870

Scoreboard with ItzyBitzySpider in 18th place

CrashPython

Points: 50

I am hosting an uncrashable Python executor API. Crash it to get the flag.

import os
import requests

from base64 import b64decode, b64encode
from flask import Flask, render_template, redirect, request, flash, Response
from constants import FLAG, ERROR, SUCCESS, JUDGE0_SUBMIT_URL, JUDGE0_BASE_URL, DECODE_KEYS, BANNED_WORDS


app = Flask(__name__)
app.secret_key = os.urandom(512).hex()


@app.route('/', methods=['GET', 'POST'])
def index() -> Response:
    """The main endpoint"""
    if request.method == 'GET':
        with open(__file__) as f:
            source_code = f.read()
        return render_template('index.html', banned_words=BANNED_WORDS, source_code=source_code)
    args = request.form
    code = args.get('code', '')

    if len(code) == 0:
        flash('Code cannot be empty', ERROR)
        return redirect('/')

    # Sanitize code
    code = sanitize(code)
    if len(code) == 0:
        return redirect('/')

    # Send to judge0
    token = send_code_to_execute(code)
    if len(token) == 0:
        flash('An unexpected error occurred', ERROR)
        return redirect('/')

    flash("Sucessfully sent", SUCCESS)
    return redirect(f"/view/{token}")


@app.route('/view/<path:path>', methods=['GET'])
def view_code(path: str) -> Response:
    """View the submitted code"""
    view_url = f'{JUDGE0_BASE_URL}/submissions/{path}/?base64_encoded=true'
    with requests.Session() as session:
        resp = session.get(view_url)
        data = resp.json()

    for key in DECODE_KEYS:
        if data.get(key, False):
            data[key] = b64decode(data[key]).decode()

    status = data.get('status', {}).get('id', 0)
    message = data.get('message', '')
    stderr = data.get('stderr', '')

    if status == 11 and message == "Exited with error status 139" and 'Segmentation fault' in stderr:
        flash(f"Congrats, you got the flag: {FLAG}!", SUCCESS)
    return render_template('results.html', **data)


def send_code_to_execute(code: str) -> str:
    """Send code to judge0 to execute"""
    b64_code = b64encode(code.encode()).decode()
    with requests.Session() as s:
        resp = s.post(JUDGE0_SUBMIT_URL, data={
            'source_code': b64_code,
            'language_id': 71,  # Python3
            'stdin': '',
        })
    return resp.json().get('token', '')


def sanitize(code: str) -> str:
    """Sanitize code"""
    for word in BANNED_WORDS:
        if word in code:
            flash(f'Banned word detected: "{word}"', ERROR)
            return ''
    return code


if __name__ == '__main__':
    app.run(debug=True)

Upon inspecting the code, we see that the flag is printed when the program exits with segmentation fault. We can’t just run process.exit(139) as the character x is blacklisted. So first things first, we ask our favourite tutor ChatGPT. Using the first answer successfully bypasses the blacklist and causes the program to exit with segmentation fault.

Output from ChatGPT to crash python program with segmentation fault

Flag: grey{pyth0n-cv3-2021-3177_0r_n0t?_cd924ee8df15912dd55c718685517d24}

Gotcha

Points:50

I have designed my own Captcha. If you solve it fast enough I’ll give you my flag. Can you help me test it?

Gotcha challenge webpage

We are given a webpage where we have 120 seconds to get 100 “captcha” images correct. We are given a relatively clear image except for some blurring.

Captcha Text

Having done a similar project before, I began setting up Selenium (to easily interact with webpages) and Tesseract OCR. Honestly, the setup took longer than the actual solving. I had to download the chromedriver for Selenium, and the Tesseract binary for Windows

Following a guide from towardsdatascience, I increased the sharpness of the image and used Tesseract to get the OCR output. I limited the search space to uppercase letters only as suggested on the website. This gave me a 20% success rate.

Although I thought of further improvements such as ignoring overlapping boxes, I noticed that the correct answers counter does not reset on wrong answers. So if I can do 5 requests per second, it’s more than enough. I decided let the code run first and optimize later.

We don’t need to click the button as sending a text input with “\n” submits the output. In addition, we need to take care of the case that Tesseract does not output anything, and submit a random output to just go to the next image.

import io
import base64
import time
from PIL import Image, ImageEnhance
import pytesseract
from selenium import webdriver
from selenium.webdriver.common.by import By


def solve(b64Img):
    b64Img = b64Img.replace("data:image/jpeg;base64,", "")
    image_data = base64.b64decode(b64Img)
    img = Image.open(io.BytesIO(image_data))

    enhancer1 = ImageEnhance.Sharpness(img)
    enhancer2 = ImageEnhance.Contrast(img)
    img_edit = enhancer2.enhance(1.5)
    img_edit = enhancer1.enhance(15.0)

    res = pytesseract.image_to_string(
        img_edit, config="-c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    )
    print(res)
    return res


driver = webdriver.Chrome("./chromedriver_win32/chromedriver.exe")
driver.get("http://34.124.157.94:5003")
timeout = time.time() + 100

while True:
    img_element = driver.find_element(By.TAG_NAME, "img")
    img_src = img_element.get_attribute("src")

    result = solve(img_src)

    if not len(result):
        result = "ABCD\n"

    input_element = driver.find_element(By.TAG_NAME, "input")
    input_element.send_keys(result)

    if time.time() > timeout:
        break

time.sleep(30)

60s was more than enough to get 100 correct answers and the flag appears.

Gotcha Flag

Flag: grey{I_4m_hum4n_n0w_059e3995f03a783dae82580ec144ad16}