10
\$\begingroup\$

This program should convert a file containing Mana, Hashcat or John the Ripper NTLMv1 hashes to another file containing Hashcat or John the Ripper hashes. Do you have any suggestions on how to make the code cleaner? The hash format selection code feels kinda dirty.

#!/usr/bin/env python3

# Takes a file of NTLMv1 hashes in mana format and spits out a
#  file of hashes in JtR or hashcat format.


import sys
import re

class Hash():
    pass

class HashcatHash(Hash):
    @staticmethod
    def parse(line):
        m = re.match("(.*?)::::([0-9a-f]{48}):([0-9a-f]{16})", line)
        if m:
            return {"username": m.group(1), "response": m.group(2), "challenge": m.group(3)}
        else:
            raise ValueError("Couldn't find hash in line")

    @staticmethod
    def format(d):
        return "{username}::::{response}:{challenge}".format(
            username=d["username"],
            response=d["response"],
            challenge=d["challenge"])

class JohnHash(Hash):
    @staticmethod
    def parse(line):
        m = re.match("(.*?):\$NETNTLM\$([0-9a-f]{16})\$([0-9a-f]{48})", line)
        if m:
            return {"username": m.group(1), "response": m.group(3), "challenge": m.group(2)}
        else:
            raise ValueError("Couldn't find hash in line")

    @staticmethod
    def format(d):
        return "{username}:$NETNTLM${challenge}${response}".format(
            username=d["username"],
            response=d["response"],
            challenge=d["challenge"])

class ManaHash(Hash):
    @staticmethod
    def parse(line):
        m = re.match("CHAP\|(.*?)\|([0-9a-f:]{23})\|([0-9a-f:]{71})", line)
        if m:
            return {"username": m.group(1), "response": remove_colons(m.group(3)), "challenge": remove_colons(m.group(2))}
        else:
            raise ValueError("Couldn't find hash in line")

    @staticmethod
    def format(d):
        raise NotImplementedError

def print_usage():
    print("Usage:")
    print("exportlog.py <informat> <infile> <outformat> <outfile>")

def remove_colons(hexstr):
    return ''.join(hexstr.split(':'))

if __name__ == '__main__':
    if (len(sys.argv) != 5) or (sys.argv[1] not in ['john', 'hashcat', 'mana']) or (sys.argv[3] not in ['john', 'hashcat']):
        print_usage()
        quit()
    import_format = sys.argv[1]
    export_format = sys.argv[3]
    with open(sys.argv[2], 'r') as infile, open(sys.argv[4], 'w') as outfile:
        for line in infile:
            if import_format == 'john':
                d = JohnHash.parse(line)
            elif import_format == 'hashcat':
                d = HashcatHash.parse(line)
            elif import_format == 'mana':
                d = ManaHash.parse(line)
            if export_format == 'john':
                outline = JohnHash.format(d)
            elif export_format == 'hashcat':
                outline = HashcatHash.format(d)
            outfile.write(outline + "\n")
\$\endgroup\$
5
\$\begingroup\$

First of all, you can improve on reading the command-line arguments and, instead of manually parsing the sys.argv, use argparse module. Something like:

import argparse


def parse_args():
    """Parses command-line arguments."""

    parser = argparse.ArgumentParser()

    parser.add_argument('infile', type=argparse.FileType('r'))
    parser.add_argument('informat', action='store', choices=HASH_FORMATS)
    parser.add_argument('outfile', type=argparse.FileType('w'))
    parser.add_argument('outformat', action='store', choices=HASH_FORMATS)

    return parser.parse_args()

if __name__ == '__main__':
    args = parse_args()
    # ...

The format selection logic can be simplified if you would use a dictionary:

HASH_FORMATS = {
    'john': JohnHash,
    'hashcat': HashcatHash,
    'mana': ManaHash
}

# get the input and output hash classes beforehand
input_hash_class = HASH_FORMATS[args.informat]
output_hash_class = HASH_FORMATS[args.outformat]

