复现一个循环问题以及两个循环问题

一个循环问题

下面是一次循环的代码,首先截取#号后面的值,然后创建一个div,然后将#号后面的值都赋值给div,然后使用querySelectorAll选取div下所有的子元素;然后获取子元素的属性,并将属性全部删除。

<!DOCTYPE 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>Document</title>
</head>
<body>
    
</body>
<script>
    // console.info(x.attributes);
    // http://127.0.0.1/domfilter/demo6.html#<img src=1 οnerrοr=alert(1)>
    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>

但是在我们输入<img src=1 οnerrοr=alert(1)>后可以发现,他只是将src删除了,onerror还保留着。
在这里插入图片描述
上面的代码为什么只删除了一个元素我们可以通过一段类似上面的简单的代码分析。
在这里插入图片描述
上面代码只删除了3个数这是为什么我们可以通过断点调试来分析。
当i在3时也就是i在第一位时,选出了最大数7并删除了,此时a=[3,5,4,6,2] 4,6,2一次向前一位来到了第三位,第四位和第五位。
在这里插入图片描述
当i在5时与就是i在第二位时,选出来最大数6并删除,此时a=[3,5,4,2]4和2个向前进一位。
在这里插入图片描述
当i在4时也就是i在第三位时,选出最大数5并删除,此时a=[3,4,2]。
在这里插入图片描述
当我们再往后调试时已经结束了,原因是此时a=[3,4,2]而i已经到达了最后一位第三位也就是2所在的位置,并且i在第三位时的循环已经结束所以整个循环就此结束。
在这里插入图片描述
这就是上面代码中只是删除了src而保留了onerror的原因,知晓上述代码删除的原理后就很容易触发弹窗,我们只要写入足够多的属性,使得删除后能够保留src和onerror属性即可。如<img x=1 src=1 title=aaa onerror=alert(1)>
在这里插入图片描述
在这里插入图片描述
其他答案:<svg/a/οnlοad=alert(1)>

两次循环

两次循环是将要循环的属性放进了数组里面,在执行循环、删除的操作。这样就避免了一次循环中出现的问题。

<!DOCTYPE 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>Document</title>
</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>

在两次循环中实现弹窗有两种方法(1)不进入到循环中 (2)进入循环但不删除有用数据

进入循环但不删除有用数据

如果有一个元素可以拦截el.attributes,有可能删除的不是el而是里面的子元素。就好像是div标签下的form和img,el相当与div下的form而el.attributes相当于form下的img,这样我们可以让img进入循环将img的属性全部删掉而form可以触发弹窗。要想让进入循环的属性生效就需要组成一个可迭代对象,即放入id或name相同的两个标签组成一个可迭代对象(数组)。img进入循环的前提是form要有一个触发方法,这里就需要用onfocus,但是onfocus不是form表单里面的属性而是input的属性,所以我们需要将焦点对到input属性中,这就要用到tabindex (tabindex 全局属性 指示其元素是否可以聚焦,以及它是否/在何处参与顺序键盘导航)来聚焦到input的子元素上。这样的话我们可以尝试以下;

<form tabindex=1 onfoucus="alert(1)" autofocus="true"><input name=attributes><input name=attributes></from>

在这里插入图片描述
这种会一直弹窗,所以我们需要在它执行成功一次之后将他移除。

<form tabindex=1 onfoucus="alert(1);this.removeAttribute(‘onfocus’);" autofocus="true"><input name=attributes><input name=attributes></from>

不进入循环

这里可以通过<svg><svg/onload=alert(1)>的方式进行绕过。它可以在过滤代码执行以前,提前执行恶意代码。也就是说,它在代码的root.innerHTML = data;处就已经执行了。要理解这个问题的话我们需要了解一点浏览器的渲染过程。
DOM树的构建
解析一份文档时,先由标记生成器做词法分析,将读入的字符转化为不同类型的Token,然后将Token传递给树构造器处理;接着标识识别器继续接收字符转换为Token,如此循环。实际上对于很多其他语言,词法分析全部完成后才会进行语法分析(树构造器完成的内容),但由于HTML的特殊性,树构造器工作的时候有可能会修改文档的内容,因此这个过程需要循环处理。在这里插入图片描述
在树构建过程中,遇到不同的Token有不同的处理方式。具体的判断是在HTMLTreeBuilder::ProcessToken(AtomicHTMLToken* token)中进行的。AtomicHTMLToken是代表Token的数据结构,包含了确定Token类型的字段,确定Token名字的字段等等。Token类型共有7种,kStartTag代表开标签,kEndTag代表闭标签,kCharacter代表标签内的文本。所以一个<script>alert(1)</script>会被解析成3个不同种类的Token,分别是kStartTagkCharacterkEndTag。在处理Token的过程中,还有一个InsertionMode的概念,用于判断和辅助处理一些异常情况。

在处理Token的时候,还会用到HTMLElementStack,一个栈的结构。当解析器遇到开标签时,会创建相应元素并附加到其父节点,然后将token和元素构成的Item压入该栈。遇到一个闭标签的时候,就会一直弹出栈直到遇到对应元素构成的item为止,这也是一个处理文档异常的办法。比如<div><p>1</div>会被浏览器正确识别成<div><p>1</p></div>正是借助了栈的能力。

而当处理script的闭标签时,除了弹出相应item,还会暂停当前的DOM树构建,进入JS的执行环境。换句话说,在文档中的script标签会阻塞DOM的构造。在DOM树构建完成以后,就会触发事件,接着加载脚本、图片等外部文件,全部加载完成之后触发load事件。同时,上文已经提到了,页面的JS执行是会阻塞DOM树构建的。所以总的来说,在script标签内的JS执行完毕以后,DOM树才会构建完成,接着才会加载图片,然后发现加载内容出错才会触发error事件
这也是我们使用<img src=x onerror=alert(1)>不会弹窗的原因。由于js阻塞dom树,一直到js语句执行结束后,才可以引入img,此时img的属性已经被sanitizer清除了,自然也不可能执行事件代码了。
但为什么我们使用svg会成功呢我们可以通过断点调试来看看。
在这里插入图片描述
在这里插入图片描述

神奇的事情发生了,直接弹出了窗口,点击确定以后,调试器才会走到下一行代码。
我们来看看他的触发流程
上文提到了一个叫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<Element>(node);
    if (element) {
      element->FinishParsingChildren();
      if (auto* select = DynamicTo<HTMLSelectElement>(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_--;
}

当我们没有正确闭合标签的时候,如<svg><svg>,就可能调用到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. </svg>) 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<SVGSVGElement>(*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)的代码。
小结:
img和其他payload的失败原因在于sanitizer执行的时间早于事件代码的执行时间,sanitizer将恶意代码清除了。

套嵌的svg之所以成功,是因为当页面为root.innerHtml赋值的时候浏览器进入DOM树构建过程;在这个过程中会触发非最外层svg标签的load事件,最终成功执行代码。所以,sanitizer执行的时间点在这之后,无法影响我们的payload。


版权声明:本文为m0_52438027原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。