Skip to content

Comments

Add ssh_keys module to extract SSH keys from Windows registry#1112

Open
mverschu wants to merge 2 commits intoPennyw0rth:mainfrom
mverschu:feature/ssh-keys-module-upstream
Open

Add ssh_keys module to extract SSH keys from Windows registry#1112
mverschu wants to merge 2 commits intoPennyw0rth:mainfrom
mverschu:feature/ssh-keys-module-upstream

Conversation

@mverschu
Copy link

Description

This PR adds the ssh_keys module over SMB which extracts unencrypted private SSH keys from Windows OpenSSH ssh-agent registry entries stored in HKCU:\Software\OpenSSH\Agent\Keys.

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)

Note: Requires pyasn1 package (pip install pyasn1)

Testing Requirements:

  • Target: Windows machine with OpenSSH ssh-agent configured and keys added
  • Authentication: Valid administrator credentials with access to HKCU registry
  • Python Version: Python 3.x (tested with Python 3.10+)
  • OS: Linux/macOS/Windows (tested on Linux)
  • Dependencies: pyasn1 package must be installed

Test Commands:

Extract SSH keys (display only):

netexec smb TARGET_HOST -u ADMIN_USER -p ADMIN_PASSWORD -M ssh_keys

Extract SSH keys and save to file:

netexec smb TARGET_HOST -u ADMIN_USER -p ADMIN_PASSWORD -M ssh_keys -o OUTPUTFILE=extracted_keys.txt

Extract SSH keys and save to file with tilde expansion:

netexec smb TARGET_HOST -u ADMIN_USER -p ADMIN_PASSWORD -M ssh_keys -o OUTPUTFILE=~/id_rsa_test

Expected Behavior:

  • Without OUTPUTFILE: Displays extracted SSH private keys in PEM format to console with centered header
  • With OUTPUTFILE: Saves extracted keys to specified file with comment headers for each key
  • Extracts keys from HKCU:\Software\OpenSSH\Agent\Keys\ registry path
  • Decrypts DPAPI-protected key data using PowerShell on remote system
  • Reconstructs RSA private keys from decrypted binary data
  • Handles multiple keys if present

Verification:

After running the module, verify the extracted keys:

  • Keys are displayed in standard PEM format (-----BEGIN RSA PRIVATE KEY-----)
  • Each key includes a comment header showing the original key comment
  • Saved file (if OUTPUTFILE specified) contains all keys with comment headers
  • Keys can be used directly with SSH clients

Notes:

  • Requires administrative privileges (uses on_admin_login hook)
  • Keys must have been added to Windows OpenSSH ssh-agent (ssh-add)
  • DPAPI decryption requires the same user context (CurrentUser scope)
  • Module automatically handles registry enumeration and key extraction
  • Supports tilde (~) expansion in OUTPUTFILE path
  • Based on method from windows_sshagent_extract by @ropnop
  • Original RSA key reconstruction logic credit: soleblaze (sshkey-grab)

Screenshots (if appropriate):

Example Output:

SSH_KEYS    192.168.122.185 445    WIN-N010TGQ2FQ5  [*] Extracting SSH keys from registry...
SSH_KEYS    192.168.122.185 445    WIN-N010TGQ2FQ5  [+] Found 1 SSH key(s) in registry
SSH_KEYS    192.168.122.185 445    WIN-N010TGQ2FQ5  [+] Successfully extracted key: administrator@WIN-N010TGQ2FQ5
SSH_KEYS    192.168.122.185 445    WIN-N010TGQ2FQ5  [*] 
================================================================================
SSH_KEYS    192.168.122.185 445    WIN-N010TGQ2FQ5  [*]        Extracted SSH Private Keys:
SSH_KEYS    192.168.122.185 445    WIN-N010TGQ2FQ5  [*] ================================================================================
SSH_KEYS    192.168.122.185 445    WIN-N010TGQ2FQ5  [+] Key Comment: administrator@WIN-N010TGQ2FQ5
SSH_KEYS    192.168.122.185 445    WIN-N010TGQ2FQ5  -----BEGIN RSA PRIVATE KEY-----
MIIFdwIBAAKCBVsA0VQf2bAADIvQ6zbV71HaujwJ4FCnbl6X9JkhY2PLEamVTqed
...
-----END RSA PRIVATE KEY-----
SSH_KEYS    192.168.122.185 445    WIN-N010TGQ2FQ5  [+] Saved 1 SSH key(s) to ~/id_rsa_test

