Skip to content

Commit 8c2c30c

Browse files
committed
Land rapid7#9330, add MQTT scanner
2 parents 8ea5057 + ae17943 commit 8c2c30c

File tree

5 files changed

+328
-3
lines changed

5 files changed

+328
-3
lines changed

data/wordlists/unix_passwords.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
admin
12
123456
23
12345
34
123456789
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
## Vulnerable Application
2+
3+
Most any MQTT instance will work. Instructions for testing against a Dockerized endpoint are provided below.
4+
5+
### Docker Install
6+
7+
A dockerized version of [mosquitto](https://mosquitto.org/) is available
8+
[here](https://github.com/toke/docker-mosquitto). There are two basic
9+
scenarios worth discussing -- mosquitto with anonymous authentication allowed
10+
and disallowed. The method for running both is similar.
11+
12+
#### Docker MQTT Server With Anonymous Authentication
13+
14+
By default, mosquitto does not require credentials and allows anonymous authentication. To run in this way:
15+
16+
```
17+
$ docker run -i -p 1883:1883 toke/mosquitto
18+
1513822879: mosquitto version 1.4.14 (build date Mon, 10 Jul 2017 23:48:43 +0100) starting
19+
1513822879: Config loaded from /mqtt/config/mosquitto.conf.
20+
1513822879: Opening websockets listen socket on port 9001.
21+
1513822879: Opening ipv4 listen socket on port 1883.
22+
1513822879: Opening ipv6 listen socket on port 1883.
23+
```
24+
25+
#### Docker MQTT Server Without Anonymous Authenticaiton
26+
27+
Msquitto can be configured to require credentials. To run in this way:
28+
29+
1. Create a simple configuration file:
30+
31+
```
32+
$ mkdir -p config && cat > config/mosquitto.conf
33+
password_file /mqtt/config/passwd
34+
allow_anonymous false
35+
```
36+
37+
2. Create a password file for mosquitto (this example creates a user admin wtth password admin)
38+
39+
```
40+
$ touch config/passwd && mosquitto_passwd -b config/passwd admin admin
41+
```
42+
43+
3. Now run the dockerized mosquitto instance, mounting the configuration files from above for use at runtime:
44+
45+
```
46+
$ docker run -ti -p 1883:1883 -v `pwd`/config/:/mqtt/config:ro toke/mosquitto
47+
1513823564: mosquitto version 1.4.14 (build date Mon, 10 Jul 2017 23:48:43 +0100) starting
48+
1513823564: Config loaded from /mqtt/config/mosquitto.conf.
49+
1513823564: Opening ipv4 listen socket on port 1883.
50+
1513823564: Opening ipv6 listen socket on port 1883.
51+
```
52+
53+
## Verification Steps
54+
55+
56+
1. Install the application without credentials
57+
2. Start msfconsole
58+
3. Do: `use auxiliary/scanner/mqtt/connect`
59+
4. Do: `set rhosts [IPs]`
60+
5. Do: `run`
61+
6. Confirm that the default or non-default credentials are discovered as configured
62+
63+
## Options
64+
65+
**CLIENT_ID**
66+
67+
When specified, this will set the ID of the client when connecting to the MQTT endpoint. While
68+
not all MQTT implementation support this, some, like mosquitto, support filtering by client ID and
69+
this option can be used in those scenarios. By default, a random ID is selected.
70+
71+
**READ_TIMEOUT**
72+
73+
The amount of time, in seconds, to wait for responses from the MQTT endpoint.
74+
75+
## Scenarios
76+
77+
### Docker MQTT Server With Anonymous Authentication
78+
79+
Configure MQTT in a Docker container without credentials as described above.
80+
81+
```
82+
> use auxiliary/scanner/mqtt/connect
83+
> set VERBOSE false
84+
VERBOSE => false
85+
> set RHOSTS localhost
86+
RHOSTS => localhost
87+
> run
88+
[+] 127.0.0.1:1883 - Does not require authentication
89+
[*] Scanned 1 of 1 hosts (100% complete)
90+
```
91+
92+
### Docker MQTT Server Without Anonymous Authentication
93+
94+
Configure MQTT in a Docker container with credentials as described above.
95+
96+
```
97+
> use auxiliary/scanner/mqtt/connect
98+
> set VERBOSE false
99+
FALSE => false
100+
resource (mqtt.rc)> set RHOSTS localhost
101+
RHOSTS => localhost
102+
resource (mqtt.rc)> run
103+
...
104+
[+] 127.0.0.1:1883 - MQTT Login Successful: admin/admin
105+
106+
```
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
require 'metasploit/framework/tcp/client'
2+
require 'rex/proto/mqtt'
3+
require 'metasploit/framework/login_scanner/base'
4+
require 'metasploit/framework/login_scanner/rex_socket'
5+
6+
module Metasploit
7+
module Framework
8+
module LoginScanner
9+
# This is the LoginScanner class for dealing with MQTT.
10+
# It is responsible for taking a single target, and a list of
11+
# credentials and attempting them. It then saves the results.
12+
class MQTT
13+
include Metasploit::Framework::LoginScanner::Base
14+
include Metasploit::Framework::LoginScanner::RexSocket
15+
include Metasploit::Framework::Tcp::Client
16+
17+
#
18+
# CONSTANTS
19+
#
20+
DEFAULT_PORT = Rex::Proto::MQTT::DEFAULT_PORT
21+
DEFAULT_SSL_PORT = Rex::Proto::MQTT::DEFAULT_SSL_PORT
22+
LIKELY_PORTS = [ DEFAULT_PORT, DEFAULT_SSL_PORT ]
23+
LIKELY_SERVICE_NAMES = [ 'MQTT' ]
24+
PRIVATE_TYPES = [ :password ]
25+
REALM_KEY = nil
26+
27+
# @!attribute read_timeout
28+
# @return [int] The timeout use while reading responses from MQTT, in seconds
29+
attr_accessor :read_timeout
30+
31+
# @!attribute client_id
32+
# @return [String] The client identifier to use when connecting to MQTT
33+
attr_accessor :client_id
34+
35+
# This method attempts a single login with a single credential against the target
36+
# @param credential [Credential] The credential object to attmpt to login with
37+
# @return [Metasploit::Framework::LoginScanner::Result] The LoginScanner Result object
38+
def attempt_login(credential)
39+
result_options = {
40+
credential: credential,
41+
host: host,
42+
port: port,
43+
protocol: 'tcp',
44+
service_name: 'MQTT'
45+
}
46+
47+
begin
48+
# Make our initial socket to the target
49+
disconnect if self.sock
50+
connect
51+
52+
client_opts = {
53+
username: credential.public,
54+
password: credential.private,
55+
read_timeout: read_timeout,
56+
client_id: client_id
57+
}
58+
client = Rex::Proto::MQTT::Client.new(sock, client_opts)
59+
connect_res = client.connect
60+
client.disconnect
61+
62+
if connect_res.return_code == 0
63+
status = Metasploit::Model::Login::Status::SUCCESSFUL
64+
proof = "Successful Connection (Received CONNACK packet)"
65+
else
66+
status = Metasploit::Model::Login::Status::INCORRECT
67+
proof = "Failed Connection (#{connect_res.return_code})"
68+
end
69+
70+
result_options.merge!(
71+
proof: proof,
72+
status: status
73+
)
74+
rescue ::EOFError, Errno::ENOTCONN, Rex::ConnectionError, ::Timeout::Error => e
75+
result_options.merge!(
76+
proof: e.message,
77+
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
78+
)
79+
ensure
80+
disconnect
81+
end
82+
83+
::Metasploit::Framework::LoginScanner::Result.new(result_options)
84+
end
85+
end
86+
end
87+
end
88+
end

lib/rex/proto/mqtt/client.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@ def initialize(sock, opts = {})
1818

1919
def connect
2020
connect_opts = {
21-
client_id: @opts[:client_id],
22-
username: @opts[:username],
23-
password: @opts[:password]
21+
client_id: @opts[:client_id]
2422
}
23+
24+
unless @opts[:username].blank?
25+
connect_opts[:username] = @opts[:username]
26+
end
27+
unless @opts[:password].blank?
28+
connect_opts[:password] = @opts[:password]
29+
end
2530
connect = ::MQTT::Packet::Connect.new(connect_opts).to_s
2631
@sock.put(connect)
2732
res = @sock.get_once(-1, @opts[:read_timeout])
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'metasploit/framework/credential_collection'
7+
require 'metasploit/framework/login_scanner/mqtt'
8+
9+
class MetasploitModule < Msf::Auxiliary
10+
include Msf::Exploit::Remote::Tcp
11+
include Msf::Auxiliary::Scanner
12+
include Msf::Auxiliary::MQTT
13+
include Msf::Auxiliary::Report
14+
include Msf::Auxiliary::AuthBrute
15+
16+
def initialize
17+
super(
18+
'Name' => 'MQTT Authentication Scanner',
19+
'Description' => %q(
20+
This module attempts to authenticate to MQTT.
21+
),
22+
'Author' =>
23+
[
24+
'Jon Hart <jon_hart[at]rapid7.com>'
25+
],
26+
'References' =>
27+
[
28+
['URL', 'http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_3.1_-']
29+
],
30+
'License' => MSF_LICENSE,
31+
'DefaultOptions' =>
32+
{
33+
'BLANK_PASSWORDS' => false,
34+
'USER_AS_PASS' => true,
35+
'USER_FILE' => 'data/wordlists/unix_users.txt',
36+
'PASS_FILE' => 'data/wordlists/unix_passwords.txt'
37+
}
38+
)
39+
end
40+
41+
def test_login(username, password)
42+
client_opts = {
43+
username: username,
44+
password: password,
45+
read_timeout: read_timeout,
46+
client_id: client_id
47+
}
48+
connect
49+
client = Rex::Proto::MQTT::Client.new(sock, client_opts)
50+
connect_res = client.connect
51+
client.disconnect
52+
connect_res.return_code.zero?
53+
end
54+
55+
def default_login
56+
vprint_status("Testing without credentials")
57+
if test_login('', '')
58+
print_good("Does not require authentication")
59+
end
60+
61+
end
62+
63+
def run_host(_ip)
64+
unless default_login
65+
brute
66+
end
67+
end
68+
69+
def brute
70+
vprint_status("Starting MQTT login sweep")
71+
72+
cred_collection = Metasploit::Framework::CredentialCollection.new(
73+
blank_passwords: datastore['BLANK_PASSWORDS'],
74+
pass_file: datastore['PASS_FILE'],
75+
password: datastore['PASSWORD'],
76+
user_file: datastore['USER_FILE'],
77+
userpass_file: datastore['USERPASS_FILE'],
78+
username: datastore['USERNAME'],
79+
user_as_pass: datastore['USER_AS_PASS']
80+
)
81+
82+
cred_collection = prepend_db_passwords(cred_collection)
83+
84+
scanner = Metasploit::Framework::LoginScanner::MQTT.new(
85+
host: rhost,
86+
port: rport,
87+
read_timeout: datastore['READ_TIMEOUT'],
88+
client_id: client_id,
89+
proxies: datastore['PROXIES'],
90+
cred_details: cred_collection,
91+
stop_on_success: datastore['STOP_ON_SUCCESS'],
92+
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
93+
connection_timeout: datastore['ConnectTimeout'],
94+
max_send_size: datastore['TCP::max_send_size'],
95+
send_delay: datastore['TCP::send_delay'],
96+
framework: framework,
97+
framework_module: self,
98+
ssl: datastore['SSL'],
99+
ssl_version: datastore['SSLVersion'],
100+
ssl_verify_mode: datastore['SSLVerifyMode'],
101+
ssl_cipher: datastore['SSLCipher'],
102+
local_port: datastore['CPORT'],
103+
local_host: datastore['CHOST']
104+
)
105+
106+
scanner.scan! do |result|
107+
credential_data = result.to_h
108+
credential_data.merge!(
109+
module_fullname: fullname,
110+
workspace_id: myworkspace_id
111+
)
112+
password = result.credential.private
113+
username = result.credential.public
114+
if result.success?
115+
credential_core = create_credential(credential_data)
116+
credential_data[:core] = credential_core
117+
create_credential_login(credential_data)
118+
print_good("MQTT Login Successful: #{username}/#{password}")
119+
else
120+
invalidate_login(credential_data)
121+
vprint_error("MQTT LOGIN FAILED: #{username}/#{password} (#{result.proof})")
122+
end
123+
end
124+
end
125+
end

0 commit comments

Comments
 (0)