English version: https://hackmd.io/s/HJhnHwTiE
CSPが有効になっているページでXSSしてCookieを盗ってください、という問題でした。
問題名が BADNONCE なので明らかにnonceの実装が悪そうです。
実際、以下のようにセッションIDに対してnonceが固定なので、これが漏れるとXSSが可能になります。
件のnonceは、ページ内の要素の属性として存在しています。
ところで、このページではscript-src
のみ制限されているので、たとえばスタイルシートなどは外部ソースから読み込み放題です。
したがって、CSS Injectionが可能です。セレクタを工夫することによって、要素の属性値を特定することができますね。
【参考リンク】
CSS Injection 再入門 – やっていく気持ち
https://diary.shift-js.info/css-injection/
ただし、管理者のブラウザを模したクローラは、毎回異なるPHPSESSIDを持つため、1度の起動で最後までnonceを抜きとって、XSSを踏ませるところまでやらないといけません。
ちょっと面倒ですが、管理者に攻撃車が用意したURLをIFRAMEで開き続けるページを踏ませて、InjectするCSSを変えながら、最終的にXSSを発火させるようにしました。
以下のような実装になりました。Web問のExploitにしてはちょっと重めかも。もっと頭のいい方法が存在する可能性もあり。
rubyで書かれたアプリケーションで、コインの送受信ができます。
たくさんのコインを集めれば、FLAGが入手できるようです。
怪しいのは送金コードで、こういう形。
ぱっと見たところ、トランザクションを考慮していないので、高頻度でリクエストを飛ばせばRace Conditionで二重送金ができそうだったんですが、軽く試したところ、タイミングがシビアでほとんどうまくいかなかったので、この方針は諦めました。
ところで、このコードをもう少しよく見ると、宛先と送金元が同一のユーザであったとき、コインが増殖することは明らかです。
もちろん、自分自身への送金はエラーになる実装となっているんですが、残高の照会をユーザ名をハッシュした値で行っているのに対して、ユーザの同一性判定は元の文字列で行っています。
つまりは、別の文字列であって、SHA1ハッシュの結果が同一になる文字列の組がもし存在すれば、無限にコインを増やすことができそうです。
SHA1の衝突といえば……SHAtteredですよね。
詳しい理屈はググってもらうとして、これを用いれば、先に述べた要件を満たすような文字列(というかバイト列)の組が用意できます。
JSONとしてnon-printableな文字を送る際に破壊されないように注意しつつ、以下のようにして用意しました。
この文字列のどちらかを使って登録した上で、もう一方の文字列を宛先として指定して送金すると、コインが増殖します。
curlを使うと容易です。
Web問です。PHPで実装された、プロフィールを登録できるサービスです。
秘密の質問として20種類のフルーツが好きか否かを選択できるようになっていて、どうやらadminの好きなフルーツをRECONすれば良いみたいです。
ソースコードを見ると、自身のプロフィールを確認するページで露骨にCSPが弱められていて、怪しさがあります。
この要素は新しい機能なので、script-src-elem
とscript-src-attr
が効いていなくて、実質XSSし放題になっているようでした。
しかしながら、このページはログインしたユーザ自身のプロフィールを表示するものですので、狙った相手にコードを実行させるのは厳しそうな雰囲気があります。
ところで、そもそも何故script-src-attr
などという特殊な(?)制限が付されているのでしょうか?
この答えは、このページのソースを注意深く見るとすぐに気が付きました。
秘密の質問がプロフィールページに表示されているんですが、この変更を禁止する目的でJavaScriptが用いられているのでした!
このコードのみ実行できるようにする目的で、部分的なunsafe-inlineが許容されていたようです。
もし、この小さなJavaScriptコードを盗むことができれば、adminの好きなフルーツを知ることできそうです。
このページでは、X-XSS-Protection: 1; mode=block
というヘッダが送信されていて、XSS Auditorがブロックモードで動作することが期待されていて、adminのブラウザもこれに従っているでしょう。
こういう場合に、XSS Auditorの誤検出を利用して、ページ内のスクリプトを盗む手法が存在します。
【参考リンク】
ブラウザのXSSフィルタを利用した情報窃取攻撃 | MBSD Blog
https://www.mbsd.jp/blog/20160407_2.html
これを利用できそうです。(できました。)
以下のような2つのIFRAMEを表示させれば、どちらか一方をXSS Auditorがブロックするはずです。
この性質を利用し、攻撃者のページで2つのIFRAMEを開かせて、どちらがブロックされたかを判別すれば良いですね。
IFRAME要素のcontentWindow.length
を見ると、XSS Auditorが作動したか否かを簡単に判別できるようでしたが、手元で試したときに何故かうまくいかなかったので(これは勘違いだったかもしれませんが)、onload
が発火するまでの時間を計測するちょっと面倒な方法で判別しています。
XSS Auditorが作動すると、関連リソースの読み込みが走らないので、onload
が早く呼ばれるはずです。
以下のように実装し、IFRAMEをプロフィールに埋め込んで、adminにアクセスさせました。
JavaScriptの記法モダンだったりレガシーだったりしていて、気持ち悪いんですが、終了ギリギリで解いていたためいろいろ焦っていて、見当違いの試行錯誤をしていた名残です。
これを用いて、フルーツ1種類ごとに計測した結果が以下のとおりです。
Captchaを連打する必要があって、激ツラかったです。チームメイトにひたすらCaptchaしてもらいました。(もっと頭の良い実装をすればよかった気もしますが。)
フルーツ | trueのonload(ms) | falseのonload(ms) | 判定結果 |
---|---|---|---|
grapes | 84 | 334 | TRUE |
melon | 347 | 65 | FALSE |
watermelon | 245 | 47 | FALSE |
tangerine | 78 | 394 | TRUE |
lemon | 83 | 418 | TRUE |
banana | 73 | 255 | TRUE |
pineapple | 79 | 452 | TRUE |
pear | 252 | 48 | FALSE |
peach | 74 | 281 | TRUE |
cherries | 76 | 336 | TRUE |
strawberry | 79 | 318 | TRUE |
tomato | 77 | 353 | TRUE |
coconut | 77 | 333 | TRUE |
mango | 92 | 404 | TRUE |
avocado | 254 | 47 | FALSE |
aubergine | 85 | 333 | TRUE |
potato | 249 | 46 | FALSE |
carrot | 72 | 321 | TRUE |
broccoli | 428 | 40 | FALSE |
mushroom | 87 | 388 | TRUE |
あとは、この結果を用いてadminのrecoveryメッセージ(FLAG)を表示させることができました。
RSA暗号
この4つが与えられるので を求めよ。
hxpCTF2017 のfleaが類題。
下のビットから決めていくDFSを書くと良い。
普通にDFS書くと(4096ビットもあって)スタックが溢れるので非再帰で実装。
TSGCTF{Absolutely, X should be 'S' in 'OPQRX'.}
として をアイゼンシュタイン整数という。
アイゼンシュタイン整数上での加算乗除と剰余算が用意されている。
を で割った余りが相違なる20個の について与えられる。
を復元せよ。
一般的な整数環であれば、中国人剰余定理の適用によって元の値を一意に復元可能である。
用意された加算乗除と剰余算が環として成り立っているのであれば、そのまま中国人剰余定理を適用できる。(この辺の記述は数ヤに怒られそう)
アイゼンシュタイン整数において、同伴という概念があり、処理の途中で何故か同伴な値になってしまう(答えが異なってしまう)ことがあったので、そこだけ若干全探索を行った。
ForensicallyのError Level Analysisを使って、ほんの少し読める画像を生成した。
そして、懸命にguessした。
ローカルリポジトリが与えられる。
コマンドミスで普通に過去コミットを漁ればflag
というファイルがあったらしい。
該当コミットをチェックアウト後、flag
の内容を出力するように、若干main.cr
を変更。
実行。
git gc
されているらしいから、とりあえず.git/packed-refs
を見る。
1c80e25f
をチェックアウトする。
あとはObliterated Fileと同様。
ttfファイルが与えられる。
言われたとおり、適当にフラグを打ってみる。
TSGCTF{flag}
という文字列を表示させると、flag
が正しくない場合、TSGCTF{flag} is incorrect
という見た目になる。
FontForgeで開くと、下の方に怪しげな文字が見える。
glyph346
は} is correct
、glyph347
は} is incorrect
のような見た目である。
Element > Font Info > Lookupsを見ると、大量にLigatureが設定されていて、これによって文字列の見た目を変えている。
この設定から、どの文字列を表示させた時にis correct
になるかを辿れば良い。
検索性が悪いから、fonttoolsを使う。
} is correct
に該当するglyph00346
を検索する。
ある特定の文字列で}
はglyph00346
が表示される。
<Lookup index="51">
に注目する。
"51"
を検索する。
完全には意味が分かってないけどとりあえずBacktrackしていく。
次にglyph00140
を検索する。
ある特定の文字列でe
はglyph00140
が表示される。
以降、index="130"
→glyph00219
(n
)→…
と辿ると、TSGCTF{ligature_state_machine}
となる。
試しに表示してみるとこうなる。
以下のスクリプトを使ってinput
を解読した。
結果は以下の通り。
guessしながら結果を整形した。
与えられたDockerfile
を使ってproblem
イメージを生成しproblem
上でコンテナを開始した。
コンテナの中で、以下のコマンドで/dev/urandom
と/dev/random
を削除し/dev/random
を再生成した。
解読結果に従って改造されたOpenSSLバイナリを作った。しかし、crypto/rand/rand_unix.c
の中のget_time_stamp()
を以下のように再改造した。
OpenSSLのソースコードを読んでkey.pem
生成をランダム化する3つの因子があることを理解した。それは、/dev/random
、生成時刻、生成するプロセスのIDであった。
解読結果から正しい/dev/random
と正しい生成時刻を知った。しかし、正しいPIDは分からないままだった。
したがって、encrypted
をコンテナの中へコピーし、以下のコマンドで総当たり攻撃を行った。
こうして、flagを見つけた。