hemanshu baviskar

← writings

28 04 2026

research: mapping whatsapp phishing farm from one tls cert

POC / source: github.com/Per0x1de-1337/WA-phishing-farm

One OpenPhish URL, 205 domains, 253 live TLS IPs, and a staging block I did not expect to find.

I started this from a single OpenPhish entry on May 10:

https://cn-hht-web-whatsapp[.]com[.]cn/shop/configs/index.php

It looked like the usual WhatsApp-themed phishing domain at first. .com.cn, city-ish naming, fake web flow. Certstream connected, but I did not get anything useful in the short window I watched it.

So I kept it simple: pull the live TLS cert, read the SANs, probe what was still alive, then pivot on IPs and keys.

That was enough.

SANs, Quickly

When a site has a TLS certificate, that certificate can cover more than one hostname. Those hostnames are in the Subject Alternative Name field, usually just called SAN.

A phishing cert might look like this:

Common Name: www[.]cn-hgh-web-whatsapp[.]com[.]cn

SANs:
  www[.]cn-fzh-web-whatsapp[.]com[.]cn
  www[.]cn-hfi-web-whatsapp[.]com[.]cn
  www[.]cn-hgh-web-whatsapp[.]com[.]cn
  www[.]cn-wuh-web-whatsapp[.]com[.]cn
  ...

That SAN list is useful because operators often issue certificates in batches. One cert can expose the rest of the batch, even if the feed only gave you one URL.

I treat the SAN list as the first graph:

One domain is an IOC. The graph tells you whether it is alone or part of a bigger setup.

Pulling The First Cert

The first thing I did was connect to the seed host on 443 and parse the cert.

This is the small helper I used. It is not a framework, just enough to grab the SANs and the SPKI hash.

import hashlib
import socket
import ssl

from cryptography import x509
from cryptography.hazmat.primitives import serialization


def grab_cert(host, port=443):
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE

    with socket.create_connection((host, port), timeout=8) as s:
        with ctx.wrap_socket(s, server_hostname=host) as tls:
            raw = tls.getpeercert(binary_form=True)

    cert = x509.load_der_x509_certificate(raw)
    pub = cert.public_key().public_bytes(
        serialization.Encoding.DER,
        serialization.PublicFormat.SubjectPublicKeyInfo,
    )

    try:
        san_ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
        sans = san_ext.value.get_values_for_type(x509.DNSName)
    except x509.ExtensionNotFound:
        sans = []

    return {
        "subject": cert.subject.rfc4514_string(),
        "issuer": cert.issuer.rfc4514_string(),
        "cert_sha256": hashlib.sha256(raw).hexdigest(),
        "spki_sha256": hashlib.sha256(pub).hexdigest(),
        "sans": sans,
        "days": (cert.not_valid_after_utc - cert.not_valid_before_utc).days,
    }

The seed domain returned a TrustAsia LiteSSL certificate with 26 SAN entries.

Every hostname followed the same pattern:

www[.]cn-{code}-web-whatsapp[.]com[.]cn

The {code} values looked like city codes: hgh, wuh, xmi, nbp, and so on.

One TLS connection gave me 26 domains.

Probing The SANs

I do not like stopping at the SAN list. A cert can be stale. DNS can move. Some names can stop serving the same certificate.

So I probed all 26 SAN domains live.

san domains: 26
live tls:    21
ips seen:    3
certs seen:  2

Most still sat on:

43.226.17.54

and still served the original 26-SAN TrustAsia cert.

Five names had already moved:

www[.]cn-xmi-web-whatsapp[.]com[.]cn  43.226.17.135
www[.]cn-aqi-web-whatsapp[.]com[.]cn  43.226.17.135
www[.]cn-qzh-web-whatsapp[.]com[.]cn  38.181.10.193
www[.]cn-mas-web-whatsapp[.]com[.]cn  43.226.17.135
www[.]cn-nto-web-whatsapp[.]com[.]cn  43.226.17.135

Those five were no longer showing the original cert. They were serving a single-name Let's Encrypt certificate for:

2026[.]webfqprxvmt-whatsapp[.]com[.]cn

That was the first useful clue. The operator was not just sitting on one static batch. Some names had already migrated while the original certificate was still valid elsewhere.

IP Pivot

The seed domain resolved to 43.226.17.54. I checked it with Shodan InternetDB because that endpoint is free and does not need an API key.

curl -s https://internetdb.shodan.io/43.226.17.54 | jq '.hostnames | length'

It returned:

90

That mattered because those 90 names were not in the 26-SAN city certificate.

They used a different pattern:

faq[.]webfzlwpakd-whatsapp[.]com[.]cn
faq[.]webwxqra-whatsapp[.]com[.]cn
faq[.]webmqlen-whatsapp[.]com[.]cn
faq[.]webbqvra-whatsapp[.]com[.]cn
...

When I connected to one of them, it served a 90-SAN Let's Encrypt cert listing the whole FAQ cluster.

So the seed certificate gave me 26 names. The IP pivot gave me 90 more. Same IP, different certificate, different naming template.

That is why I usually pivot both ways. Cert to domain is useful. IP to other hostnames is often where the next batch appears.

Subnet Scan

At that point the obvious block was 43.226.17.0/24, under AS152194, HONG KONG WAN SHOU NETWORK TECHNOLOGY LIMITED.

I scanned the /24 for TLS on 443 and pulled the cert from anything that answered.

range:       43.226.17.0/24
live tls:    43
spki groups: 10

The 90-SAN FAQ cert was repeated across seven IPs. The scan also found other cert groups I had not seen from the original seed.

Cluster SANs Issuer Pattern
FAQ random 90 Let's Encrypt R12 faq[.]web{random}-whatsapp[.]com[.]cn
3-letter random 49 Let's Encrypt R13 {abc}-whatsapp[.]com[.]cn
API/web 30 TrustAsia LiteSSL web[.]api-*-whatsapp[.]com[.]cn
Online/dev 30 TrustAsia LiteSSL online[.]*-whatsapp[.]com[.]cn
City-coded 26 TrustAsia LiteSSL www[.]cn-{city}-web-whatsapp[.]com[.]cn
Migration 1 Let's Encrypt R13 2026[.]webfqprxvmt-whatsapp[.]com[.]cn
Numeric 2 Let's Encrypt R13 128-whatsapp[.]com[.]cn

Some API/web examples:

web[.]api-app-whatsapp[.]com[.]cn
web[.]graph-api-whatsapp[.]com[.]cn
web[.]cloud-sync-whatsapp[.]com[.]cn
web[.]auto-backup-whatsapp[.]com[.]cn

Some 3-letter examples:

acy-whatsapp[.]com[.]cn
adp-whatsapp[.]com[.]cn
agk-whatsapp[.]com[.]cn
ahm-whatsapp[.]com[.]cn
ain-whatsapp[.]com[.]cn

At this point I had several naming templates, but they all pointed in the same direction: same suffix, same hosting, similar cert timing, repeated key material.

I would not call a match proof by itself. But this was not one weak link. It was a pile of matching weak links.

The Staging Block

The second range was less noisy in terms of names and more interesting in terms of infrastructure.

I scanned 38.181.8.0 through 38.181.10.255.

live tls:    244
san count:   0 on every host I checked
cert:        self-signed Managed CA
spki:        same key across all 244 boxes

That does not look like a live phishing cluster yet. It looks like staging.

Two hundred and forty-four boxes had TLS ready, but no domain names attached to the certs. Same empty self-signed certificate. Same key. Waiting for the next wave, or at least prepared for one.

Combined with the active block, my count at the end looked like this:

domains mapped:        205
live tls ips:          253
active cert clusters:  9
staging boxes:         244
main asn:              AS152194
web server:            nginx
faq http title:         Search - Microsoft Bing

The Graph

This is the rough shape I ended up with:

OpenPhish URL
  -> cn-hht-web-whatsapp[.]com[.]cn
     -> TLS cert
        -> 26 city-coded SANs
        -> 15 still on 43.226.17.54
        -> 5 moved to 43.226.17.135 / 38.181.x.x

  -> IP pivot on 43.226.17.54
     -> 90 faq[.]web* names from InternetDB
     -> 90-SAN Let's Encrypt FAQ cert

  -> scan 43.226.17.0/24
     -> 43 live TLS IPs
     -> API/web cluster
     -> online/dev cluster
     -> 3-letter random cluster

  -> scan 38.181.8.0-38.181.10.255
     -> 244 boxes with empty self-signed certs

One feed URL. Four pivots. No paid API.

Naming Templates

After pulling the certs, five templates stood out.

City-coded names:

www[.]cn-hgh-web-whatsapp[.]com[.]cn
www[.]cn-wuh-web-whatsapp[.]com[.]cn
www[.]cn-xmi-web-whatsapp[.]com[.]cn

FAQ random names:

faq[.]webfzlwpakd-whatsapp[.]com[.]cn
faq[.]webwxqra-whatsapp[.]com[.]cn

API/web names:

web[.]graph-api-whatsapp[.]com[.]cn
web[.]cloud-sync-whatsapp[.]com[.]cn

Online/dev names:

online[.]auth-s-whatsapp[.]com[.]cn
online[.]api-c-whatsapp[.]com[.]cn

Short 3-letter names:

acy-whatsapp[.]com[.]cn
adp-whatsapp[.]com[.]cn
bjs-whatsapp[.]com[.]cn

The names are not subtle, but the useful part is the batching. Different naming waves, different certificates, same broad operation.

SPKI Pivots

If I had Censys or a working CT search handy, these SPKI hashes are where I would pivot next:

# 26-SAN city cluster
3ca513f64927808bc0a97e977e944e013a90012ed9dbf55bca45459236358495

# 90-SAN faq cluster
0c10bb2779bbb2750fec8f0539302b7150d544d378937458ee7671a4546c0cca

# 49-SAN 3-letter cluster
7c3db7bf6e1edbca437c41be6735bf4345a7155989d75b4da086246447a30c7d

# 30-SAN API/web cluster
2b6d1f3f64e8f786748aa80e9f3fe9feed51dec8d87fb7be191786d91c7d887c

# migration cert
77222f1ad1b1ff34879f43f1605070438a03ebb499dbdea3b652534116c57891

On Censys, the query would be:

services.tls.certificates.leaf_data.subject_key_info.fingerprint_sha256: "<hash>"

That finds hosts that reused the same public key. It can also surface bare IPs that will never show up from a domain-only CT search.

Workflow I use

For a single suspicious domain, this is the flow I would reuse:

  1. Pull the live TLS cert.
  2. Extract SANs and SPKI.
  3. Probe every SAN domain live.
  4. Record current IP, cert hash, and SPKI.
  5. Query free IP enrichment like Shodan InternetDB.
  6. Pull certs from any new hostnames.
  7. Scan the nearby hosting range if the block looks dedicated.
  8. Group by SPKI instead of domain alone.

Steps 1 and 2 give a cluster. The IP and subnet pivots are what turn that cluster into infrastructure.

Closing Notes

The gap between one phishing URL and the shape of the operation was mostly graph work.

The SAN list gave me the first batch. The IP pivot gave me the second. The subnet scan showed the staging capacity behind it.

I would still treat every edge as a lead, not proof. Shared hosting and stale DNS can lie. But when the naming, certs, IP space, issuer timing, and SPKI reuse all point the same way, the graph is usually telling you something worth following.