Pwning UniFi Security Gateways using a 6-year old vulnerability in strongSwan (CVE-2015-3991)

Overview

  • CVE: CVE-2015-3991
  • Type: CWE-78: Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)
  • CVSSv2.0 Base Score: 7.5
  • CVSSv3.x Base Score: 9.8
  • Vendor: Ubiquiti
  • Product: UniFi Security Gateway
  • Affected versions: v4.4.51 and below
  • Patched version: v4.4.52

Introduction

Discovered by an independent security researcher in 2015, CVE-2015-3991 was identified as a potential denial-of-service (DoS) vulnerability in the strongSwan IPSec implementation. The strongSwan maintainers responded quickly by providing a patch in June 2015 and also acknowledged the potential for remote code execution.

In this blog post, I will cover the details of the vulnerability, guide through the various pitfalls in exploiting and publish the first public remote code execution POC for this vulnerability. The POC targets the MIPS32 architecture and drops a root shell after sending a single IKE packet to an UniFi Security Gateway.

It is noteworthy to mention that prior to the availability of a patch, several thousand UniFi Security Gateways accessible via the internet were susceptible to this vulnerability.

Background

UniFi is a product line developed by Ubiquiti targeting small to medium-sized business (SMB) and home user segments with their networking products and services. It includes a range of wireless access points, switches, routers, and other networking devices that can be centrally managed using a web-based software application called UniFi Controller.

From the vendor’s datasheet:

The UniFi Security Gateway combines reliable security features with high‑performance routing technology in a cost‑effective unit. …

Technical Background

IPSec (Internet Protocol Security) is a suite of protocols that provides end-to-end security for IP (Internet Protocol) traffic. IPSec operates at the network layer and can be used to secure communication between hosts and networks. It has two modes of operation: transport mode and tunnel mode. In transport mode, only the payload of the IP packet is encrypted, while in tunnel mode, the entire IP packet, including the IP header, is encrypted. IPSec provides several security services, including confidentiality, integrity, and authentication, and can be used with a variety of encryption algorithms and key exchange protocols.

IPSec has been criticized by the broader security community since its specification. Not only can it be difficult to configure and manage, particularly in large-scale deployments, but many experts have expressed concerns about its complexity, which makes it difficult to implement and maintain.

In February 1999, Bruce Schneier and Niels Ferguson published a cryptographic analysis of IPsec that revealed numerous shortcomings. The paper concludes with the following pivotal statement:

.. we do not believe that it will ever result in a secure operational system. It is far too complex, and the complexity has lead to a large number of ambiguities, contradictions, inefficiencies, and weaknesses. It has been very hard work to perform any kind of security analysis; we do not feel that we fully understand the system, let alone have fully analyzed it.

Despite these criticisms, IPSec remains a widely used and important protocol for securing internet communications.

In order to understand the background of the vulnerability, a basic understanding of the IKEv1 and IKEv2 protocol, as well as general understanding of pointer arithmetic and memory layout is required. The IKE (Internet Key Exchange) protocol is a central component of the IPSec protocol suite, as it is responsible for the secure exchange and negotiation of the key material.

What follows is a brief introduction to the IKEv1 and IKEv2 protocols, as well as the strongSwan IPSec implementation.

IKE

IKEv1, as defined in RFC 2409, is an older version of the protocol and is widely used in legacy systems. It uses a two-phase negotiation process to establish a secure connection between the two devices. In the first phase, the devices establish a secure channel to negotiate the parameters for the IPSec connection. In the second phase, the devices use the negotiated parameters to establish the actual IPSec connection.

IKEv2, as defined in RFC 4306, is a newer version of the protocol that was introduced to improve upon the limitations of IKEv1. IKEv2 uses a four-message exchange to establish a secure connection, reducing the time it takes to establish a connection. It also supports more advanced authentication methods, such as certificate-based authentication, and provides better support for mobility and multi-homing. IKEv2 has become the preferred version of the protocol for most modern IPSec VPN deployments due to its improved security and flexibility.

                         1                   2                   3
     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    !                          Initiator                            !
    !                            Cookie                             !
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    !                          Responder                            !
    !                            Cookie                             !
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    !  Next Payload ! MjVer ! MnVer ! Exchange Type !     Flags     !
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    !                          Message ID                           !
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    !                            Length                             !
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+


                      Figure 1:  Generic IKE Header

Generic IKE payload header

                           1                   2                   3
       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      ! Next Payload  !C!  RESERVED   !         Payload Length        !
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                         Figure 2:  Generic Payload Header

The IKE header encompasses initiator and responder cookies that serve multiple purposes, including identifying Security Associations (SAs), providing rudimentary defense against denial-of-service attacks, and more. Additionally, the header contains the first payload’s IKE payload ID, the major and minor versions of the IKE, and an exchange type that communicates various details about the ongoing IKE session, such as its phase, purpose, and mandatory payloads. The flags offer information on the encryption and integrity protection. Finally, the header ends with a message ID, used to detect retransmissions, and the total length of the message, encompassing all payloads.

A payload header precedes each payload, bearing the next payload header containing the payload ID of the next payload or zero if no payload follows and it’s length.

strongSwan

The key component of strongSwan is the charon daemon, which is responsible for managing the IKEv1 and IKEv2 key exchange protocols. charon was designed to be modular and flexible, allowing for the addition of custom plugins to support new authentication and encryption methods. It also supports multiple IPsec implementations, including the Linux kernel IPsec stack and the OpenSSL library.

Prior to the launch of strongSwan 5.0, the dedicated pluto daemon was responsible for handling IKEv1 packets. By default, charon listens to incoming IKEv1 and IKEv2 packets on port 500/UDP. Before strongSwan 5.0, charon would pass received IKEv1 packets to the pluto daemon. With the release of strongSwan 5.0, the processing of IKEv1 and IKEv2 packets was consolidated under charon, resulting in the discontinuation of the pluto daemon.

This consolidation laid the groundwork for CVE-2015-3991.

Vulnerability

CVE-2015-3991 can be triggered by sending an IKEv1 or IKEv2 packet that contains a payload defined only for the respective other IKE version. For instance, sending an IKEv1 message containing an IKEv2 payload of type 46 (IKEv2 Encrypted) will crash the daemon immediately. This stems from the fact that the payload parser in charon is shared between IKEv1 and IKEv2 message types and, in particular, parses all payloads contained in an IKE message, even if they are invalid for the IKE version of the containing message.

Let’s first look at the call stack when receiving and parsing an incoming IKE packet. When charon receives an IKE packet it spins up a process_message_job_t job which, based on the major version contained in the IKE header, decides if the packet should be parsed as IKEv1 or IKEv2.

Incoming packets are wrapped in a message_t structure and then passed to process_message of in task_manager_v1.c for IKEv1, or process_message in task_manager_v2.c for IKEv2 messages.

At this point it is noteworthy that the process_message function in ike_sa.c already performs a basic version validation against the current SA (Security Association) context. If the major version of the received IKE message differs from the major version of the current SA context, if one exists, the message is discarded. Unfortunately, this check is not sufficient, as we will see.

For simplicity, from here I will focus on processing an IKEv1 message with an (invalid) IKEv2 payload of type PLV2_ENCRYPTED (46). As a preliminary note, it is important to mention that by specification, not all payloads in an IKE message necessarily have to be parsed, so a proper implementation should always skip invalid payloads.

The task manager initiates the parsing of message payloads by invoking the parse_body function on the message_t structure. To effectively manage various payload types, charon makes heavy use of a hierarchical class model based C structs.

The parse_payloads function sequentially processes each payload until the value of the Next Payload header field becomes PL_NONE (0). A payload is passed to the parse_payload function in parser.c, which returns a parent class of type payload_t that encapsulates the corresponding paylaod.

To understand the actual vulnerability, it is worth taking a closer look at parse_payload function:

METHOD(parser_t, parse_payload, status_t,
    private_parser_t *this, payload_type_t payload_type, payload_t **payload)
{
    payload_t *pld;
    void *output;
    int payload_length = 0, spi_size = 0, attribute_length = 0, header_length;
    u_int16_t ts_type = 0;
    bool attribute_format = FALSE;
    int rule_number, rule_count;
    encoding_rule_t *rule;

    /* create instance of the payload to parse */
    if (payload_is_known(payload_type, this->major_version))
    {
        pld = payload_create(payload_type);
    }
    else
    {
        pld = (payload_t*)unknown_payload_create(payload_type);
    }

    ...
}
bool payload_is_known(payload_type_t type, u_int8_t maj_ver)
{
    if (type >= PL_HEADER)
    {
        return TRUE;
    }
    switch (maj_ver)
    {
        case 0:
        case IKEV1_MAJOR_VERSION:
            if (type >= PLV1_SECURITY_ASSOCIATION && type <= PLV1_CONFIGURATION)
            {
                return TRUE;
            }

            ...

            if (maj_ver)
            {
                break;
            }
            /* fall-through */
        case IKEV2_MAJOR_VERSION:
            ...
        default:
            break;
    }
    return FALSE;
}

Remember that we previously sent an encapsulated IKEv2 payload in an IKEv1 message. As a result, the switch statement jumps into the IKEV1_MAJOR_VERSION case. All of the type checks within this case statement will fail since our IKEv2 payload is not a valid inside an IKEv1 message. The function evaluates to FALSE, because the major version of the IKEv1 message is 1 and thus the break statement is executed.

Subsequently, the unknown_payload_create function creates a private_unknown_payload_t structure that contains another structure of type unknown_payload_t, while preserving the original IKEv2 payload identifier.

unknown_payload_t

struct unknown_payload_t {
    payload_t payload_interface;
    chunk_t (*get_data) (unknown_payload_t *this);
    bool (*is_critical) (unknown_payload_t *this);
    void (*destroy) (unknown_payload_t *this);
};

private_unknown_payload_t

struct private_unknown_payload_t {
    unknown_payload_t public;
    payload_type_t type;
    u_int8_t next_payload;
    bool critical;
    bool reserved[7];
    u_int16_t payload_length;
    chunk_t data;
};

Back in the parse_body function, a list of all payloads is passed to the decrypt_payloads function which invokes the following snippet on each encrypted payload:

parse_body

if (type == PLV2_ENCRYPTED || type == PLV1_ENCRYPTED)
{
    encrypted_payload_t *encryption;

    DBG2(DBG_ENC, "found an encrypted payload");
    encryption = (encrypted_payload_t*)payload;
    this->payloads->remove_at(this->payloads, enumerator);

    if (enumerator->enumerate(enumerator, NULL))
    {
        DBG1(DBG_ENC, "encrypted payload is not last payload");
        encryption->destroy(encryption);
        status = VERIFY_ERROR;
        break;
    }
    status = decrypt_and_extract(this, keymat, previous, encryption);
    encryption->destroy(encryption);
    if (status != SUCCESS)
    {
        break;
    }
    was_encrypted = "encrypted payload";
}

Recall that we are processing an IKEv1 message carrying an IKEv2 payload of type PLV2_ENCRYPTED (46), wrapped inside the unknown_payload_t type which retained the payload type ID. Therefore, the first condition evaluates to true and we enter the section.

If you looked closely, you may have noticed the typecast at line 6. There the payload, currently of generic type payload_t is cast to the expected type encrypted_payload_t without any further check. However, our payload carried the ID of a PLV2_ENCRYPTED payload, even though it was originally parsed as unknown_payload_t instead.

encrypted_payload_t

struct encrypted_payload_t {
    payload_t payload_interface;
    size_t (*get_length)(encrypted_payload_t *this);
    void (*add_payload) (encrypted_payload_t *this, payload_t *payload);
    payload_t* (*remove_payload)(encrypted_payload_t *this);
    void (*generate_payloads)(encrypted_payload_t *this,
                              generator_t *generator);
    void (*set_transform) (encrypted_payload_t *this, aead_t *aead);
    status_t (*encrypt) (encrypted_payload_t *this, u_int64_t mid,
                         chunk_t assoc);
    status_t (*decrypt) (encrypted_payload_t *this, chunk_t assoc);
    void (*destroy) (encrypted_payload_t *this);
};

