Linux.Ssh.PrivateKeys

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()
          })
      })