Skip to content

Commit a6ee189

Browse files
committed
TeamCity: Use more exceptions, cache public key
1 parent 386441d commit a6ee189

File tree

1 file changed

+34
-50
lines changed

1 file changed

+34
-50
lines changed

lib/metasploit/framework/login_scanner/teamcity.rb

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,47 +19,36 @@ class Teamcity < HTTP
1919
LOGOUT_PAGE = 'ajax.html?logout=1'
2020
SUBMIT_PAGE = 'loginSubmit.html'
2121

22-
SUCCESSFUL = ::Metasploit::Model::Login::Status::SUCCESSFUL
23-
UNABLE_TO_CONNECT = ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
24-
INVALID_PUBLIC_PART = ::Metasploit::Model::Login::Status::INVALID_PUBLIC_PART
25-
LOCKED_OUT = ::Metasploit::Model::Login::Status::LOCKED_OUT
26-
INCORRECT = ::Metasploit::Model::Login::Status::INCORRECT
27-
2822
class TeamCityError < StandardError; end
2923
class StackLevelTooDeepError < TeamCityError; end
24+
class NoPublicKeyError < TeamCityError; end
25+
class PublicKeyExpiredError < TeamCityError; end
26+
class DecryptionException < TeamCityError; end
27+
class ServerNeedsSetupError < TeamCityError; end
3028

31-
# Send a GET request to the server and return a response.
32-
# @param [Hash] opts A hash with options that will take precedence over default values used to make the HTTP request.
33-
# @return [Hash] A hash with a status and an error or the response from the login page.
34-
def get_page_data(opts: { timeout: 5 })
29+
# Extract the server's public key from the server.
30+
# @return [Hash] A hash with a status and an error or the server's public key.
31+
def get_public_key
3532
request_params = {
3633
'method' => 'GET',
3734
'uri' => normalize_uri(@uri.to_s, LOGIN_PAGE)
3835
}
3936

40-
opts.each { |param, value| request_params[param] = value }
4137
begin
4238
res = send_request(request_params)
4339
rescue ::Rex::ConnectionError, ::Rex::ConnectionProxyError, ::Errno::ECONNRESET, ::Errno::EINTR, ::Rex::TimeoutError, ::Timeout::Error, ::EOFError => e
44-
return { status: UNABLE_TO_CONNECT, proof: e }
40+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e }
4541
end
4642

47-
return { status: UNABLE_TO_CONNECT, proof: 'Unable to connect to the TeamCity service' } if res.nil?
48-
# Does the service need to be setup & configured with the initial DB migration & admin account?
49-
return { status: UNABLE_TO_CONNECT, proof: "Received an unexpected status code: #{res.code}. Does the service need to be configured?" } if res.code != 200
43+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to the TeamCity service' } if res.nil?
5044

51-
{ status: :success, proof: res }
52-
end
45+
raise ServerNeedsSetupError, 'The server has not performed the initial setup' if res.code == 503
5346

54-
# Extract the server's public key from the response.
55-
# @param [Rex::Proto::Http::Response] response The response to extract the public RSA key from.
56-
# @return [Hash] A hash with a status and an error or the server's public key.
57-
def get_public_key(response)
58-
html_doc = response.get_html_document
59-
public_key_choices = html_doc.xpath('//input[@id="publicKey"]/@value')
60-
return { status: UNABLE_TO_CONNECT, proof: 'Could not find the TeamCity public key in the HTML document' } if public_key_choices.empty?
47+
html_doc = res.get_html_document
48+
public_key = html_doc.xpath('//input[@id="publicKey"]/@value').text
49+
raise NoPublicKeyError, 'Could not find the TeamCity public key in the HTML document' if public_key.empty?
6150

62-
{ status: :success, proof: public_key_choices.first.value }
51+
{ status: :success, proof: public_key }
6352
end
6453