This is extremely dangerous as the encrypted_payload_t type is larger than the unknown_type_t type. The call to the destroy function of the payload at line 12 and 17 now points to an offset outside the parsed payload of type unknown_payload_t. In the best case, this leads to a crash.

The security researcher who originally discovered the vulnerability described the impact as follows:

When the IKE daemon crashes, it may or may not be restarted.
If it is restarted, it gives the attacker as many attempts as they want to get the IKE daemon into the startup state. Result: unknown.
If it is not restarted, the keys do not get changed. When the IV gets repeated, the stream loses some confidentiality and integrity. Replay becomes easy. Result: possible compromise.
If the system decides that the two systems should no longer use IPsec, the system may revert back to IP silently. Result: possible complete compromise.
If the system decides to instead stop sending packets to the affected system, this becomes a denial of service. Result: complete availability compromise.

Exploitation

Let’s see how we can turn this denial-of-service vector into a remote code execution exploit on UniFi Security Gateways.

The UniFi Security Gateways are based on a MIPS Cavium Octeon chipset. The firmware is based on a customized 32-bit Vyatta clone, which has been adapted to the UniFi ecosystem through various configuration services. The Octeon chipset supports the MIPS64 Release 2 ISA, but the firmware does not make use of the XD bit (MIPS NX equivalent), which in conjunction with the 32-bit firmware ultimately makes exploiting this vulnerabilitz possible.

As mentioned previously, when the IKEv2 payload is parsed, a struct of type private_unknown_payload_t is created. This structure has a total size of 64 bytes and embeds a structure of type chunk_t. chunk_t contains a pointer to the payload’s data, as well as its length. In the overall structure, the pointer to the data is located at offset 56.

chunk_t

struct chunk_t {
    u_char *ptr; // start of data
    size_t len; // length in bytes
};

For further processing, however, the called unknown_payload_create function returns only the contained public type unknown_payload_t. This public type consists of the generic public type payload_t, and three function pointers, namely get_data, is_critical and destroy. Compared to the public type encrypted_payload_t, this is 20 bytes larger, which moves the pointer of the destroy function to offset 56.

Now it becomes clear that, following a cast from type unknown_payload_t to type encrypted_payload_t, the destroy function pointer becomes the data pointer of the chunk_t structure contained in the parent type private_unknown_payload_t. So when the destroy function is called, the instructions contained in the payload data are executed.

The most alarming issue is that any IKE packet can access this code path without the need for an existing SA context. As a result, the payload can contain raw, unencrypted MIPS32 bytecode, which will be executed directly.

A crafted malicious IKE packet might look like this:

## Header
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, # Random IKE_SA initiator SPI
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # Random IKE_SA responder SPI
0x2e,                                           # Next payload (PLV2_ENCRYPTED, 46)
0x10,                                           # Version (Major/Minor)
0x02,                                           # Exchange type
0x00,                                           # Flags
0x00, 0x00, 0x00, 0x00,                         # Message ID
0x00, 0x00, 0x00, 0xFF,                         # Length

## PLV2_ENCRYPTED payload
0x00,                                           # Next payload (None)
0x00,                                           # Reserved
0x00, 0xFF,                                     # Payload length
<MIPS32 shell code>

                   Figure 3:  Malicious IKE Packet

Proof-of-Concept

The crafted packet might contain the following MIPS32 shell code that will replace the running charon daemon with an root shell. The shellcode can be easily modified to connect to a remote endpoint for establishing a remote root shell. The attached python script contains a POC that will drop a root shell by only passing a host address of a vulnerable target.

CVE

CVE-2015-3991

CVSSv3.1 Base Score

CVSS Base Score: 9.8 CVSS: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/E:H/RL:O/RC:C

Disclaimer

The information provided is released “as is” without warranty of any kind. The publisher disclaims all warranties, either express or implied, including all warranties of merchantability. No responsibility is taken for the correctness of this information. In no event shall the publisher be liable for any damages whatsoever including direct, indirect, incidental, consequential, loss of business profits or special damages, even if the publisher has been advised of the possibility of such damages.

The contents of this advisory are copyright (c) 2021 Hendrik Hagendorn and may be distributed freely provided that no fee is charged for this distribution and proper credit is given.