node.js原型链污染
NodeJS基础
Node.js 是一个基于 Chrome V8 引擎的 Javascript 运行环境
但是它是由C++开发的,它只是一个JavaScript语言解释器。
REPL环境运行JavaScript的代码
在浏览器的控制台或者node的运行环境都属于REPL运行环境,均可以运行JS代码。
在NodeJS中分为三个模块,分别是:核心模块、自定义模块、第三方模块。
这里提一点,JS代码在编程时,如果需要使用某个模块的功能,那么就需要提前将其导入,与Python类似,只不过在Python中使用import关键字,而JS中使用require关键字。
读取文件操作
文件系统模块就是核心模块
同步和异步
区别:
同步方法: 等待每个操作完成,然后只执行下一个操作
异步方式: 从不等待每个操作完成,而是只在第一步执行所有操作
看到一个比较有趣的描述:
同步: 可以拿吃饭和看电视来举例子,同步就是先吃完饭,吃完饭后再看电视,不能边看边吃,这就是同步
异步: 同样拿上边的例子来说,异步就是边吃饭边看电视,看电视和吃饭同时进行,这样举例就应该很清楚了
readFile()是异步操作
HTTP服务
1 | //引入http核心模块 |
全局变量
- __dirname:当前模块的目录名。
- __filename:当前模块的文件名。 这是当前的模块文件的绝对路径(符号链接会被解析)。
- exports变量是默认赋值给
module.exports
,它可以被赋予新值,它会暂时不会绑定到module.exports。 - module:在每个模块中,
module
的自由变量是对表示当前模块的对象的引用。 为方便起见,还可以通过全局模块的exports
访问module.exports
。 module 实际上不是全局的,而是每个模块本地的 - require模块就不多说了,用于引入模块、 JSON、或本地文件。 可以从 node_modules 引入模块。
child_process(创建子进程)
child_process提供了几种创建子进程的方式
异步方式:spawn、exec、execFile、fork
同步方式:spawnSync、execSync、execFileSync
经过上面的同步和异步思想的理解,创建子进程的同步异步方式应该不难理解。
在异步创建进程时,spawn是基础,其他的fork、exec、execFile都是基于spawn来生成的。
同步创建进程可以使用child_process.spawnSync()
、child_process.execSync()
和 child_process.execFileSync()
,同步的方法会阻塞 Node.js 事件循环、暂停任何其他代码的执行,直到子进程退出。
https://www.anquanke.com/post/id/236182
原型
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,
而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法
设计原型的初衷无非是对于每个实例对象,其拥有的共同属性没必要对每个对象实例再分配一片内存来存放这个属性。而可以上升到所有对象共享这个属性,而这个属性的实体在内存中也仅仅只有一份。
1 | 1. function F(){...} |
在c++或java这些面向对象的语言中,我们如果想要一个对象,首先需要使用关键字class声明一个类,再使用关键字new一个对象出来,但是在JavaScript中没有class以及类这种概念(为了简化编写JavaScript代码,ECMAScript 6后增加了class
语法,但class
其实只是一个语法糖)。 在JavaScript有这么两种声明对象的方式,为了好理解我们先引入类的思想。
1 | person=new Object() |
既然是通过实例化Object来创建对象或创建构造函数,在JavaScript中有两个很特殊的对象,Function() 和 Object() ,它们两个既是构造函数也是对象,作为对象是不是应该有一个“类”去作为他们的模板呢?
对于Object()来说,要声明这么一个构造函数我们可以使用关键字function来创建 。(在底层 使用function创建一个函数 其实就相当于这个过程)
1 | function Object() |
那么对于Function自己这个对象,他是怎么来的呢?如果用Function.__proto__
和Function.prototype进行比较,发现二者是全等的,所以Function创造了自己,也创造了Object,所以JavaScript中,所有函数都是对象,而对象是通过函数创建的。因此构造函数.prototype.__proto__
应该是Object.prototype,而不是Function.prototype,Function的作用是创建而不是继承。
__proto__
是任何一个对象拥有的属性
prototype
是任何一个函数拥有的一个属性
原型链继承机制
深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)
还得看p神
所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
1 | function Father() { |
Son类继承了Father类的last_name
属性,最后输出的是Name: Melania Trump
。
总结一下,对于对象son,在调用son.last_name
的时候,实际上JavaScript引擎会进行如下操作:
- 在对象son中寻找last_name
- 如果找不到,则在
son.__proto__
中寻找last_name - 如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name - 依次寻找,直到找到
null
结束。比如,Object.prototype
的__proto__
就是null
原型链污染
foo.__proto__
指向的是Foo
类的prototype
。那么,如果我们修改了foo.__proto__
中的值,是不是就可以修改Foo类呢?
1 | // foo是一个简单的JavaScript对象 |
最后,虽然zoo是一个空对象{}
,但zoo.bar
的结果居然是2:
原因也显而易见:因为前面我们修改了foo的原型foo.__proto__.bar = 2
,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,我们又用Object类创建了一个zoo对象let zoo = {}
,zoo对象自然也有一个bar属性了。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
哪些情况会被污染
在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?
我们思考一下,哪些情况下我们可以设置__proto__
的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
- 对象merge
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
以对象merge为例,我们想象一个简单的merge函数:
1 | function merge(target, source) { |
在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key如果是__proto__
,是不是就可以原型链污染呢?
我们用如下代码实验一下:
1 | let o1 = {} |
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
如何让__proto__
被认为是一个键名呢?
我们将代码改成如下:
1 | let o1 = {} |
可见,新建的o3对象,也存在b属性,说明Object已经被污染:
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
通过题[GYCTF2020]Ez_Express来加深理解
1 | require( “child_process” ).spawnSync( ‘ls’, [ ‘/‘ ] ).stdout.toString() |