目录
方法二:(%20open%20οntοggle=alert(1)>
有时可行,有时不行。有延迟的话 肯定执行成功,因为此时异步事件已经执行完成,执行点在innerhtml。如果没有延迟,有可能在js删除属性之后,异步事件才执行完成。
- html>
- <html lang="en">
-
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Documenttitle>
- head>
- <body>
- body>
- <script>
- const data = decodeURIComponent(location.hash.substr(1));
- const root = document.createElement('div');
- root.innerHTML = data;
-
- // 这里模拟了XSS过滤的过程,方法是移除所有属性
- for (let el of root.querySelectorAll('*')) {
- for (let attr of el.attributes) {
- el.removeAttribute(attr.name);
- }
- }
- document.body.appendChild(root);
- script>
- html>
基础原理:
- list = [1, 2, 3, 4, 5, 6]
- for i in list:
- if i == 2:
- list.remove(i)
- print(i)
- print(list)
与这个结果类似
- html>
- <html lang="en">
-
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Documenttitle>
- head>
-
- <body>
- <script>
-
- const data = decodeURIComponent(location.hash.substr(1));
- const root = document.createElement('div');
- root.innerHTML = data;
- // let details = root.querySelector("details")
- // root.removeChild(details)
- // 这里模拟了XSS过滤的过程,方法是移除所有属性
- for (let el of root.querySelectorAll('*')) {
- let attrs = [];
- for (let attr of el.attributes) {
- attrs.push(attr.name);
- }
- for (let name of attrs) {
- el.removeAttribute(name);
- }
- }
-
- document.body.appendChild(root);
-
- script>
-
- html>
<svg>
原理:
看起来平平无奇,但是它可以在过滤代码执行以前,提前执行恶意代码。
为更好地理解这个问题,需要稍微了解一下浏览器的渲染过程。
HTML文档也是用DOM树来表示。所以在浏览器的渲染过程中,我们最关注的就是DOM树是如何构建的。
解析一份文档时,先由标记生成器做词法分析,将读入的字符转化为不同类型的Token,然后将Token传递给树构造器处理;接着标识识别器继续接收字符转换为Token,如此循环。实际上对于很多其他语言,词法分析全部完成后才会进行语法分析(树构造器完成的内容),但由于HTML的特殊性,树构造器工作的时候有可能会修改文档的内容,因此这个过程需要循环处理。
在树构建过程中,遇到不同的Token有不同的处理方式。具体的判断是在HTMLTreeBuilder::ProcessToken(AtomicHTMLToken* token)
中进行的。AtomicHTMLToken
是代表Token的数据结构,包含了确定Token类型的字段,确定Token名字的字段等等。Token类型共有7种,kStartTag
代表开标签,kEndTag
代表闭标签,kCharacter
代表标签内的文本。所以一个会被解析成3个不同种类的Token,分别是
kStartTag
、kCharacter
和kEndTag
。在处理Token的过程中。
在处理Token的时候,还会用到 1 1HTMLElementStack
,一个栈的结构。当解析器遇到开标签时,会创建相应元素并附加到其父节点,然后将token和元素构成的Item压入该栈。遇到一个闭标签的时候,就会一直弹出栈直到遇到对应元素构成的item为止,这也是一个处理文档异常的办法。比如
会被浏览器正确识别成<div>
而当处理script的闭标签时,除了弹出相应item,还会暂停当前的DOM树构建,进入JS的执行环境。换句话说,在文档中的script标签会阻塞DOM的构造。JS环境里对DOM操作又会导致回流,为DOM树构造造成额外影响。
总的来说,在script标签内的JS执行完毕以后,DOM树才会构建完成,接着才会加载图片,然后发现加载内容出错才会触发error
事件。
继续用断点调试svg payload为何成功。
在root.innerHTML = data;断下来后,点击单步调试。
神奇的事情发生了,直接弹出了窗口,点击确定以后,调试器才会走到下一行代码。
上文提到了一个叫HTMLElementStack
的结构用来帮助构建DOM树,它有多个出栈函数。其中,除了PopAll
以外,大部分出栈函数最终会调用到PopCommon
函数。这两个函数代码如下:
- void HTMLElementStack::PopAll() {
- root_node_ = nullptr;
- head_element_ = nullptr;
- body_element_ = nullptr;
- stack_depth_ = 0;
- while (top_) {
- Node& node = *TopNode();
- auto* element = DynamicTo
(node); - if (element) {
- element->FinishParsingChildren();
- if (auto* select = DynamicTo
(node)) - select->SetBlocksFormSubmission(true);
- }
- top_ = top_->ReleaseNext();
- }
- }
-
- void HTMLElementStack::PopCommon() {
- DCHECK(!TopStackItem()->HasTagName(html_names::kHTMLTag));
- DCHECK(!TopStackItem()->HasTagName(html_names::kHeadTag) || !head_element_);
- DCHECK(!TopStackItem()->HasTagName(html_names::kBodyTag) || !body_element_);
- Top()->FinishParsingChildren();
- top_ = top_->ReleaseNext();
-
- stack_depth_--;
- }
当我们没有正确闭合标签的时候,如
,就可能调用到PopAll
来清理;而正确闭合的标签就可能调用到其他出栈函数并调用到PopCommon
。这两个函数有一个共同点,都会调用栈中元素的FinishParsingChildren
函数。这个函数用于处理子节点解析完毕以后的工作。因此,我们可以查看svg标签对应的元素类的这个函数。
- void SVGSVGElement::FinishParsingChildren() {
- SVGGraphicsElement::FinishParsingChildren();
-
- // The outermost SVGSVGElement SVGLoad event is fired through
- // LocalDOMWindow::dispatchWindowLoadEvent.
- if (IsOutermostSVGSVGElement())
- return;
-
- // finishParsingChildren() is called when the close tag is reached for an
- // element (e.g. ) we send SVGLoad events here if we can, otherwise
- // they'll be sent when any required loads finish
- SendSVGLoadEventIfPossible();
- }
这里有一个非常明显的判断IsOutermostSVGSVGElement
,如果是最外层的svg则直接返回。注释也告诉我们了,最外层svg的load
事件由LocalDOMWindow::dispatchWindowLoadEvent
触发;而其他svg的load
事件则在达到结束标记的时候触发。所以我们跟进SendSVGLoadEventIfPossible
进一步查看。
- bool SVGElement::SendSVGLoadEventIfPossible() {
- if (!HaveLoadedRequiredResources())
- return false;
- if ((IsStructurallyExternal() || IsA
(*this)) && - HasLoadListener(this))
- DispatchEvent(*Event::Create(event_type_names::kLoad));
- return true;
- }
- 先决条件 在于svg不能最外层 onload 必须保证不是最外层
这个函数是继承自父类SVGElement
的,可以看到代码中的DispatchEvent(*Event::Create(event_type_names::kLoad));
确实触发了load事件,而前面的判断只要满足是svg元素以及对load
事件编写了相关代码即可,也就是说在这里执行了我们写的onload=alert(1)
的代码。
小结:
套嵌的svg之所以成功,是因为当页面为root.innerHtml
赋值的时候浏览器进入DOM树构建过程;在这个过程中会触发非最外层svg标签的load
事件,最终成功执行代码。所以,sanitizer执行的时间点在这之后,无法影响我们的payload。
当然这种方法也可以在上个只有一个循环的xss中执行:
details 异步执行是将相应执行函数放进一个事件队列中,只要事件不停止,在你放入事件之后,你将details标签删除,已经没用了,因为事件会接着执行
此时我们需要将上面原文中的两行注释打开:
- html>
- <html lang="en">
-
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Documenttitle>
- head>
-
- <body>
- <script>
-
- const data = decodeURIComponent(location.hash.substr(1));
- const root = document.createElement('div');
- root.innerHTML = data;
- let details = root.querySelector("details")
- root.removeChild(details)
- // 这里模拟了XSS过滤的过程,方法是移除所有属性
- for (let el of root.querySelectorAll('*')) {
- let attrs = [];
- for (let attr of el.attributes) {
- attrs.push(attr.name);
- }
- for (let name of attrs) {
- el.removeAttribute(name);
- }
- }
-
- document.body.appendChild(root);
-
- script>
-
- html>
执行:
首先触发代码的点是在DispatchPendingEvent
函数里
- void HTMLDetailsElement::DispatchPendingEvent(
- const AttributeModificationReason reason) {
- if (reason == AttributeModificationReason::kByParser)
- GetDocument().SetToggleDuringParsing(true);
- DispatchEvent(*Event::Create(event_type_names::kToggle));
- if (reason == AttributeModificationReason::kByParser)
- GetDocument().SetToggleDuringParsing(false);
- }
而这个函数是在ParseAttribute
被调用的
- void HTMLDetailsElement::ParseAttribute(
- const AttributeModificationParams& params) {
- if (params.name == html_names::kOpenAttr) {
- bool old_value = is_open_;
- is_open_ = !params.new_value.IsNull();
- if (is_open_ == old_value)
- return;
-
- // Dispatch toggle event asynchronously.
- pending_event_ = PostCancellableTask(
- *GetDocument().GetTaskRunner(TaskType::kDOMManipulation), FROM_HERE,
- WTF::Bind(&HTMLDetailsElement::DispatchPendingEvent,
- WrapPersistent(this), params.reason));
-
- ....
-
- return;
- }
- HTMLElement::ParseAttribute(params);
- }
ParseAttribute
正是在解析文档处理标签属性的时候被调用的。注释也写到了,分发toggle事件的操作是异步的。可以看到下面的代码是通过PostCancellableTask
来进行回调触发的,并且传递了一个TaskRunner
。
- TaskHandle PostCancellableTask(base::SequencedTaskRunner& task_runner,
- const base::Location& location,
- base::OnceClosure task) {
- DCHECK(task_runner.RunsTasksInCurrentSequence());
- scoped_refptr
runner = - base::AdoptRef(new TaskHandle::Runner(std::move(task)));
- task_runner.PostTask(location,
- WTF::Bind(&TaskHandle::Runner::Run, runner->AsWeakPtr(),
- TaskHandle(runner)));
- return TaskHandle(runner);
- }
跟进PostCancellableTask
的代码则会发现,回调函数(被封装成task)正是通过传递的TaskRunner
去派遣执行。
清楚调用流程以后,就可以思考,为什么无法触发这个事件呢?最大的可能性,就是在任务交给TaskRunner
以后又被取消了。因为是异步调用,而且PostCancellableTask
这个函数名也暗示了这一点。
details标签的toggle事件是异步触发的,并且直接对details标签的移除不会清除原先通过属性设置的异步任务。