Achieving Telerik Remote Code Execution 100 Times Faster

22 min
I
October 27, 2021

Abstract‍

A cryptographic vulnerability from 2017 in the development software Telerik UI was considered impractical to exploit. Until now. Our research shows that the impracticability was due to the unoptimized nature of the publicly available exploit. Optimization leads to a practical exploit that puts infrastructures at risk of remote code execution. This blogpost discusses how the 2017 vulnerability was successfully exploited in a large corporation in 2021. It also details the optimization techniques deployed, which can also be applied to similar cryptographic issues in other software. 

Introduction

During a recent Red Team engagement at a Fortune500 company, only one Internet-facing vulnerability at one of the company’s subsidiaries seemed to be promising: A cryptographic weakness in Telerik UI (CVE-2017-9248). The problem was that the publicly available exploit was inefficient, making it impossible to gain access in a timely and undetectable manner.

Different optimization strategies helped us to increase the speed of the exploit by ~100 for the average case (and ~250 for our case, i.e., ~78k/314, and ~39k for the worst-case) and gained us an initial foothold from where we moved laterally to the headquarters. The blog post first provides a deep understanding of the vulnerability itself and the original exploit. With that in mind, a new, optimized version of the exploit is developed. Finally, the key takeaways in relation to exploit optimization are summed up. 

Hacking target: Applications using Progress Telerik products

Progress Telerik develops user interface(UI) controls and components for all .NET and JavaScript frameworks, helping teams to build engaging apps. According to the vendor website, Telerik products are used by more than 275,000 customers, including organizations such as Visa, Microsoft, and Samsung. Only one specific product (Telerik UI for ASP.NET AJAX) is affected by the vulnerability that this blogpost focusses on.

The risk arising from CVE-2017-9248

In 2017 a cryptographic weakness in one of the components of this specific product was identified. The vulnerable component(DialogHandler) allows invoking file browsers (Web UI Dialog Windows)and receives plaintext as well as sensitive encrypted parameters through a URL.Two of these encrypted parameters define, for example, whether files can be uploaded, and which file extensions are allowed. If the secret key can be guessed, a file browser can be invoked and arbitrary files can be uploaded, enabling unauthenticated attackers to compromise vulnerable websites via uploading .aspx web shells. Despite its discovery in 2017, the vulnerability persists in the wild and has been detected in even more recent engagements.

Existing exploit slow and detectable

A quick search gave us an exploit that should work in theory and which we tried out on the Red Team target. However, we cancelled the execution when we realized that the exploit would take too long to break the key, not to mention its noisiness (note: if the key character set would be limited, e.g., to hexadecimal, time was not a problem in our simulations). Being dissatisfied with the current outcome and driven by curiosity to search for a better solution, we continued and dove deeper into the vulnerability with the goal of optimizing the exploit.

Deep dive into CVE-2017-9248

The DialogHandler.aspx component can be thought of as a single function that receives plaintext and sensitive encrypted parameters (encoded in base64)for invoking different types of dialog windows. The interesting parameters are the encrypted parameters, since they allow enabling upload functionality and specifying valid file extensions. They are specified in the dp argument of the request and go through several conversions until the plaintext arguments are derived (see Figure 1).The goal of the exploit is figuring out the secret key that is stored on the server and used in an XOR operation for decrypting the transmitted parameters.

Sending different parameters to the server result in different error messages (note: error messages shown in the blog are abbreviations). These error messages can be used to guess the secret key.

Figure 1: Dialog window creation based on URL parameters

Table 1 shows the three possible error messages, which are used to guess the secret key, and where these error message scan occur in the server-side chain of operations. The “Index out of range” error message is referred to as the positive error message, which occurs in step 4 and implies that the guessed key is a potential candidate (it does not imply that the key is correct). “No valid base64” indicates that it is wrong. “String is too short” is listed here for the sake of completeness, but since the encoded strings sent to the server are base64-encoded, they will always be multiples of 4.

Table 1: Three possible error messages and their meaning

Figure 2 shows some examples of encoded strings sent to the server and what error message they get as a response. For example, the error message for request 7 shows that the encoded string ABCF could be successfully base64-decoded (1) and XOR-ed with the still unknown secret key on the server-side (2) and resulted in a valid base64 string (3). As mentioned in the previous section, it is essential to understand that this could have happened by accident and does not tell us that our guessed key is indeed correct.


Figure 2: Different encrypted strings and corresponding error messages

Figure 3 provides greater detail in the context of the chain of operations on the server-side. It depicts the exchange between sending the encrypted string to the server and receiving the response from it. 

Figure 3: Different encrypted strings in the context of the chain of operations

