Skip to content

Commit d05b85d

Browse files
committed
Land rapid7#18680, Shared SMB Service
Merge branch 'land-18680' into upstream-master
2 parents 7a7c7eb + 97a3e02 commit d05b85d

File tree

6 files changed

+156
-89
lines changed

6 files changed

+156
-89
lines changed

Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ group :test do
5252
# Manipulate Time.now in specs
5353
gem 'timecop'
5454
end
55+

Gemfile.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ GEM
474474
ruby-progressbar (1.13.0)
475475
ruby-rc4 (0.1.5)
476476
ruby2_keywords (0.0.5)
477-
ruby_smb (3.3.1)
477+
ruby_smb (3.3.2)
478478
bindata
479479
openssl-ccm
480480
openssl-cmac

lib/msf/core/exploit/remote/smb/server.rb

+11-69
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,6 @@ module Server
77
include ::Msf::Exploit::Remote::SocketServer
88
include ::Msf::Exploit::Remote::SMB::LogAdapter
99

10-
module ServiceMixin
11-
def start
12-
if Rex::Socket.is_ipv6?(@socket.localhost)
13-
localinfo = "[#{@socket.localhost}]:#{@socket.localport}"
14-
else
15-
localinfo = "#{@socket.localhost}:#{@socket.localport}"
16-
end
17-
18-
self.listener_thread = Rex::ThreadFactory.spawn("SMBServerListener(#{localinfo})", false) do
19-
begin
20-
run do |server_client|
21-
on_client_connect_proc.call(server_client) if on_client_connect_proc
22-
true
23-
end
24-
rescue IOError => e
25-
# this 'IOError: stream closed in another thread' is expected, so disregard it
26-
wlog("#{e.class}: #{e.message}")
27-
end
28-
end
29-
end
30-
31-
def stop
32-
@socket.close
33-
end
34-
35-
def wait
36-
listener_thread.join if listener_thread
37-
end
38-
39-
attr_accessor :listener_thread, :on_client_connect_proc
40-
end
41-
4210
def initialize(info = {})
4311
super
4412

@@ -49,60 +17,34 @@ def initialize(info = {})
4917
end
5018

5119
def start_service(opts = {})
52-
@rsock = Rex::Socket::Tcp.create(
53-
'LocalHost' => bindhost,
54-
'LocalPort' => bindport,
55-
'Comm' => _determine_server_comm(bindhost),
56-
'Server' => true,
57-
'Context' =>
58-
{
59-
'Msf' => framework,
60-
'MsfExploit' => self
61-
}
62-
)
63-
6420
unless opts[:logger]
6521
log_device = LogAdapter::LogDevice::Framework.new(framework)
6622
opts[:logger] = LogAdapter::Logger.new(self, log_device)
6723
end
6824

