HackTheBox ‘Flippin Bank’ Walkthrough | Introduction to CBC Bit-flipping Attack

Sohail Saha
6 min readJul 13, 2021
Vector Vectors by Vecteezy

‘Flippin Bank’ is a crypto challenge on HackTheBox, and I loved it because it showcased a classic CBC bit flipping attack.

I’ll be honest, I thought of a very different attack at first (which I will briefly show at the end), but then I realised that although mathematically possible, it was impossible in this challenge.

I will not only walk you through the challenge, but I will also explain the CBC bit flipping attack as I go. Hopefully, this helps you create a clearer mental picture of what’s happening.

The challenge

The challenge description reads:

The Bank of the World is under attack. Hackers found a way in and locked the admins out. However, the netcat authentication by the intruders is not perfectly secure. Could you help the admins log in?

You get a downloadable zip file that contains the python3 script that runs on the server side, and the option to deploy your own private instance of the challenge server.

What the source code says that the server performs is:

  1. The server chooses two global (unchanging), random 16 byte bytearrays, one as key and the other as theiv .
  2. The server asks you for a username and password, receives it from you, then places it in a string format like logged_username={username}&password={password}`. You receive a welcome banner in reply.
  3. Then, it checks whether this resulting string containsadmin&password=g0ld3n_b0y (that is, you sent ‘admin’ as username, ‘g0ld3n_b0y’ as the password)
  4. If it does, the server does not allow you to proceed further, and if it doesn’t, the server then uses PKCS#7 padding scheme to pad the string formed in step #3, encrypts it with the key and IV set in step #1 with AES-CBC-128, and sends you the hex digest of the ciphertext.

Your task is to find a ciphertext, which when decrypted with the same key and IV as set in step #1, and then unpadded with PKCS#7 padding scheme, results in a string that contains admin&password=g0ld3n_b0y .

Observations

Looking through the source code app.py , these are the things I noticed:

  1. All of these substrings are of 16 byteslogged_username= , admin&password=g and 0ld3n_b0y . Therefore, each of them would occupy a whole 16-byte block each.
  2. The server only checks for the presence of block #2 (as mentioned in the previous observation) when it performs step#3.

CBC Bit-flipping Attack

Look at the above diagram as a reference to recall how the CBC mode works with block ciphers (like AES). The thing to note is — Every n-th plaintext is the result of a xor operation of the (n-1)-th ciphertext and the decrypted n-th ciphertext.

The CBC bit flipping attack is about changing the ciphertext in such a manner that its corresponding decrypted plaintext changes in a desired manner. This is achieved by abusing the way how CBC decryption works (which I summarised above). Xor is a bitwise operation, and therefore when extended to bytes, it only operates on corresponding bytes. For instance, Xor-ing the strings “abc” and “xyz” would produce ‘a’ xor ‘x’ | ‘b’ xor ‘y’ | ‘c’ xor ‘z’ (considering that each character is encoded in ASCII, UTF-8, etc where each character occupies only a byte).

So, for instance, if you take the first ciphertext block and change its first byte, the first byte of the second plaintext block would change as well, and this would be the only change. How can we control this?

Working of the attack, mathemcatically

Continuing from the last section, mathematically put, c1[0] xor decrypt(c2,key,iv)[0] = p2[0] , where c1and c2 are the first and second ciphertext blocks, and p2 is the second plaintext block. Notice how the resulting plaintext byte is dependent on the first ciphertext block’s first byte, which is under your control. So, transitively, p2[0] is also under your control.

Now, to change p2[0] to any arbitrary byte, c1[0] must be substituted with another byte (or, have some of its bits flipped or inversed to form a different byte when all the bits, both the flipped and the unflipped, are taken together). Let’s call the required byte as c1'[0] and the required plaintext byte as p2'[0]. Then, mathematically,

c1'[0] = c1[0] xor p2[0] xor p2'[0]

Why? Because now when the resultant first byte is xor-ed with the decrypted first byte of the second cipher text block, the operation which happens is,

c1'[0] xor decrypt(c2,key,iv)[0] , and expanding c1'[0] , we get,

(c1[0] xor p2[0] xor p2'[0]) xor decrypt(c2,key,iv)[0] .

Now, since xor operations are associative, the above expression can be rearranged without changing its value. Rearranging, we get,

(c1[0] xor decrypt(c2,key,iv)[0]) xor p2[0] xor p2'[0] . But since (c1[0] xor decrypt(c2,key,iv)[0]) equals p2[0] , we substitute the value to get,

p2[0] xor p2[0] xor p2'[0] . Now, since p2[0] xor p2[0] = 0, and 0 xor p2'[0] = p2'[0], this expression just evaluates to p2'[0] , which is our required plaintext byte itself!

I demonstrated with the first byte, but this will work for any byte obviously.

Attack steps

Having discussed the working of the attack mathematically and noting that the required (decrypted resultant) plaintext blocks are — logged_username= (p1), admin&password=g (p2) and 0ld3n_b0y (p3) (from the Observations), an attack strategy would be:

  1. To avoid the server from detecting admin&password=g , we replace ‘a’ in ‘admin’ with something else like ‘b’. The new username is thus ‘bdmin’.
  2. We send this username and any password to the server, and receive the encrypted ciphertext blocks (c1,c2,c3,…).
  3. When the server then asks for the ciphertext block that decrypts back to something that contains admin&password=g , we just sent c1'|c2|c3, where c1' is formed by substituting the first byte of c1 as discussed in the previous section.
  4. The server performs the decryption of the ciphertext we send, and in doing so, the first byte of p2 (‘b’) becomes ‘a’ after decryption, thus changing ‘bdmin’ (from what it would have been had we submitted the same ciphertext that the server sent to us) to ‘admin’ (from the original ciphertext having the first byte of the second block substituted).

Attacking

All that remains now is to write a script to automate the attack having deployed out private instance of the challenge server. Here’s my attack script (I used pwntools for ease).

Bonus content — The impractical solution

Observe that the source code says that the server only accepts printable strings as the username and password. Also, you control the whole second ciphertext block (which is what the server returns after encrypting your username and password after having formatted it) only as long as you choose a 16 character (byte) username.

When I first started solving this, I became lost, and got excited when I found a good solution after thinking for awhile, but it was only when writing the solution script that I realised that my solution, though mathematically possible, was impractical in this case.

Here’s my solution, below, I am not going to explain it; it’s late at night and I am feeling sleepy. Take out your pen and paper, retrace my steps, and try to understand why this attack works (mathematically).

Note: p1 = ‘logged_username=’, p2 = ‘admin&password=’ and p3 = ‘0ld3n_b0y’, and c1,c2,c3 represents ciphertext blocks, first, second and third. My strategy converts ‘logged_username=admin&password=0ld3n_b0y’ directly to the required ciphertext (c1|c2|c3).

My consideration, when I was thinking about the solution (deviating from the challenge, like I said) was that all of c1,c2 and c3 were under my control because I could choose all of p1,p2,p3 to give to the server for encryption, which it was not, but, let’s assume that it was, and proceed.

Steps:

  1. Find c1 by sending any 16 character username and noting the first 16 bytes of the ciphertext (in reply) as c1.
  2. Find c2 by sending p1|p2|16-bytes-junk and noting the second block of the ciphertext receieved in reply as c2.
  3. Find c3 by finding the encryption of c2 xor p3 xor c1 in the same manner as step #2. In other words, replace p1|p2|16-bytes-junk in the previous step by c2 xor p3 xor c1. This is mathematically equal to encrypting the p3.
  4. Complete the challenge by sending c1|c2|c3.

Takeaway

Even the strongest of encryption schemes, such as the AES, can fall, due to insecure implementations and/or data leakages.

Until next time, see ya.

--

--

Sohail Saha

🚀 Frontend developer @Polygon 👨‍💻 Creator at #DevvingItWithSohail 🎮 Gamer