ruby_smb 3.1.5 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/verify.yml +27 -4
- data/README.md +1 -2
- data/lib/ruby_smb/client/authentication.rb +4 -30
- data/lib/ruby_smb/dcerpc/client.rb +6 -261
- data/lib/ruby_smb/dcerpc/dfsnm/netr_dfs_add_std_root_request.rb +24 -0
- data/lib/ruby_smb/dcerpc/dfsnm/netr_dfs_add_std_root_response.rb +21 -0
- data/lib/ruby_smb/dcerpc/dfsnm/netr_dfs_remove_std_root_request.rb +23 -0
- data/lib/ruby_smb/dcerpc/dfsnm/netr_dfs_remove_std_root_response.rb +21 -0
- data/lib/ruby_smb/dcerpc/dfsnm.rb +84 -0
- data/lib/ruby_smb/dcerpc/error.rb +21 -0
- data/lib/ruby_smb/dcerpc/icpr/cert_server_request_request.rb +27 -0
- data/lib/ruby_smb/dcerpc/icpr/cert_server_request_response.rb +28 -0
- data/lib/ruby_smb/dcerpc/icpr.rb +84 -0
- data/lib/ruby_smb/dcerpc/ndr.rb +38 -2
- data/lib/ruby_smb/dcerpc/request.rb +10 -1
- data/lib/ruby_smb/dcerpc.rb +246 -12
- data/lib/ruby_smb/error.rb +25 -11
- data/lib/ruby_smb/peer_info.rb +39 -0
- data/lib/ruby_smb/signing.rb +2 -0
- data/lib/ruby_smb/smb1/pipe.rb +23 -1
- data/lib/ruby_smb/smb2/packet/tree_connect_response.rb +5 -1
- data/lib/ruby_smb/smb2/pipe.rb +23 -0
- data/lib/ruby_smb/version.rb +1 -1
- data/spec/lib/ruby_smb/dcerpc/dfsnm/netr_dfs_add_std_root_request_spec.rb +57 -0
- data/spec/lib/ruby_smb/dcerpc/dfsnm/netr_dfs_add_std_root_response_spec.rb +34 -0
- data/spec/lib/ruby_smb/dcerpc/dfsnm/netr_dfs_remove_std_root_request_spec.rb +49 -0
- data/spec/lib/ruby_smb/dcerpc/dfsnm/netr_dfs_remove_std_root_response_spec.rb +34 -0
- data/spec/lib/ruby_smb/dcerpc/icpr/cert_server_request_request_spec.rb +64 -0
- data/spec/lib/ruby_smb/dcerpc/icpr/cert_server_request_response_spec.rb +71 -0
- data/spec/lib/ruby_smb/dcerpc/icpr/cert_trans_blob_spec.rb +33 -0
- data/spec/lib/ruby_smb/dcerpc_spec.rb +2 -1
- data/spec/spec_helper.rb +6 -8
- data/spec/support/openssl.conf +14 -0
- data.tar.gz.sig +0 -0
- metadata +27 -2
- metadata.gz.sig +0 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'ruby_smb/dcerpc/ndr'
|
2
|
+
|
3
|
+
module RubySMB
|
4
|
+
module Dcerpc
|
5
|
+
module Icpr
|
6
|
+
|
7
|
+
# [3.2.4.1.1 CertServerRequest (Opnum 0)](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-icpr/0c6f150e-3ead-4006-b37f-ebbf9e2cf2e7)
|
8
|
+
class CertServerRequestResponse < BinData::Record
|
9
|
+
attr_reader :opnum
|
10
|
+
|
11
|
+
endian :little
|
12
|
+
|
13
|
+
ndr_uint32 :pdw_request_id
|
14
|
+
ndr_uint32 :pdw_disposition
|
15
|
+
cert_trans_blob :pctb_cert
|
16
|
+
cert_trans_blob :pctb_encoded_cert
|
17
|
+
cert_trans_blob :pctb_disposition_message
|
18
|
+
ndr_uint32 :error_status
|
19
|
+
|
20
|
+
def initialize_instance
|
21
|
+
super
|
22
|
+
@opnum = CERT_SERVER_REQUEST
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module RubySMB
|
2
|
+
module Dcerpc
|
3
|
+
module Icpr
|
4
|
+
|
5
|
+
# [3.2.4.1 ICertPassage Interface](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-icpr/d98e6cfb-87ba-4915-b3ec-a1b7c6129a53)
|
6
|
+
UUID = '91ae6020-9e3c-11cf-8d7c-00aa00c091be'
|
7
|
+
VER_MAJOR = 0
|
8
|
+
VER_MINOR = 0
|
9
|
+
|
10
|
+
# Operation numbers
|
11
|
+
CERT_SERVER_REQUEST = 0x0000
|
12
|
+
|
13
|
+
# Disposition constants, see
|
14
|
+
# [3.2.1.4.2.1 ICertRequestD::Request (Opnum 3)](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/dbb2e78f-7630-4615-92c4-6734fccfc5a6)
|
15
|
+
CR_DISP_ISSUED = 0x0003
|
16
|
+
CR_DISP_UNDER_SUBMISSION = 0x0005
|
17
|
+
|
18
|
+
# [2.2.2.2 CERTTRANSBLOB](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/d6bee093-d862-4122-8f2b-7b49102097dc)
|
19
|
+
# (actually defined in MS-WCCE)
|
20
|
+
class CertTransBlob < Ndr::NdrStruct
|
21
|
+
endian :little
|
22
|
+
default_parameter byte_align: 4
|
23
|
+
|
24
|
+
ndr_uint32 :cb, initial_value: -> { pb.length }
|
25
|
+
ndr_byte_conf_array_ptr :pb
|
26
|
+
|
27
|
+
def buffer
|
28
|
+
pb.to_ary.pack('C*')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
require 'ruby_smb/dcerpc/icpr/cert_server_request_request'
|
33
|
+
require 'ruby_smb/dcerpc/icpr/cert_server_request_response'
|
34
|
+
|
35
|
+
def cert_server_request(attributes:, authority:, csr:)
|
36
|
+
cert_server_request_request = CertServerRequestRequest.new(
|
37
|
+
pwsz_authority: authority,
|
38
|
+
pctb_attribs: { pb: (attributes.map { |k,v| "#{k}:#{v}" }.join("\n").encode('UTF-16le').force_encoding('ASCII-8bit') + "\x00\x00".b) },
|
39
|
+
pctb_request: { pb: csr.to_der }
|
40
|
+
)
|
41
|
+
|
42
|
+
response = dcerpc_request(
|
43
|
+
cert_server_request_request,
|
44
|
+
auth_level: RubySMB::Dcerpc::RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
45
|
+
auth_type: RubySMB::Dcerpc::RPC_C_AUTHN_WINNT
|
46
|
+
)
|
47
|
+
begin
|
48
|
+
cert_server_request_response = CertServerRequestResponse.read(response)
|
49
|
+
rescue IOError
|
50
|
+
raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading CertServerRequestResponse'
|
51
|
+
end
|
52
|
+
|
53
|
+
ret = {
|
54
|
+
disposition: cert_server_request_response.pdw_disposition.value,
|
55
|
+
disposition_message: cert_server_request_response.pctb_disposition_message.buffer.chomp("\x00\x00").force_encoding('utf-16le').encode,
|
56
|
+
status: {
|
57
|
+
CR_DISP_ISSUED => :issued,
|
58
|
+
CR_DISP_UNDER_SUBMISSION => :submitted,
|
59
|
+
}.fetch(cert_server_request_response.pdw_disposition.value, :error)
|
60
|
+
}
|
61
|
+
|
62
|
+
# note: error_status == RPC_S_BINDING_HAS_NO_AUTH when not properly bound
|
63
|
+
if ret[:status] == :error
|
64
|
+
unless cert_server_request_response.error_status == WindowsError::NTStatus::STATUS_SUCCESS
|
65
|
+
error_status = cert_server_request_response.error_status.value
|
66
|
+
status_code = WindowsError::Win32.find_by_retval(error_status).first
|
67
|
+
if status_code.nil? && (fault_name = RubySMB::Dcerpc::Fault::Status.name(error_status))
|
68
|
+
status_code = WindowsError::ErrorCode.new(fault_name.to_s, error_status, 'DCERPC fault')
|
69
|
+
end
|
70
|
+
raise RubySMB::Dcerpc::Error::IcprError.new(
|
71
|
+
"Error returned with cert_server_request: #{status_code || "0x#{error_status.to_s(16).rjust(8, '0')}"}",
|
72
|
+
status_code: status_code
|
73
|
+
)
|
74
|
+
end
|
75
|
+
else
|
76
|
+
ret[:certificate] = OpenSSL::X509::Certificate.new(cert_server_request_response.pctb_encoded_cert.buffer)
|
77
|
+
end
|
78
|
+
|
79
|
+
ret
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/ruby_smb/dcerpc/ndr.rb
CHANGED
@@ -1208,14 +1208,50 @@ module RubySMB::Dcerpc::Ndr
|
|
1208
1208
|
extend PointerClassPlugin
|
1209
1209
|
end
|
1210
1210
|
|
1211
|
+
|
1212
|
+
# ArrayPtr definitions:
|
1213
|
+
# If the type is Ndr*ArrayPtr, it's a ConfVarArray (Uni-dimensional Conformant-varying Array)
|
1214
|
+
# If the type is Ndr*ConfArrayPtr, it's a ConfArray (Uni-dimensional Conformant Array)
|
1211
1215
|
class NdrByteArrayPtr < NdrConfVarArray
|
1212
1216
|
default_parameters type: :ndr_uint8
|
1213
1217
|
extend PointerClassPlugin
|
1218
|
+
|
1219
|
+
def assign(val)
|
1220
|
+
val = val.bytes if val.is_a?(String)
|
1221
|
+
super(val.to_ary)
|
1222
|
+
end
|
1214
1223
|
end
|
1215
1224
|
|
1216
|
-
class
|
1217
|
-
default_parameters type: :
|
1225
|
+
class NdrByteConfArrayPtr < NdrConfArray
|
1226
|
+
default_parameters type: :ndr_uint8
|
1218
1227
|
extend PointerClassPlugin
|
1228
|
+
|
1229
|
+
def assign(val)
|
1230
|
+
val = val.bytes if val.is_a?(String)
|
1231
|
+
super(val.to_ary)
|
1232
|
+
end
|
1233
|
+
end
|
1234
|
+
|
1235
|
+
%i[ Uint8 Uint16 Uint32 Uint64 ].each do |klass|
|
1236
|
+
new_klass_name = "Ndr#{klass}ArrayPtr"
|
1237
|
+
unless self.const_defined?(new_klass_name)
|
1238
|
+
new_klass = Class.new(NdrConfVarArray) do
|
1239
|
+
default_parameters type: "ndr_#{klass}".downcase.to_sym
|
1240
|
+
extend PointerClassPlugin
|
1241
|
+
end
|
1242
|
+
self.const_set(new_klass_name, new_klass)
|
1243
|
+
BinData::RegisteredClasses.register(new_klass_name, new_klass)
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
new_klass_name = "Ndr#{klass}ConfArrayPtr"
|
1247
|
+
unless self.const_defined?(new_klass_name)
|
1248
|
+
new_klass = Class.new(NdrConfArray) do
|
1249
|
+
default_parameters type: "ndr_#{klass}".downcase.to_sym
|
1250
|
+
extend PointerClassPlugin
|
1251
|
+
end
|
1252
|
+
self.const_set(new_klass_name, new_klass)
|
1253
|
+
BinData::RegisteredClasses.register(new_klass_name, new_klass)
|
1254
|
+
end
|
1219
1255
|
end
|
1220
1256
|
|
1221
1257
|
class NdrFileTimePtr < NdrFileTime
|
@@ -90,7 +90,16 @@ module RubySMB
|
|
90
90
|
drs_domain_controller_info_request Drsr::DRS_DOMAIN_CONTROLLER_INFO
|
91
91
|
drs_crack_names_request Drsr::DRS_CRACK_NAMES
|
92
92
|
drs_get_nc_changes_request Drsr::DRS_GET_NC_CHANGES
|
93
|
-
string
|
93
|
+
string :default
|
94
|
+
end
|
95
|
+
choice 'Dfsnm', selection: -> { opnum } do
|
96
|
+
netr_dfs_add_std_root_request Dfsnm::NETR_DFS_ADD_STD_ROOT
|
97
|
+
netr_dfs_remove_std_root_request Dfsnm::NETR_DFS_REMOVE_STD_ROOT
|
98
|
+
string :default
|
99
|
+
end
|
100
|
+
choice 'Icpr', selection: -> { opnum } do
|
101
|
+
cert_server_request_request Icpr::CERT_SERVER_REQUEST
|
102
|
+
string :default
|
94
103
|
end
|
95
104
|
string :default
|
96
105
|
end
|
data/lib/ruby_smb/dcerpc.rb
CHANGED
@@ -46,6 +46,8 @@ module RubySMB
|
|
46
46
|
require 'ruby_smb/dcerpc/epm'
|
47
47
|
require 'ruby_smb/dcerpc/drsr'
|
48
48
|
require 'ruby_smb/dcerpc/sec_trailer'
|
49
|
+
require 'ruby_smb/dcerpc/dfsnm'
|
50
|
+
require 'ruby_smb/dcerpc/icpr'
|
49
51
|
require 'ruby_smb/dcerpc/request'
|
50
52
|
require 'ruby_smb/dcerpc/response'
|
51
53
|
require 'ruby_smb/dcerpc/rpc_auth3'
|
@@ -54,25 +56,50 @@ module RubySMB
|
|
54
56
|
require 'ruby_smb/dcerpc/print_system'
|
55
57
|
require 'ruby_smb/dcerpc/encrypting_file_system'
|
56
58
|
|
57
|
-
# Bind to the remote server interface endpoint.
|
59
|
+
# Bind to the remote server interface endpoint. It takes care of adding
|
60
|
+
# the necessary authentication verifier if `:auth_level` is set to
|
61
|
+
# anything different than RPC_C_AUTHN_LEVEL_NONE
|
58
62
|
#
|
59
|
-
# @param
|
63
|
+
# @param [Hash] options
|
64
|
+
# @option options [Module] :endpoint the endpoint to bind to. This must be a Dcerpc
|
65
|
+
# class with UUID, VER_MAJOR and VER_MINOR constants defined.
|
66
|
+
# @option options [Integer] :auth_level the authentication level
|
67
|
+
# @option options [Integer] :auth_type the authentication type
|
60
68
|
# @return [BindAck] the BindAck response packet
|
61
69
|
# @raise [Error::InvalidPacket] if an invalid packet is received
|
62
70
|
# @raise [Error::BindError] if the response is not a BindAck packet or if the Bind result code is not ACCEPTANCE
|
71
|
+
# @raise [ArgumentError] if `:auth_type` is unknown
|
72
|
+
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
|
63
73
|
def bind(options={})
|
74
|
+
@call_id ||= 1
|
64
75
|
bind_req = Bind.new(options)
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
76
|
+
bind_req.pdu_header.call_id = @call_id
|
77
|
+
|
78
|
+
if options[:auth_level] && options[:auth_level] != RPC_C_AUTHN_LEVEL_NONE
|
79
|
+
case options[:auth_type]
|
80
|
+
when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
|
81
|
+
@ctx_id = 0
|
82
|
+
@auth_ctx_id_base = rand(0xFFFFFFFF)
|
83
|
+
raise ArgumentError, "NTLM Client not initialized. Username and password must be provided" unless @ntlm_client
|
84
|
+
type1_message = @ntlm_client.init_context
|
85
|
+
auth = type1_message.serialize
|
86
|
+
when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE
|
87
|
+
when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL
|
88
|
+
# TODO
|
89
|
+
raise NotImplementedError
|
90
|
+
else
|
91
|
+
raise ArgumentError, "Unsupported Auth Type: #{options[:auth_type]}"
|
92
|
+
end
|
93
|
+
add_auth_verifier(bind_req, auth, options[:auth_type], options[:auth_level])
|
72
94
|
end
|
73
|
-
|
74
|
-
|
95
|
+
|
96
|
+
send_packet(bind_req)
|
97
|
+
begin
|
98
|
+
dcerpc_response = recv_struct(BindAck)
|
99
|
+
rescue Error::InvalidPacket
|
100
|
+
raise Error::BindError # raise the more context-specific BindError
|
75
101
|
end
|
102
|
+
# TODO: see if BindNack response should be handled too
|
76
103
|
|
77
104
|
res_list = dcerpc_response.p_result_list
|
78
105
|
if res_list.n_results == 0 ||
|
@@ -80,9 +107,216 @@ module RubySMB
|
|
80
107
|
raise Error::BindError,
|
81
108
|
"Bind Failed (Result: #{res_list.p_results[0].result}, Reason: #{res_list.p_results[0].reason})"
|
82
109
|
end
|
83
|
-
|
110
|
+
self.max_buffer_size = dcerpc_response.max_xmit_frag
|
111
|
+
@call_id = dcerpc_response.pdu_header.call_id
|
112
|
+
|
113
|
+
if options[:auth_level] && options[:auth_level] != RPC_C_AUTHN_LEVEL_NONE
|
114
|
+
# The number of legs needed to build the security context is defined
|
115
|
+
# by the security provider
|
116
|
+
# (see [2.2.1.1.7 Security Providers](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/d4097450-c62f-484b-872f-ddf59a7a0d36))
|
117
|
+
case options[:auth_type]
|
118
|
+
when RPC_C_AUTHN_WINNT
|
119
|
+
send_auth3(dcerpc_response, options[:auth_type], options[:auth_level])
|
120
|
+
when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE
|
121
|
+
# TODO
|
122
|
+
raise NotImplementedError
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
84
126
|
dcerpc_response
|
85
127
|
end
|
86
128
|
|
129
|
+
def max_buffer_size=(value)
|
130
|
+
@tree.client.max_buffer_size = value
|
131
|
+
end
|
132
|
+
|
133
|
+
# Receive a packet from the remote host and parse it according to `struct`
|
134
|
+
#
|
135
|
+
# @param struct [Class] the structure class to parse the response with
|
136
|
+
def recv_struct(struct)
|
137
|
+
raw_response = read
|
138
|
+
begin
|
139
|
+
response = struct.read(raw_response)
|
140
|
+
rescue IOError
|
141
|
+
raise Error::InvalidPacket, "Error reading the #{struct} response"
|
142
|
+
end
|
143
|
+
unless response.pdu_header.ptype == struct::PTYPE
|
144
|
+
raise Error::InvalidPacket, "Not a #{struct} packet"
|
145
|
+
end
|
146
|
+
|
147
|
+
response
|
148
|
+
end
|
149
|
+
|
150
|
+
# Send a packet to the remote host
|
151
|
+
#
|
152
|
+
# @param packet [BinData::Record] the packet to send
|
153
|
+
# @raise [Error::CommunicationError] if socket-related error occurs
|
154
|
+
def send_packet(packet)
|
155
|
+
write(data: packet.to_binary_s)
|
156
|
+
nil
|
157
|
+
end
|
158
|
+
|
159
|
+
# Add the authentication verifier to a Request packet. This includes a
|
160
|
+
# sec trailer and the signature of the packet. This also encrypts the
|
161
|
+
# Request stub if privacy is required (`:auth_level` option is
|
162
|
+
# RPC_C_AUTHN_LEVEL_PKT_PRIVACY).
|
163
|
+
#
|
164
|
+
# @param dcerpc_req [Request] the Request packet to be updated
|
165
|
+
# @param opts [Hash] the authenticaiton options: `:auth_type` and `:auth_level`
|
166
|
+
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
|
167
|
+
# @raise [ArgumentError] if `:auth_type` is unknown
|
168
|
+
def set_integrity_privacy(dcerpc_req, auth_level:, auth_type:)
|
169
|
+
dcerpc_req.sec_trailer = {
|
170
|
+
auth_type: auth_type,
|
171
|
+
auth_level: auth_level,
|
172
|
+
auth_context_id: @ctx_id + @auth_ctx_id_base
|
173
|
+
}
|
174
|
+
dcerpc_req.auth_value = ' ' * 16
|
175
|
+
dcerpc_req.pdu_header.auth_length = 16
|
176
|
+
|
177
|
+
data_to_sign = plain_stub = dcerpc_req.stub.to_binary_s + dcerpc_req.auth_pad.to_binary_s
|
178
|
+
if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
|
179
|
+
data_to_sign = dcerpc_req.to_binary_s[0..-(dcerpc_req.pdu_header.auth_length + 1)]
|
180
|
+
end
|
181
|
+
|
182
|
+
encrypted_stub = ''
|
183
|
+
if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
184
|
+
case auth_type
|
185
|
+
when RPC_C_AUTHN_NONE
|
186
|
+
when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
|
187
|
+
encrypted_stub = @ntlm_client.session.seal_message(plain_stub)
|
188
|
+
when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS
|
189
|
+
# TODO
|
190
|
+
raise NotImplementedError
|
191
|
+
else
|
192
|
+
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
signature = @ntlm_client.session.sign_message(data_to_sign)
|
197
|
+
|
198
|
+
unless encrypted_stub.empty?
|
199
|
+
pad_length = dcerpc_req.sec_trailer.auth_pad_length.to_i
|
200
|
+
dcerpc_req.enable_encrypted_stub
|
201
|
+
dcerpc_req.stub = encrypted_stub[0..-(pad_length + 1)]
|
202
|
+
dcerpc_req.auth_pad = encrypted_stub[-(pad_length)..-1]
|
203
|
+
end
|
204
|
+
dcerpc_req.auth_value = signature
|
205
|
+
dcerpc_req.pdu_header.auth_length = signature.size
|
206
|
+
end
|
207
|
+
|
208
|
+
# Process the security context received in a response. It decrypts the
|
209
|
+
# encrypted stub if `:auth_level` is set to anything different than
|
210
|
+
# RPC_C_AUTHN_LEVEL_PKT_PRIVACY. It also checks the packet signature and
|
211
|
+
# raises an InvalidPacket error if it fails. Note that the exception is
|
212
|
+
# disabled by default and can be enabled with the
|
213
|
+
# `:raise_signature_error` option
|
214
|
+
#
|
215
|
+
# @param dcerpc_response [Response] the Response packet
|
216
|
+
# containing the security context to process
|
217
|
+
# @param opts [Hash] the authenticaiton options: `:auth_type` and
|
218
|
+
# `:auth_level`. To enable errors when signature check fails, set the
|
219
|
+
# `:raise_signature_error` option to true
|
220
|
+
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
|
221
|
+
# @raise [Error::CommunicationError] if socket-related error occurs
|
222
|
+
def handle_integrity_privacy(dcerpc_response, auth_level:, auth_type:, raise_signature_error: false)
|
223
|
+
decrypted_stub = ''
|
224
|
+
if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
225
|
+
encrypted_stub = dcerpc_response.stub.to_binary_s + dcerpc_response.auth_pad.to_binary_s
|
226
|
+
case auth_type
|
227
|
+
when RPC_C_AUTHN_NONE
|
228
|
+
when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
|
229
|
+
decrypted_stub = @ntlm_client.session.unseal_message(encrypted_stub)
|
230
|
+
when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS
|
231
|
+
# TODO
|
232
|
+
raise NotImplementedError
|
233
|
+
else
|
234
|
+
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
unless decrypted_stub.empty?
|
239
|
+
pad_length = dcerpc_response.sec_trailer.auth_pad_length.to_i
|
240
|
+
dcerpc_response.stub = decrypted_stub[0..-(pad_length + 1)]
|
241
|
+
dcerpc_response.auth_pad = decrypted_stub[-(pad_length)..-1]
|
242
|
+
end
|
243
|
+
|
244
|
+
signature = dcerpc_response.auth_value
|
245
|
+
data_to_check = dcerpc_response.stub.to_binary_s
|
246
|
+
if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
|
247
|
+
data_to_check = dcerpc_response.to_binary_s[0..-(dcerpc_response.pdu_header.auth_length + 1)]
|
248
|
+
end
|
249
|
+
unless @ntlm_client.session.verify_signature(signature, data_to_check)
|
250
|
+
if raise_signature_error
|
251
|
+
raise Error::InvalidPacket.new(
|
252
|
+
"Wrong packet signature received (set `raise_signature_error` to false to ignore)"
|
253
|
+
)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
@call_id += 1
|
258
|
+
|
259
|
+
nil
|
260
|
+
end
|
261
|
+
|
262
|
+
# Add the authentication verifier to the packet. This includes a sec
|
263
|
+
# trailer and the actual authentication data.
|
264
|
+
#
|
265
|
+
# @param req [BinData::Record] the request to be updated
|
266
|
+
# @param auth [String] the authentication data
|
267
|
+
# @param auth_type [Integer] the authentication type
|
268
|
+
# @param auth_level [Integer] the authentication level
|
269
|
+
def add_auth_verifier(req, auth, auth_type, auth_level)
|
270
|
+
req.sec_trailer = {
|
271
|
+
auth_type: auth_type,
|
272
|
+
auth_level: auth_level,
|
273
|
+
auth_context_id: @ctx_id + @auth_ctx_id_base
|
274
|
+
}
|
275
|
+
req.auth_value = auth
|
276
|
+
req.pdu_header.auth_length = auth.length
|
277
|
+
|
278
|
+
nil
|
279
|
+
end
|
280
|
+
|
281
|
+
def process_ntlm_type2(type2_message)
|
282
|
+
ntlmssp_offset = type2_message.index('NTLMSSP')
|
283
|
+
type2_blob = type2_message.slice(ntlmssp_offset..-1)
|
284
|
+
type2_b64_message = [type2_blob].pack('m')
|
285
|
+
type3_message = @ntlm_client.init_context(type2_b64_message)
|
286
|
+
auth3 = type3_message.serialize
|
287
|
+
|
288
|
+
@session_key = @ntlm_client.session_key
|
289
|
+
auth3
|
290
|
+
end
|
291
|
+
|
292
|
+
# Send a rpc_auth3 PDU that ends the authentication handshake.
|
293
|
+
#
|
294
|
+
# @param response [BindAck] the BindAck response packet
|
295
|
+
# @param auth_type [Integer] the authentication type
|
296
|
+
# @param auth_level [Integer] the authentication level
|
297
|
+
# @raise [ArgumentError] if `:auth_type` is unknown
|
298
|
+
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
|
299
|
+
def send_auth3(response, auth_type, auth_level)
|
300
|
+
case auth_type
|
301
|
+
when RPC_C_AUTHN_NONE
|
302
|
+
when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
|
303
|
+
auth3 = process_ntlm_type2(response.auth_value)
|
304
|
+
when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS
|
305
|
+
# TODO
|
306
|
+
raise NotImplementedError
|
307
|
+
else
|
308
|
+
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
|
309
|
+
end
|
310
|
+
|
311
|
+
rpc_auth3 = RpcAuth3.new
|
312
|
+
add_auth_verifier(rpc_auth3, auth3, auth_type, auth_level)
|
313
|
+
rpc_auth3.pdu_header.call_id = @call_id
|
314
|
+
|
315
|
+
# The server should not respond
|
316
|
+
send_packet(rpc_auth3)
|
317
|
+
@call_id += 1
|
318
|
+
|
319
|
+
nil
|
320
|
+
end
|
87
321
|
end
|
88
322
|
end
|
data/lib/ruby_smb/error.rb
CHANGED
@@ -70,20 +70,34 @@ module RubySMB
|
|
70
70
|
|
71
71
|
# Raised when a response packet has a NTStatus code that was unexpected.
|
72
72
|
class UnexpectedStatusCode < RubySMBError
|
73
|
-
|
73
|
+
module Mixin
|
74
|
+
attr_reader :status_code
|
74
75
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
76
|
+
def status_name
|
77
|
+
@status_code.name
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def status_code=(status_code)
|
83
|
+
case status_code
|
84
|
+
when WindowsError::ErrorCode
|
85
|
+
@status_code = status_code
|
86
|
+
when Integer
|
87
|
+
@status_code = WindowsError::NTStatus.find_by_retval(status_code).first
|
88
|
+
if @status_code.nil?
|
89
|
+
@status_code = WindowsError::ErrorCode.new("0x#{status_code.to_s(16).rjust(8, '0')}", status_code, "Unknown status: 0x#{status_code.to_s(16).rjust(8, '0')}")
|
90
|
+
end
|
91
|
+
else
|
92
|
+
raise ArgumentError, "Status code must be a WindowsError::ErrorCode or an Integer, got #{status_code.class}"
|
83
93
|
end
|
84
|
-
else
|
85
|
-
raise ArgumentError, "Status code must be a WindowsError::ErrorCode or an Integer, got #{status_code.class}"
|
86
94
|
end
|
95
|
+
end
|
96
|
+
|
97
|
+
include Mixin
|
98
|
+
|
99
|
+
def initialize(status_code)
|
100
|
+
self.status_code = status_code
|
87
101
|
super
|
88
102
|
end
|
89
103
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module RubySMB
|
2
|
+
module PeerInfo
|
3
|
+
# Extract and store useful information about the peer/server from the
|
4
|
+
# NTLM Type 2 (challenge) TargetInfo fields.
|
5
|
+
#
|
6
|
+
# @param target_info_str [String] the Target Info string
|
7
|
+
def store_target_info(target_info_str)
|
8
|
+
target_info = Net::NTLM::TargetInfo.new(target_info_str)
|
9
|
+
{
|
10
|
+
Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => :@default_name,
|
11
|
+
Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME => :@default_domain,
|
12
|
+
Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME => :@dns_host_name,
|
13
|
+
Net::NTLM::TargetInfo::MSV_AV_DNS_DOMAIN_NAME => :@dns_domain_name,
|
14
|
+
Net::NTLM::TargetInfo::MSV_AV_DNS_TREE_NAME => :@dns_tree_name
|
15
|
+
}.each do |constant, attribute|
|
16
|
+
if target_info.av_pairs[constant]
|
17
|
+
value = target_info.av_pairs[constant].dup
|
18
|
+
value.force_encoding('UTF-16LE')
|
19
|
+
instance_variable_set(attribute, value.encode('UTF-8'))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Extract the peer/server version number from the NTLM Type 2 (challenge)
|
25
|
+
# Version field.
|
26
|
+
#
|
27
|
+
# @param version [String] the version number as a binary string
|
28
|
+
# @return [String] the formatted version number (<major>.<minor>.<build>)
|
29
|
+
def extract_os_version(version)
|
30
|
+
begin
|
31
|
+
os_version = RubySMB::NTLM::OSVersion.read(version)
|
32
|
+
rescue IOError
|
33
|
+
return ''
|
34
|
+
end
|
35
|
+
|
36
|
+
"#{os_version.major}.#{os_version.minor}.#{os_version.build}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/ruby_smb/signing.rb
CHANGED
@@ -53,6 +53,8 @@ module RubySMB
|
|
53
53
|
def self.smb2_sign(packet, session_key)
|
54
54
|
packet.smb2_header.flags.signed = 1
|
55
55
|
packet.smb2_header.signature = "\x00" * 16
|
56
|
+
# OpenSSL 3 raises exceptions if the session key is an empty string
|
57
|
+
session_key = session_key == '' ? ("\x00" * 16).b : session_key
|
56
58
|
hmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), session_key, packet.to_binary_s)
|
57
59
|
packet.smb2_header.signature = hmac[0, 16]
|
58
60
|
|
data/lib/ruby_smb/smb1/pipe.rb
CHANGED
@@ -28,10 +28,20 @@ module RubySMB
|
|
28
28
|
extend RubySMB::Dcerpc::Samr
|
29
29
|
when 'wkssvc', '\\wkssvc'
|
30
30
|
extend RubySMB::Dcerpc::Wkssvc
|
31
|
+
when 'netdfs', '\\netdfs'
|
32
|
+
extend RubySMB::Dcerpc::Dfsnm
|
33
|
+
when 'cert', '\\cert'
|
34
|
+
extend RubySMB::Dcerpc::Icpr
|
31
35
|
end
|
32
36
|
super(tree: tree, response: response, name: name)
|
33
37
|
end
|
34
38
|
|
39
|
+
def bind(options={})
|
40
|
+
@size = 1024
|
41
|
+
@ntlm_client = @tree.client.ntlm_client
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
35
45
|
# Performs a peek operation on the named pipe
|
36
46
|
#
|
37
47
|
# @param peek_size [Integer] Amount of data to peek
|
@@ -96,6 +106,11 @@ module RubySMB
|
|
96
106
|
options.merge!(endpoint: stub_packet.class.name.split('::').at(-2))
|
97
107
|
dcerpc_request = RubySMB::Dcerpc::Request.new({ opnum: stub_packet.opnum }, options)
|
98
108
|
dcerpc_request.stub.read(stub_packet.to_binary_s)
|
109
|
+
if options[:auth_level] &&
|
110
|
+
[RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(options[:auth_level])
|
111
|
+
set_integrity_privacy(dcerpc_request, auth_level: options[:auth_level], auth_type: options[:auth_type])
|
112
|
+
end
|
113
|
+
|
99
114
|
trans_nmpipe_request = RubySMB::SMB1::Packet::Trans::TransactNmpipeRequest.new(options)
|
100
115
|
@tree.set_header_fields(trans_nmpipe_request)
|
101
116
|
trans_nmpipe_request.set_fid(@fid)
|
@@ -122,6 +137,10 @@ module RubySMB
|
|
122
137
|
unless dcerpc_response.pdu_header.pfc_flags.first_frag == 1
|
123
138
|
raise RubySMB::Dcerpc::Error::InvalidPacket, "Not the first fragment"
|
124
139
|
end
|
140
|
+
if options[:auth_level] &&
|
141
|
+
[RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(options[:auth_level])
|
142
|
+
handle_integrity_privacy(dcerpc_response, auth_level: options[:auth_level], auth_type: options[:auth_type])
|
143
|
+
end
|
125
144
|
stub_data = dcerpc_response.stub.to_s
|
126
145
|
|
127
146
|
loop do
|
@@ -133,11 +152,14 @@ module RubySMB
|
|
133
152
|
stub_data
|
134
153
|
else
|
135
154
|
dcerpc_response = dcerpc_response_from_raw_response(raw_data)
|
155
|
+
if options[:auth_level] &&
|
156
|
+
[RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(options[:auth_level])
|
157
|
+
handle_integrity_privacy(dcerpc_response, auth_level: options[:auth_level], auth_type: options[:auth_type])
|
158
|
+
end
|
136
159
|
dcerpc_response.stub.to_s
|
137
160
|
end
|
138
161
|
end
|
139
162
|
|
140
|
-
|
141
163
|
private
|
142
164
|
|
143
165
|
def dcerpc_response_from_raw_response(raw_data)
|
@@ -21,7 +21,11 @@ module RubySMB
|
|
21
21
|
uint8 :reserved, label: 'Reserved Space', initial_value: 0x00
|
22
22
|
share_flags :share_flags
|
23
23
|
share_capabilities :capabilities
|
24
|
-
|
24
|
+
choice :maximal_access, label: 'Maximal Access', selection: -> { share_type } do
|
25
|
+
file_access_mask SMB2_SHARE_TYPE_PIPE
|
26
|
+
file_access_mask SMB2_SHARE_TYPE_PRINT
|
27
|
+
directory_access_mask SMB2_SHARE_TYPE_DISK
|
28
|
+
end
|
25
29
|
|
26
30
|
def initialize_instance
|
27
31
|
super
|