前言
还得练
参考:
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
完全没思路啊。。
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)
?
测试发现我们最多能执行5次命令
每次返回的东西都不一样,猜测是GPT模拟的shell
发现还在沙箱环境里面
通过提示词注入,能拿到flag1,不过题目设置有问题,随便命令输几下就把1和2全搞到了