Checklist:

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

  • I have ran Ruff against my changes (via poetry: poetry run python -m ruff check . --preview, 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)
  • New and existing e2e tests pass locally with my changes
  • 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 performed a self-review of my own code
  • 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)

@NeffIsBack
Copy link
Member

Thanks for the PR, looks cool!

Parts of it look AI generated, could you describe to which extend you have used AI and what parts are done by human hand?

@NeffIsBack NeffIsBack added the enhancement New feature or request label Feb 18, 2026
@mverschu
Copy link
Author

Hi that is completely true, I use cursor as my IDE, it's able to take my current tooling and transform this in to a NetExec module. I use this all the time for several projects, ofcourse understanding the code and pointing it in to the direction I want it to behave.

I'm not a software developer, can read code well. Enough information?

@NeffIsBack
Copy link
Member

Hi that is completely true, I use cursor as my IDE, it's able to take my current tooling and transform this in to a NetExec module. I use this all the time for several projects, ofcourse understanding the code and pointing it in to the direction I want it to behave.

I'm not a software developer, can read code well. Enough information?

Yes thank you. The reason I am asking is that AI tends to be very verbose, recreates code (by its nature) that already exists and does other very weird things (also see #1109).

The PR looks not too bad tho and adds a nice technique!

Copy link
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.

I am definitely not an expert on dpapi but we have some implementations to decrypt stuff from remote. Possibly looking into that, so that command execution is not required would probably be nice as well. If you have the time feel free to look into it.

@@ -0,0 +1,396 @@
#!/usr/bin/env python3
Copy link
Member

Choose a reason for hiding this comment

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

Please remove, isn't needed.

Comment on lines 2 to 15
r"""
Module to extract unencrypted private SSH keys from Windows OpenSSH ssh-agent registry entries.

When adding private keys to ssh-agent, Windows protects the private keys with DPAPI and stores
them as registry entries under HKCU:\Software\OpenSSH\Agent\Keys

With elevated privileges, it is possible to pull out the binary blobs from the registry and
unprotect them using DPAPI. These blobs can then be restructured into the original, unencrypted
private RSA keys.

Original Python implementation credit: soleblaze
https://github.com/NetSPI/sshkey-grab/blob/master/parse_mem.py
"""

Copy link
Member

Choose a reason for hiding this comment

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

Please move/merge this with the class doc string.

Comment on lines 23 to 28
try:
from pyasn1.type import univ
from pyasn1.codec.der import encoder
HAS_PYASN1 = True
except ImportError:
HAS_PYASN1 = False
Copy link
Member

Choose a reason for hiding this comment

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

Pyasn1 is a dependency, so just assume it exists

Comment on lines 53 to 55
if not HAS_PYASN1:
context.log.fail("pyasn1 package is required. Install it with: pip install pyasn1")
return
Copy link
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 393 to 395
def _get_int(self, buf):
"""Convert bytes to big-endian integer."""
return int.from_bytes(buf, byteorder='big')
Copy link
Member

Choose a reason for hiding this comment

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

I don't think that has to be a function as it just executes one line

return output
except Exception as e:
context.log.debug(f"Error executing PowerShell decryption: {e}")
import traceback
Copy link
Member

Choose a reason for hiding this comment

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

Please do Imports at the top of the file


except Exception as e:
context.log.fail(f"Error extracting SSH keys: {e}")
import traceback
Copy link
Member

Choose a reason for hiding this comment

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

Please move imports to the top of the file

@mverschu
Copy link
Author

mverschu commented Feb 18, 2026

Understood, I tried to take as much information from already implemented modules.

@Marshall-Hallenbeck
Copy link
Collaborator

The code this is based on from NetSPI (https://github.com/NetSPI/sshkey-grab/blob/master/parse_mem.py) is 11 years old.

@zblurx would this be useful to implement inside DPloot itself or nah?

@zblurx
Copy link
Collaborator

zblurx commented Feb 18, 2026

I don't think it will be usefull to implement it in dploot, imo nxc module is the way to go

- Fixed all review comments.
- Added remote DPAPI dumping looking at wifi.py module.
- Removed local DPAPI dumping for stealth.
- Formatted the output the be more nicely looking using intend.

Signed-off-by: B3AR <69352107+mverschu@users.noreply.github.com>
@mverschu
Copy link
Author

Hi there,

  • Fixed all review comments.
  • Added remote DPAPI dumping looking at wifi.py module.
  • Removed local DPAPI dumping for stealth.
  • Formatted the output the be more nicely looking using intend.
image

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