当前位置: 首页 > ops >正文

[HFCTF2020]EasyLogin

文章目录

  • TRY
  • WP
  • 总结

TRY

注册admin报错username wrong。
随便注册一个用户点击GetFlag,permission deny。
猜测可能是需要admin权限。

看cookie发现有:
sses:aok:eyJ1c2VybmFtZSI6ImEiLCJfZXhwaXJlIjoxNzU2NDU1NjczMTAxLCJfbWF4QWdlIjo4NjQwMDAwMH0=
sses:aok.sig:cPcLr5TdZHkihzRoMmGXTwP_0wM

sses:aok可以base64解码:{“username”:“a”,“_expire”:1756455673101,“_maxAge”:86400000}
第二个参数表示失效时间,第三个参数表示最大存活时长。sses:aok.sig应该就是签名。

这种 sses:aok + sses:aok.sig 的组合,是 **“自定义令牌 + 签名验证” 的轻量化认证方案 **,核心逻辑是 “用 Base64 编码携带身份与有效期信息,用独立签名确保完整性与合法性”。

签名的编码方式不清楚,AI说是对Hash编码的二进制数进行Base64url编码。

解题方向就两个,第一,登录admin账户;第二:爆破签名,伪造cookie认证。

尝试约束攻击登录admin账户,不可行。

WP

漏掉了一些线索。
从浏览器控制台中的调试中能看到前端代码
在这里插入图片描述
app.js中留下线索:
/**

  • 或许该用 koa-static 来处理静态文件
  • 路径该怎么配置?不管了先填个根目录XD
    */
    在 Koa.js(Node.js 的一个 Web 框架)中,koa-static 是一个常用的中间件(middleware),用于处理静态文件的访问请求。
    简单来说,它能让服务器直接返回指定目录中的静态资源(如 HTML、CSS、JavaScript、图片、字体等),而无需开发者编写额外代码来读取和返回这些文件。具体功能:

koa-static 的主要功能是:
指定静态文件目录:告诉 Koa 服务器 “某个目录下的文件是静态资源,可以直接对外提供访问”。
自动映射请求路径:当客户端请求某个路径时(如 /css/style.css),koa-static 会自动去你指定的静态目录中查找对应的文件(如 ./public/css/style.css),并返回给客户端。
处理 MIME 类型:自动识别文件类型(如 .html、.js、.png),并在响应头中添加正确的 Content-Type,确保浏览器能正确解析文件。

作用就像是Golang中的HTTP.FileServer。根据线索提示将根目录指定为静态文件目录,那源码就成了可访问文件了。
于是直接访问/app.js得到Web 应用程序入口文件:

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}...`);

具体业务逻辑处理在controllers文件夹中。但是并不知道文件名。
根据wp,源码文件是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();}
};

代码中有几个重要的地方:

  1. 注册的时候会生成global [secretid=>secret],secretid存储在jwt的payload中。
  2. 登陆的时候根据secretid取出secret进行签名验证。
    const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid; 在取出secretid时没有验证签名,我们可以伪造。
  3. if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) { throw new APIError('login error', 'no such secret id'); }对secretid进行了过滤。
  4. const user = jwt.verify(token, secret, {algorithm: 'HS256'});在验证token的时候指定了签名算法’HS256’,似乎无法通过签名算法置none绕过。

secretid可伪造 + secretid过滤不彻底 + Node.js 的jsonwebtoken库低版本存在缺陷:即使指定了验证签名时的算法,如果secret为空,会默认用none来验证

以上三个条件就导致了本题中的漏洞:首先我们伪造secretid为[]即空数组,此时能通过过滤,并且global.secrets[secretid]得到的应该是undefined或者是null,我也没有验证过。验证签名时就会用none算法,我们只需要构造签名算法为none的token就能通过验证。

构造payload:
在这里插入图片描述
在这里插入图片描述
成功登录。直接就能获取flag了。

总结

这道题首先从前端JS代码中获得线索,koa-static处理静态文件时将工作区根目录指定为静态文件目录,这就导致了工作区所有源码文件泄露。但是api.js文件名来的就有些奇怪,或许这是Node.js项目中的常规命名?得到源码后能看到使用的是JWT认证,由于多个条件组合导致了JWT伪造漏洞。漏洞修改建议:对于客户端传输的token,应该遵守 在验证其正确性之后才能从中获得任何信息 的规范,避免用户伪造。用白名单限定secretid的数据类型而不是黑名单。升级jwt模块版本。另外有一个注意的点是,浏览器控制台中的网络监视器并不能捕获所有发送到服务端的请求,例如这道题,由此我漏掉了重要线索,所以下次还是要用bp。

http://www.xdnf.cn/news/19103.html

相关文章:

  • UCIE Specification详解(九)
  • 平安养老险深分开展“金融护航,安居鹏城”新市民金融服务宣传活动
  • React Native 初体验
  • LeetCode 完全背包 279. 完全平方数
  • 任意函数都有原像
  • Linux之Shell编程(二)
  • Python中一些包的使用
  • 【黑客技术零基础入门】黑客入门教程(非常详细)从零基础入门到精通,看完这一篇就够了
  • Python结构化模式匹配:解析器的革命性升级
  • playbook剧本
  • Centos卸载anaconda
  • 力扣p1011在D天送达包裹的能力 详解
  • 【网弧软著正版】2025最强软著材料AI生成系统,基于GPT5.0
  • 嵌入式Linux驱动开发:i.MX6ULL中断处理
  • 【面试场景题】怎么做业务领域划分
  • 163.在 Vue3 中使用 OpenLayers 解析 GeoJSON,并给 Feature 填充 pattern(图案)颜色
  • 交叉编译 手动安装 libzip 库 移植ARM 需要 zlib的
  • mysql安全运维之安全模型与原则-构建坚不可摧的数据库防护体系
  • 《AI智脉速递》2025 年 8 月22 日 - 29 日
  • 面向马赛克战的未来智能化作战体系发展展望
  • Linux设备驱动
  • Allegro X PCB设计小诀窍系列--26.如何在Allegro X中加密保护PCB文件?
  • Pycharm打包PaddleOCR过程及问题解决方法
  • 【Mentor Xpedition】预习一下
  • 投资之路:财富积累与人生规划的智慧
  • UART和SPI区别
  • ros2--topic/话题--接口
  • 多线程图像发送处理器的设计与实现
  • 12、做中学 | 初一上期 Golang函数 包 异常
  • cssword属性