|
| 1 | +--- |
| 2 | +title: Crypto 2 |
| 3 | +date: 2024-12-23 19:47:00 +/-0600 |
| 4 | +categories: [Capture The Flags, Hacker Conclvae v2] |
| 5 | +tags: [ctf, hacker conclave v2, crypto, writeups] |
| 6 | +description: Hacker Conclave v2 Crpyto 2 Challenge |
| 7 | +--- |
| 8 | + |
| 9 | +> Challenge description: |
| 10 | +> |
| 11 | +> In this challenge, we have access to a program that will encrypt the flag we want to obtain. When connecting to port **[redacted for privacy]** at the address **[redacted for privacy]**, it will return the program's output. Will you be able to retrieve the flag? |
| 12 | +{: .prompt-info } |
| 13 | + |
| 14 | +Alright, so lets look at this challenge, when we connect to the port, it spits out two things at us. First, the source code, and then an encrypted message. Lets look at the source code. |
| 15 | + |
| 16 | + |
| 17 | +```python |
| 18 | + |
| 19 | +import os |
| 20 | +import random |
| 21 | +import string |
| 22 | +from Cryptodome.Cipher import AES |
| 23 | +from Cryptodome.Protocol.KDF import PBKDF2 |
| 24 | +from Cryptodome.Random import get_random_bytes |
| 25 | +from Cryptodome.Util.number import bytes_to_long |
| 26 | + |
| 27 | +characters = string.ascii_letters + string.digits |
| 28 | + |
| 29 | +if os.path.exists("/flag/flag.txt"): |
| 30 | + flag=(open("/flag/flag.txt","r").read()).encode("utf-8"); |
| 31 | +else: |
| 32 | + flag=(open("flag.txt","r").read()).encode("utf-8"); |
| 33 | + |
| 34 | +key = ((""+random.choice(characters))*16).encode("utf-8"); |
| 35 | + |
| 36 | +cipher = AES.new(key, AES.MODE_ECB); |
| 37 | + |
| 38 | +padded_content = flag.ljust((len(flag) // 16 + 1) * 16, b'\x00'); |
| 39 | +encrypted_content = cipher.encrypt(padded_content); |
| 40 | +encrypted_content = bytes_to_long(encrypted_content); |
| 41 | + |
| 42 | +print(open("cifra.py","r").read()); |
| 43 | +print("encrypted_content="+str(encrypted_content)+"\n"); |
| 44 | + |
| 45 | +``` |
| 46 | +{: file="cifra.py" } |
| 47 | + |
| 48 | +Okay, so this ran when we connected, and thats why it printed the entire program and the encrypted content to the terminal, as that is the last thing that this program `cifra.py` does. Lets break down this program line by line. |
| 49 | + |
| 50 | +```python |
| 51 | + |
| 52 | +import os |
| 53 | +import random |
| 54 | +import string |
| 55 | +from Cryptodome.Cipher import AES |
| 56 | +from Cryptodome.Protocol.KDF import PBKDF2 |
| 57 | +from Cryptodome.Random import get_random_bytes |
| 58 | +from Cryptodome.Util.number import bytes_to_long |
| 59 | + |
| 60 | +``` |
| 61 | +{: file="cifra.py" } |
| 62 | + |
| 63 | +The `os` module is commonly used for using the functionality of the operating system. In this program its used to check if the flag is at `flag/flag.txt`. |
| 64 | + |
| 65 | +The `random` module is used for randomness, as the name suggests. Its used in the program with `random.choice()`, where it makes a random selection based on hat is passed to `choice()`. |
| 66 | + |
| 67 | +The `string` module is used here for the `string.ascii_letters` and `string.digits`, which is used to make the `characters` variable. |
| 68 | + |
| 69 | +Next up is `Cryptodome`. It is another module that gives python some expanded cryptographic functionalities. Here, we they are importing `AES`, `PBKDF2`, `get_random_bytes`, and `bytes_to_long`. We'll go over their functionality as we get to them. |
| 70 | + |
| 71 | +Moving away from the import statements, lets get into the meat and potatoes of the code. First up, we have: |
| 72 | + |
| 73 | +```python |
| 74 | + |
| 75 | +characters = string.ascii_letters + string.digits |
| 76 | + |
| 77 | +``` |
| 78 | +{: file="cifra.py" } |
| 79 | + |
| 80 | +This is a pretty simple line. It takes all uppercase and lowercase ascii letters, and all digits 0-9 and concatenates them, seting characters value to `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789` |
| 81 | + |
| 82 | +So, moving on, we are confronted with this: |
| 83 | + |
| 84 | +```python |
| 85 | + |
| 86 | +if os.path.exists("/flag/flag.txt"): |
| 87 | + flag=(open("/flag/flag.txt","r").read()).encode("utf-8"); |
| 88 | +else: |
| 89 | + flag=(open("flag.txt","r").read()).encode("utf-8"); |
| 90 | + |
| 91 | +``` |
| 92 | +{: file="cifra.py" } |
| 93 | + |
| 94 | +This `if...else` block first checks to see if the path to `/flag/flag.txt` exists, and if it does then it opens the file in read mode with UTF-8 encoding. However, if the path doesn't exist, it would open `./flag.txt` in read mode with UTF-8 encoding. |
| 95 | + |
| 96 | +This next line is where the vulnerability in the code lies. |
| 97 | + |
| 98 | +```python |
| 99 | + |
| 100 | +key = ((""+random.choice(characters))*16).encode("utf-8"); |
| 101 | + |
| 102 | +``` |
| 103 | +{: file="cifra.py" } |
| 104 | + |
| 105 | +Usually, a 16 character key can be pretty secure, knowing that there are 62 possible choices for each character, leaving a whopping `47,672,401,706,823,533,450,263,330,816`, or `Forty-seven octillion, six hundred seventy-two septillion, four hundred one sextillion, seven hundred six quintillion, eight hundred twenty-three quadrillion, five hundred thirty-three trillion, four hundred fifty billion, two hundred sixty-three million, three hundred thirty thousand, eight hundred sixteen`. However, the arrow in the knee is that they randomly generate one character and repeat it 16 times, taking that huge number of possilities and turning it into `62`. This, is easily brute-forcable. |
| 106 | + |
| 107 | +```python |
| 108 | + |
| 109 | +padded_content = flag.ljust((len(flag) // 16 + 1) * 16, b'\x00'); |
| 110 | +encrypted_content = cipher.encrypt(padded_content); |
| 111 | +encrypted_content = bytes_to_long(encrypted_content); |
| 112 | + |
| 113 | +print(open("cifra.py","r").read()); |
| 114 | +print("encrypted_content="+str(encrypted_content)+"\n"); |
| 115 | + |
| 116 | +``` |
| 117 | +{: file="cifra.py" } |
| 118 | + |
| 119 | +The rest of the code covers encrypting the flag, and then printing the program and the encrypted flag to the terminal. |
| 120 | + |
| 121 | +The encrypted flag I got was `33184633452588628947694484591780825103796687823569131220950080094742922022993114204314814746054128940142933245107995` |
| 122 | + |
| 123 | +Lets now review my decryption program. |
| 124 | + |
| 125 | +```python |
| 126 | + |
| 127 | +from Cryptodome.Cipher import AES |
| 128 | +from Cryptodome.Util.number import long_to_bytes |
| 129 | +import string |
| 130 | + |
| 131 | +# Given encrypted content (ciphertext as a long integer) |
| 132 | +encrypted_content = 33184633452588628947694484591780825103796687823569131220950080094742922022993114204314814746054128940142933245107995 |
| 133 | + |
| 134 | +# Convert encrypted content back to bytes |
| 135 | +ciphertext = long_to_bytes(encrypted_content) |
| 136 | + |
| 137 | +# Character set used to generate the key |
| 138 | +characters = string.ascii_letters + string.digits |
| 139 | + |
| 140 | +# Brute-force all possible single-character keys repeated 16 times |
| 141 | +for char in characters: |
| 142 | + key = (char * 16).encode("utf-8") # Key is one character repeated 16 times |
| 143 | + cipher = AES.new(key, AES.MODE_ECB) # Initialize cipher with key |
| 144 | + |
| 145 | + try: |
| 146 | + decrypted_content = cipher.decrypt(ciphertext).rstrip(b'\x00') # Remove padding |
| 147 | + if decrypted_content.startswith(b"conclave{"): # Check if it starts with "conclave{" |
| 148 | + print(f"Key: {key.decode()} | Flag: {decrypted_content.decode()}") |
| 149 | + except Exception: |
| 150 | + continue # Skip invalid decryptions |
| 151 | + |
| 152 | +``` |
| 153 | +{: file="bruteforce.py" } |
| 154 | + |
| 155 | +To quickly cover the program, we import some of the same modules as they did originally in order to reverse the encrypted content to bytes, and to recreate the `characters` variable. Then for each character in `characters` we are going to make an AES key the same way they did and attempt to decrypt it. If it starts with `conclave{`, which is the flag format for the CTF, then we know we have the correct key. And it's all wrapped in a `try...except` in order to skip past invalid decryptions that might cause the program to error out. |
| 156 | + |
| 157 | +```terminal |
| 158 | +
|
| 159 | +┌─[slavetomints@parrot]─[~/ctfs/hacker-conclave-v2/crypto/crypto2] |
| 160 | +└──╼ $python bruteforce.py |
| 161 | +Key: HHHHHHHHHHHHHHHH | Flag: conclave{40e9222eee660a0c1cd2e736d613a7e1} |
| 162 | +
|
| 163 | +``` |
| 164 | + |
| 165 | +FLAG: `conclave{40e9222eee660a0c1cd2e736d613a7e1}` |
0 commit comments