文章目錄
- 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();}
};
代碼中有幾個重要的地方:
- 注冊的時候會生成global [secretid=>secret],secretid存儲在jwt的payload中。
- 登陸的時候根據secretid取出secret進行簽名驗證。
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
在取出secretid時沒有驗證簽名,我們可以偽造。 if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) { throw new APIError('login error', 'no such secret id'); }
對secretid進行了過濾。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。