这篇文章是Cryptohack在Crypto On The Web部分的复现。

JSON WEB TOKENS

提供一个可以修改JWT的网站:JSON Web Tokens - jwt.io

Token Appreciation

在Web中,令牌(Token)是一个用于身份验证和授权的字符串。它们被用作身份验证凭证,以便验证用户的身份,并且还可以用于授权访问受保护资源。

在身份验证方面,令牌通常用于代替传统的用户名和密码。用户通过提供用户名和密码进行身份验证,并且如果验证成功,服务器会生成一个Token并返回给客户端。客户端可以在后续的请求中使用令牌来证明他们的身份,而无需再次提供用户名和密码。

令牌还可以用于授权访问受保护的资源。服务器可以要求客户端在每次请求中附带令牌,以便判断客户端是否有权限访问该资源。通过验证令牌的有效性并检查权限,服务器可以决定是否允许客户端访问请求的资源。

在Web开发中,常见的令牌类型包括JWT(JSON Web Token)、OAuth令牌(用于跨应用程序身份验证和授权)和访问令牌(用于访问API资源)等。

JavaScript对象签名和加密(JOSE)是一个指定在互联网上安全传输信息的方法的框架。它最著名的是JSON Web令牌(JWT),用于在网站或应用程序上授权自己。JWT通常会在输入用户名和密码进行身份验证后,将我们“登录会话”存储在浏览器中。换句话说,网站会给你一个JWT,其中包含你的用户ID,并且可以在不再次登录的情况下出示给网站来证明你是谁。JWT看起来是这样的:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmbGFnIjoiY3J5cHRve2p3dF9jb250ZW50c19jYW5fYmVfZWFzaWx5X3ZpZXdlZH0iLCJ1c2VyIjoiQ3J5cHRvIE1jSGFjayIsImV4cCI6MjAwNTAzMzQ5M30.shKSmZfgGVvd2OSB2CGezzJ3N6WAULo3w9zCl_T47KQ

它是base64编码的数据,分为三部分(用“.”分隔):表头(header)、有效载荷(payload)和签名(signature)。事实上,它是base64编码的变体,其中+和/被不同的特殊字符取代,因为它们可能会在URL中引起问题,他的构成形式如下:

1
header.payload.signature

JWT Sessions

存储会话的传统方式是使用会话ID cookies。登录网站后,会在后端(服务器)为我们创建一个会话对象,并为我们的浏览器(客户端)提供一个cookie,用于标识该对象。当我们向网站发出请求时,浏览器会自动将会话ID cookie发送到后端服务器,后端服务器会使用该ID在自己的内存中查找我们的会话,从而授权我们执行操作。

JWT的工作方式不同。登录后,服务器会向我们的web浏览器发送JWT中的整个会话对象,其中包含描述我们的用户名、权限和其他信息的键值对的有效负载。还包括一个使用服务器密钥创建的签名,旨在防止我们篡改有效负载。我们的web浏览器将令牌保存到本地存储中。

在随后的请求中,浏览器会将令牌发送到后端服务器。服务器首先验证签名,然后读取令牌负载以授权我们。

使用会话ID cookies,会话在服务器上运行,但使用JWT,会话在客户端上运行。

JWT相对于会话ID cookies的主要优势在于它们易于扩展。组织需要一种跨多个后端服务器共享会话的方式。当客户端从使用一个服务器或资源切换到使用另一个时,该客户端的会话应该仍然有效。此外,对于大型组织来说,可能会有数百万次会议。由于JWT位于客户端,因此它们解决了这些问题:任何后端服务器都可以通过检查令牌上的签名并读取令牌内的数据来授权用户。

不幸的是,JWT有一些缺点,因为它们通常以不安全的方式配置,客户端可以自由修改它们,并查看服务器是否仍会验证它们。

浏览器用于向服务器发送JWT的HTTP标头的常用的标头字段是”Authorization”。

具体来说,JWTs通常作为身份验证凭据发送到服务器。在HTTP请求的标头中,”Authorization”字段用于携带JWTs。JWTs被放置在该标头字段的值中,并以特定的格式传递给服务器,举个例子:

1
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

No Way JOSE

让我们来看看JWT算法。JWT的第一部分是JOSE标头,当对其进行解码时,它看起来是这样的:

1
{"typ":"JWT","alg":"HS256"}

这告诉服务器这是JWT,以及使用哪种算法来验证它。服务器必须处理此不受信任的输入,然后才能真正验证令牌的完整性。在理想的加密协议中,在对收到的消息执行任何进一步的操作之前,都要对其进行验证。

JWT中的“none”算法就是一个很好的例子。一道交互题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import base64
import json
import jwt # note this is the PyJWT module, not python-jwt


SECRET_KEY = ?
FLAG = ?


@chal.route('/no-way-jose/authorise/<token>/')
def authorise(token):
token_b64 = token.replace('-', '+').replace('_', '/') # JWTs use base64url encoding
try:
header = json.loads(base64.b64decode(token_b64.split('.')[0] + "==="))
except Exception as e:
return {"error": str(e)}

if "alg" in header:
algorithm = header["alg"]
else:
return {"error": "There is no algorithm key in the header"}

if algorithm == "HS256":
try:
decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
except Exception as e:
return {"error": str(e)}
elif algorithm == "none":
try:
decoded = jwt.decode(token, algorithms=["none"], options={"verify_signature": False})
except Exception as e:
return {"error": str(e)}
else:
return {"error": "Cannot decode token"}

if "admin" in decoded and decoded["admin"]:
return {"response": f"Welcome admin, here is your flag: {FLAG}"}
elif "username" in decoded:
return {"response": f"Welcome {decoded['username']}"}
else:
return {"error": "There is something wrong with your session, goodbye"}


@chal.route('/no-way-jose/create_session/<username>/')
def create_session(username):
encoded = jwt.encode({'username': username, 'admin': False}, SECRET_KEY, algorithm='HS256')
return {"session": encoded}

简单了解一下JWT的加密方式:

JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。

JWT的 “none” 算法在不进行任何签名验证的情况下允许解析JWT令牌,**”none” 算法表示不对JWT进行任何签名操作(签名部分去掉)**。当在JWT头部(Header)中指定算法为 “none” 时,这意味着该JWT没有进行签名,并且不提供任何完整性保障。因此,JWT使用 “none” 算法存在严重的安全风险,因为它可以被篡改和伪造。

这里我们可以使用none算法绕过验证,得到flag:

1
2
3
4
import jwt

encoded = jwt.encode({'username':'admin', 'admin': True}, None, algorithm='none')
print(encoded)

JWT Secrets

JWT中使用的最常见的签名算法是HS256和RS256。第一种是使用具有SHA256散列函数的HMAC的对称签名方案。第二种是基于RSA的非对称签名方案。互联网上的很多指南都建议使用HS256,因为它更简单。用于对令牌进行签名的密钥与用于验证令牌的密钥相同。

然而,如果签名密钥被泄露,攻击者可以签署任意令牌并伪造其他用户的会话,这可能会导致网络应用程序的完全泄露。HS256使密钥比非对称密钥对更难安全,因为密钥必须在所有验证HS256令牌的服务器上可用(除非有更好的基础设施和单独的令牌验证服务,但通常情况并非如此)。相反,使用RS256的非对称方案,签名密钥可以得到更好的保护,而验证密钥可以自由分发。

更糟糕的是,开发人员有时会使用默认的或弱的HS256密钥。以下是源代码片段,其中一个函数用于创建会话,另一个函数则用于授权会话并检查管理权限。但是有一个关于密钥的奇怪评论:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import jwt # note this is the PyJWT module, not python-jwt


SECRET_KEY = ? # TODO: PyJWT readme key, change later
FLAG = ?


@chal.route('/jwt-secrets/authorise/<token>/')
def authorise(token):
try:
decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
except Exception as e:
return {"error": str(e)}

if "admin" in decoded and decoded["admin"]:
return {"response": f"Welcome admin, here is your flag: {FLAG}"}
elif "username" in decoded:
return {"response": f"Welcome {decoded['username']}"}
else:
return {"error": "There is something wrong with your session, goodbye"}


@chal.route('/jwt-secrets/create_session/<username>/')
def create_session(username):
encoded = jwt.encode({'username': username, 'admin': False}, SECRET_KEY, algorithm='HS256')
return {"session": encoded}

这里的默认密钥在库里默认为secret,知道了密钥就可以伪造admin得到flag:

1
2
3
4
import jwt

encoded = jwt.encode({'username':'admin', 'admin': True}, "secret" , algorithm='HS256')
print(encoded)

RSA or HMAC?

还有另一个问题是允许攻击者指定自己的算法,但没有仔细验证。攻击者可以混合和匹配用于签名和验证数据的算法。当其中一个是对称算法,另一个是非对称算法时,就会产生漏洞。

RS256和HS256是两种常见的签名算法,通常用于生成和验证JWT(JSON Web Token)的签名部分。

RS256(RSA with SHA-256)是基于RSA算法和SHA-256哈希算法的签名算法。在使用RS256时,需要使用一对公私钥,其中私钥用于生成签名,公钥用于验证签名。发送方使用私钥对数据进行签名,接收方使用公钥验证签名的有效性。RS256提供了更高的安全性,适合于敏感数据的传输和验证。

HS256(HMAC SHA-256)是基于HMAC和SHA-256哈希算法的签名算法。在使用HS256时,只需要使用一个密钥,用于生成和验证签名。发送方和接收方都共享同一个密钥,发送方使用密钥对数据进行签名,接收方使用同样的密钥验证签名的有效性。HS256相对简单,适用于对数据完整性进行简单验证的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import jwt # note this is the PyJWT module, not python-jwt


# Key generated using: openssl genrsa -out rsa-or-hmac-private.pem 2048
with open('challenge_files/rsa-or-hmac-private.pem', 'rb') as f:
PRIVATE_KEY = f.read()
with open('challenge_files/rsa-or-hmac-public.pem', 'rb') as f:
PUBLIC_KEY = f.read()

FLAG = ?


@chal.route('/rsa-or-hmac/authorise/<token>/')
def authorise(token):
try:
decoded = jwt.decode(token, PUBLIC_KEY, algorithms=['HS256', 'RS256'])
except Exception as e:
return {"error": str(e)}

if "admin" in decoded and decoded["admin"]:
return {"response": f"Welcome admin, here is your flag: {FLAG}"}
elif "username" in decoded:
return {"response": f"Welcome {decoded['username']}"}
else:
return {"error": "There is something wrong with your session, goodbye"}


@chal.route('/rsa-or-hmac/create_session/<username>/')
def create_session(username):
encoded = jwt.encode({'username': username, 'admin': False}, PRIVATE_KEY, algorithm='RS256')
return {"session": encoded}


@chal.route('/rsa-or-hmac/get_pubkey/')
def get_pubkey():
return {"pubkey": PUBLIC_KEY}

这里我们不知道私钥,所以直接根据题目提供的pubkey进行HS256:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import hmac
import base64
import hashlib
import requests

url_base = 'http://web.cryptohack.org/rsa-or-hmac'
key = requests.get(url=f'{url_base}/get_pubkey').json()['pubkey'].encode()

username_with_string = '{"username":"admin","admin":True}'
encoded = base64.b64encode(username_with_string.encode("utf-8")).decode().rstrip("=")

string = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"+"."+encoded
signature = base64.urlsafe_b64encode(hmac.new(key,string.encode('utf-8'),hashlib.sha256).digest()).decode('UTF-8').rstrip("=")
print(f"{string}.{signature}")

JSON in JSON

我们已经探讨了有缺陷的验证如何破坏JWT的安全性,但有时也可能首先利用代码对意外数据进行签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import json
import jwt # note this is the PyJWT module, not python-jwt


FLAG = ?
SECRET_KEY = ?


@chal.route('/json-in-json/authorise/<token>/')
def authorise(token):
try:
decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
except Exception as e:
return {"error": str(e)}

if "admin" in decoded and decoded["admin"] == "True":
return {"response": f"Welcome admin, here is your flag: {FLAG}"}
elif "username" in decoded:
return {"response": f"Welcome {decoded['username']}"}
else:
return {"error": "There is something wrong with your session, goodbye"}


@chal.route('/json-in-json/create_session/<username>/')
def create_session(username):
body = '{' \
+ '"admin": "' + "False" \
+ '", "username": "' + str(username) \
+ '"}'
encoded = jwt.encode(json.loads(body), SECRET_KEY, algorithm='HS256')
return {"session": encoded}

注意到给我们提供的session形式为{"admin": "False", "username" : str(username)}

这里牵扯到字典的工作方式,如果字典中含有两个相同值的键,那么分配的值为靠后(最新的)值,例如:

1
2
3
a = {"admin": False, "admin": True}
print(a)
# {'admin': True}

所以我们可以在加入的str(username)伪造一个新的admin用来重写赋值为True:

{"admin": "False", "username" : "admin", "admin": "True"}

所以我们需要传入的username为admin", "admin": "True,这样我们的权限就为True了。