forked from DidierStevens/DidierStevensSuite
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ee5fdac
commit 7800387
Showing
1 changed file
with
348 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,348 @@ | ||
#!/usr/bin/env python | ||
|
||
__description__ = "Program to analyze password history" | ||
__author__ = 'Didier Stevens' | ||
__version__ = '0.0.1' | ||
__date__ = '2017/02/27' | ||
|
||
""" | ||
Source code put in public domain by Didier Stevens, no Copyright | ||
https://DidierStevens.com | ||
Use at your own risk | ||
History: | ||
2016/08/12: start | ||
2017/02/05: cleanup, refactoring | ||
2017/02/07: fixed output, added percentage and option -s | ||
2017/02/08: fixed output, added options -l and -n | ||
2017/02/10: updated man | ||
2017/02/13: added LongestCommonString | ||
2017/02/27: added option -L | ||
Todo: | ||
""" | ||
|
||
import optparse | ||
import glob | ||
import collections | ||
import re | ||
import sys | ||
import textwrap | ||
|
||
def PrintManual(): | ||
manual = ''' | ||
Manual: | ||
This program analyzes files with password history, and reports statistics on common strings (prefix, suffix, infix) of passwords per user. | ||
The minimum lenght of a common string is 3 characters by default. Use option -L to change the minimum length of the common string. | ||
Example of input file (passwords.txt): | ||
user01:HASH:azerty- | ||
user01_history0:HASH:azerty0 | ||
user01_history1:HASH:azerty1 | ||
user01_history2:HASH:azerty2 | ||
user01_history3:HASH:azerty3 | ||
user01_history4:HASH:azerty4 | ||
user01_history5:HASH:azerty5 | ||
user01_history6:HASH:azerty6 | ||
user01_history7:HASH:azerty7 | ||
user01_history8:HASH:azerty8 | ||
user01_history9:HASH:azerty9 | ||
user01_history10:HASH:azerty10 | ||
user01_history11:HASH:azerty11 | ||
user01_history12:HASH:azerty12 | ||
user01_history13:HASH:azerty13 | ||
user01_history14:HASH:azerty14 | ||
user01_history15:HASH:azerty15 | ||
user01_history16:HASH:azerty16 | ||
user01_history17:HASH:azerty17 | ||
user01_history18:HASH:azerty18 | ||
user01_history19:HASH:azerty19 | ||
user01_history20:HASH:azerty20 | ||
user01_history21:HASH:azerty21 | ||
user01_history22:HASH:azerty22 | ||
user02:HASH:99Monkey | ||
user02_history0:HASH:00Monkey | ||
user02_history1:HASH:01Monkey | ||
user02_history2:HASH:02Monkey | ||
user02_history3:HASH:03Monkey | ||
user02_history4:HASH:04Monkey | ||
user02_history5:HASH:05Monkey | ||
user02_history6:HASH:06Monkey | ||
user02_history7:HASH:07Monkey | ||
user02_history8:HASH:08Monkey | ||
user02_history9:HASH:09Monkey | ||
user02_history10:HASH:10Monkey | ||
user02_history11:HASH:11Monkey | ||
user02_history12:HASH:12Monkey | ||
user02_history13:HASH:13Monkey | ||
user02_history14:HASH:14Monkey | ||
user02_history15:HASH:15Monkey | ||
user02_history16:HASH:16Monkey | ||
user02_history17:HASH:17Monkey | ||
user02_history18:HASH:18Monkey | ||
user02_history19:HASH:19Monkey | ||
user02_history20:HASH:20Monkey | ||
user02_history21:HASH:21Monkey | ||
user02_history22:HASH:22Monkey | ||
user03:HASH:SomethingElse | ||
user03_history0:HASH:Password0 | ||
user03_history1:HASH:Password1 | ||
user03_history2:HASH:Password2 | ||
user03_history3:HASH:Password3 | ||
user03_history4:HASH:Password4 | ||
user03_history5:HASH:Password5 | ||
user03_history6:HASH:Password6 | ||
user03_history7:HASH:Password7 | ||
user03_history8:HASH:Password8 | ||
user03_history9:HASH:Password9 | ||
user03_history10:HASH:Password10 | ||
user03_history11:HASH:Azerty$1 | ||
Usage example: | ||
password-history-analysis.py passwords.txt | ||
Output: | ||
user01:24:24:100.00:azerty | ||
user02:24:24:100.00:Monkey | ||
user03:13:11:84.62:Password | ||
The first field is the username. | ||
The second field is the number of passwords for the given username. | ||
The third field is the largest number of passwords for the given username with the same prefix or suffix. | ||
The fourth field is the percentage of third and second field. | ||
The fifth field is the password's common string. | ||
The report can be written to file with option -o. | ||
Use option -l to convert usernames to lowercase. | ||
Option -n will not produce a report, but output all lines that do not match a password entry. Use this to detect entries not handled by this program. | ||
The separator (for input and output) is :, and can be changed with option -s. | ||
''' | ||
for line in manual.split('\n'): | ||
print(textwrap.fill(line)) | ||
|
||
def File2Strings(filename): | ||
try: | ||
f = open(filename, 'r') | ||
except: | ||
return None | ||
try: | ||
return map(lambda line:line.rstrip('\n'), f.readlines()) | ||
except: | ||
return None | ||
finally: | ||
f.close() | ||
|
||
def ProcessAt(argument): | ||
if argument.startswith('@'): | ||
strings = File2Strings(argument[1:]) | ||
if strings == None: | ||
raise Exception('Error reading %s' % argument) | ||
else: | ||
return strings | ||
else: | ||
return [argument] | ||
|
||
# CIC: Call If Callable | ||
def CIC(expression): | ||
if callable(expression): | ||
return expression() | ||
else: | ||
return expression | ||
|
||
# IFF: IF Function | ||
def IFF(expression, valueTrue, valueFalse): | ||
if expression: | ||
return CIC(valueTrue) | ||
else: | ||
return CIC(valueFalse) | ||
|
||
class cOutput(): | ||
def __init__(self, filename=None): | ||
self.filename = filename | ||
if self.filename and self.filename != '': | ||
self.f = open(self.filename, 'w') | ||
else: | ||
self.f = None | ||
|
||
def Line(self, line): | ||
if self.f: | ||
self.f.write(line + '\n') | ||
else: | ||
print(line) | ||
|
||
def Close(self): | ||
if self.f: | ||
self.f.close() | ||
self.f = None | ||
|
||
def ExpandFilenameArguments(filenames): | ||
return list(collections.OrderedDict.fromkeys(sum(map(glob.glob, sum(map(ProcessAt, filenames), [])), []))) | ||
|
||
class cOutputResult(): | ||
def __init__(self, options): | ||
if options.output: | ||
self.oOutput = cOutput(options.output) | ||
else: | ||
self.oOutput = cOutput() | ||
self.options = options | ||
|
||
def Line(self, line): | ||
self.oOutput.Line(line) | ||
|
||
def Close(self): | ||
self.oOutput.Close() | ||
|
||
def ProcessFile(fIn, fullread): | ||
if fullread: | ||
yield fIn.read() | ||
else: | ||
for line in fIn: | ||
yield line.strip('\n') | ||
|
||
def ComparePasswordsLeftToRight(password1, password2): | ||
common = '' | ||
for i in range(0, min(len(password1), len(password2))): | ||
if password1[i] == password2[i]: | ||
common += password1[i] | ||
else: | ||
return common | ||
return common | ||
|
||
def ComparePasswordsRightToLeft(password1, password2): | ||
return ComparePasswordsLeftToRight(password1[::-1], password2[::-1])[::-1] | ||
|
||
def AnalyzePasswordsPrefixSuffix(passwords, minimumlength): | ||
commons = {} | ||
for i in range(0, len(passwords) - 1): | ||
for j in range(i + 1, len(passwords)): | ||
common = ComparePasswordsLeftToRight(passwords[i], passwords[j]) | ||
if len(common) >= minimumlength: | ||
if not common in commons: | ||
commons[common] = 0 | ||
commons[common] += 1 | ||
common = ComparePasswordsRightToLeft(passwords[i], passwords[j]) | ||
if len(common) >= minimumlength: | ||
if not common in commons: | ||
commons[common] = 0 | ||
commons[common] += 1 | ||
return commons | ||
|
||
def LongestCommonString(string1, string2): | ||
dMatrix = {} | ||
longest = 0 | ||
result = [] | ||
for i in range(len(string1)): | ||
for j in range(len(string2)): | ||
if string1[i] == string2[j]: | ||
if i == 0 or j == 0: | ||
dMatrix[i,j] = 1 | ||
else: | ||
dMatrix[i,j] = dMatrix[i-1,j-1] + 1 | ||
if dMatrix[i,j] > longest: | ||
longest = dMatrix[i,j] | ||
result = [string1[i-longest+1:i+1]] | ||
elif dMatrix[i,j] == longest: | ||
result = result + [string1[i-longest+1:i+1]] | ||
else : | ||
dMatrix[i,j] = 0 | ||
if len(result) > 0: | ||
return result[0] | ||
else: | ||
return '' | ||
|
||
def AnalyzePasswordsLongestCommonString(passwords, minimumlength): | ||
commons = {} | ||
for i in range(0, len(passwords) - 1): | ||
for j in range(i + 1, len(passwords)): | ||
common = LongestCommonString(passwords[i], passwords[j]) | ||
if len(common) >= minimumlength: | ||
if not common in commons: | ||
commons[common] = 0 | ||
commons[common] += 1 | ||
return commons | ||
|
||
def PasswordHistoryAnalysisSingle(filenames, oOutput, options): | ||
oRE = re.compile(options.separator.join(['^([^', ']+)', '([^', ']+)', '(.+)$'])) | ||
oREHistory = re.compile('^(.+)_history\d+$') | ||
for filename in filenames: | ||
dUsernames = {} | ||
if filename == '': | ||
fIn = sys.stdin | ||
else: | ||
fIn = open(filename, 'r') | ||
for line in ProcessFile(fIn, False): | ||
results = oRE.match(line) | ||
if results != None: | ||
oMatchHistory = oREHistory.match(results.groups()[0]) | ||
if oMatchHistory != None: | ||
username = oMatchHistory.groups()[0] | ||
else: | ||
username = results.groups()[0] | ||
if options.lowercase: | ||
username = username.lower() | ||
if not username in dUsernames: | ||
dUsernames[username] = [] | ||
dUsernames[username].append(results.groups()[2]) | ||
elif options.nonmatching: | ||
oOutput.Line(line) | ||
if fIn != sys.stdin: | ||
fIn.close() | ||
if not options.nonmatching: | ||
for username, passwords in sorted(dUsernames.items()): | ||
commons = sorted([(value, key) for key, value in AnalyzePasswordsPrefixSuffix(passwords, options.length).items()]) | ||
if len(commons) == 0: | ||
commons = sorted([(value, key) for key, value in AnalyzePasswordsLongestCommonString(passwords, options.length).items()]) | ||
if len(commons) > 0: | ||
counter = 0 | ||
for password in passwords: | ||
if commons[-1][1] in password: | ||
counter += 1 | ||
oOutput.Line(options.separator.join(['%s', '%d', '%d', '%0.2f', '%s']) % (username, len(passwords), counter, float(counter) / float(len(passwords)) * 100.0, commons[-1][1])) | ||
else: | ||
oOutput.Line(options.separator.join(['%s', '%d', '%d', '%0.2f', '%s']) % (username, len(passwords), 0, 0.0, '')) | ||
|
||
def PasswordHistoryAnalysis(filenames, options): | ||
oOutput = cOutputResult(options) | ||
PasswordHistoryAnalysisSingle(filenames, oOutput, options) | ||
oOutput.Close() | ||
|
||
def Main(): | ||
global dLibrary | ||
|
||
moredesc = ''' | ||
Arguments: | ||
@file: process each file listed in the text file specified | ||
wildcards are supported | ||
Source code put in the public domain by Didier Stevens, no Copyright | ||
Use at your own risk | ||
https://DidierStevens.com''' | ||
|
||
oParser = optparse.OptionParser(usage='usage: %prog [options] [[@]file ...]\n' + __description__ + moredesc, version='%prog ' + __version__) | ||
oParser.add_option('-m', '--man', action='store_true', default=False, help='Print manual') | ||
oParser.add_option('-o', '--output', type=str, default='', help='Output to file') | ||
oParser.add_option('-s', '--separator', type=str, default=':', help='Separator used in the password files (default :)') | ||
oParser.add_option('-l', '--lowercase', action='store_true', default=False, help='Convert usernames to lowercase') | ||
oParser.add_option('-n', '--nonmatching', action='store_true', default=False, help='Print lines that do not match a password entry') | ||
oParser.add_option('-L', '--length', type=int, default=3, help='Minimum length common string') | ||
(options, args) = oParser.parse_args() | ||
|
||
if options.man: | ||
oParser.print_help() | ||
PrintManual() | ||
return | ||
|
||
if len(args) == 0: | ||
oParser.print_help() | ||
else: | ||
PasswordHistoryAnalysis(ExpandFilenameArguments(args), options) | ||
|
||
if __name__ == '__main__': | ||
Main() |