인증 우회 문제에서는 처음에 2016년도 인증 우회 사건 예시를 보여준다. 프록시를 사용하여 본인 인증 질문들에 대한 답변이 바디 데이터로 전송되는데, 이 파라미터들을 모두 지웠더니 정상적으로 인증되었다고 한다.
문제에서는 상황을 부여하는데, 비밀번호 초기화 중에 본인임을 인증하기 위한 보안 질문들에 대한 답을 해야한다. 이 때 질문에 대한 답을 잊어버렸다고 가정하고 인증을 우회해보는 것이다
아무 문자열을 입력해서 전송하면 바디 데이터에 5가지 파라미터 값이 전달되는 것을 알 수 있다.
secQuestion0, secQuestion1은 각각 질문에 대한 답을 저장하는 파라미터이고, 나머지는 브라우저에서 개발자 도구를 통해 hidden 속성을 가진 입력 값임을 알 수 있었다. 이제 이 요청 패킷을 Reapeter로 보내서 바디 데이터를 수정해서 보내보았다.
1. 파라미터 전부 삭제
2. userId=12309746
3. verifyMethod=SEC_QUESTIONS&userId=12309746
> 여기까지 테스트를 통해 verifyMethod, userId 파라미터는 필수여야 한다는 것을 알 수 있었다.
4. jsEnabled=1&verifyMethod=SEC_QUESTIONS&userId=12309746
5. secQuestion0=abcd&secQuestion1=abcd&jsEnabled=a&verifyMethod=SEC_QUESTIONS&userId=12309746
6. secQuestion0=&secQuestion1=&jsEnabled=a&verifyMethod=SEC_QUESTIONS&userId=12309746
7. secQuestion0=abcd&secQuestion1=abcd&jsEnabled=1&verifyMethod=&userId=12309746
8. secQuestion0=&secQuestion1=&jsEnabled=1&verifyMethod=SEC_QUESTIONS&userId=12309746
9. secQuestion0=abcd&secQuestion1=abcd&verifyMethod=SEC_QUESTIONS&userId=12309746
10. secQuestion0=abcd&secQuestion1=abcd&jsEnabled=&verifyMethod=SEC_QUESTIONS&userId=12309746
11. secQuestion0=abcd' '1'='1&secQuestion1=abcd' '1'='1&jsEnabled=1&verifyMethod=SEC_QUESTIONS&userId=12309746
여러 가지 방법으로 시도해 보았지만 계속 실패했다는 메시지만 리턴되었다. 그러다가 답변 파라미터명(secQuestion0, secQuestion1)을 변경해서 보내보았다.
파라미터명을 각각 secQuestion01111, secQuestion12222로 변경 후 요청 패킷을 보내니 통과 되었다는 응답 메시지를 받게 되었다. 단순히 파라미터명을 변경한 것만으로도 우회가 되었는데, 아마도 secQuestion0, secQuestion1 이라는 파라미터를 처리하는 부분에서 검증이 제대로 되지 않은 것으로 보인다.
WebGoat 깃헙에서 해당 페이지 처리 소스 코드를 보면 파라미터 중 secQuestion
이라는 문자열이 있기만 하면 userAnswers 라는 해쉬맵에 추가하는데,
이 userAnswers 해쉬맵 내에 secQuestion0
와 secQuestion1
이 존재하면서 동시에 값을 검증하는 로직으로 되어 있다. 따라서 secQuestion0
, secQuestion1
대신 다른 파라미터(secQuestion01111
, secQuestion02222
)가 존재해서 값 검증을 건너 뛰고 True 값을 리턴한 것으로 보인다.
이 섹션은 JWT token에 대한 부분으로, JWT token은 다음과 같은 구조를 가지고 있다.
token은 base64로 인코딩 되어 있어, 디코딩하면 위와 같이 .
을 구분자로 하여 헤더와 클레임(페이로드) 데이터를 알 수 있고, 헤더와 페이로드 데이터에 대한 검증으로 서명값을 뒤에 덧붙이는 형태이다.
JWT는 서명 시 사용하는 알고리즘 종류에 대해 알려주고, 어떠한 액션을 요청할 때 토큰을 서명값으로 검증한다고 한다. 문제에서는 투표 기능이 있는 페이지가 있는데, 기본 Guest 계정과 Tom, Jerry, Sylvester 계정이 존재한다.
일단 기능을 살펴보면 투표하기 위해서는 Guest 외에 다른 계정으로 접근해야 하고, admin 계정만이 투표를 초기화할 수 있다. 목표는 토큰 값을 구한 뒤, admin 권한으로 투표를 초기화하면 된다.
Tom으로 계정 전환 시 응답 헤더 중 Set-Cookie에서 access_token
값으로 JWT와 유사한 구조의 값이 저장되어 있음을 알 수 있다. 이 값을 base64로 디코딩 해보았다.
.
을 구분자로 해서 줄바꿈 후에 디코딩 하니 캡쳐 화면과 같은 값이 확인된다. 참고로 페이로드 값을 보면 끝에 Tomln0
이렇게 되어 있는데, base64 인코딩 시 사용하는 =
패딩 값을 JWT에서는 사용하지 않아서 디코딩했을 때 저렇게 되는 것으로 확인됐다.
서명 시 사용한 알고리즘은 HS512
이고 사용자는 Tom으로 되어 있다. admin 파라미터도 확인이 되는데 이 값이 false가 되어 있어서 이 값을 true로 바꾸면 되지 않을까 생각했지만, 서버에서 서명값으로 검증을 하기 때문에 재서명을 해야한다. 이 때 사용하는 키값을 모르기 때문에 다른 방법을 찾아 보았다.
처음에 JWT에서 사용하는 서명 알고리즘에 대해서 설명하는 링크가 나와있는데 확인해보니 JWT 헤더 중 alg
에 관한 내용이 있다. alg
값은 서명하는데 사용한 알고리즘을 표기하는 건데, 가장 아래에 보면 none
값이 있다. 서명을 하지 않을 때 사용한다고 되어 있는데, JWT 구조에 서명값이 존재했었는데 이 서명값이라는게 필수가 아니여도 되는 것으로 예상된다. 따라서 none
값으로 설정 후 서명값을 빼고 토큰을 서버로 전송되면 어떻게 되는지 확인해보았다.
일단 admin 권한으로만 가능한 투표 리셋 버튼을 클릭해서 패킷을 인터셉트했다.
요청 패킷 헤더 중 access_token에 JWT 값이 있는 것을 확인했다. 이 값을 이렇게 변경했다.
헤더는 alg 값을 none으로 변경 후 base64로 인코딩하고,
페이로드는 admin 값을 true로 변경 후 인코딩했다.
인코딩한 값들을 연결해서 위 캡쳐 화면처럼 토큰 값을 변경하여 전송했지만, 토큰 검증에 실패했다는 메시지만 리턴되었다.
원래 토큰 값을 보면 인코딩한 값 뒤에 =
패딩 값이 없었던 것이 생각이 나서 이를 지우고, .
구분자로 뒤에 서명값이 없다는 것 까지 표시한 후 전송하니 투표가 리셋되었다.
이번 문제는 서명된 토큰 값을 브루트 포스 공격으로 서명키를 알아낸 후, 페이로드 중 username을 WebGoat로 조작한 토큰 값을 구하면 되는 문제이다. 브루트 포스 공격을 통해 서명키를 알아낼 수 있다는 것은 토큰 값을 서명할 때 사용한 키값이 간단한 키값이라는 것을 알 수 있다. 일단 브루트 포스 공격으로 서명키를 알아낼 수 있는 방법을 찾아 보았다.
현재 WebGoat가 Kali 가상머신에 설치되어 있기 때문에 Kali linux 에서 제공하는 툴 중에 hashcat
이라는 것이 있었다. 그 중에 위 캡쳐 화면처럼 JWT 서명값을 브루트 포스 공격하는 옵션 값이 있었고, 이를 이용하기로 했다.
hashcat token.txt -m 16500 -a 3 words.txt
token.txt
파일에는 문제에 주어진 토큰값을 저장했고, words.txt
파일은 브루트 포스 공격시 사용할 단어 파일이다.
이 명령어로 브루트 포스를 진행하려 했지만 프로세스가 금방 죽어버렸다.
hashcat 가이드 페이지에서 리소스 할당량 옵션이 있어서 이를 높은 값으로 주어 다시 명령어를 실행해 보았다.
washington
라는 문자열이 키 값임을 알게 되었고,
jwt.io
사이트에서 username을 WebGoat로 수정한 후 새로 토큰을 생성했다.
하지만 토큰 검증이 제대로 이루어지지 않았다. 메시지를 확인해 보니 만료가 되었다고 나와있다.
페이로드 데이터 중에 exp
값이 만료일인데, 이를 수정하여 다시 토큰값을 전달하면 해결할 수 있다.
현재 Jerry 계정으로 로그인되어 있는 상태에서 Tom 계정을 delete하는 것이 문제이다.
delete 버튼을 클릭하면 위와 같은 패킷을 보낸다.
token 파라미터 값을 디코딩하여 1번 문제처럼 alg 값을 None으로 변경 후 서명값을 빼고 보내보았지만 해결은 되지 않았고, 서명키값을 브루트 포스 공격으로 알아내 보려고 했지만 이전에 사용했던 사전 파일 내에서는 존재하지 않았다.
이 토큰값을 디코딩해서 살펴보니 헤더 내에 kid
라는 값이 존재했다. 이 값은 서명 시 사용하는 키를 식별하는 값이라고 한다. 힌트를 보면 토큰값 내 헤더값을 살펴보라고 되어 있는데 아마도 이 값에 대한 취약점이 존재하는 것 같아서 관련 취약점이 있는지 검색해보았다.
이 kid 값에 인젝션 공격이 가능하다는 것을 알게 되었고, kid 값으로 키 파일명이 오거나, db에 저장된 식별자가 올 수 있는데, 위와 같은 방법으로 SQL 인젝션 공격이 가능하다고 한다.
abcd' UNION SELECT 'mykey' FROM information_schema.tables; --
원래 서버에 존재하는 webgoat-key 대신 abcd 라는 없는 키 식별자를 조회하려고 하기 때문에, union 절을 사용하여 결과로 나온 mykey
라는 서명키가 대신 사용하게 되는 것이다.
하지만 여전히 토큰값은 검증이 되지 않았다는 메시지만 리턴 되었다. 해결이 되지 않아서 결국 서버 소스 코드를 살펴 보았는데, 다음과 같이 키값을 리턴했다.
db 내에 저장된 키값이 base64로 인코딩 되어 있어, select 문으로 조회한 후에 이를 디코딩하여 리턴한 것이었다. 따라서 임의의 키 값인 mykey
를 base64로 인코딩하여 다시 토큰값을 생성했다.
이렇게 수정한 후 토큰값을 생성하여 요청 패킷 token 파라미터에 담아 전송하면 성공적으로 해결된다.
패스워드를 초기화할 때 주로 사용되는 보안 질문 폼이 나와있다. 그리고 webgoat 계정의 답은 red라는 것이 주어지고, 그 외 tom, admin, larry의 패스워드를 얻는 게 목적이다.
주어진 webgoat와 그에 맞는 답인 red를 입력했을 때는 위와 같은 패킷을 보냈다.
sql 인젝션 공격은 되지 않았고, 파라미터를 전부 제거해서 보내보거나, 파라미터명을 수정해서 보내보아도 힌트가 될만한 것이 없었다.
그런데 문제를 다시 살펴보니 사용하기 안전한 보안 질문에 대한 가이드를 하고, 관련 웹사이트를 소개해주고 있는데, 문제에서 제공하는 보안 질문이 가장 좋아하는 색, 즉 해당 질문에 대한 답변으로 간단한 색 하나를 입력하지 않을까 생각되었다. 그렇다는 건 충분히 예측 가능한 답변 목록이 나오고, 이를 브루트 포스 공격으로 맞출 수 있을 것 같았다.
바로 패킷을 Intruder로 보내서 파라미터에 페이로드를 설정하고,
첫번째 파라미터는 계정명, 두번째 파라미터는 여러가지 색 리스트를 추가해서 공격 시도했다.
너무나 간단하게 다른 계정이 설정한 답변을 알아낼 수 있었다.
Tom의 계정의 패스워드를 초기화하여 로그인에 성공하면 되는 문제이다. 패스워드를 초기화하려면 다음과 같이 이메일을 입력했을 때 해당 메일로 초기화 링크가 전달되는 방식이다.
이메일을 입력하면 해당 메일로 링크를 보냈다는 메시지를 받게 된다.
요청 패킷에는 입력했던 이메일 외에는 별다른 정보가 없었다.
일단 Tom이 아닌 현재 로그인된 계정으로 초기화 링크를 보내보았다.
WebWolf 서버에서 확인해보면 메일함에 패스워드 초기화 링크 메일이 수신되었음을 알 수 있다.
링크를 클릭하면 패스워드를 초기화하는 페이지로 접속할 수 있다. 패스워드를 초기화하고 나면 보안상 해당 세션을 종료하고 더이상 패스워드를 변경하지 못하게 해야 하는데, 이 링크에서는 여러번 변경이 가능하다.
즉, 세션이 계속 유지되는 취약점이 있어서, 문제에서 Tom이 링크를 받자마자 패스워드를 변경한다고 했었지만, 링크만 얻을 수 있으면 공격자 쪽에서 다시 변경이 가능하다는 얘기이다.
Referer 헤더값을 WebWolf 변경해서 보내보았지만, 수신된 초기화 링크는 차이점이 없었다.
이번에는 Host 헤더값을 임의로 변경해서 요청 패킷을 보내보았다. 그랬더니 수신한 초기화 링크 URL이 다음과 같이 되어 있었다.
https://velog.io/WebGoat/PasswordReset/reset/reset-password/5446c91e-ba6c-4542-8325-3e6e385d1065
Host 헤더값이 초기화 링크 URL 도메인 주소에 삽입이 된 것이다. WebGoat 서버에서 Host 헤더값을 이용해서 URL 생성 후 메일로 전송하는 것 같은데, 별다른 검증없이 URL을 생성하는 것이 문제가 된 것이다.
Tom은 패스워드 초기화 링크를 메일로 받으면 바로 접속한다고 문제에 나와있었다. 그리고 WebWolf에는 WebWolf로 접속하는 모든 로그를 남겨서 제공해준다. 그렇다면, 패스워드 초기화 링크를 WebWolf로 접속하도록 URL을 생성한다면 Tom이 해당 URL을 클릭하면 WebWolf에 로그가 남을 것이다.
Host 헤더값을 WebWolf로 접근하도록 변경하고, (WebGoat는 8080포트로 열려있고, WebWolf는 9090포트로 접근가능하다.) 요청 패킷을 보내보았다.
WebWolf 접속 로그에 위와 같은 기록이 남아 있었고, 이를 원래 URL인 WebGoat로 변경해서 접속했다.
성공적으로 패스워드 초기화 페이지로 접근이 가능하고, 원하는대로 패스워드를 변경한 뒤 로그인하면 문제는 해결된다.