From 97d67c6a79bb0c270ec904b402d3c2b78fdd0a65 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 20 Mar 2023 16:27:11 -0400 Subject: [PATCH 1/2] Add an AMQP login scanner --- .../auxiliary/scanner/amqp/amqp_login.md | 98 +++++++++++++++++++ .../framework/login_scanner/amqp.rb | 89 +++++++++++++++++ lib/rex/proto/amqp.rb | 8 ++ modules/auxiliary/scanner/amqp/amqp_login.rb | 89 +++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 documentation/modules/auxiliary/scanner/amqp/amqp_login.md create mode 100644 lib/metasploit/framework/login_scanner/amqp.rb create mode 100644 lib/rex/proto/amqp.rb create mode 100644 modules/auxiliary/scanner/amqp/amqp_login.rb diff --git a/documentation/modules/auxiliary/scanner/amqp/amqp_login.md b/documentation/modules/auxiliary/scanner/amqp/amqp_login.md new file mode 100644 index 0000000000..21e34c1d42 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/amqp/amqp_login.md @@ -0,0 +1,98 @@ +## Vulnerable Application + +This module will test AMQP logins on a range of machines and report successful logins. If you have loaded a database +plugin and connected to a database this module will record successful logins and hosts so you can track your access. + +## Verification Steps + +1. Install RabbitMQ and start it + 1. To use Docker, run: `docker run --rm -it --hostname "$(hostname)" -p 15672:15672 -p 5672:5672 rabbitmq:3-management` +2. Start msfconsole +3. Do: `use auxiliary/scanner/amqp/amqp_login` +4. Do: `set rhosts` +5. Do: set usernames and passwords via any of the available options +6. Do: `run` + +## Options + +### BLANK_PASSWORD + +Boolean value on if an additional login attempt should be attempted with an empty password for every user. + +### PASSWORD + +Password to try for each user. + +### PASS_FILE + +A file containing a password on every line. Kali linux example: `/usr/share/wordlists/metasploit/password.lst` + +### STOP_ON_SUCCESS + +If a valid login is found on a host, immediately stop attempting additional logins on that host. + +### USERNAME + +Username to try for each password. + +### USERPASS_FILE + +A file containing a username and password, separated by a space, on every line. An example line would be `username +password`. + +### USER_AS_PASS + +Boolean value on if an additional login attempt should be attempted with the password as the username. + +### USER_FILE + +A file containing a username on every line. + +### VERBOSE + +Show a failed login attempt. This can get rather verbose when large `USER_FILE`s or `PASS_FILE`s are used. A failed +attempt will look similar to the following: + +``` +[-] 192.168.159.128:5672 - LOGIN FAILED: admin:Password1! (Incorrect: ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.) +``` + +## Option Combinations + +It is important to note that usernames and passwords can be entered in multiple combinations. For instance, a password +could be set in `PASSWORD`, be part of either `PASS_FILE` or `USERPASS_FILE`, be guessed via `USER_AS_PASS` or +`BLANK_PASSWORDS`. This module makes a combination of all of the above when attempting logins. So if a password is set +in `PASSWORD`, and a `PASS_FILE` is listed, passwords will be generated from BOTH of these. + +## Scenarios +### RabbitMQ 3.11.10 on Docker + +The Docker container listens on 5672/tcp without SSL. There's also an administrative site running on 15672/tcp where +users can be added. The default credentials to login are `guest` / `guest`. A new `admin` account was added for this +example. + +``` +msf6 > use auxiliary/scanner/amqp/amqp_login +msf6 auxiliary(scanner/amqp/amqp_login) > set RHOSTS 192.168.159.128 +RHOSTS => 192.168.159.128 +msf6 auxiliary(scanner/amqp/amqp_login) > set USERNAME admin +USERNAME => admin +msf6 auxiliary(scanner/amqp/amqp_login) > set PASS_FILE data/wordlists/unix_passwords.txt +PASS_FILE => data/wordlists/unix_passwords.txt +msf6 auxiliary(scanner/amqp/amqp_login) > set RPORT 5672 +RPORT => 5672 +msf6 auxiliary(scanner/amqp/amqp_login) > set SSL false +[!] Changing the SSL option's value may require changing RPORT! +SSL => false +msf6 auxiliary(scanner/amqp/amqp_login) > run + +[-] 192.168.159.128:5672 - LOGIN FAILED: admin:Password1! (Incorrect: ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.) +[-] 192.168.159.128:5672 - LOGIN FAILED: admin:admin (Incorrect: ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.) +[-] 192.168.159.128:5672 - LOGIN FAILED: admin:123456 (Incorrect: ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.) +[-] 192.168.159.128:5672 - LOGIN FAILED: admin:12345 (Incorrect: ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.) +[-] 192.168.159.128:5672 - LOGIN FAILED: admin:123456789 (Incorrect: ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.) +[+] 192.168.159.128:5672 - Login Successful: admin:password +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +msf6 auxiliary(scanner/amqp/amqp_login) > +``` diff --git a/lib/metasploit/framework/login_scanner/amqp.rb b/lib/metasploit/framework/login_scanner/amqp.rb new file mode 100644 index 0000000000..d9f5295e2b --- /dev/null +++ b/lib/metasploit/framework/login_scanner/amqp.rb @@ -0,0 +1,89 @@ +require 'metasploit/framework/login_scanner/base' +require 'metasploit/framework/login_scanner/rex_socket' +require 'rex/proto/amqp' + +module Metasploit + module Framework + module LoginScanner + + class AMQP + include Metasploit::Framework::LoginScanner::Base + include Metasploit::Framework::LoginScanner::RexSocket + + DEFAULT_PORT = 5671 + LIKELY_PORTS = [ DEFAULT_PORT, 5672 ] + LIKELY_SERVICE_NAMES = [ 'amqp', 'amqps' ] + PRIVATE_TYPES = [ :password ] + REALM_KEY = nil + + # (see Base#attempt_login) + def attempt_login(credential) + result_options = { + credential: credential + } + + begin + result_options.merge!(connect_login(credential.public, credential.private)) + rescue Rex::Proto::Amqp::Error::NegotiationError => e + result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + result_options[:proof] = e.message + rescue Rex::Proto::Amqp::Error::AmqpError + result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + rescue ::EOFError, Errno::ECONNRESET, Rex::ConnectionError, Rex::ConnectionTimeout, ::Timeout::Error + result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + result = ::Metasploit::Framework::LoginScanner::Result.new(result_options) + result.host = host + result.port = port + result.protocol = 'tcp' + result.service_name = "amqp#{ssl ? 's' : ''}" + result + end + + private + + def connect_login(username, password) + result = {} + amqp_client = Rex::Proto::Amqp::Version091::Client.new( + host, + port: port, + context: { 'Msf' => framework, 'MsfExploit' => framework_module }, + ssl: ssl, + ssl_version: ssl_version + ) + amqp_client.connect(connection_timeout) + amqp_client.send_protocol_header + amqp_client.connection_start(username, password) + resp = amqp_client.recv_frame + + unless resp.is_a?(Rex::Proto::Amqp::Version091::Frames::AmqpVersion091MethodFrame) + raise Rex::Proto::Amqp::Error::UnexpectedReplyError.new(resp) + end + + if resp.class_id == Rex::Proto::Amqp::Version091::Frames::MethodArguments::AmqpVersion091ConnectionClose::CLASS_ID && \ + resp.method_id == Rex::Proto::Amqp::Version091::Frames::MethodArguments::AmqpVersion091ConnectionClose::METHOD_ID + result[:status] = Metasploit::Model::Login::Status::INCORRECT + result[:proof] = resp.arguments.reply_text + return result + end + + unless resp.class_id == Rex::Proto::Amqp::Version091::Frames::MethodArguments::AmqpVersion091ConnectionTune::CLASS_ID && \ + resp.method_id == Rex::Proto::Amqp::Version091::Frames::MethodArguments::AmqpVersion091ConnectionTune::METHOD_ID + raise Rex::Proto::Amqp::Error::UnexpectedReplyError.new(resp) + end + + result[:status] = Metasploit::Model::Login::Status::SUCCESSFUL + result + ensure + amqp_client.close + end + + def set_sane_defaults + self.connection_timeout ||= 30 + self.port ||= DEFAULT_PORT + end + end + end + end +end diff --git a/lib/rex/proto/amqp.rb b/lib/rex/proto/amqp.rb new file mode 100644 index 0000000000..a2e121b164 --- /dev/null +++ b/lib/rex/proto/amqp.rb @@ -0,0 +1,8 @@ +# -*- coding: binary -*- + +module Rex::Proto::Amqp + + require 'rex/proto/amqp/error' + require 'rex/proto/amqp/version_0_9_1' + +end diff --git a/modules/auxiliary/scanner/amqp/amqp_login.rb b/modules/auxiliary/scanner/amqp/amqp_login.rb new file mode 100644 index 0000000000..7a21c0277d --- /dev/null +++ b/modules/auxiliary/scanner/amqp/amqp_login.rb @@ -0,0 +1,89 @@ +## +# 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/amqp' + +class MetasploitModule < Msf::Auxiliary + include Msf::Auxiliary::AuthBrute + include Msf::Auxiliary::Scanner + include Msf::Auxiliary::Report + + # Creates an instance of this module. + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'AMQP 0-9-1 Login Check Scanner', + 'Description' => %q{ + This module will test AMQP logins on a range of machines and + report successful logins. If you have loaded a database plugin + and connected to a database this module will record successful + logins and hosts so you can track your access. + }, + 'Author' => [ 'Spencer McIntyre' ], + 'License' => MSF_LICENSE, + 'References' => [ + [ 'URL', 'https://www.rabbitmq.com/amqp-0-9-1-reference.html' ] + ], + 'Notes' => { + 'Stability' => [], + 'Reliability' => [], + 'SideEffects' => [] + } + ) + ) + + register_options( + [ + Opt::RPORT(5671) + ] + ) + + register_advanced_options( + [ + OptBool.new('SSL', [ true, 'Negotiate SSL/TLS for outgoing connections', true ]), + Opt::SSLVersion + ] + ) + end + + def run_host(ip) + cred_collection = build_credential_collection( + username: datastore['USERNAME'], + password: datastore['PASSWORD'] + ) + + scanner = Metasploit::Framework::LoginScanner::AMQP.new( + host: ip, + port: datastore['RPORT'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + bruteforce_speed: datastore['BRUTEFORCE_SPEED'], + framework: framework, + framework_module: self, + ssl: datastore['SSL'], + ssl_version: datastore['SSLVersion'] + ) + + 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 "#{ip}:#{datastore['RPORT']} - Login Successful: #{result.credential}" + else + invalidate_login(credential_data) + vprint_error "#{ip}:#{datastore['RPORT']} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" + end + end + end +end From 7a2643304eb604a3b03563910c171c82b08c47f0 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 27 Mar 2023 16:48:40 -0400 Subject: [PATCH 2/2] Add a missing require line --- lib/rex/proto/amqp/version_0_9_1/client.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rex/proto/amqp/version_0_9_1/client.rb b/lib/rex/proto/amqp/version_0_9_1/client.rb index 1037141608..6c4fc703f2 100644 --- a/lib/rex/proto/amqp/version_0_9_1/client.rb +++ b/lib/rex/proto/amqp/version_0_9_1/client.rb @@ -1,5 +1,6 @@ class Rex::Proto::Amqp::Version091::Client + require 'rex/stopwatch' require 'rex/proto/amqp/error' require 'rex/proto/amqp/version_0_9_1/frames' require 'rex/proto/amqp/version_0_9_1/client/channel'