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

Multiple fixes and query_service_status example script #162

Merged
merged 5 commits into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions examples/query_service_status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/ruby

# This example script is used for testing remote service status and start type query.
# It will attempt to connect to a host and query the status and start type of the provided service.
# Example usage: ruby query_service_status.rb 192.168.172.138 msfadmin msfadmin "RemoteRegistry"
# This will try to connect to \\192.168.172.138 with the msfadmin:msfadmin credentialas and get the status and start type of the "RemoteRegistry" service.

require 'bundler/setup'
require 'ruby_smb'

address = ARGV[0]
username = ARGV[1]
password = ARGV[2]
service = ARGV[3]
smb_versions = ARGV[4]&.split(',') || ['1','2','3']

sock = TCPSocket.new address, 445
dispatcher = RubySMB::Dispatcher::Socket.new(sock, read_timeout: 60)

client = RubySMB::Client.new(dispatcher, smb1: smb_versions.include?('1'), smb2: smb_versions.include?('2'), smb3: smb_versions.include?('3'), username: username, password: password)
protocol = client.negotiate
status = client.authenticate

puts "#{protocol} : #{status}"

tree = client.tree_connect("\\\\#{address}\\IPC$")
svcctl = tree.open_file(filename: 'svcctl', write: true, read: true)

puts('Binding to \\svcctl...')
svcctl.bind(endpoint: RubySMB::Dcerpc::Svcctl)
puts('Bound to \\svcctl')

puts('Opening Service Control Manager')
scm_handle = svcctl.open_sc_manager_w(address)

svc_handle = svcctl.open_service_w(scm_handle, service)
svc_status = svcctl.query_service_status(svc_handle)

case svc_status.dw_current_state
when RubySMB::Dcerpc::Svcctl::SERVICE_RUNNING
puts("Service #{service} is running")
when RubySMB::Dcerpc::Svcctl::SERVICE_STOPPED
puts("Service #{service} is in stopped state")
end

svc_config = svcctl.query_service_config(svc_handle)
case svc_config.dw_start_type
when RubySMB::Dcerpc::Svcctl::SERVICE_DISABLED
puts("Service #{service} is disabled")
when RubySMB::Dcerpc::Svcctl::SERVICE_BOOT_START, RubySMB::Dcerpc::Svcctl::SERVICE_SYSTEM_START
puts("Service #{service} starts when the system boots up (driver)")
when RubySMB::Dcerpc::Svcctl::SERVICE_AUTO_START
puts("Service #{service} starts automatically during system startup")
when RubySMB::Dcerpc::Svcctl::SERVICE_DEMAND_START
puts("Service #{service} starts manually")
end

if svcctl
svcctl.close_service_handle(svc_handle) if svc_handle
svcctl.close_service_handle(scm_handle) if scm_handle
svcctl.close
end
client.disconnect!

115 changes: 72 additions & 43 deletions lib/ruby_smb/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,16 @@ class Client

# The UID set in SMB1
# @!attribute [rw] user_id
# @return [String]
# @return [Integer]
attr_accessor :user_id

# The Process ID set in SMB1
# It is randomly generated during the client initialization, but can
# be modified using the accessor.
# @!attribute [rw] pid
# @return [Integer]
attr_accessor :pid

# The maximum size SMB message that the Client accepts (in bytes)
# The default value is equal to {MAX_BUFFER_SIZE}.
# @!attribute [rw] max_buffer_size
Expand Down Expand Up @@ -258,6 +265,14 @@ class Client
# @return [Integer] the negotiated SMB version
attr_accessor :negotiated_smb_version

# Whether or not the server supports multi-credit operations. It is
# reported by the LARGE_MTU capabiliy as part of the negotiation process
# (SMB 2.x and 3.x).
# @!attribute [rw] server_supports_multi_credit
# @return [Boolean] true if the server supports multi-credit operations,
# false otherwise
attr_accessor :server_supports_multi_credit

# @param dispatcher [RubySMB::Dispatcher::Socket] the packet dispatcher to use
# @param smb1 [Boolean] whether or not to enable SMB1 support
# @param smb2 [Boolean] whether or not to enable SMB2 support
Expand All @@ -268,6 +283,7 @@ def initialize(dispatcher, smb1: true, smb2: true, smb3: true, username:, passwo
raise ArgumentError, 'You must enable at least one Protocol'
end
@dispatcher = dispatcher
@pid = rand(0xFFFF)
@domain = domain
@local_workstation = local_workstation
@password = password.encode('utf-8') || ''.encode('utf-8')
Expand All @@ -285,6 +301,7 @@ def initialize(dispatcher, smb1: true, smb2: true, smb3: true, username:, passwo
@server_max_read_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
@server_max_write_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
@server_max_transact_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
@server_supports_multi_credit = false

# SMB 3.x options
@session_encrypt_data = always_encrypt
Expand Down Expand Up @@ -344,7 +361,7 @@ def echo(count: 1, data: '')
# @param packet [RubySMB::GenericPacket] the packet to set the message id for
# @return [RubySMB::GenericPacket] the modified packet
def increment_smb_message_id(packet)
packet.smb2_header.message_id = smb2_message_id
packet.smb2_header.message_id = self.smb2_message_id
self.smb2_message_id += 1
packet
end
Expand Down Expand Up @@ -427,7 +444,8 @@ def send_recv(packet, encrypt: false)
version = packet.packet_smb_version
case version
when 'SMB1'
packet.smb_header.uid = user_id if user_id
packet.smb_header.uid = self.user_id if self.user_id
packet.smb_header.pid_low = self.pid if self.pid
packet = smb1_sign(packet)
when 'SMB2'
packet = increment_smb_message_id(packet)
Expand All @@ -443,35 +461,32 @@ def send_recv(packet, encrypt: false)
packet = packet
end

encrypt_data = false
if can_be_encrypted?(packet) && encryption_supported? && (@session_encrypt_data || encrypt)
send_encrypt(packet)
raw_response = recv_encrypt
loop do
break unless is_status_pending?(raw_response)
sleep 1
raw_response = recv_encrypt
end
else
dispatcher.send_packet(packet)
raw_response = dispatcher.recv_packet
loop do
break unless is_status_pending?(raw_response)
sleep 1
raw_response = dispatcher.recv_packet
end unless version == 'SMB1'
encrypt_data = true
end
send_packet(packet, encrypt: encrypt_data)
raw_response = recv_packet(encrypt: encrypt_data)
smb2_header = nil
loop do
smb2_header = RubySMB::SMB2::SMB2Header.read(raw_response)
break unless is_status_pending?(smb2_header)
sleep 1
raw_response = recv_packet(encrypt: encrypt_data)
end unless version == 'SMB1'

self.sequence_counter += 1 if signing_required && !session_key.empty?
# update the SMB2 message ID according to the received Credit Charged
self.smb2_message_id += smb2_header.credit_charge - 1 if smb2_header && self.dialect != '0x0202'
raw_response
end

# Check if the response is an asynchronous operation with STATUS_PENDING
# status code.
#
# @param raw_response [String] the raw response packet
# @param smb2_header [String] the response packet SMB2 header
# @return [Boolean] true if it is a status pending operation, false otherwise
def is_status_pending?(raw_response)
smb2_header = RubySMB::SMB2::SMB2Header.read(raw_response)
def is_status_pending?(smb2_header)
value = smb2_header.nt_status.value
status_code = WindowsError::NTStatus.find_by_retval(value).first
status_code == WindowsError::NTStatus::STATUS_PENDING &&
Expand All @@ -497,37 +512,50 @@ def encryption_supported?
['0x0300', '0x0302', '0x0311'].include?(@dialect)
end

# Encrypt and send a packet
def send_encrypt(packet)
begin
transform_request = smb3_encrypt(packet.to_binary_s)
rescue RubySMB::Error::RubySMBError => e
raise RubySMB::Error::EncryptionError, "Error while encrypting #{packet.class.name} packet (SMB #{@dialect}): #{e}"
# Encrypt (if required) and send a packet.
#
# @param encrypt [Boolean] true if the packet should be encrypted, false
# otherwise
def send_packet(packet, encrypt: false)
if encrypt
begin
packet = smb3_encrypt(packet.to_binary_s)
rescue RubySMB::Error::RubySMBError => e
raise RubySMB::Error::EncryptionError, "Error while encrypting #{packet.class.name} packet (SMB #{@dialect}): #{e}"
end
end
dispatcher.send_packet(transform_request)
dispatcher.send_packet(packet)
end

# Receives the raw response through the Dispatcher and decrypt the packet.
# Receives the raw response through the Dispatcher and decrypt the packet (if required).
#
# @param encrypt [Boolean] true if the packet is encrypted, false otherwise
# @return [String] the raw unencrypted packet
def recv_encrypt
def recv_packet(encrypt: false)
begin
raw_response = dispatcher.recv_packet
rescue RubySMB::Error::CommunicationError => e
raise RubySMB::Error::EncryptionError, "Communication error with the "\
"remote host: #{e.message}. The server supports encryption but was "\
"not able to handle the encrypted request."
end
begin
transform_response = RubySMB::SMB2::Packet::TransformHeader.read(raw_response)
rescue IOError
raise RubySMB::Error::InvalidPacket, 'Not a SMB2 TransformHeader packet'
if encrypt
raise RubySMB::Error::EncryptionError, "Communication error with the "\
"remote host: #{e.message}. The server supports encryption but was "\
"not able to handle the encrypted request."
else
raise e
end
end
begin
smb3_decrypt(transform_response)
rescue RubySMB::Error::RubySMBError => e
raise RubySMB::Error::EncryptionError, "Error while decrypting #{transform_response.class.name} packet (SMB #@dialect}): #{e}"
if encrypt
begin
transform_response = RubySMB::SMB2::Packet::TransformHeader.read(raw_response)
rescue IOError
raise RubySMB::Error::InvalidPacket, 'Not a SMB2 TransformHeader packet'
end
begin
raw_response = smb3_decrypt(transform_response)
rescue RubySMB::Error::RubySMBError => e
raise RubySMB::Error::EncryptionError, "Error while decrypting #{transform_response.class.name} packet (SMB #@dialect}): #{e}"
end
end
raw_response
end

# Connects to the supplied share
Expand Down Expand Up @@ -568,6 +596,7 @@ def wipe_state!
self.smb2_message_id = 0
self.client_encryption_key = nil
self.server_encryption_key = nil
self.server_supports_multi_credit = false
end

# Requests a NetBIOS Session Service using the provided name.
Expand Down Expand Up @@ -605,7 +634,7 @@ def session_request_packet(name = '*SMBSERVER')
session_request.session_header.session_packet_type = RubySMB::Nbss::SESSION_REQUEST
session_request.called_name = called_name
session_request.calling_name = calling_name
session_request.session_header.packet_length =
session_request.session_header.stream_protocol_length =
session_request.num_bytes - session_request.session_header.num_bytes
session_request
end
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_smb/client/negotiation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def parse_negotiate_response(packet)
self.server_guid = packet.server_guid
self.server_start_time = packet.server_start_time.to_time if packet.server_start_time != 0
self.server_system_time = packet.system_time.to_time if packet.system_time != 0
self.server_supports_multi_credit = self.dialect != '0x0202' && packet&.capabilities&.large_mtu == 1
case self.dialect
when '0x02ff'
when '0x0300', '0x0302'
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_smb/dispatcher/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Base
def nbss(packet)
nbss = RubySMB::Nbss::SessionHeader.new
nbss.session_packet_type = RubySMB::Nbss::SESSION_MESSAGE
nbss.packet_length = packet.do_num_bytes
nbss.stream_protocol_length = packet.do_num_bytes
nbss.to_binary_s
end

Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_smb/dispatcher/socket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def recv_packet(full_response: false)
raise ::RubySMB::Error::NetBiosSessionService, 'NBSS Header is missing'
end

length = nbss_header.packet_length
length = nbss_header.stream_protocol_length
data = full_response ? nbss_header.to_binary_s : ''
if length > 0
if IO.select([@tcp_socket], nil, nil, @read_timeout).nil?
Expand Down
8 changes: 4 additions & 4 deletions lib/ruby_smb/nbss/session_header.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
module RubySMB
module Nbss
# Representation of the NetBIOS Session Service Header as defined in
# [4.3.1 GENERAL FORMAT OF SESSION PACKETS](https://tools.ietf.org/html/rfc1002)
# SMB: [2.1 Transport](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb/f906c680-330c-43ae-9a71-f854e24aeee6)
# SMB2: [2.1 Transport](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/1dfacde4-b5c7-4494-8a14-a09d3ab4cc83)
class SessionHeader < BinData::Record
endian :big

uint8 :session_packet_type, label: 'Session Packet Type'
bit7 :flags, label: 'Flags', initial_value: 0
bit17 :packet_length, label: 'Packet Length'
uint8 :session_packet_type, label: 'Session Packet Type', initial_value: 0
uint24 :stream_protocol_length, label: 'Stream Protocol Length'
end
end
end
12 changes: 2 additions & 10 deletions lib/ruby_smb/smb1/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,7 @@ def close
# @raise [RubySMB::Error::InvalidPacket] if the response packet is not valid
# @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
def read(bytes: @size, offset: 0)
atomic_read_size = if bytes > @tree.client.max_buffer_size
@tree.client.max_buffer_size
else
bytes
end
atomic_read_size = [bytes, @tree.client.max_buffer_size].min
remaining_bytes = bytes
data = ''

Expand Down Expand Up @@ -227,11 +223,7 @@ def write(data:, offset: 0)
total_bytes_written = 0

loop do
atomic_write_size = if bytes > @tree.client.max_buffer_size
@tree.client.max_buffer_size
else
bytes
end
atomic_write_size = [bytes, @tree.client.max_buffer_size].min
write_request = write_packet(data: buffer.slice!(0, atomic_write_size), offset: offset)
raw_response = @tree.client.send_recv(write_request)
response = RubySMB::SMB1::Packet::WriteAndxResponse.read(raw_response)
Expand Down
Loading