인증 메커리즘 취약점 - JWT

인증 메커리즘 취약점 - JWT

in

현재 작성 중인 게시물입니다.

현대 웹 애플리케이션에서 JSON Web Token (JWT)은 사용자 인증과 권한 부여를 처리하는 데 널리 사용되는 기술입니다. JWT는 무상태 세션 관리와 효율적인 정보 교환을 가능하게 하여 많은 개발자와 기업에게 매력적인 선택이 되었습니다. 그러나, JWT도 완벽하지 않으며 잘못된 구현이나 관리 소홀로 인해 심각한 보안 취약점에 노출될 수 있습니다.

JWT의 주요 장점 중 하나는 서버 상태를 유지하지 않으면서도 클라이언트와 서버 간의 신뢰할 수 있는 정보 교환을 가능하게 한다는 것입니다. 하지만 이러한 무상태의 특성은 적절한 보안 조치가 없을 경우 악용될 수 있습니다.

JSON Web Token (JWT)

JWT는 JSON Web Signature (JWS) 또는 JSON Web Encryption (JWE)를 사용하여 JWT 내에 포함된 데이터를 보호할 수 있지만, 실제로는 JWS가 웹 애플리케이션에서 훨씬 더 자주 사용됩니다.

JWT는 두 개의 추가 표준으로 구성됩니다. 이는 JSON Web Key (JWK)와 JSON Web Algorithm (JWA)입니다. JWK는 암호화 키를 위한 JSON 데이터 구조를 정의하고, JWA는 JWT를 위한 암호화 알고리즘을 정의합니다.

JWT는 .으로 구분된 Header, Payload, Signature로 구성됩니다.

JWT의 헤더는 토큰의 타입과 사용된 서명 또는 암호화 알고리즘에 대한 정보를 포함합니다.

{
  "alg": "HS256",
  "typ": "JWT"
}

헤더는 일반적으로 alg 필드와 typ 필드로 구성된 JSON 객체입니다.

  • alg: 토큰 서명에 사용된 알고리즘을 지정합니다. 예를 들어, HMAC SHA256을 나타내는 “HS256”이 있습니다.
  • typ: 토큰의 타입을 지정합니다. 일반적으로 “JWT”로 설정됩니다.

alg의 종류는  JWA standard에서 확인할 수 있습니다.

Payload

JWT의 페이로드는 토큰에 포함된 클레임(정보)을 담고 있습니다.

{
  "name": "HTB",
  "value": 1337,
  "admin": true
}

페이로드에는 여러 가지 표준 클레임 세트가 포함될 수 있으며, 애플리케이션 요구에 따라 커스텀 클레임도 추가할 수 있습니다.

클레임의 세 가지 유형은 등록된(registered), 공개(public), 그리고 비공개(private)입니다. 모든 클레임은  JWT standard에서 확인할 수 있습니다.

Signature

JWT의 서명은 토큰의 무결성과 진위를 확인하는 데 사용됩니다. 서명은 헤더와 페이로드를 결합하고, 이를 지정된 알고리즘으로 해시하여 생성됩니다. 일반적으로 비밀 키 또는 공개/개인 키 쌍을 사용하여 서명됩니다. 서명된 JWT는 토큰이 변조되지 않았음을 보장하며, 발행자의 신원을 확인할 수 있게 합니다.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

서명 생성 과정은 다음과 같습니다.

  1. 헤더와 페이로드를 base64로 인코딩합니다.
  2. 인코딩된 헤더와 페이로드를 점(.)으로 구분하여 결합합니다.
  3. 결합된 문자열을 비밀 키로 서명합니다.

JWT-based Authentication

Stateful Authentication

일반적으로, 사용자는 웹 애플리케이션에 세션 토큰을 제시하며, 애플리케이션은 데이터베이스에서 사용자의 관련 데이터를 조회합니다. 이 방식은 웹 애플리케이션이 세션과 관련된 데이터를 유지해야 하므로 상태 저장 인증이라고 불립니다.

Stateless Authentication

JWT 기반 인증에서는 세션 토큰이 사용자 정보를 포함한 JWT로 대체됩니다. 서버는 토큰의 서명을 검증한 후, JWT의 클레임에서 모든 사용자 정보를 가져올 수 있습니다. 서버가 상태를 유지할 필요가 없기 때문에 이 방식을 무상태 인증이라고 합니다.

JWT의 서명은 데이터 조작을 방지하며, 데이터가 조작될 경우 서명 검증이 실패합니다.

JWT Vulnerabilities

1. Attacking Signature Verification

Signature은 JWT의 페이로드 내 데이터를 보호합니다. 공격자는 Signature을 무효화하지 않고 JWT의 페이로드 내 데이터를 조작할 수 없습니다. 그러나 웹 애플리케이션에서 잘못된 설정으로 인해 Signature 검증이 부적절하게 이루어지는 Missing Signature Verification과 None Algorithm에 대해 알아보겠습니다.

Signature 검증이 부적절하게 이루어지는 잘 못된 설정은 JWT의 페이로드 내 데이터를 조작할 수 있게 합니다.

1-1. Missing Signature Verification

Signature은 발신자를 식별하고 메시지가 변경되지 않았음을 보장하는 데 사용됩니다. Header에 지정된 알고리즘을 사용하여 비밀 키(Secret)로 서명됩니다. 하지만 서명 검증이 미흡하다면, 페이로드의 데이터가 변조되어도 이를 탐지할 수 없습니다.

예시에 JWT의 Payload로 보이는 부분이 출력되어 있으며, 그 아래 JWT Token 값을 입력할 수 있는 Form을 확인할 수 있음. 이것을 봤을 때, 위 Payload가 들어있는 JWT Token을 생성하여 제출하라는 것으로 판단이 됩니다.

Request에서 jwtToken의 값이 빈 값으로 서버에 전달 시 Response에서 success의 값이 false로 설정되어 클라이언트로 전달됩니다. 즉, 인증이 실패하였다는 의미입니다.

jwt.io에서 자동으로 생성되는 JWT Token에서 Payload를 문제에서 요구하는 Payload로 변조 후 Signature에 임의로 비밀 키를 기입하여 JWT를 생성합니다.

생성된 JWT Token을 제출 시 서버에서 JWT의 Signature을 제대로 검증하지 않아, 공격자가 임의로 생성한 JWT에서도 인증이 성공된 모습을 확인할 수 있습니다.

1-2. None Algorithm Attack

JWT는 알고리즘을 지정하지 않은 “none” 알고리즘을 지원합니다. None 알고리즘은 JWT에 서명이 포함되지 않으며, 웹 애플리케이션이 서명을 계산하지 않고 이를 수락해야 한다는 것을 의미합니다. Signature가 없기 때문에, 잘못 구성된 경우 웹 애플리케이션은 Signature 검증 없이 토큰을 수락하게 됩니다

예시에 JWT의 Payload로 보이는 부분이 출력되어 있으며, 그 아래 JWT Token 값을 입력할 수 있는 Form을 확인할 수 있음. 이것을 봤을 때, 위 Payload가 들어있는 JWT Token을 생성하여 제출하라는 것으로 판단이 됩니다.

이전과 비슷하지만, 큰 차이점이라면 서버단에서 JWT의 Header 부분에 명시된 alg 클레임의 값으로 Signature 검증을 수행합니다. 즉, 이전과 같이 임의로 비밀 키를 넣어서 생성하여도 서버 단에선 Signature 검증으로 변조 유무를 확인이 가능합니다.

이전 문제에서 처음 시도한 것과 동일하게 jwtToken 값을 빈 값으로 서버에 전달 시 500 상태코드가 반환이 됩니다. 해당 부분을 Header의 alg 클레임 값을 None으로 지정하여, 인증을 우회하도록 하겠습니다.

현재 JWT의 라이브러리에서 None 알고리즘은 사용을 금하고 있기 때문에 Cyber Chef, 개발자 도구, 파이썬 등으로 라이브러리 없이 직접 Header 부분을 Base64 인코딩을 해줘야 합니다. 위 사진에서 Cyber Chef와 개발자 도구를 이용하여 Base64 인코딩을 수행하는 것을 확인할 수 있습니다.

이후, 변조하고자 하는 Payload도 Base64 인코딩을 시켜주면 됩니다. 저는 위 사진처럼 간단하게 jwt.io을 이용하여 Base64 인코딩된 Payload 값을 획득하였습니다.

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhZG1pbiI6dHJ1ZX0.

이렇게 획득한 Header와 Payload를 합치면 None 알고리즘 JWT을 구할 수 있습니다. 주의할 점은 Signature가 없더라도 Payload 뒤엔 .이 존재한다는 점입니다.

None 알고리즘 JWT을 서버에 제출 시, Signature가 없더라도 인증이 성공합니다. 이는 클라이언트가 전달한 JWT의 알고리즘이 서버 단에서 사용하는 알고리즘인지 검증하지 않아, 발생된 취약점 입니다.

2. Attacking the Signing Secret

Signature의비밀 키를 알게 된다면, 누구나 변조된 JWT에 대해 유효한 Signature을 생성할 수 있습니다. 떄문에 많은 공격자들은 웹 애플리케이션에서 유효한 JWT를 요청한 후, Signature의 비밀 키를 획득하기 위해 무차별 대입 공격(Brute-force) 수행합니다.

이번 문제에선 JWT Token과 위조해야하는 Payload가 존재합니다.

문제에서 제공한 JWT을 jwt.io에서 확인 시 Payload의 admin의 값이 false인 것을 확인하였습니다. 이를 예시의 Payload의 admin처럼 true로 변조하도록 하겠습니다. 그러기 위해선 Signature의 비밀 키를 무차별 대입 공격을 통해 알아내야 합니다.

$ sudo apt install golang-go -y
$ go install github.com/x1sec/gojwtcrack@latest

Golang으로 제작되어 빠르게 무차별 대입 공격을 수행할 수 있는 도구인 gojwtcrack을 설치하기 위해 Golang을 설치 후에, 도구를 다운로드하였습니다.

$ echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOmZhbHNlLCJpYXQiOjE1MTYyMzkwMjJ9.btsP_A48EIDpdLQ_UdA_KjbCAZhHQhPK33STKB89JsY" > jwt.txt
$ ~/go/bin/gojwtcrack -t jwt.txt -d /usr/share/wordlists/rockyou.txt

일단 간단하게 rockyou을 이용하여, 사전 대입 공격을 수행하였습니다. 그 결과, Signature의 비밀 키 값이 lathra 문자열이라는 것을 도출했습니다.

Signature의 비밀 키를 알아냈으니, jwt.io에서 변조한 Payload를 Signature으로 서명했습니다.

변조된 JWT를 서버에 전달했을 때, Signature 검증이 무사히 통과되어 인증에 성공했습니다. 이는 개발자가 복잡도가 낮은 문자열을 비밀 키로 설정한 데서 비롯된 취약점입니다.

대칭 알고리즘인 HS256, HS384, HS512에서는 무차별 대입 공격을 통해 비밀 키를 알아낼 수 있습니다. 하지만 비밀 키의 문자열 복잡도를 높이면 공격자가 키를 알아내는 시간을 크게 늘리는 방법으로 보안 강화를 할 수 있습니다.