Skip to content

Commit 8561170

Browse files
committed
Merge branch 'upstream-master' into feature/MSP-9707/smb-bruteforce-refactor
2 parents 669779d + 0022349 commit 8561170

File tree

12 files changed

+853
-135
lines changed

12 files changed

+853
-135
lines changed

lib/rex/proto/smb/client.rb

Lines changed: 201 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -150,72 +150,116 @@ def smb_defaults(packet)
150150
packet.v['ProcessID'] = self.process_id.to_i
151151
end
152152

153-
154-
# The main dispatcher for all incoming SMB packets
155-
def smb_recv_parse(expected_type, ignore_errors = false)
153+
# Receive a full SMB reply and cache the parsed packet
154+
def smb_recv_and_cache
155+
@smb_recv_cache ||= []
156156

157157
# This will throw an exception if it fails to read the whole packet
158158
data = self.smb_recv
159159

160160
pkt = CONST::SMB_BASE_PKT.make_struct
161161
pkt.from_s(data)
162-
res = pkt
162+
163+
# Store the received packet into the cache
164+
@smb_recv_cache << [ pkt, data, Time.now ]
165+
end
166+
167+
# Scan the packet receive cache for a matching response
168+
def smb_recv_cache_find_match(expected_type)
169+
170+
clean = []
171+
found = nil
172+
173+
@smb_recv_cache.each do |cent|
174+
pkt, data, tstamp = cent
175+
176+
# Return matching packets and mark for removal
177+
if pkt['Payload']['SMB'].v['Command'] == expected_type
178+
found = [pkt,data]
179+
clean << cent
180+
end
181+
182+
# Purge any packets older than 5 minutes
183+
if Time.now.to_i - tstamp.to_i > 300
184+
clean << cent
185+
end
186+
187+
break if found
188+
end
189+
190+
clean.each do |cent|
191+
@smb_recv_cache.delete(cent)
192+
end
193+
194+
found
195+
end
196+
197+
# The main dispatcher for all incoming SMB packets
198+
def smb_recv_parse(expected_type, ignore_errors = false)
199+
200+
pkt = nil
201+
data = nil
202+
203+
# This allows for some leeway when a previous response has not
204+
# been processed but a new request was sent. The old response
205+
# will eventually be timed out of the cache.
206+
1.upto(3) do |attempt|
207+
smb_recv_and_cache
208+
pkt,data = smb_recv_cache_find_match(expected_type)
209+
break if pkt
210+
end
163211

164212
begin
165213
case pkt['Payload']['SMB'].v['Command']
166214

167215
when CONST::SMB_COM_NEGOTIATE
168-
res = smb_parse_negotiate(pkt, data)
216+
res = smb_parse_negotiate(pkt, data)
169217

170218
when CONST::SMB_COM_SESSION_SETUP_ANDX
171-
res = smb_parse_session_setup(pkt, data)
219+
res = smb_parse_session_setup(pkt, data)
172220

173221
when CONST::SMB_COM_TREE_CONNECT_ANDX
174-
res = smb_parse_tree_connect(pkt, data)
222+
res = smb_parse_tree_connect(pkt, data)
175223

176224
when CONST::SMB_COM_TREE_DISCONNECT
177-
res = smb_parse_tree_disconnect(pkt, data)
225+
res = smb_parse_tree_disconnect(pkt, data)
178226

179227
when CONST::SMB_COM_NT_CREATE_ANDX
180-
res = smb_parse_create(pkt, data)
228+
res = smb_parse_create(pkt, data)
181229

182230
when CONST::SMB_COM_TRANSACTION, CONST::SMB_COM_TRANSACTION2
183-
res = smb_parse_trans(pkt, data)
231+
res = smb_parse_trans(pkt, data)
184232

185233
when CONST::SMB_COM_NT_TRANSACT
186-
res = smb_parse_nttrans(pkt, data)
234+
res = smb_parse_nttrans(pkt, data)
187235

188236
when CONST::SMB_COM_NT_TRANSACT_SECONDARY
189-
res = smb_parse_nttrans(pkt, data)
237+
res = smb_parse_nttrans(pkt, data)
190238

191239
when CONST::SMB_COM_OPEN_ANDX
192-
res = smb_parse_open(pkt, data)
240+
res = smb_parse_open(pkt, data)
193241

194242
when CONST::SMB_COM_WRITE_ANDX
195-
res = smb_parse_write(pkt, data)
243+
res = smb_parse_write(pkt, data)
196244

197245
when CONST::SMB_COM_READ_ANDX
198-
res = smb_parse_read(pkt, data)
246+
res = smb_parse_read(pkt, data)
199247

200248
when CONST::SMB_COM_CLOSE
201-
res = smb_parse_close(pkt, data)
249+
res = smb_parse_close(pkt, data)
202250

203251
when CONST::SMB_COM_DELETE
204-
res = smb_parse_delete(pkt, data)
252+
res = smb_parse_delete(pkt, data)
205253

206254
else
207255
raise XCEPT::InvalidCommand
208256
end
209257

210-
if (pkt['Payload']['SMB'].v['Command'] != expected_type)
211-
raise XCEPT::InvalidType
212-
end
213-
214258
if (ignore_errors == false and pkt['Payload']['SMB'].v['ErrorClass'] != 0)
215259
raise XCEPT::ErrorCode
216260
end
217261

218-
rescue XCEPT::InvalidWordCount, XCEPT::InvalidCommand, XCEPT::InvalidType, XCEPT::ErrorCode
262+
rescue XCEPT::InvalidWordCount, XCEPT::InvalidCommand, XCEPT::ErrorCode
219263
$!.word_count = pkt['Payload']['SMB'].v['WordCount']
220264
$!.command = pkt['Payload']['SMB'].v['Command']
221265
$!.error_code = pkt['Payload']['SMB'].v['ErrorClass']
@@ -1837,88 +1881,150 @@ def find_first(path)
18371881
0, # Storage type is zero
18381882
].pack('vvvvV') + path + "\x00"
18391883

1840-
begin
1841-
resp = trans2(CONST::TRANS2_FIND_FIRST2, parm, '')
1842-
search_next = 0
1843-
begin
1844-
pcnt = resp['Payload'].v['ParamCount']
1845-
dcnt = resp['Payload'].v['DataCount']
1846-
poff = resp['Payload'].v['ParamOffset']
1847-
doff = resp['Payload'].v['DataOffset']
1884+
resp = trans2(CONST::TRANS2_FIND_FIRST2, parm, '')
1885+
search_next = 0
1886+
1887+
# Loop until we run out of results
1888+
loop do
1889+
pcnt = resp['Payload'].v['ParamCount']
1890+
dcnt = resp['Payload'].v['DataCount']
1891+
poff = resp['Payload'].v['ParamOffset']
1892+
doff = resp['Payload'].v['DataOffset']
18481893

1849-
# Get the raw packet bytes
1850-
resp_rpkt = resp.to_s
1894+
# Get the raw packet bytes
1895+
resp_rpkt = resp.to_s
18511896

1852-
# Remove the NetBIOS header
1853-
resp_rpkt.slice!(0, 4)
1897+
# Remove the NetBIOS header
1898+
resp_rpkt.slice!(0, 4)
18541899

1855-
resp_parm = resp_rpkt[poff, pcnt]
1856-
resp_data = resp_rpkt[doff, dcnt]
1900+
resp_parm = resp_rpkt[poff, pcnt]
1901+
resp_data = resp_rpkt[doff, dcnt]
18571902

1858-
if search_next == 0
1859-
# search id, search count, end of search, error offset, last name offset
1860-
sid, scnt, eos, eoff, loff = resp_parm.unpack('v5')
1861-
else
1862-
# FINX_NEXT doesn't return a SID
1863-
scnt, eos, eoff, loff = resp_parm.unpack('v4')
1864-
end
1865-
didx = 0
1866-
while (didx < resp_data.length)
1867-
info_buff = resp_data[didx, 70]
1868-
break if info_buff.length != 70
1869-
info = info_buff.unpack(
1870-
'V'+ # Next Entry Offset
1871-
'V'+ # File Index
1872-
'VV'+ # Time Create
1873-
'VV'+ # Time Last Access
1874-
'VV'+ # Time Last Write
1875-
'VV'+ # Time Change
1876-
'VV'+ # End of File
1877-
'VV'+ # Allocation Size
1878-
'V'+ # File Attributes
1879-
'V'+ # File Name Length
1880-
'V'+ # Extended Attr List Length
1881-
'C'+ # Short File Name Length
1882-
'C' # Reserved
1883-
)
1884-
name = resp_data[didx + 70 + 24, info[15]].sub(/\x00+$/n, '')
1885-
files[name] =
1886-
{
1887-
'type' => ((info[14] & 0x10)==0x10) ? 'D' : 'F',
1888-
'attr' => info[14],
1889-
'info' => info
1890-
}
1891-
1892-
break if info[0] == 0
1893-
didx += info[0]
1894-
end
1895-
last_search_id = sid
1896-
last_offset = loff
1897-
last_filename = name
1898-
if eos == 0 and last_offset != 0 #If we aren't at the end of the search, run find_next
1899-
resp = find_next(last_search_id, last_offset, last_filename)
1900-
search_next = 1 # Flip bit so response params will parse correctly
1901-
end
1902-
end until eos != 0 or last_offset == 0
1903-
rescue ::Exception
1904-
raise $!
1903+
if search_next == 0
1904+
# search id, search count, end of search, error offset, last name offset
1905+
sid, scnt, eos, eoff, loff = resp_parm.unpack('v5')
1906+
else
1907+
# FIND_NEXT doesn't return a SID
1908+
scnt, eos, eoff, loff = resp_parm.unpack('v4')
1909+
end
1910+
1911+
didx = 0
1912+
while (didx < resp_data.length)
1913+
info_buff = resp_data[didx, 70]
1914+
break if info_buff.length != 70
1915+
1916+
info = info_buff.unpack(
1917+
'V'+ # Next Entry Offset
1918+
'V'+ # File Index
1919+
'VV'+ # Time Create
1920+
'VV'+ # Time Last Access
1921+
'VV'+ # Time Last Write
1922+
'VV'+ # Time Change
1923+
'VV'+ # End of File
1924+
'VV'+ # Allocation Size
1925+
'V'+ # File Attributes
1926+
'V'+ # File Name Length
1927+
'V'+ # Extended Attr List Length
1928+
'C'+ # Short File Name Length
1929+
'C' # Reserved
1930+
)
1931+
1932+
name = resp_data[didx + 70 + 24, info[15]]
1933+
1934+
# Verify that the filename was actually present
1935+
break unless name
1936+
1937+
# Key the file list minus any trailing nulls
1938+
files[name.sub(/\x00+$/n, '')] =
1939+
{
1940+
'type' => ( info[14] & CONST::SMB_EXT_FILE_ATTR_DIRECTORY == 0 ) ? 'F' : 'D',
1941+
'attr' => info[14],
1942+
'info' => info
1943+
}
1944+
1945+
break if info[0] == 0
1946+
didx += info[0]
1947+
end
1948+
1949+
last_search_id = sid
1950+
last_offset = loff
1951+
last_filename = name
1952+
1953+
# Exit the search if we reached the end of our results
1954+
break if (eos != 0 or last_search_id.nil? or last_offset.to_i == 0)
1955+
1956+
# If we aren't at the end of the search, run find_next
1957+
resp = find_next(last_search_id, last_offset, last_filename)
1958+
1959+
# Flip bit so response params will parse correctly
1960+
search_next = 1
19051961
end
19061962

1907-
return files
1963+
files
19081964
end
19091965

19101966
# Supplements find_first if file/dir count exceeds max search count
19111967
def find_next(sid, resume_key, last_filename)
19121968

