今週出たCTFの面白かった問題(2022/5/16)

こんにちは,B4のxryuseix(@ryusei_ishika)です.CTFはチームtarutaruで参加しています.

今日は今週参加したCTFで得た知見を共有します.

参加したCTF一覧

  • VolgaCTF 2022 Qualifier
    • 難しかった,ほとんど解けてない
  • m0leCon CTF 2022
    • 難しかった,ほとんど解けてない
  • TSG LIVE! 8 CTF
    • tarutaru🐈で参加して7位
  • TJ CTF
    • tarutaruで参加して21位
    • 参加人数800チームくらい

今週の面白かった問題の紹介

TJCTF web/portalstrology

チームメイトが作成したWriteupはこちら

このような画面のWebサイトのsuperigamerbeanアカウントでログインする問題です.トークンにはJWTが使用されています.当然PWを推測することはできませんが,ソースにはこのような記載もあります.

app.get('/finaid', [check_token, check_user_applied], (req, res) => {
    if (res.locals.iat >= 1652821200) {
        res.render('finaid', { flag: `${FLAG}`, username: res.locals.username, applied: res.locals.applied })
    }
    else {
        res.render('finaid', { flag: undefined, username: res.locals.username, applied: res.locals.applied })
    }
})

iatつまり登録日がMay 17(開催日はMay 15までなので未来の日付)である必要があります.つまりパスワードを推測するのではなく,JWTを改ざんして未来の日付のセッションを取得します.
ここで,JWTは以下のように作成され,

const token = jwt.sign(
        {
            username,
        },
        privatekey,
        {
            algorithm: 'RS256',
            expiresIn: "2h",
            header: {
                alg: 'RS256',
                jku: process.env.ENV === "development" ? 'http://localhost:3000/jwks.json' : 'https://dnu-financial-aid.tjc.tf/jwks.json',
                kid: '1',
            }
        }
    )

JWTのチェックはこのようになっていました.

async function check_token(req, res, next) {
    try {
        const token = req.cookies.jwt

        const { header } = jwt.decode(token, { complete: true })
        const { kid } = header
        const jwksURI = header.jku

        await axios.get(jwksURI).then(function (response) {
            const signedKey = response.data
            const keySet = jwk.JWKSet.fromObject(signedKey)
            const key = keySet.findKeyById(kid).key.toPublicKeyPEM()

            const decoded = jwt.verify(token, key, { algorithms: ['RS256'] })

            res.locals.username = decoded.username
            res.locals.iat = decoded.iat
            next()
        })
    }
    catch {
        res.redirect('/logout')
    }
}

これを見ると,JWTのPublic KeyをホスティングするサーバはJWT内に埋め込まれていました.そこで,こちらで

  1. 秘密鍵と公開鍵のペアを用意
  2. 公開鍵をこちらでホスティング
  3. 改ざんしたJWTに自分のサーバをjkuに指定し,秘密鍵で署名
    を行います
{
    algorithm: 'RS256',
    expiresIn: "2h",
    header: {
        alg: 'RS256',
        jku: 'https://{ここに自分が用意したJWKs}/jwks.json',
        kid: '1',
    }
}

最後にこのJWTをBase64形式にして(?),cookieに設定してからリロードすればフラグが手に入りました.

個人的には公開鍵を自分でホスティングするのがまっったく思い付かず,チームメイトがかなり天才だったので印象に残ってて,この問題を選びました.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です