Home » Exfiltrate data from SQL Server 2025 with sp_invoke_external_rest_endpoint

Exfiltrate data from SQL Server 2025 with sp_invoke_external_rest_endpoint

by Vlad Drumea
0 comments 16 minutes read

In this post I cover how an attacker can take advantage of SQL Server 2025’s sp_invoke_external_rest_endpoint to exfiltrate data from a compromised instance.

In my previous post I used sp_invoke_external_rest_endpoint to get some data about internet-exposed SQL Server instances.
While doing that, I realized that, similarly to xp_cmdshell, SQL Server 2025’s sp_invoke_external_rest_endpoint can be used by an attacker to exfiltrate data from SQL Server.

What is sp_invoke_external_rest_endpoint?

In short, sp_invoke_external_rest_endpoint has been introduced in SQL Server 2025.
It allows users/developers to interact with HTTPS REST API endpoints from within SQL Server itself.

Why is it a good candidate for data exfiltration?

sp_invoke_external_rest_endpoint can both receive and send data to/from entities outside of SQL Server which is already a good starting point.
But it does have some additional features that make it a perfect tool for this:

  • It can handle up to 100MB of UTF-8 encoded data through its @payload parameter.
  • The payload can contain pretty much anything as long as it’s valid JSON.
  • Don’t have a REST API endpoint at your disposal?
    No problem. If you have a basic web server and can read the access log, you can use the @url parameter’s generous 8KB (aka NVARCHAR(4000)) worth of wiggle room.
  • It doesn’t trigger the same alarms as xp_cmdshell.
    Many shops that use xp_cmdshell have locked it down properly for safe usage and audit its usage heavily, but sp_invoke_external_rest_endpoint is still under most people’s radar.

That last point is the exact reason why folks need to start paying attention to sp_invoke_external_rest_endpoint.
But what better way to prove why it can be dangerous if not treated properly than by showing you?

Requirements

In order to be able to use the sp_invoke_external_rest_endpoint stored procedure, the following requirements must be met:

  1. The “external rest endpoint enabled” instance level configuration needs to be enabled (set to 1).
    This enables or disables invocations of external REST endpoints.
  2. The login executing sp_invoke_external_rest_endpoint must be granted the EXECUTE ANY EXTERNAL ENDPOINT permission.
  3. And, obviously, the same login needs read permission on the table(s) it wants to exfiltrate the data from.
  4. The target web server/API endpoint needs to have valid certificate with a verifiable chain.

Enabling external rest endpoint

I’m doing this on a SQL Server 2025 container running on my QNAP NAS.

First, I need to enable the external rest endpoint configuration option.

Disclaimer

Before proceeding, note that both the SQL Server instance and the remote web server are owned or paid for by me.
If you want to follow along with the demos, make sure you do so against infrastructure that you own or have permission to do stuff to.
Do not go poking around at other people’s infrastructure without explicit (ideally written) permission.

Basic data exfiltration via sp_invoke_external_rest_endpoint’s @url parameter

This is the simplest version, it doesn’t need handling result sets as JSON or even a REST API endpoint.
All you need is a web server with a valid certificate and some creative T-SQL.

For the demo, I’ll just use my blog’s host as the remote server and a method similar to the one used in my post about securing SQL Server’s service account.

The above T-SQL loops through every SQL login in sys.sql_logins and:

  1. Joins the login name with the string representation of the password hash with a \ between them.
  2. Appends the string from step 2 to a URL that doesn’t exist on my blog.
  3. Uses sp_invoke_external_rest_endpoint to call the resulting URL with a GET request
  4. Returns the status code and result (just for demo purposes).

Here’s an example of how one of those URLs looks like:

https://vladdba.com/this-is-a-404/test_impersonate_any/0x030075EE80E6[redacted]4468B1EF003

This is the output in SSMS:

And then I refresh my web server’s access log and filter for “this-is-a-404”:

With this, an attacker can then use the same methods shown in my post about cracking SQL Server 2025 login hashes offline to potentially find a password for one of the more privileged logins on the instance.

What about more complex methods?

More complex data exfiltration via sp_invoke_external_rest_endpoint’s @payload parameter

