@@ -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