目录

  1. 1. 前言
  2. 2. jvm.go
  3. 3. YourWA(Unsolved)
  4. 4. Spectre(Unsolved)
  5. 5. EzQl(Unsolved)
  6. 6. BlackJack(Unsolved)
  7. 7. give your shell(misc)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

WMCTF2024复现

2024/9/7 CTF线上赛
  |     |   总文章阅读量:

前言

还得练

参考:

https://r3kapig-not1on.notion.site/WMCTF-2024-Writeup-by-r4kapig-57287aa7ffda4cd799339aa3b085393f#6fbbb8e3b6d7418989f7cba3186506e3

https://blog.wm-team.cn/index.php/archives/80/#EzQl


jvm.go

看起来像是拿go实现了jvm

看了一眼 Dockerfile 拉的还是java的镜像,不是很懂,总之看一下class

package com.ctf;

import fi.iki.elonen.NanoHTTPD;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Map;

public class App extends NanoHTTPD {
   public App() throws IOException {
      super(8080);
      this.start(0, false);
      System.out.println("Running! Point your browsers to http://localhost:8080/");
   }

   protected boolean useGzipWhenAccepted(NanoHTTPD.Response r) {
      return false;
   }

   public NanoHTTPD.Response serve(NanoHTTPD.IHTTPSession session) {
      Map<String, String> parms = session.getParms();
      String page = (String)parms.getOrDefault("page", "index.html");

      try {
         FileInputStream fs = new FileInputStream(page);
         byte[] b = new byte[fs.available()];
         fs.read(b);
         String text = new String(b);
         fs.close();
         return page.contains("flag") ? newFixedLengthResponse("you know the rules and...") : newFixedLengthResponse(text);
      } catch (IOException var7) {
         return newFixedLengthResponse("page not found");
      }
   }

   public static void main(String[] args) {
      try {
         new App();
      } catch (IOException var2) {
         System.out.println("Start server faild:\n" + var2);
      }

   }
}

注意到一个有趣的地方,这里文件读取的执行顺序是:先读取再判断 page 是否为 flag

fs.read(b)fs.close()之间存在可以竞争的机会

那么只需要我们不断请求 /flag,然后爆破 fd 句柄就能读到 /flag

(怪事,docker拉不下来没得复现)


YourWA(Unsolved)

// ! part of index.ts

import { $ } from "bun"


await import('node:fs').then(async fs => {
    await $`echo $FLAG > ./flag.txt`.quiet()
    fs.openSync('./flag.txt', 'r')
    await $`rm ./flag.txt`.quiet()
})

const server = Bun.serve({
    port: 3031,
    async fetch(req) {
        // ... Collapsed
        return Res.NotFound()
    },
    error(e) {
        console.error(e)
        return Res.NotFound()
    },
})

删flag了,发现fs.openSync('./flag.txt', 'r')没关闭句柄就删,那就去/proc/{pid}/fd里面找就行


Spectre(Unsolved)

x不动一点

hint:

# Hints

There's no RCE, R/W. Only XSS.

## Run program

To run the program with all development features, you can use the following commands:

```shell
pnpm install
pnpm build
pnpm test # or `pnpm run start:dev`
```

It's recommended to visit the program on localhost or over HTTPS, for some features only work on them due to browser security policies.

## Fast reading

The following hints may help you locate the important codes more quickly:

- Line in `app.main.mjs:238`
- Function in `src/middleware.mjs:102`
- Function in `src/middleware.mjs:112`
- Line in `app.assets.mjs:32`

## Project structure

Followings are the structure of this project:

- `app.main.mjs`: Main application
- `app.assets.mjs`: Static assets (local visit only)
- `src/`: Back-end source code
- `public-src/`: Front-end source code
- `views/`: Front-end HTML templates
- `public/`: Front-end build output
- `assets/`: Static assets (local visit only)

纯XSS啊。。。

找一下flag在哪

app.main.mjs

root.get('/flag', cors, csp, ensureAdmin, async (ctx, next) => {
    // let flag = fs.readFileSync('flag.txt');
    let flag = process.env?.FLAG || 'flag{test_flag}';
    ctx.body = `<pre><code>${flag}</code></pre>`
    ctx.set('Content-Type', 'text/html');
});

跟一下ensureAdmin

/**
 * @type {Koa.Middleware}
 */
export async function ensureAdmin(ctx, next) {
    const tokenData = parseTokenData(ctx);
    if (!tokenData || tokenData.role !== 'admin') {
        return ctx.throw(401);
    }
    ctx.token = tokenData;
    await next();
}

要验token是否为 admin 的

找一下创建 admin 的位置,在bot.mjs

