Web

JWT Token Crack

pyozzi 2020. 12. 9. 03:23

JWT 토큰은 Json Web Token으로 주로 인증(세션) 용도로 많이 사용되며, 신뢰적인 데이터 전달에도 사용할 수 있다.

이번 글에서는 JWT에 대해 간단하게 다루고, HS256을 사용하는 토큰 크랙킹에 대해 정리하려고 한다.


Json Web Token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InB5MHp6MSJ9.uplGmqf8uWWQiqu8lDGomV1pzjkDi10uULUdcGabJrw

 

JWT토큰은 .(온점)을 기준으로 데이터가 구분되며, 위 값이 실제 JWT토큰의 모습이다.

헤더에는 JWT토큰 생성 알고리즘 / 페이로드에는 사용자가 정의한 임의의 데이터 / 시그니쳐는 토큰의 무결성을 위한 서명값이 포함된다.

 

각 데이터는 Base64로 인코딩 되어 있으며, jwt.io에서 아래와 같이 간편하게 확인할 수 있다.

 

jwt.io

 

 


JWT Algorithm

 

JWT토큰 생성 알고리즘은 크게 대칭키/비대칭키 2가지로 구분할 수 있다.

    - 대칭키 사용: HS256(HMAC + SHA256)

    - 비대칭키 사용:RS256(RSA + SHA256) , ES256(ECDH + SHA256)

 

대칭키   : 암/복호화를 1개의 Key로 진행한다.
비대칭키: 암/복호화를 2개의 Key로 진행하며, A키로 암호화한 값은 B키로만 복호화 가능하다. (반대도 마찬가지)

 

실제 서비스에서는 구현하기 간단한 HS256을 많이 사용한다.

HS256은 HMAC+SHA256이 결합된 알고리즘으로, 사용자가 정의한 SecretKey값으로 해싱하여 서명 데이터를 생성한다.

 

 


JWT HS256 Crack

HS256은 대칭키를 사용하기 때문에 다른 알고리즘에 비해 상대적으로 취약한 알고리즘이라고 볼 수 있는데, 이 부분을 자세히 알아보자

 

1. HS256은 SecretKey길이에 대한 강제성이 없다.

- RFC 7518에서는 32Byte의 길이로 설정하도록 권장하고 있다.

 

2. 브루트 포싱 공격에 용이한 구조를 갖고 있다.

- 대칭키 구조를 갖고 있기 때문에 서버 요청없이 공격자 로컬에서 SecretKey 브루트포싱이 가능하다.

비대칭키 구조에서는 검증을 위해 서버 요청이 이루어져야 하기 때문에, 브루트포싱 공격에 대한 모니터링 등과 같이 선제 대응이 가능하다.

 

물론 SecretKey를 대소문자+숫자+특수문자 조합으로 32Byte 이상의 길이로 설정한다면, 불가능할 정도의 비현실적인 시간이 소요된다.

 

테스트로 "testKey"(7자)로 생성한 토큰을 브루트포싱 해봤는데, 일주일 넘게 크랙되지 않았다. 컴퓨터 사양도 하이엔드 수준이였는데 ...

 

import jwt

jwt_secretKey = "test"
JWT_TOKEN = jwt.encode({'PAYLOAD': 'py0zz1'}, jwt_secretKey)
print(JWT_TOKEN)

JWT_TOKEN_dec = jwt.decode("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQQVlMT0FEIjoicHkwenoxIn0.cAcy0Zu5d9T0_pmBLwr7aK9mn7r5jIlr5KSpDRCmf-0",jwt_secretKey)
print(JWT_TOKEN_dec)

#### OUTPUT ####
# JWT Encode
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQQVlMT0FEIjoicHkwenoxIn0.cAcy0Zu5d9T0_pmBLwr7aK9mn7r5jIlr5KSpDRCmf-0'

# JWT Decode
{'data': 'PAYLOAD'}

Python jwt모듈로 HS256 사용하여, 'test'로 서명한 JWT토큰을 생성한 모습이다.

이렇게 생성한 토큰을 jwt-cracker 라는 툴로 한번 크랙해보도록 하자

 

Ref. https://www.hahwul.com/2019/10/11/jwt-cracker-secret-key-crack/

 

 

pyozzi01-1:VulnScanner pyozzi$ jwt-cracker eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQQVlMT0FEIjoicHkwenoxIn0.cAcy0Zu5d9T0_pmBLwr7aK9mn7r5jIlr5KSpDRCmf-0
Attempts: 100000
Attempts: 200000
Attempts: 300000
Attempts: 400000
Attempts: 500000
Attempts: 600000
Attempts: 700000
Attempts: 800000
Attempts: 900000
Attempts: 1000000
Attempts: 1100000
Attempts: 1200000
Attempts: 1300000
Attempts: 1400000
Attempts: 1500000
Attempts: 1600000
Attempts: 1700000
Attempts: 1800000
Attempts: 1900000
Attempts: 2000000
Attempts: 2100000
Attempts: 2200000
Attempts: 2300000
Attempts: 2400000
Attempts: 2500000
Attempts: 2600000
Attempts: 2700000
Attempts: 2800000
Attempts: 2900000
Attempts: 3000000
Attempts: 3100000
Attempts: 3200000
Attempts: 3300000
Attempts: 3400000
Attempts: 3500000
Attempts: 3600000
Attempts: 3700000
Attempts: 3800000
Attempts: 3900000
Attempts: 4000000
Attempts: 4100000
Attempts: 4200000
Attempts: 4300000
Attempts: 4400000
Attempts: 4500000
Attempts: 4600000
Attempts: 4700000
Attempts: 4800000
SECRET FOUND: test
Time taken (sec): 119.099
Attempts: 4839926

컴퓨터 사양에 따라 조금 차이가 날 수 있지만, 'test'로 서명된 JWT토큰을 크랙하는데 2분정도 소요되었다.

이렇게 JWT토큰이 크랙되면, 임의의 JWT토큰을 생성하여 사용자 세션 하이재킹 또는 임의의 신뢰데이터를 생성하여 사용할 수 있게 된다.

 


Conclusion

이러한 크래킹을 방지하기 위한 근본적인 대처는 SecretKey를 32자 정도의 긴 길이와 복잡한 조합으로 값을 설정하는 것이다.

"testKey"와 같이 대문자 하나만 섞어도 크래킹하는데 소요되는 시간이 큰 폭으로 늘어나기 때문이다.

 

또한, SecretKey가 유출되면 그 파급력은 굉장히 크기 때문에 소스코드에 하드코딩하기 보다는 환경변수로 관리한다던가, Cloud-Config와 같은 시스템에서 관리하는 것을 권장한다.

 

 

mail.ru JWT Crack (secret)

최근에 실제로 JWT Secret Key가 브루트포싱으로 크랙되어 바운티가 지급된 사례도 있으니, 바운티할 때 백그라운드에 돌려놓는 것도 괜찮을 것 같다.

다시 생각해보니 무차별대입으로 브루트포싱 했다기 보다는 딕셔너리 방식으로 찾았을 가능성이 커보인다.

실제로 바운티에서 접근할 때도 무차별대입 보다는 딕셔너리 형태로 접근하는게 현실적으로 좋은 방법일 것 같다.