Skip to content

Commit

Permalink
Improve TeamCity Login Scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanusz-r7 committed Nov 29, 2024
1 parent f2e5dd6 commit b2b1922
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 9 deletions.
3 changes: 3 additions & 0 deletions lib/metasploit/framework/login_scanner/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ module Base
# @!attribute bruteforce_speed
# @return [Integer] The desired speed, with 5 being 'fast' and 0 being 'slow.'
attr_accessor :bruteforce_speed
# @!attribute logger
# @return [Object] The logger to use when logging messages from inside the logger.
attr_accessor :logger

validates :connection_timeout,
presence: true,
Expand Down
36 changes: 28 additions & 8 deletions lib/metasploit/framework/login_scanner/teamcity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def encrypt_data(text, public_key)

DEFAULT_PORT = 8111
LIKELY_PORTS = [8111]
LIKELY_SERVICE_NAMES = ['skynetflow'] # Comes from nmap 7.95 on MacOS
LIKELY_SERVICE_NAMES = ['teamcity']
PRIVATE_TYPES = [:password]
REALM_KEY = nil

Expand All @@ -134,9 +134,26 @@ class TeamCityError < StandardError; end
class StackLevelTooDeepError < TeamCityError; end
class NoPublicKeyError < TeamCityError; end
class PublicKeyExpiredError < TeamCityError; end
class DecryptionException < TeamCityError; end
class DecryptionError < TeamCityError; end
class ServerNeedsSetupError < TeamCityError; end

# Checks if the target is JetBrains TeamCity. The login module should call this.
#
# @return [Boolean] TrueClass if target is TeamCity, otherwise FalseClass
def check_setup
request_params = {
'method' => 'GET',
'uri' => normalize_uri(@uri.to_s, LOGIN_PAGE)
}
res = send_request(request_params)

if res && res.body.include?('Log in to TeamCity')
return true
end

false
end

# Extract the server's public key from the server.
# @return [Hash] A hash with a status and an error or the server's public key.
def get_public_key
Expand Down Expand Up @@ -209,18 +226,21 @@ def try_login(username, password, public_key, retry_counter = 0)
# 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 = try_login(username, password, public_key, retry_counter + 1)
return result
logger.print_status "#{@host}:#{@port} - User '#{username}:#{password}' locked out for #{timeout} seconds. Sleeping, and retrying..." if logger
::IO.select(nil, nil, nil, timeout + 1)
return try_login(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 DecryptionException, 'The server failed to decrypt the encrypted password' if res.body.match?('DecryptionFailedException')
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')

{ status: :success, proof: res }
successful_login_body = %r{^<response><redirect>/favorite/projects</redirect><user id="[0-9]+" /><errors /></response>$}
return { status: :success, proof: res } if res.body.match?(successful_login_body)

# Default to incorrect login.
{ status: ::Metasploit::Model::Login::Status::INCORRECT, proof: res }
end

# Send a logout request for the provided user's headers.
Expand Down
3 changes: 2 additions & 1 deletion modules/auxiliary/scanner/teamcity/teamcity_login.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def run_host(ip)
framework_module: self,
http_success_codes: [200, 302],
method: 'POST',
ssl: datastore['SSL']
ssl: datastore['SSL'],
logger: framework
)

scanner = Metasploit::Framework::LoginScanner::Teamcity.new(scanner_opts)
Expand Down

0 comments on commit b2b1922

Please sign in to comment.