diff --git a/lib/metasploit/framework/login_scanner/base.rb b/lib/metasploit/framework/login_scanner/base.rb index 6d0e5e9c8576e..80a7451e00d73 100644 --- a/lib/metasploit/framework/login_scanner/base.rb +++ b/lib/metasploit/framework/login_scanner/base.rb @@ -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, diff --git a/lib/metasploit/framework/login_scanner/teamcity.rb b/lib/metasploit/framework/login_scanner/teamcity.rb index a950bee07ec7b..1da8195a9301a 100644 --- a/lib/metasploit/framework/login_scanner/teamcity.rb +++ b/lib/metasploit/framework/login_scanner/teamcity.rb @@ -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 @@ -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 @@ -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 (?\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{^/favorite/projects$} + 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. diff --git a/modules/auxiliary/scanner/teamcity/teamcity_login.rb b/modules/auxiliary/scanner/teamcity/teamcity_login.rb index 44cdbdb6f8837..3076ccba75731 100644 --- a/modules/auxiliary/scanner/teamcity/teamcity_login.rb +++ b/modules/auxiliary/scanner/teamcity/teamcity_login.rb @@ -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)