-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathPunyPwn.py
executable file
·168 lines (130 loc) · 5.29 KB
/
PunyPwn.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PunyPwn - Exposing IDN Homograph Vulnerabilities.
This script generates IDN homograph variants of a domain using Cyrillic homoglyphs
and converts them to Punycode.
Options:
--domain DOMAIN: Base domain to generate variants from (without TLD).
--level LEVEL: Maximum number of substitutions (optional).
--style STYLE: Substitution realism level: 'any', 'realistic', 'very-realistic'.
--tlds TLD [TLD ...]: List of TLDs to generate (default: .com .fr .net .org).
"""
import argparse
import itertools
from typing import List, Dict, Tuple, Optional
import idna
# Homoglyph mapping (Cyrillic equivalents)
HOMOGLYPHS: Dict[str, str] = {
'a': 'а', # Cyrillic a
'e': 'е', # Cyrillic e
'o': 'о', # Cyrillic o
'c': 'с', # Cyrillic s
'p': 'р', # Cyrillic r
'x': 'х', # Cyrillic h
'y': 'у', # Cyrillic u
'm': 'м', # Cyrillic m
't': 'т', # Cyrillic t
'k': 'к', # Cyrillic k
'b': 'Ь', # Cyrillic soft sign
'h': 'һ' # Cyrillic h
}
# Homoglyph filtering for styles
STYLE_MAP: Dict[str, List[str]] = {
'any': list(HOMOGLYPHS.keys()),
'realistic': ['a', 'e', 'o', 'c', 'p', 'x', 'y'],
'very-realistic': ['a', 'e', 'o', 'c']
}
def find_substitutable_letters(domain: str, style: str) -> List[int]:
"""
Find positions of substitutable letters in domain based on style.
Args:
domain (str): The domain name.
style (str): Style level.
Returns:
List[int]: Indexes of substitutable letters.
"""
allowed_letters = STYLE_MAP.get(style, [])
return [i for i, c in enumerate(domain) if c in allowed_letters]
def generate_homographs(domain: str, positions: List[int], level: Optional[int]) -> List[Tuple[str, List[int]]]:
"""
Generate all possible homographs with given level of substitution.
Args:
domain (str): Original domain name.
positions (List[int]): Indexes of substitutable letters.
level (Optional[int]): Max substitutions.
Returns:
List[Tuple[str, List[int]]]: List of (homograph, substituted positions).
"""
if not positions:
return []
max_subs = level if level else len(positions)
results = []
for n in range(1, max_subs + 1):
for subset in itertools.combinations(positions, n):
domain_list = list(domain)
for idx in subset:
domain_list[idx] = HOMOGLYPHS[domain_list[idx]]
results.append(("".join(domain_list), list(subset)))
return results
def to_punycode(domain: str) -> str:
"""
Convert a domain to Punycode.
Args:
domain (str): Domain to encode.
Returns:
str: Punycode string.
"""
return idna.encode(domain).decode('ascii')
def parse_arguments() -> argparse.Namespace:
"""
Parse CLI arguments.
Returns:
argparse.Namespace: Parsed arguments.
"""
parser = argparse.ArgumentParser(description="Generate IDN homographs for a domain.")
parser.add_argument('--domain', required=True, help='Domain name (without TLD)')
parser.add_argument('--level', type=int, help='Max number of substitutions (optional)')
parser.add_argument('--style', choices=STYLE_MAP.keys(), default='any',
help='Substitution style: any, realistic, very-realistic')
parser.add_argument('--tlds', nargs='+', default=['.com', '.fr', '.net', '.org'],
help='List of TLDs (default: .com .fr .net .org)')
return parser.parse_args()
def main() -> None:
"""
Main function to orchestrate homograph generation.
"""
args = parse_arguments()
domain = args.domain.lower()
level = args.level
style = args.style
tlds = args.tlds
# Finding letters
substitutable_positions = find_substitutable_letters(domain, style)
# Manage empty cases or impossible constraints
if not substitutable_positions:
print(f"[⚠️] No substitutable letters found in '{domain}' for style '{style}'.")
return
if level and level > len(substitutable_positions):
print(f"[⚠️] Not enough letters to substitute {level} times. Found {len(substitutable_positions)}.")
return
print(f"\n🎯 Generating homographs for: '{domain}' with TLDs: {', '.join(tlds)}")
print(f"Style: {style} | Max substitutions: {level if level else 'All possible'}\n")
# Generate homographs
homographs = generate_homographs(domain, substitutable_positions, level)
if not homographs:
print("[⚠️] No homographs could be generated with current settings.")
return
# Display results
for homog, positions in homographs:
puny = to_punycode(homog)
subs_letters = ', '.join([f"{domain[idx]}→{HOMOGLYPHS[domain[idx]]}" for idx in positions])
for tld in tlds:
full_domain = homog + tld
puny_domain = puny + tld
print(f"➡️ Domain: {full_domain}")
print(f" Punycode: {puny_domain}")
print(f" Replacements: {subs_letters}")
print("--------------------------------------------------------")
if __name__ == "__main__":
main()