--- name: exchange-owa-attack description: Exchange/OWA NTLM AD leak, spray attack when mail subdomain. version: 1.1.0 revision_date: 2026-07-25 license: MIT platforms: [linux] compatibility: Requires curl, nmap, python3, masscan, subfinder, httpx, nuclei tags: [recon, exchange, OWA, NTLM, ActiveDirectory, password-spray] category: recon related_skills: - port-service-discovery - zimbra-attack - subdomain-enumeration --- # Exchange/OWA Attack Skill Exchange Outlook Web Access reconnaissance covering endpoint mapping, NTLM Type-2 metadata, authentication controls, and version evidence. Password or lockout testing requires explicit authorization and approved identities. ## When to Use - Target has `owa.`, `mail.`, `webmail.`, `exchange.`, or `autodiscover.` subdomains. - crt.sh reveals Exchange-related SAN names (`mail.domain.com`, `autodiscover.domain.com`). - Port 443 returns NTLM `WWW-Authenticate: Negotiate` or `WWW-Authenticate: NTLM`. - After `subdomain-enumeration` discovers mail-related hosts. - After `port-service-discovery` finds HTTPS on port 443 with Exchange fingerprints. ## Prerequisites - `terminal` with curl, python3. - Target Exchange/OWA URL. - For password spray: list of usernames (from recon) and password candidates. ## How to Run ```bash # Quick Exchange detection curl --max-time 30 --connect-timeout 10 -skI "https://TARGET/owa/" | grep -iE "x-owa-version|x-feserver|exchange|microsoft" # NTLM challenge capture (AD domain leak) curl --max-time 30 --connect-timeout 10 -skI "https://TARGET/owa/" -H "Authorization: Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==" | grep -i "www-authenticate" ``` ## Quick Reference | Technique | What It Reveals | Severity | |-----------|---------------|---------| | NTLM Type-2 decode | AD domain, NetBIOS name, computer name, AD timestamp | High | | OWA version header | Exchange version, CU level, patch status | Medium | | `/owa/auth/logon.aspx` | Login page, brute force surface | Medium | | `/ecp/` | Exchange Control Panel (admin) | High | | `/ews/` | Exchange Web Services (SOAP API) | Medium | | `/autodiscover/` | Autodiscover configuration | Medium | | `/mapi/` | MAPI over HTTP | Low | | `/Microsoft-Server-ActiveSync` | Mobile device sync | Medium | | `/rpc/` | Outlook Anywhere (RPC over HTTP) | Low | ## Procedure ### Phase 1 — Exchange Detection & Fingerprinting ```bash TARGET="$1" OUTDIR="$OUTDIR/exchange" mkdir -p "$OUTDIR" echo "[*] Exchange detection on $TARGET" # OWA probe OWA_RESP=$(curl -skI --max-time 10 --connect-timeout 10 "https://$TARGET/owa/" 2>/dev/null) echo "$OWA_RESP" > "$OUTDIR/owa_headers.txt" # Version extraction X_OWA=$(echo "$OWA_RESP" | grep -i "x-owa-version" | sed 's/.*: //') X_FE=$(echo "$OWA_RESP" | grep -i "x-feserver" | sed 's/.*: //') if [[ -n "$X_OWA" ]]; then echo "[+] Exchange confirmed — OWA Version: $X_OWA" echo " Frontend server: ${X_FE:-unknown}" # Map version to CU # 15.1.x = Exchange 2016, 15.2.x = Exchange 2019 MAJOR=$(echo "$X_OWA" | cut -d. -f1-2) if [[ "$MAJOR" == "15.1" ]]; then echo " Product: Exchange 2016" elif [[ "$MAJOR" == "15.2" ]]; then echo " Product: Exchange 2019" fi else echo "[-] No OWA version header — may not be Exchange" fi # Key endpoints probe declare -A EX_ENDPOINTS EX_ENDPOINTS["/owa/auth/logon.aspx"]="Login page" EX_ENDPOINTS["/ecp/"]="Exchange Control Panel (admin)" EX_ENDPOINTS["/ews/exchange.asmx"]="Exchange Web Services (SOAP)" EX_ENDPOINTS["/autodiscover/autodiscover.xml"]="Autodiscover" EX_ENDPOINTS["/mapi/emsmdb/"]="MAPI over HTTP" EX_ENDPOINTS["/Microsoft-Server-ActiveSync/"]="ActiveSync" EX_ENDPOINTS["/rpc/rpcproxy.dll"]="Outlook Anywhere" EX_ENDPOINTS["/owa/healthcheck.htm"]="Health check" echo "" echo "[*] Endpoint probe:" for ep in "${!EX_ENDPOINTS[@]}"; do code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 --connect-timeout 5 "https://$TARGET$ep") [[ "$code" == "200" ]] && echo " [OPEN] $ep — ${EX_ENDPOINTS[$ep]}" [[ "$code" == "302" ]] && echo " [REDIR] $ep — ${EX_ENDPOINTS[$ep]}" [[ "$code" == "401" ]] && echo " [AUTH] $ep — ${EX_ENDPOINTS[$ep]}" done ``` ### Phase 2 — NTLM Type-2 Challenge Capture & Decode ```bash TARGET="$1" echo "[*] NTLM challenge capture from $TARGET" # Send NTLM Type-1 (Negotiate) message via Authorization header NTLM_RESP=$(curl -skI --max-time 10 --connect-timeout 10 "https://$TARGET/owa/" \ -H "Authorization: Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==" 2>/dev/null) WWW_AUTH=$(echo "$NTLM_RESP" | grep -i "www-authenticate: negotiate" | sed 's/.*negotiate //i' | tr -d '\r\n ') if [[ -n "$WWW_AUTH" ]]; then echo "[+] NTLM Type-2 challenge received!" echo " Raw: ${WWW_AUTH:0:80}..." # Decode with Python (extract AV_PAIRS structure) echo "$WWW_AUTH" | python3 -c " import base64, struct, sys data = base64.b64decode(sys.stdin.read().strip()) # NTLM Type-2 message structure: # Offset 12: Target Name # Offset 16: Negotiate Flags # Offset 20: Server Challenge # Offset 28: Reserved # Offset 32: Target Info (AV_PAIRS) # Parse Target Info if len(data) > 40: target_info_offset = struct.unpack_from('/dev/null) echo " Attempt $i: HTTP $code" done # Check if Basic Auth is enabled (rare post-2022, but exists) BASIC_AUTH=$(curl -skI --max-time 5 --connect-timeout 5 "https://$TARGET/owa/" \ -H "Authorization: Basic dGVzdDp0ZXN0" 2>/dev/null | grep -i "www-authenticate.*basic") if [[ -n "$BASIC_AUTH" ]]; then echo " [!] Basic Auth ENABLED — easier brute force vector" fi # Check healthcheck endpoint (sometimes exposes version/config) HEALTH=$(curl -sk --max-time 5 --connect-timeout 5 "https://$TARGET/owa/healthcheck.htm" 2>/dev/null) if [[ -n "$HEALTH" ]] && echo "$HEALTH" | grep -qi "200 ok"; then echo " [+] Healthcheck accessible — server status exposed" fi ``` ### Phase 4 — ADFS/Office 365 Recon (hybrid environments) ```bash TARGET_DOMAIN="$1" # e.g., company.com echo "[*] ADFS/Office 365 recon on $TARGET_DOMAIN" # Check for ADFS ADFS_URL="https://sts.$TARGET_DOMAIN/adfs/ls/IdpInitiatedSignOn.aspx" ADFS_CODE=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 --connect-timeout 5 "$ADFS_URL") [[ "$ADFS_CODE" == "200" || "$ADFS_CODE" == "302" ]] && echo " [+] ADFS: $ADFS_URL (HTTP $ADFS_CODE)" # Check Office 365 tenant O365_XML=$(curl -sk --max-time 5 --connect-timeout 5 "https://login.microsoftonline.com/getuserrealm.srf?login=user@$TARGET_DOMAIN&xml=1" 2>/dev/null) if echo "$O365_XML" | grep -qi "Federated\|Managed"; then echo " [+] Office 365 tenant: $(echo "$O365_XML" | grep -Eo '\K[^<]+')" echo " $(echo "$O365_XML" | grep -Eo '\K[^<]+')" fi # Autodiscover (leaks internal server names) AUTODISCOVER=$(curl -sk --max-time 10 --connect-timeout 10 "https://autodiscover.$TARGET_DOMAIN/autodiscover/autodiscover.xml" \ -H "Content-Type: text/xml" \ -d 'user@'$TARGET_DOMAIN'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' 2>/dev/null) if echo "$AUTODISCOVER" | grep -qi "server\|internal"; then echo " [+] Autodiscover response — internal server names leaked" echo "$AUTODISCOVER" | grep -Eo '(?:||)[^<]+' | head -5 fi ``` ## Pitfalls - **NTLM relay requires specific network position.** Unless you control a machine the Exchange server can reach, NTLM relay is not exploitable remotely. - **Modern Exchange (Exchange Online, 2019+) blocks Basic Auth by default.** Test with Modern Auth (OAuth2) if Basic is blocked. - **Account lockout policies vary.** Test with a single known-bad password before spraying. - **ADFS is NOT Exchange.** ADFS is a separate service with its own attack surface (SAML, WS-Trust). ## Verification - NTLM Type-2 MUST decode to reveal at minimum DNS Domain Name and NetBIOS Domain Name. - OWA version MUST be extracted from `X-OWA-Version` header. - Password spray surface: confirm NO rate limiting (5 rapid attempts all return the same HTTP code). - Autodiscover MUST return internal server names (not just external URLs). - Document: Exchange version, AD domain, NetBIOS name, computer names, rate limiting status. ## Related Skills - **`password-spray-methodology`** — Universal password spray pipeline across all protocols + error code differentials