Gotta Crack Them All is a crypto challenge that was part of the 2022 CSAW CTF qualifiers. In order to complete the challenge, the player must crack all the encrypted passwords that were leaked along with a decrypted password and the encryption algorithm to get the admin’s password, the flag.

Unfortunately, we didn’t manage to solve the challenge during the event.

Files

encrypt.py

with open('key.txt','rb') as f:
	key = f.read()

def encrypt(plain):
	return b''.join((ord(x) ^ y).to_bytes(1,'big') for (x,y) in zip(plain,key))

leaked_password.txt

Cacturne-Grass-Dark

encrypted_passwords.txt

cr˪�Si��晿
lzצ�Wr����
kṣ�Zr�����.�
`zս�Xb�����p
kwĺ�Ba���ŝ�y�
kzƹ�Duˊ����o���d
{wʺ�_uɊ����s�j��v��Ҫ
{u���Sw�����U�"
`tʽ��޺���1�/��{
zsܽ�Dr���Վ�i�#��`��
...

Vulnerability

The encryption algorithm is a simple XOR.

b''.join((ord(x) ^ y).to_bytes(1,'big') for (x,y) in zip(plain,key))

Hack

We already have the password: “Cacturne-Grass-Dark” with which we can retrieve the encrypted part using the web service that the admin created to encrypt the passwords.

nc crypto.chal.csaw.io 5002

We get

b'kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd'

as return.

Now we can get the key used to encrypt the password.

def encrypt(plain):
	return b''.join((ord(x) ^ y).to_bytes(1,'big') for (x,y) in zip(plain,key))

pw = "Cacturne-Grass-Dark"
leak = b'kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd'

key = leak

key = encrypt(pw)
print("encrypt:", key , "<-- key")
leak_test = encrypt(pw)
print("leak_test -->", leak_test, "<-- leak")

Output:

encrypt: b'(\x1b\xa5\xcd\xac6\x1b\xae\xa7\xd9\x92\xfc\xcd\x1c\xc3G\xae\xaf\x0f' <-- key
leak_test --> b'kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd' <-- leak

To decrypt the other passwords we need to change the encryption function by removing the ord() since we no longer need to convert characters to integers.

def solve(plain):
	return b''.join((x ^ y).to_bytes(1,'big') for (x,y) in zip(plain,key))

Test the solve function.

def encrypt(plain):
	return b''.join((ord(x) ^ y).to_bytes(1,'big') for (x,y) in zip(plain,key))

def solve(plain):
	return b''.join((x ^ y).to_bytes(1,'big') for (x,y) in zip(plain,key))

pw = "Cacturne-Grass-Dark"
leak = b'kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd'

key = leak

key = encrypt(pw)
print("encrypt:", key , "<-- key")
leak_test = encrypt(pw)
print("leak_test -->", leak_test, "<-- leak")
solve_test = solve(leak_test)
print("solve_test -->", solve_test, "<-- pw")

Output:

encrypt: b'(\x1b\xa5\xcd\xac6\x1b\xae\xa7\xd9\x92\xfc\xcd\x1c\xc3G\xae\xaf\x0f' <-- key
leak_test --> b'kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd' <-- leak
solve_test --> b'Cacturne-Grass-Dark' <-- pw

Preparing the encrypted passwords

with open("encrypted_passwords.txt", mode="rb") as file:
	lines = file.read().splitlines()

for i, line in enumerate(lines):
	print(i, line)

Output:

0 b'cr\xcb\xaa\xc0Si\x83\xf0\xb8\xe6\x99\xbf'
1 b'lz\xd7\xa6\xdeWr\x83\xe3\xb8\xe0\x97'
2 b'ks\xcc\xa3\xcbZr\xc0\xc0\xf4\xc2\x8f\xb4\x7f\xab.\xcd'
3 b'`z\xd5\xbd\xc5Xb\x83\xe9\xb6\xe0\x91\xacp'
4 b'kw\xc4\xba\xc5Ba\xcb\xd5\xf4\xc5\x9d\xb9y\xb1'
5 b'kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd'
6 b'{w\xca\xba\xc7_u\xc9\x8a\x89\xfd\x95\xbes\xadj\xfe\xdcv\xf6\xe4\xd2\xaa'
7 b'{u\xc0\xac\xdfSw\x83\xe3\xb8\xe0\x97\xe0U\xa0"'
8 b'`t\xca\xbd\xcd\x1bK\xdd\xde\xba\xfa\x95\xae1\x84/\xc1\xdc{'
9 b'zs\xdc\xbd\xc9Dr\xc1\xd5\xf4\xd5\x8e\xa2i\xad#\x83\xfd`\xf6\xe7'
10 b'{~\xc0\xa9\xc3B6\xe9\xd5\xb8\xe1\x8f'
...

Execution of the Exploit

Final code:

#!/usr/bin/env python

def encrypt(plain):
	return b''.join((ord(x) ^ y).to_bytes(1,'big') for (x,y) in zip(plain,key))

def solve(plain):
	return b''.join((x ^ y).to_bytes(1,'big') for (x,y) in zip(plain,key))

pw = "Cacturne-Grass-Dark"
leak = b'kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd'

key = leak

key = encrypt(pw)
print("encrypt:", key , "<-- key")
leak_test = encrypt(pw)
print("leak_test -->", leak_test, "<-- leak")
solve_test = solve(leak_test)
print("solve_test -->", solve_test, "<-- pw")

with open("encrypted_passwords.txt", mode="rb") as file:
	lines = file.read().splitlines()

for i, line in enumerate(lines):
	print(i, "solve -->", solve(line))

Command:

./decrypt.py

Output:

encrypt: b'(\x1b\xa5\xcd\xac6\x1b\xae\xa7\xd9\x92\xfc\xcd\x1c\xc3G\xae\xaf\x0f' <-- key
leak_test --> b'kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd' <-- leak
solve_test --> b'Cacturne-Grass-Dark' <-- pw
0 solve --> b'Kingler-Water'
1 solve --> b'Darkrai-Dark'
2 solve --> b'Chingling-Psychic'
3 solve --> b'Happiny-Normal'
4 solve --> b'Clawitzer-Water'
5 solve --> b'Cacturne-Grass-Dark'
6 solve --> b'Slowking-Poison-Psy'
7 solve --> b'Sneasel-Dark-Ice'
8 solve --> b'Hoopa-Psychic-Ghost'
9 solve --> b'Rhyperior-Ground-Ro'
10 solve --> b'Seedot-Grass'
11 solve --> b'Chinchou-Water-Elec'
12 solve --> b'Tsareena-Grass'
13 solve --> b'Excadrill-Ground-St'
14 solve --> b'Gumshoos-Normal'
15 solve --> b'Kricketune-Bug'
16 solve --> b'Dartrix-Grass-Flyin'
17 solve --> b'Pikipek-Normal-Flyi'
18 solve --> b'Dugtrio-Ground-Stee'
19 solve --> b'Basculin-Water'
20 solve --> b'Hippowdon-Ground'
21 solve --> b'Togetic-Fairy-Flyin'
22 solve --> b'Finneon-Water'
23 solve --> b'Riolu-Fighting'
24 solve --> b'Entei-Fire'
25 solve --> b'Spritzee-Fairy'
26 solve --> b'Mantine-Water-Flyin'
27 solve --> b'Silvally-Normal'
28 solve --> b'Bellsprout-Grass-Po'
29 solve --> b'Wyrdeer-Normal-Psyc'
30 solve --> b'Marill-Water-Fairy'
31 solve --> b'Herdier-Normal'
32 solve --> b'Altaria-Dragon-Flyi'
33 solve --> b'Thwackey-Grass'
34 solve --> b'Spewpa-Bug'
35 solve --> b'Bronzong-Steel-Psyc'
36 solve --> b'Hakamo-o-Dragon-Fig'
37 solve --> b'Chespin-Grass'
38 solve --> b'Mr. Mime-Psychic-Fa'
39 solve --> b'Tornadus-Flying'
40 solve --> b'Pupitar-Rock-Ground'
41 solve --> b'Combusken-Fire-Figh'
42 solve --> b'Guzzlord-Dark-Drago'
43 solve --> b'Carnivine-Grass'
44 solve --> b'Growlithe-Fire'
45 solve --> b'Grubbin-Bug'
46 solve --> b'Gastrodon-Water-Gro'
47 solve --> b'Goomy-Dragon'
48 solve --> b'Thievul-Dark'
49 solve --> b'1n53cu2357234mc1ph3'
50 solve --> b'Seadra-Water'

The password “1n53cu2357234mc1ph3” is the only one that was not created from a list (Pokémon).

Should be the admin password, the flag.

No… we can only decrypt the passwords up to the 19th position, but we can find a longer password because a word list was used.

(“Pikipek-Normal-Flyi” should probably be “Pikipek-Normal-Flying”).

pw = "Pikipek-Normal-Flying"
leak = b'xr\xce\xa4\xdcSp\x83\xe9\xb6\xe0\x91\xacp\xee\x01\xc2\xd6f\xfb\xeb'

If we use the same script as before, we can find the correct password:

...
40 solve --> b'Pupitar-Rock-Ground'
41 solve --> b'Combusken-Fire-Fighti'
42 solve --> b'Guzzlord-Dark-Dragon'
43 solve --> b'Carnivine-Grass'
44 solve --> b'Growlithe-Fire'
45 solve --> b'Grubbin-Bug'
46 solve --> b'Gastrodon-Water-Groun'
47 solve --> b'Goomy-Dragon'
48 solve --> b'Thievul-Dark'
49 solve --> b'1n53cu2357234mc1ph32'
50 solve --> b'Seadra-Water'

Flag

The password / flag is “1n53cu2357234mc1ph32”.