19131969
parm = [
1914-
sid, # Search ID
1915-
20, # Maximum search count (Size of 20 keeps response to 1 packet)
1916-
260, # Level of interest
1917-
resume_key, # Resume key from previous (Last name offset)
1918-
6, # Close search if end of search
1919-
].pack('vvvVv') + last_filename.to_s + "\x00" # Last filename returned from find_first or find_next
1920-
resp = trans2(CONST::TRANS2_FIND_NEXT2, parm, '')
1921-
return resp # Returns the FIND_NEXT2 response packet for parsing by the find_first function
1970+
sid, # Search ID
1971+
20, # Maximum search count (Size of 20 keeps response to 1 packet)
1972+
260, # Level of interest
1973+
resume_key, # Resume key from previous (Last name offset)
1974+
6, # Close search if end of search
1975+
].pack('vvvVv') +
1976+
last_filename.to_s + # Last filename returned from find_first or find_next
1977+
"\x00" # Terminate the file name
1978+
1979+
# Returns the FIND_NEXT2 response packet for parsing by the find_first function
1980+
trans2(CONST::TRANS2_FIND_NEXT2, parm, '')
1981+
end
1982+
1983+
# Recursively search for files matching a regular expression
1984+
def file_search(current_path, regex, depth)
1985+
depth -= 1
1986+
return [] if depth < 0
1987+
1988+
results = find_first(current_path + "*")
1989+
files = []
1990+
1991+
results.each_pair do |fname, finfo|
1992+
1993+
# Skip current and parent directory results
1994+
next if %W{. ..}.include?(fname)
1995+
1996+
# Verify the results contain an attribute
1997+
next unless finfo and finfo['attr']
1998+
1999+
if finfo['attr'] & CONST::SMB_EXT_FILE_ATTR_DIRECTORY == 0
2000+
# Add any matching files to our result set
2001+
files << "#{current_path}#{fname}" if fname =~ regex
2002+
else
2003+
# Recurse into the discovery subdirectory for more files
2004+
begin
2005+
search_path = "#{current_path}#{fname}\\"
2006+
file_search(search_path, regex, depth).each {|fn| files << fn }
2007+
rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
2008+
2009+
# Ignore common errors related to permissions and non-files
2010+
if %W{
2011+
STATUS_ACCESS_DENIED
2012+
STATUS_NO_SUCH_FILE
2013+
STATUS_OBJECT_NAME_NOT_FOUND
2014+
STATUS_OBJECT_PATH_NOT_FOUND
2015+
}.include? e.get_error(e.error_code)
2016+
next
2017+
end
2018+
2019+
$stderr.puts [e, e.get_error(e.error_code), search_path]
2020+
2021+
raise e
2022+
end
2023+
end
2024+
2025+
end
2026+
2027+
files.uniq
19222028
end
19232029

19242030
# Creates a new directory on the mounted tree
@@ -1931,31 +2037,27 @@ def create_directory(name)
19312037
# public read/write methods
19322038
attr_accessor :native_os, :native_lm, :encrypt_passwords, :extended_security, :read_timeout, :evasion_opts
19332039
attr_accessor :verify_signature, :use_ntlmv2, :usentlm2_session, :send_lm, :use_lanman_key, :send_ntlm
1934-
attr_accessor :system_time, :system_zone
1935-
#misc
1936-
attr_accessor :spnopt # used for SPN
2040+
attr_accessor :system_time, :system_zone
2041+
attr_accessor :spnopt
19372042

19382043
# public read methods
19392044
attr_reader :dialect, :session_id, :challenge_key, :peer_native_lm, :peer_native_os
19402045
attr_reader :default_domain, :default_name, :auth_user, :auth_user_id
19412046
attr_reader :multiplex_id, :last_tree_id, :last_file_id, :process_id, :last_search_id
19422047
attr_reader :dns_host_name, :dns_domain_name
19432048
attr_reader :security_mode, :server_guid
1944-
#signing related
19452049
attr_reader :sequence_counter,:signing_key, :require_signing
19462050

1947-
# private methods
2051+
# private write methods
19482052
attr_writer :dialect, :session_id, :challenge_key, :peer_native_lm, :peer_native_os
19492053
attr_writer :default_domain, :default_name, :auth_user, :auth_user_id
19502054
attr_writer :dns_host_name, :dns_domain_name
19512055
attr_writer :multiplex_id, :last_tree_id, :last_file_id, :process_id, :last_search_id
19522056
attr_writer :security_mode, :server_guid
1953-
#signing related
19542057
attr_writer :sequence_counter,:signing_key, :require_signing
19552058

19562059
attr_accessor :socket
19572060

1958-
19592061
end
19602062
end
19612063
end

0 commit comments

Comments
 (0)