This one does require a REST API endpoint and some additional setup on my web server side.

The following three steps are all on the WordPress side:

  1. First, I create a low-privilege WordPress user account that only has the Subscriber role.
  2. Then, I temporarily enable Application Passwords.
    I don’t ever use WP App Passwords so I generally keep them disabled.
  3. Finally, I create a WP App Password for the low-privilege user.

The logic

What I want to do is leverage WordPress’s REST API and, using the low-privilege user’s WP App Password, create comments on one of my posts with data exfiltrated from SQL Server.

Since WP has fairly generous comment space (way more than what the @url parameter can handle) I can easily use the @payload parameter.
Maybe not up to its full limit, but enough to not have to use a cursor for 6 logins.

The actual demo

Here’s what the above T-SQL does:

  1. Base64-encodes* the WordPress credentials (username:app_password) for the Basic Auth header.
    The encoding is done with SQL Server 2025’s shiny and new Base64-encoding function: BASE64_ENCODE
  2. Builds the request headers as a JSON object with the Authorization and Content-Type fields.
  3. Queries sys.sql_logins and converts the results to JSON using FOR JSON PATH, storing the output in a variable.
  4. Uses JSON_MODIFY to inject the JSON login data into the content field of a WordPress comment payload.
    This handles string escaping automatically, so login names with special characters don’t break the JSON.
  5. Sends a POST request to the WordPress REST API comments endpoint with the assembled payload.
  6. Returns the HTTP status code from the response (201 means the comment was created successfully).

The result in SSMS is a 201 status code (again, optional).

SSMS result showing the status code and JSON response

And when I check on my blog, the comment got sent to spam, but I’m not picky.

Note the body of the comment containing the login names and their respective password hashes in JSON format.

This is neat, but maybe it’s time to level up.

Exfiltrate files using SQL Server and sp_invoke_external_rest_endpoint

Things get a lot more interesting once you realize that you can throw OPENROWSET into the mix to read files from disk and pass them to the payload parameter.

Here you’re limited by the level of access the SQL Server service account has.

Basic text file

For this I created a .txt file in the directory that’s mapped to container’s file system and to which the SQL Server instance on the container has access.

And, from SSMS, I execute the following T-SQL:

The steps are similar to the previous demo, but with one difference:

  1. Instead of querying a system view, it reads a text file from disk using OPENROWSET(BULK..., SINGLE_CLOB).
    This returns the file contents as a single large text value.
  2. The file contents are injected into the comment payload via JSON_MODIFY, same as before.
  3. The POST request sends the file contents as a WordPress comment.

Checking the comments, it landed in spam again, but it does contain the exact contents of the text file.

Screenshot of the comment with the content of the text file exfiltrated from sql server 2025 via sp_invoke_external_rest_endpoint

Pretty neat so far, but what if we want other types of files?

Non-text files

Here’s where VARBINARY and Base64 encoding come into play.

Using my pretty much non-existing graphic design skills, I made the following PNG file.

A white background with black text saying This is just a PNG

And I save it in the same location as the previous test file.

With the PNG file in place, I run the following T-SQL:

The only output I get this time is the contents of the @b64_string parameter, because I want to check it against the body of the resulting comment.

SSMS result set showing a part of the Base64-encoded string

For reference, the start and end of the Base64-encoded string look like this:
iVBORw0KGgoAAAANSUh............YAAAAASUVORK5CYII=

Checking again my blog’s spam comments I can see the Base64-encoded bites of the image exfiltrated from SQL Server using sp_invoke_external_rest_endpoint.

WP spam comment showing the comment with the base64 string exfiltrated from sql server with sp_invoke_external_rest_endpoint

Now for the moment of truth, can I reconstruct the image?

For this I had to temporarily bump up the permissions of my test WP user so that it’s able to view spam comments.
After that I can run the following PowerShell code:

Windows Terminal window with the above PS code and showing the resulting file in the C:\temp directory

So far, so good. The ultimate test is checking if the hash of the reconstructed file matches the hash of the original.

Windows Terminal showing the result of the above PS code and the hashes matching

