02.stake漏洞
2023-06-23 20:22:31 # 00.security

改正

stake漏洞

任务描述:将setup.sol中的complete方法返回true,分析一下,实质就是让我在erc20合约中的余额要大于500000ETH。

这个代码描述的业务是:有一个合约A(ztstakepool),每操作一次stake,可以传入一个参数blocknumber(这个合约A提供给你的利息,因为你把钱存到它这里),当blocknumber达到规定的数量后(当前区块达到blocknumber),才可以调用swap方法,将利息转出去。它希望我们执行完1,2,3步骤之后,合约只能让我们从新从1开始循环。合约A希望的正常交易流程如下:

但是,合约A写的合约有问题,我们可以直接获取任意数额的利息!

分析过程

我们想要获取利息,那么就必须要调用swap方法。我们分析这个代码发现,第一个require是确定转账地址,第二个require是不可以提取的利息大于利息本身,最重要的是要调用workBalanceOf这个方法,来获取利息的数量。

1
2
3
4
5
6
7
function swap(address from,address to,uint amount)external{
require(from==address(this)&&to==address(token));
uint balance = workBalanceOf(msg.sender);
require(balance>=amount,"exceed");
workbalance[msg.sender] -= amount;
token.transfer(msg.sender,amount*10**18);
}

但是workBalanceOf这个方法需要pass为true才行,也就是unstake之后才可以调用

1
2
3
4
5
6
7
8
function workBalanceOf(address account)public view returns(uint){
bool pass=passed[account];
if(pass){
return workbalance[account];
}else{
return 0;
}
}

我们查看unstake方法(也就是问题出现的地方):调用这个方法唯一的要求是block.number>=user.startTime+user.duration,我们满足了这个条件就可以取出利息了!但是stake,unstake,swap之间存在逻辑问题

黑客说:因为unstake的唯一要求是block.number>=user.startTime+user.duration,那么我们可以创建一个账户,存0元,利息设置为1(blocknumber)。因为只有一个区块的要求,那么该区块链很快就会达到这个要求,将pass设置为true。这时候我们再次存款0元,利息设置为任何值,都可以调用swap方法,因为pass已经变为true了,可以调用workBalanceOf方法,直接取出利息!

因此问题就出在:我们取出利息,将pass设置为true,就一直是true,没有任何一种机制将他设置为false。

解决方案:在每次stake之前将pass设置为false,这里不做展示。

1
2
3
4
5
6
7
8
function unstake()external{
userInfo storage user = userDetails[msg.sender];
require(block.number>=user.startTime+user.duration,"you are in a hurry ");
passed[msg.sender] = true;
uint amount = user.amount;
user.amount = 0;
token.transfer(msg.sender,amount*10**18);
}

攻击步骤

调用unstake成功,如下图:

调用unstake成功

调用swap,拿出利息

我们真的盗取了500000ETH利息!

isSolved方法返回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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;

library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");

return c;
}

function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: subtraction overflow");
uint256 c = a - b;

return c;
}

function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}

uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");

return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0, "SafeMath: division by zero");
uint256 c = a / b;

return c;
}
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0, "SafeMath: modulo by zero");
return a % b;
}
}

contract ERC20 {
using SafeMath for uint256;

mapping (address => uint256) public _balances;

mapping (address => mapping (address => uint256)) public _allowances;

string public _name;
string public _symbol;
uint8 public _decimals;

constructor (string memory name, string memory symbol) public {
_name = name;
_symbol = symbol;
_decimals = 18;
}

function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}

function transfer(address recipient, uint256 amount) public virtual returns (bool) {
_transfer(msg.sender, recipient, amount);
return true;
}

function approve(address spender, uint256 amount) public virtual returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}

function transferFrom(address sender, address recipient, uint256 amount) public virtual returns (bool) {
_transfer(sender, recipient, amount);
_approve(sender, msg.sender, _allowances[sender][msg.sender].sub(amount));
return true;
}

function _transfer(address sender, address recipient, uint256 amount) internal virtual {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");

_balances[sender] = _balances[sender].sub(amount);
_balances[recipient] = _balances[recipient].add(amount);
}

function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_balances[account] = _balances[account].add(amount);
}

function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_balances[account] = _balances[account].sub(amount);
}

function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
}

}

contract ZT is ERC20("ZERO TOKEN", "ZT"){
//RULE 1
bytes32 constant RULE_WITHDRAW_WANT = keccak256(abi.encodePacked("withdraw"));

//RULE 2
bytes32 constant RULE_NONE_WANT = keccak256(abi.encodePacked("depositByValue"));


constructor()public{
_mint(msg.sender,10000000*10**18);
}
function depositByWant(uint _amount)external payable{
uint amount = _amount.mul(10**18);
require(msg.value>=amount,"you want to trick me?");
MkaheChange(msg.sender,amount,RULE_NONE_WANT);
}

function withdraw(uint _amount)external payable returns(bool){
uint amount = _amount.mul(10**18);
require(balanceOf(msg.sender)>=amount);
_balances[msg.sender] = _balances[msg.sender].sub(amount);
return MkaheChange(msg.sender,amount,RULE_WITHDRAW_WANT);
}

function MkaheChange(address to,uint amount,bytes32 ID)internal returns(bool){
if(ID==RULE_NONE_WANT)
{
_balances[msg.sender]=_balances[msg.sender].add(amount);
return true;
}else if(ID==RULE_WITHDRAW_WANT){
bool a;
(a,)=payable(to).call.value(amount)("");
require(a,"withdraw fail");
return true;
}
else{
return false;
}
}

fallback()external payable{
MkaheChange(
msg.sender,
msg.value,
RULE_NONE_WANT
);
}
}

contract ZTstakepool{
ZT token;
uint totalsupply;
string symbol;
mapping(address=>uint)internal workbalance;
mapping(address=>bool)internal passed;

struct userInfo{
uint amount;
uint duration;
uint startTime;
}
mapping(address=>userInfo)internal userDetails;

constructor()public{
token =new ZT();
symbol = "stTGT";
totalsupply = token.balanceOf(address(this));
}

function getDetails(address account)public view returns(userInfo memory){
return userDetails[account];
}

function workBalanceOf(address account)public view returns(uint){
bool pass=passed[account];
if(pass){
return workbalance[account];
}else{
return 0;
}
}

function Zt()public view returns(address){
return address(token);
}

function stake(uint amount,uint blocknumber)external{
require(blocknumber>=1,"At least 1 block");

userInfo storage user = userDetails[msg.sender];

user.startTime = block.number;
user.duration = blocknumber;
user.amount += amount;

token.transferFrom(msg.sender,address(this),amount*10**18);
workbalance[msg.sender] += blocknumber;

}

function unstake()external{
userInfo storage user = userDetails[msg.sender];
require(block.number>=user.startTime+user.duration,"you are in a hurry ");
passed[msg.sender] = true;
uint amount = user.amount;
user.amount = 0;
token.transfer(msg.sender,amount*10**18);
}

function swap(address from,address to,uint amount)external{
require(from==address(this)&&to==address(token));
uint balance = workBalanceOf(msg.sender);
require(balance>=amount,"exceed");
workbalance[msg.sender] -= amount;
token.transfer(msg.sender,amount*10**18);
}
}

contract setup{
ZTstakepool public stakePool;
ZT public erc20;
bool solve;
constructor()public{
stakePool =new ZTstakepool();
erc20 = ZT(payable(stakePool.Zt()));
}
function isSolved()public view returns(bool){
return solve;
}
function complete()public{
require(erc20.balanceOf(msg.sender)>=500000*10**18);
solve = true;
}
}
Prev
2023-06-23 20:22:31 # 00.security
Next