Logo

GuidePoint Security CTF 2021 - Half (crypto)

3 minute read Published:

Writeup for the Guidepoint 2021 CTF Half crypto challenge

Guidepoint Security CTF 2021 - Half (crypto)

For this challenge we get some encoded/encrypted string and the python script that was used to produce it.

3539333437353461373137333532333833333333333933303335333036383639dcb33a3cca39412b58f4095cbc30faf95f72c9c1e71c01aa0a1b0f89c11f03b751dccaa5bb3ec011cc0a40a08ba87827071e1fb52716c891a1263a53af721a18
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, AES
from Crypto import Random # May be able to get rid of
from random import choice, shuffle
from OpenSSL import crypto
import binascii
import base64

pub = crypto.load_publickey(crypto.FILETYPE_PEM, base64.b64decode('LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF0YmEwY2x2RWllS3A0MUtYaGlpUwpqWnluc2E5RlQzTmpVUE1ZaVBVLzN5L2IySU8zcnFaZmh5RTRCNlAzYXpueDRZMkRoYVVVZFNnN0V5OHJzZ29jCis0dzlIdDYwSTdEWWUxblVKeUt1ekZyTDZESmdxSFR6Sml3SHBCWFNTVnVhaU5KY2NRVXJKMWNaRzdTVG44YmcKbzBCdHNGT0tyVzVzTzNyOGxNWitxWDVldXNZWW9UMDd6U0p5T1V4WVNJcWlwUVpPcEc3Y2JNYVhQZlZaaERDbwpyOW9UVFZaUFA1ZzlqOHNoSmdDVnJLeXE4V2dQTk1sWDRBMVhKQnpIcXFZN2RTK2NZRFhuMmc2dmxOa2RESXpmCjd6U1ZxL0NWZzA1MG1CdXZYdTVWaWVheHhZQnREb0xUQ0JWMmcyYXlOY2pac2tJVmhFbXpvTjNveEd5dFFVNFIKUXdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=='))
pubkey = crypto.dump_publickey(crypto.FILETYPE_PEM, pub)

def random_string(size, chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"):
  return ''.join(choice(chars) for z in range (size))

def encrypt_FileString( raw ):
  BS = 16
  pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
  key = bytes(random_string(16), 'utf-8')
  raw = bytes(pad(raw),'utf-8')
  iv = Random.new().read( AES.block_size )
  cipher = AES.new( key, AES.MODE_CBC, iv )
  return binascii.hexlify(iv + cipher.encrypt( raw )).decode('utf-8').strip(), binascii.hexlify(key)


def encrypt_RSA(pubkey, aeskey):
  try:
    rsakey = RsA.importKey(pubkey)
    rsakey = PKCS1_OAEP.new(rsakey)
    encrypted = rsakey.encrypt(aeskey)
  except:
    encrypted = aeskey
  return binascii.hexlify(encrypted)


flag = 'FlagWentHere!'

stuff = encrypt_FileString(flag)

RSA_done = encrypt_RSA(pubkey, stuff[1])

with open('half_flag.txt', 'w') as f:
  f.write(RSA_done.decode('utf-8') + stuff[0])
f.close()

A lot of the time spent on this challenge was just figuring out what the heck this script is doing exactly, so let’s get that cleared up first;

  • There is a flag
  • The flag gets AES-CBC encrypted
  • The AES encryption key gets passed to the encrypt_RSA function
  • The RSA encrypted AES key, the hex encoded AES-CBC IV, and the hex encoded AES-CBC ciphertext of the flag gets written to a file

The big “AH-HAH!” in this challenge is in the encrypt_RSA function. Specifically what happens if there is some exception in the RSA encryption process;

  except:
    encrypted = aeskey

AH-HAH! So the AES key is not RSA-encrypted at all but only hex encoded when an exception occurs. Since this is a CTF I did not dig into why the RSA operation might have failed but just proceeded under the assumption that it did.

Now it just becomes a matter of decoding the output string we got and doing the AES decryption. From the encrypt_FileString function we can see the key should be 16 bytes long, but was hex encoded before being returned and passed to the encrypt_RSA function, which hex encoded it again. So we need to take the first 64 characters of our string and hex decode them twice to get our AES-CBC key. Then we get the next 32 characters for our 16-byte hex encoded IV. (We know it’s 16 bytes because AES.block_size is 16.) The remainder of our given output is our hex encoded ciphertext.

Now all we need to do is pass these parameters to cyberchef and have it do the decryption to get our flag.