# How Doom Manages LDAP Connections: Protocol Fallbacks and NTLM Authentication

> Discover how Doom manages LDAP connections using protocol fallbacks and NTLM authentication. Learn about its robust directory query methods.

- Repository: [000pp/doom](https://github.com/000pp/doom)
- Tags: internals
- Published: 2026-02-22

---

**Doom manages LDAP connections through a two-stage fallback strategy that attempts plain LDAP on port 389 before automatically escalating to LDAPS on port 636, handling NTLM authentication with automatic hash detection and providing safe attribute accessors for robust directory queries.**

Doom, an open-source Active Directory security tool, centralizes its LDAP handling in the `doom.protocols.ldap` module. The implementation features intelligent protocol negotiation and credential preprocessing to support both standard passwords and NTLM hashes. This architecture ensures reliable authentication across diverse domain controller configurations while maintaining session persistence throughout the UI lifecycle.

## Core LDAP Connection Architecture

### The get_ldap_connection Function

The primary entry point for LDAP connectivity resides in [`src/doom/protocols/ldap.py`](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py). The `get_ldap_connection` function accepts four parameters—`host`, `username`, `password`, and `domain`—and orchestrates the entire authentication flow. It constructs the NTLM user string using the `domain\username` format and prepares connection objects for both plain LDAP and LDAPS protocols before attempting sequential binds.

### NTLM Authentication and Hash Detection

Doom automatically detects raw NTLM hashes to support pass-the-hash scenarios. When the supplied password matches a 32-character hexadecimal pattern, the function prepends the well-known LM hash prefix `aad3b435b51404eeaad3b435b51404ee` before binding. This preprocessing allows the tool to authenticate using hash-only credentials against domain controllers without requiring plaintext passwords.

```python
def get_ldap_connection(host: str, username: str, password: str, domain: str):
    """Try LDAP on 389, then LDAPS on 636."""
    user = f"{domain}\\{username}"

    # Detect raw NTLM hash and prepend the known prefix

    if len(password) == 32 and all(c in "0123456789abcdefABCDEF" for c in password):
        password = f"aad3b435b51404eeaad3b435b51404ee:{password}"

    tls = ldap3.Tls(validate=ssl.CERT_NONE,
                    version=ssl.PROTOCOL_TLSv1_2,
                    ciphers="ALL:@SECLEVEL=0")

    ldaps_server = ldap3.Server(f"ldaps://{host}", port=636,
                                use_ssl=True, get_info=ldap3.ALL, tls=tls)
    ldap_server = ldap3.Server(f"ldap://{host}", port=389,
                               use_ssl=False, get_info=ldap3.ALL)

    # 1️⃣ Try plain LDAP first

    try:
        conn = ldap3.Connection(
            server=ldap_server,
            user=user,
            password=password,
            authentication=ldap3.NTLM,
            auto_bind=True,
            session_security="ENCRYPT",
            auto_referrals=False,
            raise_exceptions=True,
        )
        base_dn = conn.server.info.naming_contexts[0]
        return conn, base_dn
    except LDAPBindError as e:
        if "strongerAuthRequired" not in str(e):
            raise Exception(f"LDAP bind error: {e}")

    # 2️⃣ Fallback to LDAPS

    try:
        conn = ldap3.Connection(
            server=ldaps_server,
            user=user,
            password=password,
            authentication=ldap3.NTLM,
            auto_bind=True,
            auto_referrals=False,
            raise_exceptions=True,
        )
        base_dn = conn.server.info.other.get("defaultNamingContext", ["<none>"])[0]
        return conn, base_dn
    except Exception as e:
        raise Exception(f"LDAPS bind failed: {e}")

```

## Two-Stage Protocol Fallback Strategy

### Plain LDAP Attempt on Port 389

Doom initially attempts to establish connectivity using unencrypted LDAP on port 389. This connection utilizes `session_security="ENCRYPT"` to negotiate encryption after the bind. Upon successful authentication, the function extracts the base DN from `conn.server.info.naming_contexts[0]`, which represents the root naming context of the directory.

### Automatic LDAPS Escalation on Port 636

If the initial bind fails with a `strongerAuthRequired` error—or any other exception—the implementation immediately falls back to LDAPS on port 636. The LDAPS configuration explicitly disables certificate validation using `ssl.CERT_NONE` and forces TLS 1.2 with permissive cipher suites (`ciphers="ALL:@SECLEVEL=0"`). This configuration ensures compatibility with domain controllers using self-signed or enterprise certificates without manual trust store management. For LDAPS connections, the base DN retrieves from `conn.server.info.other.get("defaultNamingContext", ["<none>"])[0]`.

## Safe Attribute Access with safe_ldap_attr

To prevent crashes when processing LDAP entries with missing or optional attributes, [`src/doom/protocols/ldap.py`](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py) provides the `safe_ldap_attr` helper. This function wraps attribute access with exception handling for `LDAPCursorAttributeError` and `AttributeError`, returning a configurable fallback value when fields are absent.

```python
def safe_ldap_attr(entry, attr_name, fallback=None) -> None:
    """Return a LDAP attribute value or a fallback to avoid exceptions."""
    try:
        attr = getattr(entry, attr_name, None)
        return attr.value if attr else fallback
    except (AttributeError, LDAPCursorAttributeError):
        return fallback

```

Downstream modules such as [`src/doom/modules/enumerate_templates.py`](https://github.com/000pp/doom/blob/main/src/doom/modules/enumerate_templates.py) utilize this helper exclusively when extracting certificate template attributes, ensuring robust enumeration even against non-standard directory schemas.

## UI Integration and Session Management

### Asynchronous Connection in LoadingScreen

The UI layer handles LDAP authentication in [`src/doom/screens/loading_screen.py`](https://github.com/000pp/doom/blob/main/src/doom/screens/loading_screen.py) to prevent blocking the interface. The `LoadingScreen` gathers credentials from form inputs and executes `get_ldap_connection` within an async thread using `asyncio.to_thread`. Upon success, both the connection object and base DN are stored as instance variables for transition to the main interface.

```python
async def authenticate_ldap(self) -> None:
    status_label = self.query_one("#status-label")
    try:
        status_label.update("Attempting LDAP connection...")
        connection_result = await asyncio.to_thread(
            get_ldap_connection,
            host=self.login_data.get('ip', ''),
            username=self.login_data.get('username', ''),
            password=self.login_data.get('password', ''),
            domain=self.login_data.get('domain', '')
        )
        if connection_result:
            self.ldap_connection, self.base_dn = connection_result
            status_label.update("Authentication successful!")
            # Switch to the main UI screen...

    except Exception as e:
        status_label.update(f"Authentication error: {e}")

```

### Connection Persistence in MainScreen

`MainScreen` receives the active connection and base DN from the loading screen, maintaining these objects for the entire UI session. This persistence allows enumeration modules to execute multiple queries without re-authenticating. The connection remains bound until explicit cleanup occurs, optimizing performance for extended reconnaissance operations.

## LDAP Connection Cleanup

Both `LoadingScreen` and `MainScreen` implement explicit cleanup protocols by calling `self.ldap_connection.unbind()` during logout or application exit. This method gracefully terminates the TCP session and releases directory server resources, preventing stale connection accumulation during long-running engagements.

## Summary

- **Two-stage fallback**: Doom attempts LDAP on port 389 before automatically escalating to LDAPS on port 636 when authentication requires stronger security.
- **Certificate bypass**: The LDAPS configuration disables certificate validation and uses permissive TLS settings to support self-signed enterprise certificates.
- **Hash support**: Automatic detection of 32-character NTLM hashes with LM hash prefixing enables pass-the-hash authentication without plaintext credentials.
- **Safe access**: The `safe_ldap_attr` helper prevents crashes when querying optional LDAP attributes across diverse directory schemas.
- **Async architecture**: The UI executes LDAP operations in background threads to maintain responsiveness during authentication and enumeration.
- **Session reuse**: Active connections persist across UI screens, eliminating redundant authentication overhead during multi-phase operations.

## Frequently Asked Questions

### How does Doom handle self-signed certificates on LDAPS connections?

Doom explicitly disables certificate validation by configuring the `ldap3.Tls` object with `validate=ssl.CERT_NONE` and `ciphers="ALL:@SECLEVEL=0"` in [`src/doom/protocols/ldap.py`](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py). This allows the tool to establish LDAPS connections on port 636 against domain controllers using self-signed or internally-issued certificates without requiring manual trust store modifications.

### What authentication method does Doom use for LDAP binds?

Doom utilizes **NTLM authentication** exclusively for LDAP connections. The `get_ldap_connection` function constructs the user principal in `domain\username` format and passes credentials with `authentication=ldap3.NTLM` to the underlying `ldap3` library, supporting both plaintext passwords and NTLM hashes.

### How does Doom differentiate between plaintext passwords and NTLM hashes?

The connection logic checks if the password parameter is exactly 32 characters and contains only hexadecimal characters (0-9, a-f, A-F). When these criteria match, Doom prepends the LM hash prefix `aad3b435b51404eeaad3b435b51404ee:` to convert the raw NT hash into a format compatible with NTLM authentication.

### What happens if the LDAP connection fails on both ports?

If the initial LDAP attempt on port 389 fails with `strongerAuthRequired` or any other error, Doom automatically attempts LDAPS on port 636. Should both attempts fail, the `get_ldap_connection` function raises an exception containing the specific bind error message, which the UI layer catches and displays to the user without crashing the application.