涉及的密碼學原理很簡單,但是題目看了很久纔看懂
題目代碼分析
題目要求我們挖一個boulicoin出來
實際上boulicoin就是一個flag
server.py
from flask import Flask
from flask import jsonify
from flask import request
from Crypto.Cipher import AES
import base64
import boulicoin
import json
app = Flask(__name__)
def pad(data):
length = 16 - (len(data) % 16)
data += chr(length)*length
return data
def unpad(data):
assert data[-ord(data[-1]):]==ord(data[-1])*data[-1]
return data[:-ord(data[-1])]
def encrypt(data):
aes=AES.new(boulicoin.SECRET_KEY, AES.MODE_CBC, boulicoin.AES_IV)
return base64.b64encode(aes.encrypt(pad(str(data))))
def decrypt(data):
aes=AES.new(boulicoin.SECRET_KEY, AES.MODE_CBC, boulicoin.AES_IV)
return unpad(aes.decrypt(base64.b64decode(data)))
''' AES-CBC,密鑰和IV值未知,Padding方式爲PKCS5 '''
''' 注意這裏可以看出密鑰和IV值是永遠不變的。因爲是用的boulicoin這個包裏的變量,沒有對他進行過操作,因此值肯定是固定的'''
#若請求訪問url: host/task,則執行getTask()
@app.route('/task')
def getTask():
'''
要求客戶端(我們)解決完全揹包問題:容量爲size的揹包,裝len(items)種物品,第i種物品價值爲item[i][0],物品重量爲item[i][1]。
向請求方發送 items、size、scoreMin的明文和簽名,簽名算法使用前面的AES '''
items,size,scoreMin = boulicoin.generateTask()
itemsEnc = encrypt(items)
sizeEnc = encrypt(size)
scoreMinEnc = encrypt(scoreMin)
return jsonify({'items':items,
'size':size,
'scoreMin':scoreMin,
'itemsEnc':itemsEnc,
'sizeEnc':sizeEnc,
'scoreMinEnc':scoreMinEnc})
#若使用POST方法請求url: host/coin 則執行getCoin()
@app.route('/coin', methods=['POST'])
def getCoin():
'''
驗證請求方是否解決了揹包問題,並驗證明文及其簽名是否對應,
以防止我們自己僞造items size scoreMin這些參數。
'''
req=request.json
if ('itemsEnc' not in req) or ('sizeEnc' not in req) or ('scoreMinEnc' not in req) or ('selected' not in req):
return jsonify({'status':'error','message':'JSON attributes missing'})
items=[]
size=-1
try:
items=json.loads(decrypt(req['itemsEnc']))
size=int(decrypt(req['sizeEnc']))
scoreMin=int(decrypt(req['scoreMinEnc']))
selected=req['selected']
assert type(selected)==type([])
assert len(set(selected))==len(selected)
except:
return jsonify({'status':'error','message':'Failed to decode data.'})
score=0
weight=0
try:
for it in selected:
if it<0 or it>=len(items):
return jsonify({'status':'error','message':'Invalid item index.'})
score+=items[it][0] # score = ∑items[i][0]
weight+=items[it][1] # weight = ∑items[i][1]
except:
return jsonify({'status':'error','message':'Invalid proof of work.'})
# 要求score >= scoreMin weight <= size
if score>=scoreMin and weight<=size:
coin=boulicoin.generateCoin()
assert coin.startswith('INSA{')
return jsonify({'status':'success','boulicoin':coin})
return jsonify({'status':'error','message':'Invalid proof of work.'})
if __name__ == '__main__':
app.run(debug=True)
client.py
import requests
import random
import sys
def solve(items, scoreMin, size):
"""
求解服務器發給我們的揹包問題
"""
bestScore = -1
while True:
selected = []
score = 0
occupiedSize = 0
# 瞎蒙法? 不是很理解
for i in range(len(items)):
if random.getrandbits(1):
selected.append(i)
for item in selected:
score += items[item][0]
occupiedSize += items[item][1]
if occupiedSize <= size and score > bestScore:
bestScore = score
print "%s/%s" % (score, scoreMin)
if score >= scoreMin:
return selected
def work(host):
req = requests.get('https://' + host + '/task').json()
selected = solve(req['items'], req['scoreMin'], req['size'])
req['selected'] = selected
coin = requests.post('https://' + host + '/coin', json=req).json()
if coin['status'] != 'success':
print 'Something wrong happened'
sys.exit(1)
return coin['boulicoin']
if __name__ == '__main__':
if len(sys.argv) != 2:
print 'Usage: python2 client.py host'
sys.exit(1)
wallet = []
coin = work(sys.argv[1])
wallet.append(coin)
print 'New coin added : ' + coin
如果能挖出來,用wireshark一看就能獲得flag。當然服務器肯定不會給我們一個好求的揹包問題,讓我們直接用client.py就能跑出來。(其實根本就是無解的,而不是不好求)那麼我們考慮服務器端存在的問題
漏洞分析
參考以下writeup
https://github.com/HugoDelval/inshack-2017/blob/master/challenges/crypto/boulicoin-225/writeup.md
首先經過測試發現,每一次給你的完全揹包問題(以下簡稱task)都是不一樣的。也就是每次規定的 items、size、scoreMin都是不一樣的,我們能獲得它們和它們的簽名。其中 items 是物品的重量和價值,size 是揹包的容量,scoreMin 是我們揹包裏要達到的價值。
首先你並不能自己製造 size 等參數,因爲服務器使用 m||Ek(m) 來防止你僞造 。但注意到 m || Ek(m) 每次的k都一樣。那麼雖然我們不能隨便自己造一個參數,但我們可以做一個重放攻擊。(其實這個Ek(m)可以認爲是一個對稱密碼實現的簽名)
那麼重放有什麼用?由於server對於client發回來的數據,僅僅是分別驗證task中的每一個參數是否僞造,而不驗證task 跟之前下發的task是否每個參數都相同,因此我們就可以從服務器的多個task中抽取一些我們想要的參數和MAC值重新構造出一個簡單的task來讓我們得以求解,再發回去。
比如現在服務器給你 task1(size1 = 400),task2(size2 = 1000)。 這兩個原問題哪個都不能求解,但是如果你把task1中的 size1 改成 size2,那這個問題就好求多了。
參考github上的exp
參(照)考(搬)以下腳本
https://github.com/HugoDelval/inshack-2017/blob/master/challenges/crypto/boulicoin-225/exploit/exploit.py
#!/usr/bin/python3
import requests
url = "http://52.82.121.166:28978"
def alive():
return 'status' in requests.post(url + '/coin',json={}).json()
def exploitable():
req=requests.get(url + '/task').json()
# 對應的scoreMin很小,多跑幾次一定能跑出scoreMin和scoreMinEnc對應
req['scoreMinEnc']='/eBOj8K3tGkk2olT/yGbRw=='
# 直接選0,1。沒想到這都行
req['selected']=[0,1]
return requests.post(url + '/coin',json=req).json()['boulicoin'].startswith('flag{')
if __name__ == "__main__":
print(alive() and exploitable())
#用wireshark看http,輸出True之後就能在包裏找到flag。
題目涉及的其他知識
Flask相關內容
內容可能不嚴謹,僅個人理解。有錯請指出
Flask是一個web應用框架,以一個應用爲例:
# hello.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
其中app = Flask(__name__)
是相當於開一個容器,跑一個web應用。這裏 傳參__name__
是因爲這個應用只有這一個模塊(文件)
下面這段話摘自:https://dormousehole.readthedocs.io/en/latest/quickstart.html#id2
如果你使用 一個單一模塊(就像本例),那麼應當使用 name ,因爲名稱會根據這個 模塊是按應用方式使用還是作爲一個模塊導入而發生變化(可能是 ‘main’ , 也可能是實際導入的名稱)。
@app.route('/')
表示 如果有請求訪問url ‘/’,那麼就執行下面這一段的邏輯。比如應用跑在 127.0.0.1:5000 ,訪問 http://127.0.0.1:5000/ 就會觸發hello_world()的執行
def hello_world():
return 'Hello, World!'
json相關內容
大概就是應用層解析器,把發過來的包的應用層(字節流)進行解析,方便得到其中的那些字段。具體的我並不太懂。