From Padding Oracle to Shell: Unauthenticated RCE in Telerik UI for Asp.net Ajax Tanto Security discovered an unauthenticated AES-CBC padding oracle in Telerik UI for ASP.NET AJAX, chained with two other vulnerabilities to achieve remote code execution affecting versions 2010.1.309 through 2026.2.519. Progress Software released version 2026.2.708 (2026 Q2 SP1) to prevent exploitation, but the attack requires non-default configuration including a reachable RadAsyncUpload control and an explicit encryption key. The vulnerabilities described in this post were discovered, analysed, and exploited with some AI assistance, and a lot of old-fashioned human persistence. What you need to know Tanto Security found an unauthenticated AES-CBC padding oracle in Telerik UI for ASP.NET AJAX and chained it with two other vulnerabilities to achieve remote code execution. Progress Software says https://www.telerik.com/products/aspnet-ajax/documentation/knowledge-base/kb-security-critical-rce-chain-bulletin-july-2026 that the vulnerability affects versions 2010.1.309 through 2026.2.519 inclusive, and that version 2026.2.708 2026 Q2 SP1 prevents exploitation of the chain. The vulnerability chain has preconditions that are not met by a default installation of Telerik UI for ASP.NET AJAX: - There must be a reachable page containing a RadAsyncUpload control where the page’s server-side FileUploaded handler reads UploadResult - The site must be configured with an explicit, non-default Telerik.AsyncUpload.ConfigurationEncryptionKey , which is a recommended hardening setting https://www.telerik.com/products/aspnet-ajax/documentation/controls/asyncupload/security/security recommended-settings If the site has customErrors set to On , then the padding oracle vulnerability must be exploited using timing analysis. This increases the complexity and effort of a successful attack, but it does not prevent exploitation. Users of Telerik UI for ASP.NET AJAX should upgrade to 2026.2.708 2026 Q2 SP1 or later. Telerik UI for ASP.NET AJAX is a pretty popular framework that shows up regularly in enterprise ASP.NET applications. It ships as a suite of server-side handlers and UI controls, and if you spend enough time doing penetration tests on web apps you will run into it sooner or later. Some time ago I ran into a Telerik UI for ASP.NET app doing something odd. Its AES-CBC scheme derived its encryption key straight from the ASP.NET SessionId cookie. A cookie is not a server-side secret. It is a value the client can observe and manipulate in the request, so anyone could freely decrypt and forge the application’s protected parameters. This behaviour made me wonder where it was coming from. A user-controlled key is bad on its own, but the bigger question was whether the developers of that app had introduced this behaviour themselves or whether it was baked into Telerik. If it was the framework, this would not be one app’s problem. It would ship with Telerik itself, in every version that carried that behaviour. A quick Google search confirmed that Telerik does use AES-CBC in its handlers and encrypted parameters. Then I pulled up the Telerik documentation on how it handles encryption keys for its handlers https://www.telerik.com/products/aspnet-ajax/documentation/controls/asyncupload/security/security . Based on what I read, I began to doubt that Telerik was deriving an encryption key from the ASP.NET SessionId cookie. In its documentation, Telerik explicitly tells developers to configure their own encryption key values. When it’s not configured, Telerik falls back to using the ASP.NET machine key as the encryption key. That key is a server-wide value ASP.NET keeps for protecting things like view state and forms-auth cookies. This made me think that the ASP.NET SessionId encryption key derivation was coming from the application, not the framework. However, that search left me with a different question. If Telerik is using AES-CBC , could it be vulnerable to padding oracle attacks? How padding oracle attacks work New to padding oracles? This section teaches the attack from the ground up, for readers who have not met a padding oracle before or who want a refresher, and it takes each part slowly. If that is you, read on. If you already know how padding oracles work, feel free to skip ahead to A quick tour of Telerik and its building blocks a-quick-tour-of-telerik-and-its-building-blocks . Block ciphers like AES work on fixed-size chunks called blocks. An AES block consists of 16 bytes. Real messages are rarely a neat multiple of that, so the final chunk has to be “topped up” to a full block before it can be encrypted. That “topping up” is called padding. The common scheme is PKCS 7. It works by encoding the number of padding bytes in the padding bytes themselves. Every padding byte holds the number of padding bytes that were added. If the final block is missing one byte, that byte is filled with 0x01 . Two bytes short are filled with 0x02 0x02 . Three bytes short are filled with 0x03 0x03 0x03 , and so on. When the receiver decrypts the message, it reads the last byte to know how many bytes to remove. Before removing them, it validates the padding, and if the trailing bytes do not follow the rule above, the decryption fails with an invalid padding error. PKCS 7 has one more rule worth knowing. What if the message is already an exact multiple of the block size, with nothing to top up? It still gets padded, with a whole extra block. For a 16-byte block that means sixteen bytes each holding 0x10 . The scheme always adds between one byte and a full block, never nothing, so the receiver can always trust the last byte to tell it how much to strip. Block ciphers like AES only encrypt one block at a time. Encrypting a longer message means running the cipher over each block in turn, and encrypting them independently leaks structure. To avoid that, a block cipher is paired with a “mode of operation” https://en.wikipedia.org/wiki/Block cipher mode of operation that makes multi-block encryption safe. CBC, short for Cipher Block Chaining, is a popular mode of operation. When decrypting, the process happens in two steps per block. Each encrypted block known as a ciphertext block is decrypted using the block cipher, and that result is then XORed against the previous ciphertext block to give the plaintext. The very first block has no previous block, so it uses the initialisation vector, the IV, in that role instead. None of this is specific to AES. CBC chains blocks the same way regardless of the block cipher that sits underneath it. CBC has one more gap that matters here. Unlike some block cipher modes, CBC has no built-in way to tell whether the ciphertext has been tampered with. It leaves that to the library or the developer to bolt on separately, usually with a Message Authentication Code, or MAC, whose whole job is to detect changes to the ciphertext. Without one, an adversary is free to fiddle with the CBC-encrypted bytes before they are decrypted. The server decrypts whatever it is handed and checks the padding all the same. Picture an application that does three things with every request it receives. First, it CBC-decrypts the ciphertext, with no MAC, so nothing checks whether the bytes were tampered with on the way in. Second, it checks the PKCS 7 padding on the decrypted plaintext. Third, if the padding is fine, it hands the bytes to a JSON decoder. Now watch what you can do with that. You cannot read the key. But you can change the ciphertext bytes and send them back, and the server will decrypt whatever you hand it. The block whose plaintext you are after is the target block. The ciphertext block right before it is XORed into the target block, and you control this block because you control all ciphertext blocks. Start by changing a single byte of the block you control, position 10: The block you control is itself ciphertext, so changing one of its bytes scrambles its whole decryption. Its intermediate and plaintext, the dimmed left column, turn to garbage. You never wanted that block’s plaintext, so that is no loss. What matters is the target block. There, only the byte at position 10 changes, and the reason is the rule CBC decryption follows for every byte: plaintext2 n = intermediate2 n ⊕ ciphertext1 n Here n is the byte’s position in the 16-byte block, 0 at the front to 15 at the end. intermediate2 is the target block after the cipher decrypts it, and ciphertext1 is the block you control. Each target plaintext byte is one intermediate byte XORed with the byte at the same position in the controlled block. So changing a single controlled byte moves only that one position of the target’s plaintext and leaves the rest alone. Position 10 held the closing quote of the string, so the padding is still valid. But the JSON no longer parses the scrambled previous block probably breaks it too . The server replies “Invalid JSON, check the data and re-submit”. Now put that byte back and change position 15 instead: This time the target byte that changes is the last one, position 15, which was a padding byte. The padding no longer holds, so the server rejects the message before it reaches the JSON decoder and replies “Invalid padding, check the data and re-submit”. Those are two distinguishable replies. Bad padding returns one error. Padding that passed the check but then failed the JSON parse returns the other. So for any ciphertext you craft, the error that comes back tells you whether the padding check passed. That difference between the good-padding and bad-padding answers is what we call a padding oracle. Those two diagrams also hand you the mechanism for turning the oracle into plaintext. You just saw the rule behind it: changing byte n of the controlled block moves only byte n of the target’s plaintext. To recover the intermediate2 bytes, start with the last byte of the block you control, position 15. Cycle it try all 256 values from 0x00 to 0xff and ask the oracle each time. For almost every value, the target block’s last plaintext byte comes out as something arbitrary and the padding is invalid. When the oracle reports valid padding, the last plaintext byte of the target block has landed on 0x01 , a valid single-byte pad. Now the algebra. You know the plaintext byte is 0x01 because it passed the oracle check , and you know the byte you just sent. Rearrange the rule and the intermediate for that position drops out: Formula: intermediate2 15 = ciphertext1 15 ⊕ plaintext2 15 Resolving: 0x68 = 0x69 ⊕ 0x01 To recover the genuine plaintext, XOR the intermediate byte value against the original byte from the block you control: Formula: plaintext2 15 = intermediate2 15 ⊕ original 15 Resolving: 0x04 = 0x68 ⊕ 0x6c Plaintext byte recovered without the need of the encryption key. Now move one byte to the left. This time you want the target block to end in a valid two-byte pad, 0x02 0x02 . You already know the intermediate for position 15, so you can force that last byte to decrypt to 0x02 . Set your controlled byte at position 15 to intermediate2 15 ⊕ 0x02 : Formula: ciphertext1 15 = intermediate2 15 ⊕ plaintext2 15 Resolving: 0x6a = 0x68 ⊕ 0x02 To see why that forces the byte to 0x02 , substitute the controlled byte back into the plaintext rule: plaintext2 15 = intermediate2 15 ⊕ ciphertext1 15 = intermediate2 15 ⊕ intermediate2 15 ⊕ 0x02 = intermediate2 15 ⊕ intermediate2 15 ⊕ 0x02 = 0x00 ⊕ 0x02 = 0x02 The intermediate cancels against itself, leaving the value you chose. With position 15 pinned at 0x02 , cycle position 14 in the block you control until the oracle validates again. A valid pad now means position 14 decrypted to 0x02 , so the block ends 0x02 0x02 . Recover its intermediate and plaintext the same way: Formula: intermediate2 14 = ciphertext1 14 ⊕ plaintext2 14 Resolving: 0xb3 = 0xb1 ⊕ 0x02 Formula: plaintext2 14 = intermediate2 14 ⊕ original 14 Resolving: 0x04 = 0xb3 ⊕ 0xb7 Each step to the left grows the padding by one. When you take that step, you reset every byte you have already solved to the new padding value, using each one’s known intermediate, then cycle the next unsolved byte. Repeat right to left until every byte of the target block is solved. Once you have every intermediate in the block, you have full control of the plaintext, so you can now forge it to any value you want. Reading a byte meant working out intermediate2 n . You forced the plaintext to a known pad and read the intermediate off it. Forging is that same equation but applied with a different purpose now. Now intermediate2 n is the part you already know, so you pick the plaintext you want and solve for the controlled byte that produces it: ciphertext1 n = intermediate2 n ⊕ chosen n It is the exact move from before. Forcing position 15 to 0x02 was this same equation with chosen 15 = 0x02 , a pad value picked only so you could brute-force the next byte. The difference now is that every intermediate in the block is already recovered, so you are no longer stuck choosing pad values. You can set chosen n to anything, byte by byte, and write the whole block to whatever content you want. This forges a ciphertext that decrypts to whatever you choose, still without the key, and is the primitive used by the exploit we developed for Telerik. Forging usually does not need a real, captured ciphertext to start from. You recover a block’s intermediate through the oracle the same way no matter what you feed into the vulnerable functionality, whether a genuine block, all zeros, all 0x01 , or random bytes. Any of them works as a seed. What forging does depend on is the IV, the value the very first plaintext block is XORed against. Control the IV and you can forge a whole message out of arbitrary blocks. When it is fixed by the implementation and is not given as part of the ciphertext, the first block cannot be completely controlled, and that is the catch I come back to later. A lot to take in? If that read like a wall of mathematical mumbling, don’t stress out It took me a while to fully internalise it the first time too or the first 100 times? 😅 . The interactive walkthrough below steps through the attack one byte at a time. Take the guided tour, or switch to free-play and drive it yourself. Go at your own pace. A rare false positive There is one edge case worth knowing about, now that you have the shape of the attack. Sometimes cycling the last byte reports valid padding not because that byte became 0x01 , but because it became 0x02 . The byte just before it already happened to decrypt to 0x02 , giving a valid two-byte pad by luck. It is a rare false positive, but left unchecked it hands you the wrong value for that byte. And because each recovered byte feeds the next, the mistake results in errors in the rest of the block. If your decryption fails, change the second-to-last byte of the block you control and retry decrypting the block. Changing that byte cannot disturb a genuine single-byte 0x01 pad, which depends only on the last byte, but it does break the lucky 0x02 0x02 . So the retry locks onto the real 0x01 , and the whole block decrypts as expected. A quick tour of Telerik and its building blocks Before I drag you into source code analysis and ciphertext, let me explain what Telerik actually is. The rest of this post only really lands if you have a feel for the moving parts. If you have never had the pleasure, a lot of the names are going to fly past otherwise. Telerik UI for ASP.NET AJAX is a suite of server-side UI controls for ASP.NET WebForms. You drop a