一、信息梳理 根据网络漏洞信息:
vSphere 是 VMware 推出的虚拟化平台套件,包含 ESXi、vCenter Server 等一系列的软件。其中 vCenter Server 为 ESXi 的控制中心,可从单一控制点统一管理数据中心的所有 vSphere 主机和虚拟机,使得 IT 管理员能够提高控制能力,简化入场任务,并降低 IT 环境的管理复杂性与成本。
vSphere Client(HTML5)在 vCenter Server 插件vRealize Operations(默认安装)中存在一个远程执行代码漏洞。未授权的攻击者可以通过开放 443 端口的服务器向 vCenter Server 发送精心构造的请求,从而在服务器上写入 webshell,最终造成远程任意代码执行。
vCenter Server 的 vROPS 插件的 API 未经过鉴权,存在一些敏感借口。其中 uploadova
接口存在一个上传 OVA 文件的功能,将 TAR 文件解压后上传到 /tmp/unicorn_ova_dir
目录
版本:
vmware:vcenter_server
7.0 U1c 之前的 7.0 版本
vmware:vcenter_server
6.7 U3l 之前的 6.7 版本
vmware:vcenter_server
6.5 U3n 之前的 6.5 版本
可将重要信息提取:
存在漏洞的地方vSphere Client(HTML5) 的插件vRealize Operations(默认安装)存在RCE:接口 uploadova
接口存在一个上传 OVA 文件,上传tar文件会将文件放在/tmp/unicorn_ova_dir
目录,导致可以上传shell。
具体漏洞接口位置:
1 https://<VC-IP-or-FQDN>/ui/vropspluginui/rest/services/updateova
二、本地复现 本地部署 从运维大佬那里要来的ova文件:链接:https://pan.baidu.com/s/1-jK1iY46-hiza9ni2TdDkA 提取码:ns83
导入ova,最小配置要求如下图2 vCPUs、10GB内存、300GB硬盘
:
本地部署具体可以参考https://blog.51cto.com/wangchunhai/2439987
设置单点登录密码
设置root密码
省略中间页面配置过程…
Web页面设置相关信息后保存,最后一步比较花费时间,多等待一下。
上传脚本getshell 脚本:https://github.com/NS-Sp4ce/CVE-2021-21972
三、分析 根据github已公开脚本 可以发现其利用步骤如下:
准备好jsp文件
添加文件到相应路径的tar包,Linux和Windows路径不同。
将添加好的tar包上传到漏洞API:/ui/vropspluginui/rest/services/updateova
访问上传后解压的路径对应的URL
思考问题:
通过代码分析漏洞原因
Windows相对来说简单,可以直接上传到相应路径(..\..\ProgramData\VMware\vCenterServer\data\perfcharts\tc-instance\webapps\statsreport\
)然后访问shell。但是Linux不同,他的利用方法目前网络公开的是通过免密登录或者shell上传,上传位置/usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/{REPLACE_RANDOM_ID_HERE}/0/h5ngc.war/resources/
,这个路径上传不一定能成功利用,除了这个位置,需要思考有没有其它可以利用路径,该路径需要满足可执行、未授权可访问、不存在随机性。
代码分析 目前根据公开信息,CVE-2021-21972 VMware vCenter未授权文件上传漏洞
该漏洞主要有两方面原因导致getshell,一是未授权可上传,一是路径穿越,我们一一分析下。
首先未授权很容易找到原因,vRops插件默认安装,并且默认配置了未认证,所以导致未授权可以访问上传路径,如下图可以看出:
另外就是路径穿越。上传OVA的接口如下,我们分析下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 @RequestMapping ( value = {"/uploadova" }, method = {RequestMethod.POST} ) public void uploadOvaFile (@RequestParam(value = "uploadFile" ,required = true ) CommonsMultipartFile uploadFile, HttpServletResponse response) throws Exception { logger.info("Entering uploadOvaFile api" ); int code = uploadFile.isEmpty() ? 400 : 200 ; PrintWriter wr = null ; try { if (code != 200 ) { response.sendError(code, "Arguments Missing" ); return ; } wr = response.getWriter(); } catch (IOException var14) { var14.printStackTrace(); logger.info("upload Ova Controller Ended With Error" ); } response.setStatus(code); String returnStatus = "SUCCESS" ; if (!uploadFile.isEmpty()) { try { logger.info("Downloading OVA file has been started" ); logger.info("Size of the file received : " + uploadFile.getSize()); InputStream inputStream = uploadFile.getInputStream(); File dir = new File("/tmp/unicorn_ova_dir" ); if (!dir.exists()) { dir.mkdirs(); } else { String[] entries = dir.list(); String[] var9 = entries; int var10 = entries.length; for (int var11 = 0 ; var11 < var10; ++var11) { String entry = var9[var11]; File currentFile = new File(dir.getPath(), entry); currentFile.delete(); } logger.info("Successfully cleaned : /tmp/unicorn_ova_dir" ); } TarArchiveInputStream in = new TarArchiveInputStream(inputStream); TarArchiveEntry entry = in.getNextTarEntry(); ArrayList result = new ArrayList(); while (entry != null ) { if (entry.isDirectory()) { entry = in.getNextTarEntry(); } else { File curfile = new File("/tmp/unicorn_ova_dir" , entry.getName()); File parent = curfile.getParentFile(); if (!parent.exists()) { parent.mkdirs(); } OutputStream out = new FileOutputStream(curfile); IOUtils.copy(in, out); out.close(); result.add(entry.getName()); entry = in.getNextTarEntry(); } } in.close(); logger.info("Successfully deployed File at Location :/tmp/unicorn_ova_dir" ); } catch (Exception var15) { logger.error("Unable to upload OVA file :" + var15); returnStatus = "FAILED" ; } } wr.write(returnStatus); wr.flush(); wr.close();
这段代码主要逻辑:
POST上传文件,并且判断是否上传成功,上传成功会返回SUCCESS
判断/tmp/unicorn_ova_di
是否存在,不存在则创建
以tar文件形式解压上传的文件输入流
将/tmp/unicorn_ova_dir
与解压的文件(包含路径)进行拼接,创建目录文件
由于使用File(String parent, String child)将两个目录直接拼接了,并且没有过滤任何的”../“,所以导致可以穿越目录从而上传文件到任意目录下。
四、拓展 如何实现文件上传(通过post报文、python和curl)?
http: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST/upload.html HTTP/1.1 Accept : text/plain, */* Accept-Language : zh-cn Host : 192.168.24.56Content-Type:multipart/form-data;boundary=-----------------------------7db372eb000e2 User-Agent : WinHttpClient Content-Length : 3693Connection : Keep-Alive-------------------------------7db372eb000e2 Content-Disposition : form-data; name="file"; filename="chrome.jpg"Content-Type : image/jpeg(此处省略jpeg文件二进制数据...) -------------------------------7db372eb000e2--
根据 rfc1867, multipart/form-data是必须的。---------------------------7db372eb000e2
是分隔符,分隔多个文件、表单项。其中b372eb000e2
是即时生成的一个数字,用以确保整个分隔符不会在文件或表单项的内容中出现。Form每个部分用分隔符分割,分隔符之前必须加上--
这两个字符(即–{boundary})才能被http协议认为是Form的分隔符 ,表示结束的话用在正确的分隔符后面添加”–”表示结束 。---------------------------7d
是 IE 特有的标志,Mozila 为---------------------------71
,------webkitformboundary
是safari 浏览器从客户端向服务器端传送HTML标签数据所使用的分隔符(一般会在分隔符后附加一串十六进制的数以示区别)
python: 1 2 # Requests也支持以multipart形式发送post请求,只需将一文件传给requests.post()的files参数即可。"content-type"为 "multipart/form-data",请求正文是binary r = requests.post(url, files={'file' : open('chrome.jpg', 'rb')})
CURL: 1 2 3 4 5 6 curl -F 'data=@path/to/local/file' http://localhost/upload [-H "User-Agent: WinHttpClient" -H "token: XXX" ] [-k] [-v] curl -F 'file1=@/path/to/file1' -F 'file2=@/path/to/file2' ... http://localhost/upload curl -F 'files[]=@/path/to/file1' -F 'files[]=@/path/to/file2' ... http://localhost/upload
参考链接: 1 2 3 4 5 6 https://swarm.ptsecurity.com/unauth-rce-vmware/ https://mp.weixin.qq.com/s/g2eFMSpZereNA73Hve8cQg https://www.cnblogs.com/insane-Mr-Li/p/9145152.html https://www.cnblogs.com/frustrate2/archive/2012/11/07/2759080.html https://www.gonever.com/post/45 https://www.cnblogs.com/xiaohulumanong/p/8338248.html