Ethernaut. Задача 24. Puzzle Wallet
В этой задаче нам нужно завладеть правами owner прокси контракта.
Ссылка на задачу
В чем суть?
Дааа, заставила эта задача посидеть над ней... Особенно с функцией multicall() в PuzzleWallet.
Хотя, если я правильно понял, на сегодняшний день контракта UpgradeableProxy в openzeppelin нет. Базовые прокси контракты там делятся на два типа: TransparentUpgradeableProxy и UUPSUpgradeable. Об этом был урок на канале ранее.
В прокси контрактах нужно понимать, как работают слоты памяти для переменных, которые вы объявляете в начале. Крайне важно, чтобы они были не только одинаковыми, но и располагались в одинаковом порядке.
В задаче же порядок нарушен:
address public pendingAdmin;
address public admin;
идут в прокси контракте и в PuzzleWallet:
address public owner;
uint256 public maxBalance;
Следовательно, использовав функцию в прокси контракте proposeNewAdmin(), мы можем не только предложить нового админа для прокси, но и установить админские права в PuzzleWallet.
Далее, по той же логике, нам нужно вызвать функцию в PuzzleWallet setMaxBalance(), которая, установив новый максимальный баланс у себя же, установит нового админа в прокси. Но как это сделать?
Просто вызвать ее не получится, так как на балансе контракта уже лежит 0.001 Эфира. Следовательно, нам нужно вызвать deposit() и пополнить наш баланс.
Однако, прямой вызов deposit() будет пополнять и наш счет и счет контракта, так что баланс контракт всегда будет чуть больше нашего. Поэтому напрямую его делать нельзя.
Обратимся к самой навороченной функции multicall(). Она принимает в себя массив байтов, проходит по нему, создает флаг depositCalled, и в конце через delegatecall() вызывает deposit(). Вот тут и кроется уязвимость.
Флаг depositCalled позволяет вызвать deposit() только один раз. Это защищает вызов от повторного использования. Это может затормозить решение задачи, если не знать некоторых нюансов.
А нюанс заключается в том, что мы можем внутри нашего multicall() вызвать еще один multicall, который в свою очередь будет вызывать deposit(), на который уже не будет ругаться проверка флага.
Разберем примерную логику:
bytes[] memory insideCall = new bytes[](1);
insideCall[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
calls[1] = abi.encodeWithSelector(PuzzleWallet.multicall.selector, insideCall);
Мы создаем массив байтов с фиксированной длиной, и туда с помощью abi.encodeWithSelector кладем функцию deposit().
Затем создаем уже наш "боевой" массив байтов с фиксированной длинной 2.
В первый слот массива просто также кладем зашифрованную функцию deposit(), а во второй - еще раз эту же функцию плюс наш insideCall. Далее сделаем вызов:
puzzleWallet.multicall{value: 0.001 ether}(calls);
Поняли логику? Мы передадим в multicall() наш массив байтов, в котором сможем выполнить deposit() дважды, и немного Эфира.
При этом уже внутри deposit() произойдет небольшая ошибка: так как мы дважды вызвали данную функцию, но передав всего 0.001 Эфира, то баланс контракта будет увеличен на 0.001, а наш личный баланс на 0.002.
Таким образом, после всех этих действий мы сможем вызвать функцию execute(), чтобы обнулить баланс контракта, и потом обратиться к функции setMaxBalance, чтобы захватить права админа в прокси контракте.
Фух, понимаю, пост получился длинный, но задачу по другому не разберешь.
Итак, код может выглядеть так:
Post #435
201