【网络安全系列】JavaScript原型链污染攻击总结

文章目录

0 前言


原型链污染,是 NodeJs 中常见的漏洞,在做 antCTF 时也遇到的原型链污染题目,在此记录自己学习原型链污染的过程。

1 原理

0x01 问:原型链有什么作用?

用来做继承,也就是基于原有的代码做一定的修改。下面是一个使用原型链实现继承的案例:

function Parent () {
    
    
    this.name = 'kevin';
}
Parent.prototype.getName = function () {
    
    
    console.log(this.name);
}

function Child () {
    
    

}
Child.prototype = new Parent();

var child1 = new Child();
console.log(child1.getName()) // kevin

当「方法」的 prototype 指定对象原型之后,当试图访问该类的对象属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。下面是一个修改 prototype 的案例:

注意:prototype 是「方法特有的」(需要大概了解,后面需要使用到)

  • 方法:类似 C++ 中的类。除了有属性 __proto__, 还有属性 prototype,prototype 指向该方法的原型对象。propotype 指定其他对象之后,会包含所有原型对象的属性和方法
  • 对象:类似 C++ 中的对象。对象只有属性 __proto__ 指向该对象的构造函数的原型对象。对象有 constructor 里面包含该类的 prototype。
// 让我们从一个函数里创建一个对象o,它自身拥有属性a和b的:
let f = function () {
    
    
   this.a = 1;
   this.b = 2;
}
/* 这么写也一样
function f() {
  this.a = 1;
  this.b = 2;
}
*/
let o = new f(); // {a: 1, b: 2}

// 在f函数的原型上定义属性
f.prototype.b = 3;
f.prototype.c = 4;

// 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
// o.[[Prototype]] 有属性 b 和 c
//  (其实就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 这就是原型链的末尾,即 null,
// 根据定义,null 就是没有 [[Prototype]]。

// 综上,整个原型链如下:

// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null

console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为 1

console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为 2
// 原型上也有一个'b'属性,但是它不会被访问到。
// 这种情况被称为"属性遮蔽 (property shadowing)"

console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看它的原型上有没有
// c是o.[[Prototype]]的属性吗?是的,该属性的值为 4

console.log(o.d); // undefined
// d 是 o 的自身属性吗?不是,那看看它的原型上有没有
// d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
// o.[[Prototype]].[[Prototype]] 为 null,停止搜索
// 找不到 d 属性,返回 undefined

0x02 问:__proto__ 属性有什么作用?

每个「对象」都有 __proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 __proto__ 来访问。

简而言之:用 prototype 无法直接访问,需要使用 __proto__ 访问。prototype 是一个指针属性。

这里有个需要区分的概念:

  • __proto__:指向原型对象的构造器。
  • constructor:指向当前对象的构造器。


(图片说明:右下角是图片的说明,左图的__proto__的箭头指向原型对象的构造器)


(图片说明:右下角是图片的说明,左图的constructor的箭头指向原型对象的构造器)


0x03 问:原型链污染的概念是什么?

在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。

基本原理:引用类型的属性被所有实例共享。案例:

function Parent () {
    
    
    this.names = ['kevin', 'daisy'];
}
function Child () {
    
    

}
Child.prototype = new Parent();

var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"]

按照 Java 中正常的继承,child2.names 应该和原对象一样,数组中只有 2 个数据。

2 利用

问:怎么判断是否有原型链污染?

  • 字符串可以被解析为方法或对象。例如:Json.parse 进行解析、shvl 库使用点对属性操作。
  • 对象的键和值都可控。target[key] = value,其中 key 和 value 均可控制。

下面是一个原型链污染的简单案例:

function merge(target, source) {
    
    
    console.log('merge', target, source);
    // 遍历 source 中的 key。
    for (let key in source) {
    
    
        if (key in source && key in target) {
    
    
            merge(target[key], source[key])
        } else {
    
    
            target[key] = source[key]
        }
    }
}

let o1 = {
    
    }         // {} 是一个对象,存在 __proto__ 的 key。
console.log(typeof(o1));
console.log(o1);    // object
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
console.log(typeof(o2));
console.log(o2);    // object

merge(o1, o2)
console.log('o1 proto:', o1.__proto__);
console.log('o2 proto:', o2.__proto__);
console.log('{} proto:', {
    
    }.__proto__);
console.log(o1, o2);    // 对象 {} 的原型对象变为 { a: 1 }。

o3 = {
    
    }
console.log(o3)

实战1:在 antCTF 中,使用 shvl 库对键值进行操作。

email: async function (req, res) {
    
    
    let contents = {
    
    };

    Object.keys(req.body).forEach((key) => {
    
    
      shvl.set(contents, key, req.body[key]);
    });
    // 遍历请求参数中所有的 key
    // 将键和值赋值为 contents(shvl 库的 set 函数)

    contents.from = '"admin" <[email protected]>';

    try {
    
    
      await send(contents);
      return res.json({
    
    
        message: "Success."
      });
    } catch (err) {
    
    
      return res.status(500).json({
    
    
        message: err.message
      });
    }
  }

问:常见的原型链污染方法有哪些?

案例:

function person(fullName) {
    
    
    this.fullName = fullName;
}
var person1 = new person("Satoshi");
// function:prototype, __prototype
person.prototype.sayHello = 1
console.log(person1.__proto__);
person.prototype.newConstant = 2
console.log(person1.__proto__); 

// object: __prototype__, constructor
person1.__proto__.sayHi= 3
console.log(person1.__proto__);
person1.constructor.prototype.oldConstant = 4
console.log(person1.__proto__);
/*
person { sayHello: 1 }
person { sayHello: 1, newConstant: 2 }
person { sayHello: 1, newConstant: 2, sayHi: 3 }
person { sayHello: 1, newConstant: 2, sayHi: 3, oldConstant: 4 }
*/
 

推荐一个非常 nice 的网站:https://book.hacktricks.xyz/ 。收集 hack 中的 tricks


问:原型链污染链怎么挖掘?

  1. 寻找 JavaScript 中的危险关键字(危险函数)。如:
    1. 模块:child_process
    2. 函数:eval, spwn, exec, setTimeout, setInteval, Function
  2. 寻找调用关系和可控的参数,并且确定如何进行传参。

    (图片说明:先确认危险函数和调用方式,也就是头部和尾部,再去寻找中间过程)
  3. 充分利用危险函数和能控制的参数。(读取文件或者反弹 shell)
  • 目标机器环境如果有 bash,可以反弹 shell。
  • 目标机器环境如果有 Python 等,可以反弹 shell。
  • 目标机器环境如果只有 sh,将 readflag 执行写入到其他地方,再利用其他方式读取。

(拓展阅读:反弹Shell表

问:原型链污染怎么防御?怎么绕过防御呢?

  • 如果系统中有键值的操作,并且键和值来自外部输入。可以考虑进行过滤:
    • 禁止操作 constructor
    • 禁止操作 prototype
    • 禁止操作 __proto__

绕过防御:

  • 思考并测试是否过滤完全,具体参考 antCTF 的 8-bit-pub 中的 shvl 库绕过。

参考教程:

  1. JavaScript深入之继承的多种方式和优缺点-Github
  2. JS中的prototype、__proto__与constructor-Huawei
  3. 继承与原型链 - JavaScript | MDN
  4. 深入理解 JavaScript Prototype 污染攻击-phithon
  5. NodeJS - proto & prototype Pollution-HackTricks
  6. AntCTF2021部分WP-HapHp1

猜你喜欢

转载自blog.csdn.net/qq_43085611/article/details/114745665