exchange-owa-attack について
exchange-owa-attack は、Exchange と Outlook Web Access を調査するセキュリティテスト用スキルです。許可を得たテスター向けに、エンドポイントの特定、NTLM メタデータの取得、認証制御の確認、バージョン情報の収集を案内します。
主な機能
- 01Exchange と OWA のフィンガープリント調査
- 02NTLM Type-2 メタデータの取得
- 03認証制御の評価
- 04Exchange エンドポイントの特定
- 05バージョンとパッチ情報の収集
活用シーン
- 01発見したメール関連ホストの調査
- 02Exchange バージョンと AD メタデータの特定
- 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— あらゆるプロトコルに対応する汎用パスワードスプレーパイプラインとエラーコードの差異
コメント
Markdown 対応