Dissecting an OpenSea Phishing

Hero

I received a phishing email pretending to be from OpenSea. Since cryptocurrent-related phishing like this is quite common, I usually ignore them — but this time, I decided to take a closer look.

Email Header Analysis

Spam Email Header 1

The phishing email came from a hostmail.com address - a Microsoft-owned domain that’s often used in phishing attempts, I utilized various OTINT tools to see if the email had been compromised, but couldn’t find any evidence. So it’s likely that the attacker created it recently using a random username. It looks like they’re sending the emails via Outlook.

Spam Email Header 2

The reply address is info@m[.]opensea[.]io, which at first glance looks like a legitimate email address associated with OpenSea. However, the subdomain m[.]opensea[.]io seems suspicious — a Google search and OSINT investigation turned up no relevant results. While this doesn’t conclusively prove it’s a fake, it raises red flags. Given this and the overall context, it’s likely the attacker doesn’t actually expect replies, and the address may be a decoy to make the email appear more authentic.

Subdomains

When I used Subdomain Finder, I found the subdomains such as blockchaln[.]com[.]dashboard-center[.]com, which are also likely to be used for scams related to cryptocurrency.

Phishing Site Overview

The link to hxxps://dashboard-center[.]com/adm/#?id=[MY_EMAIL_ADDRESS] is assigned to the View Item button in the email body. Multiple vendors in VirusTotal rated it as Phishing/Malicious.

NS Records in SecurityTrails

Looking at the NS record history for the domain dashboard-center[.]com in SecurityTrails, it appears that it has delegated its nameservers to Cloudflare since 2025-03-30.

The results of dynamic analysis for this URL are also published on ANY.RUN. It seems that the victim who visits the page is guided to connect to the wallet.

However, I was curious about what this site was doing internally, so I decided to actually visit the page in a sandbox environment.

Website 1

This site appears to be imitating OpenSea’s official website, even though the domain is completely different from the official one. Once accessed, it will automatically attempt to connect to my wallet. Fortunately, I didn’t have any wallet extensions installed on my sandbox browser, so the process didn’t proceed any further. However, if it is connected, it will be asked to sign, and if I approve, I will likely suffer serious damage.

Website 2

As the screenshot above, since it executes a JavaScript file with a file name made up of random strings, it seems worth analyzing this.

JavaScript Code Analysis

JS 1

When I looked at the JavaScript file which is executed when accessing this site, I found that it contained a large number of obfuscated strings in the run function, contains many escape characters and Hex strings.
The core logic is embedded in the second argument of the Function, so I need to parse its contents.

I tried to organize it and understand its logic, and decoded escape characters and Hex strings wherever possible to improve readability.

As a result, this code is nearly 10,000 lines long. I wonder how much of this is noise. Or maybe it’s all meaningful code.

Getting Global Objects without Depending on Environments

JS 2

First, in this part, the global object is obtained without depending on the environment.

The roles of the functions stored in the array FV1sWfx are as follows:

  • globalThis: Gets the global this object across environments.
  • JztcJn["HoylrS"]: Refer to get”HoylrS”(){return global}.
  • window: Gets global objects in the browser environment.
  • new Function("return this") (): Gets global objects in any environment using the classic method.

Regarding the x_6MaUp function, this is the code after deobfuscation. In reality, each case string was a random string.
Anyway, this code here retrieves the global object and prepares it for further attacks.

Using External Modules (Pako, MessagePack, CryptoJS)

JS 3

The immediate function that follows is written in thousands of lines of code, but upon investigation, I found that it is an implementation of Pako (zlib).

JS 4

Later, an implementation of MessagePack was also written. MessagePack is an efficient binary serialization format.

JS 5

Furthermore, the implementation of CryptoJS was written in the next immediate function.

Main Logic

And from here we will see the implementation of what is essentially the main function.

JS 6

Ultimately, this function will run like the code above. There, the URL of the script file itself being executed is passed as an argument, which is executed 0.2 seconds after the page loads.

Now, let’s analyze the contents of the HDHTgWc function.

Detecting Selenium, Headless Browser, Puppeteer

JS 7

This function checks if the target user is using Selenium, headless browser, or Puppeteer, etc.
Each element in the FV1sWfx array is a property that can be obtained from the global object when Selenium is used. It seems to be judged by whether they are undefined or not.

Detecting DevTools, Memory Interference

JS 8

First, the FV1sWfx function here checks whether DevTools is open.

Getting WebGL Information

JS 9

In the subsequent functions, information about WebGL (GPU renderer, etc.) is obtained. This also seems to check whether the running browser is genuine. It’s doing some very thorough checks.

Canvas Fingerprinting

I also discovered an implementation of Canvas Fingerprinting:

