codehawks-2023-08-sparkn-h01

[H-01] The same signature can be used in different distribution implementation causing that the caller who owns the signature, can distribute on unauthorized implementations

보고서

Summary

서명하는 데이터에 로직 컨트랙트 주소를 포함하지 않아 다른 로직 컨트랙트를 이용하더라도 이전 서명을 사용하여 리워드를 분배할 수 있다.

Keyword

signature, lack of input validation

Vulnerability

새로운 컨테스트를 등록하기 위해서는 먼저 관리자가 setContest를 호출해야 한다. 이 시점에 해당 컨테스트의 종료 시점, 운영자 계정, 컨테스트 ID, 로직 컨트랙트 주소를 지정한다.

File: ProxyFactory.sol
105:     function setContest(address organizer, bytes32 contestId, uint256 closeTime, address implementation)
106:         public
107:         onlyOwner
...
...
113:         bytes32 salt = _calculateSalt(organizer, contestId, implementation);
114:         if (saltToCloseTime[salt] != 0) revert ProxyFactory__ContestIsAlreadyRegistered();
115:         saltToCloseTime[salt] = closeTime;

deployProxyAndDistributeBySignature 함수를 이용하면 organizer가 직접 컨트랙트콜 하지 않고 서명을 이용하여(메타 트랜잭션) 프록시를 배포하고 상금 분배를 할 수 있다. 이 때, bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, data))); 코드를 보면 서명에 로직 컨트랙트 주소(implementation)는 포함되지 않는다는 것을 볼 수 있다.

File: ProxyFactory.sol
152:     function deployProxyAndDistributeBySignature(
153:         address organizer,
154:         bytes32 contestId,
155:         address implementation,
156:         bytes calldata signature,
157:         bytes calldata data
158:     ) public returns (address) {
159:         bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, data)));
160:         if (ECDSA.recover(digest, signature) != organizer) revert ProxyFactory__InvalidSignature();
161:         bytes32 salt = _calculateSalt(organizer, contestId, implementation);
162:         if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
163:         if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
164:         address proxy = _deployProxy(organizer, contestId, implementation);
165:         _distribute(proxy, data);
166:         return proxy;
167:     }

모종의 이유로 동일한 컨테스트 ID로 다른 로직 컨트랙트를 이용하도록 설정을 바꾸었다고 가정하자. (업데이트된 Distributor 로직을 사용하기 위해서 변경할 수 있겠다. 관리자가 setContest 함수를 호출하면 기존에 setContest를 호출해 등록한 컨테스트라도 수정할 수 있다.)

이 때, 로직 컨트랙트 주소는 변했지만 여전히 기존 서명을 이용하여 deployProxyAndDistributeBySignature를 호출할 수 있다. 따라서 organizer가 변경된 로직 컨트랙트에 동의하고 새로 서명하지 않더라도, 기존 서명을 이용하여 리워드를 분배할 수 있다.

Impact

이전 서명을 이용하여 새 로직 컨트랙트로 리워드를 분배할 수 있다.

Mitigation

서명에 로직 컨트랙트 주소도 포함한다.

    function deployProxyAndDistributeBySignature(
        address organizer,
        bytes32 contestId,
        address implementation,
        bytes calldata signature,
        bytes calldata data
    ) public returns (address) {
-      bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, data)));
+      bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, implementation, data)));

tags: bughunting, sparkn, smart contract, solidity, signature, lack-of-input-validation-vul, severity high