> For the complete documentation index, see [llms.txt](https://www.impacket.wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://www.impacket.wiki/how-to-guides/epa-detection.md).

# Detecting Extended Protection For Authentication on HTTP and MSSQL

You need to determine whether Extended Protection for Authentication (EPA) is enforced on MSSQL or HTTP/HTTPS services before attempting an NTLM relay, so you don't waste time setting up a relay that will fail.

This guide implements the detection methodology from [RelayInformer](https://github.com/zyn3rgy/RelayInformer) by Nick Powers and Matt Creel (SpecterOps), translated into standalone Impacket code blocks.

## What you need

* Valid domain credentials (any domain user works for MSSQL, credentials that produce an HTTP 200 for HTTP/HTTPS targets)
* Network access to the target on port 1433 (MSSQL), 80 (HTTP), or 443 (HTTPS)

***

## Background: what EPA checks

EPA uses two AV pairs in the NTLM Type 3 authentication message to bind the authentication to the intended channel and service:

* **MsvAvChannelBindings**: a hash derived from the TLS session (channel binding token, or CBT). Present on encrypted connections.
* **MsvAvTargetName**: the SPN of the target service (service binding). Present on both encrypted and unencrypted connections.

The detection strategy is for us authenticate with valid credentials while manipulating these AV pairs (sending invalid values, omitting them). Different EPA enforcement levels produce different server responses, letting you distinguish between Disabled, Allowed, and Required.

***

## Compute a Channel Binding Token from a TLS certificate

Both MSSQL and HTTPS detection need the ability to compute a valid CBT from the server's TLS certificate. This is the `tls-server-end-point` binding type used by IIS, and the `tls-unique` type used by MSSQL:

### tls-server-end-point CBT (for HTTP/HTTPS)

```python
import ssl
import socket
import hashlib
from struct import pack

def get_server_certificate(host, port=443):
    """Connect via TLS and retrieve the server's DER-encoded certificate."""
    context = ssl.create_default_context()
    context.check_hostname = False
    context.verify_mode = ssl.CERT_NONE

    with socket.create_connection((host, port)) as sock:
        with context.wrap_socket(sock, server_hostname=host) as tls_sock:
            cert_der = tls_sock.getpeercert(binary_form=True)
    return cert_der


def compute_cbt_tls_server_endpoint(cert_der):
    """Compute the Channel Binding Token using tls-server-end-point."""
    from cryptography import x509
    from cryptography.hazmat.primitives import hashes

    cert = x509.load_der_x509_certificate(cert_der)
    sig_algo = cert.signature_hash_algorithm

    # Per RFC 5929: if the cert's signature uses MD5 or SHA-1, use SHA-256 instead
    if isinstance(sig_algo, (hashes.MD5, hashes.SHA1)):
        hash_algo = hashlib.sha256
    else:
        hash_algo = hashlib.new(sig_algo.name)
        hash_algo = lambda data=None: hashlib.new(sig_algo.name, data) if data else hashlib.new(sig_algo.name)

    cert_hash = hashlib.new(
        'sha256' if isinstance(sig_algo, (hashes.MD5, hashes.SHA1)) else sig_algo.name,
        cert_der
    ).digest()

    # Build the gss_api channel binding structure
    # MD5 hash of: application_data = "tls-server-end-point:" + cert_hash
    application_data = b'tls-server-end-point:' + cert_hash

    # The channel binding struct: 4 DWORDs (initiator, acceptor, application lengths) + data
    cb_struct = pack('<IIIII', 0, 0, 0, 0, len(application_data)) + application_data
    cb_hash = hashlib.md5(cb_struct).digest()

    return cb_hash
```

### tls-unique CBT (for MSSQL)

MSSQL uses `tls-unique` binding, which is based on the TLS Finished message, not the certificate:

```python
def get_tls_unique(host, port=1433):
    """Establish a TLS connection and extract the tls-unique binding value."""
    context = ssl.create_default_context()
    context.check_hostname = False
    context.verify_mode = ssl.CERT_NONE

    # For MSSQL, TLS wraps the existing connection after the pre-login exchange
    # The tls-unique value comes from ssl.SSLSocket.get_channel_binding('tls-unique')
    with socket.create_connection((host, port)) as sock:
        # Send TDS pre-login, negotiate encryption...
        # After TLS handshake:
        with context.wrap_socket(sock, server_hostname=host) as tls_sock:
            tls_unique = tls_sock.get_channel_binding('tls-unique')
    return tls_unique
```

***

## Detect EPA on MSSQL

The detection logic sends three authentication attempts with valid credentials, varying the channel binding and service binding AV pairs:

### Step 1: Determine if encryption is required

```python
from impacket import tds
import socket
import struct

def check_mssql_encryption(host, port=1433):
    """Send a TDS pre-login packet and check the encryption response."""
    sock = socket.create_connection((host, port))

    # Build TDS pre-login packet
    prelogin_data = b''
    # VERSION token
    prelogin_data += struct.pack('!BHH', 0x00, 6, 6)  # token, offset, length
    # ENCRYPTION token
    prelogin_data += struct.pack('!BHH', 0x01, 12, 1)  # token, offset, length
    # Terminator
    prelogin_data += b'\xff'
    # VERSION data (any version)
    prelogin_data += struct.pack('!IBBH', 0x0e000000, 0, 0, 0)[:6]
    # ENCRYPTION data: 0x00 = off, 0x01 = on, 0x02 = not supported, 0x03 = required
    prelogin_data += b'\x01'  # request encryption

    # TDS header
    tds_header = struct.pack('!BBHI', 0x12, 0x01, 8 + len(prelogin_data), 0)
    sock.send(tds_header + prelogin_data)

    # Read response
    response = sock.recv(4096)
    sock.close()

    # Parse the encryption byte from the response
    # The encryption option is in the pre-login response tokens
    # Return True if server requires or accepts encryption
    return response  # parse as needed
```

### Step 2: Authenticate with manipulated AV pairs

The core detection uses Impacket's TDS client with modified NTLM authentication. For each test case, authenticate with valid credentials but alter the NTLM Type 3 message:

```python
from impacket import tds, ntlm
from impacket.spnego import SPNEGO_NegTokenInit, SPNEGO_NegTokenResp

def test_mssql_epa(host, port, username, password, domain, test_type):
    """
    Authenticate to MSSQL with manipulated EPA AV pairs.

    test_type:
      'valid'          - valid CBT/SPN (baseline, should succeed)
      'invalid_cbt'    - invalid channel binding hash
      'no_cbt'         - omit MsvAvChannelBindings entirely
      'invalid_spn'    - wrong service binding SPN
      'no_spn'         - omit MsvAvTargetName entirely
    """
    client = tds.MSSQL(host, port)
    client.connect()

    # The TDS pre-login and NTLM exchange happen inside client.login()
    # To manipulate AV pairs, you need to intercept the NTLM message construction

    # Using Impacket's ntlm module directly:
    negotiate = ntlm.getNTLMSSPType1()

    # Send negotiate, receive challenge (Type 2)
    # The TDS login flow wraps NTLM in SSPI tokens
    # Parse the Type 2 to get the server challenge and target info

    # Modify the target info AV pairs before computing the Type 3:
    av_pairs = ntlm.AV_PAIRS(challenge_message['TargetInfo'])

    if test_type == 'invalid_cbt':
        av_pairs[ntlm.NTLMSSP_AV_CHANNEL_BINDINGS] = b'\x00' * 16  # invalid hash
    elif test_type == 'no_cbt':
        if ntlm.NTLMSSP_AV_CHANNEL_BINDINGS in av_pairs.fields:
            del av_pairs.fields[ntlm.NTLMSSP_AV_CHANNEL_BINDINGS]
    elif test_type == 'invalid_spn':
        av_pairs[ntlm.NTLMSSP_AV_TARGET_NAME] = 'INVALID/SPN'.encode('utf-16le')
    elif test_type == 'no_spn':
        if ntlm.NTLMSSP_AV_TARGET_NAME in av_pairs.fields:
            del av_pairs.fields[ntlm.NTLMSSP_AV_TARGET_NAME]

    # Compute Type 3 with the modified AV pairs
    # Send Type 3, check if authentication succeeds or fails

    client.disconnect()
```

### Step 3: Determine enforcement level from results

```python
def detect_mssql_epa(host, port, username, password, domain):
    """
    Determine EPA enforcement on an MSSQL instance.

    Returns: 'disabled', 'allowed', or 'required'
    """
    # First, validate credentials work with proper bindings
    baseline = test_mssql_epa(host, port, username, password, domain, 'valid')
    if not baseline:
        print("[!] Baseline authentication failed, check credentials")
        return None

    # Check encryption requirement to determine which binding to test
    encryption_required = check_mssql_encryption(host, port)

    if encryption_required:
        # Test channel binding manipulation
        invalid_cbt = test_mssql_epa(host, port, username, password, domain, 'invalid_cbt')
        no_cbt = test_mssql_epa(host, port, username, password, domain, 'no_cbt')

        if invalid_cbt and no_cbt:
            return 'disabled'
        elif not invalid_cbt and no_cbt:
            return 'allowed'   # accepts when client doesn't support, rejects bad values
        elif not invalid_cbt and not no_cbt:
            return 'required'
    else:
        # Test service binding manipulation
        invalid_spn = test_mssql_epa(host, port, username, password, domain, 'invalid_spn')
        no_spn = test_mssql_epa(host, port, username, password, domain, 'no_spn')

        if invalid_spn and no_spn:
            return 'disabled'
        elif not invalid_spn and no_spn:
            return 'allowed'
        elif not invalid_spn and not no_spn:
            return 'required'
```

***

## Detect EPA on HTTP/HTTPS

### Step 1: Find NTLM-enabled endpoints

Before testing EPA, confirm the target accepts NTLM authentication:

```python
import requests

def find_ntlm_endpoint(host, port=443, use_tls=True):
    """Check if the target HTTP service accepts NTLM authentication."""
    scheme = 'https' if use_tls else 'http'
    url = f"{scheme}://{host}:{port}/"

    # Common paths that accept NTLM
    paths = [
        '/certsrv/',           # ADCS web enrollment
        '/certsrv/certfnsh.asp',
        '/ews/',               # Exchange Web Services
        '/autodiscover/',
        '/rpc/rpcproxy.dll',
        '/',
    ]

    for path in paths:
        try:
            resp = requests.get(f"{scheme}://{host}:{port}{path}",
                               verify=False, allow_redirects=False)
            www_auth = resp.headers.get('WWW-Authenticate', '')
            if 'NTLM' in www_auth or 'Negotiate' in www_auth:
                return path
        except Exception:
            continue
    return None
```

### Step 2: Authenticate with manipulated AV pairs

For HTTP/HTTPS, NTLM authentication happens through the `WWW-Authenticate` / `Authorization` headers:

```python
import base64
from impacket import ntlm

def http_ntlm_auth(host, port, path, username, password, domain,
                    use_tls=True, modify_cbt=None, modify_spn=None):
    """
    Perform NTLM authentication to an HTTP endpoint with AV pair manipulation.

    modify_cbt: None (valid), 'invalid' (wrong hash), 'omit' (remove)
    modify_spn: None (valid), 'invalid' (wrong SPN), 'omit' (remove)

    Returns: HTTP status code
    """
    import ssl
    import http.client

    if use_tls:
        context = ssl.create_default_context()
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE
        conn = http.client.HTTPSConnection(host, port, context=context)
    else:
        conn = http.client.HTTPConnection(host, port)

    # Step 1: Send Type 1 (Negotiate)
    negotiate = ntlm.getNTLMSSPType1()
    negotiate_b64 = base64.b64encode(negotiate).decode()
    conn.request('GET', path, headers={'Authorization': f'NTLM {negotiate_b64}'})
    resp = conn.getresponse()
    resp.read()

    # Step 2: Parse Type 2 (Challenge) from WWW-Authenticate header
    auth_header = resp.getheader('WWW-Authenticate')
    challenge_b64 = auth_header.split('NTLM ')[1].strip()
    challenge_bytes = base64.b64decode(challenge_b64)
    challenge = ntlm.NTLMAuthChallenge(challenge_bytes)

    # Step 3: Modify AV pairs
    av_pairs = ntlm.AV_PAIRS(challenge['TargetInfoFields'])

    if modify_cbt == 'invalid':
        av_pairs[ntlm.NTLMSSP_AV_CHANNEL_BINDINGS] = b'\xde\xad' * 8
    elif modify_cbt == 'omit':
        if ntlm.NTLMSSP_AV_CHANNEL_BINDINGS in av_pairs.fields:
            del av_pairs.fields[ntlm.NTLMSSP_AV_CHANNEL_BINDINGS]

    if modify_spn == 'invalid':
        av_pairs[ntlm.NTLMSSP_AV_TARGET_NAME] = 'HTTP/INVALID.FAKE'.encode('utf-16le')
    elif modify_spn == 'omit':
        if ntlm.NTLMSSP_AV_TARGET_NAME in av_pairs.fields:
            del av_pairs.fields[ntlm.NTLMSSP_AV_TARGET_NAME]

    # Step 4: Compute Type 3 (Authenticate) with modified AV pairs
    # Build the NTLM Type 3 using the modified target info
    type3, _ = ntlm.getNTLMSSPType3(
        challenge, username, password, domain,
        # Pass modified AV pairs into the computation
    )

    type3_b64 = base64.b64encode(type3).decode()
    conn.request('GET', path, headers={'Authorization': f'NTLM {type3_b64}'})
    resp = conn.getresponse()
    status = resp.status
    resp.read()
    conn.close()

    return status
```

### Step 3: Determine enforcement level

```python
def detect_http_epa(host, port, path, username, password, domain, use_tls=True):
    """
    Determine EPA enforcement on an HTTP/HTTPS endpoint.

    Returns dict with channel_binding and service_binding enforcement levels.
    """
    result = {'channel_binding': 'N/A', 'service_binding': 'N/A'}

    # Baseline: valid authentication
    baseline = http_ntlm_auth(host, port, path, username, password, domain,
                              use_tls=use_tls)
    if baseline != 200:
        print(f"[!] Baseline failed with status {baseline}, check credentials")
        return None

    if use_tls:
        # Test channel binding
        invalid_cbt = http_ntlm_auth(host, port, path, username, password, domain,
                                     use_tls=True, modify_cbt='invalid')
        no_cbt = http_ntlm_auth(host, port, path, username, password, domain,
                                use_tls=True, modify_cbt='omit')

        if invalid_cbt == 200 and no_cbt == 200:
            result['channel_binding'] = 'disabled'
        elif invalid_cbt != 200 and no_cbt == 200:
            result['channel_binding'] = 'allowed'
        elif invalid_cbt != 200 and no_cbt != 200:
            result['channel_binding'] = 'required'

    # Test service binding (works for both HTTP and HTTPS)
    invalid_spn = http_ntlm_auth(host, port, path, username, password, domain,
                                 use_tls=use_tls, modify_spn='invalid')
    no_spn = http_ntlm_auth(host, port, path, username, password, domain,
                            use_tls=use_tls, modify_spn='omit')

    if invalid_spn == 200 and no_spn == 200:
        result['service_binding'] = 'disabled'
    elif invalid_spn != 200 and no_spn == 200:
        result['service_binding'] = 'allowed'
    elif invalid_spn != 200 and no_spn != 200:
        result['service_binding'] = 'required'

    return result
```

***

## IIS-specific considerations

IIS has a critical nuance: when EPA is enabled through the IIS Manager GUI, it only applies channel binding to HTTPS. A plain HTTP binding on the same site remains unprotected. This means:

```python
# Always check both HTTP and HTTPS if the target serves both
http_result = detect_http_epa(host, 80, path, username, password, domain, use_tls=False)
https_result = detect_http_epa(host, 443, path, username, password, domain, use_tls=True)

if http_result and http_result['service_binding'] == 'disabled':
    print(f"[+] HTTP on {host}:80 has no EPA, relay is possible to HTTP")

if https_result and https_result['channel_binding'] == 'disabled':
    print(f"[+] HTTPS on {host}:443 has no channel binding, relay is possible to HTTPS")
```

Protection on the HTTP binding requires manually editing `applicationHost.config` to set service binding flags. Most administrators don't do this, meaning plain HTTP ADCS web enrollment is almost always relayable even when "EPA is enabled" in the IIS GUI.

***

## Interpreting results for relay decisions

* **Disabled**: relay will succeed. No EPA validation on the server side.
* **Allowed/When Supported**: relay will likely fail for standard coercion scenarios (PrinterBug, PetitPotam, Responder) because Windows SSPI includes EPA AV pairs by default. However, NTLMv1 authentication or clients that don't support EPA will still succeed.
* **Required**: relay will fail. The server validates the binding values and rejects authentication without correct values.

The practical upshot: if the result is anything other than "disabled," your standard relay attack path is blocked for that target. Move on to other targets or other attack paths.

***

## OPSEC notes

These checks generate failed logon events (Event ID 4625) for the test cases with invalid bindings. The valid baseline authentication generates a successful logon event. The failed events do not increment the account's `badPwdCount` since the password is correct; the failure is due to binding validation.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://www.impacket.wiki/how-to-guides/epa-detection.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
