2023年5月23日(金)
著者:カウンター・スレット・ユニット(CTU™️)リサーチチーム
※本記事は、https://www.secureworks.com/ で公開されている Tampering with Conditional Access Policies Using Azure AD Graph API を翻訳したもので、 2022年5月23日執筆時点の見解となります。
要約
Microsoftが提供するAzure Active Directory(Azure AD)は、IDおよびアクセス管理のクラウドサービスであり、複数の認証方法をサポートしています。Azure AD Premiumでは、定義された基準(デバイスのコンプライアンス、ユーザーの所在地など)をもとにアクセスの許可またはブロックが可能な条件付きアクセスポリシー(CAP)も利用できます。認証方法およびCAPの設定はAzure ADに保存されます。CAPは、Azure ADポータル、PowerShell、API呼び出しで変更できます。
Secureworks®カウンター・スレット・ユニット(CTU™)のリサーチャーは2022年5月、CAPの設定を編集可能なAPIの種類について調査しました。その結果、廃止予定のAzure AD Graph(別名AADGraph) API、Microsoft Graph API、および文書化されていないAzure IAM APIの3つを使うとCAPの設定を編集できることが判明しました。このうち、CAPのメタデータを含む設定項目すべてを変更可能なAPIは、AADGraph APIのみでした。この機能があれば、管理者側で作成日時と変更日時のタイムスタンプを含むすべてのCAP設定を改ざんすることが可能になります。AADGraph経由で行った変更は適切な形でログに記録されないため、Azure ADポリシーの完全性および否認防止が損なわれる恐れがあります。
当社CTU™は、2022年5月26日付で前述の所見をMicrosoft社に報告しました。Microsoft社による確認は1か月後に得られましたが、「想定通りの動作である」という回答でした。2023年5月11日、Microsoft社より当社CTUに対し、「今後は監査ログを改善し、AADGraph経由でのCAPの更新を制限する」旨の通知がありました。
Azure ADのCAP
Azure ADのCAPを使うと、組織内でAzure ADの保護下にあるサービスへのアクセスを許可/ブロックすることができます。また、セッションの監視やセッションの有効期間を制限する目的でも利用できます。CAPはAzure ADの認証プロセス時に適用されます。Azure ADは、以下の一般的なシグナルを用いてアクセスポリシーを判定します。
- ユーザーまたはグループのメンバーシップ
- IPロケーション情報
- デバイス
- アプリケーション
Azure ADポータルのCAPには、特定のロールを付与されたユーザー以外はアクセスできません(表1)。
アクセスの種類 | Azure ADのロール |
---|---|
読み取り |
グローバル管理者 グローバル閲覧者 セキュリティ管理者 条件付きアクセス管理者 セキュリティ閲覧者 |
変更 |
グローバル管理者 セキュリティ管理者 条件付きアクセス管理者 |
表1:CAPへのアクセス時に必要なAzure ADのロール
図1は、ユーザーすべてに多要素認証(MFA)を義務付けるCAPのサンプルです。サンプルはレポート専用モードに設定されているため、ポリシーは有効化されていません。このモードを使うと、CAPの適用に先立ち、適用による組織全体への影響度を評価できます。
図1:CAPのサンプル(出典:Secureworks)
Azure ADポータルにはポリシー名、状態、作成日時/変更日時のタイムスタンプが表示されます(図2)。
図2:CAP一覧(出典:Secureworks)
CAPが変更される都度、Azure ADポータルに変更内容が反映されます(図3)。
図3:変更されたCAP(出典:Secureworks)
Azure ADの監査ログには、CAPの作成・変更イベントが記録されます(図4)。
図4:CAP作成イベント(下部赤枠部分)および変更イベント(上部緑色部分)が記録されたAzure ADの監査ログ(出典:Secureworks)
「Add conditional access policy」、「Update conditional access policy」どちらのイベントにも、変更されたプロパティの詳細が表示されています(図5)。これにより、設定変更履歴を含む完全な監査証跡を入手できます。
図5:監査ログに表示された「Update conditional access policy」イベントの詳細(出典:Secureworks)
API呼び出しによる条件付きアクセスの変更
Azure ADポータルは、管理者がブラウザ経由でCAPを作成・維持管理できるグラフィカルユーザーインターフェース(GUI)です。GUI上ではアドホックなタスクを実行できますが、自動化やプログラムによるアクセスはできないため、CAPと連携可能なAPI(以下の3つ)がMicrosoftより提供されています。
- Azure AD IAM
- MS Graph
- Azure AD Graph (AADGraph)
Azure AD IAM API
Azure ADポータルはCAPを作成、閲覧、編集する際に文書化されていないAzure AD IAM APIを使用しています。このAPIはhttps ://main . iam . ad . ext . azure . com/api/Policies/Policiesで使用できます。AzureADポータルがAzure AD IAM APIを使用しているため、アクセス時には前述の表1に記載された権限が必要となります。APIを呼び出すと、CAP一覧がJSONオブジェクト形式で返されます(図6)。
図6:Azure AD IAM APIからの応答(出典:Secureworks)
API経由でCAPを編集しようとすると、APIからCAPの詳細がJSONオブジェクト形式で返されます(図7)。このオブジェクトには、Azure ADポータルに表示されるCAPの設定に対応するフィールドが多数含まれています。レスポンスには、作成日時/変更日時のタイムスタンプも含まれています。
図7:呼び出されたAzure AD IAM APIのレスポンス(出典:Secureworks)
CAPを変更すると、JSONオブジェクトがHTTP POSTリクエストとしてhttps: //main . iam . ad . ext . azure . com/api/Policies/ConvertPolicyMsGraphに送信されます。図8は、無効化された状態からレポート専門モードに変更されたCAPのJSONオブジェクトです。メタデータではなく、変更されたデータのみがAzure ADに送信されます。
図8:Azure AD IAM APIを介したCAPの変更リクエスト(出典:Secureworks)
MS Graph API
MS Graph APIを介した条件付きアクセスのサポートについては、詳細に文書化されています。Microsoft のサイトでは、CAPの作成・変更サンプルも公開されています。表2は、MS Graph API経由でCAPを利用する際に必要な権限の一覧です。
アクセスの種類 | 必要な権限 |
---|---|
変更(3つすべてが必要) | Policy.Read.All Policy.ReadWrite.ConditionalAccess Application.Read.All |
読み取り | Policy.Read.All |
表2:MS Graph API経由でCAPを利用する際に必要な権限
前述の権限が付与されたユーザーまたはアプリケーションは、https: //graph . Microsoft . com/v1.0/identity/conditionalAccess/policiesのAPIを呼び出してCAPを列挙できます。APIは、すべてのCAP、およびその詳細をJSONオブジェクト形式で返します(図9)。
図9:MS Graph APIからの応答(出典:Secureworks)
CAPの作成/変更時は、同一のAPIエンドポイントを使用します。
- CAP作成時はJSONオブジェクト形式のHTTP POSTリクエストを実行(図10)
図10:MS Graph APIを介した条件付きアクセスポリシーの作成(出典:Secureworks) - CAP変更時は、JSONオブジェクト形式のHTTP PATCHリクエストを実行。
Azure ADに送信されるのは、メタデータではなく変更されたデータのみです。
Azure AD Graph API (AADGraph)
Microsoft社はこの数年、AADGraph APIの廃止に向けて動いていました。本ブログ公表日現在、廃止は2023年6月30日以降に実施される予定です。廃止に向けて利用者を徐々に減らす目的で、公式サイトからAADGraph APIのドキュメントが削除されています。
CAPは、AADGraph API(https: //graph . windows . net/<tenant>/policies?api-version=<api version>)経由でアクセスできます。<tenant> にはAzure ADテナント、<api version>にはAADGraph APIの任意のバージョンを入力します。APIのバージョンを1.6に指定してアクセスすると、ユーザーが適切な権限を持つ場合には、いくつかのアクセス可能なAzure ADポリシーが返されますが、その中にCAPは含まれません。しかし、APIのバージョン1.61-internalを指定すると、ユーザーの権限に関係なく、CAPを含むすべてのAzure ADポリシーが返されます。その結果、テナントのどのユーザーでも必要なロールがなくてもCAPを列挙することができます。
APIは、すべてのポリシーをJSONオブジェクトとして返します。図11は、応答に含まれるCAPポリシー(policyTypeが18) です。
図11:AADGraph APIのレスポンス(出典:Secureworks)
CAPの設定およびメタデータは、JSONオブジェクト形式でpolicyDetail属性に格納されます(図12)。CAPの変更権限をもつ管理者は、この属性を編集し、CAPの条件やメタデータを改ざんすることができます。
図12:policyDetail属性に含まれるCAPの設定情報(出典:Secureworks)
既存のCAPをAADGraph API経由で更新する場合は、HTTP PATCHリクエストをhttps: //graph . windows . net/<tenant>/policies/<objectid>?api-version=1.61-internalに送信します。<objectid>欄には変更対象のCAPのオブジェクトIDを入力します。リクエストの内容はpolicyDetail属性のみを含んだJSONオブジェクトとなります(図13)。
図13:AADGraph APIを介した CAPの更新(出典:Secureworks)
条件付きアクセスポリシーの改ざん
当社CTUリサーチャーは、AADInternalsツールキットを用いたCAP改ざんの検証を実施しました。管理者または攻撃者がAADGraph APIを活用すると、適切なログ記録を残さない形でCAPを変更することができます。
-
サンプル用CAPの現在のpolicyDetail値を取得。
- CAP変更権限をもつ管理者用アクセストークンを入手
- サンプル用CAPを任意の変数に保存
- policyDetailの値を抽出し、データをクリップボードにコピー(図14)
図14:AADInternalsを用いてCAPの現在のpolicyDetailを取得(出典:Secureworks)
-
policyDetailの値をテキストエディタにペーストし、JSONを読みやすく加工(図15)。その後、ModifiedDateTime属性を空にし(4行目)、State属性をReportingからDisabledに変更。加工後のJSONをフラット化してクリップボードにコピー。
図15:CAPのpolicyDetail変更(出典:Secureworks) -
クリップボードにコピーされた変更済policyDetailを用いてCAPを更新(図16)。
図16:AADInternalsを用いてCAPのpolicyDetail属性を更新(出典:Secureworks)変更内容は1分以内にAzure ADポータルに反映されました(図17)。
図17:変更済CAPが表示されたAzure ADポータル(出典:Secureworks)
AADGraph API経由でCAPを更新する際、「Update conditional access policy」イベントは監査ログ上に生成されません(図18)。その結果、変更内容がすべて記録されない不完全な監査証跡になってしまいます。
図18:AADGraph経由でCAPを変更しても、Update conditional accessイベントは生成されない(出典:Secureworks)
攻撃者が管理者権限を入手すれば、ログに残らないという点を悪用し、CAPを判読しづらい名称に改ざんすることが可能です。たとえば図19のPowerShellスクリプトによって、CAP全件のタイムスタンプおよび表示名が削除されています。
図19:CAPの表示名およびタイムスタンプを削除するPowerShellスクリプト(出典:Secureworks)
スクリプト実行後でも、CAPは完全に機能します。ただし、Azure ADで開いたり、編集したりすることはできません(図20)。
図20:CAPの表示名とタイムスタンプを削除した後のAzure ADポータル画面(出典:Secureworks)
管理者は、CAPを削除したり、既存のCAP設定を表示するために複製を作成したりすることが可能です。組織が監査ログを長期間保存している場合、過去の監査ログデータに基づいてCAPの名前とタイムスタンプを復元できる可能性があります。
Microsoft社とのやり取り
当社CTUは2022年5月20日付で、メタデータの編集およびログ記録に関する問題をMicrosoft Security Response Center(MSRC)宛に報告しました。メタデータの変更は管理者側でも実行できるため、データ改ざんおよび権限昇格に関する問題として報告しました。MSRCからは6月26日付で以下の回答がありました。
Azure AD Graph API経由で条件付きアクセスポリシーが変更された際
(またはAAD Graph APIのPowerShellモジュール経由で変更された際)の挙動を、以下のとおり確認しました。
監査ログにはCore Directoryサービスの監査ログアイテムのみが表示され、これに対応する条件付きアクセスサービスの監査ログアイテムは表示されない。
変更されたプロパティおよび値の詳細は、Core Directoryサービスの監査ログアイテムに表示されない。
ポリシーオブジェクトの編集によって変更された日付データが、Azureポータルの条件付きアクセス画面上で更新されない。
該当するシナリオを分析した結果、以下の結論に達しました。
権限が昇格されることはありません:必要なアクセス権限をもつユーザー以外は、ポリシーオブジェクトにアクセスしたり、変更したりすることはできないためです。
必要な情報はサインインログに記録されているため、悪意ある条件付きアクセスポリシーに関する調査には影響しません。
ポリシー変更に関する情報(Date、Activity、Target、Actor)はアクティビティログに表示されるため、管理者が監査を実施し、
誰がいつポリシーを変更したのか確認できます。
2022年8月23日、当社CTUよりMSRC宛に、「すべてのユーザーが条件付きアクセスを閲覧可能な状態になっている」旨、申し入れました。管理者権限のないユーザーであってもCAPを閲覧できるため、権限昇格の問題として報告しました。2023年2月2日付でMSRCより以下の回答がありました。
認証済・承認済のユーザーがAzure ADの設定に関する特定のデータ(認証ポリシー、その他これに類似する設定内容など)を閲覧できるという事例はいくつか存在します。
これらは設計上意図どおりの挙動です:ユーザーは承認済、データは読み取り専用であり、特定のユーザー情報は含まれていません。
認証ポリシーなどのデータを閲覧することはセキュリティ侵害とはみなされません。ただしセキュリティの観点から、その他のデータや設定内容については、編集/作成/削除の権限が付与された管理者ロール以外のユーザーが閲覧または変更できないよう、Azure ADを最適化しています。
2023年5月11日、MSRCより当社CTUリサーチチーム宛に、これらの問題に対処するための以下の変更計画が通知されました。
- AAD Graph経由でCAPが更新された際、更新されたポリシーの種類がログに反映されるよう、監査ログを改善します。
- 管理者がAAD Graph経由でCAPを更新できない仕様に変更します。
上記の改善に加え、AAD Graphを廃止する予定です。
まとめ
管理者がAADGraph APIを使うとCAPを変更することができます。API経由の場合、変更内容がログに記録されず、監査証跡が不完全になるため、CAPの完全性および否認防止性が損なわれます。その結果、組織内のAzure ADポータルに表示される、または監査ログに間接的に表示されるCAP情報の信頼性が低下します。さらに、テナントユーザーなら誰でも、管理者権限がなくてもCAPを閲覧できるため、権限の低い攻撃者にCAPの抜け穴が特定されたり、改ざん目的で狙われたりする恐れがあります。ROADToolsやTSxAzureADExportなどの外部ツールを使うと、この点(誰でも閲覧できる状態)を悪用できます。各組織の皆様は、Azure ADの監査ログをLog AnalyticsワークスペースまたはSecureworks Taegis™ XDRなどのストレージソリューション上に保管することをCTUリサーチャーより推奨します。AADGraph API を介したCAPの変更有無は、監査ログを監視することで確認できます。監査ログ上で、「Update policy」イベント発生後2秒以内に、これに対応する「Update conditional access policy」が生成されていない場合はAADGraph API 経由でCAPが変更されたことを示します。
Appendix
以下のスクリプトを実行すると、Azure ADポータルまたはMS Graph API経由で作成/変更されたCAPの名称および変更日を復元できます。
# Read legit CAP events from the audit log $CAPEvents=Get-AADIntAzureAuditLog -Export ` | Where-Object activityDisplayName -in ` "Add conditional access policy", ` "Update conditional access policy" ` | Select-Object "activityDateTime" -ExpandProperty "targetResources" ` | Select-Object "id","displayName","activityDateTime" # Loop through the events to get the first (latest) update $CAPInfos=@{} foreach($CAPEvent in $CAPEvents) { if(!$CAPInfos.ContainsKey($CAPEvent.id)) { $CAPInfos[$CAPEvent.id] = [pscustomobject]@{ "displayName" = $CAPEvent.displayName "modifiedDateTime" = $CAPEvent.activityDateTime } } } # Read current CAPs $CAPs = Get-AADIntConditionalAccessPolicies # Loop through CAPs foreach($CAP in $CAPs) { # Create the return value $retVal = [pscustomobject][ordered]@{ "id" = $CAP.objectId "isEmpty" = [string]::IsNullOrWhiteSpace($CAP.displayName) "success" = $null "name" = $CAP.displayName } # Check whether the displayName is empty if($retVal.isEmpty) { # Check whether we found the old information if($CAPInfo = $CAPInfos[$retVal.id]) { # Get policyDetails and fix Modified Date $policyDetail = $CAP.policyDetail[0] | ConvertFrom-Json try { $policyDetail.ModifiedDateTime = $CAPInfo.modifiedDateTime } catch{} $newPolicyDetail = $policyDetail | ConvertTo-Json -Depth 10 -Compress # Replace name with the old displayName $retVal.name = $CAPInfo.displayName try { Set-AADIntAzureADPolicyDetails -ObjectId $retVal.id ` -PolicyDetail $newPolicyDetail ` -DisplayName $retVal.name ` | Out-Null $retVal.success = $true } catch { # Failed $retVal.success = $false $retVal.name = $CAP.displayName } } } # Return $retVal }
以下のKQLクエリを使用すると、2秒以内に対応する「Update conditional access policy」イベントがない「Update policy」イベントを特定することができます。
AuditLogs | where OperationName == "Update policy" | mv-expand TargetResources | where TargetResources.displayName != "Default Policy" | mv-expand InitiatedBy | project PolicyName = TargetResources.displayName, PolicyId = tostring(TargetResources.id), UserPrincipalName = InitiatedBy.user.userPrincipalName, UserId = tostring(InitiatedBy.user.id), OperationName, Time = bin(TimeGenerated, 2s), TimeGenerated, CorrelationId |join kind=leftanti (AuditLogs | where OperationName == "Update conditional access policy" | mv-expand TargetResources | mv-expand InitiatedBy | project PolicyId = tostring(TargetResources.id), UserId = tostring(InitiatedBy.user.id), Time = bin(TimeGenerated, 2s)) on PolicyId,UserId,Time | order by TimeGenerated