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: DecryptionError, 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',
  'teamcity'
]
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?, #send_request

Instance Method Details

#attempt_login(credential) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 265

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

#check_setupBoolean

Checks if the target is JetBrains TeamCity. The login module should call this.

Returns:

  • (Boolean)

    TrueClass if target is TeamCity, otherwise FalseClass



147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 147

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

  if res && res.code == 200 && res.body&.include?('Log in to TeamCity')
    return false
  end

  "Unable to locate \"Log in to TeamCity\" in body. (Is this really TeamCity?)"
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.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 191

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:



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

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.



251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 251

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:



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
239
240
241
242
243
244
245
246
247
# File 'lib/metasploit/framework/login_scanner/teamcity.rb', line 212

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 "#{@host}:#{@port} - User '#{username}:#{password}' locked out for #{timeout} seconds. Sleeping, and retrying..." if framework_module
    sleep(timeout + 1)
    return (username, password, public_key, retry_counter + 1)
  end

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

  raise DecryptionError, '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')

  # After filtering out known failures, default to retuning the credential as working.
  # This way, people are more likely to notice any incorrect credential reporting going forward and report them,
  # the scenarios for which can then be correctly implemented and handled similar to the above.
  { status: :success, proof: res }
end