69-
thread_factory = Proc.new do |server_client, &block|
70-
Rex::ThreadFactory.spawn("SMBServerClient(#{server_client.peerhost}->#{server_client.dispatcher.tcp_socket.localhost})", false, &block)
71-
end
72-
73-
server = RubySMB::Server.new(
74-
server_sock: @rsock,
25+
self.service = Rex::ServiceManager.start(
26+
Rex::Proto::SMB::Server,
27+
(opts['ServerPort'] || bindport).to_i,
28+
opts['ServerHost'] || bindhost,
29+
{
30+
'Msf' => framework,
31+
'MsfExploit' => self,
32+
},
33+
opts['Comm'] || _determine_server_comm(opts['ServerHost'] || bindhost),
7534
gss_provider: opts[:gss_provider],
76-
logger: opts[:logger],
77-
thread_factory: thread_factory
35+
logger: opts[:logger]
7836
)
7937

80-
server.extend(ServiceMixin)
81-
server.on_client_connect_proc = Proc.new { |client|
38+
service.on_client_connect_proc = Proc.new { |client|
8239
on_client_connect(client)
8340
}
84-
self.service = server
85-
self.service.start
8641

8742
print_status("Server is running. Listening on #{bindhost}:#{bindport}")
8843
end
8944

9045
def on_client_connect(client)
9146
vprint_status("Received SMB connection from #{client.peerhost}")
9247
end
93-
94-
def cleanup_service
95-
if service
96-
begin
97-
self.service.stop
98-
self.service.wait
99-
true
100-
rescue ::Exception => e
101-
print_error(e.message)
102-
false
103-
end
104-
end
105-
end
10648
end
10749
end
10850
end

lib/msf/core/exploit/remote/smb/server/share.rb

+22-10
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ def initialize(info = {})
2828
register_options(
2929
[
3030
OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 445 ]),
31-
OptString.new('SHARE', [ false, 'Share (Default Random)']),
32-
OptString.new('FILE_NAME', [ false, 'File name to share (Default Random)']),
33-
OptString.new('FOLDER_NAME', [ false, 'Folder name to share (Default none)'])
31+
OptString.new('SHARE', [ false, 'Share (Default: random); cannot contain spaces or slashes'], regex: /^[^\s\/\\]*$/),
32+
OptString.new('FILE_NAME', [ false, 'File name to share (Default: random)']),
33+
OptString.new('FOLDER_NAME', [ false, 'Folder name to share (Default: none)'])
3434
], Msf::Exploit::Remote::SMB::Server::Share)
3535
register_advanced_options(
3636
[
@@ -58,21 +58,27 @@ def start_service(opts = {})
5858

5959
super(opts)
6060

61-
virtual_disk = RubySMB::Server::Share::Provider::VirtualDisk.new(@share)
62-
# the virtual disk expects the path to use the native File::SEPARATOR so normalize on that here
63-
virtual_disk.add_dynamic_file("#{@folder_name}#{File::SEPARATOR}#{@file_name}".gsub(/\/|\\/, File::SEPARATOR)) do |client, _smb_session|
64-
get_file_contents(client: client)
61+
if share.present?
62+
if service.shares.key?(share)
63+
fail_with(Msf::Module::Failure::BadConfig, "The specified SMB share '#{share}' already exists.")
64+
end
65+
66+
virtual_disk = RubySMB::Server::Share::Provider::VirtualDisk.new(share)
67+
# the virtual disk expects the path to use the native File::SEPARATOR so normalize on that here
68+
virtual_disk.add_dynamic_file("#{@folder_name}#{File::SEPARATOR}#{@file_name}".gsub(/\/|\\/, File::SEPARATOR)) do |client, _smb_session|
69+
get_file_contents(client: client)
70+
end
71+
service.add_share(virtual_disk)
6572
end
66-
service.add_share(virtual_disk)
6773
end
6874

6975
# Setups the server configuration.
7076
def setup
7177
super
7278

7379
self.folder_name = datastore['FOLDER_NAME']
74-
self.share = datastore['SHARE'] || Rex::Text.rand_text_alpha(4 + rand(3))
75-
self.file_name = datastore['FILE_NAME'] || Rex::Text.rand_text_alpha(4 + rand(3))
80+
self.share = datastore['SHARE'].present? ? datastore['SHARE'] : Rex::Text.rand_text_alpha(4 + rand(3))
81+
self.file_name = datastore['FILE_NAME'].present? ? datastore['FILE_NAME'] : Rex::Text.rand_text_alpha(4 + rand(3))
7682
end
7783

7884
# Builds the UNC Name for the shared file
@@ -92,6 +98,12 @@ def unc
9298
def get_file_contents(client:)
9399
file_contents
94100
end
101+
102+
def cleanup
103+
self.service.remove_share(share) if share.present?
104+
105+
super
106+
end
95107
end
96108
end
97109
end

lib/rex/proto/smb/server.rb

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# -*- coding: binary -*-
2+
require 'forwardable'
3+
require 'rex/socket'
4+
5+
6+
module Rex
7+
module Proto
8+
module SMB
9+
10+
###
11+
#
12+
# Acts as an SMB server, processing requests and dispatching them to
13+
# registered procs.
14+
#
15+
###
16+
class Server
17+
18+
include Proto
19+
extend Forwardable
20+
21+
def_delegators :@rubysmb_server, :dialects, :guid, :shares, :add_share, :remove_share
22+
23+
def initialize(port = 445, listen_host = '0.0.0.0', context = {}, comm = nil, gss_provider: nil, logger: nil)
24+
self.listen_host = listen_host
25+
self.listen_port = port
26+
self.context = context
27+
self.comm = comm
28+
@gss_provider = gss_provider
29+
@logger = logger
30+
self.listener = nil
31+
@listener_thread = nil
32+
@rubysmb_server = nil
33+
end
34+
35+
# @return [String]
36+
def inspect
37+
"#<#{self.class} smb://#{listen_host}:#{listen_port} >"
38+
end
39+
40+
#
41+
# Returns the hardcore alias for the SMB service
42+
#
43+
def self.hardcore_alias(*args, **kwargs)
44+
gss_alias = ''
45+
if (gss_provider = kwargs[:gss_provider])
46+
gss_alias << "#{gss_provider.class}("
47+
attrs = {}
48+
if gss_provider.is_a?(RubySMB::Gss::Provider::NTLM)
49+
allows = []
50+
allows << 'ANONYMOUS' if gss_provider.allow_anonymous
51+
allows << 'GUESTS' if gss_provider.allow_guests
52+
attrs['allow'] = allows.join('|') unless allows.empty?
53+
attrs['default_domain'] = gss_provider.default_domain if gss_provider.respond_to?(:default_domain) && gss_provider.default_domain.present?
54+
attrs['ntlm_status'] = gss_provider.ntlm_type3_status.name if gss_provider.respond_to?(:ntlm_type3_status) && gss_provider.ntlm_type3_status.present?
55+
end
56+
gss_alias << attrs.map { |k,v| "#{k}=#{v}"}.join(', ')
57+
gss_alias << ')'
58+
end
59+
"#{(args[0] || '')}-#{(args[1] || '')}-#{args[3] || ''}-#{gss_alias}"
60+
end
61+
62+
def alias
63+
super || "SMB Server"
64+
end
65+
66+
def start
67+
self.listener = Rex::Socket::TcpServer.create(
68+
'LocalHost' => self.listen_host,
69+
'LocalPort' => self.listen_port,
70+
'Context' => self.context,
71+
'Comm' => self.comm
72+
)
73+
74+
thread_factory = Proc.new do |server_client, &block|
75+
Rex::ThreadFactory.spawn("SMBServerClient(#{server_client.peerhost}->#{server_client.dispatcher.tcp_socket.localhost})", false, &block)
76+
end
77+
78+
@rubysmb_server = RubySMB::Server.new(
79+
server_sock: self.listener,
80+
gss_provider: @gss_provider,
81+
logger: @logger,
82+
thread_factory: thread_factory
83+
)
84+
85+
localinfo = Rex::Socket.to_authority(self.listener.localhost, self.listener.localport)
86+
@listener_thread = Rex::ThreadFactory.spawn("SMBServerListener(#{localinfo})", false) do
87+
begin
88+
@rubysmb_server.run do |server_client|
89+
on_client_connect_proc.call(server_client) if on_client_connect_proc
90+
true
91+
end
92+
rescue IOError => e
93+
# this 'IOError: stream closed in another thread' is expected, so disregard it
94+
wlog("#{e.class}: #{e.message}")
95+
end
96+
end
97+
end
98+
99+
def stop
100+
self.listener.close
101+
end
102+
103+
def wait
104+
@listener_thread.join if @listener_thread
105+
end
106+
107+
attr_accessor :context, :comm, :listener, :listen_host, :listen_port, :on_client_connect_proc
108+
end
109+
110+
end
111+
end
112+
end

lib/rex/service_manager.rb

+9-9
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ class ServiceManager < Hash
2020
#
2121
# Calls the instance method to start a service.
2222
#
23-
def self.start(klass, *args)
24-
self.instance.start(klass, *args)
23+
def self.start(klass, *args, **kwargs)
24+
self.instance.start(klass, *args, **kwargs)
2525
end
2626

2727
#
2828
# Calls the instance method to stop a service.
2929
#
30-
def self.stop(klass, *args)
31-
self.instance.stop(klass, *args)
30+
def self.stop(klass, *args, **kwargs)
31+
self.instance.stop(klass, *args, **kwargs)
3232
end
3333

3434
#
@@ -48,9 +48,9 @@ def self.stop_service(service)
4848
#
4949
# Starts a service and assigns it a unique name in the service hash.
5050
#
51-
def start(klass, *args)
51+
def start(klass, *args, **kwargs)
5252
# Get the hardcore alias.
53-
hals = "#{klass}" + klass.hardcore_alias(*args)
53+
hals = "#{klass}" + klass.hardcore_alias(*args, **kwargs)
5454

5555
# Has a service already been constructed for this guy? If so, increment
5656
# its reference count like it aint no thang.
@@ -59,7 +59,7 @@ def start(klass, *args)
5959
return inst
6060
end
6161

62-
inst = klass.new(*args)
62+
inst = klass.new(*args, **kwargs)
6363
als = inst.alias
6464

6565
# Find an alias that isn't taken.
@@ -93,8 +93,8 @@ def start(klass, *args)
9393
# what was originally passed to start exactly. If the reference count of
9494
# the service drops to zero the service will be destroyed.
9595
#
96-
def stop(klass, *args)
97-
stop_service(hals[hardcore_alias(klass, *args)])
96+
def stop(klass, *args, **kwargs)
97+
stop_service(hals[hardcore_alias(klass, *args, **kwargs)])
9898
end
9999

100100
#

0 commit comments

Comments
 (0)