247CTF-CRYPTOGRAPHY: PART 3

Girithar Ram R
10 min readApr 8, 2024

--

247CTF is a security Capture The Flag (CTF) learning environment. The platform contains several hacking challenges where we can test our skills across various areas of CyberSecurity by solving problems to recover flags.

If you haven’t checked out the previous parts, those can be found here.

Objective:

This article will cover the 7th challenge ‘PREDICTABLE VECTORS’ from the Cryptography topic.

The 7th challenge involves Browser Exploit Against SSL/TLS (BEAST) vulnerability and we’ll see how this could be exploited.

For this challenge, we are given with the following description:

We are trying to save time by limiting our calls to random. Can you abuse the predictable encryption mechanism to recover the flag?

Now let’s dive into the server-side code:

from flask import Flask, session, request
from Crypto.Cipher import AES
from secret import flag, aes_key, secret_key, new_random


class AESCipher:
def __init__(self):
self.pad = lambda s: s + (AES.block_size - len(s) % AES.block_size) * chr(
AES.block_size - len(s) % AES.block_size
)
self.used_ivs = set()

def encrypt(self, raw):
iv = session.get("IV")
if iv in self.used_ivs:
return "Too predictable!"
self.used_ivs.add(iv)
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(self.pad(raw + flag))
session["IV"] = encrypted[-AES.block_size :]
return encrypted.encode("hex")


app = Flask(__name__)
app.secret_key = secret_key
app.config["DEBUG"] = False
custom_cipher = AESCipher()


@app.before_request
def before_request():
if session.get("IV") is None:
session["IV"] = new_random()


@app.route("/")
def main():
return "
%s
" % open(__file__).read()


@app.route("/flag_format")
def flag_format():
return """The flag format for this challenge is non-standard.

The flag to obtain for this challenge (stored in the flag variable) is 32-HEX only.

Once you obtain this flag, submit your solution in the regular 247CTF{32-HEX} format."""


@app.route("/encrypt")
def encrypt():
try:
return custom_cipher.encrypt(
request.args.get("plaintext").decode("hex")[: AES.block_size * 2]
)
except:
return "Something went wrong!"


if __name__ == "__main__":
app.run()

/flag_format:

When we visit the /flag_format page, we are informed that the flag for this challenge is non-standard, which means we won’t get the flag beginning and ending with ‘247CTF{’ and ‘}’. The flag value that we want to retrieve is a 32-HEX value.

Once we obtain this 32-HEX value, we append and prepend it as ‘247CTF{32-HEX}’.

/encrypt:

The above Python code defines a route /encrypt in a Flask web application. When a GET request is made to this route i.e. request.args.get("plaintext"), it attempts to perform encryption using a cipher.

The given plaintext value is operated with .decode("hex")[:AES.block_size * 2]logic.
In python2, .decode("hex") method converts a hexadecimal string into its corresponding byte string.

For example:

>>> hexdecimal_string = '41'
>>> result = hexdecimal_string.decode("hex")
>>> print('result:',result)
('result:', 'A')

Here, [:AES.block_size * 2] is used to slice the decoded byte string to ensure it is no longer than the length of two AES blocks.
One block of AES is 16, so a total of 32 bytes.

For example:

>>> hexdecimal_string = '41'*34
>>> converted_string = hexdecimal_string.decode("hex")
>>> print(len(converted_string),converted_string)
(34, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
>>> converted_string_after_slicing = hexdecimal_string.decode("hex")[:16 * 2]
>>> print(len(converted_string_after_slicing), converted_string_after_slicing)
(32, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')

encrypt():

The encrypt function takes 2 arguments -> self and raw

self represents the instance of the class on which the method is called, with which used_ivs is accessed.

raw variable has the sliced input.

Here the iv variable stores the base64 decoded value from the session token. This can be found in the browser:

If the IV is used again, the server will return “Too predictable!”.

The server checks the repetition of IV by storing the value in a set named used_ivs and uses an if condition to check whether the upcoming value of IV is in that set.

If the IV value is empty, it takes a random value [logic is defined in before_request()].

The cipher:

The cipher used in the provided code is AES (Advanced Encryption Standard) in CBC (Cipher Block Chaining) mode.

A cipher object is created by:

cipher = AES.new(aes_key, AES.MODE_CBC, iv)

Here, ‘aes_key’ is a value imported from the secret module and is unknown. Then the mode of AES is specified as CBC and it takes the IV value of the current session.

Actual encryption:

encrypted = cipher.encrypt(self.pad(raw+flag))

Now, this cipher object can be used to perform AES-CBC encryption by calling the encrypt function.

In our case, a method named pad is called by passing the variables raw and flag [Which we need to find out].

padding:

self.pad = lambda s: s + (AES.block_size-len(s) % AES.block_size) * chr(AES.block_size-len(s) % AES.block_size)

In the above logic, a lambda function is defined and assigned to the instance variable pad of the AESCipher class (self). This lambda function serves as a padding function which is used to ensure that the length of the value of raw and flag is a multiple of the AES block size -16.

For example:

>>> flag = 'F'*32 #assume flag is 32bytes of F's
>>> raw = 'A'*31 #raw contains 31 bytes of A's
>>> pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16)
>>> result = pad(raw+flag)
>>> result
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\x01'
>>> len(result)
64

>>> raw = 'A'*30
>>> result = pad(raw+flag)
>>> result
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\x02\x02'
>>> len(result)
64

Here, the PKCS#7 padding standard is used.

Coming to encrypted = cipher.encrypt(self.pad(raw+flag))again.

Here, the padded value that contains the flag is passed to the encrypt function.

The encrypted value will be a length of 64 bytes. The last 16 bytes of the encrypted value are sliced with encrypted[-AES.block_size :] logic.

The sliced 16 bytes are stored in session[“IV”], which will be used as an IV if we give a value to the server to encrypt next time.

So, all this time, we have seen how the server takes our input and throws us the encrypted value.

Our goal is to find the appended flag value which we know is encrypted. We need to find a way to determine it.

Since we don’t know the key being used to encrypt, we won’t be able to perform a decryption operation to find the flag. That is the checkpoint here.

We need to use some other way to abuse the server to find the flag and solve this challenge.

We know:

  • We know that the encryption involved here is AES CBC.
  • We know that the first use of cipher employs a randomly generated IV and each subsequent block uses the IV that is derived from the last block of the previous encryption.
  • We know that the IV is XORed with the Plaintext and sent to the Key to perform a block cipher encryption.
  • We know the user input is appended with the flag.

Vulnerability:

The vulnerability that is involved here is Browser Exploit Against SSL/TLS (BEAST)

It is an attack against network vulnerabilities in TLS 1.0 and older SSL protocols.

Exploitation:

In this article, we’ll see 2 different methods to exploit this vulnerability.

Method 1:

Note: This method can only retrieve 1st 16 bytes of the flag and not the other half.

In the above diagram:

  • The 1st and 2nd blocks ‘R1’ and ‘R2’ contain the 32-byte [each 16] plaintext we gave as a parameter.
  • The 3rd and 4th blocks — ‘N1’ and ‘N2’ contain the 32-byte [each 16] flag appended by the server.

Since we know the IV Z1’ [last block of the previous encryption] and control the PlaintextR1’ and ‘R2’.

And, we know that AES CBC performs XOR operation [To know more on XOR] on the IV and Plaintext before it gets to the key for actual encryption.

We need to make sure to nullify the effect of IV Z1’ [That happens in the server]

This can be done by XORing the plaintext with IV Z1’ [we already know it] making R1` pass in as plaintext.

R1` = Z1 XOR R1

Then, We can reduce 1 byte from block 2 making it 15 bytes, which lets 1 byte from block 3 — flag to block 2 — non-xored plaintext.

This will be 1st request we send in after having a dummy request to know the IV.

Then we do the same thing to the 2nd request. As we know the IV [Last 16 bytes from the previous response], we can replicate the 1st block of the 2nd request the same as the 1st request.

For the 2nd request, we try to give the following values on the final byte of the 2nd block R2`.

possiblities = ['61','62','63','64','65','66','30','31','32','33','34','35','36','37','38','39']

Above are the hex strings of possible characters — ‘a-f’ and ‘1–9’-in the flag.

As we have narrowed down the scope of our attack to only known values, this made us implement a Known-plaintext attack.

Basically, we are exploiting the BEAST vulnerability with Known-plaintext attack ‘byte by byte’.

We redo the above process in a loop until the C2 and C6 are equal.

When the 1st byte of the flag is known to us, we reduce the number of values by 1 from the 2nd block, which becomes 30.

Then we append the retrieved flag in between the non-xored plaintext and guessed flag for the 2nd request.

This process is repeated until the 1st 16 bytes of the flag is found.

The below Python code can do the above-explained process:

import requests
import binascii

IV = []
retrieved_flag = ''
retrieved_flag_in_chr = ''

request_session = requests.Session()
url = "https://.247ctf.com/" #place the host address
possiblities = ['61','62','63','64','65','66','30','31','32','33','34','35','36','37','38','39']
texts = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g']

def get_IV(converted_text):
result = request_session.request('GET', url+'encrypt?plaintext='+converted_text)
return result.text[-32:]

def xor_operation(a, b):
xored = []
for i in range(len(a)):
xored_value = ord(a[i%len(a)]) ^ ord(b[i%len(b)])
hex_value = hex(xored_value)[2:]
if len(hex_value) == 1:
hex_value = "0" + hex_value
xored.append(hex_value)
return ''.join(xored)

def sending_the_plaintext_after_XOR_1(possible_value,text):
print('Count',count)
global retrieved_flag
global retrieved_flag_in_chr
converted_text = binascii.hexlify(bytes(text, 'utf-8') * count).decode('utf-8')
known_text = text*count

IV = get_IV(converted_text)
print('IV value:',IV)

IV_XOR1 = bytes.fromhex(IV).decode('latin-1')
xored_value1 = xor_operation(IV_XOR1, known_text[:16])
result1 = request_session.request('GET', url+'encrypt?plaintext='+xored_value1+converted_text[32:])
print(url+'encrypt?plaintext='+xored_value1+converted_text[32:])
print(result1.text)

IV_XOR2 = bytes.fromhex(result1.text[-32:]).decode('latin-1')
xored_value2 = xor_operation(IV_XOR2, known_text[:16])
result2 = request_session.request('GET', url+'encrypt?plaintext='+xored_value2+converted_text[32:]+retrieved_flag+possible_value)
print(url+'encrypt?plaintext='+xored_value2+converted_text[32:]+retrieved_flag+possible_value)
print(result2.text)
print(possible_value)

if(result1.text[32:64]==result2.text[32:64]):
print('True')
retrieved_flag = retrieved_flag + possible_value
ascii_string = binascii.unhexlify(possible_value).decode('ascii')
retrieved_flag_in_chr = retrieved_flag_in_chr + ascii_string
return 'True'
else:
print('False')

count = 31 - int(len(retrieved_flag)/2)

for i in range(32):
print('loop:',i)
for possibile_value,text in zip(possiblities,texts):
result = sending_the_plaintext_after_XOR_1(possibile_value,text)
if(result == 'True'):
print('hi')
count = count - 1
print('Retrieved flag in hex:',retrieved_flag)
print('Retrieved flag in chr:',retrieved_flag_in_chr)
break

The above code will terminate with a ‘Too predictable!’ error message as the 1st method can only produce the 1st 16 bytes of the flag.

The code output with the final print statement ‘Retrieved flag in hex:’ contains the 1st 16 bytes of the flag.

Note: request_session = requests.Session() is used to ensure cookies and headers persist among the requests we send to the server.

Method 2:

Note: This method can retrieve all the 32 bytes of the flag with some slight modification. But in our case, the code is written to only get the next 16 bytes of the flag.

Coming again to this diagram:

  • The 1st and 2nd blocks — ‘R1’ and ‘R2’ contain the 32-byte [each 16] plaintext we gave as a parameter.
  • The 3rd and 4th blocks — ‘N1’ and ‘N2’contain the 32-byte [each 16] flag appended by the server.

Since we already know the 1st 16 bytes of the flag.

We implement the below logic:

We send a 15-byte non-xored plaintext with a 1-byte know flag on the 1st block.

The second block carries, a total of 15 known flag bytes and a 1-byte unknown flag.

This is repeated for the 2nd request.

Now, we have Z1 and Z3, which are operated with XOR, becoming Z`.

Z` = Z1` XOR Z3`

The value Z` is then XORed with the known flag bytes, plaintext, and Z`, becoming Z``.

Z`` = known_flag_bytes XOR plaintext + Z`

We then take z``, and append it with the retrieved flag values and a 1 byte guessing value.

encrypt?plaintext = Z``+ retrieved_flag + guessing value

We need to make sure whether C2 and C8 are equal.

If those are equal, we have correctly guessed the 17th byte of the flag.

The above process is continued until we get the remaining 15 bytes of the flag.

The below Python code is implemented for the above-explained theory:

import requests
import binascii

IV = []
retrieved_flag = '.....' #insert the value from 'Retrieved flag in hex:'
retrieved_flag_in_chr = '.....' #insert the value from 'Retrieved flag in chr:'

request_session = requests.Session()
url = "https://.247ctf.com/" #place the host address
possiblities = ['61','62','63','64','65','66','30','31','32','33','34','35','36','37','38','39']
texts = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P']

def get_IV(converted_text):
result = request_session.request('GET', url+'encrypt?plaintext='+converted_text)
return result.text[-32:]

def xor_operation(a, b):
xored = []
for i in range(len(a)):
xored_value = ord(a[i%len(a)]) ^ ord(b[i%len(b)])
hex_value = hex(xored_value)[2:]
if len(hex_value) == 1:
hex_value = "0" + hex_value
xored.append(hex_value)
return ''.join(xored)

def sending_the_plaintext_after_XOR_2(possible_value,text,iteration_count):
print('Count',count)
global retrieved_flag
global retrieved_flag_in_chr
converted_text = binascii.hexlify(bytes(text, 'utf-8') * count).decode('utf-8')
known_text = text*count

IV = get_IV(converted_text)
print('IV value:',IV)

result1 = request_session.request('GET', url+'encrypt?plaintext='+converted_text)
print(url+'encrypt?plaintext='+converted_text)
print(result1.text)
IV = bytes.fromhex(IV).decode('latin-1')
IV_2 = bytes.fromhex(result1.text[-32:]).decode('latin-1')
xored_value1 = bytes.fromhex(xor_operation(IV, IV_2)).decode('latin-1')
xored_value2 = xor_operation(xored_value1,known_text+retrieved_flag_in_chr[:iteration_count])
result2 = request_session.request('GET', url+'encrypt?plaintext='+xored_value2+retrieved_flag[iteration_count*2:]+possible_value)
print(url+'encrypt?plaintext='+xored_value2+retrieved_flag[iteration_count*2:]+possible_value)
print(result2.text)
print(possible_value)

if(result1.text[32:64]==result2.text[32:64]):
print('True')
ascii_string = binascii.unhexlify(possible_value).decode('ascii')
retrieved_flag_in_chr = retrieved_flag_in_chr + ascii_string
retrieved_flag = retrieved_flag + possible_value
return 'True'
else:
print('False')

count = 31 - int(len(retrieved_flag)/2)

for i in range(16):
print('loop:',i+1)
iteration_count = i+1
for possibile_value,text in zip(possiblities,texts):
result = sending_the_plaintext_after_XOR_2(possibile_value,text,iteration_count)
if(result == 'True'):
print('hi')
count = count - 1
break

print('Retrieved flag in Hex:',retrieved_flag)
print('Actual flag : 247CTF{'+ retrieved_flag_in_chr +'}')

This code gives the remaining 16 bytes of the flag.

The End:

I hope this article has given you some ideas for solving the cryptography challenge provided by 247CTF involving the BEAST vulnerability and its exploitation methods.

Finally, I thank whoever reading this, for spending your valuable time on my article.

Author: Girithar Ram Ravindran

Contact: https://www.linkedin.com/in/girithar-ram-ravindran-a4341017b/s

--

--