企业内部产品应用使用JWT作为用户的身份认证方式,在对应用评估时发现了新的关于JWT的会话安全带来的安全问题,后期再整理时又加入了之前遗留的部分JWT安全问题,至此汇总成一篇完整的JWT文章
JWT(JSON Web Token)是一种用于身份认证和授权的开放标准,它通过在网络应用间传递被加密的JSON数据来安全地传输信息使得身份验证和授权变得更加简单和安全,JWT对于渗透测试人员而言可能是一种非常吸引人的攻击途径,因为它们不仅是让你获得无限访问权限的关键而且还被视为隐藏了通往以下特权的途径,例如:特权升级、信息泄露、SQLi、XSS、SSRF、RCE、LFI等
JWS:Signed JWT,签名过的JWT
JWK:JWT的密钥,也就是我们常说的SECRET
JWE:Encrypted JWT部分payload经过加密的JWT
JKU:JKU(JSON Web Key Set URL)是JWT Header中的一个字段,字段内容是一个URI,该URI用于指定用于验证令牌秘钥的服务器,该服务器用于回复JWK
X5U:X5U是JWT Header中的一个字段,指向一组X509公共证书的URL,与JKU功能类似
X.509标准:X.509标准是密码学里公钥证书的格式标准,包括TLS/SSL(WWW万维网安全浏览的基石)在内的众多Internet协议都应用了X.509证书)
JWT(JSON Web Token)的结构由三部分组成,分别是Header、Payload和Signature,下面是每一部分的详细介绍和示例:
Header包含了JWT使用的算法和类型等元数据信息,通常使用JSON对象表示并使用Base64编码,Header中包含两个字段:alg和typ
alg(algorithm):指定了使用的加密算法,常见的有HMAC、RSA和ECDSA等算法
typ(type):指定了JWT的类型,通常为JWT
下面是一个示例Header:
{ "alg": "HS256", "typ": "JWT" }
其中alg指定了使用HMAC-SHA256算法进行签名,typ指定了JWT的类型为JWT
Payload包含了JWT的主要信息,通常使用JSON对象表示并使用Base64编码,Payload中包含三个类型的字段:注册声明、公共声明和私有声明
公共声明(Public Claims):是自定义的字段,用于传递非敏感信息,例如:用户ID、角色等
私有声明(Private Claims):是自定义的字段,用于传递敏感信息,例如密码、信用卡号等
注册声明(Registered Claims):预定义的标准字段,包含了一些JWT的元数据信息,例如:发行者、过期时间等
下面是一个示例Payload:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
其中sub表示主题,name表示名称,iat表示JWT的签发时间
Signature是使用指定算法对Header和Payload进行签名生成的,用于验证JWT的完整性和真实性,Signature的生成方式通常是将Header和Payload连接起来然后使用指定算法对其进行签名,最终将签名结果与Header和Payload一起组成JWT,Signature的生成和验证需要使用相同的密钥,下面是一个示例Signature
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
其中HMACSHA256是使用HMAC SHA256算法进行签名,header和payload是经过Base64编码的Header和Payload,secret是用于签名和验证的密钥,最终将Header、Payload和Signature连接起来用句点(.)分隔就形成了一个完整的JWT,下面是一个示例JWT,其中第一部分是Header,第二部分是Payload,第三部分是Signature,注意JWT 中的每一部分都是经过Base64编码的,但并不是加密的,因此JWT中的信息是可以被解密的
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
下面是一个JWT在线构造和解构的平台:
https://jwt.io/
JWT的工作流程如下:
用户在客户端登录并将登录信息发送给服务器
服务器使用私钥对用户信息进行加密生成JWT并将其发送给客户端
客户端将JWT存储在本地,每次向服务器发送请求时携带JWT进行认证
服务器使用公钥对JWT进行解密和验证,根据JWT中的信息进行身份验证和授权
服务器处理请求并返回响应,客户端根据响应进行相应的操作
Step 1:用户携带JWS(带有签名的JWT)访问应用
Step 2:应用程序解码JWS得到JKU字段
Step 3:应用根据JKU访问返回JWK的服务器
Step 4:应用程序得到JWK
Step 5:使用JWK验证用户JWS
Step 6:验证通过则正常响应
JWT(JSON Web Token)的签名验证过程主要包括以下几个步骤:
分离解构:JWT的Header和Payload是通过句点(.)分隔的,因此需要将JWT按照句点分隔符进行分离
验证签名:通过使用指定算法对Header和Payload进行签名生成签名结果,然后将签名结果与JWT中的签名部分进行比较,如果两者相同则说明JWT的签名是有效的,否则说明JWT的签名是无效的
验证信息:如果JWT的签名是有效的则需要对Payload中的信息进行验证,例如:可以验证JWT中的过期时间、发行者等信息是否正确,如果验证失败则说明JWT是无效的
下面是一个使用JAVA进行JWT签名验证的示例代码:
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JWTExample { private static final String SECRET_KEY = "my_secret_key"; public static void main(String[] args) { // 构建 JWT String jwtToken = Jwts.builder() .setSubject("1234567890") .claim("name", "John Doe") .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); // 验证 JWT try { // 分离 Header, Payload 和 Signature String[] jwtParts = jwtToken.split("\\."); String header = jwtParts[0]; String payload = jwtParts[1]; String signature = jwtParts[2]; // 验证签名 String expectedSignature = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(jwtToken) .getSignature(); if (!signature.equals(expectedSignature)) { throw new RuntimeException("Invalid JWT signature"); } // 验证 Payload 中的信息 Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(jwtToken) .getBody(); System.out.println("Valid JWT"); } catch (Exception e) { System.out.println("Invalid JWT: " + e.getMessage()); } } }
在上面的示例代码中使用jwt库进行JWT的签名和验证,首先构建了一个JWT,然后将其分离为Header、Payload和Signature三部分,使用parseClaimsJws函数对JWT进行解析和验证,从而获取其中的Payload中的信息并进行验证,最后如果解析和验证成功,则说明JWT是有效的,否则说明JWT是无效的,在实际应用中应该将SECRET_KEY替换为应用程序的密钥
JWT库会通常提供一种验证令牌的方法和一种解码令牌的方法,比如:Node.js库jsonwebtoken有verify()和decode(),有时开发人员会混淆这两种方法,只将传入的令牌传递给decode()方法,这意味着应用程序根本不验证签名,而我们下面的使用则是一个基于JWT的机制来处理会话,由于实现缺陷服务器不会验证它收到的任何JWT的签名,如果要解答实验问题,您需要修改会话令牌以访问位于/admin的管理面板然后删除用户carlos,您可以使用以下凭据登录自己的帐户:wiener:peter
靶场地址:https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-unverified-signature
演示步骤:
Step 1:点击上方的"Access the Lab"访问靶场并登录账户
Step 2:进入到Web界面并登录靶场账户
wiener:peter
登录之后会看到如下一个更新邮箱的界面
Step 3:此时在我们的burpsuite中我们可以看到如下的会话信息
此时查询当前用户可以看到会显示当前用户为wiener:
截取上面中间一部分base64编码的部分更改上面的sub为"administrator"
eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY4Nzc5MDA4M30
构造一个sub为"administrator"的载荷并将其进行base64编码处理:
eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2ODc3OTAwODN9
替换之后重新发送请求:
按照题目要求访问/admin路径,发现两个删除用户的调用接口:
请求敏感链接——删除用户carlos
GET /admin/delete?username=carlos HTTP/1.1
完成靶场的解答:
在JWT的Header中alg的值用于告诉服务器使用哪种算法对令牌进行签名,从而告诉服务器在验证签名时需要使用哪种算法,目前可以选择HS256,即HMAC和SHA256,JWT同时也支持将算法设定为"None",如果"alg"字段设为"None",则标识不签名,这样一来任何token都是有效的,设定该功能的最初目的是为了方便调试,但是若不在生产环境中关闭该功能,攻击者可以通过将alg字段设置为"None"来伪造他们想要的任何token,接着便可以使用伪造的token冒充任意用户登陆网站
{ "alg": "none", "typ": "JWT" }
实验靶场:https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-flawed-signature-verification
实验流程:
Step 1:点击上方的"Access the lab"访问靶场环境
https://0a9c00a8030ba77784d7b92d00cc0086.web-security-academy.net/
Step 2:使用账户密码进行登录
wiener:peter
Step 3:登录之后可以看到如下界面
Step 4:捕获到的数据报信息如下所示
截取JWT的第二部分对其进行base64解码:
eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY4Nzc5MzQ5M30
将上述的sub字段值更改为"administrator"
eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2ODc3OTM0OTN9
Step 4:在使用wiener用户的凭据访问/admin是会提示401 Unauthorized
Step 5:将第一步分的alg参数改为none
eyJraWQiOiIyNmNlNGNmMi0wYjFhLTQzZTUtOWYzNy1kOTA2ZjkxZmY2MzkiLCJhbGciOiJSUzI1NiJ9
更改之后的header部分:
eyJraWQiOiIyNmNlNGNmMi0wYjFhLTQzZTUtOWYzNy1kOTA2ZjkxZmY2MzkiLCJhbGciOiJub25lIn0=
替换JWT Token中的第二部分为之前我们构造的信息,同时移除签名部分,再次请求数据获取到敏感数据链接
调用敏感链接移除用户信息,完成解题操作:
在JT中密钥用于生成和验证签名,因此密钥的安全性对JWT的安全性至关重要,一般来说JWT有以下两种类型的密钥:
对称密钥:对称密钥是一种使用相同的密钥进行加密和解密的加密算法,在JWT中使用对称密钥来生成和验证签名,因此密钥必须保密,只有发送方和接收方知道,由于对称密钥的安全性取决于密钥的保密性,因此需要采取一些措施来保护它
非对称密钥:非对称密钥使用公钥和私钥来加密和解密数据,在JWT中使用私钥生成签名,而使用公钥验证签名,由于公钥可以公开,因此非对称密钥通常用于验证方的身份
下面是一个使用JWT和对称密钥的JAVA示例代码:
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JWTExample { private static final String SECRET_KEY = "mysecretkey"; // 设置密钥 public static void main(String[] args) { String token = createJWT("123456"); // 生成JWT System.out.println(token); String result = parseJWT(token); // 验证JWT System.out.println(result); } public static String createJWT(String id) { // 设置JWT过期时间为1小时 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); long expMillis = nowMillis + 3600000; // 1小时 Date exp = new Date(expMillis); // 生成JWT String token = Jwts.builder() .setId(id) .setIssuer("issuer") .setSubject("subject") .setIssuedAt(now) .setExpiration(exp) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); return token; } public static String parseJWT(String token) { // 验证JWT是否合法 String result = ""; try { result = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getId(); } catch (Exception e) { e.printStackTrace(); } return result; } }
下面是一个使用JWT和非对称密钥的Java示例代码,代码中使用了RSA算法生成非对称密钥对:
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Date; public class JWTExample { private static final String ISSUER = "example.com"; private static final String SUBJECT = "user@example.com"; public static void main(String[] args) throws Exception { KeyPair keyPair = generateKeyPair(); String token = createJWT(ISSUER, SUBJECT, keyPair.getPrivate()); System.out.println(token); Claims claims = parseJWT(token, keyPair.getPublic()); System.out.println(claims.getIssuer()); System.out.println(claims.getSubject()); } public static String createJWT(String issuer, String subject, PrivateKey privateKey) { Date now = new Date(); Date expiration = new Date(now.getTime() + 3600 * 1000); // 1 hour return Jwts.builder() .setIssuer(issuer) .setSubject(subject) .setIssuedAt(now) .setExpiration(expiration) .signWith(privateKey, SignatureAlgorithm.RS256) .compact(); } public static Claims parseJWT(String token, PublicKey publicKey) { return Jwts.parserBuilder() .setSigningKey(publicKey) .build() .parseClaimsJws(token) .getBody(); } public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(2048); return generator.generateKeyPair(); } }
在这个示例中我们使用了Java中的KeyPairGenerator类来生成一个2048位的RSA密钥对,然后使用私钥生成JWT,使用公钥验证JWT,在创建JWT时我们设置了JWT的颁发者、主题、签发时间和过期时间并使用signWith()方法和SignatureAlgorithm.RS256算法使用私钥进行签名,在验证JWT时我们使用公钥来解析JWT并获取声明的内容,在实际的研发编码中我们一方面要妥善保管密钥,另一方面需要使用较为复杂难以被猜解的密钥作为密钥首选,例如:随机字母+数字的32位长度组合
在实现JWT应用程序时,开发人员有时会犯一些错误,比如:忘记更改默认密码或占位符密码,他们甚至可能复制并粘贴他们在网上找到的代码片段然后忘记更改作为示例提供的硬编码秘密,在这种情况下攻击者使用众所周知的秘密的单词列表来暴力破解服务器的秘密是很容易的,下面是一个公开已知密钥列表:
https://github.com/wallarm/jwt-secrets/blob/master/jwt.secrets.list
在这里我们也建议使用hashcat来强力破解密钥,您可以手动安装hashcat,也可以在Kali Linux上使用预先安装好的hashcat,您只需要一个来自目标服务器的有效的、签名的JWT和一个众所周知的秘密的单词表然后就可以运行以下命令,将JWT和单词列表作为参数传入:
hashcat -a 0 -m 16500 <jwt> <wordlist>
Hashcat会使用单词列表中的每个密钥对来自JWT的报头和有效载荷进行签名,然后将结果签名与来自服务器的原始签名进行比较,如果有任何签名匹配,hashcat将按照以下格式输出识别出的秘密以及其他各种详细信息,由于hashcat在本地机器上运行不依赖于向服务器发送请求,所以这个过程非常快,即使使用一个巨大的单词表一旦您确定了密钥,您就可以使用它为任何JWT报头和有效载荷生成有效的签名
<jwt>:<identified-secret>
靶场地址:https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-weak-signing-key
实验步骤:
Step 1:点击上述"Access the lab"进入到靶场环境
Step 2:使用以下账户进行登录操作
wiener:peter
Step 3:捕获到如下有效的JWT凭据信息
eyJraWQiOiI4M2RhOGNjMi1hZmZiLTRmZGMtYWMwYS1iMWNmMTBkNjkyZGYiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY4Nzc5NjQwMn0.IhZV-7RHTpEcQvkcZOA3knCYmQD0YUg-NFMj9fWSFjw
Step 5:使用字典进行暴力猜解操作
方式一:HashCat
项目地址:https://github.com/hashcat/hashcat
项目使用:
#命令格式: hashcat -a 0 -m 16500 <jwt> <wordlist> #执行示例: hashcat -m 16500 jwt.txt -a 0 secrets.txt --force
方式二:jwt_tool
项目地址:https://github.com/ticarpi/jwt_tool
项目介绍:此项目主要用于JWT安全脆弱性评估,目前支持如下几种安全评估测试
(CVE-2015-2951) The alg=none signature-bypass vulnerability
(CVE-2016-10555) The RS/HS256 public key mismatch vulnerability
(CVE-2018-0114) Key injection vulnerability
(CVE-2019-20933/CVE-2020-28637) Blank password vulnerability
(CVE-2020-28042) Null signature vulnerability
Step 1:克隆项目到本地
https://github.com/ticarpi/jwt_tool
Step 2:安装依赖库
pip3 install pycryptodomex
Step 3:运行jwt_tool并查看用法信息
python3 jwt_tool.py -h