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")
2 Answers 2
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.
-
1\$\begingroup\$ Would you be willing to show how you would implement the ABC? \$\endgroup\$2017年02月27日 18:21:38 +00:00Commented Feb 27, 2017 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– redfast002017年02月27日 19:40:41 +00:00Commented Feb 27, 2017 at 19:40
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.
Explore related questions
See similar questions with these tags.