ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI攻防 外部资源加载利用

AI攻防 外部资源加载利用 一个分享对话功能把用户输入原样写进 JSONP 文件前端再用innerHTML渲染一个检测 XSS的验证接口谁调用都发 Flag。 两个靶场串起同一条链为安全而生的功能最后自己成了缺口。⚠️ 本文所有测试均在授权靶场中完成仅用于安全学习与防御研究请勿用于未授权目标。 〇、先看通关记录#关卡难度卡在哪一句话打法关键 payload1看看我的分享功能怎么啦3 级分享内容直写 JSONP前端innerHTML渲染事件型 XSS 触发alert也可直接调/xssapiimg srcx onerroralert(XSS)2看看我的分享功能怎么啦24 级新增 URL 过滤但相对路径放行只能走完整 XSS 链触发alertimg srcx onerroralert(XSS)两关的命门是同一个后端把用户输入当数据存进分享文件前端却把它当代码渲染。 一、原理什么叫外部资源加载利用 1.1 靶场是什么应用一个 AI 聊天站核心有两个功能功能接口说明对话POST /chat用户发消息大模型回复分享POST /share把user_messageai_reply打包返回一个share_id分享页/ai.html?#/ai/Dialogue?Url/share/id.js用script加载 JSONP 文件并渲染聊天记录验证POST /xssapi接收弹窗内容校验通过后返回 Flag 一个环境细节靶场的公共 API Key 余额不足对话报错402需自备 DeepSeek Keyhttps://api.deepseek.com/v1/chat/completions模型deepseek-chat才能恢复对话功能。漏洞本身与这一层无关但没它就没法正常走业务流程。 1.2 三处关键机制审计ai.html源码三处逻辑决定了整条攻击链① 分享文件是 JSONP前端点击分享后POST /share拿到share_id再通过script标签加载/share/id.js—— 也就是说分享内容会以 JavaScript 文件的形式被加载执行。② 用户输入未转义直写进 JSON后端生成.js时把user_message、ai_reply直接拼进 JSON未做 HTML 实体编码或过滤。③alert被重写绑定了验证接口(function() { const originalAlert window.alert; window.alert function(message) { fetch(/xssapi, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message: message, url: window.location.href, timestamp: new Date().toISOString() }) }).then(r r.json()) .then(data { if (data.success) { showMessage(data.message); // -- 这里显示真正的 Flag } }); return originalAlert.apply(this, arguments); }; })();靶场把拿 Flag的条件绑定在了触发alert这个动作上只要页面上有一次弹窗前端就会把弹窗内容发给/xssapi校验通过后把 Flag 显示在页面顶部的蓝色提示框里。 1.3 为什么归类为业务伴生漏洞出题人为了验证XSS 是否被触发额外加了/xssapi这个检测端点。这个端点的初衷是保护业务却成了新的攻击面它不校验请求从哪来、由谁触发、上下文是否真实。 这正是业务伴生漏洞的典型形态为业务/安全功能补的机制自身没经过安全设计。⚔️ 二、逐关复盘 2.1 靶场 1看看我的分享功能怎么啦题目信息项目内容靶场名称看看我的分享功能怎么啦难度3 级类型Agent 安全 / 外部资源加载利用业务伴生漏洞标签Agent安全、分享、CChen.入口http://靶场地址/ai.htmlFlagflag{...}核心考点存储型 XSS、JSONP 注入、验证端点未授权调用信息收集按 1.2 节的思路审计源码后攻击面已经清晰注入点POST /share的user_message、ai_reply执行点分享页加载/share/id.js后的innerHTML渲染出口window.alert→POST /xssapi→ 蓝色提示框显示 Flag。攻击过程思路一完整 XSS 利用链预期解步骤尝试结果1在user_message里塞../../../../tmp/flag.txt想让后端直接读文件❌ 后端只返回随机share_id不拼接文件名但访问生成的.js可见输入被原样写入 JSON2用};alert(document.domain);//闭合 JSONP 字符串与回调❌ 双引号被转义闭合失败且分享内容经innerHTML插入而按 HTML 规范innerHTML写入的script不会执行3改用 HTML 事件属性绕过✅img srcx onerroralert(XSS)成功弹窗第 3 步的具体操作Burp 拦截POST /share把user_message改成img srcx onerroralert(XSS)发送后拿到新的share_id浏览器访问分享链接alert弹出 → 重写的alert捕获调用并请求/xssapi→ 页面顶部蓝色提示框显示 Flag。最终 Payload读取文件版为了名正言顺地读/tmp/flag.txt把弹窗内容换成文件读取结果img srcx onerrorfetch(/tmp/flag.txt).then(rr.text()).then(dalert(d))访问分享链接http://靶场地址/ai.html?#/ai/Dialogue?Url/share/19c44dec1204.js时先弹出一个黑色的404 Not Found弹窗 —— 浏览器读不到服务器本地文件fetch拿到的是 404 页面 HTML紧接着蓝色提示框弹出真正的 Flagflag{...}。⚠️这里必须点破Flag 并不是读文件读出来的。/tmp/flag.txt根本不在 Web 可达路径上第二次弹窗的 Flag 来自/xssapi。XSS 的作用只是制造一次alert弹窗内容是什么并不重要。思路二直接调用/xssapi非预期解既然源码已经把验证接口的地址、方法、请求格式全暴露了那就跳过整个前端流程直接找后端要POST /xssapi HTTP/1.1 Host: 靶场地址 Content-Type: application/json Cookie: user_agreed1 Content-Length: 22 ​ {message:XSS test}后端不校验message是否真来自浏览器弹窗收到请求就返回{ message: flag{...}, success: true } 靶场实例重建时 Flag 会变本例前后两次实例给出的 Flag 并不相同。Flag 与实例一一对应——本文正文统一采用预期解链路XSS 触发的结果。漏洞分析设计意图实际效果分享功能复用 JSONP 加载聊天记录❌ 用户输入未转义等于提供一个可控的 JS 文件前端用innerHTML渲染分享内容❌ 事件型标签img onerror可被解析执行重写alert自动上报检测 XSS⚠️ 检测生效但把是否拿到 Flag简化为是否调用过alert/xssapi返回 Flag 用于验证❌ 无鉴权、无来源校验可直接调用根因整条链缺的是输出编码。后端把用户输入当数据写进.js前端却按代码渲染 —— 中间没有任何一层做 HTML 实体编码于是分享内容变成了可执行脚本。认知关键本关真正的判定条件是有没有触达/xssapi不是有没有读到文件。理解了这一点思路一和思路二其实是同一个洞的两种走法。防御思路输出编码POST /share落盘前对user_message、ai_reply做 HTML 实体编码→lt;JSONP 里的数据只应是纯文本。安全渲染前端改用textContent/innerText不要用innerHTML插入用户内容。验证接口加固/xssapi需鉴权 来源校验且不应返回 FlagFlag 只进日志不进响应体。不要用前端动作当验证条件调过alert就算漏洞已复现这种判据等于给攻击者留了一条直达后门。 2.2 靶场 2看看我的分享功能怎么啦2题目信息项目内容靶场名称看看我的分享功能怎么啦2难度4 级类型Agent 安全 / 外部资源加载利用业务伴生漏洞入口http://靶场地址/Flagflag{...}核心考点前端过滤绕过、事件型 XSS、完整利用链⚠️特别注意靶场描述明确写了请勿使用非预期解提交 WP否则会直接扣除积分。也就是说第一关那种直接调 API 的取巧方法在本关行不通必须走完整的 XSS 利用链。信息收集应用架构与第一关一致多了一处新增的 URL 过滤。在parseHashParams中let url decodeURIComponent(match[1]); // 检查是否以靶场域名开头 if (url.startsWith(http://靶场域名)) { // 检查后缀如果不是 xlsx则进行 xss 过滤 if (!url.toLowerCase().endsWith(.xlsx)) { url url.replace(/[\]/g, ); // -- 过滤了尖括号和引号 } return { url: url }; } else if (url.startsWith(/)) { return { url: url }; // -- 相对路径直接放行不过滤 }分析过滤只覆盖http://靶场域名开头的绝对 URL。而分享页加载的是相对路径/share/id.js命中url.startsWith(/)分支被直接放行 ——和引号一个都没被过滤。事件型 XSS 的入口完好无损。验证机制与第一关相同alert重写后仍然指向/xssapi命中即显示 Flag。攻击过程① 复杂 Payload失败第一反应是让 XSS 自动点击反馈按钮走一遍业务流程{ user_message: img srcx onerror\setTimeout((){document.querySelector(.feedback-btn).click(); setTimeout((){document.querySelector(.feedback-option[data-typehelpful]).click(); document.getElementById(feedbackContent).valuetest; document.getElementById(submitFeedback).click();}, 500);}, 500);\, ai_reply: test }页面无任何反应。原因有二都很朴素img的onerror触发时AI 回复和反馈按钮还没渲染出来分享页根本没有反馈按钮—— 那是主聊天页面的组件。② 最简单的事件型 Payload成功回到 Burp修正上一条请求缺失的逗号构造最小 Payload{ user_message: img srcx onerror\alert(XSS)\, ai_reply: test }发送POST /share拿到新的share_id如0188059e896e浏览器访问http://靶场地址/ai.html?#/ai/Dialogue?Url/share/0188059e896e.js页面加载后弹出XSS提示框重写的alert将内容发往/xssapi页面顶部蓝色提示框显示 Flagflag{...}别把简单 payload 想复杂。本关的难点在想明白过滤漏在哪不在payload 有多花。既然只需触发一次alert多写一行自动点击代码都是给自己找麻烦。漏洞分析设计意图实际效果对http://靶场域名开头的 URL 过滤❌ 遗漏相对路径/share/过滤被整体绕过重写alert检测 XSS 是否触发✅ 检测有效/xssapi返回 Flag⚠️ 仍然让触发一次弹窗等于拿到 Flag攻击链审计分享页 JS发现/xssapi与alert重写逻辑发现 URL 过滤只拦绝对路径相对路径/share/放行构造img srcx onerroralert(XSS)经POST /share注入访问分享链接触发 XSS →alert→ 后端校验拿到 Flag。本质第一关的输出编码缺失没修第二关只在前端入口加了一层字符串替换。过滤写在错误的位置等于没写——只要存在任何一条绕过它抵达innerHTML的路径前面的洞就原样复发。防御思路过滤要写在后端、写在输出处对写入 JSONP 的内容统一做 HTML 实体编码而不是在前端parseHashParams里对某类 URL 做字符串替换。白名单优于黑名单replace(/[\]/g, )是典型黑名单漏一个字符就失效应改为只允许预期字符集。同第一关/xssapi不返回 Flag、需鉴权前端不要用innerHTML渲染不可信内容。别让检测机制成为通关路径任何命中某动作即发放敏感信息的设计都应默认它会被直接调用。 三、两关手法总表手法原理用在哪一关事件型 XSSscript能被过滤、innerHTML不执行脚本但img onerror照样触发靶场 1、2JSONP 注入用户输入被直写进可被script加载的 .js 文件靶场 1、2黑名单绕过过滤只覆盖绝对路径相对路径/share/直接放行靶场 2验证端点直调从源码拿到/xssapi后跳过前端直接重放请求靶场 1非预期解最小 payload 原则通关只需一次alertpayload 越简单越稳靶场 2再往上抽一层只有两句话数据被当代码执行—— 分享内容、聊天记录凡是用户可控 被渲染的地方都是注入点。验证机制自己就是后门—— 为了检测漏洞而加的接口往往没做鉴权和上下文校验。️ 四、防御清单开发者视角层面措施针对的问题输出编码第一优先级所有用户输入在写入 HTML / JSONP 前做 HTML 实体编码存储型 XSS、JSONP 注入安全渲染前端用textContent禁用innerHTML渲染不可信内容事件型 XSS过滤位置过滤/白名单放在后端输出处不要放在前端 URL 解析里黑名单绕过接口鉴权/xssapi之类敏感接口必须鉴权 校验来源与上下文未授权调用最小信息泄露验证接口只记日志响应体不含任何敏感信息含 Flag直接调 API 拿 FlagCSP配置Content-Security-Policy禁止加载不受信任的外部脚本整体缓解 五、总结两关走下来真正的收获不是一个 payload而是这条固定推理先找渲染点—— 用户输入最终被谁、以什么方式渲染这里是innerHTML再看过滤写在哪—— 过滤在前端还是后端覆盖了哪些路径这里是前端 只覆盖绝对路径最后找出口—— 拿到 Flag 的判定条件是什么这里是调用过一次alert一句话总结分享功能把数据当代码存验证接口把后门当闸门用。修复的关键从来不是过滤得更狠而是把数据永远当数据以及别让安全机制自己成为攻击面。
返回列表