const FV1sWfx = x_6MaUp("document").createElement("canvas");
const YOalYyJ = FV1sWfx.getContext("2d");
...Omitted...
YOalYyJ.fillText("Cwm fjordbank glyphs vext quiz", 0x2, 0xf);

We can see what the “Cwm fjordbank glyphs vext quiz” is in this tweet.

Port Scan (but only for HTTP protocol)

JS 10

Surprisingly, it also appears to be performing a port scan. However, JavaScript can only do this for the HTTP protocol.

Exfiltration

JS 11

Finally, this processing is executed at the end of this main logic. The target information collected so far is summarized in JSON format, encrypted with random key, and then sent as a POST request to this website.
The part highlighted in red in the screenshot above is the hard-coded random key.

JS 12

Looking at the implementation of the encryption method G0Z8kCJ as above, it seems to be encrypted using AES-CBC with PKCS#7. The encryption key is hard-coded as an argument in place of the line uvj9_L = G0Z8kCJ(HDHTgWc, "[KEY]"); as I mentioned, so it seems possible to decrypt what kind of data is being sent using this.

I (and ChatGPT) actually wrote a script to decryption it and was able to reveal what data is being sent. Here is the script:

const CryptoJS = require('crypto-js');

const encryptedBase64 = "[BASE_64_ENCODED_DATA]";
const key = "[HARDCODED_KEY]";

const encryptedBytes = CryptoJS.enc.Base64.parse(encryptedBase64);

const iv = CryptoJS.lib.WordArray.create(encryptedBytes.words.slice(0, 4), 16);
const ciphertext = CryptoJS.lib.WordArray.create(encryptedBytes.words.slice(4));

const aesKey = CryptoJS.enc.Utf8.parse(key.padEnd(16, '0'));

