mcpField
E
exchange-owa-attack

exchange-owa-attack

github.com/uphiagoセキュリティ1.3k42 回インストール8 週間前に更新
リポジトリ

exchange-owa-attack について

exchange-owa-attack は、Exchange と Outlook Web Access を調査するセキュリティテスト用スキルです。許可を得たテスター向けに、エンドポイントの特定、NTLM メタデータの取得、認証制御の確認、バージョン情報の収集を案内します。

主な機能

  1. 01Exchange と OWA のフィンガープリント調査
  2. 02NTLM Type-2 メタデータの取得
  3. 03認証制御の評価
  4. 04Exchange エンドポイントの特定
  5. 05バージョンとパッチ情報の収集

活用シーン

  1. 01発見したメール関連ホストの調査
  2. 02Exchange バージョンと AD メタデータの特定
  3. 03許可を得た認証制御の評価

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/uphiago/recon-skills --skill exchange-owa-attack

インストール後、Claude Code で/exchange-owa-attackを入力して読み込まれたか確認します。

詳細

Exchange/OWA 攻撃スキル

Exchange Outlook Web Access の偵察について、エンドポイントのマッピング、NTLM Type-2 メタデータ、認証制御、バージョンの証拠を調査します。パスワードテストやアカウントロックアウトテストには、明示的な許可と承認済みのアカウントが必要です。

使用するタイミング

  • ターゲットに owa., mail., webmail., exchange., autodiscover. のサブドメインがある。
  • crt.sh で Exchange 関連の SAN 名(mail.domain.com、autodiscover.domain.com)が見つかった。
  • ポート 443 が NTLM WWW-Authenticate: Negotiate または WWW-Authenticate: NTLM を返す。
  • subdomain-enumeration によってメール関連ホストが見つかった後。
  • port-service-discovery によって、Exchange のフィンガープリントを持つポート 443 の HTTPS が見つかった後。

前提条件

  • curl、python3 を備えた terminal。
  • ターゲットの Exchange/OWA URL。
  • パスワードスプレーを行う場合は、(偵察で得た)ユーザー名のリストとパスワード候補。

実行方法

# 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"

クイックリファレンス

手法判明する情報重大度
NTLM Type-2 のデコードAD ドメイン、NetBIOS 名、コンピューター名、AD タイムスタンプ高
OWA バージョンヘッダーExchange のバージョン、CU レベル、パッチ適用状況中
/owa/auth/logon.aspxログインページ、ブルートフォース攻撃対象中
/ecp/Exchange コントロールパネル(管理者用)高
/ews/Exchange Web Services(SOAP API)中
/autodiscover/Autodiscover の設定中
/mapi/HTTP 経由の MAPI低
/Microsoft-Server-ActiveSyncモバイルデバイスの同期中
/rpc/Outlook Anywhere(HTTP 経由の RPC)低

手順

フェーズ 1 — Exchange の検出とフィンガープリンティング
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
フェーズ 2 — NTLM Type-2 チャレンジの取得とデコード
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('<I', data, 40)[0]
    target_info_len = struct.unpack_from('<I', data, 44)[0]
    av_pairs = data[target_info_offset:target_info_offset + target_info_len]

    print()
    print('=== NTLM Type-2 Decoded ===')
    pos = 0
    while pos < len(av_pairs) - 4:
        av_type = struct.unpack_from('<H', av_pairs, pos)[0]
        av_len = struct.unpack_from('<H', av_pairs, pos + 2)[0]
        av_value = av_pairs[pos + 4:pos + 4 + av_len]

        # AV_PAIR types
        types = {
            1: 'NetBIOS Computer Name',
            2: 'NetBIOS Domain Name',
            3: 'DNS Computer Name',
            4: 'DNS Domain Name',
            5: 'DNS Tree Name',
            6: 'Product Version',
            7: 'Timestamp',
        }
        label = types.get(av_type, f'Unknown({av_type})')
        if av_type in (1, 2, 3, 4, 5):
            value = av_value.decode('utf-16-le', errors='replace')
            print(f'  {label}: {value}')
        elif av_type == 7:
            ts = struct.unpack_from('<Q', av_value)[0]
            from datetime import datetime, timezone
            dt = datetime.fromtimestamp(ts / 10000000 - 11644473600, tz=timezone.utc)
            print(f'  Timestamp: {dt}')
        else:
            print(f'  {label}: {av_value.hex()}')

        pos += 4 + av_len
else:
    print('  No AV_PAIRS in response')
"
fi
フェーズ 3 — パスワードスプレー対象の評価
TARGET="$1"

echo "[*] Password spray surface assessment"

# Check for account lockout by testing rapid logins with invalid password
echo "[*] Rate limiting test (5 rapid attempts with wrong password)..."
for i in $(seq 1 5); do
  code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 --connect-timeout 10 \
    -X POST "https://$TARGET/owa/auth.owa" \
    -d "destination=https://$TARGET/owa/&[email protected]&password=WrongPass123!" 2>/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
フェーズ 4 — ADFS/Office 365 の偵察(ハイブリッド環境)
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 '<NameSpaceType>\K[^<]+')"
  echo "  $(echo "$O365_XML" | grep -Eo '<DomainName>\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 '<?xml version="1.0"?><Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><EMailAddress>user@'$TARGET_DOMAIN'</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>' 2>/dev/null)
if echo "$AUTODISCOVER" | grep -qi "server\|internal"; then
  echo "  [+] Autodiscover response — internal server names leaked"
  echo "$AUTODISCOVER" | grep -Eo '(?:<Server>|<InternalRpcClientServer>|<ASUrl>)[^<]+' | head -5
fi

注意点

  • NTLM リレーには特定のネットワーク上の位置が必要です。 Exchange サーバーが接続可能なマシンを管理していない限り、NTLM リレーをリモートから悪用することはできません。
  • 最新の Exchange(Exchange Online、2019 以降)では、既定で Basic 認証がブロックされています。 Basic 認証がブロックされている場合は、モダン認証(OAuth2)でテストしてください。
  • アカウントロックアウトポリシーは環境によって異なります。 スプレーを行う前に、確実に誤りとわかっているパスワードを 1 つ使ってテストしてください。
  • ADFS は Exchange ではありません。 ADFS は独自の攻撃対象領域(SAML、WS-Trust)を持つ別のサービスです。

検証

  • NTLM Type-2 をデコードし、最低でも DNS ドメイン名と NetBIOS ドメイン名を特定する必要があります。
  • OWA のバージョンは X-OWA-Version ヘッダーから抽出する必要があります。
  • パスワードスプレー対象の評価では、レート制限がないことを確認します(短時間に 5 回試行しても、すべて同じ HTTP コードが返る)。
  • Autodiscover からは、外部 URL だけでなく内部サーバー名も返される必要があります。
  • Exchange のバージョン、AD ドメイン、NetBIOS 名、コンピューター名、レート制限の状況を記録します。

関連スキル

  • password-spray-methodology — あらゆるプロトコルに対応する汎用パスワードスプレーパイプラインとエラーコードの差異

コメント