diff --git a/README.md b/README.md
index c6e81c4..cdc851a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
## Purpose
-Psudohash is a password list generator for orchestrating brute force attacks and cracking hashes. It imitates certain password creation patterns commonly used by humans, like substituting a word's letters with symbols or numbers (leet), using char-case variations, adding a common padding before or after the main passphrase and more. It is keyword-based and highly customizable. 🎥 -> [Video Presentation](https://www.youtube.com/watch?v=oj3zjApOOGc)
+Psudohash is a password list generator for orchestrating brute force attacks and cracking hashes. It imitates certain password creation patterns commonly used by humans, like substituting a word's letters with symbols or numbers (leet), using char-case variations, adding a common padding before or after the main passphrase and more. It is keyword-based and highly customizable. 🎥 -> [Video Presentation](https://www.youtube.com/watch?v=oj3zjApOOGc) Psudohash also features single random password generation, able to generate random alphanumeric passwords in lowercase, uppercase, upper- and lowercase, and solely numeric, all with the option to add special characters an at a specified length.
## Pentesting Corporate Environments
System administrators and other employees often use a mutated version of the Company's name to set passwords (e.g. Am@z0n_2022). This is commonly the case for network devices (Wi-Fi access points, switches, routers, etc), application or even domain accounts. With the most basic options, psudohash can generate a wordlist with all possible mutations of one or multiple keywords, based on common character substitution patterns (customizable), case variations, strings commonly used as padding and more. Take a look at the following example:
@@ -46,12 +46,17 @@ chmod +x psudohash.py
```
## Usage
```
+# Keywords
./psudohash.py [-h] -w WORDS [-an LEVEL] [-nl LIMIT] [-y YEARS] [-ap VALUES] [-cpb] [-cpa] [-cpo] [-o FILENAME] [-q]
+
+# Random
+./psudohash.py [-ra] [length] / [-rua] [length] / [-rula] [length] / [-n] [length] [-sc]
```
The help dialog [ -h, --help ] includes usage details and examples.
## Usage Tips
1. Combining options `--years` and `--append-numbering` with a `--numbering-limit` ≥ last two digits of any year input, will most likely produce duplicate words because of the mutation patterns implemented by the tool.
2. If you add custom padding values and/or modify the predefined common padding values in the source code, in combination with multiple optional parameters, there is a small chance of duplicate words occurring. psudohash includes word filtering controls but for speed's sake, those are limited.
+3. Default lengths for random passwords are 12 for all options except solely numeric, which is 9.
## Individuals
When it comes to people, i think we all have (more or less) set passwords using a mutation of one or more words that mean something to us e.g., our name or wife/kid/pet/band names, sticking the year we were born at the end or maybe a super secure padding like "!@#". Well, guess what?
diff --git a/psudohash.py b/psudohash.py
index b16604f..224f756 100644
--- a/psudohash.py
+++ b/psudohash.py
@@ -3,7 +3,7 @@
# Author: Panagiotis Chartas (t3l3machus)
# https://github.com/t3l3machus
-import argparse, sys, itertools
+import argparse, sys, itertools, random
# Colors
MAIN = '\033[38;5;50m'
@@ -25,13 +25,27 @@
Basic:
python3 psudohash.py -w -cpa
+ python3 psudohash.py -ra
Thorough:
python3 psudohash.py -w -cpa -an 3 -y 1990-2022
+ python3 psudohash.py -rula 14 -sc
'''
)
-parser.add_argument("-w", "--words", action="store", help = "Comma seperated keywords to mutate", required = True)
+# Prevent flags for random generation from being used together.
+# Random generation flags do not conflict with keyword flags, and keyword flags are prioritized
+random_group = parser.add_mutually_exclusive_group()
+
+random_group.add_argument("-ra", "--random-alphanumeric", action = "store", type = int, nargs = "?", const = "12", help = "Generate a single lowercase alphanumeric password of specified length (default 12, must be >= 1)")
+random_group.add_argument("-rua", "--random-uppercase-alphanumeric", action = "store", type = int, nargs = "?", const = "12", help = "Generate a single uppercase alphanumeric password of specified length (default 12, must be >= 1)")
+random_group.add_argument("-rula", "--random-uppercase-lowercase-alphanumeric", action = "store", type = int, nargs = "?", const = "12", help = "Generate a single upper- and lowercase alphanumeric password of specified length (default 12, must be >= 1)")
+random_group.add_argument("-n", "--numbers", action = "store", type = int, nargs = "?", const = "9", help = "Generate a single random numeric of specified length (default 9, must be >= 1)")
+
+# Special characters flag does not interfere with other code.
+parser.add_argument("-sc", "--special-characters", action = "store_true", help = "Include special characters in any random password, does not work alone")
+
+parser.add_argument("-w", "--words", action="store", help = "Comma seperated keywords to mutate")
parser.add_argument("-an", "--append-numbering", action="store", help = "Append numbering range at the end of each word mutation (before appending year or common paddings).\nThe LEVEL value represents the minimum number of digits. LEVEL must be >= 1. \nSet to 1 will append range: 1,2,3..100\nSet to 2 will append range: 01,02,03..100 + previous\nSet to 3 will append range: 001,002,003..100 + previous.\n\n", type = int, metavar='LEVEL')
parser.add_argument("-nl", "--numbering-limit", action="store", help = "Change max numbering limit value of option -an. Default is 50. Must be used with -an.", type = int, metavar='LIMIT')
parser.add_argument("-y", "--years", action="store", help = "Singe OR comma seperated OR range of years to be appended to each word mutation (Example: 2022 OR 1990,2017,2022 OR 1990-2000)")
@@ -61,7 +75,6 @@ def unique(l):
return unique_list
-
# Append numbering
if args.numbering_limit and not args.append_numbering:
exit_with_msg('Option -nl must be used with -an.')
@@ -147,6 +160,24 @@ def banner():
basic_mutations = []
outfile = args.output if args.output else 'output.txt'
trans_keys = []
+random_sequence = ""
+
+# If the user wants a random alphanumeric password, set the selection with appropriate characters
+if not args.words:
+ if args.random_alphanumeric:
+ random_sequence = "abcdefghijklmnopqrstuvwxyz1234567890"
+
+ elif args.random_uppercase_alphanumeric:
+ random_sequence = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
+
+ elif args.random_uppercase_lowercase_alphanumeric:
+ random_sequence = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
+
+ elif args.numbers:
+ random_sequence = "1234567890"
+
+ if args.special_characters:
+ random_sequence += "~`!@#$%^&*()_-+={[}]|\:;\"'<,>.?/"
transformations = [
{'a' : ['@', '4']},
@@ -496,6 +527,14 @@ def chill():
+def generate_random_password(length: int):
+
+ # Randomly choose characters from the selection
+ pwd = ''.join(random.choice(random_sequence) for _ in range(length))
+
+ return pwd
+
+
def main():
banner() if not args.quiet else chill()
@@ -503,82 +542,148 @@ def main():
global basic_mutations, mutations_cage
keywords = []
- for w in args.words.split(','):
- if w.strip().isdecimal():
- exit_with_msg('Unable to mutate digit-only keywords.')
-
- elif w.strip() not in [None, '']:
- keywords.append(w.strip())
-
- # Calculate total words and size of output
- total_size = [0, 0]
-
- for keyw in keywords:
- count_size = calculate_output(keyw.strip().lower())
- total_size[0] += count_size[0]
- total_size[1] += count_size[1]
-
- size = round(((total_size[1]/1000)/1000), 1) if total_size[1] > 100000 else total_size[1]
- prefix = 'bytes' if total_size[1] <= 100000 else 'MB'
- fsize = f'{size} {prefix}'
-
- print(f'[{MAIN}Info{END}] Calculating output length and size...')
-
- # Inform user about the output size
- try:
- concent = input(f'[{ORANGE}Warning{END}] This operation will produce {BOLD}{total_size[0]}{END} words, {BOLD}{fsize}{END}. Are you sure you want to proceed? [y/n]: ')
- except KeyboardInterrupt:
- exit('\n')
-
- if concent.lower() not in ['y', 'yes']:
- sys.exit(f'\n[{RED}X{END}] Aborting.')
+ if args.words:
+ # This is necessary only when using arg -w
+ for w in args.words.split(','):
+ if w.strip().isdecimal():
+ exit_with_msg('Unable to mutate digit-only keywords.')
+
+ elif w.strip() not in [None, '']:
+ keywords.append(w.strip())
- else:
+ # Calculate total words and size of output
+ total_size = [0, 0]
- open(outfile, "w").close()
+ for keyw in keywords:
+ count_size = calculate_output(keyw.strip().lower())
+ total_size[0] += count_size[0]
+ total_size[1] += count_size[1]
- for word in keywords:
- print(f'[{GREEN}*{END}] Mutating keyword: {GREEN}{word}{END} ')
- mutability = check_mutability(word.lower())
-
- # Produce case mutations
- print(f' ├─ Producing character case-based transformations... ')
- caseMutationsHandler(word.lower(), mutability)
-
- if mutability:
- # Produce char substitution mutations
- print(f' ├─ Mutating word based on commonly used char-to-symbol and char-to-number substitutions... ')
- trans = evalTransformations(word.lower())
- mutations_handler(word, trans[0], trans[1])
-
- else:
- print(f' ├─ {ORANGE}No character substitution instructions match this word.{END}')
+ size = round(((total_size[1]/1000)/1000), 1) if total_size[1] > 100000 else total_size[1]
+ prefix = 'bytes' if total_size[1] <= 100000 else 'MB'
+ fsize = f'{size} {prefix}'
+
+ print(f'[{MAIN}Info{END}] Calculating output length and size...')
- # Append numbering
- if args.append_numbering:
- print(f' ├─ Appending numbering to each word mutation... ')
- append_numbering()
+ # Inform user about the output size
+ try:
+ concent = input(f'[{ORANGE}Warning{END}] This operation will produce {BOLD}{total_size[0]}{END} words, {BOLD}{fsize}{END}. Are you sure you want to proceed? [y/n]: ')
+ except KeyboardInterrupt:
+ exit('\n')
+
+ if concent.lower() not in ['y', 'yes']:
+ sys.exit(f'\n[{RED}X{END}] Aborting.')
+
+ else:
- # Handle years
- if args.years:
- print(f' ├─ Appending year patterns after each word mutation... ')
- mutate_years()
+ open(outfile, "w").close()
- # Append common paddings
- if args.common_paddings_after or args.custom_paddings_only:
- print(f' ├─ Appending common paddings after each word mutation... ')
- append_paddings_after()
+ for word in keywords:
+ print(f'[{GREEN}*{END}] Mutating keyword: {GREEN}{word}{END} ')
+ mutability = check_mutability(word.lower())
+
+ # Produce case mutations
+ print(f' ├─ Producing character case-based transformations... ')
+ caseMutationsHandler(word.lower(), mutability)
- if args.common_paddings_before:
- print(f' ├─ Appending common paddings before each word mutation... ')
- append_paddings_before()
+ if mutability:
+ # Produce char substitution mutations
+ print(f' ├─ Mutating word based on commonly used char-to-symbol and char-to-number substitutions... ')
+ trans = evalTransformations(word.lower())
+ mutations_handler(word, trans[0], trans[1])
+
+ else:
+ print(f' ├─ {ORANGE}No character substitution instructions match this word.{END}')
+
+ # Append numbering
+ if args.append_numbering:
+ print(f' ├─ Appending numbering to each word mutation... ')
+ append_numbering()
+
+ # Handle years
+ if args.years:
+ print(f' ├─ Appending year patterns after each word mutation... ')
+ mutate_years()
+
+ # Append common paddings
+ if args.common_paddings_after or args.custom_paddings_only:
+ print(f' ├─ Appending common paddings after each word mutation... ')
+ append_paddings_after()
+
+ if args.common_paddings_before:
+ print(f' ├─ Appending common paddings before each word mutation... ')
+ append_paddings_before()
+
+ basic_mutations = []
+ mutations_cage = []
+ print(f' └─ Done!')
+
+ print(f'\n[{MAIN}Info{END}] Completed! List saved in {outfile}\n')
- basic_mutations = []
- mutations_cage = []
- print(f' └─ Done!')
+ elif (args.random_alphanumeric or args.random_uppercase_alphanumeric
+ or args.random_uppercase_lowercase_alphanumeric or args.numbers):
- print(f'\n[{MAIN}Info{END}] Completed! List saved in {outfile}\n')
-
+ # Determines message to print based on selected random option
+ choice = ""
+
+ # Desired password length
+ password_length = 0
+
+ # Displayed if special characters are included
+ special_characters_message = ""
+
+ if args.random_alphanumeric:
+
+ choice = "lowercase alphanumeric"
+
+ if args.random_alphanumeric < 1:
+ exit_with_msg("Random password length must be >= 1.")
+ else:
+ password_length = args.random_alphanumeric
+
+
+ elif args.random_uppercase_alphanumeric:
+
+ choice = "uppercase alphanumeric"
+
+ if args.random_uppercase_alphanumeric < 1:
+ exit_with_msg("Random password length must be >= 1.")
+ else:
+ password_length = args.random_uppercase_alphanumeric
+
+ elif args.random_uppercase_lowercase_alphanumeric:
+
+ choice = "uppercase and lowercase alphanumeric"
+
+ if args.random_uppercase_lowercase_alphanumeric < 1:
+ exit_with_msg("Random password length must be >= 1.")
+ else:
+ password_length = args.random_uppercase_lowercase_alphanumeric
+
+
+ elif args.numbers:
+
+ choice = "numeric"
+
+ if args.numbers < 1:
+ exit_with_msg("Random password length must be >= 1.")
+ else:
+ password_length = args.numbers
+
+
+ if args.special_characters:
+ special_characters_message = f" with {GREEN}special characters{END}"
+
+ print(f'[{GREEN}*{END}] Generating random {GREEN}{choice}{END} password of length {GREEN}{password_length}{END}{special_characters_message}...')
+
+ with open(outfile, 'w') as output:
+ output.write(generate_random_password(password_length))
+
+ print(f' └─ Done!')
+
+ print(f'\n[{MAIN}Info{END}] Completed! Password saved in {outfile}\n')
+
+
if __name__ == '__main__':
main()