for line in args.infile:
    parsed_line = input_hash_class.parse(line)
    converted_line = output_hash_class.format(parsed_line)
    args.outfile.write(converted_line)

And, as a side note about defining the Hash-based classes - I think you can make use of Abstract Base Classes with abstract methods which may lead to a cleaner object-oriented design.

\$\endgroup\$
  • 1
    \$\begingroup\$ Would you be willing to show how you would implement the ABC? \$\endgroup\$ – Peilonrayz Feb 27 '17 at 18:21
  • \$\begingroup\$ Thanks for the detailed explanation. Could you indeed show how it would be implemented with ABC? Also, is there a pythonic way of writing a line to a file with newline? \$\endgroup\$ – redfast00 Feb 27 '17 at 19:40
5
\$\begingroup\$

If you're going to write a lot of Hash classes, then you'll want to simplify the creation of the class. Take for example HashcatHash, the following is much simpler to create, than what you're doing:

class HashcatHash(Hash):
    _regex = "(.*?)::::([0-9a-f]{48}):([0-9a-f]{16})"
    _format = "{d[username]}::::{d[response]}:{d[challenge]}"
    def parse(m):
        return {
            "username": m.group(1),
            "response": m.group(2),
            "challenge": m.group(3)
        }

To do this, you'd want to make Hash default parse and format to raise NotImplementedError, and to create a metaclass, based on this answer, that mutates your input to dynamically crate these static functions. This is somewhat horrible, but it makes creation of the classes much cleaner. And so you could get:

import re
from types import FunctionType


class HashMetaclass(type):
    def __new__(meta, classname, bases, class_dict):
        class_dict = class_dict.copy()

        # Wrap parse, to use _regex and pass a match.
        regex = class_dict.get("_regex")
        if regex is not None:
            parse = class_dict["parse"]
            def _parse(line, _match=re.match):
                m = _match(regex, line)
                if not m:
                    raise ValueError("Couldn't find hash in line")
                return parse(m)
            class_dict['parse'] = _parse

        # change _format to overwrite format
        _format = class_dict.get("_format")
        if _format is not None:
            class_dict['format'] = lambda d: _format.format(d=d)

        for key, attr in class_dict.items():
            if isinstance(attr, FunctionType):
                class_dict[key] = staticmethod(attr)
        return type.__new__(meta, classname, bases, class_dict)


class Hash(metaclass=HashMetaclass):
    def parse(m):
        raise NotImplementedError()

    def format(d):
        raise NotImplementedError()


class HashcatHash(Hash):
    _regex = "(.*?)::::([0-9a-f]{48}):([0-9a-f]{16})"
    _format = "{d[username]}::::{d[response]}:{d[challenge]}"
    def parse(m):
        return {
            "username": m.group(1),
            "response": m.group(2),
            "challenge": m.group(3)
        }


class JohnHash(Hash):
    _regex = "(.*?):\$NETNTLM\$([0-9a-f]{16})\$([0-9a-f]{48})"
    _format = "{d[username]}:$NETNTLM${d[challenge]}${d[response]}"
    def parse(m):
        return {
            "username": m.group(1),
            "response": m.group(3),
            "challenge": m.group(2)
        }


class ManaHash(Hash):
    _regex = "CHAP\|(.*?)\|([0-9a-f:]{23})\|([0-9a-f:]{71})"
    def parse(m):
        return {
            "username": m.group(1),
            "response": remove_colons(m.group(3)),
            "challenge": remove_colons(m.group(2))
        }

After this you want to simplify your ifs, rather than manually writing an if/elif/else for each one, you can create a hash of the hash's name, and the class. This means that you can simplify all the ifs to one readable line:

HASHES = {
    'john': JohnHash,
    'hashcat': HashcatHash,
    'mana': ManaHash
}
parse = HASHES[import_format].parse
format = HASHES[export_format].format

After this you may want to use argparse to simplify your argument input.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.