
첫 번째 문제를 번역기로 해석해보니
JWT 토큰 디코딩
JWT 토큰을 디코딩해 보겠습니다. 이를 위해 WebWolf 내부의 JWT 기능을 사용할 수 있습니다. 주어진 토큰은 다음과 같습니다:

요약
1. 제공된 JWT(JSON Web Token)를 디코딩하여 그 안에 담긴 정보를 확인

문제를 확인해 보니
문제에서 제시한 토큰을 복사하여 붙여 넣고 토큰을 디코딩하면 토큰 안에서 사용자를 찾을 수 있는지에 대한 문제인 거 같습니다.

문제에서 제시한 JWT토큰을 웹 브라우저를 열어 주소창에 jwt.io를 입력하여 접근합니다. 그다음 복사한 JWT토큰을 디코딩을 진행하여 JWT의 Header 부분에는 암호화 알고리즘 "alg": "HS256"이 있고 Payload 부분에는 실질적인 데이터가 있는 것을 확인하여 해당 인코딩된 JWT토큰이 어떤 값을 가졌는지 확인합니다. 페이로드 부분에 아래쪽을 보면 "user_name" 값이 "user"라는 것도 확인할 수 있습니다.

문제로 돌아와서 user_name에 해당하는 값을 넣으면 이번 문제를 해결할 수 있습니다.

JWT 서명
각 JWT 토큰은 최소한 클라이언트에게 전송하기 전에 서명해야 하며, 토큰이 서명되지 않은 경우 클라이언트 애플리케이션은 토큰의 내용을 변경할 수 있습니다. 서명 사양은 여기에 정의되어 있으며, 사용할 수 있는 특정 알고리즘은 여기에 설명되어 있습니다. 기본적으로 토큰 서명을 위해 "HMAC with SHA-2 Functions" 또는 "디지털 서명 with RSASSA-PKCS1-v1_5/ECDSA/RSASSA-PSS" 기능을 사용합니다.
서명 확인하기
한 가지 중요한 단계는 다른 작업을 수행하기 전에 서명을 확인하는 것입니다. 토큰을 확인하기 전에 주의해야 할 몇 가지 사항을 확인해 보겠습니다.
요약
1. 모든 JWT는 클라이언트에 전송되기 전에 반드시 서명(Signing)되어야 합니다.
2. 서명이 되어 있지 않으면, 클라이언트가 토큰 내부의 데이터(예: 사용자 권한, ID 등)를 마음대로 조작할 수 있기 때문입니다.

문제를 확인해 보니
토큰을 변경하여 관리자가 되려고 시도해 봐라. 토큰을 변경하고 관리자가 되면 투표가 재설정할 수 있다.

먼저 아무거나 투표를 하기 위해서 투표 버튼을 눌러봤습니다. 그랬더니 게스트로서 투표할 수 없습니다. 먼저 로그인해 주세요. 라는 메세지창이 나옵니다.

그렇다면 계정을 바꿔서 진행해 보도록 하겠습니다. 저는 Tom으로 계정을 전환해서 진행해 보겠습니다.

Tom으로 계정을 전환했더니 user 파라미터에 Tom을 추가해서 요청하는걸 볼 수 있습니다. 아직은 access_token 값이 비어 있는걸 볼 수 있습니다.

응답을 확인해 보니 서버에서 새로운 토큰을 발급해 주는걸 볼 수 있습니다.

이번에는 Jerry로 계정을 전환해봤습니다. 요청에는 user 파라미터에 Jerry가 포함되어 전달하는 거 같습니다. access_token을 확인해 보니 Tom의 계정을 사용했을 때 토큰을 확인할 수 있습니다.

다시 Tom계정으로 전환했을 때는 Jerry의 access_token 값을 가진 상태에서 계정 전환을 요청합니다. 아마 access_token은 사용자마다 정해진 쿠키값을 받는 걸 확인할 수 있습니다.

이제 투표 결과를 초기화하기 위해서 쓰레기통 아이콘을 눌렀습니다. 그런데 Admin만 초기화할 수 있다는 메시지가 문제에 나타났습니다. 그렇다면 Admin 권한을 획득해야겠군요.

앞에 문제에서 사용자 전환했을 시 요청에서 user 파라미터만 변경했으면 다른 계정으로 이용할 수 있었으니, 이번에는 Admin으로 변경해서 진행해 봤습니다.

응답을 확인해 보니 권한이 없다고 나오면서 실패하게 됩니다. 어떻게 해야 Admin을 인정받을 수 있을까요? 앞서 우리는 JWT토큰을 통해서 해당 사용자가 맞는지 검증하였습니다. 그렇다면 JWT토큰을 한번 조사를 해봐야겠습니다. JWT.IO로 돌아가서 Tom의 토큰을 한번 디코딩해 봐야겠습니다.

Tom이 가지고 있던 JWT토큰에 포함된 데이터를 확인해 보니 페이로드 부분에 "admin" 값이 false입니다. 우리는 admin 권한을 가져야만 투표를 초기화할 수 있으니 해당 부분만 값을 변경하면 될거같지만 JWT토큰은 Signature 부분을 통해 무결성을 검증하기 때문에 바꿀 수가 없습니다. 하지만 방법이 없는 건 아닙니다. 우리가 Header 부분에서 JWT토큰을 암호화하는 알고리즘을 알 수 있었습니다. 만약 서버에서 클라이언트로부터 전달받은 JWT값을 처리할 때 안전하지 않은 메서드를 사용할 경우 해커는 JWT Header에 존재하는 암호화 알고리즘을 None으로 수정해서 암호화하지 않는 JWT토큰인척 변조해서 인증을 우회할 수 있습니다.

base64 URL 인코딩을 해주는 사이트로 가서 알고리즘을 None으로 설정한 텍스트를 공백과 줄 바꿈을 없앤 후에 base64 URL인코딩을 진행합니다.

JWT.IO로 돌아가서 Header 부분에 새로 base64 URL 인코딩된 값을 넣으면 알고리즘 부분이 None으로 수정된 것을 확인할 수 있습니다.

이번에는 페이로드 값을 가져와서 공백과 줄 바꿈을 지우고 admin을 true로 바꿔서 진행하였습니다.

페이로드 부분에 admin이 true로 수정된 것을 확인할 수 있습니다.

마지막으로 Signature 부분을 삭제해 줍니다. 왜냐하면 우리는 이미 Header 부분에 알고리즘을 None으로 설정했기 때문에 암호화된 데이터가 대입되는 signature 역시 없는 게 맞습니다. 하지만 페이로드와 시그니처 부분을 구분해 주는. (온점)은 남겨두어야 합니다.

burp suite으로 돌아와서 요청에서 access_token 부분에 우리가 수정했던 JWT토큰을 넣어줍니다.


문제로 돌아가면 문제가 해결됨과 동시에 투표수가 초기화된걸 볼 수 있습니다.

세 번째 문제를 번역기로 해석해 보니
JWT 크래킹
HMAC와 SHA-2 함수를 사용하면 비밀 키를 사용하여 토큰에 서명하고 검증할 수 있습니다. 이 키를 알아낸 후에는 새로운 토큰을 생성하여 서명할 수 있습니다. 따라서 키가 충분히 강해서 무차별 대입이나 사전 공격이 불가능한 것이 매우 중요합니다. 토큰을 갖게 되면 오프라인 무차별 대입이나 사전 공격을 시작할 수 있습니다.
요약
1. HMAC(SHA-2) 방식은 하나의 비밀 키(Secret Key)를 공유하여 토큰을 서명하고 검증합니다.
2. 만약 공격자가 이 비밀 키를 알아낸다면, 토큰의 내용을 마음대로 수정하고 유효한 서명을 직접 생성하여 서버를 완벽하게 속일 수 있습니다.
3. JWT는 암호화한 signature 값을 활용해 무결성을 검증하는데 이때 암호화에 사용되는 secret이 너무 짧거나 유추할 수 있는 단어일 경우 무작위 대입 공격을 통해 값을 알아낼 수 있다.

문제를 확인해 보니
JWT값의 secret을 알아내 페이로드의 username claim을 WebGoat로 변경하라는 문제 같습니다.

먼저 JWT에 무작위 대입 공격을 수행하기 위해 문제에서 제시한 JWT값을 복사한 후 docker 컨테이너를 생성합니다.

docker run 명령어로 temp_linux3 컨테이너를 정상적으로 설치했다면 다음으로 무작위 대입 공격 중 사전 대입 공격 방식을 사용할 건데, 이 공격은 사전에 존재하는 다양한 단어로, 무작위로 대입하는 공격 방식입니다. temp_linux3 컨테이너에 접근한 후 사전 대입 공격 데이터를 다운로드 하기 위해 wget과 무작위 대입 공격을 수행해 주는 도구인 hashcat을 설치합니다.

https://github.com/danielmiessler/SecLists/blob/master/Discovery/Web-Content/raft-small-words.txt
이어서 사전 대입 공격에 사용할 데이터를 다운받을겁니다. 위에 사진에 있는 웹 사이트에 접근하면 다양한 단어들을 확인할 수 있습니다.

