My syntax highlighter is a bit broken. Until I fix it, the TSQL shown below will be full of crud. Fix it before trying it on your system. Or just wait until I fix up the highlighter. Your choice, really.
Much has been written about why SQL’s xp_cmdshell is a bad thing. It has its uses, of course, and sometimes we just have to enable it for our apps to work.
In my most recent post about SQL, you would’ve detected a certain.. disdain.. for applications developers. It’s not their fault, though. They’re just trying to write something that works. In my workplace, there is a “just make it work” mentality, which can lead to some awful misconfigurations. That’s why this post is dedicated to just how bad xp_cmdshell can be.
By default, xp_cmdshell is disabled, and with good reason. If it’s not set up just so, it can be exploited. The gist of it is this: if you can run a command from a DOS shell, non-interactively, xp_cmdshell will run it, too. So it stands to reason that you should lock xp_cmdshell down as much as possible. But this is at odds with the “just make it work” mentality, and sometimes security is compromised as a result.
If you’ve set up your SQL server per best practice, your SQL Server service account is an unprivileged domain account. That means it’s just a regular account, not a member of any special domain or local groups. When you tell the SQL installer to use this account, it sets up file permissions, registry permissions and local policies so that your service account has all the things it needs to run SQL properly. But by default, the SQL installer doesn’t prompt you for a domain account. No. Left to its own devices, it will let you choose from a drop-down:
<insert screenshot here that Crayon syntax highlighter doesn’t like 🙁 >
Interesting. The local system account is here, as well as the option to browse. Hey, Local System looks good. So, for this demonstration, let’s assume the naive person installing SQL selects this account. For those of you not in the know, the Local System account has the keys to the city over the local machine. It has no rights on any other system. But for this demo, that doesn’t matter. Let’s then assume the “Use the same account for all SQL Server services” is clicked.
So we carry on installing, and voila, we have a running SQL instance. Let’s say the apps developer asks for xp_cmdshell to be enabled. That’s easy enough to do:
EXEC master.dbo.sp_configure 'show advanced options', 1
RECONFIGURE
go
EXEC master.dbo.sp_configure 'xp_cmdshell', 1
RECONFIGURE
go
And we get a successful result!
Configuration option ‘show advanced options’ changed from 0 to 1. Run the RECONFIGURE statement to install.
Configuration option ‘xp_cmdshell’ changed from 0 to 1. Run the RECONFIGURE statement to install.
Let’s try a command:
xp_cmdshell 'whoami.exe'
Hey, this is great! It WORKS!!!!!!!!!!!!!!!!!!!!!!
Let’s try another one:
xp_cmdshell 'dir c:'
Yay! More working xp_cmdshell goodness.
We can even use it to write to a text file:
exec xp_cmdshell 'echo hello > c:\file.txt'
exec xp_cmdshell 'echo appended data >> c:\file.txt'
exec xp_cmdshell 'echo more data >> c:\file.txt'
And the result is, funnily enough, a text file C:\FILE.TXT:
hello
appended data
more data
Let’s try something a little more interesting.
xp_cmdshell 'format c:'
We should all breathe a sigh of relief here. The format command needs to be run interactively. It wants you to answer yes or no. For once, Microsoft’s efforts to save us from ourselves have been useful.
xp_cmdshell 'diskpart'
And thankfully, by default, diskpart also runs interactively:
Here’s where it can get interesting. Diskpart runs interactively by default, but can also run in scripted mode. Let’s jump to a DOS box for a minute to see how that works:
I’ve just deleted the contents of disk 1. It didn’t take much effort, did it? Let’s see if we can script that.
First, create a text file somewhere that contains this:
select disk 1
clean all
Now, let’s run diskpart scripted:
Hmm.
I wonder if we can do the same thing with xp_cmdshell?
First, let’s create the command file:
exec xp_cmdshell 'echo select disk 1 > c:\diskpartcommands.txt'
exec xp_cmdshell 'echo clean all >> c:\diskpartcommands.txt'
Then, let’s run diskpart scripted:
xp_cmdshell 'diskpart -s c:\diskpartcommands.txt'
If you don’t think a misconfigured xp_cmdshell is a bad thing, try this on a production system. See how long you stay employed.
Some notes on the demo above. I ran these commands using an account that was a member of the Administrators group on the local machine. That’s why I didn’t have to do anything else special in order for xp_cmdshell to work.
Let’s say I have an unprivileged user that I want to be able to run xp_cmdshell commands. I need to give them access to the command:
grant execute on xp_cmdshell to [sql\gpotest]
Let’s try this again, this time running it under the context of a standard user account:
xp_cmdshell 'whoami.exe'
Msg 15153, Level 16, State 1, Procedure xp_cmdshell, Line 1
The xp_cmdshell proxy account information cannot be retrieved or is invalid. Verify that the ‘##xp_cmdshell_proxy_account##’ credential exists and contains valid information.
SQL is thoughtful enough to protect us from ourselves. But we want to JUST MAKE IT WORK!!!!!!!!!!!!!!
So, let’s do what every app developer I’ve ever met would do. Let’s configure the proxy account using a privileged account – maybe even the app developer’s own account (which is, of course, a member of the Administrators group). This can be done via the GUI or TSQL, but for this demo, I’ll show the TSQL:
use master
go
EXEC sp_xp_cmdshell_proxy_account 'SQL\sillydeveloper','password'
Now when we run the whoami command, we get this result:
Msg 229, Level 14, State 5, Procedure xp_cmdshell, Line 1
The EXECUTE permission was denied on the object ‘xp_cmdshell’, database ‘mssqlsystemresource’, schema ‘sys’.
OK, fine. Let’s grant this unprivileged user the rights to use xp_cmdshell:
USE master
GRANT exec ON xp_cmdshell TO [SQL\gpotest]
Let’s run the whoami command again:
Yay! It works! Great, huh?
Now, let’s try that diskpart stuff again:
exec xp_cmdshell 'echo select disk 1 > c:\diskpartcommands.txt'
exec xp_cmdshell 'echo clean all >> c:\diskpartcommands.txt'
You’ll recall those commands created the diskpart script file. Let’s then run the script file:
xp_cmdshell 'diskpart -s c:\diskpartcommands.txt'
Oh dear.
Those of you in the know might be thinking we could do “select disk 0” in our diskpart script and run it to trash the system volume. Fortunately, diskpart protects us from ourselves:
If there’s a saving grace, it’s that the system volume can’t be trashed using this simple method. But hey, we can still wreak havoc. Let’s say we have a database called SuckedIn, whose files live in F: drive:
sp_helpfile
You know where I’m going with this, right? If we don’t know the disk number, we can still use diskpart to identify the volume number, by changing the script to “list volume” to identify where F: drive lives:
Then we just change the diskpart script to:
select volume 2
clean all
And the result is data destruction:
Thankfully, we still can’t trash the system volume, but if someone trashed your database volumes, you’d be just as screwed. Don’t let this happen to you!
In case you passed out from boredom while reading my post, here’s the important bits that led to this disaster:
- The SQL service account is set as Local System
- The SQL proxy account is a privileged account, i.e. one that has Administrator rights over the local machine
- xp_cmdshell is enabled under these conditions.
That is all it takes to allow an otherwise regular, unprivileged user to HOSE YOUR SQL BOX! All of this could have been prevented by using best practice, which in case you fell asleep again, is very simple:
- Configure your SQL instance to use an unprivileged account for its service account
- For the love of all that’s holy, use an unprivileged account for the SQL proxy account
- Don’t enable xp_cmdshell unless you absolutely, positively have to.
To change the SQL Server service account, use the SQL Server Configuration Manager tool. You’ll need to restart the SQL Server service, so schedule an outage window to do this.
Once you’ve done this, use this script to configure the SQL Proxy account. Make sure this is an unprivileged account:
use master
go
EXEC sp_xp_cmdshell_proxy_account 'TEST\svc-sql-proxy','password'
This change takes effect straight away. No outage required.
Now when you run the whoami account as an administrator, you’ll get this result:
If you run it as an unprivileged user, you’ll get this result:
Now try running the diskpart script as the unprivileged user. It won’t work. YAY for system security!
Like this:
Like Loading...