Advisory | ManageEngine Applications Manager Remote Code Execution and SQLi

It is an interesting coincidence that almost 1 year ago we identified a critical security issue in a different product (Eventlog Analyzer) of this company. Now, this time we’ve came across with another product of this company during penetration test. To be honest I’ve seen more than 20 different high/critical vulnerability during the analysis of the product but I will only share two of them now, as a full disclosure.

Advisory Informations

Remotely Exploitable: Yes
Authentication Required: NO
Vendor URL: https://www.manageengine.com/products/applications_manager/download.html
CVSSv3 Score: 10.0
Date of found: 07 Mar 2018

Technical Details

Vulnerability #1 – Unauthenticated SQL Injection (BONUS!)

I do always start the analysis by reading web.xml files. That will give you abstract level of idea what the heck is happening within the software. Following definition was quite interesting for me.

...
<action path="/jsonfeed" type="com.adventnet.appmanager.struts.actions.JSONFeed" scope="request" parameter="method">
</action>
...

While I was reviewing the class I immediately detect multiple potential SQLi issue. Here is one example.

public void getConsoleJSONFeed(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response)
    throws Exception
  {
    StringBuffer jsonStr = new StringBuffer();
    try
    {
      String toReturn = request.getParameter("toReturn");
      String mgId = request.getParameter("mgId");
      String monType = request.getParameter("category");
      
      String query = null;
      if ((toReturn != null) && (toReturn.equals("allMGResource"))) {
        query = "select RESOURCENAME,RESOURCEID,TYPE from AM_ManagedObject where AM_ManagedObject.TYPE='HAI'";
      } else if ((toReturn != null) && (toReturn.equals("allMonInMG"))) {
        query = "select RESOURCENAME,RESOURCEID,TYPE from AM_ManagedObject, AM_PARENTCHILDMAPPER where AM_ManagedObject.RESOURCEID=AM_PARENTCHILDMAPPER.CHILDID and AM_PARENTCHILDMAPPER.PARENTID='" + mgId + "' and AM_ManagedObject.TYPE in " + Constants.serverTypes;
      } else if ((toReturn != null) && (toReturn.equals("OpManResource"))) {
        if (monType != null)
        {
          monType = "OpManager-" + monType;
          query = "select RESOURCENAME,RESOURCEID,SUBSTRING(AM_ManagedObject.TYPE,11),AM_AssociatedExtDevices.IPADDRESS from AM_ManagedObject, AM_PARENTCHILDMAPPER, AM_AssociatedExtDevices, ExternalDeviceDetails where AM_ManagedObject.RESOURCEID=AM_PARENTCHILDMAPPER.CHILDID and AM_PARENTCHILDMAPPER.PARENTID='" + mgId + "' and AM_ManagedObject.TYPE like 'OpManager-%' and AM_AssociatedExtDevices.RESID=AM_PARENTCHILDMAPPER.CHILDID and AM_AssociatedExtDevices.IPADDRESS=ExternalDeviceDetails.IPADDRESS and ExternalDeviceDetails.CATEGORY='" + monType + "'";
        }
        else if (mgId == null)
        {
          query = "select RESOURCENAME,RESOURCEID,SUBSTRING(TYPE,11),IPADDRESS from AM_ManagedObject,AM_AssociatedExtDevices where AM_ManagedObject.TYPE like 'OpManager-%' and AM_AssociatedExtDevices.RESID=AM_ManagedObject.RESOURCEID";
        }
        else
        {
          query = "select RESOURCENAME,RESOURCEID,SUBSTRING(TYPE,11),IPADDRESS from AM_ManagedObject,AM_AssociatedExtDevices,AM_PARENTCHILDMAPPER where AM_ManagedObject.RESOURCEID=AM_PARENTCHILDMAPPER.CHILDID and AM_AssociatedExtDevices.RESID=AM_ManagedObject.RESOURCEID and AM_PARENTCHILDMAPPER.PARENTID='" + mgId + "' and AM_ManagedObject.TYPE like 'OpManager-%'";
        }
      }
      ArrayList monList = this.mo.getRows(query);
...

Since I was familiar with the ManageEngine company’s product, I knew how to trigger this section of the class.

GET /jsonfeed.do?method=getParentGroups&haid=10000055 HTTP/1.1
Host: 12.0.0.226:9090
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
--
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID_APM_9090=88629946E13962211BA3562D33EB2ED8; Path=/; HttpOnly
Cache-Control: max-age=0, no-cache, no-store, must-revalidate
Expires: 0
Pragma: no-cache
Content-Type: text/html;charset=UTF-8
Content-Length: 32
Date: Wed, 07 Mar 2018 19:54:13 GMT
Connection: close

{"0":["Applications Manager"]}

How did I know that I can access this end-point without authentication ? That question is not something that I want to jump-in right now. I started exploiting this SQLi issue manually but somehow I wasn’t receiving expected behavior. Thus, I’ve decided to understand what kind of sql query was received by RDMS.

Protip: Do not try to patch application or change configuration of RDMS service. These kind of product always logs error. So you just need to find a log file location and trace it.

Here is the what I’ve seen when I send following payload.

12.0.0.226:9090/jsonfeed.do?method=getParentGroups&haid=10000055%27%22%3C%3E
root@asd:/opt/ME/AppManager13/AppManager13# tail -f logs/swissql00.log


Mar 07, 2018 11:59:35 AM com.adventnet.appmanager.db.AMConnectionPool executeQueryStmt
SEVERE: [SQL ERROR] select RESOURCENAME,RESOURCEID from AM_ManagedObject,AM_PARENTCHILDMAPPER where AM_PARENTCHILDMAPPER.CHILDID='10000055&#39;&quot;&lt;&gt;' and AM_ManagedObject.RESOURCEID=AM_PARENTCHILDMAPPER.PARENTID and AM_ManagedObject.TYPE='HAI'
org.postgresql.util.PSQLException: ERROR: invalid input syntax for integer: "10000055&#39;&quot;&lt;&gt;"
  Position: 110
  at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2102)
  at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1835)
  at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:257)
  at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:500)

Okay, this is awkward. You know that one of most common mottos of the application security is “Validate input, encode output”. But it seems, whoever has tried to apply this motto into this project has mind-confusion. Such global modification of inputs can cause more trouble then you can imagine. But in this case, developers made two completely unrelated mistakes. First one cause a SQLi. But second one, a stroke of luck, makes first one impossible to exploit.

Obviously, this is unexploitable vulnerability. Okay… So we just need to find a following things at the same time so we could have SQLi.

  1. We don’t have any credentials. Authenticated SQLis are of the table (I’ve seen 50+ authenticated SQLi during this analysis)
  2. It seem special characters, such as quotes, are globally encoded. So we need to find a sql query that takes user input without surrounding any quotes. (Protip: Look for integer inputs 🙂

Within a several seconds, I’ve detected following public method of same class.

public void getMonitorCount(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response)
   throws Exception
 {
   JSONObject count = new JSONObject();
   AMConnectionPool cp = AMConnectionPool.getInstance();
   ResultSet result = null;
   String haid = request.getParameter("haid");
   if (haid == null) {
     haid = "0";
   }
   String query = "select \"SYS\",count(*) from AM_ManagedObject,AM_PARENTCHILDMAPPER where AM_PARENTCHILDMAPPER.PARENTID=" + haid + " and AM_PARENTCHILDMAPPER.CHILDID=AM_ManagedObject.RESOURCEID and type in" + Constants.serverTypes + " union select \"APP\",count(*) from AM_ManagedObject,AM_PARENTCHILDMAPPER where AM_PARENTCHILDMAPPER.PARENTID=" + haid + " and AM_PARENTCHILDMAPPER.CHILDID=AM_ManagedObject.RESOURCEID and type not in " + Constants.serverTypes + " and type not like '%OpManager%' union  select \"NWD\",count(*) from AM_ManagedObject,AM_PARENTCHILDMAPPER where AM_PARENTCHILDMAPPER.PARENTID=" + haid + " and AM_PARENTCHILDMAPPER.CHILDID=AM_ManagedObject.RESOURCEID and type like '%OpManager%'";
   try
   {
     result = AMConnectionPool.executeQueryStmt(query);
     while (result.next()) {
       count.append(result.getString(1), result.getString(2));
     }
     try
     {
       if (result != null) {
         result.close();
       }
     }
     catch (Exception e)
     {
       e.printStackTrace();
     }
     out = response.getOutputStream();
   }

Look for the haid parameter at the middle of the SQL query. You can see that it’s not given into the query within single/double quotes. Which means we don’t need to escape from anything. It’s quite possible to directly modify the query.

Most funny thing about this case is that the final query was designed only for MsSQL. But this product also support Postgresql… Actually, it’s shipped with psql.

POC URL

http://12.0.0.226:9090/jsonfeed.do?method=getMonitorCount&haid=10000055

Vulnerability #2 – Unauthenticated Remote Code Execution

While I was poking around, I’ve realised that testCredentials.do endpoint’s was accessible without having authentication cookie.  So i thought it’s worth to give a try to dive into the business logic of this module.

TestCredentials class have two different publicly accessible class method. Here is the most interesting one.

public ActionForward testCredentialForConfMonitors(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response)
  {
    Properties authResult = new Properties();
    String monType = null;
    try
    {
      monType = request.getParameter("montype");
      if ((monType == null) || (monType.equalsIgnoreCase("null"))) {
        monType = request.getParameter("type");
      }
      NewMonitorConf newMonConf = new NewMonitorConf();
      if ((newMonConf.preConfMap.containsKey(monType)) || (monType.equalsIgnoreCase("node")))
      {
        monType = newMonConf.getResourceTypeForPreConf(monType);
        
        authResult = newMonConf.getAuthResultAsPerResourceType(monType, request, true);
      }
      else
      {
        Properties props = NewMonitorConf.getClass(monType);
        ArrayList args = NewMonitorUtil.getArgsforConfMon(monType);
        String dcclass = props.getProperty("dcclass");
        CustomDCInf amdc = (CustomDCInf)Class.forName(dcclass).newInstance();
        Properties argsasprops = NewMonitorConf.getValuesforArgs(request, args);
        authResult = amdc.CheckAuthentication(argsasprops);
      }
      response.setContentType("text/html; charset=UTF-8");
      PrintWriter out = response.getWriter();
      if (authResult.getProperty("authentication").equalsIgnoreCase("passed"))
      {
        String passedMsg = NmsUtil.GetString("Passed");
        out.println("<font color=green>" + passedMsg + "</font>");
        out.flush();
      }
      else
      {
        // ... OMITTED CODE SECTION ...
      }
    }
    catch (NoClassDefFoundError er)
    {
      er.printStackTrace();
      try
      {
        if ("WebsphereMQ".equals(monType))
        {
          // ... OMITTED CODE SECTION ...
        }
      }
      catch (Exception e)
      {
        e.printStackTrace();
      }
    }
    catch (Exception ex)
    {
      ex.printStackTrace();
    }
    return null;
  }

That was the moment I started to think about some attack vectors. After all, this products name is “Applications Manager”. It means that this product is able to access servers, applications, databases, etc. etc etc. For this reason, I’ve decided to checkout the features of  the product.

Following screenshot shows a list of product that can be tracked by using Application Manager. I do understand how to fetch information from databases, linux systems but what about MS Office SharePoint or Microsoft Lync ? I wouldn’t directly execute powershell or vbs script but most of the developers don’t think like me us.

If i truly understand what is happening at NewMonitor class, I could find more realistic attack vectors instead of assumptions.

After spending multiple hours poking around. I’ve came across with following class.

public Properties CheckAuthentication(Properties props)
  {
    Properties authresult = new Properties();
    String availmess = null;
    boolean authentication = false;
    
    String host = props.getProperty("HostName");
    String username = props.getProperty("UserName");
    String password = props.getProperty("Password");
    boolean isPowershellEnabled = Boolean.parseBoolean(props.getProperty("Powershell", "FALSE"));
    String authMode = (props.getProperty("CredSSP") != null) && (props.getProperty("CredSSP").equals("Yes")) ? "CredSSP" : "";
    if (!isPowershellEnabled)
    {
      WMIDataCollector wl = new WMIDataCollector();
      String wmiquery = "Select * from Win32_PerfRawData_PerfOS_Processor where Name='_Total'";
      Properties output = wl.getData(host, username, password, wmiquery, new Vector(), "wmiget.vbs");
      if (output.get("ErrorMsg") != null)
      {
        if (((String)output.get("ErrorMsg")).indexOf("The RPC server is unavailable") != -1) {
          availmess = FormatUtil.getString("am.webclient.sharepoint.rpcerror.text");
        } else if (((String)output.get("ErrorMsg")).indexOf("Access is denied") != -1) {
          availmess = FormatUtil.getString("am.webclient.sharepoint.accessdenied.text");
        } else {
          availmess = (String)output.get("ErrorMsg");
        }
      }
      else {
        authentication = true;
      }
    }
    else
    {
      List<String[]> outputFromScript = null;
      boolean farmtype = props.getProperty("SPType", "SPServer").equalsIgnoreCase("Farm");
      
      String psFilePath = System.getProperty("user.dir") + File.separator + "conf" + File.separator + "application" + File.separator + "scripts" + File.separator + "powershell" + File.separator + "TestConnectivity.ps1";
      File psFile = new File(psFilePath);
      password = password.replaceAll("'", "''");
      String scriptToExecute = "powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden \"&{&'" + psFile.getAbsolutePath() + "' " + host + " " + username + " '" + password + "'}\"";
      if (farmtype) {
        scriptToExecute = "powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden \"&{&'" + psFile.getAbsolutePath() + "' " + host + " " + username + " '" + password + "' " + "'FarmType' '" + authMode + "'}\"";
      }
      AMLog.debug("SharePointServerDataCollector::resourcename: " + props.getProperty("resourcename") + " ,reourceid: " + props.getProperty("resourceid") + " ,hostname: " + props.getProperty("HostName") + ",powershell: " + props.getProperty("PowerShell") + " ::scriptToExecute:" + psFilePath);
      try
      {
        Process proc = Runtime.getRuntime().exec(scriptToExecute);
        RuntimeProcessStreamReader readerThread = new RuntimeProcessStreamReader(host, scriptToExecute, proc, 300, true, "inputstream", true);

As you can see host, username and password are being passed to the powershell command without sanitisation. Ofcourse I’ve trace down from input to here in order to make sure about it.

Here is the necessary HTTP request to trigger this issue.

POST /testCredential.do HTTP/1.1
Host: 12.0.0.226:9090
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 595
Connection: close

&method=testCredentialForConfMonitors&cacheid=1520419442645&type=OfficeSharePointServer&serializedData=url=%2Fjsp%2FnewConfType.jsp&searchOptionValue=&query=&method=createMonitor&addtoha=null&resourceid=&montype=OfficeSharePointServer&isAgentEnabled=NO&resourcename=null&isAgentAssociated=false&hideFieldsForIT360=null&childNodesForWDM=%5B%5D&type=OfficeSharePointServer&displayname=asd&HostName=12.0.0.226&Version=2013&Services=False&Service=False&Powershell=True&CredSSP=False&SPType=SPServer&CredentialDetails=nocm&cmValue=-1&UserName=qwe&Password=qwe&allowEdit=true&pollinterval=5&groupname=

Metasploit Module

Here is the metasploit module that exploits this command injection vulnerability.
PR to the msf master branch (https://github.com/rapid7/metasploit-framework/pull/9684)

Mehmet Ince

Master Ninja @ Prodaft / INVICTUS Europe.