This article continues the series of challenges write ups from Cryptohack. This time we are going to look at the vulnerability in AES-CFB8 which lays in the heart of Logonzero vulnerability (CVE-202–1472). I will explain the essentials, but if you feel like you need some more details, read this paper.
For the challenge we are provided with the server network address and its source code. Let’s check the source code:
We should consider two pieces of the provided code.
- The
decrypt
function withinCFB8
class (encrypt
is never called, so we don’t look there). challenge
function, of course.
The decrypt
function does the following:
- Set
IV
to first 16 bytes of the providedciphertext
- Set
ct
to other part ofciphertext
- Initialize
AES
instance and empty byte stringpt
- Set
state
toIV
- For every byte of
ct
, encryptstate
and XOR correspondingct
byte with only first byte of the encrypted state. Add XOR result to resultingpt
string. Finally remove first byte ofstate
and add currentct
byte to the end ofstate
The challenge
function performs some actions according to the provided option
. We have three options:
authenticate
— we must providepassword
, it will be compared to the current password, that server holds. If passwords are equal, we get the flag.reset_connection
— just reinitializesCFB8
object with the new random key.reset_password
— changes server’s password according to our input. It takes some providedtoken
, decodes it from hex value and decrypts it withCFB8
. Next, it takes 4 last bytes of decrypted value as a password length and then takes corresponding amount of data from the start as a new password.
Now, it is obvious that we should somehow apply our attack within reset_password
option. We should be able to send such a token
that after it is decrypted we can guess new_password
value. But what can we do?
Once again let’s look at the decrypt
function. We can control ciphertext
, therefore we actually control the state. Too bad the resulting pt
value depends on the cipher.encrypt
result, and even knowing the state
at every iteration there is no way we can predict its output without the key. Well, the least we can do, is we can manufacture such a state that ciphertext.encrypt
will always return the same value, at least that’s good.
Another good thing is that only first byte of the AES encryption is used, so there are actually only 256 possible values for that byte. We could try and guess that value, but there is a better way.
Remember, that we also have reset_connection
option, which changes the key. The new AES key is still random, but there is a high possibility that new key will yield new encryption result for the same state. The most amazing thing is that there are such keys that will encrypt the state into some value that starts with 0
. And there is 1 to 256 chance that new key will be such a key. What does it mean for us? It means, that if the first byte of encrypted state is 0
, than the ct
byte will be put to pt
as is.
The last thing left — we need state
value to be the same for every iteration. Well, that’s easy, we just need to send single repeated byte as a ciphertext. Any byte will do, but the best value is 0
. If ciphertext
only contains zeroes, state never changes, and if first byte of encrypted state is also 0
we will make pt
to contain only zeroes. And that’s great, because than we will have new password’s length equal to 0
as well, so we effectively erase the password.
Now let’s combine all above into an attack algorithm. There are actually only 3 steps in our algorithm:
- Send
reset_password
withtoken
equal to all zeroes - Try to
authenticate
with emptypassword
. - If authentication successful, we get the flag. Otherwise, we send
reset_connection
to change the encryption key.
We repeat these steps over and over until we lucky enough to get such a random key that will encrypt our all zeroes state into some value that also starts with 0
. There is 1 to 256 chance of that, so we will find that key soon enough. But we should automate that, of course. Here is the exploit scrip: