mb you know I already shared a script to find clones of scam websites
as finding new or clone CW/SC websites doesn’t work properly I modified the script and hopefully some of you can run it on your own and post/report them.
As I don’t want to teach scammers (hi
if you read this go eff yourself) but enable people here to use it, here’s a breakdown with as much information as needed of how it works:
my prior script “clone-sites” only works for scanning for certain images and reverse search sites using the same hash-images (works for the main purpose: finding cloned sites from investment scammers, that’s why I wrote/use it).
As this didn’t properly work for the CW/SC sites but could be useful, I tried a different approach.
I manually checked some of the latest CW sites posted in the main thread and checked on urlscan.io which “Resource Hash” could be useful to track clone sites.
I found “Default.css” to be helpful (not an image so not caught by the original script, but resembling the background image of the typical CW/SC sites). To find it on a urlscan.io result: move to http then CSS and look for the hash of “Default.css” (magnifying glass)
The new script allows you to:
- unput a list of hash values you found from urlscan.io
- if you run the script repeatedly you will find new clone sites (based on the hash). This is important as: urlscan.io relies on users to submit the scam sites: if they don’t we don’t find them.
- this is also helpful as: if you know a CW/SC website but IT’S ALREADY DOWN: you won’t find any results UNLESS you kept the hash from a prior scan and run it again
So:
- If you find a new CW/SC site: submit it to urlscan.io, check for a hash that gives you lots of other sites (Remark: some of the hash will give you normal ConnectWise sites as well, don’t use them or you need to sort them manually)
- run the script on a regular basis with all hash’s and you’ll get clone sites re-using this background image/css (newly submitted to urlscan.io) under “new clone sites:” in the all_known_clones.txt
while I can try to manually check it I still hope someone more tech-savvy can automate this (and maybe combine it with an automatic reporting).
FYI: I was not able to combine it with a script to detect if the sites found are still online (or maybe already suspended) so you’ll have to check it yourself prior to posting/reporting.
here’s the script, replace YOUR-urlscan.io-API-KEY-HERE with your own, free urlscan.io API key
import requests
import os
import time
from urllib.parse import urlparse
API_KEY = "YOUR-urlscan.io-API-KEY-HERE"
GLOBAL_CLONE_LIST = "all_known_clones.txt"
def normalize_url(url):
parsed = urlparse(url)
netloc = parsed.netloc or parsed.path
netloc = netloc.lower()
if not netloc:
return None
return f"https://{netloc}/"
def validate_hash(hash_str):
return isinstance(hash_str, str) and len(hash_str) == 64 and all(c in '0123456789abcdef' for c in hash_str.lower())
def search_hash_in_urlscan(hash_value, api_key):
url = "https://urlscan.io/api/v1/search/"
headers = {'API-Key': api_key, 'Content-Type': 'application/json'}
params = {'q': f"hash:{hash_value}", 'size': 100}
resp = requests.get(url, headers=headers, params=params)
if resp.status_code == 200:
return resp.json().get('results', [])
else:
print(f"[-] Error searching hash: {resp.status_code}")
return []
def get_websites_for_hash(hash_val, api_key):
results = search_hash_in_urlscan(hash_val, api_key)
unique = set()
for entry in results:
urls = entry.get('lists', {}).get('urls', [])
if urls:
for u in urls:
norm = normalize_url(u)
if norm:
unique.add(norm)
else:
task_url = entry.get('task', {}).get('url')
if task_url:
norm = normalize_url(task_url)
if norm:
unique.add(norm)
time.sleep(1)
return sorted(unique), results
def load_previous_sites(path):
if not os.path.exists(path):
return set()
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
return set(line.strip() for line in lines if line.strip() and not line.startswith("Hash:"))
def save_sites_to_txt(hash_val, sites, folder):
file_path = os.path.join(folder, f"{hash_val}.txt")
with open(file_path, "w", encoding="utf-8") as f:
f.write(f"Hash: {hash_val}\n")
for site in sites:
f.write(f"{site}\n")
print(f"\n[+] Full clone list saved to {file_path}")
def save_new_clones(new_sites, folder):
file_path = os.path.join(folder, "new_clones.txt")
with open(file_path, "w", encoding="utf-8") as f:
for site in new_sites:
f.write(f"{site}\n")
print(f"[+] New clones saved to {file_path}")
def load_global_clones(path=GLOBAL_CLONE_LIST):
if not os.path.exists(path):
return set()
with open(path, "r", encoding="utf-8") as f:
lines = [line.strip() for line in f if line.strip()]
in_all_section = False
known = set()
for line in lines:
if line.lower().startswith("all clone sites:"):
in_all_section = True
continue
if in_all_section:
known.add(line)
return known
def update_global_clones(new_sites, all_sites, path=GLOBAL_CLONE_LIST):
with open(path, "w", encoding="utf-8") as f:
f.write("new clone sites:\n")
for site in sorted(new_sites):
f.write(f"{site}\n")
f.write("\nall clone sites:\n")
for site in sorted(all_sites):
f.write(f"{site}\n")
print(f"[+] Global clone list updated in {path}")
def main():
print("Enter SHA256 hashes to search for clones, one per line. Finish input with an empty line:")
hashes = []
while True:
line = input().strip()
if not line:
break
hashes.append(line)
valid_hashes = [h for h in hashes if validate_hash(h)]
invalid_hashes = [h for h in hashes if not validate_hash(h)]
if invalid_hashes:
print(f"\n[-] Warning: {len(invalid_hashes)} invalid hash(es) skipped:")
for h in invalid_hashes:
print(f" {h}")
if not valid_hashes:
print("\nNo valid hashes provided. Exiting.")
return
global_clones = load_global_clones()
all_seen_sites = set(global_clones)
new_global_sites = set()
for idx, hash_input in enumerate(valid_hashes, 1):
print(f"\n[{idx}/{len(valid_hashes)}] Searching urlscan.io for hash: {hash_input}...\n")
output_folder = os.path.join(os.getcwd(), hash_input)
if not os.path.exists(output_folder):
os.makedirs(output_folder)
previous_path = os.path.join(output_folder, f"{hash_input}.txt")
first_time_seen = not os.path.exists(previous_path)
sites, results = get_websites_for_hash(hash_input, API_KEY)
if not sites:
print(f"No sites found reusing that hash: {hash_input}")
continue
previous_sites = load_previous_sites(previous_path)
save_sites_to_txt(hash_input, sites, output_folder)
new_for_this_hash = [s for s in sites if s not in previous_sites]
new_globally = [s for s in new_for_this_hash if s not in global_clones]
if new_globally:
print(f"\n[+] {len(new_globally)} globally new clone(s):")
for site in new_globally:
print(f"[NEW-GLOBAL] {site}")
new_global_sites.update(new_globally)
if not first_time_seen:
save_new_clones(new_globally, output_folder)
elif new_for_this_hash:
print(f"\n[+] {len(new_for_this_hash)} new clone(s) for this hash (already seen globally):")
for site in new_for_this_hash:
print(f"[SEEN] {site}")
if not first_time_seen:
save_new_clones(new_for_this_hash, output_folder)
else:
print("\n[+] No new clone sites found for this hash.")
all_seen_sites.update(sites)
update_global_clones(new_global_sites, all_seen_sites)
print(f"\n[+] Found {len(new_global_sites)} globally new clone(s).")
input("\n[Done] Press Enter to close...")
if __name__ == "__main__":
main()




