Skip to content

Commit 310ab9c

Browse files
committed
Land rapid7#9573, fixes for bind_named_pipe
2 parents e48f538 + b3f26ea commit 310ab9c

File tree

3 files changed

+69
-34
lines changed

3 files changed

+69
-34
lines changed

lib/msf/core/handler/bind_named_pipe.rb

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
#
77
# KNOWN ISSUES
88
#
9-
# 1) Interactive channels (ie shell) do not work well/at all. Neither do pivots. Issue
10-
# seems to be multiple threads writing to the pipe or some writes not being synchronized
11-
# and bypassing the OpenPipeSock methods.
12-
# 2) A peek named pipe operation is carried out before every read to prevent blocking. This
13-
# generates extra traffic.
9+
# 1) A peek named pipe operation is carried out before every read to prevent blocking. This
10+
# generates extra traffic. SMB echo requests are also generated to force the packet
11+
# dispatcher to perform a read.
1412
#
1513

1614
#
@@ -22,22 +20,27 @@
2220
# an issue when there are multiple writes since it will cause select to return which
2321
# triggers a read, but there is nothing to read since the pipe will already have read
2422
# the response. This read will then hold the mutex while the socket read waits to timeout.
23+
# A peek operation on the pipe fixes this.
2524
#
2625
class OpenPipeSock < Rex::Proto::SMB::SimpleClient::OpenPipe
27-
attr_accessor :mutex, :chunk_size, :last_comm, :write_queue, :write_thread, :read_buff
26+
attr_accessor :mutex, :last_comm, :write_queue, :write_thread, :read_buff, :echo_thread, :server_max_buffer_size
2827

29-
def initialize(*args)
28+
STATUS_BUFFER_OVERFLOW = 0x80000005
29+
30+
def initialize(*args, server_max_buffer_size:)
3031
super(*args)
3132
self.client = args[0]
3233
self.mutex = Mutex.new # synchronize read/writes
3334
self.last_comm = Time.now # last successfull read/write
3435
self.write_queue = Queue.new # queue message to send
3536
self.write_thread = Thread.new { dispatcher }
37+
self.echo_thread = Thread.new { force_read }
3638
self.read_buff = ''
39+
self.server_max_buffer_size = server_max_buffer_size
40+
self.chunk_size = server_max_buffer_size - 260
3741
end
3842

39-
# Check if there are any bytes to read and return number available.
40-
# Access must be synchronized.
43+
# Check if there are any bytes to read and return number available. Access must be synchronized.
4144
def peek_named_pipe
4245
# 0x23 is the PeekNamedPipe operation. Last 16 bits is our pipes file id (FID).
4346
setup = [0x23, self.file_id].pack('vv')
@@ -46,11 +49,34 @@ def peek_named_pipe
4649
avail = 0
4750
begin
4851
avail = pkt.to_s[pkt['Payload'].v['ParamOffset']+4, 2].unpack('v')[0]
52+
self.last_comm = Time.now
4953
rescue
5054
end
55+
56+
if (avail == 0) and (pkt['Payload']['SMB'].v['ErrorClass'] == STATUS_BUFFER_OVERFLOW)
57+
avail = self.client.default_max_buffer_size
58+
end
59+
5160
avail
5261
end
5362

63+
# Send echo request to force select() to return in the packet dispatcher and read from the socket.
64+
# This allows "channel -i" and "shell" to work.
65+
def force_read
66+
wait = 0.5 # smaller is faster but generates more traffic
67+
while true
68+
elapsed = Time.now - self.last_comm
69+
if elapsed > wait
70+
self.mutex.synchronize do
71+
self.client.echo()
72+
self.last_comm = Time.now
73+
end
74+
else
75+
Rex::ThreadSafe.sleep(wait-elapsed)
76+
end
77+
end
78+
end
79+
5480
# Runs as a thread and synchronizes writes. Allows write operations to return
5581
# immediately instead of waiting for the mutex.
5682
def dispatcher
@@ -74,8 +100,8 @@ def dispatcher
74100
def close
75101
# Give the meterpreter shutdown command a chance
76102
self.write_queue.close
77-
while self.write_queue.size > 0
78-
sleep(0.1)
103+
if self.write_queue.size > 0
104+
sleep(1.0)
79105
end
80106
self.write_thread.kill
81107

@@ -90,25 +116,32 @@ def close
90116
def read(count)
91117
data = ''
92118
begin
93-
self.mutex.synchronize do
94-
avail = peek_named_pipe
95-
if avail > 0
96-
while count > 0
97-
buff = super([count, self.chunk_size].min)
98-
self.last_comm = Time.now
99-
count -= buff.length
100-
data += buff
119+
if count > self.read_buff.length
120+
# need more data to satisfy request
121+
self.mutex.synchronize do
122+
avail = peek_named_pipe
123+
if avail > 0
124+
left = [count-self.read_buff.length, avail].max
125+
while left > 0
126+
buff = super([left, self.chunk_size].min)
127+
self.last_comm = Time.now
128+
left -= buff.length
129+
self.read_buff += buff
130+
end
101131
end
102132
end
103133
end
104134
rescue
105135
end
106136

137+
data = self.read_buff[0, [count, self.read_buff.length].min]
138+
self.read_buff = self.read_buff[data.length..-1]
139+
107140
if data.length == 0
108141
# avoid full throttle polling
109-
Rex::ThreadSafe.sleep(0.1)
142+
Rex::ThreadSafe.sleep(0.2)
110143
end
111-
return data
144+
data
112145
end
113146

114147
def put (data)
@@ -157,7 +190,7 @@ def initialize(*args)
157190
def create_pipe(path)
158191
pkt = self.client.create_pipe(path, Rex::Proto::SMB::Constants::CREATE_ACCESS_EXIST)
159192
file_id = pkt['Payload'].v['FileID']
160-
self.pipe = OpenPipeSock.new(self.client, path, self.client.last_tree_id, file_id)
193+
self.pipe = OpenPipeSock.new(self.client, path, self.client.last_tree_id, file_id, server_max_buffer_size: self.server_max_buffer_size)
161194
end
162195
end
163196

@@ -202,7 +235,6 @@ def initialize(info={})
202235
register_advanced_options(
203236
[
204237
OptString.new('SMBDirect', [true, 'The target port is a raw SMB service (not NetBIOS)', true]),
205-
OptInt.new('CHUNKSIZE', [false, 'Max pipe read/write size per request', 4096]) # > 4096 is unreliable
206238
], Msf::Handler::BindNamedPipe)
207239

208240
self.conn_threads = []
@@ -236,7 +268,6 @@ def start_handler
236268
smbdomain = datastore['SMBDomain']
237269
smbdirect = datastore['SMBDirect']
238270
smbshare = "\\\\#{rhost}\\IPC$"
239-
chunk_size = datastore['CHUNKSIZE']
240271

241272
# Ignore this if one of the required options is missing
242273
return if not rhost
@@ -304,7 +335,6 @@ def start_handler
304335
return
305336
end
306337

307-
pipe.chunk_size = chunk_size
308338
vprint_status("Opened pipe \\#{pipe_name}")
309339

310340
# Increment the has connection counter

lib/msf/core/payload/windows/x64/bind_named_pipe.rb

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ module Payload::Windows::BindNamedPipe_x64
2626
#
2727
def initialize(*args)
2828
super
29-
register_advanced_options(Msf::Opt::stager_retry_options)
29+
register_advanced_options(
30+
[
31+
OptInt.new('WAIT_TIMEOUT', [false, 'Seconds pipe will wait for a connection', 10])
32+
]
33+
)
3034
end
3135

3236
#
@@ -36,8 +40,7 @@ def generate
3640
conf = {
3741
name: datastore['PIPENAME'],
3842
host: datastore['PIPEHOST'],
39-
retry_count: datastore['StagerRetryCount'],
40-
retry_wait: datastore['StagerRetryWait'],
43+
timeout: datastore['WAIT_TIMEOUT'],
4144
reliable: false,
4245
}
4346

@@ -136,14 +139,14 @@ def asm_send_uuid(uuid=nil)
136139
# @option opts [String] :exitfunk The exit method to use if there is an error, one of process, thread, or seh
137140
# @option opts [Bool] :reliable Whether or not to enable error handling code
138141
# @option opts [String] :name Pipe name to create
139-
# @option opts [Integer] :retry_count The number of times to retry a failed request before giving up
140-
# @option opts [Integer] :retry_wait The seconds to wait before retry a new request
142+
# @option opts [Int] :timeout Seconds to wait for pipe connection
141143
#
142144
def asm_bind_named_pipe(opts={})
143145

144146
reliable = opts[:reliable]
145-
retry_count = [opts[:retry_count].to_i, 1].max
146-
retry_wait = [opts[:retry_wait].to_i, 1].max * 1000 # kernel32!Sleep takes millisecs
147+
timeout = opts[:timeout] * 1000 # convert to millisecs
148+
retry_wait = 500
149+
retry_count = timeout / retry_wait
147150
full_pipe_name = "\\\\\\\\.\\\\pipe\\\\#{opts[:name]}" # double escape -> \\.\pipe\name
148151
chunk_size = 0x10000 # pipe buffer size
149152
cleanup_funk = reliable ? 'cleanup_file' : 'failure'

lib/rex/proto/smb/simpleclient.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class SimpleClient
2323
EVADE = Rex::Proto::SMB::Evasions
2424

2525
# Public accessors
26-
attr_accessor :last_error
26+
attr_accessor :last_error, :server_max_buffer_size
2727

2828
# Private accessors
2929
attr_accessor :socket, :client, :direct, :shares, :last_share
@@ -34,6 +34,7 @@ def initialize(socket, direct = false)
3434
self.direct = direct
3535
self.client = Rex::Proto::SMB::Client.new(socket)
3636
self.shares = { }
37+
self.server_max_buffer_size = 1024 # 4356 (workstation) or 16644 (server) expected
3738
end
3839

3940
def login(name = '', user = '', pass = '', domain = '',
@@ -55,7 +56,8 @@ def login(name = '', user = '', pass = '', domain = '',
5556
self.client.use_lanman_key = use_lanman_key
5657
self.client.send_ntlm = send_ntlm
5758

58-
self.client.negotiate
59+
ok = self.client.negotiate
60+
self.server_max_buffer_size = ok['Payload'].v['MaxBuff']
5961

6062
# Disable NTLMv2 Session for Windows 2000 (breaks authentication on some systems)
6163
# XXX: This in turn breaks SMB auth for Windows 2000 configured to enforce NTLMv2

0 commit comments

Comments
 (0)