And we have a match.
The full attack chain was verified end-to-end. The SHA256 hash of the exfiltrated file matched the hash of the reconstructed file perfectly.
No xp_cmdshell, no external tools, just T-SQL and HTTPS.

Plus the image renders perfectly and I’m uploading it here too.

resulting png image

I’d say this counts as another example of successful data exfiltration via sp_invoke_external_rest_endpoint.

What about larger files?

The technique is the same, all that’s needed is some extra T-SQL work to handle splitting the contents of the @b64_string parameter into more manageable chunks.
In this case they’ll need to be small enough to not hit the 100MB size limit of the @payload parameter when combined with the rest of the payload data.
But they also have to be small enough to not hit any WP comment or other REST API endpoint size limits.
So, whichever is the smallest size, that’s the one you’ll have to work it.

But wait! There’s more.

If you get really creative with something like SQL Server’s startup stored procedures you could potentially have a stored procedure that executes at startup, loops every x seconds/minutes/hours and does a bunch of things with sp_invoke_external_rest_endpoint and the response it might get.

Just off the top of my head some example would be:

  1. Have it send any new SQL login that gets created.
  2. Or have it send any new database backup files.
  3. Issue a GET request for a specific page periodically, and, when that request returns a 200 code, then do something else.
    Either create a login or other database objects with the information provided as part of the response.
  4. And even have a TRY CATCH block that re-enables the “external rest endpoint enabled” server option if it was disabled at some point and caused sp_invoke_external_rest_endpoint to error out.

Given that startup stored procedures run under the sa login, there are a lot of possibilities here.

This means that, with the right permissions and some creative T-SQL, you only need access to the instance once and any subsequent change can be pushed through the endpoint that you have control over.

Mitigation

Disable it if you don’t use it

This is just common sense, don’t enable features you’re not planning on using.
And, if you did enable it just to test something, disable it when done.

Follow the principle of least privilege

Modifying instance level configuration requires membership in either sysadmin or serveradmin fixed server roles, or the ALTER SETTINGS permission.
Executing sp_invoke_external_rest_endpoint requires the EXECUTE ANY EXTERNAL ENDPOINT permission.

So, don’t add every login to high privilege roles, and don’t grant them permissions they don’t absolutely require.
And, if you have logins that need to be able to execute sp_invoke_external_rest_endpoint, make sure they also don’t have permission to re-enable the “external rest endpoint enabled” server option if you ever have to disable it as part of an emergency response.

Make sure the SQL Server service account doesn’t have elevated privileges.

Network segmentation and firewall rules

Ideally, the SQL Server host won’t have access to the “outside world” and should only be able to interact with REST API endpoints that are in the same network.

If you REALLY NEED to use external endpoints, restrict outbound traffic from the SQL Server host using firewall rules.
Add only known endpoints to the allow list.
Just be aware that you’re making an assumption here about that external endpoint never being compromised and your data always being safe at the receiving end of your REST API call.

Monitor and audit

If you don’t have “external rest endpoint enabled” enabled and will never need to enable it, then ensure you monitor for configuration changes and get alerted if this gets enabled.

If you do need to work with REST API endpoints from SQL Server, set up SQL Server Audit for executions of sp_invoke_external_rest_endpoint the same way you (hopefully) do for xp_cmdshell.

Audit and monitor for OPENROWSET usage, especially when it’s targeting unexpected locations.

Periodically review logins and their permissions.

Monitor for all egress traffic from your SQL Server host and alert for anything that doesn’t match your expected/known baseline.

The upcoming release of PSBlitz will also include additional sp_invoke_external_rest_endpoint related security checks.

Conclusion

SQL Server 2025’s sp_invoke_external_rest_endpoint was designed for developer convenience, but it also creates a direct exfiltration channel between SQL Server and the outside world.
Like xp_cmdshell before it, this procedure deserves the same level of scrutiny and hardening.

I hope the demos in this post showed how easy it can be to exfiltrate data, including files, from SQL Server using sp_invoke_external_rest_endpoint.

You may also like

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

This site uses Akismet to reduce spam. Learn how your comment data is processed.