SECCON Beginners CTF 2023 – Writeup

ctf4b 2023で自分が担当した問題のWriteupをば。あまりスマートな方法ではないものが混じっているかもしれませんし、(これはある種の定型文ですが)解法は他にもあるかもしれません。

poem

与えられたソースコード↓

#include <stdio.h>
#include <unistd.h>

char *flag = "ctf4b{***CENSORED***}";
char *poem[] = {
    "In the depths of silence, the universe speaks.",
    "Raindrops dance on windows, nature's lullaby.",
    "Time weaves stories with the threads of existence.",
    "Hearts entwined, two souls become one symphony.",
    "A single candle's glow can conquer the darkest room.",
};

int main() {
  int n;
  printf("Number[0-4]: ");
  scanf("%d", &n);
  if (n < 5) {
    printf("%s\n", poem[n]);
  }
  return 0;
}

__attribute__((constructor)) void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  alarm(60);
}

nが負の数のときのチェックが抜けているので、マイナス以降を試していったところ数個でflagをゲット。

phisher2

問題文の『目に見える文字が全てではない』が実は特大のヒント。

与えられたソースコード(app.py)↓

import os
import uuid
from admin import share2admin
from flask import Flask, request

app = Flask(__name__)

@app.route("/", methods=["GET"])
def index():
    return open("./index.html").read()

@app.route("/", methods=["POST"])
def chall():
    try:
        text = request.json["text"]
    except Exception:
        return {"message": "text is required."}
    fileId = uuid.uuid4()
    file_path = f"/var/www/uploads/{fileId}.html"
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(f'<p style="font-size:30px">{text}</p>')
    message, ocr_url, input_url = share2admin(text, fileId)
    os.remove(file_path)
    return {"message": message, "ocr_url": ocr_url, "input_url": input_url}


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0")

ソースコードその2(admin.py)↓

import os
import re
import pyocr
import requests
from PIL import Image
from selenium import webdriver

APP_URL = os.getenv("APP_URL", "http://localhost:16161/")
FLAG = os.getenv("FLAG", "ctf4b{dummy_flag}")

# read text from image
def ocr(image_path: str):
    tool = pyocr.get_available_tools()[0]
    return tool.image_to_string(Image.open(image_path), lang="eng")


def openWebPage(fileId: str):
    try:
        chrome_options = webdriver.ChromeOptions()
        chrome_options.add_argument("--no-sandbox")
        chrome_options.add_argument("--headless")
        chrome_options.add_argument("--disable-gpu")
        chrome_options.add_argument("--disable-dev-shm-usage")
        chrome_options.add_argument("--window-size=1920,1080")
        driver = webdriver.Chrome(options=chrome_options)
        driver.implicitly_wait(10)
        url = f"file:///var/www/uploads/{fileId}.html"
        driver.get(url)

        image_path = f"./images/{fileId}.png"
        driver.save_screenshot(image_path)
        driver.quit()
        text = ocr(image_path)
        os.remove(image_path)
        return text
    except Exception:
        return None


def find_url_in_text(text: str):
    result = re.search(r"https?://[\w/:&\?\.=]+", text)
    if result is None:
        return ""
    else:
        return result.group()


def share2admin(input_text: str, fileId: str):
    # admin opens the HTML file in a browser...
    ocr_text = openWebPage(fileId)
    if ocr_text is None:
        return "admin: Sorry, internal server error."

    # If there's a URL in the text, I'd like to open it.
    ocr_url = find_url_in_text(ocr_text)
    input_url = find_url_in_text(input_text)

    # not to open dangerous url
    if not ocr_url.startswith(APP_URL):
        return "admin: It's not url or safe url.", ocr_url, input_text

    try:
        # It seems safe url, therefore let's open the web page.
        requests.get(f"{input_url}?flag={FLAG}")
    except Exception:
        return "admin: I could not open that inner link.", ocr_url, input_text
    return "admin: Very good web site. Thanks for sharing!", ocr_url, input_text

基本的にadmin.pyだけ見れば良いです。

コードとしては、問題サーバーのドメイン名が含まれるURLのみが『安全なURL』で、攻撃者はURLが安全かを確認するコードをなんとかして迂回し、自前で用意したサーバーにアクセスさせ、そのURLからflagを読み取る、という感じです。

アクセスさせるサーバーにはPipedreamさんをお借りしました。

この問題の勘所は、

ocr_url = find_url_in_text(ocr_text)
input_url = find_url_in_text(input_text)

です。つまり、安全なURLかどうかの判定には一度画像化したテキストをOCRで再度テキスト化して使用するという点です。

対して(安全なことが確認後)実際にアクセスするURLは生のテキストが使われています。

加えて各URLは判定直前に正規化が行われており、最初に「https://」が出てくる箇所以降が使用されます。

そして、安全なURLとは「https://事前指定のドメイン」から始まるURLであり、その文字列から始まりさえすれば安全と判定されます。

要するに解法としては、「OCRにはhttps://に見えるがそうではないURL(もどき) + 本当にhttps://から始まるURL」を用意し、前者には安全なURLを、後者には実際にアクセスさせたいURLを設定すればOKです。

使用する攻撃手法の名前としては「ホモグラフ攻撃」です。

私が使用したのはキリル文字のрです。本物のp、キリル文字のр。はい、まったく見分けが付きません…

これをjsonの形式で送り込んであげれば無事flagゲット。URLデコードをお忘れず。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA