├── Screenshots ├── ms-example.png ├── psudohash.png ├── micro-example.png └── multiple-words.png ├── common_padding_values.txt ├── LICENSE.md ├── README.md └── psudohash.py /Screenshots/ms-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3l3machus/psudohash/HEAD/Screenshots/ms-example.png -------------------------------------------------------------------------------- /Screenshots/psudohash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3l3machus/psudohash/HEAD/Screenshots/psudohash.png -------------------------------------------------------------------------------- /Screenshots/micro-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3l3machus/psudohash/HEAD/Screenshots/micro-example.png -------------------------------------------------------------------------------- /Screenshots/multiple-words.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3l3machus/psudohash/HEAD/Screenshots/multiple-words.png -------------------------------------------------------------------------------- /common_padding_values.txt: -------------------------------------------------------------------------------- 1 | 123 2 | 1234 3 | 12345 4 | 123456 5 | 234 6 | 345 7 | 456 8 | 567 9 | 678 10 | 789 11 | 890 12 | !@# 13 | !@! 14 | @#@ 15 | !@#$% 16 | 123!@# 17 | !@#$% 18 | $%^& 19 | ! 20 | !! 21 | !!! 22 | @ 23 | @@ 24 | @@@ 25 | # 26 | ## 27 | ### 28 | $ 29 | $$ 30 | $$$ 31 | % 32 | ^ 33 | & 34 | && 35 | &&& 36 | * 37 | ** 38 | *** 39 | . 40 | .. 41 | ... 42 | ? 43 | ?? 44 | ??? 45 | - 46 | -- 47 | --- 48 | !@ 49 | @# 50 | #$ 51 | $% 52 | %^ 53 | ^& 54 | &* 55 | *( 56 | () 57 | @#$ 58 | #$% 59 | $%^ 60 | %^& 61 | ^&* 62 | &*( 63 | *() 64 | )_+ 65 | 1!1 66 | 2@2 67 | 3#3 68 | 4$4 69 | 5%5 70 | 6^6 71 | 7&7 72 | 8*8 73 | 9(9 74 | 0)0 75 | @2@ 76 | #3# 77 | $4$ 78 | %5% 79 | ^6^ 80 | &7& 81 | *8* 82 | (9( 83 | +++ 84 | ,./ 85 | abc123 86 | 1111 87 | 2222 88 | 3333 89 | 4444 90 | 5555 91 | 6666 92 | 7777 93 | 8888 94 | 9999 95 | asd 96 | asdf 97 | zxc 98 | zxcv 99 | qwe 100 | qwer 101 | qwerty 102 | q1 103 | w2 104 | e3 105 | r4 106 | t5 107 | q1w2 108 | q1w2e3 109 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Panagiotis Chartas (t3l3machus) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # psudohash 2 | [![Python 3.x](https://img.shields.io/badge/python-3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-MIT-red.svg)](https://github.com/t3l3machus/psudohash/blob/main/LICENSE) 3 | 4 | 5 | 6 | ## Cool New Features of v1.1.0 7 | Special thanks to [DavidAngelos](https://github.com/DavidAngelos): 8 | ▶️ Added a progress bar in every step to track execution. 9 | ▶️ Added options: 10 | - **In-order joins** (`-i` / `--inorder`): join keywords only in the original order (e.g. `foo,bar,baz` → `foo, bar, baz, foobar, foobaz, barbaz, foobarbaz`). 11 | - **All-order combinations** (`-c` / `--combinations`): generate every ordering of each subset (e.g. `foo,bar,baz` → `foo, bar, baz, foobar, foobaz, barfoo, …, bazbarfoo`). 12 | - **Custom separator** (`--sep `): when joining words, insert this string between tokens (defaults to no separator). 13 | - **Max combine size** (`--max-combine `): limit how many raw keywords get joined together (default: 2). 14 | - **Min/Max length filtering of final words** (`--minlen/--maxlen `): filter the final wordlist only with word with the desired length. 15 | 16 | ## Purpose 17 | 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) 18 | 19 | ## Pentesting Corporate Environments 20 | 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: 21 | 22 | ![image](https://github.com/t3l3machus/psudohash/assets/75489922/4a25ef08-8b21-4798-8b1a-97bdbd2dc2e3) 23 | 24 | 25 | ## Customization 26 | ### Leet Character Substitution 27 | The script implements the following character substitution schema. You can add/modify character substitution mappings by editing the `transformations` list in `psudohash.py` and following the data structure presented below (default): 28 | ``` 29 | transformations = [ 30 | {'a' : ['@', '4']}, 31 | {'b' : '8'}, 32 | {'e' : '3'}, 33 | {'g' : ['9', '6']}, 34 | {'i' : ['1', '!']}, 35 | {'o' : '0'}, 36 | {'s' : ['$', '5']}, 37 | {'t' : '7'} 38 | ] 39 | ``` 40 | ### Common Padding Values 41 | When setting passwords, I believe it's pretty standard to add a sequence of characters before and/or after the main passphrase to make it "stronger". For example, one may set a password "dragon" and add a value like "!!!" or "!@#" at the end, resulting in "dragon!!!", "dragon!@#", etc. Psudohash reads such values from `common_padding_values.txt` and uses them to mutate the provided keywords by appending them before (`-cpb`) or after (`-cpa`) each generated keyword variation. You can modify it as you see fit. 42 | 43 | ### Year Values 44 | When appending a year value to a mutated keyword, psudohash will do so by utilizing various seperators. by default, it will use the following seperators which you can modify by editing the `year_seperators` list: 45 | ``` 46 | year_seperators = ['', '_', '-', '@'] 47 | ``` 48 | For example, if the given keyword is "amazon" and option `-y 2023` was used, the output will include "amazon2023", "amazon_2023", "amazon-2023", "amazon@2023", "amazon23", "amazon_23", "amazon-23", "amazon@23". 49 | 50 | ## Installation 51 | Install Python 3.x and `tqdm` first: 52 | 53 | ```bash 54 | pip3 install tqdm 55 | ``` 56 | Then clone the repo and make the script executable: 57 | ``` 58 | git clone https://github.com/t3l3machus/psudohash.git 59 | cd ./psudohash 60 | chmod +x psudohash.py 61 | ``` 62 | ## Usage 63 | ``` 64 | ./psudohash.py [-h] -w WORDS [-i] [-c] [--sep SEP] [--max-combine N] [-an LEVEL] [-nl LIMIT] [-y YEARS] [-ap VALUES] [-cpb] [-cpa] [-cpo] [-o FILENAME] [-q] 65 | ``` 66 | The help dialog [ -h, --help ] includes usage details and examples. 67 | 68 | ## Options 69 | 70 | - **`-w, --words `** 71 | Comma‐separated raw keywords (required). 72 | 73 | - **`-i, --inorder`** 74 | Join up to `--max-combine` keywords in the given order (e.g. `foo,bar,baz` → `foo, bar, baz, foobar, foobaz, barbaz, foobarbaz`). 75 | 76 | - **`-c, --combinations`** 77 | Generate every permutation of each subset (up to `--max-combine`) (e.g. `foo,bar,baz` → `foo, bar, baz, foobar, foobaz, barfoo, …`). 78 | 79 | - **`--max-combine `** (default: 2) 80 | Maximum number of raw keywords to join into one base string. 81 | 82 | - **`--sep `** 83 | When joining words (`-i` or `-c`), place this string between tokens. Defaults to an empty string. 84 | 85 | - **`--minlen `** 86 | Discard any final password shorter than N characters. 87 | 88 | - **`--maxlen `** 89 | Discard any final password longer than N characters. 90 | 91 | - **`-an, --append-numbering `** 92 | Append numbered suffixes (zero‐padded to `` digits) to each word mutation. 93 | 94 | - **`-nl, --numbering-limit `** 95 | Maximum number to count up to when appending numbers (default: 50). 96 | 97 | - **`-y, --years `** 98 | Append one or more years to each mutation (e.g. `1990-2000`, or `2022,2023`). 99 | 100 | - **`-ap, --append-padding `** 101 | Append custom padding values (comma‐separated). Must be used with `-cpb` or `-cpa`. 102 | 103 | - **`-cpb, --common-paddings-before`** 104 | Prepend values from `common_padding_values.txt` before each mutation. 105 | 106 | - **`-cpa, --common-paddings-after`** 107 | Append values from `common_padding_values.txt` after each mutation. 108 | 109 | - **`-cpo, --custom-paddings-only`** 110 | Use only user‐provided paddings (no defaults). Must be used with `-ap`. 111 | 112 | - **`-o, --output `** 113 | Write the results to `` (default: `output.txt`). 114 | 115 | - **`-q, --quiet`** 116 | Suppress the ASCII art banner on startup. 117 | 118 | 119 | ### Usage Examples 120 | 121 | 1. **No multi‐word (singletons only)** 122 | ```bash 123 | ./psudohash.py -w foo,bar,baz -cpa 124 | # → foo, bar, baz 125 | ``` 126 | 127 | 2. **In‐order joins (-i, up to 2 words by default)** 128 | ```bash 129 | ./psudohash.py -w foo,bar,baz -i 130 | # → foo, bar, baz, foobar, foobaz, barbaz 131 | ``` 132 | 133 | 3. **All‐order combinations (-c, up to 2 words by default)** 134 | ```bash 135 | ./psudohash.py -w foo,bar,baz -c 136 | # → foo, bar, baz, foobar, foobaz, barfoo, barbaz, bazfoo, bazbar 137 | ``` 138 | 139 | 4. **Change separator between joined words** 140 | ```bash 141 | ./psudohash.py -w foo,bar,baz -i --sep "_" 142 | # → foo, bar, baz, foo_bar, foo_baz, bar_baz 143 | ``` 144 | 145 | 5. **Length Filtering (`--minlen`/`--maxlen`)** 146 | ```bash 147 | ./psudohash.py -w apple,banana -i --minlen 10 148 | # Warning: exact size cannot be determined because of length filters. 149 | # Example final outputs might include “applebanana” (11 chars), “bananaapple” (11 chars). 150 | ``` 151 | 152 | 6. **Combine up to 3 words (instead of default 2)** 153 | 154 | ## Usage Tips 155 | 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. 156 | 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. 157 | 3. When using `--minlen` or `--maxlen`, the script cannot pre-calculate the exact word count; you’ll see a “exact size cannot be determined” warning and the size without this filter will be calculated, the final size will be smaller. 158 | 159 | ## Individuals 160 | 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? 161 | 162 | ![usage_example_png](https://raw.github.com/t3l3machus/psudohash/master/Screenshots/multiple-words.png) 163 | 164 | 165 | ## Future 166 | I'm gathering information regarding commonly used password creation patterns to enhance the tool's capabilities. 167 | -------------------------------------------------------------------------------- /psudohash.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | # 3 | # Author: Panagiotis Chartas (t3l3machus) 4 | # https://github.com/t3l3machus 5 | 6 | import argparse, sys, itertools 7 | from tqdm import tqdm 8 | 9 | # Colors 10 | MAIN = '\033[38;5;50m' 11 | LOGO = '\033[38;5;41m' 12 | LOGO2 = '\033[38;5;42m' 13 | GREEN = '\033[38;5;82m' 14 | ORANGE = '\033[0;38;5;214m' 15 | PRPL = '\033[0;38;5;26m' 16 | PRPL2 = '\033[0;38;5;25m' 17 | RED = '\033[1;31m' 18 | END = '\033[0m' 19 | BOLD = '\033[1m' 20 | 21 | # -------------- Arguments & Usage -------------- # 22 | parser = argparse.ArgumentParser( 23 | formatter_class=argparse.RawTextHelpFormatter, 24 | epilog=''' 25 | Usage examples: 26 | 27 | # 1) No multi-word: treat each keyword separately 28 | python3 psudohash.py -w foo,bar,baz -cpa 29 | # → foo, bar, baz 30 | 31 | # 2) In-order joins (-i): up to 2 words by default 32 | python3 psudohash.py -w foo,bar,baz -i 33 | # → foo, bar, baz, foobar, foobaz, barbaz 34 | 35 | # 3) All-order combinations (-c): up to 2 words by default 36 | python3 psudohash.py -w foo,bar,baz -c 37 | # → foo, bar, baz, foobar, foobaz, barfoo, barbaz, bazfoo, bazbar 38 | 39 | # 4) Change separator between joined words 40 | python3 psudohash.py -w foo,bar,baz -i --sep "_" 41 | # → foo, bar, baz, foo_bar, foo_baz, bar_baz, foo_bar_baz 42 | 43 | # 5) Combine up to 3 words (instead of default 2) 44 | python3 psudohash.py -w foo,bar,baz -i --max-combine 3 45 | # → foo, bar, baz, foobar, foobaz, barbaz, foobarbaz, ... 46 | ''' 47 | ) 48 | 49 | parser.add_argument("-w", "--words", action="store", help = "Comma seperated keywords to mutate", required = True) 50 | parser.add_argument("-i", "--inorder", action="store_true", help="Join keywords only in the order given: for each 1≤r≤max_combine, concatenate each r-subset in original sequence.") 51 | parser.add_argument("-c", "--combinations", action="store_true", help="Generate every ordering of every subset (up to max_combine) of the provided keywords.") 52 | parser.add_argument("--max-combine", type=int, default=2, help="Maximum number of raw keywords to join into one base string (default: 2). Applies when using -i or -c.") 53 | parser.add_argument("--minlen", type=int, help="Minimum length (inclusive) of any resulting password. Mutations shorter than this are skipped.") 54 | parser.add_argument("--maxlen", type=int, help="Maximum length (inclusive) of any resulting password. Mutations longer than this are skipped.") 55 | parser.add_argument("--sep", type=str, default="", help="Separator to insert between joined keywords (default: no separator).") 56 | 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') 57 | 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') 58 | 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)") 59 | parser.add_argument("-ap", "--append-padding", action="store", help = "Add comma seperated values to common paddings (must be used with -cpb OR -cpa)", metavar='VALUES') 60 | parser.add_argument("-cpb", "--common-paddings-before", action="store_true", help = "Append common paddings before each mutated word") 61 | parser.add_argument("-cpa", "--common-paddings-after", action="store_true", help = "Append common paddings after each mutated word") 62 | parser.add_argument("-cpo", "--custom-paddings-only", action="store_true", help = "Use only user provided paddings for word mutations (must be used with -ap AND (-cpb OR -cpa))") 63 | parser.add_argument("-o", "--output", action="store", help = "Output filename (default: output.txt)", metavar='FILENAME') 64 | parser.add_argument("-q", "--quiet", action="store_true", help = "Do not print the banner on startup") 65 | 66 | args = parser.parse_args() 67 | 68 | def exit_with_msg(msg): 69 | parser.print_help() 70 | print(f'\n[{RED}Debug{END}] {msg}\n') 71 | sys.exit(1) 72 | 73 | 74 | 75 | def unique(l): 76 | 77 | unique_list = [] 78 | 79 | for i in l: 80 | if i not in unique_list: 81 | unique_list.append(i) 82 | 83 | return unique_list 84 | 85 | 86 | # Append numbering 87 | if args.numbering_limit and not args.append_numbering: 88 | exit_with_msg('Option -nl must be used with -an.') 89 | 90 | if args.append_numbering: 91 | if args.append_numbering <= 0: 92 | exit_with_msg('Numbering level must be > 0.') 93 | 94 | _max = args.numbering_limit + 1 if args.numbering_limit and isinstance(args.numbering_limit, int) else 51 95 | 96 | 97 | # Create years list 98 | if args.years: 99 | 100 | years = [] 101 | 102 | if args.years.count(',') == 0 and args.years.count('-') == 0 and args.years.isdecimal() and int(args.years) >= 1000 and int(args.years) <= 3200: 103 | years.append(str(args.years)) 104 | 105 | elif args.years.count(',') > 0: 106 | for year in args.years.split(','): 107 | if year.strip() != '' and year.isdecimal() and int(year) >= 1000 and int(year) <= 3200: 108 | years.append(year) 109 | else: 110 | exit_with_msg('Illegal year(s) input. Acceptable years range: 1000 - 3200.') 111 | 112 | elif args.years.count('-') == 1: 113 | years_range = args.years.split('-') 114 | start_year = years_range[0] 115 | end_year = years_range[1] 116 | 117 | if (start_year.isdecimal() and int(start_year) < int(end_year) and int(start_year) >= 1000) and (end_year.isdecimal() and int(end_year) <= 3200): 118 | for y in range(int(years_range[0]), int(years_range[1])+1): 119 | years.append(str(y)) 120 | else: 121 | exit_with_msg('Illegal year(s) input. Acceptable years range: 1000 - 3200.') 122 | else: 123 | exit_with_msg('Illegal year(s) input. Acceptable years range: 1000 - 3200.') 124 | 125 | 126 | def banner(): 127 | padding = ' ' 128 | 129 | P = [[' ', '┌', '─', '┐'], [' ', '├','─','┘'], [' ', '┴',' ',' ']] 130 | S = [[' ', '┌','─','┐'], [' ', '└','─','┐'], [' ', '└','─','┘']] 131 | U = [[' ', '┬',' ','┬'], [' ', '│',' ','│'], [' ', '└','─','┘']] 132 | D = [[' ', '┌','┬','┐'], [' ', ' ','│','│'], [' ', '─','┴','┘']] 133 | O = [[' ', '┌','─','┐'], [' ', '│',' ','│'], [' ', '└','─','┘']] 134 | H = [[' ', '┐', ' ', '┌'], [' ', '├','╫','┤'], [' ', '┘',' ','└']] 135 | A = [[' ', '┌','─','┐'], [' ', '├','─','┤'], [' ', '┴',' ','┴']] 136 | S = [[' ', '┌','─','┐'], [' ', '└','─','┐'], [' ', '└','─','┘']] 137 | H = [[' ', '┬',' ','┬'], [' ', '├','─','┤'], [' ', '┴',' ','┴']] 138 | 139 | banner = [P,S,U,D,O,H,A,S,H] 140 | final = [] 141 | print('\r') 142 | init_color = 37 143 | txt_color = init_color 144 | cl = 0 145 | 146 | for charset in range(0, 3): 147 | for pos in range(0, len(banner)): 148 | for i in range(0, len(banner[pos][charset])): 149 | clr = f'\033[38;5;{txt_color}m' 150 | char = f'{clr}{banner[pos][charset][i]}' 151 | final.append(char) 152 | cl += 1 153 | txt_color = txt_color + 36 if cl <= 3 else txt_color 154 | 155 | cl = 0 156 | 157 | txt_color = init_color 158 | init_color += 31 159 | 160 | if charset < 2: final.append('\n ') 161 | 162 | print(f" {''.join(final)}") 163 | print(f'{END}{padding} by t3l3machus\n') 164 | 165 | 166 | # ----------------( Base Settings )---------------- # 167 | mutations_cage = [] 168 | basic_mutations = [] 169 | outfile = args.output if args.output else 'output.txt' 170 | trans_keys = [] 171 | 172 | transformations = [ 173 | {'a' : ['@', '4']}, 174 | {'b' : '8'}, 175 | {'e' : '3'}, 176 | {'g' : ['9', '6']}, 177 | {'i' : ['1', '!']}, 178 | {'o' : '0'}, 179 | {'s' : ['$', '5']}, 180 | {'t' : '7'} 181 | ] 182 | 183 | for t in transformations: 184 | for key in t.keys(): 185 | trans_keys.append(key) 186 | 187 | # Common Padding Values 188 | if (args.custom_paddings_only or args.append_padding) and not (args.common_paddings_before or args.common_paddings_after): 189 | exit_with_msg('Options -ap and -cpo must be used with -cpa or -cpb.') 190 | 191 | 192 | elif (args.common_paddings_before or args.common_paddings_after) and not args.custom_paddings_only: 193 | 194 | try: 195 | f = open('common_padding_values.txt', 'r') 196 | content = f.readlines() 197 | common_paddings = [val.strip() for val in content] 198 | f.close() 199 | 200 | except: 201 | exit_with_msg('File "common_padding_values.txt" not found.') 202 | 203 | elif (args.common_paddings_before or args.common_paddings_after) and (args.custom_paddings_only and args.append_padding): 204 | common_paddings = [] 205 | 206 | elif not (args.common_paddings_before or args.common_paddings_after): 207 | common_paddings = [] 208 | 209 | else: 210 | exit_with_msg('\nIllegal padding settings.\n') 211 | 212 | if args.append_padding: 213 | for val in args.append_padding.split(','): 214 | if val.strip() != '' and val not in common_paddings: 215 | common_paddings.append(val) 216 | 217 | 218 | if (args.common_paddings_before or args.common_paddings_after): 219 | common_paddings = list(set(common_paddings)) 220 | 221 | 222 | # ----------------( Functions )---------------- # 223 | # The following list is used to create variations of password values and appended years. 224 | # For example, a passwd value {passwd} will be mutated to "{passwd}{seperator}{year}" 225 | # for each of the symbols included in the list below. 226 | year_seperators = ['', '_', '-', '@'] 227 | 228 | 229 | 230 | # ----------------( Functions )---------------- # 231 | def evalTransformations(w): 232 | 233 | trans_chars = [] 234 | total = 1 235 | c = 0 236 | w = list(w) 237 | 238 | for char in w: 239 | for t in transformations: 240 | if char in t.keys(): 241 | trans_chars.append(c) 242 | if isinstance(t[char], list): 243 | total *= 3 244 | else: 245 | total *= 2 246 | c += 1 247 | 248 | return [trans_chars, total] 249 | 250 | 251 | 252 | def mutate(tc, word): 253 | 254 | global trans_keys, mutations_cage, basic_mutations 255 | 256 | i = trans_keys.index(word[tc].lower()) 257 | trans = transformations[i][word[tc].lower()] 258 | limit = len(trans) * len(mutations_cage) 259 | c = 0 260 | 261 | for m in mutations_cage: 262 | w = list(m) 263 | 264 | if isinstance(trans, list): 265 | for tt in trans: 266 | w[tc] = tt 267 | transformed = ''.join(w) 268 | mutations_cage.append(transformed) 269 | c += 1 270 | else: 271 | w[tc] = trans 272 | transformed = ''.join(w) 273 | mutations_cage.append(transformed) 274 | c += 1 275 | 276 | if limit == c: break 277 | 278 | return mutations_cage 279 | 280 | 281 | 282 | def mutations_handler(kword, trans_chars, total): 283 | """ 284 | Perform character→symbol/number substitutions and write each new mutation. 285 | """ 286 | global mutations_cage, basic_mutations 287 | 288 | container = [] 289 | for word in basic_mutations: 290 | mutations_cage = [word.strip()] 291 | for tc in trans_chars: 292 | results = mutate(tc, kword) 293 | container.append(results) 294 | 295 | for m_set in container: 296 | for m in m_set: 297 | basic_mutations.append(m) 298 | 299 | basic_mutations = list(set(basic_mutations)) 300 | 301 | desc = " ├─ Mutating word based on commonly used char‐to‐symbol/number substitutions" 302 | with open(outfile, 'a') as wordlist, \ 303 | tqdm(total=len(basic_mutations), desc=desc, leave=False) as pbar: 304 | 305 | for m in basic_mutations: 306 | # Only final‐filter if no numbering/years/padding follow 307 | if not args.append_numbering and not args.years and not (args.common_paddings_after or args.common_paddings_before): 308 | if not (args.minlen and len(m) < args.minlen) \ 309 | and not (args.maxlen and len(m) > args.maxlen): 310 | wordlist.write(m + '\n') 311 | else: 312 | wordlist.write(m + '\n') 313 | pbar.update(1) 314 | 315 | print(f"{desc}... [100.0%]") 316 | 317 | 318 | def mutateCase(word): 319 | trans = list(map(''.join, itertools.product(*zip(word.upper(), word.lower())))) 320 | return trans 321 | 322 | 323 | 324 | def caseMutationsHandler(word, mutability): 325 | """ 326 | Generate all upper/lower combos, add to basic_mutations, 327 | and write to file immediately if mutability is False. 328 | """ 329 | global basic_mutations 330 | case_mutations = mutateCase(word) 331 | 332 | desc = " ├─ Producing character case‐based transformations" 333 | with open(outfile, 'a') as wordlist, \ 334 | tqdm(total=len(case_mutations), desc=desc, leave=False) as pbar: 335 | 336 | for m in case_mutations: 337 | basic_mutations.append(m) 338 | if not mutability: 339 | # Only final‐filter if no substitutions/numbering/years/padding follow 340 | if not args.combinations and not args.inorder \ 341 | and not args.append_numbering and not args.years \ 342 | and not (args.common_paddings_after or args.common_paddings_before): 343 | if not (args.minlen and len(m) < args.minlen) \ 344 | and not (args.maxlen and len(m) > args.maxlen): 345 | wordlist.write(m + '\n') 346 | else: 347 | wordlist.write(m + '\n') 348 | pbar.update(1) 349 | 350 | print(f"{desc}... [100.0%]") 351 | 352 | 353 | def append_numbering(): 354 | """ 355 | For each word in basic_mutations, append numbering variants (zfilled up to LEVEL). 356 | """ 357 | global _max, basic_mutations 358 | 359 | lvl = args.append_numbering 360 | first_cycle = True 361 | previous_list = [] 362 | 363 | # total lines = len(basic_mutations) * lvl * (_max - 1) * 2 364 | total_lines = len(basic_mutations) * lvl * (_max - 1) * 2 365 | desc = " ├─ Appending numbering to each word mutation" 366 | 367 | with open(outfile, 'a') as wordlist, \ 368 | tqdm(total=total_lines, desc=desc, leave=False) as pbar: 369 | 370 | for word in basic_mutations: 371 | for i in range(1, lvl + 1): 372 | for k in range(1, _max): 373 | num_z = str(k).zfill(i) 374 | line1 = f"{word}{num_z}\n" 375 | line2 = f"{word}_{num_z}\n" 376 | 377 | if first_cycle: 378 | # Only final‐filter if no years or padding follow 379 | if not args.years and not (args.common_paddings_after or args.common_paddings_before): 380 | if not (args.minlen and len(line1) < args.minlen) \ 381 | and not (args.maxlen and len(line1) > args.maxlen): 382 | wordlist.write(line1) 383 | pbar.update(1) 384 | else: 385 | pbar.update(1) 386 | if not (args.minlen and len(line2) < args.minlen) \ 387 | and not (args.maxlen and len(line2) > args.maxlen): 388 | wordlist.write(line2) 389 | pbar.update(1) 390 | else: 391 | pbar.update(1) 392 | else: 393 | if not args.years and not (args.common_paddings_after or args.common_paddings_before): 394 | if not (args.minlen and len(line1) < args.minlen) \ 395 | and not (args.maxlen and len(line1) > args.maxlen): 396 | wordlist.write(line1) 397 | pbar.update(1) 398 | else: 399 | pbar.update(1) 400 | if not (args.minlen and len(line2) < args.minlen) \ 401 | and not (args.maxlen and len(line2) > args.maxlen): 402 | wordlist.write(line2) 403 | pbar.update(1) 404 | else: 405 | pbar.update(1) 406 | else: 407 | wordlist.write(line1) 408 | wordlist.write(line2) 409 | pbar.update(2) 410 | previous_list.append(f"{word}{num_z}") 411 | else: 412 | if previous_list[k - 1] != f"{word}{num_z}": 413 | wordlist.write(line1) 414 | wordlist.write(line2) 415 | previous_list[k - 1] = f"{word}{num_z}" 416 | pbar.update(2) 417 | 418 | first_cycle = False 419 | 420 | print(f"{desc}... [100.0%]") 421 | 422 | 423 | def mutate_years(): 424 | """ 425 | For each entry in basic_mutations, append year variants (full YYYY + short YY). 426 | """ 427 | global basic_mutations 428 | current_mutations = basic_mutations.copy() 429 | 430 | # total lines = len(current_mutations) * len(years) * len(year_seperators) * 2 431 | total_lines = len(current_mutations) * len(years) * len(year_seperators) * 2 432 | desc = " ├─ Appending year patterns after each word mutation" 433 | 434 | with open(outfile, 'a') as wordlist, \ 435 | tqdm(total=total_lines, desc=desc, leave=False) as pbar: 436 | 437 | for word in current_mutations: 438 | for y in years: 439 | for sep in year_seperators: 440 | full = f"{word}{sep}{y}\n" 441 | short = f"{word}{sep}{y[2:]}\n" 442 | # Only final-filter if no padding follows 443 | if not (args.common_paddings_after or args.common_paddings_before): 444 | if not (args.minlen and len(full) < args.minlen) \ 445 | and not (args.maxlen and len(full) > args.maxlen): 446 | wordlist.write(full) 447 | basic_mutations.append(full.strip()) 448 | # count one for pbar regardless 449 | pbar.update(1) 450 | if not (args.minlen and len(short) < args.minlen) \ 451 | and not (args.maxlen and len(short) > args.maxlen): 452 | wordlist.write(short) 453 | basic_mutations.append(short.strip()) 454 | pbar.update(1) 455 | else: 456 | wordlist.write(full) 457 | basic_mutations.append(full.strip()) 458 | wordlist.write(short) 459 | basic_mutations.append(short.strip()) 460 | pbar.update(2) 461 | 462 | print(f"{desc}... [100.0%]") 463 | 464 | 465 | def check_underscore(word, pos): 466 | if word[pos] == '_': 467 | return True 468 | else: 469 | return False 470 | 471 | 472 | def append_paddings_before(): 473 | """ 474 | Prepend each common_paddings value before each word in basic_mutations. 475 | """ 476 | global basic_mutations 477 | current_mutations = basic_mutations.copy() 478 | 479 | # total lines = sum(len(common_paddings)*2 for each word) 480 | total_lines = sum(len(common_paddings) * 2 for _ in current_mutations) 481 | desc = " ├─ Appending common paddings before each word mutation" 482 | 483 | with open(outfile, 'a') as wordlist, \ 484 | tqdm(total=total_lines, desc=desc, leave=False) as pbar: 485 | 486 | for word in current_mutations: 487 | for val in common_paddings: 488 | line1 = f"{val}{word}\n" 489 | wordlist.write(line1) 490 | pbar.update(1) 491 | 492 | if not check_underscore(val, -1): 493 | line2 = f"{val}_{word}\n" 494 | wordlist.write(line2) 495 | pbar.update(1) 496 | 497 | print(f"{desc}... [100.0%]") 498 | 499 | 500 | def append_paddings_after(): 501 | """ 502 | Append each common_paddings value after each word in basic_mutations. 503 | """ 504 | global basic_mutations 505 | current_mutations = basic_mutations.copy() 506 | 507 | # total lines = sum(len(common_paddings)*2 for each word) 508 | total_lines = sum(len(common_paddings) * 2 for _ in current_mutations) 509 | desc = " ├─ Appending common paddings after each word mutation" 510 | 511 | with open(outfile, 'a') as wordlist, \ 512 | tqdm(total=total_lines, desc=desc, leave=False) as pbar: 513 | 514 | for word in current_mutations: 515 | for val in common_paddings: 516 | line1 = f"{word}{val}\n" 517 | # Only final-filter if no before-padding 518 | if not args.common_paddings_before: 519 | if not (args.minlen and len(line1) < args.minlen) \ 520 | and not (args.maxlen and len(line1) > args.maxlen): 521 | wordlist.write(line1) 522 | pbar.update(1) 523 | else: 524 | pbar.update(1) 525 | else: 526 | wordlist.write(line1) 527 | pbar.update(1) 528 | 529 | if not check_underscore(val, 0): 530 | line2 = f"{word}_{val}\n" 531 | if not args.common_paddings_before: 532 | if not (args.minlen and len(line2) < args.minlen) \ 533 | and not (args.maxlen and len(line2) > args.maxlen): 534 | wordlist.write(line2) 535 | pbar.update(1) 536 | else: 537 | pbar.update(1) 538 | else: 539 | wordlist.write(line2) 540 | pbar.update(1) 541 | 542 | print(f"{desc}... [100.0%]") 543 | 544 | 545 | def calculate_output(keyw): 546 | 547 | global trans_keys 548 | 549 | c = 0 550 | total = 1 551 | basic_total = 1 552 | basic_size = 0 553 | size = 0 554 | numbering_count = 0 555 | numbering_size = 0 556 | 557 | # Basic mutations calc 558 | for char in keyw: 559 | if char in trans_keys: 560 | i = trans_keys.index(keyw[c].lower()) 561 | trans = transformations[i][keyw[c].lower()] 562 | basic_total *= (len(trans) + 2) 563 | else: 564 | basic_total = basic_total * 2 if char.isalpha() else basic_total 565 | 566 | c += 1 567 | 568 | total = basic_total 569 | basic_size = total * (len(keyw) + 1) 570 | size = basic_size 571 | 572 | # Words numbering mutations calc 573 | if args.append_numbering: 574 | global _max 575 | word_len = len(keyw) + 1 576 | first_cycle = True 577 | previous_list = [] 578 | lvl = args.append_numbering 579 | 580 | for w in range(0, total): 581 | for i in range(1, lvl+1): 582 | for k in range(1, _max): 583 | n = str(k).zfill(i) 584 | if first_cycle: 585 | numbering_count += 2 586 | numbering_size += (word_len * 2) + (len(n) * 2) + 1 587 | previous_list.append(f'{w}{n}') 588 | 589 | else: 590 | if previous_list[k - 1] != f'{w}{n}': 591 | numbering_size += (word_len * 2) + (len(n) * 2) + 1 592 | numbering_count += 2 593 | previous_list[k - 1] = f'{w}{n}' 594 | 595 | first_cycle = False 596 | 597 | del previous_list 598 | 599 | # Adding years mutations calc 600 | if args.years: 601 | patterns = len(year_seperators) * 2 602 | year_chars = 4 603 | year_short = 2 604 | years_len = len(years) 605 | size += (basic_size * patterns * years_len) 606 | 607 | for sep in year_seperators: 608 | size += (basic_total * (year_chars + len(sep)) * years_len) 609 | size += (basic_total * (year_short + len(sep)) * years_len) 610 | 611 | total += total * len(years) * patterns 612 | basic_total = total 613 | basic_size = size 614 | 615 | # Common paddings mutations calc 616 | patterns = 2 617 | 618 | if args.common_paddings_after or args.common_paddings_before: 619 | paddings_len = len(common_paddings) 620 | pads_wlen_sum = sum([basic_total*len(w) for w in common_paddings]) 621 | _pads_wlen_sum = sum([basic_total*(len(w)+1) for w in common_paddings]) 622 | 623 | if args.common_paddings_after and args.common_paddings_before: 624 | size += ((basic_size * patterns * paddings_len) + pads_wlen_sum + _pads_wlen_sum) * 2 625 | total += (total * len(common_paddings) * 2) * 2 626 | 627 | elif args.common_paddings_after or args.common_paddings_before: 628 | size += (basic_size * patterns * paddings_len) + pads_wlen_sum + _pads_wlen_sum 629 | total += total * len(common_paddings) * 2 630 | 631 | return [total + numbering_count, size + numbering_size] 632 | 633 | 634 | 635 | def check_mutability(word): 636 | 637 | global trans_keys 638 | m = 0 639 | 640 | for char in word: 641 | if char in trans_keys: 642 | m += 1 643 | 644 | return m 645 | 646 | 647 | 648 | def chill(): 649 | pass 650 | 651 | 652 | 653 | def main(): 654 | 655 | banner() if not args.quiet else chill() 656 | 657 | global basic_mutations, mutations_cage 658 | 659 | # 1) Read raw keywords (ignore empty or digit-only) 660 | raw = [] 661 | for w in args.words.split(','): 662 | w = w.strip() 663 | if not w: 664 | continue 665 | if w.isdecimal(): 666 | exit_with_msg('Unable to mutate digit-only keywords.') 667 | raw.append(w) 668 | 669 | # 2) Build "base" keywords according to flags: 670 | # - If --inorder: join each r-subset in the given order, for r=1..max_combine. 671 | # - Elif --combinations: for each r-subset (1..max_combine), generate every permutation. 672 | # - Else: treat each raw word on its own. 673 | from itertools import combinations, permutations 674 | 675 | keywords = [] 676 | limit = min(len(raw), args.max_combine) 677 | 678 | if args.inorder: 679 | for r in range(1, limit + 1): 680 | for combo in combinations(raw, r): 681 | keywords.append(args.sep.join(combo)) 682 | elif args.combinations: 683 | for r in range(1, limit + 1): 684 | for combo in combinations(raw, r): 685 | for perm in permutations(combo): 686 | keywords.append(args.sep.join(perm)) 687 | else: 688 | keywords = list(raw) 689 | 690 | # Calculate total words and size of output 691 | total_size = [0, 0] 692 | 693 | for keyw in keywords: 694 | count_size = calculate_output(keyw.strip().lower()) 695 | total_size[0] += count_size[0] 696 | total_size[1] += count_size[1] 697 | 698 | size = round(((total_size[1]/1000)/1000), 1) if total_size[1] > 100000 else total_size[1] 699 | prefix = 'bytes' if total_size[1] <= 100000 else 'MB' 700 | fsize = f'{size} {prefix}' 701 | 702 | print(f'[{MAIN}Info{END}] Calculating output length and size...') 703 | 704 | # Inform user about the output size 705 | if args.minlen or args.maxlen: 706 | prompt = (f'[{ORANGE}Warning{END}] Exact final size cannot be determined because min/max-length filtering is active. Without filtering, this would produce {BOLD}{total_size[0]}{END} words, {BOLD}{fsize}{END}. Continue? [y/n]: ') 707 | else: 708 | prompt = (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]: ') 709 | 710 | try: 711 | concent = input(prompt) 712 | except KeyboardInterrupt: 713 | exit('\n') 714 | 715 | if concent.lower() not in ['y', 'yes']: 716 | sys.exit(f'\n[{RED}X{END}] Aborting.') 717 | 718 | else: 719 | 720 | open(outfile, "w").close() 721 | 722 | for word in keywords: 723 | print(f'[{GREEN}*{END}] Mutating keyword: {GREEN}{word}{END} ') 724 | mutability = check_mutability(word.lower()) 725 | 726 | # Stage 1: Case mutations 727 | caseMutationsHandler(word.lower(), mutability) 728 | # Stage 2: Substitution mutations 729 | if mutability: 730 | trans = evalTransformations(word.lower()) 731 | mutations_handler(word, trans[0], trans[1]) 732 | else: 733 | print(f" ├─ {ORANGE}No character substitution instructions match this word.{END}") 734 | # Stage 3: Numbering 735 | if args.append_numbering: 736 | append_numbering() 737 | # Stage 4: Years 738 | if args.years: 739 | mutate_years() 740 | # Stage 5: Common paddings after 741 | if args.common_paddings_after or args.custom_paddings_only: 742 | append_paddings_after() 743 | # Stage 6: Common paddings before 744 | if args.common_paddings_before: 745 | append_paddings_before() 746 | basic_mutations = [] 747 | mutations_cage = [] 748 | print(f' └─ Done!') 749 | 750 | print(f'\n[{MAIN}Info{END}] Completed! List saved in {outfile}\n') 751 | 752 | 753 | if __name__ == '__main__': 754 | main() --------------------------------------------------------------------------------