SSH Private keys can be either encrypted or unencrypted. Unencrypted private keys are more risky because an attacker can use them without needing to unlock them with a password.
In particular, AWS instances are usually accessed by way of an SSH key pair generated by the AWS console. This key is not encrypted by default and it is possible that administrators simply save the key on their systems without encrypting it.
This artifact searches for private keys in the usual locations and also records if they are encrypted or not. Not all key types are supported
NOTE: In order to encrypt your private key run:
ssh-keygen -p -f my_private_key
Change the glob to /** if you would like to search the entire filesystem. Be aware, this is an expensive operation.
name: Linux.Ssh.PrivateKeys
description: |
SSH Private keys can be either encrypted or unencrypted. Unencrypted
private keys are more risky because an attacker can use them without
needing to unlock them with a password.
In particular, AWS instances are usually accessed by way of an SSH
key pair generated by the AWS console. This key is not encrypted by
default and it is possible that administrators simply save the key
on their systems without encrypting it.
This artifact searches for private keys in the usual locations and
also records if they are encrypted or not. Not all key types are
supported
NOTE: In order to encrypt your private key run:
```
ssh-keygen -p -f my_private_key
```
Change the glob to /** if you would like to search the entire filesystem.
Be aware, this is an expensive operation.
reference:
- https://attack.mitre.org/techniques/T1145/
- https://coolaj86.com/articles/the-openssh-private-key-format/
- https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html
precondition: SELECT OS From info() where OS = 'linux'
parameters:
- name: KeyGlobs
default: /home/*/.ssh/{*.pem,id_rsa,id_dsa}
- name: ExcludePathRegex
default: "^/(proc|sys|run|snap)"
type: regex
description: If this regex matches the path of any directory we do not even descend inside of it.
- name: LocalFilesystemOnly
default: Y
type: bool
description: |
When set, we stay on local attached filesystems including loop, attached disk, cdrom, device mapper, and excluding proc, nfs etc.
When set, it can miss keys in some Linux distros. If not sure, or run accross multiple distros, it is recommended to not set it.
sources:
- query: |
-- For new OpenSSH format
LET SSHProfile = '''[
["Header", 0, [
["Magic", 0, "String", {
"length": 100,
}],
["cipher_length", 15, "uint32b"],
["cipher", 19, "String", {
"length": "x=>x.cipher_length",
}]
]]]
'''
-- Device major numbers considered local. See Linux.Search.FileFinder
LET LocalDeviceMajor <= (NULL,
253, 7, 8, 9, 11, 65, 66, 67, 68, 69, 70,
71, 128, 129, 130, 131, 132, 133, 134, 135, 202, 253, 254, 259)
-- By default set to 'True', to only search local filesystems.
LET RecursionCallback = if(
condition=LocalFilesystemOnly,
then=if(condition=ExcludePathRegex,
then="x=>x.Data.DevMajor IN LocalDeviceMajor AND NOT x.OSPath =~ ExcludePathRegex",
else="x=>x.Data.DevMajor IN LocalDeviceMajor"),
else=if(condition=ExcludePathRegex,
then="x=>NOT x.OSPath =~ ExcludePathRegex",
else=""))
LET _Hits = SELECT OSPath,
read_file(filename=OSPath, length=20240) AS Data
FROM glob(globs=KeyGlobs, recursion_callback=RecursionCallback)
WHERE Size < 20000
LET Hits = SELECT OSPath, Data,
base64decode(
string=parse_string_with_regex(
string=Data,
regex="(?sm)KEY-----(.+)-----END").g1) || "" AS Decoded,
parse_string_with_regex(
string=Data,
regex="(BEGIN.* PRIVATE KEY)").g1 AS Header,
read_file(filename=OSPath.Dirname + (OSPath.Basename + ".pub") ) AS PublicKey
FROM _Hits
WHERE Header
LET OpenSSHKeyParser(OSPath, Decoded) = SELECT OSPath,
parse_binary(accessor="data", filename=Decoded,
profile=SSHProfile, struct="Header") AS Parsed
FROM scope()
-- Support both types of ssh keys dependingg on the header
SELECT * FROM foreach(row={SELECT * FROM Hits},
query={
SELECT * FROM switch(
a={
-- new format
SELECT OSPath,
Parsed.Magic AS KeyType,
Parsed.cipher AS Cipher,
Header, PublicKey
FROM OpenSSHKeyParser(OSPath= OSPath, Decoded=Decoded)
WHERE Header =~ "BEGIN OPENSSH PRIVATE KEY"
},
a2={
-- encrypted rsa key from e.g. putty
SELECT OSPath,
"PKCS8" AS KeyType,
parse_string_with_regex(string=Data,
regex="DEK-Info: ([-a-zA-Z0-9]+)").g1 AS Cipher,
Header, PublicKey
FROM scope()
WHERE Header =~ "BEGIN RSA PRIVATE KEY"
AND "Proc-Type: 4,ENCRYPTED" in Data
},
b={
-- unencrypted rsa key from e.g. AWS
SELECT OSPath,
"PKCS8" AS KeyType,
"none" AS Cipher,
Header, PublicKey
FROM scope()
WHERE Header =~ "BEGIN (RSA )?PRIVATE KEY"
},
c={
-- old format encrypted
SELECT OSPath,
"PKCS8" AS KeyType,
"PKCS#5" AS Cipher,
Header, PublicKey
FROM scope()
WHERE Header =~ "BEGIN ENCRYPTED PRIVATE KEY"
},
d={
-- catch all for unknown keys
SELECT OSPath,
"Unknown" AS KeyType,
"Unknown" AS Cipher,
Header, PublicKey
FROM scope()
})
})