Hey, do a flip!
We get the source code to a machine we can connect via netcat
on port 1337.
Reconnaissance
Source Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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 = 'access_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()
In the source code, we can see the server asks for a username and password. If the username is not “admin” and the password is not “sUp3rPaSs1” then the server encrypts a message, using AES in CBC mode, that contains the credentials we input 'access_username=' + username +'&password=' + password
. It then leaks the encrypted message.
The caveat is that we only get the flag if we provide the cipher text, a.k.a the encrypted value, of access_username=admin&password=sUp3rPaSs1
which we don’t know as we can’t input these values in the first stage.
AES CBC mode explained
In this example, AES (Advanced Encryption Standard) in CBC (Cipher Block Chaining) mode is being used as encryption algorithm. AES is an symmetric encryption algorithm, which means the same key is being used for encrypting and decrypting data. CBC is Cipher Block Chaining which is a mode that XORs the previous cipher text (encrypted value) block with the current plaintext block before encryption.
Block by Block: Encrypting Your Message
First things first, we need to divide our message, which could be any string of characters, into blocks of 16 bytes (or 16 characters). Let’s say we have the string “HELLO, WORLD!” as our secret message. We’ll split it into two blocks:
- Block 1: “HELLO, WORL”
- Block 2: “D!” + 14 * Padding Characters
Since Block 2 needs to be exactly 16 bytes long, we add some extra characters (let’s say spaces) to fill it up. Think of them as the “invisible ink” that keeps our message length consistent.
Block 1 Takes the First Step: Encrypting with IV
Block 1, which contains “HELLO, WORL,” will be encrypted using an initialization vector (IV). The IV adds randomness and uniqueness to the encryption process, making it harder for adversaries to crack our code. So, Block 1 becomes “encrypted_block1.”
Block 2 Follows Suit: Encrypted with Block 1 Cipher
With Block 1 out of the way, it’s time for Block 2 to shine. But wait! We don’t encrypt Block 2 directly. Instead, we encrypt it using the cipher generated from encrypting Block 1. So, Block 2 transforms into “encrypted_block2.”
Bringing It All Together: Concatenating the Encrypted Blocks
Now that we have “encrypted_block1” and “encrypted_block2,” we simply concatenate them together:
Encrypted Message: “encrypted_block1” + “encrypted_block2”
Now we’ve transformed our secret message into a secure, encrypted form ready for transmission.
Decryption: Unraveling the Encrypted Message
Our encrypted message reaches its destination, and it’s time to decipher it. Here’s how the decryption process unfolds:
Step 1: Decrypting “encrypted_block1” with IV
To retrieve the original contents of “encrypted_block1,” we decrypt it using the same IV we used for encryption. This step unravels the first part of our secret message, leaving us with “HELLO, WORL.”
Step 2: Decrypting “encrypted_block2” with Block 1 Cipher
Remember the magic we applied to Block 2 during encryption? Now it’s time to reverse it! We decrypt “encrypted_block2” using the cipher generated from Block 1. This completes the decryption process and reveals our final secret character: “D!”
Exploit
This can be exploited by a bit flipping attack. We will first supply a value that differs with one character from the value that will provide us the flag.
Bit-flipping explained
Imagine a the message “ABCDEFGHIJKLMNOPQRSTUVWXYZ”, this will be divided in two blocks. The AES CBC encryption algorithm will encrypt the second block like this:
1
2
3
ABCDEFGHIJKLMNOP
XOR
QRSTUVWXYZ______
So for each character in the first block, it gets XORed to the second block, such as A XOR Q, B XOR R, and so on.
For a bit flip attack, you need to locate the character that needs to be flipped in the leaked encrypted value. Then find the matching value in the previous block that gets XORed with the value you want to flip.
You can use XOR to calculate other values. For example:
- IF
- A XOR Q = 1
- Then
- Q = A XOR 1
To summarize, in a bit flip attack in AES CBC, the first block is XORed with the second block character by character. The attacker needs to identify the target character in the leaked encrypted value and find the corresponding value in the previous block. By performing XOR operations, the attacker can calculate the modified value that, when XORed with the target character, achieves the desired bit flip. This attack takes advantage of vulnerabilities in the XOR-based encryption process and the chaining mechanism in CBC mode.
Manual
We provide a value that is close to the orginal value (“xdmin” instead of “admin”).
1
2
3
4
5
6
$ nc 10.10.218.124 1337
Welcome! Please login as the admin!
username: xdmin
xdmin's password: sUp3rPaSs1
Leaked ciphertext: e8b23de395191a4a5e354cd4d9c3778929b17636e222c774952e74357001f5c6e7be92f571c7557b7598aba9d8c634f9
enter ciphertext:
We now know that the message corresponds with which cipher text as the server provides it as a response.
1
2
3
logged_username=xdmin&password=sUp3rPaSs1
=>
4f0327529ec685422f357a827a4cfbb560bdea9e229e79d7be21df7da31960a42e389783fbc797dcf7f6fde80cd3e29f
We now need to find the corresponding character x
so we can flip it to a
so the user value is admin
instead of xdmin
. The cipher text is in hex format so each character in the original string represents two characters in the cipher text.
We can divide the original text in 3 blocks of 16 characters. The last block will have padding (represented by underscores).
logged_username=
xdmin&password=s
u3rPaSS1_______
We know the character we want to flip (x
) is located in the second block. This means that we need to change the corresponding value in the first block as the first block gets XOR with the second block. This results in us needing to change the first character of the first block which is the l
character to flip the x
to a
.
Each one hex-byte represents two characters for each ascii character in the original string.
1
2
3
4
5
6
>>> len("logged_username=xdmin&password=sUp3rPaSs1")
41
>>> len("logged_username=xdmin&password=sUp3rPaSs1_______")
48
>>> len("4f0327529ec685422f357a827a4cfbb560bdea9e229e79d7be21df7da31960a42e389783fbc797dcf7f6fde80cd3e29f")
96
The hex of the first block is 32 bytes.
1
2
>>> len("4f0327529ec685422f357a827a4cfbb5")
32
This means we need to change 4f
as this represents the first character in the first block (“l”) to change “x” which translates to 60
(33th place in the encrypted value). So we know 4f
XOR the decrypted value of 60
equals the ASCII representation of “x”.
We first need to calculate the ascii format of “x”.
1
2
>>> ord('x')
120
As we know that this is the result of the XOR.
1
4f ^ decrypted(60) = 120
Now we need to find the decrypted(60)
value.
1
2
3
# decrypted(60) = 4f ^ 120
>>> hex(0x4f ^ 120)
'0x37'
Then we can calculate which value we need to put in the encrypted value to do the bit flip. First, we need the ascii format of “a” which is the value we want to flip to.
1
2
>>> ord('a')
97
Then we can use it to calculate the hex-bytes we need as these will get XORed which we previously deducted.
1
2
3
# ? ^ 0x37 = 97
>>> hex(0x37 ^ 97)
'0x56'
We can now replace the original 4f
value with 56
and we successfully performed a bit flipping attack.
1
2
3
4
5
6
7
8
$ nc 10.10.135.211 1337
Welcome! Please login as the admin!
username: xdmin
xdmin's password: sUp3rPaSs1
Leaked ciphertext: 4f0327529ec685422f357a827a4cfbb560bdea9e229e79d7be21df7da31960a42e389783fbc797dcf7f6fde80cd3e29f
enter ciphertext: 560327529ec685422f357a827a4cfbb560bdea9e229e79d7be21df7da31960a42e389783fbc797dcf7f6fde80cd3e29f
No way! You got it!
A nice flag for you: THM{Fl**REDACTED**3d}
Automatic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from Crypto.Util.number import bytes_to_long
from Crypto.Random import get_random_bytes
from binascii import unhexlify
from pwn import *
import re
import sys
key = get_random_bytes(16)
iv = get_random_bytes(16)
host = sys.argv[1]
port = 1337
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 = 'admin&paxsword=sUp3rPaSs1'
password = 'sUp3rPaSs1'
msg = 'logged_username=' + user +'&password=' + password
print(msg, len(msg))
xor = ord('x') ^ ord('s')
cipher = encrypt_data(msg)
cipher = cipher[:16] + hex(int(cipher[16:18], 16) ^ xor)[2:] + cipher[18:]
print(decrypt_data(cipher))
conn = remote(host, port)
print(conn.recv())
print(conn.recv())
conn.send(user + '\r\n')
print(conn.recv())
conn.send('\r\n')
match = re.match(r'Leaked ciphertext: (.+)\n', conn.recv().decode())
print('Ciphertext:', match[1])
cipher = match[1]
cipher = cipher[:16] + hex(int(cipher[16:18], 16) ^ xor)[2:] + cipher[18:]
print('Modified Ciphertext', cipher)
print()
conn.send(cipher + '\r\n')
print(conn.recv())
conn.close()
Mitigation
Using AES in GCM (Galois/Counter Mode) encryption mode is recommended. GCM is an authenticated encryption mode that combines both confidentiality and integrity protection. It provides built-in protection against bit flip attacks and other types of tampering.
GCM mode operates by using a counter to generate a unique nonce (number used once) for each block of plaintext. This nonce is combined with a secret key to encrypt the plaintext and produce the ciphertext. Additionally, GCM incorporates an authentication tag, which is a small piece of data that provides integrity protection.