Hiding in the Ether - Wallet Draining Phish

Good morning, readers! While poking around in inboxes and spam folders, I managed to dig up an interesting crypto wallet drainer that makes some interesting use of infrastructure. Grab a coffee, sit back, and let's take a look at this phish and how it leverages basic redirection tactics, IPFS, and even Ethereum Smart Contracts to deliver it's final landing page.


Email Pretext

The phishing email itself is spoofing Bittrex. In this case, the email is stating that an admistration service provider has been assigned to us so that we may claim our funds.

YwRAeL.jpg

If you aren't aware, the reason that this pretext is even remotely believable is that Bittrex has notified users that they are shutting down services. Here's the notification available on their site:

YwRfYT.jpg

Interesting Redirect Infrastructure

Before just jumping to the end and showing that this phish is designed to steal our seed phrases, let's take a look to see what's actually going on and how our session is handled. If you're more interested in the IoCs instead, you can find them at the bottom.

When we click the link, we are initially sent to an X/Twitter shortened URL:

  • hxxps://t[.]co/Khmb9qWFUZ

This is the underlying html:

<head>
  <noscript>
    <meta
      http-equiv="refresh"
      content="0;URL=hxxps://ipfs[.]io/ipfs/QmXm7F2nGe72y5vqZiHWpJyDix6GETqzoRUePax1PiKc5K"
    />
  </noscript>
  <title>
    hxxps://ipfs[.]io/ipfs/QmXm7F2nGe72y5vqZiHWpJyDix6GETqzoRUePax1PiKc5K
  </title>
</head>
<script>
  window[.]opener = null; location[.]replace("https:\/\/ipfs[.]io\/ipfs\/QmXm7F2nGe72y5vqZiHWpJyDix6GETqzoRUePax1PiKc5K")
</script>

The redirect is taking us to an IPFS url:

  • hxxps://ipfs[.]io/ipfs/QmXm7F2nGe72y5vqZiHWpJyDix6GETqzoRUePax1PiKc5K

While the use of IPFS for phishing isn't overly novel, I really liked how the actors handled the next section, or how we get the final redirect to the phishing host. When loading the ipfs hosted content, we are presented with the following html:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <script>
      fetch("hxxps://1rpc[.]io/eth", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON[.]stringify({ id: 0, jsonrpc: "2.0", method: "eth_call", params: [{ to: "0x0558aaa5290385322bb5af964bff7e90477d7636", data: "0x8a054ac2" }, "latest"] }) }).then(t => { t[.]json().then(t => { let e = "", a = t[.]result[.]slice(130); for (; a[.]length > 0;) { let o = String[.]fromCharCode(parseInt(a[.]substring(0, 2), 16)); if ("\0" == o) break; e  = o, a = a[.]slice(2) } window[.]location = `hxxps://${e}` }) });
    </script>
  </head>
</html>

We can see that a fetch is happening to 1rpc[.]io/eth. 1RPC is a blockchain relay, from their site, they describe themselves as:

1RPC is a relay that protects user privacy and keeps AI accountable with attested TEEs. A Trusted Execution Environment, or TEE, provides hardware-based isolation to ensure the confidentiality and integrity of computation that runs on it.

What does this mean for our phish? The code that's being run is fetching the content of an Ethereum smart contract. Specifically, 0x0558aaa5290385322bb5AF964bFF7e90477d7636. We'll dive just a bit further into the contract in a bit. Once the contract is fetched, the code takes the result, decodes a portion of it, then puts the intended URL into a window.location for redirect.

I'll break this down:

1RPC response with the following:

{
  jsonrpc: '2.0',
  result: '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000027636c69656e742e3739333231352d626974747265782e636f6d2f686f6d652f3f763d363332313400000000000000000000000000000000000000000000000000',
  id: 0
}

Our code then takes the result field content and removes the first 130 characters. This is to remove some of the extra content within the Ethereum ABI.

// First 32 bytes are pointer, next 32 bytes are length, +2, equals a 130 character offset
let a = t.result.slice(130);
// results in 636c69656e742e3739333231352d626974747265782e636f6d2f686f6d652f3f763d363332313400000000000000000000000000000000000000000000000000

Now the code's loop can iterate and hex decode the following (it skips the nulls so I removed them):

636c69656e742e3739333231352d626974747265782e636f6d2f686f6d652f3f763d3633323134

This then results in the following decoded URL (oh hey, it's spoofing Bittrex):

client[.]793215-bittrex[.]com/home/?v=63214

Here's a full commented verion of the JavaScript for those interested:

// Makes a network request to an Ethereum RPC endpoint
fetch("https://1rpc.io/eth", {
  // Specifies this is a POST request
  method: "POST",
  // Sets the request headers to indicate JSON content
  headers: {
    "Content-Type": "application/json"
  },
  // Constructs and stringifies the JSON-RPC payload
  body: JSON.stringify({
    // Request ID (standard in JSON-RPC)
    id: 0,
    // JSON-RPC protocol version
    jsonrpc: "2.0",
    // Calling the eth_call method to execute a read-only contract function 
    method: "eth_call",
    // Parameters for the eth_call
    params: [{
      // Target contract address to call
      to: "0x0558aaa5290385322bb5af964bff7e90477d7636",
      // Function selector/calldata (0x8a054ac2 is the first 4 bytes of the keccak256 hash of the function signature)
      data: "0x8a054ac2"
    }, "latest"] // Using the latest block for this call
  })
// Handles the response after the fetch completes
}).then(t => {
  // Parses the response body as JSON
  t.json().then(t => {
    // Logs the complete response to console
    console.log(t);
    // Creates an empty string to build the final output
    let e = "";
    // Extracts a portion of the result, skipping the first 130 characters
    // First 32 bytes are pointer, next 32 bytes are length, +2, equals a 130 character offset
    let a = t.result.slice(130);
    // Loop to process the hex-encoded sliced string
    // 636c69656e742e3739333231352d626974747265782e636f6d2f686f6d652f3f763d363332313400000000000000000000000000000000000000000000000000
    while (a.length > 0) {
      // Converts each hex byte (2 characters) to its ASCII character
      let o = String.fromCharCode(parseInt(a.substring(0, 2), 16));
      // Stops at null terminators, because that's the end of the string
      if (o == "\0") {
        break;
      }
      // Appends the decoded character to the result string
      e += o;
      // Removes the processed hex byte from the string
      a = a.slice(2);
    }
    // COMMENTED OUT: Would redirect the browser to the extracted URL
    //window.location = `https://${e}`;
    // Logs the extracted string to console
    console.log(e)
  });
});

Seed Phrase Stealing

Now that we've received the final landing page for the phish, we're presented with a window to enter our email.

YwR2Su.jpg

Once we enter our email, we're presented with a wallet selector:

YwRRtt.jpg

What's really interesting is that reown is described a:

"Reown AppKit to enable wallet connections and interact with 500+ EVM networks, Solana, and Bitcoin"

Essentially, the attackers' integration of reown allows them to directly embed wallets into our session without us actually needing to install them. This is a feature of reown: https://reown.com/blog/appkits-universal-embedded-wallets

Let's just stick with Metamask

YwRZca.jpg

Oh hey, we need to enter our seed phrase, surely an app with embedded wallets and hosted on attacker infrastructure wouldn't steal them (insert eye roll here).

YwR9aw.jpg

Smart Contracts

When taking a look at the contract, 0x0558aaa5290385322bb5AF964bFF7e90477d7636, we can see that the contract creator was 0x3249b1630d6a2accb9498ee701c983ea8f815bd2. Because this blockchain is, quite literally designed for transparency, we can see this all on etherscan:

This allows us to see transactional data which, in our case, allows us to see updates the threat actors have made to the contract. Allowing us to see all previous URLs they were using on this contract:

client[.]793215-bittrex[.]com/home/?v=63214
client[.]793215-bittrex[.]com/home/?v=63214
client[.]842739-bittrex[.]com/home/?v=78391
client[.]793215-bittrex[.]com/home/?v=63214
client[.]942379-blockfi[.]com/home/?v=234908
client[.]793215-bittrex[.]com/home/?v=63214
client[.]942379-blockfi[.]com/home/?v=234908
secured[.]client-galxe[.]com/@/g/
example[.]com
client[.]secured-galxe[.]com/@/g/
rewards[.]gate-galxe[.]com/@/g/
rewards[.]beta-galxe[.]com/@/g/
wikipedia[.]org
reddit[.]com

Building on the blockchain transparency, we can also see all the other contracts that this wallet created along with all URLs they used/updated contracts with:

  • 0xAD8Bcd576470deb183dFF61B557E3Ab37e5F2e73

URLs:

client[.]328713-blockfi[.]com/home/?v=342984
client[.]328713-blockfi[.]com/home/
rebate-kroll[.]com/home/?ref=872842
secured-kroll[.]com/home/?ref=872842
client[.]secure-kroll[.]com/home/
client-kroll[.]com/home/
client-blockfi[.]com/home/
client[.]32183-blockfi[.]com/home/
client[.]87242-blockfi[.]com/home/
wikipedia[.]org
  • 0xdd4F833aa72f3e1F4e0BB7aDe3e93F12860B3BE7

URLs:

client[.]423839-ftx[.]com/home/?v=32817
client[.]749243-ftx[.]com/home/?v=32891
client[.]32982-ftx[.]com/home/?v=572943
client[.]32982-ftx[.]com/home/
crypto-chrono24[.]com/home/
secured-chrono24[.]com/home/
secured-chrono24[.]com/home/
secured-chrono24[.]com/home/
rewards-chrono24[.]com/home/
  • 0x4AFDb927c9218F71b1EE0a4ADf0C6f39beF913d8

URLs:

example[.]com
  • 0x366ad92A50F3770f20E9d5cf87F53023014f3c9b

URLs:

client[.]789482-blockfi[.]com/home/?v=73194
client[.]879312-blockfi[.]com/home/?v=84922
client[.]463479-blockfi[.]com/home/?v=73293
client[.]368251-blockfi[.]com/home/?v=1
client[.]781293-blockfi[.]com/home/?v=789421
client[.]897421-blockfi[.]com/home/?v=482931
example[.]com

For all of these contracts, we can see that the creator is 0x3249b1630d6a2accb9498ee701c983ea8f815bd2

Update 2 May, 2025: I've added a script that will allow you to extract every domain that an address has sent to a ethereum smart contract:

GitHub - synfinner/ethContractor: Python investication tool to retrieve eth smart contract interactions from an address to find URLs that may be added (or other ascii strings)
Python investication tool to retrieve eth smart contract interactions from an address to find URLs that may be added (or other ascii strings) - synfinner/ethContractor

IOCs

Unique list of domains/paths used by this address for redirection:

client-blockfi[.]com/home/
client-kroll[.]com/home/
client[.]32183-blockfi[.]com/home/
client[.]328713-blockfi[.]com/home/
client[.]328713-blockfi[.]com/home/?v=342984
client[.]32982-ftx[.]com/home/
client[.]32982-ftx[.]com/home/?v=572943
client[.]368251-blockfi[.]com/home/?v=1
client[.]423839-ftx[.]com/home/?v=32817
client[.]463479-blockfi[.]com/home/?v=73293
client[.]749243-ftx[.]com/home/?v=32891
client[.]781293-blockfi[.]com/home/?v=789421
client[.]789482-blockfi[.]com/home/?v=73194
client[.]793215-bittrex[.]com/home/?v=63214
client[.]842739-bittrex[.]com/home/?v=78391
client[.]87242-blockfi[.]com/home/
client[.]879312-blockfi[.]com/home/?v=84922
client[.]897421-blockfi[.]com/home/?v=482931
client[.]942379-blockfi[.]com/home/?v=234908
client[.]secure-kroll[.]com/home/
client[.]secured-galxe[.]com/@/g/
crypto-chrono24[.]com/home/
example[.]com
rebate-kroll[.]com/home/?ref=872842
reddit[.]com
rewards-chrono24[.]com/home/
rewards[.]beta-galxe[.]com/@/g/
rewards[.]gate-galxe[.]com/@/g/
secured-chrono24[.]com/home/
secured-kroll[.]com/home/?ref=872842
secured[.]client-galxe[.]com/@/g/
wikipedia[.]org

URLs used for redirection:

hxxps://t[.]co/Khmb9qWFUZ
hxxps://ipfs[.]io/ipfs/QmXm7F2nGe72y5vqZiHWpJyDix6GETqzoRUePax1PiKc5K

ETH Wallet Address:

0x3249b1630d6a2accb9498ee701c983ea8f815bd2

Smart Contracts (at time of publication)

0xAD8Bcd576470deb183dFF61B557E3Ab37e5F2e73
0xdd4F833aa72f3e1F4e0BB7aDe3e93F12860B3BE7
0x4AFDb927c9218F71b1EE0a4ADf0C6f39beF913d8
0x366ad92A50F3770f20E9d5cf87F53023014f3c9b