← Back to Writeups
CUSTOMMediumCrypto

Logon Zero

XESXOR9/2/20263 min read
#crypto#custom#medium#all_zero_token#cfb8_state_collapse#retry_until_zero_keystream#authentication_bypass#cve-2020-1472

Logon Zero

Platform: cryptohack | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-22 | Status: Solved Techniques: all_zero_token, cfb8_state_collapse, retry_until_zero_keystream, authentication_bypass

Summary

Task: a CryptoHack AES-CFB8 'Domain Controller' that re-creates the ZeroLogon (REDACTED) cryptographic core; reset_password decrypts an attacker-controlled token in CFB-8 (attacker controls IV + ciphertext). Solution: submit a 28-byte all-zero token so the CFB-8 state stays zero; with probability 1/256 over the random key E_K(0)[0]=0, making every decrypted byte 0, which sets the password to empty (length=0), so authenticate("") returns the flag after a few hundred reset_connection retries.

Logon Zero — CryptoHack

Description

Please authenticate to this Domain Controller to proceed

Symmetric Ciphers / AES challenge (80 pts). A networked "Domain Controller" service over socket.cryptohack.org:13399 re-creates the cryptographic core of ZeroLogon (REDACTED) using AES in CFB-8 mode with a secret random key. The goal is to authenticate without knowing the random 20-byte password and recover the flag.

Analysis

The server (13399.py) exposes a JSON protocol with three options: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • authenticate — returns the flag iff password == self.password (initially urandom(20)).
  • reset_connection — re-randomizes the AES key (CFB8(urandom(16))).
  • reset_password — decrypts an attacker-supplied hex token with CFB-8, then:
    python
    token = self.cipher.decrypt(token_ct)   # token_ct >= 28 bytes
    new_password = token[:-4]
    self.password_length = bytes_to_long(token[-4:])
    self.password = new_password[:self.password_length]

The CFB-8 implementation is the textbook self-feeding one, and crucially the attacker controls both the IV and every ciphertext byte (the token is sent as raw ciphertext, IV = ciphertext[:16]): ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
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]      # keystream byte = E_K(state)[0]
        c = b ^ ct[i]                     # plaintext byte
        pt += bytes([c])
        state = state[1:] + bytes([ct[i]])  # feed CIPHERTEXT byte into state
    return pt

The all-zero token (ZeroLogon analogue)

Submit a 28-byte all-zero token: a 16-byte zero IV followed by 12 zero ciphertext bytes ("00" * 28). Trace the decrypt: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • Initial state = IV = b"\x00" * 16.
  • Byte 0: b = E_K(0)[0], pt[0] = b ^ ct[0] = E_K(0)[0] ^ 0 = E_K(0)[0]. Then state = state[1:] + bytes([ct[0]]) = (15 zero bytes) + 0x00, which is still all zeros.
  • Because the state never changes (we always feed the zero ciphertext byte back in), every keystream byte is E_K(0)[0] and every plaintext byte is E_K(0)[0].

So the entire 12-byte decrypted token is bytes([E_K(0)[0]]) * 12.

The 1/256 probability

With probability 1/256 over the random key, E_K(0)[0] == 0. When that happens, the decrypted token is b"\x00" * 12, and: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
new_password    = token[:-4]   = b"\x00" * 8
password_length = bytes_to_long(token[-4:]) = int(b"\x00\x00\x00\x00") = 0
password        = new_password[:0] = b""       # EMPTY

The password becomes the empty string, so authenticate with password="" succeeds and returns the flag.

This is exactly the ZeroLogon flaw: Netlogon used AES-CFB8 with a zero IV, and an all-zero ciphertext keeps the cipher state zero, so for ~1/256 of session keys the keystream collapses to zero and an attacker can bypass authentication simply by retrying. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Exploit loop:

  1. reset_password with the all-zero token.
  2. authenticate with password="".
  3. On success the response contains the flag. On failure (current key has E_K(0)[0] != 0), reset_connection to re-randomize the key and repeat.

Each iteration has ~1/256 success probability, so it lands within a few hundred iterations (we hit it around iteration ~500).

python
#!/usr/bin/env python3
"""Logon Zero (port 13399) — ZeroLogon (CVE-2020-1472) on AES-CFB8."""
import socket, json, sys
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

HOST, PORT = "socket.cryptohack.org", 13399

def main():
    s = socket.create_connection((HOST, PORT), timeout=60)
    f = s.makefile("rwb")
    f.readline()  # banner

    def send(obj):
        f.write((json.dumps(obj) + "\n").encode()); f.flush()
        return json.loads(f.readline().decode())

    zero_token = "00" * 28  # 16-byte zero IV + 12 zero ciphertext bytes
    it = 0
    while True:
        it += 1
        send({"option": "reset_password", "token": zero_token})
        r = send({"option": "authenticate", "password": ""})
        msg = r.get("msg", "")
        if "crypto{" in msg:
            print(msg)
            return
        if it % 50 == 0:
            print(f"[*] iter {it}, still trying...", file=sys.stderr)
        send({"option": "reset_connection"})  # new random key
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

if __name__ == "__main__":
    main()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