Class: Metasploit::Framework::LoginScanner::Teamcity

Inherits:
HTTP
  • Object
show all
Includes:
Crypto
Defined in:
lib/metasploit/framework/login_scanner/teamcity.rb

Overview

This is the LoginScanner class for dealing with JetBrains TeamCity instances. It is responsible for taking a single target, and a list of credentials and attempting them. It then saves the results.

Defined Under Namespace

Modules: Crypto Classes: DecryptionException, NoPublicKeyError, PublicKeyExpiredError, ServerNeedsSetupError, StackLevelTooDeepError, TeamCityError

Constant Summary collapse

DEFAULT_PORT =
8111
LIKELY_PORTS =
[8111]
LIKELY_SERVICE_NAMES =

Comes from nmap 7.95 on MacOS

['skynetflow']
PRIVATE_TYPES =
[:password]
REALM_KEY =
nil
LOGIN_PAGE =
'login.html'
LOGOUT_PAGE =
'ajax.html?logout=1'
SUBMIT_PAGE =
'loginSubmit.html'

Constants inherited from HTTP

HTTP::AUTHORIZATION_HEADER, HTTP::DEFAULT_HTTP_NOT_AUTHED_CODES, HTTP::DEFAULT_HTTP_SUCCESS_CODES, HTTP::DEFAULT_REALM, HTTP::DEFAULT_SSL_PORT

Instance Attribute Summary

Attributes inherited from HTTP

#digest_auth_iis, #evade_header_folding, #evade_method_random_case, #evade_method_random_invalid, #evade_method_random_valid, #evade_pad_fake_headers, #evade_pad_fake_headers_count, #evade_pad_get_params, #evade_pad_get_params_count, #evade_pad_method_uri_count, #evade_pad_method_uri_type, #evade_pad_post_params, #evade_pad_post_params_count, #evade_pad_uri_version_count, #evade_pad_uri_version_type, #evade_shuffle_get_params, #evade_shuffle_post_params, #evade_uri_dir_fake_relative, #evade_uri_dir_self_reference, #evade_uri_encode_mode, #evade_uri_fake_end, #evade_uri_fake_params_start, #evade_uri_full_url, #evade_uri_use_backslashes, #evade_version_random_invalid, #evade_version_random_valid, #http_password, #http_success_codes, #http_username, #keep_connection_alive, #kerberos_authenticator_factory, #method, #ntlm_domain, #ntlm_send_lm, #ntlm_send_ntlm, #ntlm_send_spn, #ntlm_use_lm_key, #ntlm_use_ntlmv2, #ntlm_use_ntlmv2_session, #uri, #user_agent, #vhost

Instance Method Summary collapse

Methods included from Crypto

#encrypt_data, #max_data_size, #pkcs1pad2, #rsa_encrypt, #two_byte_chars?

Methods inherited from HTTP

#authentication_required?, #check_setup, #send_request

Instance Method Details

#attempt_login(credential) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 242

def (credential)
  result_options = {
    credential:   credential,
    host:         @host,
    port:         @port,
    protocol:     'tcp',
    service_name: 'teamcity'
  }

  if @public_key.nil?
    public_key_result = get_public_key
    return Result.new(result_options.merge(public_key_result)) if public_key_result[:status] != :success

    @public_key = public_key_result[:proof]
  end

   = (credential.public, credential.private, @public_key)
  return Result.new(result_options.merge()) if [:status] != :success

  # Ensure we log the user out, so that our logged in session does not appear under the user's profile.
  logout_with_headers([:proof].headers)

  result_options[:status] = ::Metasploit::Model::Login::Status::SUCCESSFUL
  Result.new(result_options)
end

#create_login_request(username, password, public_key) ⇒ Hash

Create a login request for the provided credentials.

Parameters:

  • username (String)

    The username to create the login request for.

  • password (String)

    The password to log in with.

  • public_key (String)

    The public key to encrypt the password with.

Returns:

  • (Hash)

    The login request parameter hash.



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 170

