0. Basic
1) JWT란?
JWT란, JSON Web Token의 약자로 JSON 문서를 Base64로 인코딩 후 서명하는 방식을 말한다.
대부분 웹사이트 사용자 인증에 사용되고, 쿠키에 주로 포함되어 쓰인다.
JWT는 사용자의 정보나 세션 만료 시간 등을 JSON 문서 형식(Dictionary)으로 나타낸다.
2) JWT 구성
JWT의 형태는 다음과 같다.
세 부분으로 나뉘어져 있고, 각각은 .(dot)
으로 구분된다.
Frame) Header . Payload . Signature
- Header
- alg: 어떤 알고리즘을 이용하여 서명(Sign)을 할 것인가?
- typ: 미디어타입. 옵션값이며 JWT일 경우 "JWT"
Base64({ "alg": "HS256", "typ": "JWT" })
- Payload(Claims)
- Registered Claims
e.g. iss(issuer), exp(expiration time), sub(subject), iat(issued at), ... - Public/Private Claims
⇒ 사용자 정의 Claims
- Registered Claims
Base64({ "sub": "1234567890", "name": "guest", "iat": 1516239022 })
- Signature
- HS256 (HMAC-SHA256)
⇒ Secret Key로 암호화 & 복호화 (대칭키) - RS256 (RSA-SHA256)
⇒ Private Key로 암호화, Public Key로 복호화 (비대칭키) - ES256 (ECDSA-SHA256)
- None
- ...
- HS256 (HMAC-SHA256)
Base64(HMACSHA256(header + "." + payload, secret))
3) JWT 인코딩에 이용되는 Base64
각각의 부분들은 URL-Safe Base64 인코딩 방식을 사용한다.
💡 URL-Safe Base64: Padding(=)이 존재하지 않고 + ⇒ - , / ⇒ _ 으로 인코딩하는 방식
실제 JWT 변환 시 Base64 Encoding 함수는 아래와 같다.
static string base64urlencode(byte [] arg){
string s = Convert.ToBase64String(arg); // Regular base64 encoder
s = s.Split('=')[0]; // Remove any trailing '='s
s = s.Replace('+','-'); // 62nd char of encoding
s = s.Replace('/','_'); // 63rd char of encoding
return s;
}
PyJWT 최신 버전은 아래에서 설명할 공격 기법에 대해 패치가 적용되어있다. 따라서 테스트 또는 실습 시, PyJWT==0.4.3 버전 설치를 권장한다.
1. JWT None Algorithm Attack
1) 개념 설명
None Algorithm Attack(None type injection)은 Header의 Algorithm을 None으로 변조하여 인증을 우회하는 공격이다.
2) 공격 원리
JWT 라이브러리 내에서 RS256, HS256, ES256 등의 많은 알고리즘 타입을 제공한다.
이 중, 이미 검증된 토큰을 디코딩하기 위한 알고리즘으로 "none" 알고리즘이 존재한다.
# PyJWT-0.4.3 > jwt > algorithm.py
def _register_default_algorithms():
"""
Registers the algorithms that are implemented by the library.
"""
register_algorithm('none', NoneAlgorithm()) # none algorithm
register_algorithm('HS256', HMACAlgorithm(hashlib.sha256))
register_algorithm('HS384', HMACAlgorithm(hashlib.sha384))
register_algorithm('HS512', HMACAlgorithm(hashlib.sha512))
if has_crypto:
register_algorithm('RS256', RSAAlgorithm(hashes.SHA256()))
register_algorithm('RS384', RSAAlgorithm(hashes.SHA384()))
**•••**
class NoneAlgorithm(Algorithm):
"""
Placeholder for use when no signing or verification
operations are required.
"""
def prepare_key(self, key):
return None
def sign(self, msg, key):
return b''
def verify(self, msg, key, sig):
return False
JWT는 알고리즘이 "none"일 경우 검증 절차를 더이상 진행하지 않고 끝내도록 처리가 되어있는데, 이 때 JWT Signature 인증 우회가 가능한 취약점이 발생할 수 있다.
// Header and Payload
{
"typ": "JWT",
"alg": "none" // 환경에 따라 none, None, NonE, ... 등의 형태가 가능한 경우가 존재한다.
// ( "none" 문자열 필터링 우회 가능 )
}.
{
"username": "guest",
"level": 999999
}
// Base64 encoded and replaced
ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIm5vbmUiCn0.ewogICJ1c2VybmFtZSI6ICJndWVzdCIsCiAgImxldmVsIjogOTk5OTk5Cn0.
따라서 검증 가능한 토큰을 SIgn(생성) 할 수 있다.
3) 관련 문제 풀이
- http://web.cryptohack.org/no-way-jose/
- JLT_None.zip
2. JWT RS256 to HS256 key Confusion
1) 개념 설명
- HMAC: Signing(서명), Verifying(검증) key가 동일. (대칭키)
- RSA: Private Key, Public Key가 각각 서명, 검증용 key임. (비대칭키)
JWT를 사용하는 웹서버에서 토큰 검증 절차에서 HS, RS 알고리즘을 모두 지원하는 경우, 공격자가 Public Key를 이용하여 Private Key 없이 토큰 서명을 할 수 있는 공격이다.
2) 공격 원리
JWT 토큰 검증 절차에서 HMAC과 RSA 암호화 알고리즘을 동시에 지원하고 클라이언트에 Public Key가 전달되는 경우, RSA public key로 HMAC 서명을 생성할 수 있다.
만약 아래와 같은 코드로 decode를 시도한다고 가정해보자.
decoded = jwt.decode(token, PUBLIC_KEY)
알고리즘을 특정하지 않기 때문에 RS256, HS256 등의 다양한 알고리즘을 사용해도 디코딩이 가능하다.
decode() 함수 안에서 verify() 함수를 호출한다.
decode() 함수가 호출될 때 코드 구성에 따라 다른 Key값이 전달되는데, 개발자가 RS256을 기본으로 개발을 진행했다면 위 코드와 같이 PUBLIC_KEY가 key값으로 전달될 것이다.
이 때 타겟 token이 사용하는 알고리즘이 HMAC인 경우, 전달된 key값(Public_key)을 secret key로 인식한다.
verify(string token, string verificationKey)
따라서 RS256의 PUBLIC_KEY로, HS256 알고리즘을 이용하는 JWT를 서명(생성)할 수 있다.
3) 관련 문제 풀이
3. JWT HS256 Bruteforce Attack
1) 개념 설명
- 무작위 대입 공격(Bruteforce Attack): 암호를 풀기 위해 가능한 모든 값을 대입하는 공격 방식
HMAC의 secret key를 복잡도가 낮은 값으로 사용할 경우 공격자가 secret key 값을 유추할 수 있는 공격 방식이다.
2) 공격 원리
개발자가 secret key를 유추할 수 있는 문자열로 설정하거나 secret key를 generate 하는 코드나 방법을 유출한 경우, 공격자는 무작위 대입 공격을 진행하여 secret key를 알아낼 수 있다.
예를 들면 아래와 같은 파이썬 코드로 Brute Forcing을 진행할 수 있다.
import jwt
from itertools import permutations # permutations: 순열 구해주는 함수
import string
result = 'Header.Payload.Signature'; # result: JWT 토큰 값
for secret in map(lambda x: "".join(x), permutations(string.printable[10:36], 5)): # key 문자열 length가 5인 경우
try:
print(jwt.decode(result, secret, algorithms="HS256"))
print(secret)
break
except:
pass
# Output(secret key): 9ucc1
3) 관련 문제 풀이
- Json_Likes_TikToken