Underlying assumptions for exploits

Before we address the exploits, we would like to clarify two underlying assumptions to ensure comparability between the exploits:

  • We assumed that the secret key could consist of all 95 printable ASCII characters.
  • If no key is set by the user in the web.config, there are several fallback methods, which automatically generate a key with a length of 48. We also assumed that the secret key has a default length of 48 characters.
  • To exploit the vulnerability, the exact version of Telerik UI is needed (e.g., 2017.3.913)and encrypted with the other parameters. For simplicity reasons, it was left out in this blogpost.

Exploit 0: Most Trivial Brute-Forcing

The most trivial approach to brute-forcing the secret key consists of three nested loops: ‍

Loop 1 loops over each of the 12 secret key blocks, consisting of 4 characters each (12).‍

Loop 2 loops over each key character combination for each of these secret key blocks (95^4). ‍

Loop 3 loops over all possible combinations for the plaintext (64^4) and encrypts each of these combinations with the key character combinations from loop 2. Once there is a secret key block for which we get the positive error message for all possible combinations of the plaintext, we have found the correct secret key block.

The problem with this approach is that it does not make use of available information, e.g., the approach has no way to find out which key character caused the decryption to fail in the first place. Therefore, the worst case is 12 * 95^4 * 64^4 requests, which is astronomically high. 

Figure 4: Pseudo code of exploit 0

Exploit 1: First Workable Exploit

The one that we tried out at the very beginning consists of four main nested loops:

Loop 1 loops over the total key length (48) to guess each key character individually.

Loop 2 loops over all padding characters. Padding is necessary as base64 requires multiples of 4 as its input and the exploit code goes over each key character individually (e.g., when determining the second key character, two padding characters are needed). Padding (if correct) also helps to focus on one key character at a time; in other words, it helps us to trigger an error for a single character.

Loop 3 loops over the key character set, which consists of all possible characters for the key.

Loop 4 loops over the accuracy threshold, which consists of all possible characters that could appear in the (base64-encoded)plaintext (i.e., 64 characters). Within this last loop, the plaintext is created for which it is checked if the (so-far-guessed) key works. The assumption is that if the key is correct, any plaintext that we encrypt on our side should be decryptable on the server side. If the plaintext does not work, anew key character is chosen through loop 3 for the same padding. Once the exploit has gone through all key characters, it will jump to the next padding and go through the same procedure again.  The worst case is 48 (number of characters in key) * 256 (number of padding characters) * 95 (number of characters in key character set) * 64 (accuracy iterations) requests, which equals 2.37 years assuming a throttling of one second. [In reality, the worst case is slightly better since the number of padding characters decreases within each block. Think of two cases: To determine the first key character of a block, 3 padding characters are needed; to determine the third key character only 1 padding character is needed. It is clearly harder to find a valid padding for the first case. That means, there are different worst-cases depending on the number of padding characters needed.]

Figure 5: Pseudo code of exploit 1

Exploit 1 makes use of a couple of optimization strategies. For example, it focuses on one character at the time to reduce the possibility space for each iteration by using padding, pre-defines the possible character sets for the secret key (e.g., hexadecimal only), and makes use of a sorted list of plaintext characters. However, it has still not exhausted the full optimization potential. For example, information about working paddings in loop 2 are not reused and withdrawn for every new key character iteration.

Exploit 2: Fully Optimized Exploit‍

Our exploit approach, assuming a throttle of one second, has a worst-case duration of 32 minutes. The theory behind it is as follows: While the other exploits loop over each possible key character and try to confirm whether they are correct, we send probes that help us to exclude sets of possible key characters. For this, we determine offline for each possible key character and each encrypted character whether they result in positive or negative error messages. We choose three encrypted characters that group the key character set into three groups (“buckets”) (Step1). We determine all possible combinations of these encrypted characters and sort them by likelihood (Step 2). Using the sorted combinations of encrypted characters, we figure out which bucket the 4 characters of a key block belong to (Step 3). We then perform divide-and-conquer for each key character (Step 4)

Simplified example

  • Assume 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C,D, E, and F are the possible key characters
  • 0, 1, 2, 3, 4, 5, 6, 8, and 9 result in a positive error message when XOR-ed with the encrypted byte ‘0x??’ (this is a simplified example and therefore no specific byte is provided here)
  • A, B, C, D, E, and F instead result in a negative error message
  • If we send ‘0x??’ to the server and get a bad result, the possible keys characters are A, B, C, D, E, and F
  • In the next step we determine an encrypted character that returns a positive or negative result for half of the possible key characters
  • We continue divide-and-conquer until there is only one possible character

Step 1

