Fetching SharpHound data entirely in-memory (no dropped ZIP or JSON files) using BOF.NET and Cobalt Strike

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:

  1. 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.
  2. With BOF.NET you must currently only execute it with bofnet_execute rather than bofnet_job. The latter throws an error during DownloadFile() 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.
  3. 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.
  4. 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:

  1. 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.
  2. 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.
  3. 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 is timestamp_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.
Output ZIP file (3) and cache file (1) post execution
Default JSON filenames (2) generated mid-execution
Pseudo-randomised JSON filenames (2) generated mid-execution

New Features for Avoiding Artefacts


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.


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.


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_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.

Leave a Reply