JSON WEB TOKENS

문제 및 개념

JOSE(JavaScript Object Signing and Encryption)는 인터넷에서 정보를 안전하게 전송하는 방법을 지정하는 프레임워크입니다. 웹 사이트나 응용 프로그램에서 사용자 권한을 부여하는 데 사용되는 JSON 웹 토큰(JWT)으로 가장 잘 알려져 있습니다. JWT는 일반적으로 사용자 이름과 암호를 입력하여 사용자 자신을 인증한 후 "로그인 세션"을 브라우저에 저장하여 이 작업을 수행합니다. 즉, 웹사이트는 사용자 ID가 포함된 JWT를 제공하며, 사이트에 제시되어 다시 로그인하지 않아도 사용자가 누구인지 증명할 수 있습니다. JWT는 이렇게 생겼습니다.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmbGFnIjoiY3J5cHRve2p3dF9jb250ZW50c19jYW5fYmVfZWFzaWx5X3ZpZXdlZH0iLCJ1c2VyIjoiQ3J5cHRvIE1jSGFjayIsImV4cCI6MjAwNTAzMzQ5M30.shKSmZfgGVvd2OSB2CGezzJ3N6WAULo3w9zCl_T47KQ

 헤더, 페이로드 및 시그니처('.'으로 구분)로 나누어 Base64 인코딩으로 되어 있습니다. 그리고 JWT는 base64 인코딩의 변형으로, URL에 오류를 일으킬 수 있기 때문에 '+'와 '/'가 다른 특수 문자로 대체되었습니다.
 일부 개발자들은 JWT 인코딩이 암호화와 같다고 생각하여, 중요한 데이터를 토큰 안에 넣습니다.오류를 입증하려면 위의 JWT를 디코딩하여 플래그를 찾으십시오. 이를 신속하게 수행할 수 있는 온라인 도구가 있지만, Python의 PyJWT 라이브러리를 사용한다면 다음 문제들을 해결하는 데에 좋습니다.

풀이

더보기

1) 제시된 JSON 웹 토큰

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJmbGFnIjoiY3J5cHRve2p3dF9jb250ZW50c19jYW5fYmVfZWFzaWx5X3ZpZXdlZH0iLCJ1c2VyIjoiQ3J5cHRvIE1jSGFjayIsImV4cCI6MjAwNTAzMzQ5M30.
shKSmZfgGVvd2OSB2CGezzJ3N6WAULo3w9zCl_T47KQ

 

2) 풀이

원문은 JOSE 및 JWT를 설명한다.

언급했던 것과 같이 JWT는 세 부분으로

첫 번째 부분은 헤더,두 번째 부분은> 페이로드, 마지막 부분은 시그니처이다.

이 각각의 값들은 base64로 인코딩 되어있고,

오류를 방지하게끔  '+'와 '/'가 다른 특수 문자로 대체된다

 

그리고 JWT를 인코딩과 디코딩 쉽게 제공하는 사이트이다.

https://jwt.io/

jwt.io을 디코딩을 손쉽게 이용하게 되면

헤더와 페이로드 시그니처를 확인할 수 있고,

페이로드에 이번 문제의 플래그값인

crypto {jwt_contents_can_be_easily_viewed}이 나오게 됩니다.

 

또한 원문에 따르면

파이썬에서 pyJWT 라이브러리를 제공하므로

import jwt

jwt_enc = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmbGFnIjoiY3J5cHRve2p3dF9jb250ZW50c19jYW5fYmVfZWFzaWx5X3ZpZXdlZH0iLCJ1c2VyIjoiQ3J5cHRvIE1jSGFjayIsImV4cCI6MjAwNTAzMzQ5M30.shKSmZfgGVvd2OSB2CGezzJ3N6WAULo3w9zCl_T47KQ"
print(jwt.decode(jwt_enc, options={"verify_signature": False}))

출력값으로도 플래그값을 알아낼 수 있습니다

 

 

JWT Sessions

 

문제 및 개념

세션을 저장하는 전통적인 방법은 세션 ID 쿠키를 사용하는 것입니다. 웹 사이트에 로그인하면 백엔드(서버)에 세션 개체가 생성되고 브라우저(클라이언트)에 해당 개체를 식별하는 쿠키가 제공됩니다. 사이트에 요청할 때 브라우저는 자동으로 세션 ID 쿠키를 백엔드 서버로 전송합니다. 백엔드 서버는 이 ID를 사용하여 자신의 메모리에서 세션을 찾아 사용자에게 작업을 수행할 권한을 부여합니다.

 JWT는 다르게 작동합니다. 로그인한 후 서버는 사용자 이름, 권한 및 기타 정보를 설명하는 키-값 쌍의 페이로드가 포함된 JWT의 전체 세션 개체를 웹 브라우저에 보냅니다. 또한 서버의 비밀 키를 사용하여 작성된 서명도 포함되어 페이로드의 조작을 방지합니다. 웹 브라우저는 토큰을 로컬 저장소에 저장합니다.

이후 요청 시 브라우저는 토큰을 백엔드 서버로 보냅니다. 서버는 먼저 서명을 확인한 후 토큰 페이로드를 읽어 사용자에게 권한을 부여합니다.


 이후 요청 시 브라우저는 토큰을 백엔드 서버로 보냅니다. 서버는 먼저 서명을 확인한 후 토큰 페이로드를 읽어 사용자에게 권한을 부여합니다.

   💡요약하자면 세션아이디 쿠키는 서버에, JWT는 클라이언트에 세션이 생성

 세션 ID 쿠키에 비해 JWT의 주요 장점은 확장이 쉽다는 것입니다. 조직들은 여러 백엔드 서버 간에 세션을 공유할 수 있는 방법이 필요합니다. 클라이언트가 한 서버 또는 리소스 사용에서 다른 서버로 전환하더라도 해당 클라이언트의 세션은 계속 작동합니다. 게다가, 큰 조직들의 경우 수백만 개의 세션이 있을 수 있습니다. JWT는 클라이언트에 상주하기 때문에 이러한 문제를 해결합니다. 토큰의 서명을 확인하고 내부의 데이터를 읽는 것만으로 백엔드 서버가 사용자에게 권한을 부여할 수 있습니다.

 그러나 JWT는 종종 안전하지 않은 방식으로 구성될 수 있기 때문에 몇 가지 단점이 있으며, 고객은 자유롭게 JWT를 수정하고 서버가 여전히 확인할 수 있습니다. 이러한 악용 사례는 다음 과제에서 살펴보겠습니다. 그래서 플래그는 브라우저가 서버로 JWT를 보낼때 같이 보내는 헤더의 이름이다.

설명 및 정답

세션 ID 쿠키는 서버에 세션을 생성하여 권한을 부여하고,

JWT의 주요 이점은 JWT값이 클라이언트에 세션을 생성하여 권한을 부여한다.

예시로 원문 그림을 통해 JWT를 이용하여 로그인하는 과정으로 설명한다.

그림 4번 설명을 보면 권한 부여 헤더와 함께 JWT를 전송한다.

그래서 해당 답은 권한 부여Authorization입니다.

No Ways JOSE

문제 및 개념

JWT 알고리즘을 살펴보겠습니다. JOSE의 첫 번째 부분은 헤더이며, 이 헤더를 디코딩하면 다음과 같습니다

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

이것은 서버에 JWT이며 이를 검증하기 위해 어떤 알고리즘을 사용해야 하는지 알려줍니다.
여기 문제가 보이십니까?
서버는 토큰의 무결성을 확인하기 전에 이 신뢰할 수 없는 토큰을 처리해야 합니다!
이상적인 암호화 프로토콜에서는 메시지에 대한 추가 작업을 수행하기 전에 수신한 메시지를 확인합니다.
그렇지 않으면 Moxie Marlinspike의 말에 따르면 "어떻게든 필연적으로 파멸로 이어질 것입니다."


JWT의 "none" 알고리즘이 좋은 예입니다. 아래 링크는 많이 존재하는 취약점을 보여주는 JWT 라이브러리와 고장난 세션 API와 상호 작용할 수 있는 페이지로 이동합니다. 이를 사용하여 승인을 무시하고 플래그를 가져오시면 됩니다.
 https://web.cryptohack.org/no-way-jose

 

풀이

더보기

 이 문제에선 취약점이 많은 JWT라이브러리와 고장 난 세션 API가 있는 사이트를 제공한다.

https://web.cryptohack.org/no-way-jose/

