Script to find new ConnectWise sites

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 :waving_hand: 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:

  1. 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)
  2. 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()

「いいね!」 3

Thanks for posting this @dubloox3, I ran into the same problem of running into valid CW sites, I did list some of them in the hopes of www.ScreenConnect.com would join in the fight to take out the cloned sites.

「いいね!」 2

yeah, back-tracking some of the scripts on the sites gives useless results.. and the CSS for the background image only works if they have one.
I will still try to monitor new sites and run the hashes now and then, maybe we find some new ones.
I’m still looking for an alternative to urlscan.io (as it depends on someone having scanned the site)

「いいね!」 1

I tested a couple of hashes that lead to new CW/SC sites, feel free to give it a try:

39ad554ecb56c04433cd9a618e019e0c4670c5f255c089266c779d1cbd141e4e
3270f59b64211cf247d6f01056e6a97ffd39acd40a815d3b3ff3431d894774ca
1f9bf98e43b7dd4317006be99ecbcf871b0ec475dd6dcb656bb8439239d0e4e5
9b9339876c1a3666f1c61d7a29fdcee0a55c819f6b57c5cd09872a811c4aa861
f3a74cc8eaf2dbdb157e4631b084cee9b0b4a7da241a31aefe380b17dca98c6e
609fefa2a7f61eba1ef899c2dd17410e4b56d4c4caca3543f654f54e0a07dac5
ef74b692962d1a6fe98b2c5df598bf7f767774e055f42ce3ab7eaac0122219c7
63b08bdee2146e502024b606f3184d85122e498261a1ce5c04132dd2d0090a30
abbdac7c77ef31cf2de911270d70d52607998fee37d15845f211df4115bdd035
34a35311b81103a8f5d1294d06d1dbec930d29868147000a88e031fa09f6ba3d
b190ac124955aed6cc0623295a86d523ee8850e124f159f1077342cb10ece29e
0daf87e2021454f9007fee36a04fd1c01655cbcff9b3a77cc80fe0396d877c51
4ba4c1cfb6f7726be125528f7b79f4602bbf1bec2561bf0ff920f730d36a96bb
0c4bb2fb704141ec1ed7034487a17c52dd22ff9845ca2a8006cc57065c786dfb
789cdc0df1396de40909bcb7bbd91efc898053eca6b811bd363e5c931b7b3e5e
de2f886abc9b5e6f7927645f018de46b8da3674413630e7d0dd22e77e31455cf
1e94d37078a46c1ee98f285f72df4c3e24b1eb2c2d9ff198d3e1a54f38dcdd20
b636a98c046d58b66f6b1dac691f5fcd77a329273be652c05399f00e6e7ac8b0
de2f886abc9b5e6f7927645f018de46b8da3674413630e7d0dd22e77e31455cf
4459e7299ae85321500a67292535230dd887c1badf248a844bcbffd28b419e06
bf5183c81bee3a3fc9a736d7a854cc11b6277795d0a467711e56d493c429c4f4
c4e3eeaa27f91df2509fb8c531b9e924f85eb208b1ca462cda45469a74e6cca8
5ad57a8645c8d273ba9c29aa6aaf55ed3b6a1b91fbb9ae8373451460774c4a78
4c4755a97532ae85bd47279b443fcb25136c88aa8118d6e5b4addd72bff29eba
d2950bc23fcbab2328082856cf1ff8d4f6840219ba287ee02afb2792607ed434
119d944d183d6c43938c805ab1dfdd83985fca92cbb74d1c7d00e573a5dae968
13bdee2b705e8785e13edbc412071427c4d84ac4420d6ef729e7d609bf51d45e
b3a491a95f0bceb6d6dddaa70ff147eb8b51536879980cbafc9615da66eecfc8

If you run the script for the first time you will get a lot of sites (some useless, some already taken down) but if you repeat this on a regular basis you will find new sites to report.
FYI: I don’t have the time to report these sites, as I already wrote in the main topic: hopefully someone with contact to Netcraft can automate this (otherwise we just have a list of these sites and depend on ppl reporting them on their own).

「いいね!」 1

the script works pretty decent atm.
For me to find clone sites it’s important to have different “variations” of the sites (mostly the layout/images).

If you run across CW/SC sites with different layouts compared to the ones below:
Please let me know asap (then I can try to add them to the clone script for us to find more sites to report faster)
thanks





「いいね!」 1

Nice! Thanks for helping so much, please keep this going, with you taking more of the lead, I’m free for another project, greetings to Germany!

「いいね!」 1

I updated the script so it can distinguish between real sites and sites that only redirect to another site (seen that lately from the scammers), the new script will mark sites redirecting only with URL (redirect) + grab the redirected-to site and add it to the list

here’s the new code

import requests
import os
import time
import re
from urllib.parse import urlparse

API_KEY = "YOUR-URLSCAN.IO-API-KEY_HERE"
GLOBAL_CLONE_LIST = "all_known_clones.txt"

REDIRECT_TAG = " (redirect)"

def normalize_url(url):
    if not url:
        return None
    parsed = urlparse(url)
    netloc = parsed.netloc or parsed.path
    netloc = netloc.lower().strip()
    if not netloc:
        return None
    # We store canonical as https://domain/
    return f"https://{netloc}/"

def strip_annotation(line):
    """Return canonical site (https://domain/) from a line that may end with ' (redirect)'."""
    if not line:
        return None
    # Remove the redirect tag if present
    if line.endswith(REDIRECT_TAG):
        line = line[: -len(REDIRECT_TAG)]
    return line.strip()

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):
    """
    Returns:
      sites: sorted list of canonical sites (https://domain/)
      results: raw urlscan results
      redirect_domains: set of domains (canonical) that acted as a redirect source
    """
    results = search_hash_in_urlscan(hash_val, api_key)
    domains = set()
    redirect_domains = set()

    for entry in results:
        # 1) Collect from lists.urls if present (fallbacks seen on urlscan)
        urls = entry.get('lists', {}).get('urls', []) or []
        for u in urls:
            norm = normalize_url(u)
            if norm:
                domains.add(norm)

        # 2) Always consider submitted and effective URLs
        submitted_url = (entry.get('task') or {}).get('url')
        effective_url = (entry.get('page') or {}).get('url')

        sub_norm = normalize_url(submitted_url)
        eff_norm = normalize_url(effective_url)

        if sub_norm:
            domains.add(sub_norm)

        if eff_norm:
            domains.add(eff_norm)

        # If effective differs from submitted, flag the submitted as redirect
        if sub_norm and eff_norm and eff_norm != sub_norm:
            redirect_domains.add(sub_norm)

        # Be nice to the API
        time.sleep(1)

    # Sorted stable output
    sites = sorted(domains)
    return sites, results, redirect_domains

def load_previous_sites(path):
    """
    Load previously saved sites for a specific hash file, stripping annotations like ' (redirect)'.
    """
    if not os.path.exists(path):
        return set()
    with open(path, "r", encoding="utf-8") as f:
        lines = f.readlines()
    prev = set()
    for line in lines:
        line = line.strip()
        if not line or line.startswith("Hash:"):
            continue
        prev.add(strip_annotation(line))
    return prev

def save_sites_to_txt(hash_val, sites, folder, redirect_domains):
    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:
            if site in redirect_domains:
                f.write(f"{site}{REDIRECT_TAG}\n")
            else:
                f.write(f"{site}\n")
    print(f"\n[+] Full clone list saved to {file_path}")

def save_new_clones(new_sites, folder, redirect_domains):
    file_path = os.path.join(folder, "new_clones.txt")
    with open(file_path, "w", encoding="utf-8") as f:
        for site in sorted(new_sites):
            if site in redirect_domains:
                f.write(f"{site}{REDIRECT_TAG}\n")
            else:
                f.write(f"{site}\n")
    print(f"[+] New clones saved to {file_path}")

def load_global_clones(path=GLOBAL_CLONE_LIST):
    """
    Load the 'all clone sites:' section as canonical domains (strip annotations).
    """
    if not os.path.exists(path):
        return set()
    with open(path, "r", encoding="utf-8") as f:
        lines = [line.rstrip("\n") for line in f if line.strip()]
    in_all_section = False
    known = set()
    for line in lines:
        lower = line.lower()
        if lower.startswith("all clone sites:"):
            in_all_section = True
            continue
        if in_all_section:
            # Stop if a new header is encountered (defensive)
            if re.match(r"^\s*(new clone sites:|all clone sites:)\s*$", lower):
                break
            canon = strip_annotation(line)
            if canon:
                known.add(canon)
    return known

def update_global_clones(new_sites, all_sites, redirect_domains, path=GLOBAL_CLONE_LIST):
    """
    Write the global file with two sections.
    Lines are annotated with '(redirect)' if seen as redirect source in this run,
    but de-dup remains based on the bare canonical domain.
    """
    with open(path, "w", encoding="utf-8") as f:
        f.write("new clone sites:\n")
        for site in sorted(new_sites):
            if site in redirect_domains:
                f.write(f"{site}{REDIRECT_TAG}\n")
            else:
                f.write(f"{site}\n")
        f.write("\nall clone sites:\n")
        for site in sorted(all_sites):
            if site in redirect_domains:
                f.write(f"{site}{REDIRECT_TAG}\n")
            else:
                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

    # Known global from previous runs (bare canonical)
    global_clones = load_global_clones()

    all_seen_sites = set(global_clones)          # bare canonicals
    new_global_sites = set()                     # bare canonicals found this run
    redirect_domains_overall = set()             # bare canonicals that are redirect sources

    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, redirect_domains = 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)  # bare canonical set
        save_sites_to_txt(hash_input, sites, output_folder, redirect_domains)

        # Compare on bare canonical
        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:
                tag = " (redirect)" if site in redirect_domains else ""
                print(f"[NEW-GLOBAL] {site}{tag}")
            new_global_sites.update(new_globally)

            if not first_time_seen:
                save_new_clones(new_globally, output_folder, redirect_domains)

        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:
                tag = " (redirect)" if site in redirect_domains else ""
                print(f"[SEEN] {site}{tag}")

            if not first_time_seen:
                save_new_clones(new_for_this_hash, output_folder, redirect_domains)
        else:
            print("\n[+] No new clone sites found for this hash.")

        all_seen_sites.update(sites)
        redirect_domains_overall.update(redirect_domains)

    update_global_clones(new_global_sites, all_seen_sites, redirect_domains_overall)
    print(f"\n[+] Found {len(new_global_sites)} globally new clone(s).")
    input("\n[Done] Press Enter to close...")

if __name__ == "__main__":
    main()

「いいね!」 1