SQL injection inside Microsoft-signed system stored procedures is not supposed to happen. Yet, as I’ve been documenting in this series, it happens more often than you may assume.
This article walks through another one I found and reported to the Microsoft Security Response Center (MSRC). It’s a textbook SQL injection sitting inside sys.sp_MSdeletefoldercontents, a system stored procedure used by SQL Server replication.
What makes it interesting is not the injection technique itself (there is no clever Unicode trick this time), but what it enables: a working execution path for xp_cmdshell on a server where xp_cmdshell is explicitly disabled by configuration.
This article is part of Fabiano Amorim’s series on SQL Server vulnerabilities you may not be aware of.
Introducing the vulnerability (and some background)
Disabling xp_cmdshell is one of the most familiar SQL Server security recommendations. Microsoft disables it by default on new installations, security baselines routinely check that its configuration value is zero, and many teams alert whenever someone enables it.
The underlying assumption is simple: if xp_cmdshell is disabled, operating-system commands cannot be executed through it until somebody explicitly changes the configuration.
The vulnerability in this article challenges that assumption. The internal procedure accepts a folder path, concatenates that value into a dynamically-generated T-SQL batch, and executes the batch with sp_executesql.
Because the folder value is not parameterized or safely escaped, however, a caller can terminate the intended string literal and append arbitrary T-SQL.
While a direct call to xp_cmdshell fails with the expected ‘component is turned off’ error, it’s a different situation entirely when injected into sys.sp_MSdeletefoldercontents.
There, the exact same xp_cmdshell call succeeds without an explicit sp_configure/reconfigure.
Why this is not just a privilege escalation vulnerability
This is not a privilege escalation vulnerability from a normal database user to sysadmin. The procedure itself checks for sysadmin membership, so the prerequisite is already highly privileged.
Instead, the security relevance is different: it creates an alternate execution path around a configuration control that administrators, auditors, and detection systems may treat as authoritative.
Technical scope
The supplied proof of concept requires a login that is already a member of the sysadmin fixed server role. This article does not claim a lower-privileged user can exploit this path. The issue demonstrated here is a SQL injection and security-control bypass: a sysadmin session can reach xp_cmdshell through sys.sp_MSdeletefoldercontents even when a direct xp_cmdshell call is blocked because the feature is disabled.
Disclosure note
I reported this vulnerability to the Microsoft Security Response Center on March 25, 2026. Microsoft investigated it, classified it as a low-severity “defense-in-depth” issue (MSRC Case 111384), and stated that it did not meet the bar for immediate service. That classification does not mean you should ignore it.
What is sys.sp_MSdeletefoldercontents?
sys.sp_MSdeletefoldercontents is an internal system stored procedure used to remove a collection of replication-related files from a supplied folder.
The procedure contains an explicit security check: if the current login is not a member of the sysadmin fixed server role, it raises error 21089 and returns.
And, since the exploitation of the supplied path begins from sysadmin rather than a less-privileged database role, this is important.
Here’s the relevant security check:
|
1 2 3 4 5 6 |
-- Security check inside sys.sp_MSdeletefoldercontents if (isnull(is_srvrolemember('sysadmin'),0) = 0) begin raiserror(21089, 16, -1) return (1) end |
If that were the end of the story, the procedure would simply be a privileged housekeeping routine. Instead, a problem appears when @folder is copied directly into executable SQL text.
The vulnerable dynamic SQL
Let’s take a look at the procedure code – the vulnerable construction pattern at @command concatenation:
|
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 |
create procedure sys.sp_MSdeletefoldercontents ( @folder nvarchar(255) ) as begin set nocount on declare @command_prefix nvarchar(4000) declare @command nvarchar(4000) declare @retcode int declare @pathSeparator nvarchar(2) select @pathSeparator = CAST(SERVERPROPERTY('pathseparator') as nvarchar(2)) -- -- security check -- only sysadmin can execute this -- if (isnull(is_srvrolemember('sysadmin'),0) = 0) begin raiserror(21089, 16, -1) return (1) end select @retcode = 0 if len(@folder) = 0 or @folder is null begin return 0 end -- \ terminate path if substring(@folder, len(@folder), 1) <> @pathSeparator begin select @folder = @folder + @pathSeparator end select @command = N'exec @retcode = xp_delete_files N''' + @folder + N'sysmergesubsetfilters*.sch' + N''', N''' + @folder + N'sysmergesubsetfilters*.bcp' + N''', N''' + @folder + N'*_*.sch' + N''', N''' + @folder + N'*_*.bcp' + N''', N''' + @folder + N'*_*.idx' + N''', N''' + @folder + N'*_*.sql' + N''', N''' + @folder + N'*_*.ftx' + N''', N''' + @folder + N'*_*.pre' + N''', N''' + @folder + N'snapshot.cab' + N''', N''' + @folder + N'dynsnapvalidation.tok' + N''', N''' + @folder + N'snapshot.pre' + N''', N''' + @folder + N'*_*.trg' + N''', N''' + @folder + N'*_*.xpp' + N''', N''' + @folder + N'*_*.cft' + N''', N''' + @folder + N'*_*.dri' + N''', N''' + @folder + N'*_*.prc' + N'''' begin try exec sp_executesql @command, N'@retcode int OUTPUT', @retcode OUTPUT end try begin catch set @retcode = 1 end catch return @retcode end |
There is a sysadmin guard at the top, but what comes after that is the interesting part: the procedure builds a dynamic T-SQL command string by concatenating @folder (the user-supplied nvarchar(255) parameter.)
In other words, this is the dangerous pattern:
|
1 2 |
SET @command = N'...''' + @folder + N'...'; EXEC sp_executesql @command; |
It’s the textbook definition of SQL injection, whereby the procedure trusts @folder without validation. If a caller passes a value containing an apostrophe, it closes the string literal early, and everything after that apostrophe is interpreted as T-SQL – not data.
The injected code then runs through sp_executesql – inheriting the procedure’s own security context rather than the caller’s.
The sysadmin catch (and why it still matters)
Before we jump to the proof of concept, let’s confront the objection head-on, because Microsoft did.
The procedure explicitly checks is_srvrolemember('sysadmin') and refuses to run for anyone else. This is why MSRC classified this as a low-severity, defense-in-depth issue rather than a privilege escalation vulnerability.
From MSRC (Case 111384):
After careful investigation, this case has been assessed as a low severity, defense-in-depth item, and does not meet MSRC’s bar for immediate servicing due to sys.sp_MSdeletefoldercontents requiring sysadmin privileges.
The obvious counterargument is fair: a member of sysadmin already controls SQL Server. A sysadmin can usually enable xp_cmdshell, execute a command, and disable it again.
But that just prompts the question: why should we care if the attacker is already sysadmin?
What an attacker can (and will) do once they’ve obtained sysadmin
Well, privilege is only one part of the security equation. Visibility is another. Obtaining sysadmin is not often the end of the intrusion during a real attack – rather, it’s when the attacker begins their ‘post-exploitation activities.’
These activities include collecting information, accessing credentials and sensitive data, moving laterally, establishing persistence, and/or executing operating-system commands.
This is where techniques such as this one become valuable. From the attacker’s perspective, they are gaining from the ‘evasion’ element, not the privilege escalation itself.
In my own SQL Server security research, once I obtain sysadmin, one of the next steps is frequently to test operating-system command execution. I’ve used this technique extensively to reach xp_cmdshell without having to perform the configuration change that defenders commonly expect to see.
In fact, I’ve never seen anyone monitoring for sp_MSdeletefoldercontents in my time. After all, it’s a Microsoft-owned procedure, so it must be safe! Why should they monitor for it?
Instead, focus goes toward controlling xp_cmdshell activity using common security best practices like monitoring, audit, alert setup, etc.
Future-proof database monitoring with Redgate Monitor
The vulnerability in action (and how to recreate it)
Here’s the full vulnerability in action, and a step-by-step guide on how to recreate it.
Step 1: confirm direct xp_cmdshell execution is blocked
Start with xp_cmdshell disabled, then invoke it directly:
|
1 2 3 4 5 6 7 8 9 10 |
EXEC xp_cmdshell 'dir c:\'; GO Result: Msg 15281, Level 16, State 1, Procedure xp_cmdshell, Line 1 SQL Server blocked access to procedure 'sys.xp_cmdshell' of component 'xp_cmdshell' because this component is turned off as part of the security configuration for this server. A system administrator can enable the use of 'xp_cmdshell' by using sp_configure. For more information about enabling 'xp_cmdshell', search for 'xp_cmdshell' in SQL Server Books Online. |
This is exactly the message any hardened server should return, and exactly what an auditor expects to see.
Step 2: invoke xp_cmdshell through the injection
Next, call sys.sp_MSdeletefoldercontents and place the xp_cmdshell statement inside @folder:
|
1 2 3 4 5 6 7 8 9 10 |
USE master; GO EXEC sys.sp_MSdeletefoldercontents @folder = 'C:\temp\''; EXEC xp_cmdshell ''whoami''--'; GO Result: -------------------------------------------------------------------- Computer1\administrator |
There it is. xp_cmdshell executed, and whoami returned the SQL Server service account.
Step 3: prove the primitive is general T-SQL injection
The report also uses a harmless SELECT payload to show that the primitive is not specific to xp_cmdshell:
|
1 2 3 |
EXEC sys.sp_MSdeletefoldercontents @folder = 'C:\temp\''; SELECT @@Version AS v;--'; GO |
That query returns the SQL Server version string, useful because it isolates the core defect in which arbitrary T-SQL can be appended to the dynamically-generated batch. xp_cmdshell is simply the most security-relevant demonstration of the resulting control bypass.
Why, and how, does this bypass the xp_cmdshell disable?
When sys.sp_MSdeletefoldercontents calls sp_executesql, the batch runs inside the execution context of the system procedure, which is a Microsoft-signed object living in the sys schema. Along that path, the feature-switch check for xp_cmdshell is not enforced the same way it is for a direct call from a user session.
Overall, sysadmin being able to run arbitrary T-SQL is not the problem. However, sysadmin being able to run xp_cmdshell through a Microsoft-signed system procedure, without ever asking sp_configure for permission or leaving a “feature enabled” event behind, is most definitely a problem.
What can you do to prevent the vulnerability?
Since the supplied exploit requires sysadmin, the most important defensive control is still strict control of sysadmin membership.
This finding shouldn’t be interpreted as a reason to re-enable xp_cmdshell – rather, it’s reason to avoid treating the disabled flag as the only relevant control.
Here’s everything you can do to prevent the vulnerability:
- Keep
sysadminmembership extremely small. Review permanent human accounts, service accounts, and automation identities that hold the role. - Continue to keep
xp_cmdshelldisabled unless there’s a documented operational need. Microsoft still recommends this. - Monitor for calls to
sys.sp_MSdeletefoldercontentsoutside expected replication workflows – especially those containing quote characters, semicolons, comment markers, or other values that don’t resemble normalfilesystempaths. - Do not exclusively depend on alerts for
sp_configure'xp_cmdshell'. Monitor the SQL Server service account at the operating-system layer. Process creation and command-shell telemetry can detect OS execution even when the SQL-side invocation is indirect. - Alert on actual child process creation by the SQL Server service and on suspicious use of extended procedures. The most valuable detection strategy is to correlate SQL activity with host process creation.
- If you don’t use replication, deny explicit execute on the procedure.
sp_MSdeletefoldercontentsis a replication-facing procedure. On instances where replication is not configured and never will be, an explicitDENY EXECUTE ON sys.sp_MSdeletefoldercontents TO public;gives you one more small barrier. It won’t stop a determinedsysadmin(they canGRANTit back), but it’ll show up in an audit trail if someone tries!
In summary: the sys.sp_MSdeletefoldercontents vulnerability in SQL Server
The vulnerability in SQL Server sys.sp_MSdeletefoldercontents is easy to understand once the generated SQL is visible. Essentially, @folder is inserted directly into a dynamic batch, so a crafted folder string can escape the intended pathname literal and append arbitrary T-SQL.
What makes the finding interesting is the behavior demonstrated with xp_cmdshell. The direct call is rejected because the feature is disabled, yet the injected call through the internal procedure succeeds.
The fix for SQL Server developers: never concatenate caller-controlled text into executable SQL when it can be passed as data.
For SQL Server administrators, it’s similar but not the same. Keep xp_cmdshell disabled if possible, and do not treat that single setting as proof that operating-system command execution is impossible.
After all, if an attacker wants to run a shell command, it has several options, including: extended procedures, OLE Automation, SQL Agent jobs, common language runtime (CLR), and external scripts.
Finally, enforcing auditing, monitoring, and alerting, is always important.
References
Move fast. Govern at scale.
FAQs: The sys.sp_MSdeletefoldercontents vulnerability in SQL Server
1. What is the vulnerability in sys.sp_MSdeletefoldercontents?
It’s a SQL injection flaw where the @folder parameter is concatenated directly into a dynamic SQL batch without parameterization, allowing an attacker to break out of the intended string and inject arbitrary T-SQL.
2. Does this let a non-admin user gain sysadmin access?
No. The procedure requires the caller to already be a sysadmin, so it’s not a privilege escalation bug. Its significance is that it lets an already-privileged user bypass a security control (the xp_cmdshell disable flag) rather than gain new privileges.
3. Why does xp_cmdshell run here if it's disabled server-wide?
Because the injected command executes inside the security context of the Microsoft-signed system procedure via sp_executesql, the feature-switch check that blocks direct xp_cmdshell calls from a user session isn’t enforced the same way along that internal execution path.
4. How did Microsoft respond to this report?
MSRC investigated and classified it as a low-severity “defense-in-depth” issue (Case 111384), stating it didn’t meet the bar for immediate servicing since sysadmin privileges are a prerequisite.
5. If sysadmin can already do anything, why does this matter?
Because privilege and visibility are separate concerns. This path lets a sysadmin reach OS command execution without ever triggering an sp_configure change or a “feature enabled” event — the exact signal most detection setups rely on to catch xp_cmdshell abuse.
6. How can organizations detect or mitigate this?
Keep sysadmin membership minimal, monitor calls to sys.sp_MSdeletefoldercontents for anomalous input (quotes, semicolons, comment markers), correlate SQL activity with OS-level process creation on the service account, and consider denying execute on the procedure if replication isn’t in use.
This document contains proprietary information and is protected by copyright law.
Copyright © 2026 Red Gate Software Limited. All rights reserved
Load comments