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

在html中一个for循环

先展示代码





    
    
    
    Document





这是一段完整的代码,其中主要的运行作用是就是xss过滤,当然只有这么一个功能在网页上打开是没有东西的;
运行的过程呢就是将url #号后边的所有都提取出来放入一个建立的

标签里,对输入的字段进行遍历、循环、删除;(标签是无法被删除的,所以只能删除标签后的字段或者函数),
我们要做的是绕过这个xss过滤,两种方法: 一、进入循环,摸清循环的规则,钻空子; 二、不进入循环

第一种方法

我们先看下进入循环,先输入去看下运行结果
复现一个循环问题以及两个循环问题_第1张图片
这里明显是少了src证明它确实是进行了删除,但是并没有删除完,如果按我们正常的流程走它其实只会剩下img这个标签但是并不是;
这其实是一个很有意思的案例,我们可以把它简单的举例出来:

a = [2, 3, 6, 9, 7, 1]

for i in a:
    print(max(a), end=',')
    a.remove(max(a))

我们设一个a,给a定义一个数组,然后在a中进行for循环,挑选其中最大的数进行删除,但是,我们看下结果:
在这里插入图片描述
它只输出了三个值就结束了循环,我们了可以想想其中的过程,第一次循环的时候数值按顺序往下走,找到最大的值然后挑出来删除掉,这时候数组中就剩下了2,3,6,7,1;第二次循环一样,剩下2,3,6,1;第三次循环剩下2,3,1;注意这时候我们已经循环了三次了,而我们的数组中只剩下了三个值,没有办法进行第四次循环,这时候循环就结束了;(可能我表达的不是很清楚,各位可以自己在python中进行断片运行下,一步一步走能看出来);
同样的道理,在上边的html中也是这样的道理,边运行边循环,删掉了第一个之后,下一个字段就顶替了第一个字段,循环就结束了,所以这样的话我们的输入就只有src被删除掉了,onerror留了下来,但是这样虽然有,但是实际意义也并不是很大,那我们就要利用它的边运行边删除的特性来将我们这整段话插入进去;
那我们就往img继续添加参数,看看会怎样->我们往里边插入了两个xxx=aaaaa bbbbb=cccccc
然后回车看看会有什么变化

在这里插入图片描述
发现留下了xxx 和 onerror 那我们就调换下位置
复现一个循环问题以及两个循环问题_第2张图片
这样就绕过了,我们看运行后的代码就能看见它其实就是把第一个和第三个元素删除掉了,
在这里插入图片描述
跟上边那个选最大的并删除掉其实是一样的,这样的话就是进入循环删除无用信息后绕过循环

两个循环绕过

先看代码


第一种方法

内容大致一样,不同的点就是将#后的提出来之后放入了一个数组中,然后在数组中进行循环删除这个可以算是上一个问题的解决方案
那么我们可以先插入一段看下结果
在这里插入图片描述
结果显而易见 全删了,那么就先开始第一种方式-------进循环
进入循环的话现在我们就找一个能帮我们的,例如:form以及form下的img
就像是这样:

    

因为我们的代码会找

下的元素加载到数组中,那我们的这个
下的form就相当于代码中的el,而img就相当于其中的el.attributes这样代码循环就会将我们插入的img下的内容删掉,并且我们的form也能触发的话,可以说是两全其美;
那我们就浅尝一下,给img随便一个值。输入

在这里插入图片描述
内容依然是被删除了,前提我们要做数组,而input是可以组成数组的,有一个触发的方法有一个是onfocus,
但是他却不是form表单下,而是在input表单下而且是要把焦点放在input事件下,刚好有个全局属性叫tabindex;是可以用来聚焦的,
我们可以借用tabindex将焦点放到input中,然后用onfocus来触发form可能有戏
输入

复现一个循环问题以及两个循环问题_第3张图片
这样就绕过成功了,但是因为一直用的tabindex将聚焦在input上所以这个弹窗会一直弹出来那么我们在加上删除的语句

将触发元素删除只触发一次这样就是进入循环绕过了

第二种方法

不进循环,那我们就要在插入的语句在没有进入循环的时候提前运行


复现一个循环问题以及两个循环问题_第4张图片
复现一个循环问题以及两个循环问题_第5张图片
至于说为什么要加两个

在DOM树构建过程中,遇到不同的Token有不同的处理方式。具体的判断是在HTMLTreeBuilder::ProcessToken(AtomicHTMLToken* token)中进行的。AtomicHTMLToken是代表Token的数据结构,包含了确定Token类型的字段,确定Token名字的字段等等。Token类型共有7种,kStartTag代表开标签,kEndTag代表闭标签,kCharacter代表标签内的文本。所以一个会被解析成3个不同种类的Token,分别是kStartTagkCharacterkEndTag。在处理Token的过程中,还有一个InsertionMode的概念,用于判断和辅助处理一些异常情况。

在处理Token的时候,还会用到HTMLElementStack,一个栈的结构。当解析器遇到开标签时,会创建相应元素并附加到其父节点,然后将token和元素构成的Item压入该栈。遇到一个闭标签的时候,就会一直弹出栈直到遇到对应元素构成的item为止,这也是一个处理文档异常的办法。比如

1

会被浏览器正确识别成

1

正是借助了栈的能力。

而当处理script的闭标签时,除了弹出相应item,还会暂停当前的DOM树构建,进入JS的执行环境。换句话说,在文档中的script标签会阻塞DOM的构造。JS环境里对DOM操作又会导致回流,为DOM树构造造成额外影响

那么img失败的原因

打断点调试一下
复现一个循环问题以及两个循环问题_第6张图片
可以发现即使代码已经执行到最后一步,但在没有退出JS环境以前依然还没有弹窗。
复现一个循环问题以及两个循环问题_第7张图片

此时再点击单步调试就会来到我们的代码的执行环境了。此外,这里还有一个细节就是appendChild被注释并不影响代码的执行,证明即使img元素没有被添加到DOM树也不影响相关资源的加载和事件的触发。

那么很明显,alert(1)是在页面上script标签中的代码全部执行完毕以后才被调用的。这里涉及到浏览器渲染的另外一部分内容:在DOM树构建完成以后,就会触发DOMContentLoaded事件,接着加载脚本、图片等外部文件,全部加载完成之后触发load事件 。
继续用断点调试svg payload为何成功。

root.innerHtml = data断下来后,点击单步调试。
复现一个循环问题以及两个循环问题_第8张图片

神奇的事情发生了,直接弹出了窗口,点击确定以后,调试器才会走到下一行代码。而且,这个地方如果只有一个,那么结果将同img一样,直到script标签结束以后才能执行相关的代码,这样的代码放到挑战里也将失败(测试单个svg时要注意,不能像img一样注释掉appendChild那一行)。

触发流程

上文提到了一个叫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)的代码。

实验

我们可以将过滤的代码注释,并添加相关代码来验证这个事件的触发时间。

  window.addEventListener("DOMContentLoaded", (event) => {
    console.log('DOMContentLoaded')
  });
  window.addEventListener("load", (event) => {
    console.log('load')
  });

同时,我们将注入代码也再套嵌一层
复现一个循环问题以及两个循环问题_第9张图片
可以看到结果不出所料,最内层的svg先触发,然后再到下一层,而且是在DOM树构建完成以前就触发了相关事件;最外层的svg则得等到DOM树构建完成才能触发。

小结

img和其他payload的失败原因在于sanitizer执行的时间早于事件代码的执行时间,sanitizer将恶意代码清除了。

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

你可能感兴趣的:(前端,网络安全)