temp_linux3 컨테이너에 필요한 패키지 설치가 끝나면 cd 명령어로, 홈디렉터리로 이동한 후
명령어 :
wget https://github.com/danielmiessler/SecLists/blob/master/Discovery/Web-Content/raft-small-words.txt
명령어를 입력해서 사전 대입 공격에 필요한 데이터를 모두 다운받습니다.

명령어 :
wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/raft-small-words.txt
필자는 다운로드를 하려고 할 때 너무 많은 요청이 있기 때문에 다운로드 받을 수 없다는 메시지를 받아서 Raw URL을 사용해 다운 받았습니다. wget은 파일의 실제 데이터(Raw data) 주소를 사용해야 하는데 현재 필자가 입력한 주소는 파일의 내용을 보여주는 웹 페이지(HTML) 주소입니다.

다운로드가 완료되면 맨 처음에 보고사해둔 JWT값을 파일 형태로 저장해야 합니다.
명령어 : echo "복사한 JWT토큰" > hash.txt
명령어를 통해 공격 대상인 JWT값을 hash.txt 파일에 저장합니다.

이제 모든 준비가 끝났습니다. hashcat을 이용해 hashcat hash.txt -a 3 -m 16500 raft-small-words.txt 명령어를 실행하면 공격이 진행됩니다. hash.txt는 공격 대상이며, -a 옵션은 공격 방식으로 3은 brute-force 방식을 의미합니다. -m은 공격 대상의 유형으로 16500은 JWT를 의미하고 마지막 raft-small-words.txt는 wget을 통해 다운로드한 사전 데이터로 공격 시 대입하고자 하는 데이터를 의미합니다.

공격을 시도하고 조금 기다리면 사전 대입 공격에 성공하게 되어 위에 사진과 같이 secret 값을 확인할 수 있습니다.


이제 기존의 JWT토큰을 JWT.IO에 들어가면 중간에 위에 사진처럼 JWT Encoder/JWT Decoder 이렇게 구분하는 버튼이 보이는데요. 거기서 먼저 decoder 부분에 복사해 둔 JWT토큰을 붙여 넣어주시고 encoder 버튼을 눌러서 Payload 부분에 username을 WebGoat로 수정해 줍니다. 마지막으로 맨 아래 signature 부분에는 아까 공격에서 나온 secret 값을 넣어서 완성하고 수정이 끝나면 오른쪽 부분에 새롭게 설정이 바뀐 JWT토큰을 복사합니다.

JWT토큰을 입력하고 제출하면 JWT가 만료되었다는 메시지가 나옵니다.

JWT가 만료되면 페이로드에 있는 exp를 찾아서 시간을 큰 값으로 수정한 후 변경된 토큰을 다시 복사합니다.

그러면 이렇게 이번 문제를 해결할 수 있습니다.

네 번째 문제를 번역기로 해석해 보니
토큰 새로 고침
액세스 토큰을 새로 고침하기 위한 좋은 전략을 구현하는 것이 중요합니다. 이 할당은 Bugcrowd의 개인 버그 현상금 프로그램에서 발견된 취약점을 기반으로 하며, 여기에서 전체 글을 읽을 수 있습니다.
요약
1. 이 문제는 실제 프라이빗 버그 바운티 프로그램에서 발견된 취약점을 바탕으로 합니다.

문제를 확인해 보니
Tom의 계정으로 쇼핑에 성공하면 되는 문제인 거 같습니다.

쇼핑하기 전에 먼저 checkout 버튼을 클릭하면 하단에 Not a valid JWT token, please try again 라는 메시지가 나타나면서 유효하지 않은 토큰이니 재시도를 바란다고 합니다.

해당 요청을 확인해 보면 사용자 인증을 담당하는 Authorization 헤더에 NULL 값이 포함되어 있어 오류가 발생한 것을 알 수 있습니다. 아직 인증과 관련된 어떤 정보도 알아내지 못했기에 문제에서 제공한 logfile을 참고해 봐야겠습니다.

logfile을 들여다보니 194.201.170.15 IP를 사용하는 사용자가 접근했던 흔적이 남아 있습니다. 좀 더 자세히 보니 /JWT/refresh/checkout?token=URI로 GET 방식의 요청을 확인할 수 있습니다. 이때 파라미터에 JWT를 넣어서 보낸 걸 알 수 있죠. 이 값을 복사해서 jwt.io에서 디코딩해 보겠습니다.

페이로드 부분을 보면 해당 토큰이 Tom의 JWT값인걸 알수 있습니다. JWT만료시간을 의미하는 exp 값이 실습에서 사용할 수 없는 만료된 JWT임을 확인할 수 있습니다.

