Sommaire
Cryptographie
Limited1
Enoncé :
It's pretty easy to find random integers if you know the seed, but what if every second has a different seed?
Code :
import time
import random
import sys
if __name__ == '__main__':
flag = input("Flag? > ").encode('utf-8')
correct = [189, 24, 103, 164, 36, 233, 227, 172, 244, 213, 61, 62, 84, 124, 242, 100, 22, 94, 108, 230, 24, 190, 23, 228, 24]
time_cycle = int(time.time()) % 256
if len(flag) != len(correct):
print('Nope :(')
sys.exit(1)
for i in range(len(flag)):
random.seed(i+time_cycle)
if correct[i] != flag[i] ^ random.getrandbits(8):
print('Nope :(')
sys.exit(1)
print(flag)
Découverte
On découvre ce chall, il y a plusieurs choses qu'on sait.
La longueur du flag, ici 25 caractères.
Le fait que chaque caractère soit chiffré avec une clé nommé correct
.
On voit un random.seed
on sait donc que le random.getrandbits(8)
est prédictible si on connaît la seed, en l'occurrance ici, il est assez facile de retrouver la seed, car time_cycle
est comprise entre 0 et 255 puisqu'on à cette ligne : time_cycle = int(time.time()) % 256
Ainsi, en faisant du brute force de la seed, on retrouvera le flag.
Il ne faut pas oublier une chose,
donc
Résolution
import random
correct = [189, 24, 103, 164, 36, 233, 227, 172, 244, 213, 61, 62, 84, 124, 242, 100, 22, 94, 108, 230, 24, 190, 23, 228, 24]
for time_cycle in range(256):
flag = []
for i in range(len(correct)):
random.seed(i+time_cycle)
flag.append(correct[i] ^ random.getrandbits(8))
try:
print(bytes(flag).decode('utf-8'))
except:
pass
Flag
wctf{f34R_0f_m1ss1ng_0ut}
Limited2
Enoncé :
I was AFK when the flag was being encrypted, can you help me get it back?
Code :
import time
import random
import sys
if __name__ == '__main__':
flag = input("Flag? > ").encode('utf-8')
correct = [192, 123, 40, 205, 152, 229, 188, 64, 42, 166, 126, 125, 13, 187, 91]
if len(flag) != len(correct):
print('Nope :(')
sys.exit(1)
if time.gmtime().tm_year >= 2024 or time.gmtime().tm_year < 2023:
print('Nope :(')
sys.exit(1)
if time.gmtime().tm_yday != 365 and time.gmtime().tm_yday != 366:
print('Nope :(')
sys.exit(1)
for i in range(len(flag)):
# Totally not right now
time_current = int(time.time())
random.seed(i+time_current)
if correct[i] != flag[i] ^ random.getrandbits(8):
print('Nope :(')
sys.exit(1)
time.sleep(random.randint(1, 60))
print(flag)
Découverte
Même challenge que celui d'avant, seul différence, c'est sur le time, il faut que ce soit le 31 décembre 2023.
Résolution
J'ai juste pris le 30 décembre 2023 et le 1 janvier 2024 pour être sûr.
import time
import random
import sys
correct = [192, 123, 40, 205, 152, 229, 188, 64, 42, 166, 126, 125, 13, 187, 91]
year = 2023
day = 365 #ou 366
if year >= 2024 or year < 2023:
print('Nope :(')
sys.exit(1)
if day != 365 and day != 366:
print('Nope :(')
sys.exit(1)
for time_current in range(1703894400,1704153600):
flag = []
for i in range(len(correct)):
random.seed(i+time_current)
flag.append(correct[i] ^ random.getrandbits(8))
time_current += random.randint(1, 60)
try:
print(bytes(flag).decode('utf-8'))
except:
pass
Flag
wctf{b4ll_dr0p}
Blocked1
Enoncé :
The WOLPHV group (yes, this is an actual article) group encrypted our files, but then blocked us for some reason. I think they might have lost the key. Let's log into their accounts and find it...
nc blocked1.wolvctf.io 1337
Code du serveur :
"""
----------------------------------------------------------------------------
NOTE: any websites linked in this challenge are linked **purely for fun**
They do not contain real flags for WolvCTF.
----------------------------------------------------------------------------
"""
import random
import secrets
import sys
import time
from Crypto.Cipher import AES
MASTER_KEY = secrets.token_bytes(16)
def generate(username):
iv = secrets.token_bytes(16)
msg = f'password reset: {username}'.encode()
print(iv + msg)
if len(msg) % 16 != 0:
msg += b'\0' * (16 - len(msg) % 16)
cipher = AES.new(MASTER_KEY, AES.MODE_CBC, iv=iv)
return iv + cipher.encrypt(msg)
def verify(token):
iv = token[0:16]
msg = token[16:]
cipher = AES.new(MASTER_KEY, AES.MODE_CBC, iv=iv)
pt = cipher.decrypt(msg)
username = pt[16:].decode(errors='ignore')
print(username)
return username.rstrip('\x00')
def main():
username = f'guest_{random.randint(100000, 999999)}'
print(""" __ __
_ ______ / /___ / /_ _ __
| | /| / / __ \\/ / __ \\/ __ \\ | / /
| |/ |/ / /_/ / / /_/ / / / / |/ /
|__/|__/\\____/_/ .___/_/ /_/|___/
/_/""")
print("[ password reset portal ]")
print("you are logged in as:", username)
print("")
while True:
print(" to enter a password reset token, please press 1")
print(" if you forgot your password, please press 2")
print(" to speak to our agents, please press 3")
s = input(" > ")
if s == '1':
token = input(" token > ")
if verify(bytes.fromhex(token)) == 'doubledelete':
print(open('flag.txt').read())
sys.exit(0)
else:
print(f'hello, {username}')
elif s == '2':
print(generate(username).hex())
elif s == '3':
print('please hold...')
time.sleep(2)
# thanks chatgpt
print("Thank you for reaching out to WOLPHV customer support. We appreciate your call. Currently, all our agents are assisting other customers. We apologize for any inconvenience this may cause. Your satisfaction is important to us, and we want to ensure that you receive the attention you deserve. Please leave your name, contact number, and a brief message, and one of our representatives will get back to you as soon as possible. Alternatively, you may also visit our website at https://wolphv.chal.wolvsec.org/ for self-service options. Thank you for your understanding, and we look forward to assisting you shortly.")
print("<beep>")
main()
Découverte
Un classique, AES bit flipping
Résolution
Avec 2
on récupère le token de l'utilisateur en cours.
Avec 1
on déchiffre le token et on récupère l'utilisateur : Hello, guest_540874
.
Puis on bitflip de password reset: guest_540874
vers password reset: doubledelete
.
Et on l'envoie à 1
le nouveau token.
Utilisation du repo de Vozec pour craft le nouveau token : AES-Flipper
from AES_flipper import Aesflipper
enc = b'021632c825d0e35e0a019cfb54ee0706a4b6c61b020919953d89b373d40999c824eea5246ebf05f6f714037bc55b2141'
plain = b'password reset: guest_540874'
target = b'password reset: doubledelete'
flipper = Aesflipper(
plain=plain,
ciphertext=enc,
add_iv=True,
debug=True
)
token = flipper.full_flip(target=target)
print(token)
Flag
wctf{th3y_l0st_th3_f1rst_16_byt35_0f_th3_m3ss4g3_t00}
Blocked2
Enoncé :
We managed to log into doubledelete's email server. Hopefully this should give us some leads...
nc blocked2.wolvctf.io 1337
Code :
import random
import secrets
import sys
import time
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.strxor import strxor
MASTER_KEY = secrets.token_bytes(16)
def encrypt(message):
if len(message) % 16 != 0:
print("message must be a multiple of 16 bytes long! don't forget to use the WOLPHV propietary padding scheme")
return None
iv = secrets.token_bytes(16)
cipher = AES.new(MASTER_KEY, AES.MODE_ECB)
blocks = [message[i:i+16] for i in range(0, len(message), 16)]
# encrypt all the blocks
encrypted = [cipher.encrypt(b) for b in [iv, *blocks]]
# xor with the next bloc of plaintext
for i in range(len(encrypted) - 1):
encrypted[i] = strxor(encrypted[i], blocks[i])
return iv + b''.join(encrypted)
def main():
message = open('message.txt', 'rb').read()
print(""" __ __
_ ______ / /___ / /_ _ __
| | /| / / __ \\/ / __ \\/ __ \\ | / /
| |/ |/ / /_/ / / /_/ / / / / |/ /
|__/|__/\\____/_/ .___/_/ /_/|___/
/_/""")
print("[ email portal ]")
print("you are logged in as doubledelete@wolp.hv")
print("")
print("you have one new encrypted message:")
print(encrypt(message).hex())
while True:
print(" enter a message to send to dree@wolp.hv, in hex")
s = input(" > ")
message = bytes.fromhex(s)
print(encrypt(message).hex())
main()
Découverte
Ici, il faut bien comprendre comment fonctionne le chiffrement et comment ECB fonctionne.
On commence par récupérer un message
chiffré avec une clé MASTER_KEY
.
Ensuite, on peut chiffrer autant de messages qu'on veut avec la même clé.
Comment un message est chiffré
On a un IV qui est initialisé.
Ensuite, on découpe notre message
en bloc de 16.
blocks = [message[i:i+16] for i in range(0, len(message), 16)]
Ensuite on chiffre ces blocs avec une petite particularité, on chiffre aussi l'IV.
encrypted = [cipher.encrypt(b) for b in [iv, *blocks]]
Si F est notre fonction de chiffrement, on a pour un message de longueur 32 :
Ensuite, on boucle sur tous les éléments de encrypted
sauf le dernier
On a donc :
Ensuite, la fonction retourne :
iv + b''.join(encrypted)
ainsi on connaît l'IV généré
L'idée
C'est de récupérer chaque bloc du texte chiffré qui contient le flag. (C'est l'IV en clair au début)
Ensuite, on chiffre ce bloc : con.sendline(block_i)
On récupère le dernier bloc. Mais pourquoi ?
Souvenez-vous comment sont chiffré les données, en chiffrant juste le bloc de 16 bytes, on obtient ça :
et donc, les 16 derniers bytes, contienne notre message passé par la fonction de chiffrage.
Ainsi sur le premier tour, on obtient:
Et il suffit de xorer ça avec le deuxième bloc du message
du début pour récupérer le premier bloc en clair du message
chiffré.
Pour rappel, le deuxième bloc de message :
et ensuite, on réutilise se nouveau bloc connue, pour récupérer celui d'après, et ainsi de suite.
Résolution
from pwn import *
import time
con = remote('blocked2.wolvctf.io', 1337)
con.recvuntil(b'message:\n')
message_encrypt = con.recvline().strip().decode()
block_i = message_encrypt[:32].encode()
msg_found = b''
for i in range(20):
print("Tour : ",i)
print("bloc : ",block_i)
con.recvuntil(b'> ')
con.sendline(block_i)
time.sleep(0.5)
test_encrypt = con.recvline().strip().decode()
last_encrypt = test_encrypt[-32:]
block_i = xor(bytes.fromhex(last_encrypt), bytes.fromhex(message_encrypt[32*(i+1):32*(i+2)]))
msg_found += block_i
block_i = block_i.hex().encode()
print(msg_found)
Flag
wctf{s0m3_g00d_s3cur1ty_y0u_h4v3_r0lling_y0ur_0wn_crypt0_huh}
TagSeries1
Enoncé :
Don't worry, the interns wrote this one.
nc tagseries1.wolvctf.io 1337
Code du serveur :
import sys
import os
from Crypto.Cipher import AES
MESSAGE = b"GET FILE: flag.txt"
QUERIES = []
BLOCK_SIZE = 16
KEY = os.urandom(BLOCK_SIZE)
def oracle(message: bytes) -> bytes:
aes_ecb = AES.new(KEY, AES.MODE_ECB)
return aes_ecb.encrypt(message)[-BLOCK_SIZE:]
def main():
for _ in range(3):
command = sys.stdin.buffer.readline().strip()
tag = sys.stdin.buffer.readline().strip()
if command in QUERIES:
print(b"Already queried")
continue
if len(command) % BLOCK_SIZE != 0:
print(b"Invalid length")
continue
result = oracle(command)
if command.startswith(MESSAGE) and result == tag and command not in QUERIES:
with open("flag.txt", "rb") as f:
sys.stdout.buffer.write(f.read())
sys.stdout.flush()
else:
QUERIES.append(command)
assert len(result) == BLOCK_SIZE
sys.stdout.buffer.write(result + b"\n")
sys.stdout.flush()
if __name__ == "__main__":
main()
Découverte
Ici, on utilise une propriété désastreuse du mode ECB
Le but est d'envoyer une command
ainsi que le message chiffré. (le tag)
Et si ce n'est pas la command
attendu, le serveur nous donne le tag.
Subtilité, le serveur ne renvoie que les 16 derniers bytes du texte chiffré, mais du coup, il ne demande aussi que les 16 derniers.
Résolution
Ainsi on découpe notre flag en 2 blocs de 16 bytes,
GET FILE: flag.t
et xtaaaaaaaaaaaaaa
On envoie xtaaaaaaaaaaaaaa
au serveur, il chiffre le bloc et nous donne son tag.
Ensuite, on concatène les deux bloc on l'envoie et on donne le tag récupéré avec xtaaaaaaaaaaaaaa
et hop, on a le flag.
from pwn import *
conn = remote('tagseries1.wolvctf.io', 1337)
conn.recvuntil(b'== proof-of-work: disabled ==\n')
conn.sendline(b'xtaaaaaaaaaaaaaa')
conn.sendline(b'a' * 16)
r2 = conn.recvline()
print(r2)
conn.sendline(b'GET FILE: flag.txtaaaaaaaaaaaaaa')
conn.sendline(r2)
conn.interactive()
Flag
wctf{C0nGr4ts_0n_g3tt1ng_p4st_A3S}
TagSeries3
Enoncé :
Surely they got it right this time.
nc tagseries3.wolvctf.io 1337
Code serveur :
import sys
import os
from hashlib import sha1
MESSAGE = b"GET FILE: "
SECRET = os.urandom(1200)
def main():
_sha1 = sha1()
_sha1.update(SECRET)
_sha1.update(MESSAGE)
sys.stdout.write(_sha1.hexdigest() + '\n')
sys.stdout.flush()
_sha1 = sha1()
command = sys.stdin.buffer.readline().strip()
hash = sys.stdin.buffer.readline().strip()
_sha1.update(SECRET)
_sha1.update(command)
if command.startswith(MESSAGE) and b"flag.txt" in command:
if _sha1.hexdigest() == hash.decode():
with open("flag.txt", "rb") as f:
sys.stdout.buffer.write(f.read())
if __name__ == "__main__":
main()
Découverte
Classique Hash length extension attack.
Comprendre l'attaque : Hash length attack
Résolution
J'ai changé le fonctionnement d'un repository github pour récupérer la réponse directement en python.
Le repo : Hash_Extender (Il va vraiment falloir que je fasse ma propre lib ^^)
from pwn import *
import os
from Length_Extender import main
conn = remote('tagseries3.wolvctf.io', 1337)
conn.recvuntil(b'== proof-of-work: disabled ==\n')
hash1 = conn.recvline()
print(hash1)
hash_name = "SHA1"
sign = hash1.decode().strip()
data_ = "GET FILE: "
append = "flag.txt"
key_length = 1200
message, hash2 = main(hash_name, sign, data_, append, key_length)
conn.sendline(message)
conn.sendline(hash2.encode())
conn.interactive()
Flag
wctf{M4n_t4er3_mu5t_b3_4_bett3r_w4y}