function createRandomUser(role, overflow = 0) {
    // never conflict on client side if overflow > 0
    const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
    let uid = randomString(alphabet, 10 + overflow);
    let password = randomString(alphabet, 20 + overflow);
    let password_sha256 = crypto.createHash('sha256').update(password).digest('hex');
    Storage.account.set(uid, { password: password_sha256, role: role });
    return { uid, password, password_sha256, role };
}

function genDefaultAccount() {
    console.log("------ Default Account ------");
    let d = createRandomUser('developer');
    console.log(`Developer uid: ${d.uid}`);
    console.log(`Developer password: ${d.password}`);
    console.log(`Developer password sha256: ${d.password_sha256}`);
    let a = createRandomUser('admin');
    console.log(`Admin uid: ${a.uid}`);
    console.log(`Admin password: ${a.password}`);
    console.log(`Admin password sha256: ${a.password_sha256}`);
    console.log("-----------------------------");
}
if (Config["generate_default_account"]) {
    genDefaultAccount();
}

跟一下 Config ,到config.mjs

export default {
    "main_port": 3000,
    "assets_port": 3001,
    // "token_key": process.env["TOKEN_KEY"] || "h1LxPW90aJehe6sV",
    "token_key": process.env["TOKEN_KEY"] || randomTokenKey(16),
    "placeholder_code_default": "<!-- Write your code here -->",
    "placeholder_code_404": "<!-- This is not what you are looking for -->",
    "default_role": "user",
    "bot_visit_timeout": 30 * 1000,
    "generate_default_account": (process.env["NODE_ENV"].trim() === "development") ? true : false,
    "cf_turnstile": {
        "enable": false,
        "site_key": "",
        "secret_key": ""
    }
}

看下csp:

/**
 * @type {Koa.Middleware}
 */
export async function csp(ctx, next) {
    const nonce = ctx.nonce ||
        crypto.randomBytes(18).toString('base64').replace(/[^a-zA-Z0-9]/g, '');
    // let srcOriginPrefix = ctx.request.protocol + "://" + ctx.request.host.split(":")[0];
    let srcOriginPrefix = 'http://localhost';
    let assetsSrc = srcOriginPrefix + ':' + Config["assets_port"].toString();
    ctx.set('Content-Security-Policy', [
        ['default-src', `'self'`],
        ['script-src', `'nonce-${nonce}'`, 'blob:', assetsSrc],
        ['worker-src', `'self'`, 'blob:'],
        ['style-src', `'nonce-${nonce}'`, 'blob:'],
        ['connect-src', `'self'`, 'https:'],
        ['object-src', `'none'`],
        ['base-uri', `'self'`],
        ['frame-src', `'self'`, 'https://challenges.cloudflare.com']
    ].map(a => a.join(' ')).join(';'));
    await next();
}

转成请求头的形式丢给Google CSP Evaluator

Content-Security-Policy: default-src 'self'; script-src 'nonce-{nonce}' blob: assetsSrc; worker-src 'self' blob:; style-src 'nonce-{nonce}' blob:; connect-src 'self' https:; object-src 'none'; base-uri 'self'; frame-src 'self' https://challenges.cloudflare.com

image-20240907092031521

完全没思路啊。。


EzQl(Unsolved)


BlackJack(Unsolved)

hint1:CVE-2024-21733 2:POST Upload file

只有一个路由?

@RequestMapping({"/check"})
public String adminAccess() {
    int port = Integer.parseInt(this.env.getProperty("local.server.port", "8080"));
    String flag = System.getenv("ICQ_FLAG");
    String targetUrl = "http://127.0.0.1:" + port + "/where_is_my_flag";
    HttpHeaders headers = new HttpHeaders();
    headers.set("Host", "127.0.0.1:" + port);
    headers.set("accept-language", "zh,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7");
    headers.set("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
    headers.set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36");
    headers.set("Password", flag);
    HttpEntity<String> entity = new HttpEntity((Object)null, headers);

    try {
        this.restTemplate.exchange(targetUrl, HttpMethod.POST, entity, String.class, new Object[0]);
    } catch (Exception var7) {
    }

    return "ok";
}

内网有个 /where_is_my_flag,flag在 headers 里面

看起来要想办法获取this.restTemplate.exchange(targetUrl, HttpMethod.POST, entity, String.class, new Object[0]);的请求体

那么关注的重点就在RestTemplate


give your shell(misc)

image-20240907164909591

测试发现我们最多能执行5次命令

image-20240907184205052

每次返回的东西都不一样,猜测是GPT模拟的shell

image-20240907192747440

发现还在沙箱环境里面

通过提示词注入,能拿到flag1,不过题目设置有问题,随便命令输几下就把1和2全搞到了