We have a challenge that mimics an authentication server. It generates a random password of 16 bytes and a random key, used later for encryption operations.
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.number import bytes_to_long
from os import urandom
from utils import listener
FLAG = "crypto{???????????????????????????????}"
class CFB8:
def __init__(self, key):
self.key = key
def encrypt(self, plaintext):
IV = urandom(16)
cipher = AES.new(self.key, AES.MODE_ECB)
ct = b""
state = IV
for i in range(len(plaintext)):
b = cipher.encrypt(state)[0]
c = b ^ plaintext[i]
ct += bytes([c])
state = state[1:] + bytes([c])
return IV + ct
def decrypt(self, ciphertext):
IV = ciphertext[:16]
ct = ciphertext[16:]
cipher = AES.new(self.key, AES.MODE_ECB)
pt = b""
state = IV
for i in range(len(ct)):
b = cipher.encrypt(state)[0]
c = b ^ ct[i]
pt += bytes([c])
state = state[1:] + bytes([ct[i]])
return pt
class Challenge:
def __init__(self):
self.before_input = "Please authenticate to this Domain Controller to proceed\n"
self.password = urandom(20)
self.password_length = len(self.password)
self.cipher = CFB8(urandom(16))
def challenge(self, your_input):
if your_input["option"] == "authenticate":
if "password" not in your_input:
return {"msg": "No password provided."}
your_password = your_input["password"]
if your_password.encode() == self.password:
self.exit = True
return {"msg": "Welcome admin, flag: " + FLAG}
else:
return {"msg": "Wrong password."}
if your_input["option"] == "reset_connection":
self.cipher = CFB8(urandom(16))
return {"msg": "Connection has been reset."}
if your_input["option"] == "reset_password":
if "token" not in your_input:
return {"msg": "No token provided."}
token_ct = bytes.fromhex(your_input["token"])
if len(token_ct) < 28:
return {"msg": "New password should be at least 8-characters long."}
token = self.cipher.decrypt(token_ct)
new_password = token[:-4]
self.password_length = bytes_to_long(token[-4:])
self.password = new_password[: self.password_length]
return {"msg": "Password has been correctly reset."}
import builtins
builtins.Challenge = Challenge # hack to enable challenge to be run locally, see https://cryptohack.org/faq/#listener
listener.start_server(port=13399)
Note that we have the possibility of changing our password if we are able to supply a text that can be decrypted successfully. Because of the fact that the last 4 bytes are also used to determine the password length, if we managed to obtain any ciphertext whose last bytes are all 0
, we could supply an empty password and login.
The mode of operation used by the server to decrypt our message is CFB-8
, which is a variant of the CFB mode of operation. This mode of operation came to prominence when CVE-2020-1472 was released, and the reason was that CFB-8
was used in Windows’ Active Directory service to authenticate users. A researcher found that, because of this mode of operation, if the IV is not properly random it is possible to produce a ciphertext that consists of all zeros:
In order to solve the challenge we just need to send a message with 28 0 bytes:
#!/usr/bin/env python
from pwn import *
import json
attack_token = b"\x00" * 28
c = remote("socket.cryptohack.org", 13399)
i = 0
c.recvline()
while True:
print("Try:", i, end="\r")
c.sendline(
json.dumps({"option": "reset_password", "token": attack_token.hex()}).encode()
)
c.recvline()
c.sendline(json.dumps({"option": "authenticate", "password": ""}).encode())
text = c.recvline()
if b"admin" in text:
print("Flag:", json.loads(text)["msg"])
break
c.sendline(json.dumps({"option": "reset_connection"}).encode())
c.recvline()
i += 1
Flag: crypto{Zerologon_Windows_CVE-2020-1472}