그렇다면 우리는 Tom의 만료 시간을 늘리고 알고리즘을 무효화시켜서 JWT를 만들면 되지 않을까? 위에 사진처럼 알고리즘을 none으로 설정하고 exp 만료 시간을 대폭 늘려서 JWT토큰을 만들어봅시다.

설정이 끝난 JWT토큰을 복사해서 해줍니다.

조금 전에 사용자 인증을 담당하는 Authorization 헤더에 NULL 값이 포함되어 있어 오류가 발생한 것을 알 수 있습니다. 이번에는 수정한 JWT토큰을 가지고 요청 인증을 해보겠습니다.

문제로 돌아가면 Tom의 JWT토큰을 이용하여 쇼핑을 이용할 수 있게 되면서 문제가 해결된걸 볼 수 있습니다.

다섯 번째 문제를 해석해 보니
최종 도전
아래에 두 개의 계정이 있습니다. 하나는 제리이고 다른 하나는 톰입니다. 제리는 트위터에서 톰의 계정을 삭제하고 싶지만, 그의 토큰은 그의 계정만 삭제할 수 있습니다. 제리를 도와 톰의 계정을 삭제해 주실 수 있나요?
요약
1. Jerry의 계정으로 Tom의 계정을 삭제시켜라.

Jerry의 계정으로 Tom의 계정을 삭제시켜야 하는 문제인 거 같습니다. 먼저 Tom의 계정을 삭제하기 위해 삭제 버튼을 눌렀더니 위의 사진과 같이 유효한 JWT 토큰이 아닙니다 라고 나옵니다.

요청을 확인해 보니 token파라미터 JWT값을 추가해서 전달하는 거로 보입니다. 해당 토큰값을 디코딩하여 데이터를 한번 확인해 보겠습니다.

디코딩된 JWT값을 확인해 보니 Jerry의 JWT값인걸 알수 있습니다. 다른 점이 있다면 header 부분에 kid라는 처음 보는 값이 존재합니다. 아마 저게 힌트인 거 같습니다. 정확하게 파악하기 위해서 소스코드를 확인해 보겠습니다.

소스코드를 확인해 보니 삭제 버튼을 눌렀을 때 해당 코드가 실행되는데, 중간에 질의문이 보입니다. 자세히 코드를 살펴보니 jwt_keys테이블에서 id 값을 가지고 key를 찾는 거 같습니다. 즉, kid 값은 jwt암호화 key 값을 db에서 조회하기 위한 값이라는 것을 알았습니다. 중간에 while 문을 보면 조회한 key 값을 base64디코딩해서 return을 해주는데, 이 말인즉슨 db에 저장된 key 값들은 base64인코딩된 상태라는 것도 알게 되었습니다. 그런데 질의문을 보면 상당히 신경이 쓰이지 않나요? 우리가 SQL Injection 공격을 배울 때 저렇게 문자열을 붙여서 사용하면 안 된다고 배웠는데 말이죠. 결론은 해당 질의에 SQL Injection 공격을 시도하면 임의로 JWT암호화 key를 설정할 수 있어 JWT무결성 검증을 우회할 수 있다는 결론을 내릴 수 있습니다. 그럼 jwt.io로 돌아가서 kid에 SQL Injection 공격을 시도해 보겠습니다.

간단하게 설명하자면 앞부분은 'aaaa'라는 id를 찾을 거지만 조회가 되지 않습니다. 뒷부분은 information_schema 데이터베이스의 columns 테이블에서 'MTIzNA=='라는 값을 조회한 후 주석 처리하는 질의입니다. select 구문으로 특정 필드를 조회하는 게 아니라 정해진 값을 조회하는 구문입니다.
요약
1. 첫 번째 질의: 결과 없으면. (Empty)
2. 두 번째 질의: 'MTIzNA=='
3. 최종 합계 결과: MTIzNA==

그럼, 이제 JWT값을 처음부터 바꿔보겠습니다. header 부분에는 kid 값에 SQL Injection 공격을 넣어주고 payload 부분에는 만료 시간을 최대한 늘리고 username을 Tom으로 바꿔줘야 합니다. 그래야 Tom을 삭제할 수 있겠죠? 마지막으로 signature에 임의로 만든 암호화 key 값 1234를 넣어주면 모든 준비는 끝납니다. 설정이 끝난 JWT토큰을 복사해 주세요.

다시 문제로 돌아와서 삭제 버튼을 눌러준 다음 이번에는 token 파라미터에 복사한 JWT값을 집어넣습니다. 그런 다음 요청을 해보겠습니다.

문제로 돌아와 보면 문제가 해결된 것을 볼 수 있습니다.
이번 실습에서는 JWT토큰을 잘못 사용하면 일어나는 문제점들에 대해서 알아보았습니다.