https://web.cryptohack.org/no-way-jose/

사진과 나온 코드는 이렇다.

import base64
import json
import jwt # note this is the PyJWT module, not python-jwt


SECRET_KEY = ?
FLAG = ?


@chal.route('/no-way-jose/authorise/<token>/')
def authorise(token):
    token_b64 = token.replace('-', '+').replace('_', '/') # JWTs use base64url encoding
    try:
        header = json.loads(base64.b64decode(token_b64.split('.')[0] + "==="))
    except Exception as e:
        return {"error": str(e)}

    if "alg" in header:
        algorithm = header["alg"]
    else:
        return {"error": "There is no algorithm key in the header"}

    if algorithm == "HS256":
        try:
            decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        except Exception as e:
            return {"error": str(e)}
    elif algorithm == "none":
        try:
            decoded = jwt.decode(token, algorithms=["none"], options={"verify_signature": False})
        except Exception as e:
            return {"error": str(e)}
    else:
        return {"error": "Cannot decode token"}

    if "admin" in decoded and decoded["admin"]:
        return {"response": f"Welcome admin, here is your flag: {FLAG}"}
    elif "username" in decoded:
        return {"response": f"Welcome {decoded['username']}"}
    else:
        return {"error": "There is something wrong with your session, goodbye"}


@chal.route('/no-way-jose/create_session/<username>/')
def create_session(username):
    encoded = jwt.encode({'username': username, 'admin': False}, SECRET_KEY, algorithm='HS256')
    return {"session": encoded}

먼저 create_session 함수를 보게 되면 username을 받아 JWT값을 주게 된다.

그리고 authorise함수를 보게되면 jwt값을 가져와 

해당 jwt값을 받아 admin으로 확인되면 

Welcome admin, here is your flag: {FLAG}

로 보아선 플레그값을 주고,

 그게 아니라면 

Welcome {decoded['username']}
입력했던 유저이름과 함께 출력이 된다.

 

userMSS를 입력할때 나온 JWT값

 

JWT(session)값을 입력후 모습

 먼저 JWT값을 디코딩해 보면

 

여기서 admin값이 false인걸 확인하여 true로 바꾸게 되면 인코딩 값이 바꾸어지게 되어 이를 사이트에 대입하였다.

시그니처 증명 실패

Signature verification failed가 나오게 된다.

문제 및 개념에서 보시다시피 "none" 알고리즘을 사용하게 된다면 승인을 무시하고 플래그값을 가져올 수 있다.

그래서 기존의 HS256를 none으로 바꾸어 시도를 해보려고 한다.

 

해당 사이트에선 알고리즘에 'none'이 적용이 안되어 인코딩이 안된 걸 볼 수 있다.

참고로 JWT는 헤더, 페이로드, 확인된 시그니처를 각각 base64 인코딩을 시킨 후의 '='를 뺀 형태로 보여준다.

현재 헤더가

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

로 확인 되는데

 

이를

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

로 바꾸어 base64 인코딩을 직접 하여 이를 헤더로 바꾼다.

import base64

userMSSHeader= '{"typ":"JWT","alg":"none"}'
userMSSHeader_bytes = userMSSHeader.encode('ascii')
userMSSHeader_base64 = base64.b64encode(userMSSHeader_bytes)
userMSSHeader_base64_str = userMSSHeader_base64.decode('ascii')

print(userMSSHeader_base64_str)
인코딩된 값

그래서

헤더 값은

eyJ0 eXAiOiJKV1 QiLCJhbGciOiJub25 lIn0

페이로드 값은

eyJ1c2VybmFtZSI6InVzZXJNU1MiLCJhZG1pbiI6dHJ1ZX0

증명할 시그니처 값은

w1Ctdzpb9RcSQyaZ6AtqMFh0bn6qXg-HDVfqVqbpUlw

base64인코딩을 하여

 

eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6InVzZXJNU1MiLCJhZG1pbiI6dHJ1ZX0.w1Ctdzpb9RcSQyaZ6AtqMFh0bn6qXg-HDVfqVqbpUlw

JWT값을 만들어 대입을 하면

 

어드민과 플래그값

어드민과 플래그값을 얻을 수 있다.

플래그값은 crypto{The_Cryptographic_Doom_Principle} 이다.

 

복사했습니다!