6554
# Create a login request for the provided credentials.
@@ -96,11 +85,11 @@ def try_login(username, password, public_key, retry_counter = 0)
9685
begin
9786
res = send_request(login_request)
9887
rescue ::Rex::ConnectionError, ::Rex::ConnectionProxyError, ::Errno::ECONNRESET, ::Errno::EINTR, ::Rex::TimeoutError, ::Timeout::Error, ::EOFError => e
99-
return { status: UNABLE_TO_CONNECT, proof: e }
88+
return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e }
10089
end
10190

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

10594
# Check if the current username is timed out. Sleep if so.
10695
# TODO: This can be improved. The `try_login` method should not block until it can retry credentials.
@@ -115,34 +104,24 @@ def try_login(username, password, public_key, retry_counter = 0)
115104
return result
116105
end
117106

118-
return { status: INCORRECT, proof: res } if res.body.match?('Incorrect username or password')
119-
return { status: UNABLE_TO_CONNECT, proof: res } if res.body.match?('ajax') # TODO: Get the exact error message here.
120-
return { status: INVALID_PUBLIC_PART, proof: res } if res.body.match?('publicKeyExpired') # TODO: Invalid public part? Or Incorrect/Unable_to_connect?
107+
return { status: ::Metasploit::Model::Login::Status::INCORRECT, proof: res } if res.body.match?('Incorrect username or password')
108+
109+
raise DecryptionException, 'The server failed to decrypt the encrypted password' if res.body.match?('DecryptionFailedException')
110+
raise PublicKeyExpiredError, 'The server public key has expired' if res.body.match?('publicKeyExpired')
121111

122112
{ status: :success, proof: res }
123113
end
124114

125115
# Send a logout request for the provided user's headers.
126116
# This header stores the user's cookie.
127-
# @return [Hash] A hash with the status and an error or the response.
128117
def logout_with_headers(headers)
129118
logout_params = {
130119
'method' => 'POST',
131120
'uri' => normalize_uri(@uri.to_s, LOGOUT_PAGE),
132121
'headers' => headers
133122
}
134123

135-
begin
136-
logout_res = send_request(logout_params)
137-
rescue ::Rex::ConnectionError, ::Rex::ConnectionProxyError, ::Errno::ECONNRESET, ::Errno::EINTR, ::Rex::TimeoutError, ::Timeout::Error, ::EOFError => e
138-
return { status: UNABLE_TO_CONNECT, proof: e }
139-
end
140-
141-
return { status: UNABLE_TO_CONNECT, proof: 'Unable to connect to the TeamCity service' } if logout_res.nil?
142-
# A successful logout request wants to redirect us back to the login page
143-
return { status: UNABLE_TO_CONNECT, proof: "Received an unexpected status code: #{logout_res.code}" } if logout_res.code != 302
144-
145-
{ status: :success, proof: logout_res }
124+
send_request(logout_params)
146125
end
147126

148127
def attempt_login(credential)
@@ -154,22 +133,27 @@ def attempt_login(credential)
154133
service_name: 'teamcity'
155134
}
156135

157-
# Needed to retrieve the public key that will be used to encrypt the user's password.
158-
page_data = get_page_data
159-
return Result.new(result_options.merge(page_data)) if page_data[:status] != :success
136+
if @public_key.nil?
137+
public_key_result = get_public_key
138+
return Result.new(result_options.merge(public_key_result)) if public_key_result[:status] != :success
160139

161-
public_key_result = get_public_key(page_data[:proof])
162-
return Result.new(result_options.merge(public_key_result)) if public_key_result[:status] != :success
140+
@public_key = public_key_result[:proof]
141+
end
163142

164-
login_result = try_login(credential.public, credential.private, public_key_result[:proof])
143+
login_result = try_login(credential.public, credential.private, @public_key)
165144
return Result.new(result_options.merge(login_result)) if login_result[:status] != :success
166145

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

170-
result_options[:status] = SUCCESSFUL
149+
result_options[:status] = ::Metasploit::Model::Login::Status::SUCCESSFUL
171150
Result.new(result_options)
172151
end
152+
153+
private
154+
155+
attr_accessor :public_key
156+
173157
end
174158
end
175159
end

0 commit comments

Comments
 (0)