From 351fb51d4aa65b3e43335fcc5891d6bd54d6078e Mon Sep 17 00:00:00 2001 From: Pradeep krishnamurthy Date: Wed, 18 May 2022 12:21:27 +0200 Subject: [PATCH] ssh2john.py: handle multiple keys and index the output --- run/ssh2john.py | 273 +++++++++++++++++++++++++----------------------- 1 file changed, 144 insertions(+), 129 deletions(-) diff --git a/run/ssh2john.py b/run/ssh2john.py index c49ed99838e..9feec7722de 100755 --- a/run/ssh2john.py +++ b/run/ssh2john.py @@ -45,6 +45,27 @@ 'AES-256-CTR': {'cipher': AES_256, 'keysize': 32, 'blocksize': 16, 'mode': "AES.MODE_CTR"}, } +def get_all_tags_ktypes(lines): + tags = [] + ktypes = [] + for line in lines: + if "BEGIN RSA PRIVATE" in line: + tags.append("RSA") + ktypes.append(0) + elif "BEGIN DSA PRIVATE KEY" in line: + tags.append("DSA") + ktypes.append(1) + # new private key format for OpenSSH (automatically enabled for + # keys using ed25519 signatures), ed25519 stuff is not supported + # yet! + elif "BEGIN OPENSSH PRIVATE KEY" in line: + tags.append("OPENSSH") + ktypes.append(2) # bcrypt pbkdf + aes-256-cbc + elif "BEGIN EC PRIVATE KEY" in line: + tags.append("EC") + ktypes.append(3) + + return tags, ktypes def read_private_key(filename): """ @@ -60,146 +81,140 @@ def read_private_key(filename): return lines = f.readlines() - all_lines = ''.join(lines) - ktype = -1 - tag = None - if "BEGIN RSA PRIVATE" in all_lines: - tag = "RSA" - ktype = 0 - elif "-----BEGIN OPENSSH PRIVATE KEY-----" in all_lines: - # new private key format for OpenSSH (automatically enabled for - # keys using ed25519 signatures), ed25519 stuff is not supported - # yet! - ktype = 2 # bcrypt pbkdf + aes-256-cbc - tag = "OPENSSH" - elif "-----BEGIN DSA PRIVATE KEY-----" in all_lines: - ktype = 1 - tag = "DSA" - elif "-----BEGIN EC PRIVATE KEY-----" in all_lines: - ktype = 3 - tag = "EC" - - if not tag: - sys.stderr.write("[%s] couldn't parse keyfile\n" % filename) - return - start = 0 - while (start < len(lines)) and (lines[start].strip() != '-----BEGIN ' + tag + ' PRIVATE KEY-----'): - start += 1 - if start >= len(lines): - sys.stderr.write("%s is not a valid private key file\n" % f.name) + num_processed_keys = 0 + tags, ktypes = get_all_tags_ktypes(lines) + if not tags: + sys.stderr.write("[%s] couldn't parse keyfile\n" % filename) return - # parse any headers first - headers = {} - start += 1 - while start < len(lines): - l = lines[start].split(': ') - if len(l) == 1: + for tag, ktype in zip(tags, ktypes): + join_lines = ''.join(lines[start:]) + if not join_lines: break - headers[l[0].lower()] = l[1].strip() - start += 1 - # find end - end = start - while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)): - end += 1 - # if we trudged to the end of the file, just try to cope. - try: - data = ''.join(lines[start:end]).encode() - data = base64.b64decode(data) - except base64.binascii.Error: - e = sys.exc_info()[1] - raise Exception('base64 decoding error: ' + str(e)) - - if ktype != 2: - if 'proc-type' not in headers: # unencrypted key file? - sys.stderr.write("%s has no password!\n" % f.name) + while (start < len(lines)) and (lines[start].strip() != '-----BEGIN ' + tag + ' PRIVATE KEY-----'): + start += 1 + if start >= len(lines): + sys.stderr.write("%s is not a valid private key file\n" % f.name) return + # parse any headers first + headers = {} + start += 1 + while start < len(lines): + l = lines[start].split(': ') + if len(l) == 1: + break + headers[l[0].lower()] = l[1].strip() + start += 1 + # find end + end = start + while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)): + end += 1 + # if we trudged to the end of the file, just try to cope. try: - encryption_type, saltstr = headers['dek-info'].split(',') - except: - raise Exception('Can\'t parse DEK-info in private key file') - - if encryption_type not in CIPHER_TABLE: - raise Exception('Unknown private key cipher "%s"' % encryption_type) - - - if ktype == 2: # bcrypt_pbkdf format, see "sshkey_private_to_blob2" in sshkey.c - AUTH_MAGIC = b"openssh-key-v1" - salt_length = 16 # fixed value in sshkey.c - # find offset to salt - offset = 0 - if not data.startswith(AUTH_MAGIC): - raise Exception('Missing AUTH_MAGIC!') - offset = offset + len(AUTH_MAGIC) + 1 # sizeof(AUTH_MAGIC) - length = unpack(">I", data[offset:offset+4])[0] # ciphername length - offset = offset + 4 - cipher_name = data[offset:offset+length].decode('ascii') - if cipher_name == 'none': - sys.stderr.write("%s has no password!\n" % f.name) - return - elif cipher_name == 'aes256-cbc': - encryption_type = "AES-256-CBC" - elif cipher_name == 'aes256-ctr': - encryption_type = "AES-256-CTR" + data = ''.join(lines[start:end]).encode() + data = base64.b64decode(data) + except base64.binascii.Error: + e = sys.exc_info()[1] + raise Exception('base64 decoding error: ' + str(e)) + + + if ktype != 2: + if 'proc-type' not in headers: # unencrypted key file? + sys.stderr.write("%s has no password!\n" % f.name) + return + + try: + encryption_type, saltstr = headers['dek-info'].split(',') + except: + raise Exception('Can\'t parse DEK-info in private key file') + + if encryption_type not in CIPHER_TABLE: + raise Exception('Unknown private key cipher "%s"' % encryption_type) + + + if ktype == 2: # bcrypt_pbkdf format, see "sshkey_private_to_blob2" in sshkey.c + AUTH_MAGIC = b"openssh-key-v1" + salt_length = 16 # fixed value in sshkey.c + # find offset to salt + offset = 0 + if not data.startswith(AUTH_MAGIC): + raise Exception('Missing AUTH_MAGIC!') + offset = offset + len(AUTH_MAGIC) + 1 # sizeof(AUTH_MAGIC) + length = unpack(">I", data[offset:offset+4])[0] # ciphername length + offset = offset + 4 + cipher_name = data[offset:offset+length].decode('ascii') + if cipher_name == 'none': + sys.stderr.write("%s has no password!\n" % f.name) + return + elif cipher_name == 'aes256-cbc': + encryption_type = "AES-256-CBC" + elif cipher_name == 'aes256-ctr': + encryption_type = "AES-256-CTR" + else: + raise Exception('Unknown encryption type') + offset = offset + length + length = unpack(">I", data[offset:offset+4])[0] # kdfname length + offset = offset + 4 + length + length = unpack(">I", data[offset:offset+4])[0] # kdf length + salt_offset = offset + 4 + 4 # extra "4" to skip over salt length field + # print(salt_offset) # this should be 47, always? + # find offset to check bytes + offset = offset + 4 + length # number of keys + offset = offset + 4 # pubkey blob + length = unpack(">I", data[offset:offset+4])[0] # pubkey length + offset = offset + 4 + length + offset = offset + 4 # skip over length of "encrypted" blob + if offset > len(data): + raise Exception('Internal error in offset calculation!') + ciphertext_begin_offset = offset + saltstr = binascii.hexlify(data[salt_offset:salt_offset+salt_length]).decode("ascii") + # rounds value appears after salt + rounds_offset = salt_offset + salt_length + rounds = data[rounds_offset: rounds_offset+4] + rounds = unpack(">I", rounds)[0] + if rounds == 0: + rounds == 16 + + keysize = CIPHER_TABLE[encryption_type]['keysize'] + salt = binascii.unhexlify(saltstr) + + filename_idx = '' if len(tags) == 1 else '_' + str(num_processed_keys+1) + data = binascii.hexlify(data).decode("ascii") + if keysize == 24 and encryption_type == "AES-192-CBC" and (ktype == 0 or ktype == 1): # RSA, DSA keys using AES-192 + hashline = "%s%s:$sshng$%s$%s$%s$%s$%s" % (f.name, filename_idx, 4, len(saltstr) // 2, + saltstr, len(data) // 2, data) + elif keysize == 32 and encryption_type == "AES-256-CBC" and (ktype == 0 or ktype == 1): # RSA, DSA keys using AES-256 + hashline = "%s%s:$sshng$%s$%s$%s$%s$%s" % (f.name, filename_idx, 5, len(saltstr) // 2, + saltstr, len(data) // 2, data) + elif keysize == 24: + hashline = "%s%s:$sshng$%s$%s$%s$%s$%s" % (f.name, filename_idx, 0, # 0 -> 3DES + len(salt), saltstr, len(data) // 2, data) + elif keysize == 16 and (ktype == 0 or ktype == 1): # RSA, DSA keys using AES-128 + hashline = "%s%s:$sshng$%s$%s$%s$%s$%s" % (f.name, filename_idx, 1, len(saltstr) // 2, + saltstr, len(data) // 2, data) + elif keysize == 16 and ktype == 3: # EC keys using AES-128 + hashline = "%s%s:$sshng$%s$%s$%s$%s$%s" % (f.name, filename_idx, 3, len(saltstr) // 2, + saltstr, len(data) // 2, data) + elif keysize == 32 and encryption_type == "AES-256-CBC" and ktype == 2: # bcrypt pbkdf + aes-256-cbc + hashline = "%s%s:$sshng$%s$%s$%s$%s$%s$%d$%d" % (f.name, filename_idx, 2, len(saltstr) // 2, + saltstr, len(data) // 2, data, rounds, ciphertext_begin_offset) + elif keysize == 32 and encryption_type == "AES-256-CTR" and ktype == 2: # bcrypt pbkdf + aes-256-ctr + hashline = "%s%s:$sshng$%s$%s$%s$%s$%s$%d$%d" % (f.name, filename_idx, 6, len(saltstr) // 2, + saltstr, len(data) // 2, data, rounds, ciphertext_begin_offset) else: - raise Exception('Unknown encryption type') - offset = offset + length - length = unpack(">I", data[offset:offset+4])[0] # kdfname length - offset = offset + 4 + length - length = unpack(">I", data[offset:offset+4])[0] # kdf length - salt_offset = offset + 4 + 4 # extra "4" to skip over salt length field - # print(salt_offset) # this should be 47, always? - # find offset to check bytes - offset = offset + 4 + length # number of keys - offset = offset + 4 # pubkey blob - length = unpack(">I", data[offset:offset+4])[0] # pubkey length - offset = offset + 4 + length - offset = offset + 4 # skip over length of "encrypted" blob - if offset > len(data): - raise Exception('Internal error in offset calculation!') - ciphertext_begin_offset = offset - saltstr = binascii.hexlify(data[salt_offset:salt_offset+salt_length]).decode("ascii") - # rounds value appears after salt - rounds_offset = salt_offset + salt_length - rounds = data[rounds_offset: rounds_offset+4] - rounds = unpack(">I", rounds)[0] - if rounds == 0: - rounds == 16 - - keysize = CIPHER_TABLE[encryption_type]['keysize'] - salt = binascii.unhexlify(saltstr) - - data = binascii.hexlify(data).decode("ascii") - if keysize == 24 and encryption_type == "AES-192-CBC" and (ktype == 0 or ktype == 1): # RSA, DSA keys using AES-192 - hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 4, len(saltstr) // 2, - saltstr, len(data) // 2, data) - elif keysize == 32 and encryption_type == "AES-256-CBC" and (ktype == 0 or ktype == 1): # RSA, DSA keys using AES-256 - hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 5, len(saltstr) // 2, - saltstr, len(data) // 2, data) - elif keysize == 24: - hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 0, # 0 -> 3DES - len(salt), saltstr, len(data) // 2, data) - elif keysize == 16 and (ktype == 0 or ktype == 1): # RSA, DSA keys using AES-128 - hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 1, len(saltstr) // 2, - saltstr, len(data) // 2, data) - elif keysize == 16 and ktype == 3: # EC keys using AES-128 - hashline = "%s:$sshng$%s$%s$%s$%s$%s" % (f.name, 3, len(saltstr) // 2, - saltstr, len(data) // 2, data) - elif keysize == 32 and encryption_type == "AES-256-CBC" and ktype == 2: # bcrypt pbkdf + aes-256-cbc - hashline = "%s:$sshng$%s$%s$%s$%s$%s$%d$%d" % (f.name, 2, len(saltstr) // 2, - saltstr, len(data) // 2, data, rounds, ciphertext_begin_offset) - elif keysize == 32 and encryption_type == "AES-256-CTR" and ktype == 2: # bcrypt pbkdf + aes-256-ctr - hashline = "%s:$sshng$%s$%s$%s$%s$%s$%d$%d" % (f.name, 6, len(saltstr) // 2, - saltstr, len(data) // 2, data, rounds, ciphertext_begin_offset) - else: - sys.stderr.write("%s uses unsupported cipher, please file a bug!\n" % f.name) - return + sys.stderr.write("%s uses unsupported cipher, please file a bug!\n" % f.name) + return - sys.stdout.write("%s\n" % hashline) + sys.stdout.write("%s\n" % hashline) + start = end + 1 + num_processed_keys += 1 + if num_processed_keys != len(tags): + sys.stderr.write("Error likely in [%s], kindly remove erroneous keys and process again\n" % filename) + return if __name__ == "__main__": if len(sys.argv) < 2: