Nodejs原型链污染学习

文章目录

  • 前置知识
    • JavaScript数据类型
    • prototype原型
    • 同步和异步
    • child_process模块
  • 原型链污染
    • 利用条件


前置知识

JavaScript数据类型

let和var关键字的区别

使用var或let关键字可以定义变量

let和var的区别如下:

  1. var是全局作用域,let 只在当前代码块内有效
  2. 当在代码块外访问let声明的变量时会报错
  3. var有变量提升,let没有变量提升
  4. let必须先声明再使用,否则报Uncaught ReferenceError xxx is not defined;var可以在声明前访问,只是会报undefined
  5. let变量不能重复声明,var变量可以重复声明

普通变量

var a=1;
let b=2;

数组变量

var a = new Array();
var b= [];

字典

var a = {};
var b= {"foo":"bar"};

prototype原型

JavaScript只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为__proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上知道一个对象的原型对象为null。

实例图
Nodejs原型链污染学习_第1张图片我们编写代码测试下
源码

function Son(){};
var son = new Son();
console.log(Son.prototype)
console.log(son.__proto__)
console.log(Son.prototype == son.__proto__)

不难发现Son.prototype == son.__proto__是成立的
Nodejs原型链污染学习_第2张图片

然后再了解下继承

原型是继承的基础,JavaScript中原型链可以通过prototype这个属性来实现继承机制

实例代码

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继承了Father类,在调用son.last_name时,首先在son对象里找,如果没有找到,就会在son.__proto__(也就是Son.prototype),找不到就再往上son.__proto__.__proto__里找 ,知道null为止

在这里插入图片描述

同步和异步

Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的
fs.readFileSync()。异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。

很简单理解,当你先读取文件输出后输出一段话的时候
同步:先输出文件内容,再输出一段话
异步:先输出一段话,后输出文件内容

就比如,涉及同步和异步的问题我们使用的exec是异步进程,在我们输入ls,查取目录时,就已经eval执行了,所以我们要使用创造同步进程的函数如execSync

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 事件循环、暂停任何其他代码的执行,直到子进程退出。

其中的一些函数,在一些情况下,可以导致命令执行漏洞

原型链污染

下面用代码解释下

let foo = {bar: 1}
console.log(foo.bar)
foo.__proto__.bar = 2
console.log(foo.bar)
let zoo = {}
console.log(zoo.bar)

可以发现当我们修改foo的原型时,由于查找顺序的原因,foo.bar仍为1;如果我们重新创建空对象,发现成功进行原型链污染
在这里插入图片描述

利用条件

原型链污染在哪些情况下可以实现呢,其实找找能够控制数组(对象)的“键名”的操作即可

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)

比如merge函数

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

在合并的过程中,存在赋值的操作target[key] = source[key],那么,这个key如果是__proto__,是不是就可以原型链污染呢?
添加如下代码测试

let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

可以发现合并虽然成功了,但原型链没有被污染
Nodejs原型链污染学习_第3张图片
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, “proto”: {b: 2}})中,__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],__proto__并不是一个key,自然也不会修改Object的原型。

绕过方法很简单,修改代码如下

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)

Nodejs原型链污染学习_第4张图片
这是因为,JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。

merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。

你可能感兴趣的:(学习,原型模式,node.js,web安全,json,前端)