In this chapter, you will understand what is meant by code injection and how DBAs can protect against it. Finally, there will be a discussion around code signing.
Understanding Code Injection
You should think of code injection as being a corporate equivalent to a Trojan Horse virus . Seemingly innocent code is deployed, but the code contains a back door through which individuals without authorization can elevate their privileges. Code injection should not be confused with SQL injection, which is a very different type of attack.
Code injection differs from most type of attacks in the respect that it is not usually designed to be malicious, although the results can have vastly negative results. In corporate SQL Server estates, code injection usually happens because of a lack of trust between SQL Server developers and DBAs. The developers will naturally have a very limited set of permissions on production, if any. Some developers, whose relationships have broken down with the production DBA, however, will insert code into a deployment, which allows them to have elevated permissions to the SQL Server instance. This, in turn, allows them to investigate or resolve issues, without the reliance upon the database administrators.
While not exactly malicious to the level of data theft or destruction, these actions are still breaking company policy, compromising security, and potentially breaking legal regulations, such as SOX, which requires separation of duties. It also means that the DBA team is required to administer an instance where they do not have the final say on what happens and where the environment may change without them knowing.
A code injection attack can take many forms. A simplistic attack will involve adding a line of code to a deployment script, which creates a login using SQL authentication, which has sa permissions or is included in the db_owner role of specific databases. This level of simplicity is quite easy for a DBA to spot, however, with a standard SQL Server Audit implementation, which checks for changes to the sa server role, or db_owner database roles.
Note
Please refer to Chapter 3 for further details on implementing SQL Server Audit.
A more sophisticated form of attack involves creating a stored procedure, which will create a highly privileged user “on request” and then delete it when it is no longer needed. To understand how this attack is performed, you should be familiar with EXECUTE AS.
Understanding EXECUTE AS
EXECUTE AS is a security feature of SQL Server that helps enforce the principle of least privilege. Instead of granting users’ permissions to database objects, such as tables or views, it is possible to only grant a user the permissions to execute a stored procedure. The stored procedure can then run in an elevated security context. The predecessor to EXECUTE AS is the SETUSER statement, and this is still available in SQL Server 2017 for backward compatibility.
Note
EXECUTE AS can be used as a statement in ad-hoc SQL as well as a clause in code modules, including stored procedures and triggers. This chapter will focus on the use of EXECUTE AS within a stored procedure, however.
EXECUTE AS Contexts With a Stored Procedure
EXECUTE AS Context | Definition |
---|---|
CALLER | The stored procedure will execute under the security context of the user that invoked the stored procedure. |
OWNER | The stored procedure will run under the security context of the user that owns the schema to which the procedure belongs. |
SELF | The stored procedure will execute under the security context of the user that either created or last altered the procedure. |
'UserName' | The stored procedure will run under the context of a named user. |
The script in Listing 13-1 demonstrates the behavior of EXECUTE AS within a stored procedure. The script first creates a stored procedure, which returns the user name of the current security context. The script then selects the user’s current security context before executing the stored procedure. If you run the script, you will note that two different security contexts are returned.
Note
Change the PrivilegedUser user to a user that you have configured within your instance. Also note that the demonstration assumes that you are not executing the stored procedure, while logged in as a privileged user.
Demonstrating EXECUTE AS
Note
It is possible to use this technique to impersonate a user based on an SQL login, or a user based on a Windows login. It is not possible, however, to impersonate a user that is based on a Windows group.
Using EXECUTE AS to Perform a Code Injection Attack
In order to implement code injection, a rogue developer will use the EXECUTE AS approach to fire a stored procedure, which will create a privileged user account. A stored procedure that will achieve this can be found in Listing 13-2.
Note
Before running the script, please change MyDatabase to the name of a database that exists on your instance.
Creating a Stored Procedure to Create a User
Note
The first line of the script turns TRUSTWORTHY to ON for the database. This is required for the attack to work, but a developer with this intent will usually set TRUSTWORTHY to ON when creating the database The TRUSTWORTHY property defines if EXECUTE AS can be used in a database and if UNSAFE CLR assemblies (which can access any resources, inside or outside of the SQL Server instance) can be used within a database. It can only be configured by a member of the sysadmin role.
Once the procedure has been created, a developer only needs EXEC permissions on the procedure in order to execute it. When they pass the value 1 to the procedure, it will create the login, with admin rights. Once they have performed their desired activity, they can run the procedure again, passing a value of 0. This will delete the login, before it is noticed by a DBA.
Increasing the Attack Complexity With Obfuscation
To increase the complexity of the attack and reduce the chances of a DBA spotting what is happening, a developer may choose to obfuscate the stored procedure. This will stop a DBA from being able to view the code within the stored procedure after it has been created. The script in Listing 13-3 demonstrates how a developer can add obfuscation to the stored procedure created in Listing 13-2.
Note
Code obfuscation is very weak and can even be decrypted by tools such as SQL Compare.
Obfuscate the Stored Procedure
Protecting Against Code Injection
The following sections will discuss how DBAs can avoid developers executing code injection attacks.
DevOps
The first method that we should discuss in avoiding code injection attacks is non-technical. Code injection attacks are usually performed in environments where there is a lack of cooperation and a lack of trust between development and operations teams. This can be addressed with the introduction of DevOps processes.
DevOps is a set of processes (and sometimes tooling) that allows for a closer collaboration between development and operational teams. Sometimes it will include the creation of a cross-functional team, where operations staff are hired as part of a development team and perform activities such as code deployments.
Cross-functional teams are not possible to spin-up in all circumstances, as some organizations will require clear separation of duties to comply with regulatory requirements. Even if a cross-functional team is not possible, however, a DevOps mindset can still alleviate the issue by encouraging teams to work more closely together.
Using Policy-Based Management to Protect Against Code Injection
In order to create a stored procedure that uses the security context of a privileged user, the security entity that creates the procedure must either have impersonation rights against the user or be a member of the sysadmin role. Therefore, this attack relies on the fact that the DBA team will be running the deployment themselves using an account that is member of the sysadmin server role. Essentially, the developer, who wants a back-door to the system, is relying on the assumption that the DBA will not evaluate their code before deploying it. With a large deployment consisting of thousands of lines of code, this is perfectly plausible.
To protect themselves against this scenario, a DBA can implement simple checks using Policy-Based Management (PBM), which will either stop the deployment from finishing or allow the DBA to easily evaluate the deployment, post-execution.
Note
A broader discussion around PBM can be found in Chapter 9. Therefore, this chapter will focus on creating a policy specifically to avoid back-door code injection attacks.
Creating the Condition
The first step in creating a policy that will avoid the use of EXECUTE AS in stored procedures is to create a condition that will check the execution context of stored procedures and ensure they are configured to execute under the context of the entity that executes the stored procedure, which is the default behavior.
You will notice that we have defined a name for the condition and selected the Stored Procedure facet from the drop-down, exposing the properties of a stored procedure. In the Expression window, we have chosen the @ExecutionContext property and stated that it must be Caller (the default behavior).
Parameters Accepted by sp_syspolicy_add_condition
Parameter | Description |
---|---|
@name | The name to be assigned to the condition |
@description | An optional description of the condition |
@facet | The name of the facet to which the required properties belong |
@expression | The @expression parameter has the data type NVARCHAR(MAX), but accepts an XML representation of the required condition. |
@is_name_condition | Indicates if the expression will evaluate the @name property of an object. Possible values are: • 0 - Indicates that the condition is not associated with the @name property • 1 - Indicates that the condition is associated with the @name property |
@obj_name | Specifies the name of the object that will be evaluated, in the event that @is_name_condition is 1 |
@condition_id | An output parameter, detailed the unique identifier (integer) that has been assigned to the new condition |
Create a Condition With sp_syspolicy_add_condition
Creating the Policy
You will notice that we have given the policy a name that is consistent with the condition and checked the Enabled option to ensure that the policy is enabled on creation. In the Check Condition drop-down, we have selected our AvoidCodeInjection condition and have neglected to apply any filters, meaning that every stored procedure within every database on the instance will be assessed. In a multi-server environment, we could also exclude specific instances by applying a condition to the Server Restriction filter. One of the most important configurations is the Evaluation Mode. We have configured this to be OnChange: Prevent. This means that the code deployment will fail should a stored procedure within the deployment use the EXECUTE AS clause. This means that a DBA will not even need to check a log, post-deployment.
Tip
If there was a stored procedure that was using EXECUTE AS for a legitimate reason that had been agreed to with the DBA Team, we could create an additional condition and apply this condition as a filter, so that the specific stored procedure could be ignored during evaluation.
Alternatively, we could create the policy using T-SQL by using the sp_syspolicy_add_object_set, sp_syspolicy_add_target_set, sp_syspolicy_add_target_set_level, and sp_syspolicy_add_policy system-stored procedures, all of which can be found in the dbo schema of the MSDB database.
sp_syspolicy_add_object_set Parameters
Parameter | Description |
---|---|
@object_set_name | The object set name, derived from the policy name |
@facet | The facet used by the condition associated with the policy |
@object_set_id | An output parameter, supplying the unique ID associated with the object set |
sp_syspolicy_add_target_set Parameters
Parameter | Description |
---|---|
@object_set_id | The ID of the associated object set. Only pass if the @object_set_name parameter is not passed. |
@object_set_name | The name of the associated object set. Only pass if the @object_set_id parameter is omitted. |
@type_skeleton | The SQL Server object hierarchy path to the target set object type |
@type | The type of the object set (which maps to a facet) |
@enabled | Specifies if the target set is enabled. Possible values are: • 0 - Indicating that it is disabled • 1 - Indicating that it is enabled |
@target_set_id | An output parameter that passes the ID of the newly created target set |
sp_syspolicy_add_policy Parameters
Parameter | Description |
---|---|
@name | The name of the Policy to be created |
@condition_id | The ID of the condition that will be evaluated. This parameter should only be passed if the @condition_name parameter is omitted. |
@condition_name | The name of the condition to be evaluated. This should only be supplied if the @condition_id parameter is omitted. |
@schedule_uid | The GUID of the schedule on which the policy will be evaluated |
@policy_category | The name of the category with which the policy will be associated |
@description | A description of the policy |
@help_text | Optional help text that can be displayed when the policy is evaluated. This is helpful to give a brief explanation of the policy. |
@help_link | Optional hyperlink that can be displayed when the policy is evaluated. This is helpful if you wish to direct a DBA toward a hosting standards or policy document. |
@execution_mode | An integer value, representing the evaluation mode of the policy |
@is_enabled | Indicates if the policy will be enabled on creation. Possible values are: 0 — Indicates that the policy will be disabled (This is always the value if the evaluation mode is configured as On Demand.) 1 — Indicates that the policy will be enabled on creation |
@root_condition_id | The ID of the condition that will be used to limit the policies evaluation range. Do not pass if the @root_condition_name parameter is passed. |
@root_condition_name | The name of the condition that will be used to limit the policies evaluation range. Do not pass, if the @root_condition_id parameter is passed. |
@object_set | The name of the object set that binds the policy |
@policy_id | An output parameter supplying the ID of the newly created policy |
Create the AvoidCodeInjection Policy
Code Signing
An alternative to using the EXECUTE AS clause on a stored procedure is to use code signing on the stored procedure. Unlike the EXECUTE AS clause, code signing does not change the context in which the code is run. Instead, it will sign the stored procedure with a certificate and combine the permissions granted to the certificate with the permissions granted to the caller of the stored procedure. Code signing is intended for scenarios where it is not possible to control permissions through ownership chains or where it is insecure to use ownership chains, such as in multi-database applications.
To use code signing for a stored procedure, the first step is to create a certificate within the relevant database. To create a self-signed certificate (as opposed to a certificate issued by a trusted certificate authority), you can use the CREATE CERTIFICATE statement.
Note
A further discussion around the SQL Server encryption hierarchy, including full details of how to use the CREATE CERTIFICATE statement, can be found in Chapter 5.
The command in Listing 13-6 demonstrates how to create a self-signed certificate for the purpose of code signing.
Note
Before executing the script, change MyDatabase to a valid database within your instance.
Create a Self-Signed Certificate
Note
Before creating a certificate, you must have a database master key. Please see Chapter 5 for further details.
Create a User Associated with the Certificate
We can now grant user-appropriate permissions based upon the requirements. Let’s imagine that the stored procedure will read data from MyTable. Listing 13-8 grants the permissions to CodeSigning.
Note
Before executing the code in Listings 13-8 and 13-9, please change MyTable to be a valid table in your database. The table you choose should be a table within a schema not owned by the same user as dbo. Otherwise ownership chaining will be used.
Grant Permissions to the CodeSigning User
Create the Stored Procedure to Access the Table
Sign the Stored Procedure
When the stored procedure is executed by a user, it will return the desired results, even if they do not have permissions to the underlying table, as the caller’s permissions will be combined with those of the certificate. The only permission required by the caller is the EXECUTE permission on the stored procedure.
It is important to note, however, that if a user has specifically been denied permissions to the underlying table, then they will not be able to successfully return results from the procedure. This is because when the two permission sets are combined, the DENY associated with the caller will override the GRANT associated with the certificate.
Summary
Code injection is an anomalous form of attack, in the respect that it is often not perceived by the attacker (usually a database developer) as malicious. Instead, it is perceived as a “means of getting things done.”
Despite this, code injection is dangerous to the SQL Server estate, as it allows for unauthorized changes to be made, without the DBA team being aware. A good way to avoid such attacks is for the DBA team to work more closely with the development team, ideally with a DevOps mentality being introduced. If the developer feels that he can resolve issues quickly, in collaboration with a DBA, then he is less likely to look for a back-door into the system.
Policy-Based Management can be used as a technical tool for avoiding code injection attacks by constraining the development team’s deployments to a hosting standard where the use of the EXECUTE AS clause is not permitted.
Code signing is designed for situations where ownership chains cannot be followed but can also be used in some circumstances as an alternative to the EXECUTE AS clause. A user is created and associated with a certificate. After the stored procedure has been signed using the certificate, the certificate’s permissions will be combined with that of the caller so that the caller may access the base tables without the need for a context change.