This post details some proof-of-concept changes to SharpHound's output functionality to avoid forensic artefacts. Namely, doing everything in-memory, and avoiding ever touching disk. This also leverages the fantastic recent changes to BOF.NET to support sending memory buffers to Cobalt Strike as pseudo file downloads. For this, two new (non-official) SharpHound flags are introduced: --MemoryOnlyJSON
and --MemoryOnlyZIP
(the latter having a dependency on BOF.NET). Code here.
Existing evasion features and artefacts are also discussed for the blue teamers amongst us.
Update December 2021: The --MemoryOnlyZIP
function has been renamed to --MemoryOnlyZIPToBOFNET
. In addition, I've added the --MemoryOnlyZIPToSectionObject function. This saves the final ZIP file to a section object rather than returning it directly with BOF.NET. The benefit of this is that bofnet_jobassembly
can now be used to execute SharpHound and get real-time updates while keeping everything in-memory. Of course you need to download the section object as a file through Beacon (e.g., with a BOF), but I've left the coding of that as a task for the reader (hint: check the nanodump implementation of download_file
).
Important usage caveats and warnings if you don't want to read the whole thing:
- Consider it proof-of-concept as it's currently only been tested in small domain environments. No promises that I haven't borked existing functionality.
- With BOF.NET you must currently only execute it with
bofnet_execute
rather thanbofnet_job
. The latter throws an error duringDownloadFile()
usage, and may kill your session. As explained by CCob here, this is due to the lifetime of the BOF runtime. Bit unfortunate, but still great that we have some in-memory file upload functionality at all. bofnet_execute
stops your Beacon calling back until execution finishes. It doesn't mean your shell has died if it seems like it's no longer calling back, it's just doing something, and can't call back until its finished due to the limitation of a BOF.- You need to download and compile BOF.NET. Don't use the pre-compiled binaries. Use Linux for compilation to make your life easier.
SharpHound Artefacts We Want to Avoid (Existing Features)
There are three notable on-disk artefacts from SharpHound use:
- A cache file is used by default which speeds up collection.
- Artefact: By default SharpHound takes the machine ID from the registry (a GUID), base64 encodes it, and appends ".bin" to make up the filename.
- Evasion: SharpHound has functionality to avoid this using the
--NoSaveCache
flag which keeps everything in-memory, or--CacheFileName
which stores a file on-disk using a custom name.
- JSON files with information about the various Active Directory objects (users, computers, etc).
- Artefact: Six files are created for each of the target object types. The default filename is
<timestamp>_<type>.json
. For example,20210717235450_users.json
. The six files are deleted once the ZIP (see (3)) is created. - Evasion: SharpHound has functionality to mitigate (but not avoid) this using partially randomised filenames using the
--RandomizedFilenames
flag. In this case, the default filename is<timestamp>_<8-random-chars>.<3-random-chars>
. For example,20210717235450_20r0rfs31.1va
.
- Artefact: Six files are created for each of the target object types. The default filename is
- The ZIP file that aggregates the files from (2).
- Artefact: During standard execution one ZIP file is created. The default filename is
timestamp_BloodHound.zip
. For example,20210717235450_BloodHound.zip
. If the--Loop
paremeter is used, you'll get multiple of these files, before they're aggregated into one ZIP file. In this case, the default filename istimestamp_BloodHoundLoopResults.zip
. For example,20210717235450_BloodHoundLoopResults.zip
. - Evasion: SharpHound has functionality to mitigate (but not avoid) this using a partially user-controlled filename using the
--ZipFileName
flag. In this case, the default filename is<timestamp>_<chosen-ZipFileName>.zip
. For example,20210717235450_my.file.zip
. It's important to note that this is a always a ".zip" extension.
- Artefact: During standard execution one ZIP file is created. The default filename is
New Features for Avoiding Artefacts
--MemoryOnlyJSON
This fully mitigates the artefacts created by the six JSON files. Instead of being stored on disk, they're stored in memory (in a C# MemoryStream).
Once collection has completed they're zipped again entirely in-memory into another MemoryStream. This provides some flexibility. If you don't want to use BOF.NET and instead want to stick with something like execute-assembly
you can still use this flag to reduce artefacts. In this case, the MemoryStream for the ZIP just gets stored on disk as a file.
With respect to the code, I've pushed this to my fork and created a pull request for it.
--MemoryOnlyZIP
This fully mitigates ZIP files being stored on-disk post-collection. Instead of being stored on-disk, the MemoryStream containing the ZIP file is forwarded back to your Cobalt Strike Team Server using the DownloadFile()
functionality that was recently added to BOF.NET. Due to this, there's an obvious dependency for execution through BOF.NET.
This also works with the --Loop
functionality within SharpHound. ZIPs are tracked in-memory, before the combined ZIP is created and returned to Cobalt Strike.
In terms of the code, I've pushed this to a branch on my fork. There's no pull request for this, as it's not necessarily something that belongs in the main tool due to the dependency on another tool. This branch contains the code for --MemoryOnlyJSON
and --MemoryOnlyZIP
.
Usage
To execute SharpHound in this manner, you need to load BOF.NET into your Cobalt Strike client, and then execute the modified SharpHound code.
For BOF.NET you must compile it yourself. The pre-compiled binaries on the official repository don't currently support the DownloadFile()
API. My word of advice is to do this on Linux - I spent quite a bit of time trying to get it working on Windows without success, but it's quite straight forward on Linux. The CNA script for this should be then loaded into your Cobalt Strike client.
You should also pull and compile SharpHound from my fork.
From here, you can execute as follows:
bofnet_init
bofnet_load /path/to/SharpHound.exe
bofnet_execute SharpHound3.BOFNET -c All --NoSaveCache --MemoryOnlyJSON --MemoryOnlyZIP
Execution with BOF.NET must also use bofnet_execute
which blocks Beacon until execution is complete. Real-time updates with bofnet_job
is not supported.
The above command should create no file creation events. The existing flag --NoSaveCache
prevents cache file creation, -MemoryOnlyJSON
stores JSON files in-memory, then --MemoryOnlyZIP
creates the ZIP archive in-memory and forwards it to Cobalt Strike without saving it to disk.