Flip

Hey, do a flip! - by hadrian3689

Reviewing the Source Code

The provided source code sets up a TCP server that listens on the port 1337. The client has to provide the encryption of the admin credentials to retrieve the flag. For this, the AES-CBC is used with a block size of 16 bytes.

On an incoming connection, the function start is called, which generates a random key and IV and initiates the authentication process.

It prompts the client to provide a username and password and requests an admin login. The credentials of the admin are in cleartext visible in the source code. If a client attempts to log in as the admin with its credentials, the server sends a rejection; otherwise, the setup function is called. The setup function handles the authentication process for the flag.

In the setup function, a message is constructed with the provided username and password as follows:

access_username=USERNAME&password=PASSWORD

This message will be encrypted with AES-CBC, using the key and IV generated by the function start. The message will then be leaked to the connected client. Next, the application asks for an encrypted message. This message will then be decrypted with the previously generated IV and key. If the decrypted string contains the information admin&password=sUp3rPaSs1 the flag will be provided.

app.py
import socketserver 
import socket, os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from Crypto.Random import get_random_bytes
from binascii import unhexlify

flag = open('flag','r').read().strip()

def encrypt_data(data,key,iv):
    padded = pad(data.encode(),16,style='pkcs7')
    cipher = AES.new(key, AES.MODE_CBC,iv)
    enc = cipher.encrypt(padded)
    return enc.hex()

def decrypt_data(encryptedParams,key,iv):
    cipher = AES.new(key, AES.MODE_CBC,iv)
    paddedParams = cipher.decrypt( unhexlify(encryptedParams))
    if b'admin&password=sUp3rPaSs1' in unpad(paddedParams,16,style='pkcs7'):
        return 1
    else:
        return 0

def send_message(server, message):
    enc = message.encode()
    server.send(enc)

def setup(server,username,password,key,iv):
        message = 'access_username=' + username +'&password=' + password
        send_message(server, "Leaked ciphertext: " + encrypt_data(message,key,iv)+'\n')
        send_message(server,"enter ciphertext: ")

        enc_message = server.recv(4096).decode().strip()

        try:
                check = decrypt_data(enc_message,key,iv)
        except Exception as e:
                send_message(server, str(e) + '\n')
                server.close()

        if check:
                send_message(server, 'No way! You got it!\nA nice flag for you: '+ flag)
                server.close()
        else:
                send_message(server, 'Flip off!')
                server.close()

def start(server):
        key = get_random_bytes(16)
        iv = get_random_bytes(16)
        send_message(server, 'Welcome! Please login as the admin!\n')
        send_message(server, 'username: ')
        username = server.recv(4096).decode().strip()

        send_message(server, username +"'s password: ")
        password = server.recv(4096).decode().strip()

        message =_username=' + username +'&password=' + password

        if "admin&password=sUp3rPaSs1" in message:
            send_message(server, 'Not that easy :)\nGoodbye!\n')
        else:
            setup(server,username,password,key,iv)

class RequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        start(self.request)

if __name__ == '__main__':
    socketserver.ThreadingTCPServer.allow_reuse_address = True
    server = socketserver.ThreadingTCPServer(('0.0.0.0', 1337), RequestHandler)
    server.serve_forever()

Key and IV are not being leaked, so it is not possible to encrypt those credentials on our own. Using the admin credentials won't get us to the setup function. To craft an encrypted text containing the admin credentials, the leaked encrypted message can be reused, referring to the AES-CBC bit flip attack. If we can control where the flip happens, we can create an encrypted message by providing the credentials, that are minimally changed by only one character, to pass the login prompt. Next, this changed character has to be flipped via the leaked cipher, so that it results in the correct credentials while decrypting the modified cipher text, to give us the flag.

Offline Test

For a more detailed view, this is how the AES-CBC encryption and decryption works.

In CBC mode, the plaintext is divided into blocks, and each block is XORed with the previous ciphertext block before being encrypted. This chaining process adds randomness and makes the encryption more secure. The first block is XORed with the so-called initialization vector. However, if an attacker can modify the ciphertext, they can change the corresponding plaintext block when decrypted.

Looking at the decryption process, its vice versa.

Changing a bit in the ciphertext leads to a complete wrongful decryption and affects every bit in the corresponding plaintext block, as visualized in red in the following graphic. But it also affects the single-bit XORed of the following plaintext block, visualized in green.

So, if we choose to put the string admin&password=sUp3rPaSs1\r\n as the parameter for the username and \r\n as the password, the complete plaintext will look like the following:

access_username=admin&password=sUp3rPaSs1\r\n&password=\r\n

In this case, the AES-CBC here encrypts in 16-byte blocks; if the block does not reach a length of 16 bytes, it is padded in the style of pkcs7. This results in the following plaintext blocks being encrypted.

Block 1: access_username=

Block 2: admin&password=s

Block 3: Up3rPaSs1\r\n&pass

Block 4: word=\r\n\t\t\t\t\t\t\t\t\t

We see that the first block just contains the string access_username= which is not relevant to our authentication process. So we are able to choose to flip a bit in the first ciphertext block to change a character in the following plaintext block to result in the correct login credentials string, for the price of destroying the first plaintext block.

To create an input, that is being accepted by the function start and its corresponding ciphertext is manipulable to create valid credentials with the AES-CBC bitflip attack, we choose to put the string bdmin&password=sUp3rPaSs1\r\n as the parameter for the username and \r\n as the password, the complete plaintext will look like the following:

access_username=bdmin&password=sUp3rPaSs1\r\n&password=\r\n

This allows us to flip the first byte from the letter b to a. The first byte of the first ciphertext has to be set to evaluate C_0_0 xor 'b' = 'a'. To get the value for C_0_0 the formula just has to be changed to C_0_0 = 'a' xor 'b'.

To test the bitflip offline, the following Python script encrypts the plaintextaccess_username=bdmin&password=sUp3rPaSs1\r\n&password=\r\n and flips the first byte of the cipher to change the b of bdmin to a.

offline.py
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from Crypto.Random import get_random_bytes


key = get_random_bytes(16)
iv = get_random_bytes(16)

def encrypt_data(data):
    padded = pad(data.encode(),16,style='pkcs7')
    cipher = AES.new(key, AES.MODE_CBC,iv)
    enc = cipher.encrypt(padded)
    return enc.hex()

def decrypt_data(encryptedParams):
    cipher = AES.new(key, AES.MODE_CBC,iv)
    paddedParams = cipher.decrypt( unhexlify(encryptedParams))
    print(paddedParams)
    if b'admin&password=sUp3rPaSs1' in unpad(paddedParams,16,style='pkcs7'):
        return 1
    else:
        return 0
 

user = 'bdmin&password=sUp3rPaSs1\r\n'
password = '\r\n'
msg = 'access_username=' + user +'&password=' + password
cipher = encrypt_data(msg)
c_0_0 = ord('a') ^ ord('b')
print("xorval: " + str(xor))
print("cipher: " + str(cipher))
print("1 byte of cipher: " + hex(int(cipher[0:2], 16)))
print("remaining bytes of cipher: " + cipher[2:])

modified_cipher = hex(int(cipher[0:2], 16) ^ c_0_0)[2:] + cipher[2:]
print(modified_cipher)

print("modified cipher " + modified_cipher)
print("decrypted cipher:")
decrypt_data(cipher)
print("decrypted modified cipher:")
decrypt_data(modified_cipher)

As seen in the console output, the modified cipher leads to a string containing the necessary credentials, with the first part of the plaintext being destroyed.

Getting the Flag

To get the flag, we connect to the application via Pwn, pass the credentials as described, and retrieve the leaked encryption. Next, the cipher is being manipulated at the first byte, to change the second plaintext block to the desired result.

attack.py
from pwn import *
import re


conn = remote('10.10.137.124', 1337)
c_0_0 = ord('a') ^ ord('b')
print(conn.recv())
print(conn.recv())
conn.send('bdmin&password=sUp3rPaSs1\r\n')
print(conn.recv())
conn.send('\r\n')

match = re.match(r'Leaked ciphertext: (.+)\n', conn.recv().decode())
print('Ciphertext:', match[1])
print('lenght:', len(match[1]))

cipher = match[1]
cipher = hex(int(cipher[0:2], 16) ^ c_0_0)[2:] + cipher[2:]
print('Modified Ciphertext', cipher)

print()
conn.send(cipher + '\r\n')
print(conn.recv())
conn.close()

Run the script to get the flag.

Last updated