const decrypted = CryptoJS.AES.decrypt(
    { ciphertext: ciphertext },
    aesKey,
    { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
);

const plaintext = decrypted.toString(CryptoJS.enc.Utf8);

// Display the result
const json = JSON.parse(plaintext);
console.log("Decrypted data:\n\n", JSON.stringify(json, null, 2));

As a result, we found that the following data was actually sent:

{
  "canvas": {
    "canvasHash": [REDACTED]
  },
  "webgl": {
    "renderer": "ANGLE (...)",
    "vendor": "Google Inc. (Google)",
    "maxTextureSize": 8192,
    "maxVertexUniforms": 4096,
    "maxFragmentUniforms": 4096,
    "extensions": [
      "EXT_clip_control",
      // ...Omitted
      "WEBGL_stencil_texturing"
    ],
    "parameters": {
      "antialias": true,
      "blueBits": 8,
      "redBits": 8,
      "greenBits": 8,
      "alphaBits": 8,
      "depthBits": 24,
      "maxCombinedTextureImageUnits": 64,
      "maxCubeMapTextureSize": 16384,
      "maxRenderbufferSize": 8192,
      "maxViewportDims": {
        "0": 8192,
        "1": 8192
      },
      "maxAnisotropy": 16
    }
  },
  "features": {
    "deviceMemory": 4,
    "hardwareConcurrency": 2,
    "userAgent": "[REDACTED]",
    "language": "en-US",
    "languages": [
      "en-US"
    ],
    "platform": "Linux x86_64",
    "plugins": [],
    "mimeTypes": [
      {
        "type": "application/pdf",
        "description": "",
        "suffixes": "pdf"
      },
    ],
    "doNotTrack": null,
    "cookieEnabled": true,
    "timezone": "[REDACTED]",
    "timezoneOffset": 0,
    "touchPoints": 0,
    "screenResolution": "2238x1159",
    "colorDepth": 24,
    "pixelRatio": 1.5
  },
  "capabilities": {
    "localStorage": true,
    "sessionStorage": true,
    "indexedDB": true,
    "addBehavior": false,
    "openDatabase": false,
    "webdriver": false,
    "battery": true,
    "webSocket": true,
    "webWorker": true,
    "serviceWorker": true,
    "canvas2D": true,
    "webGL": true,
    "webGL2": true,
    "webXR": true,
    "gamepads": true,
    "touchscreen": false,
    "cssMedia": {
      "prefersDarkMode": false,
      "prefersReducedMotion": false,
      "prefersReducedData": false,
      "colorGamut": "srgb",
      "forcedColors": false,
      "hdr": false
    }
  },
  "sensors": {
    "deviceMotion": true,
    "deviceOrientation": true,
    "absoluteOrientation": true,
    "accelerometer": true,
    "gyroscope": true,
    "magnetometer": false,
    "ambient": false,
    "proximity": false
  },
  "window_properties": {
    "0": {
      "type": "object",
      "properties": {}
    },
    "CookieDeprecationLabel": {
      "type": "function",
      "code": "function CookieDeprecationLabel() { [native code] }",
      "prototype": {
        "type": "error",
        "message": "Inaccessible"
      },
      "toString": "function toString() { [native code] }"
    },
    "FetchLaterResult": {
      "type": "function",
      "code": "function FetchLaterResult() { [native code] }",
      "prototype": {
        "type": "error",
        "message": "Inaccessible"
      },
      "toString": "function toString() { [native code] }"
    },
    "fetchLater": {
      "type": "function",
      "code": "function fetchLater() { [native code] }",
      "prototype": {},
      "toString": "function toString() { [native code] }"
    },
    "onscrollsnapchange": {
      "type": "object",
      "value": null,
      "setter": true
    },
    "onscrollsnapchanging": {
      "type": "object",
      "value": null,
      "setter": true
    },
    "CSSNestedDeclarations": {
      "type": "function",
      "code": "function CSSNestedDeclarations() { [native code] }",
      "prototype": {
        "type": "error",
        "message": "Inaccessible"
      },
      "toString": "function toString() { [native code] }"
    },
    "SnapEvent": {
      "type": "function",
      "code": "function SnapEvent() { [native code] }",
      "prototype": {
        "type": "error",
        "message": "Inaccessible"
      },
      "toString": "function toString() { [native code] }"
    },
    // ...Omitted...
  },
  "errors": [],
  "native_functions": {
    "mainScope": {
      "console.log": "function log() { [native code] }",
      "setTimeout": "function setTimeout() { [native code] }",
      "setInterval": "function setInterval() { [native code] }",
      "Function": "function Function() { [native code] }",
      "Error": "function Error() { [native code] }",
      "RegExp": "function RegExp() { [native code] }",
      "eval": "function eval() { [native code] }",
      "Object.defineProperty": "function defineProperty() { [native code] }"
    },
    "workerScope": {
      "console.log": "function log() { [native code] }",
      "setTimeout": "function setTimeout() { [native code] }",
      "setInterval": "function setInterval() { [native code] }",
      "Function": "function Function() { [native code] }",
      "Error": "function Error() { [native code] }",
      "RegExp": "function RegExp() { [native code] }",
      "eval": "function eval() { [native code] }",
      "Object.defineProperty": "function defineProperty() { [native code] }"
    }
  },
  "codecs": {
    "codecsHash": [REDACTED]
  },
  "fonts": {
    "detectedFonts": [
      "Arial",
      "Courier",
      "Courier New",
      "Helvetica",
      "Lato",
      "Quicksand",
      "Times",
      "Times New Roman"
    ],
    "fontHash": [REDACTED]
  },
  "rebrowser": {
    "detectionTimestampStart": [REDACTED],
    "detectionTimestampEnd": [REDACTED],
    "timedOut": false,
    "patchLikelihood": "Low",
    "confidenceScore": 0,
    "findings": [],
    "detections": {
      "randomEventListenerMain": false,
      "randomEventListenerIframe": false,
      "randomEventListenerWorker": false
    },
    "suspiciousEventNames": [],
    "listenerStrings": {
      "main": [],
      "iframe": [],
      "worker": []
    },
    "errors": []
  },
  "ports": {
    "error": "[REDACTED]"
  }
}

Wallet Connecting JavaScript Code Analysis

After that information-stealing JavaScript is executed, other JavaScripts are executed that automatically attempt to connect to the victim’s crypto wallet.

Requests 1

After observing the HTTP requests, it appears that ethers.js is being used for this purpose as above screenshot.

JS Eth 1

As we can see from the code above, it appears to be attempting to connect to the victim’s Ethereum wallet using WalletConnect v2 protocol.

JS Eth 2

It also uses Web3Modal v2 to prompt victims to connect to their wallets.

JS Eth 3

This code above is also obfuscated and difficult to read, but since it uses the getProvider and getAccount methods, it seems to be acquiring information about the connected wallet.

JS Eth 4

And it looks like this part executes the wallet connection and then performs the callback processing.

JS Eth 5

This code seems to be the UI implementation of the Connect your wallet button.

Conclusion

The phishing site, which pretends to be OpenSea, first collects information about the victim’s browser and open ports, encrypts that data and sends it in a POST request, and then attempts to connect to a crypto wallet. The first JavaScript saw sophisticated implementations to detect if the victim was using sandboxing or automation tools. Personally, I felt this was a rather elaborate way of writing.

IOCs

Domains

  • m[.]opensea[.]io

URLs

  • hxxps://dashboard-center[.]com

Platforms

  • Microsoft
  • OpenSea
  • Outlook