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

[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.dtcms.com/a/357544.html

相关文章:

  • 日本IT|C++相关面试及问答技巧分享
  • STM32项目分享:基于单片机的自行车测速系统设计
  • Win11 压缩实测:Win11 的压缩软件的最佳配置和使用方式
  • 网站酷炫换皮肤?——PC 端 H5 换肤方案实战分享
  • WebGIS开发智慧校园(8)地图控件
  • A股大盘数据-20250829 分析
  • 03.《交换的底层逻辑:从基础到应用》
  • vue3中安装tailwindcss
  • ​​字节跳动重磅开源 Seed-OSS 大模型系列,12T tokens训练,原生支持512K长上下文​
  • python 2025/7/28
  • 【完整源码+数据集+部署教程】工地建筑进度监测系统源码和数据集:改进yolo11-SDI
  • 【笔记】扩散模型(一二)U-ViT|Diffusion with Transformer
  • 智慧园区系统:基于Java微服务架构与全栈信创国产化的数字化赋能平台
  • 人工智能一些基础概念与应用场景学习笔记
  • C++基础(③反转字符串(字符串 + 双指针))
  • solidity地址、智能合约、交易概念
  • Pointer--Learing MOOC-C语言第九周指针
  • 鸿蒙地址选择库(ArkTs UI)
  • Idea2025.2 MybatisX插件失效问题
  • Suno-API - OpenI
  • 【计算机网络】前端基础知识Cookie、localStorage、sessionStorage 以及 Token
  • 04.《VLAN基础与配置实践指南》
  • 掌握 Linux 文件权限:chown 命令深度解析与实践
  • css绘制三角形
  • 软件开发准则
  • 隧道搭建技术
  • 零成本解锁 Cursor Pro:虚拟卡白嫖1个月+14天试用全攻略
  • 鬼泣:索定系统
  • 基于能量方法的纳维-斯托克斯方程高阶范数有界性理论推导-陈墨仙
  • Java接口和抽象类的区别,并举例说明