We first need to find the detector bytes, which divide the key characters into buckets. A detector byte of a bucket always leads to a valid base 64 character if XOR-ed with characters of this specific bucket. Table 2.1-4 show the results of XOR combinations of different example detector bytes and key characters. The green color indicates that theXOR result is a valid base 64 character (i.e., positive error message).

Table 2: Example XOR results

Since the goal is to find a positive error message as quickly as possible (to then start the divide-and-conquer part),the first detector byte should cover as many characters of the key character set as possible. For all 95 printable ASCII characters, we determined three bytes, which divide the key characters into 3 buckets (Table 3).

Table 3: Key buckets and their detector bytes

Note: If password includes “=” sign (which is a valid base64 character), there is a little trick to be made, which will not be covered in this blog post.

Step 2‍

Next, we need to determine all possible combinations of detector bytes. Ordering them by likelihood minimizes the number of requests needed to find out the first valid combination. When ordering, it needs to be ensured that each position of the 4 characters is first checked with the byte of the largest bucket (i.e., 0x0) and then gradually with bytes of smaller buckets (i.e., 0x6B and 0x8). The logic: if 0x0 raises a negative error message, the possible key characters are in the buckets covered by 0x6B or 0x8. We would then send 0x6B. If that raises a negative error message, we will still send 0x8 to verify our assumption that the key consists of printable ASCII characters.  Table 4 shows the ordered detector bytes by likelihood.

Table 4 Top 10 detector byte combinations sorted by their likelihoods

Step 3‍

We then send combinations of detector bytes determined and ordered in step 2 from the top onwards to find valid buckets for each of the 4 characters that is being disclosed (e.g., {0x0, 0x0, 0x0, 0x0},{0x0, 0x0, 0x0, 0x6b}). Since each response of the server is considered for the next request, we called this a decision tree and illustrated it in Figure 7.  If we for example receive a positive response n the third request (0x0 0x0 0x6B 0x0), we know that the 1st, 2nd, and 4th character belong to the first bucket (all base64characters). The 3rd character belongs to the second bucket (seeFigure 7).

Figure 6: Decision tree for finding first valid combination of detector bytes

Step 4

At this stage, we have a valid detector byte combination for one block of the secret key (e.g., {0x6B,0x0, 0x0, 0x0}). We determine a divide-and-conquer byte for the first detector byte (i.e., 0x6B), for which half of the possible key characters from the bucket will return a positive error message and the other half a negative one.

If we receive a positive error message, we know that key character belongs to the first half.

If we receive a negative error message, we know that key character belongs to the second half.

This will again create a decision tree analogous to the detector byte decision tree (Figure 7). This process is repeated until the key character is unambiguously identified. We then move to the second detector byte (i.e., 0x0) with a different bucket and repeat the whole process.

The worst case is 12 (number of blocks) * (81 (detector byte combinations) + 20 (max. tree depth per characters) * 4 (number of characters per block)) requests, which equals 32 min assuming a throttling of one second.  

Key Takeaways

This blog post showed how the initial dissatisfaction with the existing exploit and sheer curiosity helped us to increase exploitation speed by a factor of ~39k for the worst-case scenario (and 250 in the concrete case). In what follows, we list some optimization strategies that we applied and that can be used when developing other exploits:

  1. Understand the key concepts behind the problem and its implications: A very simple example is XOR. It is important not only to understand how it works, but also that a key character XOR-ed with 0x0 results in the same key character. This understanding allowed us to develop the concept around detector bytes in the first place.
  2. Think about how information can be generated before the exploitation: To start with as much information as possible and to reduce noisiness, it makes sense to think of potential simulations and how that could provide additional information for exploitation. In our case, we determined how the target server would respond to specific input and made use of the knowledge about the behavior. This worked without sending a single request to the target server.
  3. Think about how information can be (re-)used during the exploitation: Optimization is essentially the clever use of available information. This means that all helpful information in answers from previous requests should flow into ensuing requests. In our case, instead of guessing paddings for each iteration again and again as done in exploit 1, we only determine it once in the form of a working detector byte combination. This allows us to remove one factor from the complexity calculation.
  4. Reduce the problem to its essence: While the other exploits loop over the key characters as well as the plaintext characters, encrypt both, and send them to the server, we directly used the encrypted strings as a start for finding the secret key. As a result, we were able to reduce the complexity of the problem.
  5. Decide on a suitable, algorithmic method: Broadly speaking, keys can be guessed through a procedure of exclusion or a procedure of confirmation. While exploit 1 attempted to get a confirmation for a guessed secret key, our exploit tried to exclude wrong options of the secret key through divide-and-conquer. This requires fundamentals in data structures and algorithmics.