Practical Cryptanalysis: Reversing Cisco Type 7 Password Hashes

by mcandre

Cisco routers use an extremely weak algorithm for their passwords.  Cisco has acknowledged the insecurity of Type 7 passwords and encourages engineers to use modern hash algorithms such as MD5 (www.cisco.com/en/US/tech/tk59/technologies_tech_note09186a00809d38a7.shtml).  While the rare publicly accessible Cisco router may still use this weak algorithm and therefore constitutes a real security risk, we can set that aside and explore the algorithm academically.  For those without access to an old Cisco router, a short Python program (github.com/mcandre/ios7crypt/blob/master/ios7crypt.py) simulates the algorithm.

$ ./ios7crypt.py -e monkey
104306170e120b
$ ./ios7crypt.py -d 104306170e120b
monkey

Through a known-plaintext attack, the entirety of the algorithm is elucidated.  The first two digits are a random sample from [00, 15].  What follows is a hexadecimal string that is twice the length of the password (though on actual Cisco routers, the original password is truncated).  It turns out that the hexadecimal string is composed of pairs of hex bytes (0x43, 0x06, 0x17, ...) which constitute the encrypted password sequence.  Continued plaintext attack reveals that the relation between the plainbytes and cipherbytes is XOR, with a repeating static key and a random starting index (seed).  Decryption will always output the same password, but encryption will output one of 16 variant hashes for each password input, details to follow.

The essence of the algorithm is contained in the encrypt() and decrypt() functions.

Here is a code snippet of these functions from ios7crypt.py:

xlat = [
    0x64, 0x73, 0x66, 0x64, 0x3b, 0x6b, 0x66, 0x6f,
    0x41, 0x2c, 0x2e, 0x69, 0x79, 0x65, 0x77, 0x72,
    0x6b, 0x6c, 0x64, 0x4a, 0x4b, 0x44, 0x48, 0x53,
    0x55, 0x42, 0x73, 0x67, 0x76, 0x63, 0x61, 0x36,
    0x39, 0x38, 0x33, 0x34, 0x6e, 0x63, 0x78, 0x76,
    0x39, 0x38, 0x37, 0x33, 0x32, 0x35, 0x34, 0x6b,
    0x3b, 0x66, 0x67, 0x38, 0x37,
    ]

def encrypt(password):
    seed = ord(os.urandom(1)) % 16
    return "%02d%s" % (seed, "".join(["%02x" % (ord(password[i]) ^ xlat[seed + i]) for i in range(len(password))]))

def decrypt(h):
    (seed, h) = (int(h[:2]), h[2:])
    cipher_bytes = [int(h[i:i + 2], 16) for i in range(0, len(h), 2)]
    return "".join([chr(cipher_bytes[i] ^ xlat[(seed + i) % len(xlat)]) for i in range(len(cipher_bytes))])

The encryption algorithm is symmetrical; the only difference between encrypting a password and decrypting the hash is the concatenation of the seed (decimal) with the ciphertext (hexadecimal pairs), a trivially reversible process.

The insecurity of this algorithm is that it relies on XORing a password with a static key.  XOR is really only useful in cryptography as the basic of a more substantial algorithm, or used to combine a signal with a random key (one-time pad).  In this algorithm, the random seed modifies where the index of the key begins in the static sequence, each time a password is encrypted.  But even then, there are only 16 possible places to begin, or 16 possible repeating keys.  Furthermore, the algorithm is weakened by the key wrapping around, repeating.  The exact algorithm implemented in IOS also truncates passwords to a limit of 11 characters, but there is no reason to allow the decryption algorithm to handle arbitrarily long hashes.  If not for truncation, a single password of a few hundred characters would be enough to elucidate the entire static key, allowing the analyst to skip brute force known-plaintext attacks altogether.

In porting ios7crypt.py to another language, the programmer must learn the new language's syntax for sequence structures and the library functions for parsing and formatting hexadecimal digits, converting strings to ASCII byte sequences, generating random numbers, and parsing command line arguments.  These procedures form a representative sample of operating in many programming languages, much more so than the traditional print ("Hello World"), which in its simplicity hardly teaches anything at all for programmers already versed in one or more languages.

This laughably insecure encryption system eventually led Cisco's PSA and altered training course material, advising network engineers to "enable secret", which uses MD5, rather than "enable password", which uses the old proprietary algorithm, in all router configuration files.

Thus, witness a prime example of proprietary crypto gone wrong, where developers could have easily deferred to much more secure hash algorithms in the first place: MD5 (1992), MD4 (1990), or MD2 (1989).  Nevertheless, the simplicity of the algorithm allows for its use as a teaching tool for budding cryptographers and programming language enthusiasts.  Publicity of the weak algorithm is paramount, as some hapless networks are likely still using it, and knowledge is power.

#!/usr/bin/python
"""IOS7Crypt password encryptor/decryptor"""

import os
import sys
from getopt import getopt

XLAT = [
    0x64, 0x73, 0x66, 0x64, 0x3b, 0x6b, 0x66, 0x6f,
    0x41, 0x2c, 0x2e, 0x69, 0x79, 0x65, 0x77, 0x72,
    0x6b, 0x6c, 0x64, 0x4a, 0x4b, 0x44, 0x48, 0x53,
    0x55, 0x42, 0x73, 0x67, 0x76, 0x63, 0x61, 0x36,
    0x39, 0x38, 0x33, 0x34, 0x6e, 0x63, 0x78, 0x76,
    0x39, 0x38, 0x37, 0x33, 0x32, 0x35, 0x34, 0x6b,
    0x3b, 0x66, 0x67, 0x38, 0x37,
    ]

def encrypt(password):
    seed = ord(os.urandom(1)) % 16
    return "%02d%s" % (seed, "".join(["%02x" % (ord(password[i]) ^ XLAT[seed + i]) for i in range(len(password))]))

def decrypt(h):
    (seed, h) = (int(h[:2]), h[2:])
    cipher_bytes = [int(h[i:i + 2], 16) for i in range(0, len(h), 2)]
    return "".join([chr(cipher_bytes[i] ^ XLAT[(seed + i) % len(XLAT)]) for i in range(len(cipher_bytes))])

def test():
    pass

def usage():
    print "Usage: %s [options]" % sys.argv[0]
    print "-e --encrypt\t<password>"
    print "-d --decrypt\t<hash>"
    print "-t --test\trun unit tests"
    print "-h --help\tusage"
    sys.exit()

def main():
    encrypt_mode = "ENCRYPT"
    decrypt_mode = "DECRYPT"
    test_mode = "TEST"
    mode = encrypt_mode
    password = ""
    h = ""

    (optlist, args) = ([], [])

    try:
        (optlist, args) = getopt(sys.argv[1:], "e:d:th", ["encrypt=", "decrypt=", "test", "help"])
    except Exception:
        usage()

    if len(optlist) < 1:
        usage()

    for (option, value) in optlist:
        if option == "-h" or option == "--help":
            usage()
        elif option == "-e" or option == "--encrypt":
            mode = encrypt_mode
            password = value
        elif option == "-d" or option == "--decrypt":
            mode = decrypt_mode
            h = value
        elif option == "-t" or option == "--test":
            mode = test_mode

    if mode == encrypt_mode:
        print encrypt(password)
    elif mode == decrypt_mode:
        print decrypt(h)
    elif mode == test_mode:
        test()

if __name__ == "__main__":
    main()

Code: ios7crypt.py

Return to $2600 Index