Skip to content

Add scshell SMB execution method#1185

Open
pr1mu2 wants to merge 1 commit intoPennyw0rth:mainfrom
pr1mu2:feature/smb-scshell
Open

Add scshell SMB execution method#1185
pr1mu2 wants to merge 1 commit intoPennyw0rth:mainfrom
pr1mu2:feature/smb-scshell

Conversation

@pr1mu2
Copy link
Copy Markdown

@pr1mu2 pr1mu2 commented Apr 2, 2026

Description

This PR adds a new SMB command execution method: --exec-method scshell.

The implementation uses SCMR to temporarily update the target service binary path, start the service to execute the supplied command, and then restore the original service configuration.

This adds an additional execution method for SMB -x / -X usage. The method currently does not support command output retrieval, so it is intended to be used with --no-output.

Additional CLI options were added to support the technique:

  • --sc-service-name to choose the target service
  • --sc-no-cmd to avoid prepending cmd.exe /c

Tests were also added/updated:

  • parser regression coverage for --exec-method scshell
  • tests/e2e_commands.txt updated with SMB scshell examples

No new third-party dependencies are required.

Relevant sources / references:

  • Original scshell approach by @MrUn1k0d3r
  • Impacket SCMR / DCE-RPC service control patterns
  • Service configuration abuse via ChangeServiceConfig

AI disclosure:
This PR was AI-assisted using OpenAI Codex. AI was used for implementation assistance and test scaffolding. All code was reviewed, adjusted, and manually tested by me before submission.

Type of change

  • 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

Tested locally on Linux using Python 3.13.

Validation performed locally:

  • pytest tests/test_smb_cli.py tests/test_smb_database.py -q
  • python -m py_compile nxc/protocols/smb/scshell.py nxc/protocols/smb.py nxc/protocols/smb/proto_args.py tests/test_smb_cli.py
  • Installed from the branch with: pipx install --editable . --suffix=-scshelltest

Example review/test command:

  • netexec smb TARGET -u USER -p PASS -x whoami --exec-method scshell --sc-service-name RemoteRegistry --no-output

PowerShell variant:

  • netexec smb TARGET -u USER -p PASS -X whoami --exec-method scshell --sc-service-name RemoteRegistry --no-output

Notes for reviewers:

  • This execution method does not retrieve command output
  • The target must have a suitable existing service available
  • RemoteRegistry was used as the default/configurable service name for testing
  • No extra software or dependency changes are required
  • No GPO changes or registry changes are required specifically for this feature

Screenshots (if appropriate):

scshell scshell-sliver

Checklist:

  • 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)

@Mr-Un1k0d3r
Copy link
Copy Markdown

I love it. Thanks for adding SCShell concept to an already great tool.

@Dfte
Copy link
Copy Markdown
Contributor

Dfte commented Apr 3, 2026

@Mr-Un1k0d3r what an amazing technique omgggggggggggg
Also thanks @pr1mu2 for the PR, with that one and the psexec one I made a few days ago, we are definitely demolishing EDR's ahahahaha!

@NeffIsBack NeffIsBack added the enhancement New feature or request label Apr 3, 2026
@NeffIsBack
Copy link
Copy Markdown
Member

Thanks for the PR, definitely looks cool!

Copy link
Copy Markdown
Member

@NeffIsBack NeffIsBack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely looks cool!

A few thoughts I had while reading the code: As far as i can tell we alter the path of the service binary to our executable (cmd/powershell), what happens if it fails to revert that? Do we just broke that computer? Could potentially be dangerous.

Also, this looks very similar to #1174, with the difference being that we upload&install our own service binary (psexec), is that correct @Dfte? What would be the advantage of using psexec over simply this technique? Maybe #1174 is even a special case of this general technique here and we could make a module that uses this service class here to abuse it with psexec? Maybe I am missing something tho.

Comment on lines +11 to +26
def __init__(
self,
target,
username="",
password="",
domain="",
doKerberos=False,
aesKey=None,
remoteHost=None,
kdcHost=None,
hashes=None,
logger=None,
service_name="RemoteRegistry",
no_cmd=False,
wait_time=5,
):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put this into one line, similar to the other exec methods

Comment on lines +144 to +151
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell these NULL values are default values so we can just omit them.

Comment on lines +13 to +29
monkeypatch.setattr(
sys,
"argv",
[
"netexec",
"smb",
"127.0.0.1",
"-u",
"user",
"-p",
"pass",
"-x",
"whoami",
"--exec-method",
"scshell",
],
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one line please

cmd_exec_group.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results", type=int, default=10)
cmd_exec_group.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output. If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec")
cmd_exec_group.add_argument("--no-output", action="store_true", help="do not retrieve command output")
cmd_exec_group.add_argument("--sc-service-name", default="RemoteRegistry", help="service name to reconfigure temporarily when using --exec-method scshell")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is always difficult to add arguments that only work with one exec method. Usually, these exec methods should be replaceable and therefore not have different options that could break/result in different behaviour.

What would be use cases where you would alter the service name? For stealth reasons? What if we implement something like an array of possible services and then we just randomly chose one?

cmd_exec_group.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output. If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec")
cmd_exec_group.add_argument("--no-output", action="store_true", help="do not retrieve command output")
cmd_exec_group.add_argument("--sc-service-name", default="RemoteRegistry", help="service name to reconfigure temporarily when using --exec-method scshell")
cmd_exec_group.add_argument("--sc-no-cmd", action="store_true", help="when using --exec-method scshell, do not prepend cmd.exe /c to the command")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, but what would we use that for? E.g. executing powershell.exe instead of cmd? Maybe we should just change the way the commands are generated then, so we have this ability (to just call powershell.exe without cmd.exe) in every exec-method.

Comment on lines +137 to +143
scmr.hRChangeServiceConfigW(
self.__scmr,
self.__serviceHandle,
scmr.SERVICE_NO_CHANGE,
scmr.SERVICE_DEMAND_START,
scmr.SERVICE_ERROR_IGNORE,
final_command,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One line please

Comment on lines +174 to +189
scmr.hRChangeServiceConfigW(
self.__scmr,
self.__serviceHandle,
scmr.SERVICE_NO_CHANGE,
self.__startType,
self.__errorControl,
self.__binaryPath,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

Comment on lines +195 to +198
try:
scmr.hRCloseServiceHandle(self.__scmr, self.__serviceHandle)
except Exception:
pass
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please no empty exception handling. Either contextlib.suppress or we should alert the user/log a debug message if there is something useful to log.

Comment on lines +203 to +214
try:
scmr.hRCloseServiceHandle(self.__scmr, self.__scHandle)
except Exception:
pass
finally:
self.__scHandle = None

if self.__scmr:
try:
self.__scmr.disconnect()
except Exception:
pass
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above, probably all of that should just be:

with contextlib.suppress:
  scmr.closeHandle()
  scmr.close()
  scmr.disconnect()

elif method == "scshell":
try:
exec_method = SCSHELL(
self.host if not self.kerberos else self.hostname + "." + self.domain,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be self.remoteName

@pr1mu2
Copy link
Copy Markdown
Author

pr1mu2 commented Apr 3, 2026

Thanks a lot for the review - these are very fair points.

For the restore concern: the current code already attempts to restore the original service configuration in a finally block, but you are absolutely right that a failed revert could still leave the service in a bad state. I want to harden that before merge by adding stricter restore handling and explicit post-restore verification.

On #1174: I had the same impression while working on this. scshell feels like the more general primitive (“temporarily reconfigure an existing service and execute through it”), whereas the psexec-style flow feels like a specific variant built on top of the same idea.

I am happy to adapt this in whichever direction fits the project best:

Regarding the service-name option: I agree that method-specific arguments should be treated carefully. The main reason I exposed it here is reliability, not just stealth. This technique depends more on the chosen service than methods like wmiexec or atexec, and not every existing service is a good candidate across different environments and Windows versions.

That said, I think a curated internal candidate list would be a good follow-up improvement if that is the preferred direction.

If so, I can rework this PR to focus on:

  1. stronger restore safety and verification
  2. reducing/removing manual service selection
  3. clarifying the relationship to Add the psexec execution method #1174, or extracting shared logic

@Mr-Un1k0d3r
Copy link
Copy Markdown

Mr-Un1k0d3r commented Apr 3, 2026

Based on my understanding of pull request #1174, I would consider these to be two separate options, as 1174 still relies on the presence of PsExec, which this pull request does not use. The original SCShell (https://github.com/Mr-Un1k0d3r/SCShell) was designed as a truly fileless lateral movement technique, as it does not require dropping any files, unlike using PsExec as the execution channel.

@NeffIsBack
Copy link
Copy Markdown
Member

Okay yeah my idea was that maybe is #1174 is a specific version of the general idea here in this PR and we therefore should do #1174 as a module that uses this exec-method technique with psexec. Similar to the schtask_as module uses atexec's logic for doing special stuff. Thoughts? @Dfte what do you think?

Regarding https://github.com/Pennyw0rth/NetExec/pull/1185/changes#r3033220153, we talked internally a bit about cmd/powershell and general command invocation and the TLDR was pretty much that we should rip this out of the execution methods and generalize it in the protocol itself, so that you directly have the option to invoke cmd/powershell or your own binary.

  1. reducing/removing manual service selection

Yeah I think that would be good. If we can generalize this so that it works without having to enumerate/check why which service work on which host that would be great (if possible).

@Dfte
Copy link
Copy Markdown
Contributor

Dfte commented Apr 12, 2026

Ayo, I wouldn't merge both psexec and scshell because it makes things harder for a user to know exactly what's happening. Also, I kinda expect psexec to be the default opsec option for command execution so... (eyes)
I'd keep these two exec methods even tho that means dupplicated code (which is already the case with smbexec).

Concerning the hardcoded cmd/powershell binary, I'll be looking at this after finishing some PR's of mine :)!

@NeffIsBack
Copy link
Copy Markdown
Member

Sounds good, then let's keep them separate👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants