在twitter上看到INTIGRITI发的一个nodejs漏洞的悬赏(原帖),所以学习一下
【这个老外的有口音,但是马马虎虎还是听懂了是啥意思】
jwt.verify()可以自己选择secretOrPublicKey,而且加密算法是HS256(对称加密),同时secretOrPublicKey是我们可控的,利用这一点进行jwt伪造,从而进入/protected
源码:
1 | const fs = require('fs'); |
这里关键的代码就是
1 | jwt.verify(token, |
先用以下代码生成一段jwt
1 | var jwt = require('jsonwebtoken'); |
设置断点,debug一下
这里将解析jwt得到的head,赋值给了header常量
然后判断secretOrPublicKey的类型,如果是函数的话,就进入if分支。而我们这里的secretOrPublicKey就是一个匿名函数
这里就将secretOrPublicKey赋值给了getSecret,再往后看
他会执行参数secretOrPublicKey的函数,即
1 | (header, cb) => { |
根据形参判断,传入getSecret里secretOrPublicKey的值就是fs.readFileSync(header.kid)
的值
而header.kid
就是jwt中head里的kid(keyid的简称),继续往下
他会执行到这,secretOrPublicKey的值就是fs.readFileSync(header.kid)
获取的值
jws.verify(signature, algorithm, secretOrKey)
(同步)返回
true
或false
用于签名是否与secret或key匹配。
signature
是 JWS 签名。header.alg
必须是在 中找到的值jws.ALGORITHMS
。请参阅上面的受支持算法表。secretOrKey
是一个字符串或缓冲区,包含 HMAC 算法的密钥或 RSA 和 ECDSA 的 PEM 编码公钥。请注意,
"alg"
签名标头中的值将被忽略。
如果fs.readFileSync(header.kid)
获取的值和传入的jwt进行加密的secret一样,这里就会返回true。继续往下会执行到函数的末尾
1 | return done(null, payload); |
而done函数在上面被定义为传入jwt.verify()
的回调函数,payload的值就是jwt里的payload
1 | let done; |
所以最后就会返回jwt里payload的值,就意味着jwt验证成功
综上所述,就是因为jwt.verify()的secretOrPublicKey是我们可控的,同时因为用的是对称加密,所以导致了jwt可以被伪造
因为这里使用fs.readFileSync()
读取文件,我们需要利用一个可以知道内容的文件
这里就是利用linux中的/proc/sys/kernel/ftrace_enabled
ftrace是linux内置的内核调试工具,读取/proc/sys/kernel/ftrace_enabled返回的都是1\n
payload:
1 | var jwt = require('jsonwebtoken'); |