前言
在2022祥云杯時遇到有關JWT的題,當時沒有思路,對JWT進行學習后來對此進行簡單總結,希望能對正在學習JWT的師傅們有所幫助,
JWT
JWT,即JSON WEB TOKEN,它是一種用于通信雙方之間傳遞安全資訊的簡潔的、URL安全的表述性宣告規范,是一種標準化的格式,用于在系統之間發送經過加密簽名的JSON資料,理論上可以包含任何型別的資料,但最常用于發送關于用戶的資訊(“宣告”),以進行身份認證、會話處理和訪問控制,
簡單了解了它的定義后,我們接下來來看一下JWT的組成部分它分為三個部分,如下所示
1、Headers:頭部
2、Payload:有效載荷
3、Signature:簽名
這三個部分以.符號來連接,所以JWT的格式通常是xxx.yyy.zzz這種樣子
Headers
Headers通常由兩部分組成,令牌的型別和簽名演算法,常見的演算法有很多種,例如 HMAC SHA256或 RSA,但它也還有一個kid引數,這是一個可選引數,全稱是key ID,它用于指定加密演算法的密鑰,
示例如下
ewogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9
這就是一個Headers,當我們對它進行Base64解碼就可以看到它的具體內容,具體如下
{
"alg": "HS256",
"typ": "JWT"
}
alg指的就是演算法,這里的演算法就是HS256,typ指的是令牌型別,這里需要說明一點,就是明文在加密時其實采用的是Base64URL加密,這種加密方式并非Base64encode+URLencode,而是對一些特殊字符進行了替換,具體說明如下
JWT 作為一個令牌(token),有些場合可能會放到 URL(比如 api.example.com/?token=xxx),Base64有三個字符+、/和=,在 URL 里面有特殊含義,所以要被替換掉:=被省略、+替換成-,/替換成_ ,這就是 Base64URL 演算法,
Payload
有效載荷就是存放有效資訊的地方,其中包含宣告,宣告包含三個部分 1、已注冊宣告這個部分的話就是已經預先定義過的宣告,常見的宣告主要有以下幾種
iss: jwt簽發者
sub: jwt所面向的用戶
aud: 接收jwt的一方
exp: jwt的過期時間,這個過期時間必須要大于簽發時間
nbf: 定義在什么時間之前,該jwt都是不可用的.
iat: jwt的簽發時間
jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊,
2、公共的宣告這些可以由使用 JWT 的人隨意定義,一般用于添加用戶的相關資訊或其他業務需要的必要資訊,但不建議添加敏感資訊,因為該部分在客戶端可進行解碼.3、私有的宣告這些是為在同意使用它們的各方之間共享資訊而創建的自定義宣告,私有宣告是提供者和消費者所共同定義的宣告,一般不建議存放敏感資訊,
示例如下
ewoJInN1YiI6ICJhZG1pbiIsCiAgICAidXNlcl9yb2xlIiA6ICJhZG1pbiIsCiAgICAiaXNzIjogImFkbWluIiwKICAgICJpYXQiOiAxNTczNDQwNTgyLAogICAgImV4cCI6IDE1NzM5NDAyNjcsIAogICAgIm5iZiI6IDE1NzM0NDA1ODIsIAogICAgImp0aSI6ICJkZmY0MjE0MTIxZTgzMDU3NjU1ZTEwYmQ5NzUxZDY1NyIgICAKfQ
進行base64URL解碼,結果如下
{
"sub": "admin", //jwt所面向的用戶
"user_role" : "admin", //當前登錄用戶
"iss": "admin", //該JWT的簽發者,有些是URL
"iat": 1573440582, //簽發時間
"exp": 1573940267, //過期時間
"nbf": 1573440582, //該時間之前不接收處理該Token
"jti": "dff4214121e83057655e10bd9751d657" //Token唯一標識
}
Signature
由于頭部和有效載荷以明文形式存盤,因此,需要使用簽名來防止資料被篡改,所以這部分是一個簽證資訊,這個簽證資訊由三部分組成
1、header (base64URL編碼)
2、payload (base64URL編碼)
3、secret(密鑰)
它的計算方式如下
Signature=HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
//假設這里是HS256演算法,如果是其他演算法的話開頭設定為其他演算法即可
現在了解了JWT的大致作用和其組成,接下來來學習一下JWT攻擊,
JWT 攻擊
JWT攻擊有多種情況,現在來對其進行逐一講解,
敏感資訊泄露
JWT保證的是資料傳輸程序中的完整性而不是機密性,
因為JWT的payload部分是使用Base64url編碼的,所以它其實是相當于明文傳輸的,當payload中攜帶了敏感資訊時,我們對payload部分進行Base64url解碼,就可以讀取到payload中攜帶的敏感資訊,
靶場演示
題目鏈接https://www.ctfhub.com/#/skilltree題目描述如下
JWT 的頭部和有效載荷這兩部分的資料是以明文形式傳輸的,如果其中包含了敏感資訊的話,就會發生敏感資訊泄露,試著找出FLAG,格式為 flag{}
進入環境后發現一個登錄框

隨便輸入賬號密碼,登錄后發現界面如下

查看此時的JWT

想到題目中說頭部和載荷可能會有敏感泄露,將值取出分別進行Base64URL解碼

兩處拼接一下,得到ctfhub{bb89d985db8cea6a2f2d34cb}
【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備注 “博客園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
演算法修改攻擊
首先來簡述一下JWT中兩個常用的加密演算法
HMAC(HS256):是一種對稱加密演算法,使用秘密密鑰對每條訊息進行簽名和驗證RSA(RS256):是一種非對稱加密演算法,使用私鑰加密明文,公鑰解密密文,
從上面不難看出,HS256自始至終只有一個密鑰,而RS256是有兩個密鑰的,在通常情況下,HS256的密鑰我們是不能取到的,RS256的密鑰也是很難獲得的,RS256的的公鑰相對較容易獲取,但無論是HS256加密還是RS256加密,都是無法實作偽造JWT的,但當我們修改RSA256演算法為HS256演算法時,后端代碼會使用公鑰作為密鑰,然后用HS256演算法驗證簽名,如果我們此時有公鑰,那么此時我們就可與實作JWT的偽造,
靶場演示
題目鏈接https://www.ctfhub.com/#/skilltree
題目描述
有些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 