├── LICENSE ├── README.md ├── core ├── __init__.py ├── __init__.pyc ├── common.py ├── common.pyc ├── confusable.py ├── confusable.pyc ├── creator.py ├── creator.pyc ├── domain.py ├── domain.pyc ├── exceptions.py ├── exceptions.pyc ├── logger.py ├── logger.pyc ├── phishingdomain.py ├── phishingdomain.pyc ├── phishingtest.py ├── phishingtest.pyc ├── vt_scan.py └── vt_scan.pyc ├── misc ├── FILES.md ├── blacklist_letters.json ├── charset.json ├── letters.json └── whitelist_letters.json ├── punydomaincheck.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 anilyuk 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 | # Puny Domain Check 2 | 3 | Punycode is a special encoding used to convert Unicode characters to ASCII, which is a smaller, restricted character set. Punycode is used to encode internationalized domain names (IDN). 4 | 5 | Confusable characters are from "http://www.unicode.org/Public/security/latest/confusables.txt". Update character set regularly to take new confusable characters! 6 | 7 | To add new letters read [FILES.md](misc/FILES.md). 8 | 9 | Punycode alternative domain names are widely used for phishing attacks. This tool helps to identify alternative domain names and look for several information of that domanin. It looks for DNS info, Whois info and open ports (TCP/443, TCP/80), Virustotal API. 10 | 11 | When it finds a domain name with IP address, it checks similarities between original domain and warns you for phishing attack 12 | 13 | #### Requirements: #### 14 | * Virustotal API Key 15 | * Download geoip database and put the database to "./misc" folder: 16 | https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz 17 | 18 | 19 | #### Python requirements: #### 20 | * coloredlogs 21 | * pythonwhois 22 | * bs4 23 | * dnspython 24 | * requests 25 | * ratelimit 26 | * tabulate 27 | 28 | #### Usage: #### 29 | python punydomaincheck.py -d yourdomain -s com -os com -op 443 -c 2 30 | 31 | -u --update Update character set 32 | --debug Enable debug logging 33 | -d --domain Domain without prefix and suffix. (google) 34 | -s --suffix Suffix to check alternative domain names. (.com, .net) 35 | -c --count Character count to change with punycode alternative 36 | -os --original_suffix Original domain to check for phisihing 37 | -op --original_port Original port to check for phisihing 38 | -f --force Force to create alternative domain names 39 | -t --thread Thread count (Default: 10) 40 | 41 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/__init__.py -------------------------------------------------------------------------------- /core/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/__init__.pyc -------------------------------------------------------------------------------- /core/common.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | from sys import stdout, platform 7 | from core.logger import LOG_HEADER 8 | 9 | VERSION = "1.0.4" 10 | CONFUSABLE_URL = "http://www.unicode.org/Public/security/latest/confusables.txt" 11 | CONFUSABLE_FILE = "./misc/confusables.txt" 12 | BLACKLIST_LETTERS = "./misc/blacklist_letters.json" 13 | WHITELIST_LETTERS = "./misc/whitelist_letters.json" 14 | CHARSET_FILE = "./misc/charset.json" 15 | LETTERS_FILE = "./misc/letters.json" 16 | MAX_THREAD_COUNT = 7 17 | OUTPUT_DIR = "./output" 18 | GEOLOCATION_DATABASE_FILE = "./misc/GeoLite2-City.mmdb" 19 | ### YOUR VIRUSTOTAL API KEYs, Add more API keys to increase request limit. Free account has 4 request per minute limitis. 20 | VT_APIKEY_LIST = [] 21 | 22 | BANNER = ''' _ __ _ _ _ __ _ _ ___| |__ ___ ___| | __ 23 | | '_ \| | | | '_ \| | | |/ __| '_ \ / _ \/ __| |/ / 24 | | |_) | |_| | | | | |_| | (__| | | | __/ (__| < 25 | | .__/ \__,_|_| |_|\__, |\___|_| |_|\___|\___|_|\_\\ 26 | |_| |___/ {} 27 | '''.format(VERSION) 28 | 29 | 30 | # Set console colors 31 | if platform != 'win32' and stdout.isatty(): 32 | YEL = '\x1b[33m' 33 | MAG = '\x1b[35m' 34 | BLU = '\x1b[34m' 35 | GRE = '\x1b[32m' 36 | RED = '\x1b[31m' 37 | RST = '\x1b[39m' 38 | CYA = '\x1b[36m' 39 | 40 | 41 | else: 42 | YEL = '' 43 | MAG = '' 44 | GRE = '' 45 | RED = '' 46 | BLU = '' 47 | CYA = '' 48 | RST = '' 49 | 50 | 51 | def alternative_filename(args, output_dir): 52 | return "{}/{}_{}char_alternatives".format(output_dir, args.domain, args.count) 53 | 54 | 55 | def print_percentage(args, logger, current, total=0, last_percentage=0, header_print=False): 56 | if total != 0: 57 | percentage = int((100 * current) / total) 58 | else: 59 | percentage = current 60 | 61 | if not header_print and not args.debug: 62 | stdout.write("{}Processing: {}0%".format(LOG_HEADER, BLU)) 63 | header_print = True 64 | stdout.flush() 65 | 66 | if percentage % 10 == 0 and last_percentage != percentage and percentage != 0: 67 | 68 | last_percentage = percentage 69 | if args.debug: 70 | 71 | logger.info("[*] Processing... {}{}{}".format(BLU, percentage, RST)) 72 | 73 | else: 74 | 75 | if percentage == 100: 76 | 77 | string_stdout = "...{}%{}\n".format(percentage, RST) 78 | 79 | else: 80 | 81 | string_stdout = "...{}%".format(percentage) 82 | 83 | stdout.write(string_stdout) 84 | 85 | stdout.flush() 86 | 87 | return last_percentage, header_print 88 | -------------------------------------------------------------------------------- /core/common.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/common.pyc -------------------------------------------------------------------------------- /core/confusable.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | 7 | from urllib import urlretrieve, urlopen 8 | from copy import deepcopy 9 | import json 10 | from core.common import CONFUSABLE_URL, CONFUSABLE_FILE, BLACKLIST_LETTERS, CHARSET_FILE, WHITELIST_LETTERS 11 | from os import remove 12 | 13 | 14 | def update_charset(logger, letters_json): 15 | logger.info("[*] Updating character list") 16 | 17 | # Download confusables.txt 18 | u = urlopen(CONFUSABLE_URL) 19 | filesize = int(u.info().getheaders("Content-Length")[0]) / 1000 20 | 21 | logger.info("[*] Downloading {}kb".format(filesize)) 22 | 23 | urlretrieve(CONFUSABLE_URL, CONFUSABLE_FILE) 24 | 25 | logger.info("[*] Download completed. Processing now...") 26 | 27 | confusables = open(CONFUSABLE_FILE) 28 | 29 | whitelist_file = open(WHITELIST_LETTERS) 30 | whitelist_file_json = json.load(whitelist_file) 31 | whitelist_file.close() 32 | 33 | new_letters_json = deepcopy(letters_json) 34 | 35 | for char_num in range(97, 123): 36 | new_letters_json[str(chr(char_num))] = [] 37 | 38 | # First, get characters from whitelist_letters.json file 39 | for char_num in range(97, 123): 40 | 41 | for char in whitelist_file_json[str(chr(char_num))]: 42 | 43 | try: 44 | 45 | with_idna = (unicode(char).decode('unicode_escape')).encode("idna") 46 | 47 | except: 48 | 49 | logger.error("[-] Punny Code error for {}".format(char)) 50 | 51 | None 52 | 53 | else: 54 | 55 | # if "xn" in with_idna: 56 | character_array = list(new_letters_json[str(chr(char_num))]) 57 | character_array.append(char) 58 | new_letters_json[str(chr(char_num))] = character_array 59 | 60 | blacklist_letters = open(BLACKLIST_LETTERS) 61 | blacklist_letters_json = json.load(blacklist_letters) 62 | 63 | # Read characters from confusable and check if character can be punycode encodede 64 | for line in confusables: 65 | 66 | for char_num in range(97, 123): 67 | 68 | letter_normal = str(chr(char_num)) 69 | letter_unicode = letters_json[str(chr(char_num))] 70 | 71 | if str(letter_unicode[0]) in line: 72 | 73 | line_spilt = line.split(";") 74 | 75 | if len(line_spilt) > 1: 76 | 77 | if len(line_spilt[1].rstrip().split(" ")) < 2: 78 | 79 | line_spilt[0].rstrip() 80 | 81 | character = str(line_spilt[0]).rstrip() 82 | 83 | if character not in blacklist_letters_json[chr(char_num)]: 84 | 85 | if len(character) == 4: 86 | 87 | character = unicode('\u{}'.format(character)) 88 | 89 | else: 90 | 91 | character = '\u' + "0" * (8 - len(character)) + character 92 | 93 | try: 94 | 95 | with_idna = (unicode(character).decode('unicode_escape')).encode("idna") 96 | 97 | except: 98 | 99 | None 100 | 101 | else: 102 | 103 | if "xn" in with_idna: 104 | character_array = list(new_letters_json[letter_normal]) 105 | character_array.append(character) 106 | new_letters_json[letter_normal] = character_array 107 | 108 | logger.info("[*] Charset successfully created") 109 | 110 | charset = open(CHARSET_FILE, "w") 111 | json.dump(new_letters_json, charset) 112 | confusables.close() 113 | remove(CONFUSABLE_FILE) 114 | charset.close() 115 | -------------------------------------------------------------------------------- /core/confusable.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/confusable.pyc -------------------------------------------------------------------------------- /core/creator.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | import json 7 | from core.exceptions import CharSetException, AlternativesExists, NoAlternativesFound 8 | from itertools import product, combinations 9 | from datetime import datetime 10 | from core.common import LETTERS_FILE, CHARSET_FILE, alternative_filename, print_percentage, BLU, RST, GRE 11 | from os.path import isfile 12 | 13 | 14 | def load_letters(): 15 | json_data = open(LETTERS_FILE) 16 | letters_json = json.load(json_data) 17 | json_data.close() 18 | 19 | return letters_json 20 | 21 | 22 | def load_charset(): 23 | try: 24 | 25 | charset = open(CHARSET_FILE) 26 | charset_json = json.load(charset) 27 | charset.close() 28 | 29 | except: 30 | 31 | raise CharSetException("CharSet not found") 32 | 33 | return charset_json 34 | 35 | 36 | def calculate_alternative_count(domain_name, charset_json, combination): 37 | count = 1 38 | total_count = 0 39 | 40 | for positions in combination: 41 | 42 | for i in range(0, len(domain_name)): 43 | 44 | if i in positions: 45 | count = count * len(charset_json[domain_name[i]]) 46 | 47 | total_count += count 48 | count = 1 49 | if total_count == 0: 50 | raise NoAlternativesFound 51 | return total_count 52 | 53 | 54 | def create_alternatives(args, charset_json, logger, output_dir): 55 | alternatives_filename = alternative_filename(args=args, output_dir=output_dir) 56 | 57 | domain_name = str(args.domain).split(".")[0] 58 | 59 | combination = list(combinations(range(0, len(domain_name)), int(args.count))) 60 | 61 | total_alternative_count = calculate_alternative_count(domain_name, charset_json, combination) 62 | 63 | alternative_count = 0 64 | last_percentage = 1 65 | header_print = False 66 | 67 | logger.info( 68 | "[*] {}{}{} alternatives found for {}{}.{}{}".format(BLU, total_alternative_count, RST, GRE, domain_name, 69 | args.suffix, RST)) 70 | 71 | if isfile(alternatives_filename) and not args.force: 72 | raise AlternativesExists 73 | 74 | alternatives_file = open(alternatives_filename, 'w') 75 | 76 | logger.info( 77 | "[*] Creating idna domain names for {} and {} character will be changed".format(domain_name, args.count)) 78 | 79 | logger.info("[*] {}".format(datetime.now())) 80 | 81 | for positions in combination: 82 | 83 | character_alternative_list = [] 84 | 85 | for i in range(0, len(domain_name)): 86 | 87 | if i in positions: 88 | 89 | try: 90 | 91 | character_alternative_list.append(charset_json[domain_name[i]]) 92 | 93 | except KeyError, k: 94 | 95 | character_alternative_list.append(domain_name[i]) 96 | 97 | else: 98 | 99 | character_alternative_list.append(domain_name[i]) 100 | 101 | all_alternatives_product = (product(*character_alternative_list)) 102 | 103 | for item in all_alternatives_product: 104 | 105 | temp_str = "".join(item) 106 | 107 | temp_str_unicode = (unicode(temp_str).decode('unicode_escape')) 108 | try: 109 | 110 | with_idna = temp_str_unicode.encode('idna') 111 | # if "xn" not in with_idna: 112 | #print "{} - {}".format(temp_str, with_idna) 113 | 114 | except: 115 | 116 | logger.error("[-] PunyCode problem: {}".format(temp_str)) 117 | 118 | else: 119 | 120 | alternatives_file.write("{}\n".format(with_idna)) 121 | 122 | alternative_count += 1 123 | 124 | last_percentage, header_print = print_percentage(args, logger, alternative_count, total_alternative_count, 125 | last_percentage, header_print) 126 | 127 | alternatives_file.close() 128 | logger.info("[*] {}".format(datetime.now())) 129 | -------------------------------------------------------------------------------- /core/creator.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/creator.pyc -------------------------------------------------------------------------------- /core/domain.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | from core.common import alternative_filename, GEOLOCATION_DATABASE_FILE, VT_APIKEY_LIST 7 | import geoip2.database 8 | from threading import Thread 9 | import dns.resolver 10 | from phishingdomain import PhishingDomain 11 | from pythonwhois import get_whois 12 | from phishingtest import CheckPhishing 13 | import requests 14 | from os.path import isfile 15 | 16 | class dns_client(Thread): 17 | def __init__(self, args, logger, domain_list, output_queue, thread_name): 18 | 19 | Thread.__init__(self) 20 | 21 | self.domain_list = domain_list 22 | self.output_queue = output_queue 23 | self.output_list = [] 24 | self.args = args 25 | self.logger = logger 26 | self.thread_name = thread_name 27 | self.resolver = None 28 | self.percentage = 0 29 | 30 | def run(self): 31 | 32 | count = 0 33 | last_percentage = 1 34 | total = len(self.domain_list) 35 | 36 | for domain in self.domain_list: 37 | 38 | if len(domain) > 1: 39 | query = domain + "." + self.args.suffix 40 | dns_result = self.query_dns(query) 41 | 42 | if dns_result: 43 | 44 | whois_result = self.query_whois(query) 45 | 46 | for answer in dns_result: 47 | result = PhishingDomain(domain_name=domain, ipaddress=str(answer), 48 | whois_result=whois_result) 49 | 50 | if isfile(GEOLOCATION_DATABASE_FILE): 51 | geolocation_result = self.query_geolocation(ip_address=answer) 52 | result.set_geolocation(geolocation=geolocation_result) 53 | 54 | if self.args.domain and self.args.original_suffix: 55 | 56 | original_domain = "{}.{}".format(self.args.domain, self.args.original_suffix) 57 | similarity = CheckPhishing(or_domain=original_domain, or_site_port=self.args.original_port, 58 | test_domain=str(query), logger=self.logger) 59 | result.set_similarity(similarity=similarity) 60 | 61 | self.output_list.append(result) 62 | 63 | count += 1 64 | 65 | self.percentage = int((100 * count) / total) 66 | if last_percentage != self.percentage: 67 | # self.logger.info("Thread{} - {}% Completed".format(self.thread_name, percentage)) 68 | last_percentage = self.percentage 69 | 70 | self.output_queue.put(self.output_list) 71 | 72 | def get_percentage(self): 73 | 74 | return self.percentage 75 | 76 | def query_dns(self, query): 77 | 78 | try: 79 | self.resolver = dns.resolver.Resolver() 80 | self.resolver.timeout = 5 81 | #self.resolver.lifetime = 1 82 | # ip_address = gethostbyname(query) 83 | answers = self.resolver.query(query) 84 | 85 | # except gaierror, g: 86 | except dns.resolver.NXDOMAIN, n: 87 | self.logger.debug("[-] {} for {}".format(n, str(query))) 88 | return None 89 | 90 | except dns.resolver.Timeout, t: 91 | self.logger.debug("[-] {} for {}".format(t, str(query))) 92 | return None 93 | except dns.resolver.NoNameservers, n: 94 | self.logger.debug("[-] {} for {}".format(n, str(query))) 95 | return None 96 | 97 | except Exception, e: 98 | self.logger.debug("[-] {} for {}".format(e, str(query))) 99 | return None 100 | 101 | else: 102 | 103 | if answers: 104 | return answers 105 | else: 106 | return None 107 | 108 | def query_whois(self, query): 109 | 110 | try: 111 | 112 | whois_result = get_whois(domain=query) 113 | 114 | if "raw" in whois_result: 115 | 116 | if "No match" in str(whois_result["raw"][0]): 117 | return None 118 | 119 | else: 120 | return whois_result 121 | 122 | else: 123 | 124 | return None 125 | 126 | except UnicodeDecodeError, e: 127 | 128 | self.logger.debug(e) 129 | return None 130 | 131 | except UnicodeEncodeError, e: 132 | 133 | self.logger.debug(e) 134 | return None 135 | 136 | except Exception, e: 137 | 138 | self.logger.debug("[-] {} for {}".format(e, str(query))) 139 | return None 140 | 141 | def query_geolocation(self, ip_address): 142 | 143 | try: 144 | reader = geoip2.database.Reader(GEOLOCATION_DATABASE_FILE) 145 | response = reader.city(ip_address=str(ip_address)) 146 | 147 | except TypeError, e: 148 | self.logger.warn(e) 149 | self.logger.warn(ip_address) 150 | return None 151 | except IOError, e: 152 | self.logger.warn("[-] Download GeoIP Database to query geolocation!") 153 | return None 154 | else: 155 | return response 156 | 157 | def load_domainnames(args, output_dir): 158 | 159 | alternatives_file = open(alternative_filename(args, output_dir)) 160 | alternatives = alternatives_file.read().split("\n") 161 | 162 | thread_count = int(args.thread) 163 | if len(alternatives) < thread_count: thread_count = len(alternatives) 164 | alternatives_list = create_chunks(alternatives, int(thread_count)) 165 | 166 | return alternatives_list 167 | 168 | 169 | def create_chunks(seq, num): 170 | avg = len(seq) / float(num) 171 | out = [] 172 | last = 0.0 173 | 174 | while last < len(seq): 175 | out.append(seq[int(last):int(last + avg)]) 176 | last += avg 177 | 178 | return out 179 | -------------------------------------------------------------------------------- /core/domain.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/domain.pyc -------------------------------------------------------------------------------- /core/exceptions.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | class CharSetException(Exception): 7 | pass 8 | 9 | 10 | class AlternativesExists(Exception): 11 | pass 12 | 13 | 14 | class NoAlternativesFound(Exception): 15 | pass 16 | -------------------------------------------------------------------------------- /core/exceptions.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/exceptions.pyc -------------------------------------------------------------------------------- /core/logger.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | from logging import DEBUG, INFO, getLogger 7 | from coloredlogs import install 8 | from os import environ 9 | 10 | LOG_HEADER = "[*] " 11 | 12 | 13 | def start_logger(args): 14 | logger = getLogger("PunyCodeCheck") 15 | environ["COLOREDLOGS_LOG_FORMAT"] = '%(message)s' 16 | 17 | if args.debug: 18 | 19 | logger.setLevel(DEBUG) 20 | install(DEBUG, logger=logger) 21 | 22 | else: 23 | 24 | logger.setLevel(INFO) 25 | install(INFO, logger=logger) 26 | 27 | return logger 28 | -------------------------------------------------------------------------------- /core/logger.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/logger.pyc -------------------------------------------------------------------------------- /core/phishingdomain.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | class PhishingDomain: 7 | 8 | def __init__(self, domain_name, ipaddress, whois_result=""): 9 | 10 | self.domain_name = domain_name 11 | self.ipaddress = ipaddress 12 | self.whois_result = whois_result 13 | self.similarity = "" 14 | self.geolocation = None 15 | self.vt_result = None 16 | 17 | def get_domain_name(self): 18 | 19 | return self.domain_name 20 | 21 | def get_ipaddress(self): 22 | 23 | return self.ipaddress 24 | 25 | def get_whois_result(self): 26 | 27 | return self.whois_result 28 | 29 | def set_whois_result(self, whois_result): 30 | 31 | self.whois_result = whois_result 32 | 33 | def set_similarity(self, similarity): 34 | 35 | self.similarity = similarity 36 | 37 | def get_similarity(self): 38 | 39 | return self.similarity 40 | 41 | def set_geolocation(self, geolocation): 42 | 43 | self.geolocation = geolocation 44 | 45 | def get_geolocation(self): 46 | 47 | return self.geolocation 48 | 49 | def get_vt_result(self): 50 | 51 | return self.vt_result 52 | 53 | def set_vt_result(self, vt_result): 54 | 55 | self.vt_result = vt_result -------------------------------------------------------------------------------- /core/phishingdomain.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/phishingdomain.pyc -------------------------------------------------------------------------------- /core/phishingtest.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | import socket 7 | from requests import get, packages, exceptions 8 | from bs4 import BeautifulSoup 9 | from difflib import SequenceMatcher 10 | import re 11 | 12 | SOCKET_TIMEOUT_SECONDS = 1 13 | 14 | 15 | class PhishingTest: 16 | similarity_checks = None # SimilarityCheck object 17 | ip_match = None # Boolean comparison 18 | port_match = None # Boolean comparison 19 | 20 | def __init__(self): 21 | self.similarity_checks = None 22 | self.ip_match = None 23 | self.port_match = None 24 | 25 | def toDictionary(self): 26 | temp = [] 27 | if self.similarity_checks: 28 | for item in self.similarity_checks: 29 | temp.append(item.toDictionary()) 30 | toReturn = {"ip_match": self.ip_match, "port_match": self.port_match, "similarity_checks": temp} 31 | return toReturn 32 | 33 | 34 | class SimilarityCheck: 35 | test_url = None # String url 36 | visible_text = None # Double ratio 37 | external_link = None # Double ratio 38 | external_link_site = None 39 | 40 | def __init__(self): 41 | pass 42 | 43 | def toDictionary(self): 44 | toReturn = {"test_url": self.test_url, "visible_text": self.visible_text, "external_link": self.external_link, 45 | "external_link_site": self.external_link_site 46 | } 47 | return toReturn 48 | 49 | 50 | class HttpResponse: 51 | url = None 52 | status_code = None 53 | source_code = None 54 | 55 | def __init__(self, url=None, status_code=None, source_code=None): 56 | self.url = url 57 | self.status_code = status_code 58 | self.source_code = source_code 59 | 60 | def toDictionary(self): 61 | toReturn = {"url": self.url, "status_code": self.status_code, "source_code": self.source_code} 62 | return toReturn 63 | 64 | 65 | def checkOpenPorts(ip_address, ports=-1): 66 | open_ports = [] 67 | 68 | if ports == -1 or len(ports) < 1: 69 | ports = [80, 443] 70 | 71 | for port in ports: 72 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 73 | sock.settimeout(SOCKET_TIMEOUT_SECONDS) 74 | result = sock.connect_ex((ip_address, port)) 75 | if result == 0: 76 | open_ports.append(port) 77 | sock.close() 78 | 79 | return open_ports 80 | 81 | 82 | def getIPAddress(domain_name): 83 | try: 84 | return socket.gethostbyname(domain_name) 85 | except: 86 | return None 87 | 88 | 89 | def addProtocol(domain_name, http_port=80): 90 | if http_port == 80: 91 | return "http://" + domain_name 92 | else: 93 | return "https://" + domain_name 94 | 95 | 96 | def makeRequest(url, logger, redirect=True, proxies={}): 97 | packages.urllib3.disable_warnings() 98 | try: 99 | 100 | response = get(url, allow_redirects=redirect, verify=False, proxies=proxies, timeout=25) 101 | newurl = "{}/{}".format(url, meta_redirect(response.text)) 102 | 103 | while newurl: 104 | response = get(newurl, allow_redirects=redirect, verify=False, proxies=proxies, timeout=25) 105 | newurl = meta_redirect(response.text) 106 | 107 | except exceptions.ConnectionError, e: 108 | 109 | logger.debug("[-] {} - {}".format(url, str(e))) 110 | return 111 | 112 | except exceptions.ReadTimeout, e: 113 | 114 | logger.debug("[-] {} - {}".format(url, str(e))) 115 | return 116 | 117 | else: 118 | 119 | return parseResponse(response) 120 | 121 | 122 | def meta_redirect(content): 123 | soup = BeautifulSoup(content, "html.parser") 124 | result = soup.find("meta", attrs={"http-equiv": re.compile("^refresh", re.I)}) 125 | 126 | if result and ["content"] in result: 127 | 128 | wait, text = result["content"].split(";") 129 | if text.strip().lower().startswith("url="): 130 | url = text[4:] 131 | return url 132 | return "" 133 | 134 | 135 | def parseResponse(requests_response): 136 | toReturn = HttpResponse() 137 | 138 | toReturn.url = requests_response.url 139 | toReturn.status_code = requests_response.status_code 140 | toReturn.source_code = requests_response.text.encode("utf-8") 141 | 142 | return toReturn 143 | 144 | 145 | def checkVisibleTextSimilarity(original_source, test_source): 146 | original_soup = BeautifulSoup(original_source, 'html.parser') 147 | test_soup = BeautifulSoup(test_source, 'html.parser') 148 | 149 | original_text = grabText(original_soup) 150 | original_text = makeSingleString(original_text) 151 | 152 | test_text = grabText(test_soup) 153 | test_text = makeSingleString(test_text) 154 | 155 | return similarity(original_text, test_text) 156 | 157 | 158 | def checkExternalLinkSimilarities(original_source, test_source, or_url): 159 | original_soup = BeautifulSoup(original_source, 'html.parser') 160 | test_soup = BeautifulSoup(test_source, 'html.parser') 161 | 162 | original_links = grabLinks(original_soup) 163 | test_links = grabLinks(test_soup) 164 | 165 | if len(original_links) == 0 or len(test_links) == 0: 166 | 167 | return (0, 0) 168 | 169 | else: 170 | 171 | return (calculateLinkSimilarity(original_links, test_links), checkOriginalDomainExist(or_url, test_links)) 172 | 173 | 174 | def grabText(soup): 175 | toReturn = soup.findAll(text=True) 176 | return filter(visibleTextFilter, toReturn) 177 | 178 | 179 | def visibleTextFilter(element): 180 | tags = ['style', 'script', '[document]', 'head', 'link'] 181 | if element.parent.name in tags: 182 | return False 183 | elif re.match('', str(element.encode("utf-8"))): 184 | return False 185 | elif element.name in tags: 186 | return False 187 | else: 188 | return True 189 | 190 | 191 | def makeSingleString(str_list): 192 | toReturn = "" 193 | for item in str_list: 194 | toReturn = toReturn + item 195 | 196 | toReturn = toReturn.encode("utf-8") 197 | toReturn = "".join(toReturn.split()) 198 | return toReturn 199 | 200 | 201 | def similarity(a, b): 202 | return str(round(SequenceMatcher(None, a, b).ratio() * 100, 2)) 203 | 204 | 205 | def grabLinks(soup): 206 | toReturn = [] 207 | toReturn = toReturn + find_list_resources('img', "src", soup) 208 | toReturn = toReturn + find_list_resources('script', "src", soup) 209 | toReturn = toReturn + find_list_resources("link", "href", soup) 210 | toReturn = toReturn + find_list_resources("a", "href", soup) 211 | toReturn = toReturn + find_list_resources("video", "src", soup) 212 | toReturn = toReturn + find_list_resources("audio", "src", soup) 213 | toReturn = toReturn + find_list_resources("iframe", "src", soup) 214 | toReturn = toReturn + find_list_resources("embed", "src", soup) 215 | toReturn = toReturn + find_list_resources("object", "data", soup) 216 | toReturn = toReturn + find_list_resources("source", "src", soup) 217 | toReturn = toReturn + find_list_resources("form", "action", soup) 218 | return toReturn 219 | 220 | 221 | def find_list_resources(tag, attribute, soup): 222 | toReturn = [] 223 | for x in soup.findAll(tag): 224 | try: 225 | toReturn.append(x[attribute]) 226 | except KeyError: 227 | pass 228 | return toReturn 229 | 230 | 231 | def calculateLinkSimilarity(original_links, test_links): 232 | total_original_links = len(original_links) 233 | 234 | for test_link in test_links: 235 | 236 | for i in range(0, len(original_links)): 237 | if test_link == original_links[i]: 238 | original_links.pop(i) 239 | break 240 | 241 | remaning_original_links = len(original_links) 242 | 243 | return (total_original_links - remaning_original_links) * 100 / total_original_links 244 | 245 | 246 | def checkOriginalDomainExist(or_site, test_links): 247 | count = 0 248 | 249 | for test_link in test_links: 250 | 251 | if or_site in test_link: 252 | count += 1 253 | 254 | return (100 * count) / len(test_links) 255 | 256 | 257 | def CheckPhishing(or_domain, or_site_port, test_domain, logger): 258 | test_result = PhishingTest() 259 | 260 | or_url = addProtocol(or_domain, or_site_port) 261 | 262 | or_ip = getIPAddress(or_domain) 263 | 264 | or_site = makeRequest(or_url, logger) 265 | 266 | test_ip = getIPAddress(test_domain) 267 | 268 | if test_ip and or_ip and or_site: 269 | test_ports = checkOpenPorts(test_ip) 270 | test_urls = [] 271 | for port in test_ports: 272 | test_urls.append(addProtocol(test_domain, port)) 273 | 274 | similarity_checks = [] 275 | for test_url in test_urls: 276 | test_site = makeRequest(test_url, logger) 277 | if test_site: 278 | temp = SimilarityCheck() 279 | temp.test_url = test_url 280 | or_site.toDictionary() 281 | # print test_site.source_code 282 | temp.visible_text = checkVisibleTextSimilarity(or_site.source_code, test_site.source_code) 283 | temp.external_link, temp.external_link_site = checkExternalLinkSimilarities(or_site.source_code, 284 | test_site.source_code, 285 | or_domain) 286 | similarity_checks.append(temp) 287 | 288 | test_result.similarity_checks = similarity_checks 289 | test_result.ip_match = (or_ip == test_ip) 290 | test_result.port_match = (len(test_ports) == 1 and test_ports[0] == or_site_port) 291 | 292 | http_similarity = None 293 | https_similarity = None 294 | 295 | test_result = test_result.toDictionary() 296 | 297 | if not test_result["ip_match"]: 298 | 299 | similarity_checks = test_result["similarity_checks"] 300 | 301 | for item in similarity_checks: 302 | 303 | if float(item["visible_text"]) > 40 or float(item["external_link"]) > 40 or float( 304 | item["external_link_site"]): 305 | 306 | if "https://" in item["test_url"]: 307 | 308 | https_similarity = True 309 | 310 | else: 311 | 312 | http_similarity = True 313 | 314 | else: 315 | 316 | if "https://" in item["test_url"]: 317 | 318 | https_similarity = False 319 | 320 | else: 321 | 322 | http_similarity = False 323 | 324 | return {"http_similarity": http_similarity, "https_similarity": https_similarity} 325 | 326 | else: 327 | 328 | return {"http_similarity": False, "https_similarity": False} 329 | -------------------------------------------------------------------------------- /core/phishingtest.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/phishingtest.pyc -------------------------------------------------------------------------------- /core/vt_scan.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | import requests 7 | from ratelimit import rate_limited 8 | from core.common import VT_APIKEY_LIST 9 | from requests import packages 10 | 11 | # Configurations 12 | 13 | # Global Parameters 14 | apikey_cursor = 0 15 | vt_api_request_limit = 4 16 | 17 | # Request URLs 18 | vt_scan_url = "https://www.virustotal.com/vtapi/v2/url/scan" 19 | vt_report_url = "http://www.virustotal.com/vtapi/v2/{type}/report" 20 | 21 | # Constants values 22 | http_method_get = "GET" 23 | http_method_post = "POST" 24 | 25 | vt_report_type_url = "url" 26 | vt_report_type_domain = "domain" 27 | vt_paramkey_resource = "resource" 28 | vt_request_param_url = "url" 29 | vt_request_param_domain = "domain" 30 | vt_request_param_apikey = "apikey" 31 | vt_request_param_scan = "scan" 32 | vt_scan_scan_id = "scan_id" 33 | vt_report_key_scans = "scans" 34 | vt_report_key_positives = "positives" 35 | vt_report_total = "total" 36 | vt_report_key_subdomains = "subdomains" 37 | vt_report_key_response_code = "response_code" 38 | 39 | 40 | def scanURL(url): 41 | try: 42 | vt_url_result = virusTotalURLScan(url) 43 | vt_domain_search_result = virusTotalDomainSearch(url) 44 | 45 | return dict(vt_url_result, **vt_domain_search_result) 46 | except ValueError as err: 47 | return None 48 | except TypeError: 49 | return None 50 | 51 | 52 | def virusTotalURLScan(url): 53 | params = {vt_request_param_url: url} 54 | scan_response = makeRequest(vt_scan_url, params, http_method_post) 55 | scan_response = scan_response.json() 56 | 57 | scan_report = virusTotalReport(vt_report_type_url, vt_paramkey_resource, scan_response[vt_scan_scan_id], 58 | http_method_post) 59 | 60 | return getScanReportResults(scan_report) 61 | 62 | 63 | def virusTotalDomainSearch(domain): 64 | report = virusTotalReport(vt_report_type_domain, vt_request_param_domain, domain, http_method_get) 65 | 66 | return getDomainReportResults(report) 67 | 68 | 69 | def virusTotalReport(report_type, report_type_param_key, report_id, http_method): 70 | params = {vt_request_param_scan: 1, report_type_param_key: report_id} 71 | 72 | report_url = vt_report_url.format(type=report_type) 73 | 74 | report = makeRequest(report_url, params, http_method) 75 | report = report.json() 76 | 77 | return report 78 | 79 | 80 | def getScanReportResults(report): 81 | to_return = {} 82 | 83 | if report and report.has_key(vt_report_key_response_code) and int(report[vt_report_key_response_code]) == 1: 84 | 85 | if report.has_key(vt_report_key_positives): 86 | to_return[vt_report_key_positives] = report[vt_report_key_positives] 87 | 88 | if report.has_key(vt_report_total): 89 | to_return[vt_report_total] = report[vt_report_total] 90 | 91 | return to_return 92 | else: 93 | return None 94 | 95 | 96 | def getDomainReportResults(report): 97 | to_return = {} 98 | 99 | if report and report.has_key(vt_report_key_response_code) and int(report[vt_report_key_response_code]) == 1: 100 | 101 | if report.has_key(vt_report_key_subdomains) and report[vt_report_key_subdomains]: 102 | to_return[vt_report_key_subdomains] = report[vt_report_key_subdomains] 103 | 104 | return to_return 105 | else: 106 | return None 107 | 108 | 109 | #@rate_limited(vt_api_request_limit * len(VT_APIKEY_LIST), 110 | # 60) # virustotal api has request limit: 4 request per minute 111 | def makeRequest(url, params, http_method): 112 | params[vt_request_param_apikey] = changeApiKey() 113 | packages.urllib3.disable_warnings() 114 | if http_method == http_method_post: 115 | return requests.post(url, params=params, verify=False, timeout=25) 116 | else: 117 | return requests.get(url, params=params, verify=False, timeout=25) 118 | 119 | 120 | def changeApiKey(): 121 | global apikey_cursor 122 | 123 | apikey_cursor = apikey_cursor + 1 124 | 125 | if apikey_cursor >= len(VT_APIKEY_LIST): 126 | apikey_cursor = 0 127 | 128 | return VT_APIKEY_LIST[apikey_cursor] 129 | -------------------------------------------------------------------------------- /core/vt_scan.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anilyuk/punydomaincheck/da5e25edabc1f9509c7d99bb9c6be0ab0312ac62/core/vt_scan.pyc -------------------------------------------------------------------------------- /misc/FILES.md: -------------------------------------------------------------------------------- 1 | If you want to add new alternative to blacklist or whitelist, you must use unicode values. After you add new entries, run tool with "-u" parameter. 2 | 3 | ### whitelist_letters.json ### 4 | Stores letters, that aren't in unicode.org's confusable list, can be used for alternative creation. 5 | For example, "ii" can be alternative for "i". 6 | 7 | ### blacklist_letters.json ### 8 | Stores letters that can't be used to create punycode domains. 9 | 10 | ### charset.json ### 11 | Stores alternative letters which is created using; 12 | - Confusables list 13 | - blacklist_letters.json 14 | - whitelist_letters.json 15 | 16 | ### letters.json ### 17 | Stores unicode value for every letter. 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /misc/blacklist_letters.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": [""], 3 | "b": ["13CF"], 4 | "c": [""], 5 | "d": ["D4B9"], 6 | "e": ["AB32", "212E"], 7 | "f": ["AB35", "A799", "017F", "1E9D", "0584"], 8 | "g": ["0581"], 9 | "h": ["0570", "13C2"], 10 | "i": ["02DB", "026A", "1FBE", "037A", "A647"], 11 | "j": [""], 12 | "k": ["03F0", "FF4B"], 13 | "l": ["05D5", "05DF", "0627", "FE8E", "FE8D", "05C0", "05C0", "007C", "0661", "06F1", "2110", "2111", "0196", 14 | "05D5", "05DF", "0627", "FE8D", "23FD", "0399", "05C0", "FFE8", "2160", "FF4C", "2113", "04C0", "FF29", "0406", 15 | "0049"], 16 | "m": [""], 17 | "n": ["03D6", "213C", "1D28", "0578", "057C"], 18 | "o": ["05E1", "0647", "FEEB", "FEEC", "FEEA", "FEE9", "06BE", "FBAC", "FBAD", "FBAB", "FBAA", "06C1", "FBA8", 19 | "FBA7", "FBA9", "FBA6", "06D5", "0C02", "0C82", "0D02", "0966", "0A66", "0BE6", "1040", "0665", "06F5", "2134", 20 | "AB3D", "03C3", "10FF", "0585", "05E1", "0647", "FEEB", "FEEC", "FEEA", "FEE9", "06BE", "FBAC", "FBAD", "FBAB", 21 | "FBAA", "06C1", "FBA8", "FBA9", "FBA7", "FBA6", "06D5"], 22 | "p": ["03F1"], 23 | "q": ["0563", "0566", "AB47", "AB48"], 24 | "r": ["AB47", "AB48"], 25 | "s": ["01BD"], 26 | "t": ["03C4"], 27 | "u": ["A79F", "AB4E", "AB52", "028B", "03C5", "0446", "057D"], 28 | "v": ["05D8"], 29 | "w": [""], 30 | "x": ["292B", "292C", "1541", "157D"], 31 | "y": ["AB5A", "0263", "1D8C", "213D", "FF59", ""], 32 | "z": [""] 33 | } -------------------------------------------------------------------------------- /misc/charset.json: -------------------------------------------------------------------------------- 1 | {"a": ["\\u00E0", "\\u00E1", "\\u00E2", "\\u00E3", "\\u00E4", "\\u00E5", "\\u0227", "\\u006F", "\\u237A", "\\u0251", "\\u03B1", "\\u0430"], "c": ["\\u0107", "\\u0180", "\\u1E03", "\\u00E7", "\\u1D04", "\\u03F2", "\\u2CA5", "\\u0441", "\\uABAF"], "b": ["\\u0184", "\\u042C", "\\u15AF"], "e": ["\\u00E9", "\\u00E8", "\\u0117", "\\u0435", "\\u04BD"], "d": ["\\u0501", "\\u13E7", "\\u146F", "\\uA4D2"], "g": ["\\u01F5", "\\u0261", "\\u1D83", "\\u018D"], "f": ["\\u0192", "ff"], "i": ["\\u00ED", "\\u00EC", "ii", "\\u2373", "\\u0131", "\\u0269", "\\u03B9", "\\u0456", "\\u04CF", "\\uAB75", "\\u13A5"], "h": ["\\u04BB"], "k": ["\\u1E31", "\\u2C6A"], "j": ["\\u0237", "jj", "\\u03F3", "\\u0458"], "m": ["\\u1E3F", "\\u1E43"], "l": ["\\u013A", "ll", "\\u2223", "\\u01C0", "\\u2C92", "\\u07CA", "\\u2D4F", "\\u16C1", "\\uA4F2"], "o": ["\\u00F3", "\\u00F2", "\\u1ECD", "\\u0D82", "\\u0AE6", "\\u0C66", "\\u0CE6", "\\u0D66", "\\u0E50", "\\u0ED0", "\\u1D0F", "\\u1D11", "\\u03BF", "\\u2C9F", "\\u043E", "\\u0D20", "\\u101D"], "n": ["\\u0144", "\\u1E47"], "q": ["\\u1E83", "\\u051B"], "p": ["\\u1E55", "\\u1E57", "\\u2374", "\\u03C1", "\\u2CA3", "\\u0440"], "s": ["\\u015B", "\\u015F", "\\u1E63", "\\uA731", "\\u0455", "\\uABAA"], "r": ["\\u0155", "\\u1E5B", "\\u1D26", "\\u2C85", "\\u0433", "\\uAB81"], "u": ["\\u00FA", "\\u00FC", "\\u01B0", "\\u1D1C"], "t": ["\\u0165", "\\u1E6D", "tt"], "w": ["\\u1E83", "\\u1E81", "\\u1E89", "vv", "\\u026F", "\\u1D21", "\\u0461", "\\u051D", "\\u0561", "\\uAB83"], "v": ["\\u2228", "\\u22C1", "\\u1D20", "\\u03BD", "\\u0475", "\\uABA9"], "y": ["\\u00FD", "\\u1EF5", "\\u1EF3", "\\u028F", "\\u1EFF", "\\u03B3", "\\u0443", "\\u04AF", "\\u10E7"], "x": ["\\uFF58", "\\u166E", "\\u00D7", "\\u2A2F", "\\u0445"], "z": ["\\u017A", "\\u1E93", "\\uFF5A", "\\u1D22", "\\uAB93"]} -------------------------------------------------------------------------------- /misc/letters.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": ["\u0061"], 3 | "b": ["\u0062"], 4 | "c": ["\u0063"], 5 | "d": ["\u0064"], 6 | "e": ["\u0065"], 7 | "f": ["\u0066"], 8 | "g": ["\u0067"], 9 | "h": ["\u0068"], 10 | "i": ["\u0069"], 11 | "j": ["\u006A"], 12 | "k": ["\u006B"], 13 | "l": ["\u006C"], 14 | "m": ["\u006D"], 15 | "n": ["\u006E"], 16 | "o": ["\u006F"], 17 | "p": ["\u0070"], 18 | "q": ["\u0071"], 19 | "r": ["\u0072"], 20 | "s": ["\u0073"], 21 | "t": ["\u0074"], 22 | "u": ["\u0075"], 23 | "v": ["\u0076"], 24 | "w": ["\u0077"], 25 | "x": ["\u0078"], 26 | "y": ["\u0079"], 27 | "z": ["\u007A"] 28 | } -------------------------------------------------------------------------------- /misc/whitelist_letters.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": ["\\u00E0", "\\u00E1", "\\u00E2", "\\u00E3", "\\u00E4", "\\u00E5", "\\u0227", "\\u006F"], 3 | "b": [], 4 | "c": ["\\u0107", "\\u0180", "\\u1E03", "\\u00E7"], 5 | "d": [], 6 | "e": ["\\u00E9", "\\u00E8", "\\u0117"], 7 | "f": ["\\u0192","ff"], 8 | "g": ["\\u01F5"], 9 | "h": [], 10 | "i": ["\\u00ED", "\\u00EC", "ii"], 11 | "j": ["\\u0237","jj"], 12 | "k": ["\\u1E31", "\\u2C6A"], 13 | "l": ["\\u013A", "ll"], 14 | "m": ["\\u1E3F", "\\u1E43"], 15 | "n": ["\\u0144", "\\u1E47"], 16 | "o": ["\\u00F3", "\\u00F2", "\\u1ECD"], 17 | "p": ["\\u1E55", "\\u1E57"], 18 | "q": ["\\u1E83"], 19 | "r": ["\\u0155", "\\u1E5B"], 20 | "s": ["\\u015B", "\\u015F", "\\u1E63"], 21 | "t": ["\\u0165", "\\u1E6D", "tt"], 22 | "u": ["\\u00FA", "\\u00FC", "\\u01B0"], 23 | "v": [], 24 | "w": ["\\u1E83", "\\u1E81", "\\u1E89", "vv"], 25 | "x": ["\\uFF58"], 26 | "y": ["\\u00FD", "\\u1EF5", "\\u1EF3"], 27 | "z": ["\\u017A", "\\u1E93", "\\uFF5A"] 28 | } -------------------------------------------------------------------------------- /punydomaincheck.py: -------------------------------------------------------------------------------- 1 | # Puny Domain Check v1.0 2 | # Author: Anil YUKSEL, Mustafa Mert KARATAS 3 | # E-mail: anil [ . ] yksel [ @ ] gmail [ . ] com, mmkaratas92 [ @ ] gmail [ . ] com 4 | # URL: https://github.com/anilyuk/punydomaincheck 5 | 6 | from argparse import ArgumentParser, RawTextHelpFormatter 7 | from sys import exit 8 | 9 | from core.creator import * 10 | from core.exceptions import CharSetException, AlternativesExists 11 | from core.logger import start_logger 12 | from core.confusable import update_charset 13 | from core.domain import load_domainnames, dns_client 14 | from core.common import print_percentage, OUTPUT_DIR, BANNER, BLU, RST, RED, GRE, VT_APIKEY_LIST, GEOLOCATION_DATABASE_FILE 15 | from time import sleep 16 | from Queue import Queue 17 | from os.path import getsize 18 | from tabulate import tabulate 19 | from os import remove, mkdir, stat 20 | from core.phishingdomain import PhishingDomain 21 | 22 | if VT_APIKEY_LIST: 23 | from core.vt_scan import vt_report_key_positives, vt_report_total, vt_report_key_subdomains, scanURL 24 | 25 | def arg_parser(): 26 | parser = ArgumentParser(formatter_class=RawTextHelpFormatter) 27 | parser.add_argument("-u", "--update", action="store_true", default=False, help="Update character set") 28 | parser.add_argument("--debug", action="store_true", default=False, help="Enable debug logging") 29 | parser.add_argument("-d", "--domain", default=None, help="Domain without prefix and suffix. (google)") 30 | parser.add_argument("-s", "--suffix", default=None, help="Suffix to check alternative domain names. (.com, .net)") 31 | parser.add_argument("-c", "--count", default=1, help="Character count to change with punycode alternative (Default: 1)") 32 | parser.add_argument("-os", "--original_suffix", default=None, 33 | help="Original domain to check for phisihing\n" 34 | "Optional, use it with original port to run phishing test") 35 | parser.add_argument("-op", "--original_port", default=None, help="Original port to check for phisihing\n" 36 | "Optional, use it with original suffix to run phishing test") 37 | parser.add_argument("-f", "--force", action="store_true", default=False, 38 | help="Force to calculate alternative domain names") 39 | parser.add_argument("-t", "--thread", default=15, help="Thread count") 40 | 41 | return parser.parse_args() 42 | 43 | 44 | def punyDomainCheck(args, logger): 45 | letters_json = load_letters() 46 | 47 | if args.original_port and not args.original_suffix: 48 | 49 | logger.info("[-] Original suffix required!") 50 | exit() 51 | 52 | elif not args.original_port and args.original_suffix: 53 | 54 | logger.info("[-] Original port required!") 55 | exit() 56 | 57 | try: 58 | 59 | charset_json = load_charset() 60 | 61 | except CharSetException: 62 | 63 | if (not args.update): 64 | update_charset(logger, letters_json) 65 | charset_json = load_charset() 66 | 67 | if args.update: 68 | update_charset(logger, letters_json) 69 | 70 | elif int(args.count) <= len(args.domain): 71 | 72 | if not args.domain: 73 | logger.info("[-] Domain name required!") 74 | exit() 75 | 76 | if not args.suffix: 77 | logger.info("[-] Domian Suffix required!") 78 | exit() 79 | 80 | try: 81 | 82 | stat(OUTPUT_DIR) 83 | 84 | except: 85 | 86 | mkdir(OUTPUT_DIR) 87 | 88 | output_dir = "{}/{}".format(OUTPUT_DIR, args.domain) 89 | 90 | try: 91 | 92 | stat(output_dir) 93 | 94 | except: 95 | 96 | mkdir(output_dir) 97 | 98 | try: 99 | 100 | create_alternatives(args=args, charset_json=charset_json, logger=logger, output_dir=output_dir) 101 | 102 | except AlternativesExists: 103 | 104 | logger.info("[*] Alternatives already created. Skipping to next phase..") 105 | except NoAlternativesFound: 106 | logger.info("[*] No alternatives found for domain \"{}\".".format(args.domain)) 107 | exit() 108 | except KeyboardInterrupt: 109 | exit() 110 | 111 | if not isfile(GEOLOCATION_DATABASE_FILE) : 112 | logger.warn("[*] Download GeoIP Database to query geolocation!") 113 | 114 | 115 | domain_name_list = load_domainnames(args=args, output_dir=output_dir) 116 | dns_thread_list = [] 117 | threads_queue = [] 118 | thread_count = 0 119 | logger.info("[*] Every thread will resolve {}{}{} names".format(BLU, str(len(domain_name_list[0])), RST)) 120 | # logger.info("[*] {}".format(datetime.now())) 121 | 122 | for list in domain_name_list: 123 | 124 | if len(list) > 0: 125 | thread_queue = Queue() 126 | threads_queue.append(thread_queue) 127 | 128 | dns_thread = dns_client(args=args, logger=logger, domain_list=list, output_queue=thread_queue, 129 | thread_name=str(thread_count)) 130 | dns_thread.daemon = True 131 | dns_thread.start() 132 | dns_thread_list.append(dns_thread) 133 | 134 | thread_count += 1 135 | 136 | logger.info("[*] DNS Client thread started. Thread count: {}{}{}".format(BLU, len(dns_thread_list), RST)) 137 | 138 | dns_client_completed = False 139 | query_result = [] 140 | 141 | last_percentage = 1 142 | header_print = False 143 | 144 | while not dns_client_completed: 145 | 146 | try: 147 | sleep(0.001) 148 | except KeyboardInterrupt: 149 | print RST 150 | exit() 151 | 152 | total_percentage = 0 153 | 154 | for dns_thread in dns_thread_list: 155 | total_percentage += dns_thread.get_percentage() 156 | 157 | total_percentage = total_percentage / int(thread_count) 158 | 159 | last_percentage, header_print = print_percentage(args, logger, total_percentage, 160 | last_percentage=last_percentage, 161 | header_print=header_print) 162 | 163 | for queue in threads_queue: 164 | try: 165 | 166 | if not queue.empty(): 167 | query_result.append(queue.get()) 168 | except KeyboardInterrupt: 169 | print RST 170 | exit() 171 | 172 | if len(query_result) == int(thread_count): 173 | dns_client_completed = True 174 | 175 | if VT_APIKEY_LIST: 176 | 177 | logger.info("[*] Checking for VirusTotal") 178 | 179 | for results in query_result: 180 | 181 | for result in results: 182 | result.set_vt_result(scanURL(url=(result.get_domain_name()+"."+str(args.suffix)))) 183 | # VirusTotal Free has 4 request per minute requests limit and sleep time is set based on this. 184 | # If you have VirusTotal Premium membership change sleep value based on your limits. 185 | sleep((60/(len(VT_APIKEY_LIST) * 4)) + 2) 186 | 187 | dns_file_name = "{}/{}_dns".format(output_dir, args.domain) 188 | dns_file_content = [] 189 | dns_file_new_created = True 190 | try: 191 | with open(dns_file_name, "r") as file: 192 | dns_file_content = file.readlines() 193 | 194 | except: 195 | pass 196 | 197 | else: 198 | dns_file_new_created = False 199 | 200 | print_header = True 201 | 202 | headers_list = ["","Domain Name", "IP Address", "Whois Name", "Whois Organization", "Whois Email", 203 | "Whois Updated Date", "HTTP Similarity", "HTTPS Similarity", 204 | "Country", "City", "Virustotal Result", "Subdomains",""] 205 | 206 | dns_file = open(dns_file_name, 'a') 207 | string_array = [] 208 | 209 | for results in query_result: 210 | 211 | for result in results: 212 | 213 | if len(result.get_domain_name()) > 1: 214 | 215 | whois_email = "" 216 | whois_name = "" 217 | whois_organization = "" 218 | whois_creation_date = "" 219 | whois_updated_date = "" 220 | whois_result = result.get_whois_result() 221 | 222 | if whois_result: 223 | 224 | if "contacts" in whois_result: 225 | 226 | whois_contacts = whois_result["contacts"] 227 | 228 | if "admin" in whois_contacts: 229 | 230 | whois_admin = whois_contacts["admin"] 231 | 232 | if whois_admin: 233 | 234 | if "email" in whois_admin: whois_email = whois_admin["email"] 235 | else: whois_email="NA" 236 | 237 | if "name" in whois_admin: whois_name = whois_admin["name"] 238 | else: whois_name = "NA" 239 | 240 | if "organization" in whois_admin: whois_organization = whois_admin["organization"] 241 | else: whois_organization = "NA" 242 | else: 243 | whois_email = "NA" 244 | whois_name = "NA" 245 | whois_organization = "NA" 246 | 247 | if "updated_date" in whois_result: whois_updated_date = whois_result["updated_date"][0] 248 | 249 | if print_header: 250 | 251 | header_string = ";".join(headers_list[1:-1]) 252 | 253 | if dns_file_new_created: 254 | dns_file.write("{}\n".format(header_string)) 255 | print_header = False 256 | 257 | http_similarity = "" 258 | https_similarity = "" 259 | if "http_similarity" in result.get_similarity(): 260 | http_similarity = result.get_similarity()["http_similarity"] 261 | if "https_similarity" in result.get_similarity(): 262 | https_similarity = result.get_similarity()["https_similarity"] 263 | 264 | virustotal_result = "" 265 | subdomains = "" 266 | 267 | if result.get_vt_result(): 268 | virustotal_result = "{}/{}".format( 269 | result.get_vt_result()[vt_report_key_positives], result.get_vt_result()[vt_report_total]) 270 | if vt_report_key_subdomains in result.get_vt_result(): 271 | subdomains = ",".join(result.get_vt_result()[vt_report_key_subdomains]) 272 | 273 | if result.get_geolocation() is not None: 274 | country_name = result.get_geolocation().country.name 275 | city_name = result.get_geolocation().city.name 276 | else: 277 | country_name = "" 278 | city_name = "" 279 | 280 | string_to_write = "{};{};{};{};{};{};{};{};{};{};{};{};{}".format( 281 | result.get_domain_name(), 282 | result.get_ipaddress(), 283 | whois_name, 284 | whois_organization, 285 | whois_email, 286 | whois_creation_date, 287 | whois_updated_date, 288 | http_similarity, 289 | https_similarity, 290 | country_name, 291 | city_name, 292 | virustotal_result, 293 | subdomains) 294 | color = "" 295 | if "{}\n".format(string_to_write) not in dns_file_content: 296 | dns_file.write("{}\n".format(string_to_write)) 297 | color = RED 298 | 299 | string_array.append( 300 | [color, result.get_domain_name(), 301 | result.get_ipaddress(), 302 | whois_name, 303 | whois_organization, 304 | whois_email, 305 | whois_updated_date, 306 | http_similarity, 307 | https_similarity, 308 | country_name, 309 | city_name, 310 | virustotal_result, 311 | subdomains, RST]) 312 | 313 | 314 | logger.info( 315 | "[+] Punycheck result for {}{}.{}{}:\n {}".format(GRE, args.domain, args.suffix, RST, 316 | tabulate(string_array, headers=headers_list))) 317 | 318 | dns_file.close() 319 | 320 | if getsize(dns_file_name) == 0: 321 | remove(dns_file_name) 322 | 323 | # logger.info("[*] {}".format(datetime.now())) 324 | 325 | 326 | charset_json = None 327 | 328 | if __name__ == '__main__': 329 | print '%s%s%s' % (BLU, BANNER, RST) 330 | 331 | args = arg_parser() 332 | logger = start_logger(args) 333 | punyDomainCheck(args, logger) 334 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coloredlogs 2 | pythonwhois 3 | bs4 4 | dnspython 5 | requests 6 | ratelimit 7 | tabulate 8 | geoip2 --------------------------------------------------------------------------------