随笔 - 40  文章 - 0  评论 - 0  阅读 - 11110

XSS高阶案例--循环

一,一个循环

先看源码:首先是截取#后面的值,然后创建了一个div,再把#后面的值赋值给了div。

再在for循环中的querySelectorAll就相当于把div下的所有子元素全部选中,然后获取到了子元素的属性,

然后把这些属性全部删除。

 

 

因为有innerHTML,所以<script>alert(1)</script>传不进去

只能用属性值传<img src=1 onerror=alert(1)>

 

 

 

但它把src这个属性删了,留下了onerror这个属性

我们断点调试看一下

开始的el还没有值,我们往下走

 

 

 

现在把img的值传了进来

 

 

 

再往下,它通过attr.name就把src属性删除掉了

 

 

 

 

再往下走,就结束了,说明onerror属性就没有删除

 

 

 

这也说明了,这个代码是边循环边删除的(这是非常危险的)

既然它有删除有留下的,所以我们试试加其他属性看看

我在原先的每个属性前都加了一个属性,直接就成功了

 

 

 

 

 

 

 

二,两个循环

这里是加了一个数组,将选中的属性放进数组里,再在数组里面进行循环

这也是上一个循环的解决方案

 

 

 

显然用上一个的方法是不行了,所以我们考虑,就让它不要进到循环或者进循环删除无用的数据

 

 我们先考虑进循环删除无用数据

假如有一个元素可以劫持el.attributes,那么就有可能删除的就不是el.attributes,就只是删除它里面的一个子元素

假如我们这样做,让它进去删除img,而让form触发

 

 

 

这里说el.attributes不是一个可迭代对象,其实我们已经进去了,但代码里是for循环,for循环一定要是可迭代对象,

因为我们只进去了一个img,那么就不可能是可迭代对象。

 

 

 

所以我们要把进去的组成一个集合或一个数组

 

 

 

 

 

 

input也是可以的,因为input里面有onfocus,所以我们用tabindex将元素聚焦

 

 

 

 

 

 

 

我们尝试一下,还是可行的,但是我们是无限聚焦在input上的,我们点确定一直都在那,

<form tabindex=1 onfocus="alert(1)" autofocus="ture"><input name=attributes><input name=attributes></form>

 

 

 

 

所以我们想让聚焦执行完把它删除掉,然后就成功了

<form tabindex=1 onfocus="alert(1);this.removeAttribute('onfocus');"autofocus="ture"><input name=attributes><input name=attributes></form>

 

 

 

不进循环

先写答案

 

 

 

它可以在过滤代码执行以前,提前执行恶意代码

 

 

DOM树的构建

 

我们知道JS是通过DOM接口来操作文档的,而HTML文档也是用DOM树来表示。所以在浏览器的渲染过程中,我们最关注的就是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的构造。JS环境里对DOM操作又会导致回流,为DOM树构造造成额外影响

 

 

img失败原因

 

先来找一下失败案例的原因,看看是在哪里触发了img payload中的事件代码。将过滤的代码注释以后,注入payload并打断点调试一下。

 

 

可以发现即使代码已经执行到最后一步,但在没有退出JS环境以前依然还没有弹窗。

 

 

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

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

同时,上文已经提到了,页面的JS执行是会阻塞DOM树构建的。所以总的来说,在script标签内的JS执行完毕以后,DOM树才会构建完成,接着才会加载图片,然后发现加载内容出错才会触发error事件

可以在页面上添加以下代码来测试这一点。

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

 

测试结果

 

 

那么失败的原因也很明显了,由于js阻塞dom树,一直到js语句执行结束后,才可以引入img,此时img的属性已经被sanitizer清除了,自然也不可能执行事件代码了。

svg成功原因

 

我们可以通过断点调试来看一下

点击确定以后,直接弹出了窗口。而且,这个地方如果只有一个<svg onload=alert(1)>,那么结果将同img一样,直到script标签结束以后才能执行相关的代码,这样的代码放到挑战里也将失败(测试单个svg时要注意,不能像img一样注释掉appendChild那一行)。那为什么多了一个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)的代码。

实验

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

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

同时,我们将注入代码也再套嵌一层

<svg onload=console.log("svg0")>

<svg onload=console.log("svg1")>

<svg onload=console.log("svg2")>

 

 

 

可以看到结果不出所料,最内层的svg先触发,然后再到下一层,而且是在DOM树构建完成以前就触发了相关事件;最外层的svg则得等到DOM树构建完成才能触发。

 

 

小结

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

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

 

posted on   {轩}  阅读(81)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示