def (username, password, public_key)
  {
    'method' => 'POST',
    'uri' => normalize_uri(@uri.to_s, SUBMIT_PAGE),
    'ctype' => 'application/x-www-form-urlencoded',
    'vars_post' => {
      username: username,
      remember: true,
      _remember: '',
      submitLogin: 'Log in',
      publicKey: public_key,
      encryptedPassword: encrypt_data(password, public_key)
    }
  }
end

#get_public_keyHash

Extract the server’s public key from the server.

Returns:

  • (Hash)

    A hash with a status and an error or the server’s public key.

Raises:



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 142

def get_public_key
  request_params = {
    'method' => 'GET',
    'uri' => normalize_uri(@uri.to_s, LOGIN_PAGE)
  }

  begin
    res = send_request(request_params)
  rescue ::Rex::ConnectionError, ::Rex::ConnectionProxyError, ::Errno::ECONNRESET, ::Errno::EINTR, ::Rex::TimeoutError, ::Timeout::Error, ::EOFError => e
    return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e }
  end

  return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to the TeamCity service' } if res.nil?

  raise ServerNeedsSetupError, 'The server has not performed the initial setup' if res.code == 503

  html_doc = res.get_html_document
  public_key = html_doc.xpath('//input[@id="publicKey"]/@value').text
  raise NoPublicKeyError, 'Could not find the TeamCity public key in the HTML document' if public_key.empty?

  { status: :success, proof: public_key }
end

#logout_with_headers(headers) ⇒ Object

Send a logout request for the provided user’s headers. This header stores the user’s cookie.



228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 228

def logout_with_headers(headers)
  logout_params = {
    'method' => 'POST',
    'uri' => normalize_uri(@uri.to_s, LOGOUT_PAGE),
    'headers' => headers
  }

  begin
    send_request(logout_params)
  rescue Rex::ConnectionError => _e
    # ignore
  end
end

#try_login(username, password, public_key, retry_counter = 0) ⇒ Hash

Try logging in with the provided username, password and public key.

Parameters:

  • username (String)

    The username to send the login request for.

  • password (String)

    The user’s password.

  • public_key (String)

    The public key used to encrypt the password.

Returns:

  • (Hash)

    A hash with the status and an error or the response.

Raises:



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 191

def (username, password, public_key, retry_counter = 0)
  raise StackLevelTooDeepError, 'try_login stack level too deep!' if retry_counter >= 2

   = (username, password, public_key)

  begin
    res = send_request()
  rescue ::Rex::ConnectionError, ::Rex::ConnectionProxyError, ::Errno::ECONNRESET, ::Errno::EINTR, ::Rex::TimeoutError, ::Timeout::Error, ::EOFError => e
    return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e }
  end

  return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to the TeamCity service' } if res.nil?
  return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: "Received an unexpected status code: #{res.code}" } if res.code != 200

  # Check if the current username is timed out. Sleep if so.
  # TODO: This can be improved. The `try_login` method should not block until it can retry credentials.
  # This responsibility should fall onto the caller, and the caller should keep track of the tried, locked out and untried sets of credentials,
  # and it should be up to the caller and its scheduler algorithm to retry credentials, rather than force this method to block.
  # Currently, those building blocks are not available, so this is the approach I have implemented.
  timeout = res.body.match(/login only in (?<timeout>\d+)s/)&.named_captures&.dig('timeout')&.to_i
  if timeout
    framework_module.print_status "User '#{username}' locked out for #{timeout} seconds. Sleeping, and retrying..."
    sleep(timeout + 1) # + 1 as TeamCity is off-by-one when reporting the lockout timer.
    result = (username, password, public_key, retry_counter + 1)
    return result
  end

  return { status: ::Metasploit::Model::Login::Status::INCORRECT, proof: res } if res.body.match?('Incorrect username or password')

  raise DecryptionException, 'The server failed to decrypt the encrypted password' if res.body.match?('DecryptionFailedException')
  raise PublicKeyExpiredError, 'The server public key has expired' if res.body.match?('publicKeyExpired')

  { status: :success, proof: res }
end