CVE-2024-43639:微软Windows KDC代理远程代码执行漏洞
发布日期:2025年3月4日 | 趋势科技研究团队
在趋势科技漏洞研究服务的漏洞报告摘录中,趋势科技研究团队的Simon Humbert和Guy Lederfein详细介绍了微软Windows密钥分发中心(KDC)代理中一个已修补的代码执行漏洞。该漏洞最初由k0shl和Wei在昆仑实验室与Cyber KunLun发现。成功利用可能导致在目标服务的安全上下文中执行任意代码。以下是他们关于CVE-2024-43639的报告部分,经过最小修改。
漏洞概述
微软Windows KDC代理报告了一个整数溢出漏洞。该漏洞是由于缺少对Kerberos响应长度的检查。远程未经身份验证的攻击者可以引导KDC代理将Kerberos请求转发到其控制的服务器,该服务器随后会发送一个特制的Kerberos响应。成功利用可能导致在目标服务的安全上下文中执行任意代码。
漏洞详情
微软Windows操作系统实现了一组默认的身份验证协议,包括Kerberos、NTLM、传输层安全/安全套接字层(TLS/SSL)和摘要,作为可扩展架构的一部分。对于Active Directory域内的身份验证,Windows使用Kerberos。
Kerberos是一种计算机网络身份验证协议,基于票据工作,允许在非安全网络上通信的节点以安全方式相互证明身份。Kerberos基于对称密钥密码学,需要一个可信第三方,即密钥分发中心(KDC),它与认证领域中的所有其他方共享密钥。客户端和服务与KDC交换Kerberos消息。Kerberos消息可以通过UDP或TCP端口88传输。然而,当通过TCP发送时,每个请求和响应前面都有4个八位组的消息长度(网络字节顺序)。
微软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编码字段具有以下结构:
- Identifier Octets:编码Contents Octets的类型。通常,它由一个单独的八位组组成,结构如下:
- Class字段:可以是Universal(位:00)、Application Specific(位:01)、Context-specific(位:10)或Private(位:11)。
- P/C字段:指定字段是原始数据类型(位:0)(如INTEGER)还是构造数据类型(位:1)(即其Content Octets包含其他原始或构造数据类型)。
- Tag Number:如果Class字段是Universal,则规范定义了几个标准标记号,如BOOLEAN(\x1)、INTEGER(\x2)、OCTET STRING(\x4)、UTF8STRING(\x0C)、SEQUENCE(\x10)、IA5STRING(\x16)、GeneralString(\x1B)等。对于非Universal类,有编码大于30的标记号的规则。
在DER中,有两种编码Length Octets的方法。在短形式中,使用单个Length Octet,最高位设置为0,剩余的7位表示Content Octets的数量。在长形式中,第一个Length Octet的最高位设置为1,剩余的7位编码后续Length Octets的数量,这些Length Octets本身包含Content Octets的数量。长形式通常仅在必要时使用。所有多字节整数都是大端格式。
例如,以下是编码后的KDC代理请求的样子:
(缩进显示了构造和原始数据类型之间的关系。请注意,SEQUENCE的标记号是\x10,但是SEQUENCE字段是一个构造数据类型,P/C字段设置为1。因此,SEQUENCE的Identifier Octet是0x30。SEQUENCE项被分配了从0到2的显式标记,编码时,它们被封装在EXPLICIT标记数据类型中。在EXPLICIT标记的Identifier Octet中,Class字段设置为Context-Specific(位: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响应的正文中返回给客户端。
微软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()编码Identifier Octets,首先调用ASN1EncCheck()以确保ASN1_encoder.buf有足够的空间,然后在地址ASN1_encoder.current处写入Identifier Octets,最后递增ASN1_encoder.current。在此阶段,构造字段的长度未知,因为它取决于尚未编码的其他构造和原始字段的长度。因此,ASN1BEREncExplicitTag()通过调用ASN1EncCheck()并传递大小1,在ASN1_encoder.buf中为Length Octets保留一个字节,并将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()来编码Identifier Octets,然后调用ASN1BEREncLength(),传递ASN1_KDC_PROXY_MSG.len作为参数。
ASN1BEREncLength()首先计算编码Length Octets所需的字节数,添加ASN1_KDC_PROXY_MSG.len,然后将结果值作为参数传递给ASN1EncCheck()。这确保ASN1_encoder.buf有足够的空间用于Length Octets和Contents Octets。ASN1BEREncLength()然后在地址ASN1_encoder.current处写入Length Octets,最后将ASN1_encoder.current递增Length Octets的大小。最后,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个字节来编码Length Octets,然后添加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库中,然而,微软公告提到了KDC代理服务器。此外,通过