nodejs原型链污染


补充

constructor.prototype可以代替**proto**

前置知识

Nodejs和JavaScipt和JSON的区别

先来了解一下Nodejs和JavaScipt和JSON的区别吧,一直也是懵懵懂懂

如果没有node,那么我们的JavaScript代码则由浏览器中的JavaScript解析器进行解析。几乎所有的浏览器都配备了JavaScript的解析功能(最出名的就是google的v8), 这也是为什么我们能在f12中直接执行JavaScript的原因

而Nodejs则是由这个解析器单独从浏览器中拿出来,并进行了一系列的处理,最后成为了一个可以在服务端运行JavaScript的环境

JSON是JavaScript的对象表示方法,它表示的是声明对象的一种格式, 由于我们从前端接收到的数据基本都是字符串,因此在服务端如果要将这些字符串处理为其他格式,比如对象,就需要用到JSON了。

接着便是引入我们的nodejs原型链污染的知识了

原型对象(prototype)与原型连接点(proto)与原型链

在c++或java这些面向对象的语言中,我们如果想要一个对象,首先需要使用关键字class声明一个类,再使用关键字new一个对象出来(经常书写的php也是这样),但是在JavaScript中没有class以及类这种概念(为了简化编写JavaScript代码,ECMAScript 6后增加了class语法,但class其实只是一个语法糖)。 在JavaScript有这么两种声明对象的方式

直接实例化构造方法Object()来创建对象

person=new Object()
person.firstname="John";
person.lastname="Doe";
person.age=50;
person.eyecolor="blue";

这种创建对象的方法还有另一种写法 如下
person={firstname:"John",lastname:"Doe",age:50,eyecolor:"blue"};
这种方法通过直接实例化构造方法Object()来创建对象

先创建构造函数 再实例化构造函数

function person(firstname,lastname,age,eyecolor)  这里创建了一个“类” 但是在JavaScript中叫做构造函数或者构造器
{
    this.firstname=firstname;
    this.lastname=lastname;
    this.age=age;
    this.eyecolor=eyecolor;
}
var myFather=new person("John","Doe",50,"blue");    通过这个“类”实例化对象
var myMother=new person("Sally","Rally",48,"green");

这种方法先创建构造函数 再实例化构造函数 构造函数function也属于Object 
如果对这里为什么属于Object而不属于Function有疑问请继续阅读 下面会解释
污染前置知识

既然是通过实例化Object来创建对象或创建构造函数,在JavaScript中有两个很特殊的对象,Function() 和 Object() ,它们两个既是构造函数也是对象,作为对象是不是应该有一个“类”去作为他们的模板呢?

对于Object()来说,要声明这么一个构造函数我们可以使用关键字function来创建 。(在底层 使用function创建一个函数 其实就相当于这个过程)

function Object()
{
    
}
在底层为
var Object = new Function();

先引入一些概念方便理解以下的讲解

proto是任何一个对象拥有的属性

prototype是任何一个函数拥有的一个属性

那么对于Function自己这个对象,他是怎么来的呢?如果用Function.protoFunction.prototype进行比较,发现二者是全等的,所以Function创造了自己,也创造了Object,所以JavaScript中,所有函数都是对象,而对象是通过函数创建的。因此

构造函数.prototype.__proto__应该是Object.prototype,而不是Function.prototype,Function的作用是创建而不是继承。

eg;

person=….

这个person对象就拥有了person.proto这个属性,而Object()我们刚才提到了是由Function创建来的一个构造函数,那么Object就天生有了Object.prototype。

为什么要说这些呢就是为了说明白

1.某一对象的__proto__指向它的prototype(原型对象), 也就是说如果

直接访问person.proto 那么就相当于访问了Object.prototype。

2.JavaScript使用prototype链实现继承机制。

3.构造函数xxx.prototype是一个对象,xxx.prototype也有自己的__proto__属性,并且可以继续指向它的的prototype。

4.Object.prototype.proto最终指向null,这也是所有原型链的终点。

5.从一个对象的__proto__不断向上指向原型对象,最终指向Objecct.prototype后,接着指向为Null,这一条链子就叫做原型链。

例子

这么长的文章也是讲的略显抽象,我们来个例子便于理解

function Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}

function Son() {
    this.first_name = 'Melania'
}

Son.prototype = new Father()

let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

在上边的例子中(下边语言非正式完全准确,为个人总结便于理解,以待不断改正)

son.__proto__就指向了Son.prototype

直接访问对象.proto 那么就相当于访问了构造函数.prototype。注意对于第一种的实例方法(没构造函数换句话说构造方法就是Object)会指向Object.prototype

Son.prototype.__proto__指向father.prototype

Son.prototype = new Father()+上一条理解

father.prototype.__proto__指向Object.prototype

构造函数.prototype.__proto__应该是Object.prototype

Object.prototype.proto指向null

Object.prototype.proto最终指向null,这也是所有原型链的终点。

我们在简单引入一下程序运行过程,以理解原型链污染の原理

对于对象son,在调用son.last_name的时候,实际上JavaScript引擎会进行如下操作:

  1. 在对象son中寻找last_name。
  2. 如果找不到,则在son.__proto__中寻找last_name。
  3. 如果仍然找不到,则继续在son.proto.__proto__中寻找last_name。
  4. 依次寻找,直到找到null结束。

原型链污染

ok,前置知识基本已经明了,我们直接上例子来理解污染

// 这个对象直接实例化Object()
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar)
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar
console.log(zoo.bar)

这里由于修改了foo.proto.bar 也就是修改了Object.bar,因此在后续的实例化对象中,新的对象会继承这一属性 造成了原型链污染。

但是上边很显然是我们自己的实验过程,在ctf中什么情况可以使用呢

和python原型链污染类似,有合并函数对象merge,对象clone(其实内核就是将待操作的对象merge到一个空对象中),再举一个伪ctf的例子

eg:

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {  
            // 如果target与source有相同的键名 则让target的键值为source的键值
            merge(target[key], source[key])
        } else {
            target[key] = source[key]  // 如果target与source没有相通的键名 则直接在target新建键名并赋给键值
        }
    }
}
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)
//1 2
//undefined

// 如果target与source没有相通的键名 则直接在target新建键名并赋给键值

这里的__proto__被当作普通建值处理了,merge(o1, o2) 的执行相当于再o1新建merge(o1, o2) 的执行相当于也就是o1变成了**{a: 1, “proto“: {b: 2}}**

原型对象Object并没有被更改

我么稍作更改把o2以json格式传递来确保__proto__不被当做普通键值

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

// 如果target与source有相同的键名 则让target的键值为source的键值 merge(target[key], source[key])

这里等于 merge(Object.prototype, b: 2),把所有对象的原型都污染了,o1变成了{a:1,b: 2}

深入理解 JavaScript Prototype 污染攻击

以上内容过于复杂繁琐,我这里引用一下p神及其简洁的总结

  1. 每个构造函数(constructor)都有一个原型对象(prototype)
  2. 对象的__proto__属性,指向类的原型对象prototype
  3. JavaScript使用prototype链实现继承机制

文章作者: YUNiversity
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YUNiversity !
评论
  目录