CVE-2024-43639: Microsoft Windows KDC代理远程代码执行漏洞
2025年3月4日 | Trend Micro研究团队
本文节选自趋势科技漏洞研究服务的漏洞报告,由趋势科技研究团队的Simon Humbert和Guy Lederfein详细分析了Microsoft Windows密钥分发中心(KDC)代理中一个已修补的代码执行漏洞。该漏洞最初由Kunlun Lab的k0shl和Wei与Cyber KunLun共同发现。成功利用可能导致在目标服务的安全上下文中执行任意代码。以下是他们关于CVE-2024-43639的报告部分,略有修改。
漏洞概述
Microsoft Windows KDC代理存在整数溢出漏洞。该漏洞是由于缺少对Kerberos响应长度的检查。远程未经身份验证的攻击者可引导KDC代理将Kerberos请求转发到其控制的服务器,服务器随后发回特制的Kerberos响应。成功利用可能导致在目标服务的安全上下文中执行任意代码。
技术背景
Windows认证协议与Kerberos
Microsoft Windows操作系统实现了一套默认认证协议,包括Kerberos、NTLM、传输层安全/安全套接字层(TLS/SSL)和摘要协议,作为可扩展架构的一部分。对于Active Directory域内的认证,Windows使用Kerberos。
Kerberos是一种计算机网络认证协议,基于票据机制,允许节点在非安全网络上以安全方式相互证明身份。Kerberos基于对称密钥加密,需要可信第三方——密钥分发中心(KDC),与认证领域中的所有其他方共享密钥。客户端和服务与KDC交换Kerberos消息。Kerberos消息可通过UDP或TCP端口88传输。当通过TCP发送时,每个请求和响应前都有4个八位组网络字节序的消息长度。
KDC代理工作机制
Microsoft Windows Server操作系统实现Kerberos版本5认证协议。每个Active Directory域控制器运行Kerberos KDC实例,使用域的目录服务数据库作为安全账户数据库。要认证,客户端必须与域控制器有网络连接。虽然这对于组织网络内的机器通常成立,但对于使用远程连接的客户端可能不成立。为支持远程工作负载(特别是RDP网关和DirectAccess等服务),可通过KDC代理使用HTTPS代理Kerberos流量。
KDC代理是基于HTTP的服务器,实现Kerberos KDC代理协议(KKDCP)。客户端将Kerberos请求包装在KDC代理消息中,并将其放在HTTPS POST请求的正文中发送,其中Request-URI设置为/KdcProxy。KDC代理消息使用抽象语法记法一(ASN.1)定义。ASN.1是定义数据结构的标准接口描述语言(IDL),可跨平台序列化和反序列化。ASN.1的完整规范包括词法单元、分隔符、递归定义、本地数据类型、空白、产生式规则等可在此处找到。
KDC代理消息的结构如下:
|
|
其中:
kerb-message
是Kerberos消息,包括4八位组消息长度前缀。target-domain
是DNS或NetBIOS域名,表示Kerberos消息必须发送到的领域(KDC代理请求需要target-domain,但KDC代理响应不使用)。dclocator-hint
是可选字段,包含用于查找域控制器的附加数据。
KDC代理消息使用可辨别编码规则(DER)编码。DER是一种类型-长度-值编码系统,每个DER编码字段具有以下结构:
|
|
标识符八位组编码内容八位组的类型。通常,它由单个八位组组成,结构如下:
|
|
类字段可以是通用(位:00)、应用特定(位:01)、上下文特定(位:10)或私有(位:11)。P/C字段指定字段是原始数据类型(位:0)(如INTEGER)还是构造数据类型(位:1)(即内容八位组包含其他原始或构造数据类型)。如果类字段是通用,则规范定义了几个标准标签号,如BOOLEAN(\x1)、INTEGER(\x2)、OCTET STRING(\x4)、UTF8STRING(\x0C)、SEQUENCE(\x10)、IA5STRING(\x16)、GeneralString(\x1B)等。对于非通用类,有编码大于30的标签号的规则。
在DER中,有两种编码长度八位组的方法。在短形式中,使用单个长度八位组,最高位设置为0,剩余7位表示内容八位组的数量。在长形式中,第一个长度八位组的最高位设置为1,剩余7位编码后续长度八位组的数量,这些八位组本身包含内容八位组的数量。长形式通常仅在必要时使用。所有多字节整数都是大端格式。
例如,编码后的KDC代理请求如下所示:
|
|
缩进显示了构造数据类型和原始数据类型之间的关系。请注意,SEQUENCE的标签号是\x10,但SEQUENCE字段是构造数据类型,P/C字段设置为1。因此,SEQUENCE的标识符八位组是0x30。SEQUENCE项分配了从0到2的显式标签,编码时封装在EXPLICIT标签数据类型中。在EXPLICIT标签的标识符八位组中,类字段设置为上下文特定(位:10),P/C字段设置为1。最后,Kerberos领域编码为KerberosString,它是GeneralString的别名。
接收KDC代理请求后,KDC代理提取target-domain并定位该领域的域控制器。首先,KDC代理查询DNS SRV记录,名称为_kerberos._tcp.Default-First-Site-Name._sites.dc._msdcs.<target_domain>,并在需要时解析匹配的A记录。然后,KDC代理向结果IP地址集发送LDAP ping。LDAP ping是无连接LDAP(CLDAP)rootDSE搜索,用于Netlogon属性,验证域控制器的活跃性,并检查是否满足特定要求。域控制器返回小端字节字符串,编码NETLOGON_SAM_LOGON_RESPONSE_EX结构。
最后,KDC代理从KDC代理请求中提取kerb-message并将其转发到域控制器。请注意,KDC代理仅通过TCP转发Kerberos请求。另请注意,虽然只能在域加入的机器上运行,但KDC代理会代理任意域的Kerberos请求。当KDC代理从域控制器接收Kerberos响应时,将其包装在KDC代理消息中(仅包含kerb-message字段),并在HTTPS 200 OK响应的正文中返回给客户端。
漏洞详情
漏洞根源
Microsoft Windows KDC代理报告了整数溢出漏洞。该漏洞是由于缺少对Kerberos响应长度的检查。
将Kerberos请求发送到域控制器后,KDC代理从网络套接字读取4字节以获取Kerberos响应长度。然后,尝试读取所需字节以获取完整响应。多个函数参与读取Kerberos响应,所有函数都传递指向_KPS_IO结构的指针作为参数。_KPS_IO结构的大小为0x120字节,部分定义如下(本节所有结构定义通过逆向工程确定;大多数结构和字段名称由我们选择):
|
|
每当从套接字读取后续字节时,调用DLL文件kpssvc.dll中的函数KpsSocketRecvDataIoCompletion()。它检查是否已读取足够字节以获取完整响应,如果是,则调用函数KpsPackProxyResponse(),传递指向_KPS_IO结构的指针作为参数。
KpsPackProxyResponse()首先调用函数KpsCheckKerbResponse()验证Kerberos响应。值得注意的是,如果紧接消息长度前缀后的字节设置为0x7E
或0x6B
,KpsCheckKerbResponse()验证响应是否正确构造的Kerberos消息。如果不是这种情况,它不执行任何验证并无错误返回。
KpsPackProxyResponse()局部变量包括类型为ASN1_KDC_PROXY_MSG的结构。ASN1_KDC_PROXY_MSG结构的大小为0x28
字节,部分定义如下:
|
|
调用KpsCheckKerbResponse()后,KpsPackProxyResponse()初始化结构如下:ASN1_KDC_PROXY_MSG.buf设置为_KPS_IO.recvbuf,ASN1_KDC_PROXY_MSG.len设置为_KPS_IO.bytesread。然后,为将Kerberos响应包装在KDC代理响应中,它调用函数KpsDerPack(),传递ASN1_KDC_PROXY_MSG结构的地址作为参数。
从此开始,代码流在实现KDC代理服务器的DLL文件kpssvc.dll中的函数和Microsoft ASN.1库msasn1.dll中的函数之间交替。后者随后称为"MSASN.1"函数。
KpsDerPack()调用MSASN.1函数ASN1_CreateEncoder(),分配类型为ASN1_encoder的结构。ASN1_encoder结构的大小为0x50
字节,部分定义如下:
|
|
KpsDerPack()然后调用MSASN.1函数ASN1_Encode(),传递指向ASN1_encoder和ASN1_KDC_PROXY_MSG结构的指针作为参数。ASN1_Encode()调用函数ASN1Enc_KDC_PROXY_MESSAGE()。ASN1Enc_KDC_PROXY_MESSAGE()调用MSASN.1函数ASN1BEREncExplicitTag(),传递指向ASN1_encoder结构的指针作为参数。ASN1BEREncExplicitTag()被调用两次,以编码SEQUENCE和EXPLICIT字段。
编码数据附加到ASN1_encoder.buf,缓冲区在编码字段时分配然后重新分配。为此,MSASN.1函数调用ASN1EncCheck(),传递所需大小作为参数。对于初始分配,ASN1EncCheck()通过调用Windows API函数LocalAlloc()在堆中分配空间。初始分配的大小至少为1,024字节。在后续调用期间,如果缓冲区无法容纳所需大小,ASN1EncCheck()重新分配缓冲区。在这种情况下,它将缓冲区的当前大小和所需大小相加,然后将结果作为参数传递给Windows API函数LocalReAlloc()。
ASN1BEREncExplicitTag()调用MSASN.1函数ASN1BEREncTag()。ASN1BEREncTag()编码标识符八位组,首先调用ASN1EncCheck()确保ASN1_encoder.buf有足够空间,然后在地址ASN1_encoder.current写入标识符八位组,最后递增ASN1_encoder.current。在此阶段,构造字段的长度未知,因为它取决于尚未编码的其他构造和原始字段的长度。因此,ASN1BEREncExplicitTag()通过调用ASN1EncCheck()并传递大小1,在ASN1_encoder.buf中为长度八位组保留一个字节,并将ASN1_encoder.current递增1。
ASN1Enc_KDC_PROXY_MESSAGE()然后调用MSASN.1函数ASN1DEREncOctetString()编码kerb-message OCTET STRING字段,传递指向ASN1_encoder结构的指针以及ASN1_KDC_PROXY_MSG.buf和ASN1_KDC_PROXY_MSG.len作为参数。ASN1DEREncOctetString()是函数ASN1BEREncCharString()的别名。ASN1BEREncCharString()首先调用ASN1BEREncTag()编码标识符八位组,然后调用ASN1BEREncLength(),传递ASN1_KDC_PROXY_MSG.len作为参数。
ASN1BEREncLength()首先计算编码长度八位组所需的字节数,添加ASN1_KDC_PROXY_MSG.len,然后将结果值作为参数传递给ASN1EncCheck()。这确保ASN1_encoder.buf有足够空间用于长度八位组和内容八位组。ASN1BEREncLength()然后在地址ASN1_encoder.current写入长度八位组,最后将ASN1_encoder.current递增长度八位组的大小。最后,ASN1BEREncCharString()调用Windows API函数memcpy()将ASN1_KDC_PROXY_MSG.len从地址ASN1_KDC_PROXY_MSG.buf复制到地址ASN1_encoder.current。
漏洞触发机制
然而,MSASN.1函数并不总是正确处理意外输入,特别是在处理大长度值时不会检查可能的整数溢出。此外,KpsSocketRecvDataIoCompletion()在调用KpsPackProxyResponse()之前不检查Kerberos响应的长度。最后,通过将紧接消息长度前缀后的字节设置为0x7E或0x6B以外的任何值,可以绕过KpsCheckKerbResponse()中的Kerberos响应验证。因此,恶意域控制器可能发送大的Kerberos响应,导致内存破坏错误。
整数溢出和内存破坏错误在编码kerb-message OCTET STRING字段时发生。此时,SEQUENCE和EXPLICIT字段均已编码,ASN1_encoder.buf指向大小为1,024的缓冲区,ASN1_encoder.current指向地址ASN1_encoder.buf + 4。KDC代理接受的Kerberos响应的最大大小为4,294,967,295。
如果发送长度从4,294,967,291到4,294,967,295(含)的Kerberos响应,ASN1BEREncLength()将发现需要5字节编码长度八位组,然后添加Kerberos响应的长度。但是,加法结果存储在4字节无符号变量中,该变量溢出。因此,作为参数传递给ASN1EncCheck()的大小非常小。ASN1EncCheck()不重新分配ASN1_encoder.buf缓冲区,随后当ASN1BEREncCharString()调用memcpy()时,发生堆缓冲区溢出。
或者,当发送长度从4,294,966,267到4,294,967,290(含)的Kerberos响应时,ASN1BEREncLength()调用ASN1EncCheck()。由于当前ASN1_encoder.buf缓冲区太小,ASN1EncCheck()继续重新分配它。它将缓冲区的当前大小(1,024)与Kerberos响应的长度相加。但是,加法结果存储在4字节无符号变量中,该变量溢出。因此,LocalReAlloc()实际上减小了缓冲区的大小。随后当ASN1BEREncCharString()调用memcpy()时,发生越界写入或堆缓冲区溢出。
作为一个有趣的边缘情况,可能将0作为新大小传递给LocalReAlloc()。LocalReAlloc()返回内存地址而不是错误,但是内存实际上未分配,尝试写入该地址时发生访问冲突。
远程未经身份验证的攻击者可引导KDC代理将Kerberos请求转发到其控制的服务器,服务器随后发回特制的Kerberos响应。成功利用可能导致在目标服务的安全上下文中执行任意代码。
注意:要达到易受攻击的代码,仅发送短Kerberos响应且前四个字节中具有大消息长度前缀值是不够的。Kerberos响应长度必须实际匹配前缀值。
检测指导
要检测利用此漏洞的攻击,检测设备必须监视和解析UDP端口389和TCP端口88上的流量。Kerberos消息可通过UDP或TCP端口88传输。但是,当通过TCP发送时,每个请求和响应前都有4个八位组网络字节序的消息长度。
检测设备必须检查Kerberos响应。请注意,KDC代理仅使用TCP端口88进行Kerberos流量(非UDP)。因此,设备不需要完全解析Kerberos响应。它只需要解析4字节消息长度前缀并能够隔离TCP流中的响应。如果Kerberos响应为0x80000000(2,147,483,648)字节或更长,流量应视为可疑,可能正在进行利用此漏洞的攻击。
注意:上述检测指导基于Kerberos V5 RFC的7.2.2节。它提到,在4八位组消息长度前缀中,高位必须设置为0。因此,根据RFC,通过TCP传输的Kerberos消息的最大长度为0x7FFFFFFF。
补丁相关问题
我们的研究表明,漏洞存在于ASN.1库中,但是Microsoft公告提到KDC代理服务器。此外,通过