diff --git a/lib/metasploit/framework/login_scanner/base.rb b/lib/metasploit/framework/login_scanner/base.rb index 2d40fb948d..fc093984ab 100644 --- a/lib/metasploit/framework/login_scanner/base.rb +++ b/lib/metasploit/framework/login_scanner/base.rb @@ -77,6 +77,14 @@ module Metasploit raise NotImplementedError end + # @note Override this to detect that the service is up, is the right + # version, etc. + # @return [false] Indicates there were no errors + # @return [String] a human-readable error message describing why + # this scanner can't run + def check_setup + false + end def each_credential cred_details.each do |raw_cred| @@ -85,6 +93,11 @@ module Metasploit credential = raw_cred.to_credential if credential.realm.present? && self.class::REALM_KEY.present? + # The class's realm_key will always be the right thing for the + # service it knows how to login to. Override the credential's + # realm_key if one exists for the class. This can happen for + # example when we have creds for DB2 and want to try them + # against Postgres. credential.realm_key = self.class::REALM_KEY yield credential elsif credential.realm.blank? && self.class::REALM_KEY.present? && self.class::DEFAULT_REALM.present? @@ -93,12 +106,14 @@ module Metasploit yield credential elsif credential.realm.present? && self.class::REALM_KEY.blank? second_cred = credential.dup - # Strip the realm off here, as we don't want it + # This service has no realm key, so the realm will be + # meaningless. Strip it off. credential.realm = nil credential.realm_key = nil yield credential # Some services can take a domain in the username like this even though # they do not explicitly take a domain as part of the protocol. + # e.g., telnet second_cred.public = "#{second_cred.realm}\\#{second_cred.public}" second_cred.realm = nil second_cred.realm_key = nil diff --git a/lib/metasploit/framework/login_scanner/glassfish.rb b/lib/metasploit/framework/login_scanner/glassfish.rb index 7a956dbe61..5b925fc5b2 100644 --- a/lib/metasploit/framework/login_scanner/glassfish.rb +++ b/lib/metasploit/framework/login_scanner/glassfish.rb @@ -5,10 +5,6 @@ module Metasploit module Framework module LoginScanner - # I don't want to raise RuntimeError to be able to abort login - class GlassfishError < StandardError - end - # The Glassfish HTTP LoginScanner class provides methods to do login routines # for Glassfish 2, 3 and 4. class Glassfish < HTTP @@ -16,14 +12,45 @@ module Metasploit DEFAULT_PORT = 4848 PRIVATE_TYPES = [ :password ] - # @!attribute version + # @!attribute [r] version # @return [String] Glassfish version - attr_accessor :version + attr_reader :version # @!attribute jsession # @return [String] Cookie session attr_accessor :jsession + # (see Base#check_setup) + def check_setup + res = send_request({'uri' => '/common/index.jsf'}) + return "Connection failed" if res.nil? + if !([200, 302].include?(res.code)) + return "Unexpected HTTP response code #{res.code} (is this really Glassfish?)" + end + + # If remote login is enabled on 4.x, it redirects to https on the + # same port. + if !self.ssl && res.headers['Location'] =~ /^https:/ + self.ssl = true + res = send_request({'uri' => '/common/index.jsf'}) + if res.nil? + return "Connection failed after SSL redirection" + end + if res.code != 200 + return "Unexpected HTTP response code #{res.code} after SSL redirection (is this really Glassfish?)" + end + end + + res = send_request({'uri' => '/login.jsf'}) + return "Connection failed" if res.nil? + extract_version(res.headers['Server']) + + if @version.nil? || @version !~ /^[2349]/ + return "Unsupported version ('#{@version}')" + end + + false + end # Sends a HTTP request with Rex # @@ -126,9 +153,9 @@ module Metasploit return {:status => Metasploit::Model::Login::Status::SUCCESSFUL, :proof => res.body} end elsif res && is_secure_admin_disabled?(res) - return {:status => Metasploit::Model::Login::Status::SUCCESSFUL, :proof => res.body} + return {:status => Metasploit::Model::Login::Status::DENIED_ACCESS, :proof => res.body} elsif res && res.code == 400 - raise GlassfishError, "400: Bad HTTP request from try_login" + return {:status => Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, :proof => res.body} end {:status => Metasploit::Model::Login::Status::INCORRECT, :proof => res.body} @@ -146,12 +173,10 @@ module Metasploit case self.version when /^[29]\.x$/ status = try_glassfish_2(credential) - result_opts.merge!(status: status[:status], proof:status[:proof]) + result_opts.merge!(status) when /^[34]\./ status = try_glassfish_3(credential) - result_opts.merge!(status: status[:status], proof:status[:proof]) - else - raise GlassfishError, "Glassfish version '#{self.version}' not supported" + result_opts.merge!(status) end rescue ::EOFError, Rex::ConnectionError, ::Timeout::Error result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) @@ -160,6 +185,31 @@ module Metasploit Result.new(result_opts) end + # + # Extract the target's glassfish version from the HTTP Server header + # (ex: Sun Java System Application Server 9.x) + # + # @param banner [String] `Server` header from a Glassfish service response + # @return [String] version string, e.g. '2.x' + # @return [nil] If the banner did not match any of the expected values + def extract_version(banner) + # Set version. Some GlassFish servers return banner "GlassFish v3". + if banner =~ /(GlassFish Server|Open Source Edition)[[:blank:]]*(\d\.\d)/ + @version = $2 + elsif banner =~ /GlassFish v(\d)/ + @version = $1 + elsif banner =~ /Sun GlassFish Enterprise Server v2/ + @version = '2.x' + elsif banner =~ /Sun Java System Application Server 9/ + @version = '9.x' + else + @version = nil + end + + return @version + end + + end end end diff --git a/lib/metasploit/framework/login_scanner/http.rb b/lib/metasploit/framework/login_scanner/http.rb index 45c661d0de..bb6f0ecb10 100644 --- a/lib/metasploit/framework/login_scanner/http.rb +++ b/lib/metasploit/framework/login_scanner/http.rb @@ -35,6 +35,26 @@ module Metasploit presence: true, length: { minimum: 1 } + # (see Base#check_setup) + def check_setup + http_client = Rex::Proto::Http::Client.new( + host, port, {}, ssl, ssl_version + ) + request = http_client.request_cgi( + 'uri' => uri, + 'method' => method + ) + # Use _send_recv instead of send_recv to skip automatiu + # authentication + response = http_client._send_recv(request) + + if !(response && response.code == 401 && response.headers['WWW-Authenticate']) + "No authentication required" + else + false + end + end + # Attempt a single login with a single credential against the target. # # @param credential [Credential] The credential object to attempt to @@ -73,20 +93,9 @@ module Metasploit 'method' => method ) - # First try to connect without logging in to make sure this - # resource requires authentication. We use #_send_recv for - # that instead of #send_recv. - response = http_client._send_recv(request) - if response && response.code == 401 && response.headers['WWW-Authenticate'] - # Now send the creds - response = http_client.send_auth( - response, request.opts, connection_timeout, true - ) - if response && response.code == 200 - result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL, proof: response.headers) - end - else - result_opts.merge!(status: Metasploit::Model::Login::Status::NO_AUTH_REQUIRED) + response = http_client.send_recv(request) + if response && response.code == 200 + result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL, proof: response.headers) end rescue ::EOFError, Errno::ETIMEDOUT, Rex::ConnectionError, ::Timeout::Error result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) diff --git a/modules/auxiliary/scanner/http/glassfish_login.rb b/modules/auxiliary/scanner/http/glassfish_login.rb index d4f7cd61c1..8e51a03cdb 100644 --- a/modules/auxiliary/scanner/http/glassfish_login.rb +++ b/modules/auxiliary/scanner/http/glassfish_login.rb @@ -51,52 +51,11 @@ class Metasploit3 < Msf::Auxiliary # the LoginScanner class so the authentication can proceed properly # - def jsession - @jsession || '' - end - - def set_jsession(res) - if res && res.get_cookies =~ /JSESSIONID=(\w*);/i - @scanner.jsession = $1 - end - end - # Overrides the ssl method from HttpClient def ssl @scanner.ssl || datastore['SSL'] end - # - # Return GlassFish's edition (Open Source or Commercial) and version (2.x, 3.0, 3.1, 9.x, 4.0) and - # banner (ex: Sun Java System Application Server 9.x) - # - def get_version(res) - # Extract banner from response - banner = res.headers['Server'] || '' - - # Default value for edition and glassfish version - edition = 'Commercial' - version = 'Unknown' - - # Set edition (Open Source or Commercial) - p = /(Open Source|Sun GlassFish Enterprise Server|Sun Java System Application Server)/ - edition = 'Open Source' if banner =~ p - - # Set version. Some GlassFish servers return banner "GlassFish v3". - if banner =~ /(GlassFish Server|Open Source Edition)[[:blank:]]*(\d\.\d)/ - version = $2 - elsif banner =~ /GlassFish v(\d)/ && version.nil? - version = $1 - elsif banner =~ /Sun GlassFish Enterprise Server v2/ && version == 'Unknown' - version = '2.x' - elsif banner =~ /Sun Java System Application Server 9/ && version == 'Unknown' - version = '9.x' - end - - return edition, version, banner - end - - # # For a while, older versions of Glassfish didn't need to set a password for admin, # but looks like no longer the case anymore, which means this method is getting useless @@ -107,14 +66,12 @@ class Metasploit3 < Msf::Auxiliary if version =~ /^[29]\.x$/ res = send_request_cgi({'uri'=>'/applications/upload.jsf'}) - set_jsession(res) p = /Deploy Enterprise Applications\/Modules/ if (res && res.code.to_i == 200 && res.body.match(p) != nil) success = true end elsif version =~ /^3\./ res = send_request_cgi({'uri'=>'/common/applications/uploadFrame.jsf'}) - set_jsession(res) p = /<title>Deploy Applications or Modules/ if (res && res.code.to_i == 200 && res.body.match(p) != nil) success = true @@ -185,6 +142,10 @@ class Metasploit3 < Msf::Auxiliary print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'" do_report(ip, rport, result) :next_user + when Metasploit::Model::Login::Status::DENIED_ACCESS + print_brute :level => :status, :ip => ip, :msg => "Correct credentials, but unable to login: '#{result.credential}'" + do_report(ip, rport, result) + :next_user when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT print_brute :level => :verror, :ip => ip, :msg => "Could not connect" invalidate_login( @@ -215,74 +176,26 @@ class Metasploit3 < Msf::Auxiliary end - def init_bruteforce - res = nil - tried = false - - begin - print_status("Sending a request to /common/index.jsf...") - res = send_request_cgi({'uri'=>'/common/index.jsf'}) - set_jsession(res) - - # Abort if res returns nil due to an exception (broken pipe or timeout) - if res.nil? - print_error('Unable to get a response from the server.') - return - end - - # Automatic HTTP to HTTPS transition (when needed) - if @scanner.ssl == false && res && res.headers['Location'] =~ /^https:\/\// - print_status("Glassfish is asking us to use HTTPS") - print_status("SSL option automatically set to: true") - print_status("SSL version option automatically set to: #{datastore['SSLVersion']}") - @scanner.ssl = true - @scanner.ssl_version = datastore['SSLVersion'] - # Set the SSL options, and let the exception handler to resend the HTTP request - # one more time. - raise "SSL error" - end - rescue ::Exception => e - # Retry the HTTP request with updated SSL options - if e.message == 'SSL error' && tried == false - tried = true - retry - else - # Make sure we don't shut other problems up - raise e - end - end - - # A normal client starts with /login.jsf, so we start with /login.jsf - if res && res.code.to_i == 302 - res = send_request_cgi({'uri' => '/login.jsf'}) - set_jsession(res) - end - - res - end - # # main # def run_host(ip) init_loginscanner(ip) - res = init_bruteforce - edition, version, banner = get_version(res) - @scanner.version = version + msg = @scanner.check_setup + if msg + print_brute :level => :error, :ip => rhost, :msg => msg + return + end - print_status('Checking if Glassfish requires a password...') - if version =~ /^[239]\.x$/ && is_password_required?(version) + print_brute :level=>:status, :ip=>rhost, :msg=>('Checking if Glassfish requires a password...') + if @scanner.version =~ /^[239]\.x$/ && is_password_required?(@scanner.version) print_brute :level => :good, :ip => ip, :msg => "Note: This Glassfish does not require a password" else - print_status("Glassfish is protected with a password") + print_brute :level=>:status, :ip=>rhost, :msg=>("Glassfish is protected with a password") end - begin - bruteforce(ip) unless version.blank? - rescue ::Metasploit::Framework::LoginScanner::GlassfishError => e - print_error(e.message) - end + bruteforce(ip) unless @scanner.version.blank? end end diff --git a/modules/auxiliary/scanner/http/http_login.rb b/modules/auxiliary/scanner/http/http_login.rb index 0238e3c266..3ff9137690 100644 --- a/modules/auxiliary/scanner/http/http_login.rb +++ b/modules/auxiliary/scanner/http/http_login.rb @@ -142,6 +142,12 @@ class Metasploit3 < Msf::Auxiliary connection_timeout: 5, ) + msg = scanner.check_setup + if msg + print_brute :level => :error, :ip => ip, :msg => "Verification failed: #{msg}" + return + end + scanner.scan! do |result| credential_data = result.to_h credential_data.merge!( @@ -162,9 +168,6 @@ class Metasploit3 < Msf::Auxiliary when Metasploit::Model::Login::Status::INCORRECT print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'" invalidate_login(credential_data) - when Metasploit::Model::Login::Status::NO_AUTH_REQUIRED - print_brute :level => :error, :ip => ip, :msg => "Failed: '#{result.credential}'" - break end end diff --git a/spec/lib/metasploit/framework/login_scanner/glassfish_spec.rb b/spec/lib/metasploit/framework/login_scanner/glassfish_spec.rb index 73d3826a00..49ff0dc0a8 100644 --- a/spec/lib/metasploit/framework/login_scanner/glassfish_spec.rb +++ b/spec/lib/metasploit/framework/login_scanner/glassfish_spec.rb @@ -63,7 +63,7 @@ describe Metasploit::Framework::LoginScanner::Glassfish do end before do - http_scanner.version = good_version + http_scanner.instance_variable_set(:@version, good_version) end context '#send_request' do @@ -222,13 +222,6 @@ describe Metasploit::Framework::LoginScanner::Glassfish do end end - context 'when unsupported Glassfish version' do - it 'raises a GlassfishError exception' do - http_scanner.version = bad_version - expect { http_scanner.attempt_login(cred) }.to raise_exception(Metasploit::Framework::LoginScanner::GlassfishError) - end - end - context 'when Glassfish version 2' do let(:login_ok_message) do '<title>Deploy Enterprise Applications/Modules' @@ -303,5 +296,14 @@ describe Metasploit::Framework::LoginScanner::Glassfish do end end + context '#extract_version' do + let(:server_header) { "GlassFish Server Open Source Edition 4.0" } + + specify do + expect(http_scanner.extract_version(server_header)).to eq("4.0") + end + + end + end