[Security-04] Web Security Pt.3

유영석·2022년 9월 18일
2

Security

목록 보기
4/12
post-thumbnail

이어서 이번에는 쿠키 보안을 위한 정책에 대해 알아보도록 하겠습니다.

Same Origin Policy(SOP)

SOP에 들어가긴 전, 우리는 먼저 Origin이라는 개념을 확실히 할 필요가 있습니다. 어떤 웹의 두 페이지가 잇을 때 둘의 origin이 같다는 것은 Protocol(프로토콜), Port(포트 번호), Domain(도메인)이 같다는 것을 의미합니다. http://www.flicker.cxx/galleries/와 비교한 예시는 아래와 같습니다.

그러면 본격적으로 SOP란 무엇을 보장할까요?

한 origin 위에서 돌아가고 있는 자바스크립트는 오직 같은 ORIGIN의 object들만을 읽고 쓸 수 있다.

정확히는 읽고 쓰는 것 뿐만 아니라 자바스크립트로 실행하고, 보내고, 받는 것까지를 포함합니다. 명심해야 될 부분이 몇 가지 있습니다.

  • SOP는 다른 origin의 object들에 해당하는 정책이지 자바스크립트가 혼자 독립적으로 할 일하는 것은 SOP가 관여하지 않습니다.
  • 또한, 이미지와 같이 단순히 다른 origin의 리소스를 로드하는 것은 문제가 되지 않습니다.
  • HTTP 메소드 중 GETPOST는 문제가 되지 않습니다. 하지만 서버 데이터에 직접적으로 영향으로 미칠 수 있는 PUTDELETE가 SOP에 의해 제한됩니다.

다음과 같이 a 도메인에서 a 도메인의 리소스와 b 도메인의 리소스를 동시에 사용할 수 있습니다. 물론 GET과 POST 요청만이 가능하지요. 그리고 이와 같이 두 개의 도메인 오브젝트를 동시에 요청하는 것을 Cross Domain Request, 다른 말로는 Cross site request 혹은 Cross origin request(COR)이라고도 합니다.

각각의 웹사이트들은 Cookie, DOM, Javascript, image, video, canvas와 같은 object들을 가지고 있습니다. 그리고 각각의 object들은 그것은 URL을 통해서 origin을 파악할 수 있습니다. 그리고 가장 중요한 개념은 다음과 같습니다.

자바스크립트는 그것이 최종적으로 실행되는 origin의 Context 내에서 동작합니다.

이게 도대체 무슨 뜻일까요? 예를 봅시다. A라는 origin에서 B라는 origin의 스크립트는 로드할 수 있습니다. 하지만 그것이 실질적으로 A에서 돌아가기 때문에 A의 context에서 동작합니다. 그렇기 때문에 B에서 온 스크립트는 A의 컨텐츠나 소스 코드에 닿을 수가 없죠. 물론 B의 페이지도 iframe으로 로드할 수는 있지만, 그 iframe의 DOM에 접근을 할 수는 없습니다. 마지막으로 B에서 로드된 이미지의 비트 정보들을 A에서 접근할 수 없습니다.

이와 같이 서로 다른 origin인 A와 B는 서로의 오브젝트를 정말 딱 가져올 수만 있을 뿐, 그 어떤 영향도 끼치지를 못합니다. SOP 때문에!!!😳

이를 기반으로 이전 글의 Cookie Stealing을 다시 봅시다. SOP 덕분에 origin이 같지 않은 Gmail 프레임의 DOM을 조작하는 스크립트를 실행할 수가 없습니다. 예를 들어, 아래의 코드를 볼까요?

<iframe src="http://othersite.com"></iframe>
alert(frames[0].contentDocument.body.innerHTML) // 불가능
alert(frames[0].src) // 불가능

다른 웹사이트의 iframe을 가져오는 것은 문제가 되지 않지만, 그 밑의 코드처럼 해당 프레임의 오브젝트들을 읽으려고 하는 것은 SOP에 의해 제한됩니다.

아! 물론 브라우저는 이 모든 소스들을 다 읽을 수 있습니다! 위의 코드를 html로 하는 www.site.com에서 읽지 못하는 거지요...🧐

그러나 SOP에 걸리지 않는 것이 바로 Navigating입니다. Navigation이란 로드한 프레임의 내용을 바꾸는 것입니다. 다시 말하자면, 가져온 프레임의 도메인을 자바스크립트가 변경하는 것은 허용된다는 것입니다.

frames[0].location = "http://mysite.com/"

위와 같이 자바스크립트를 통해 어떤 프레임의 location을 원하는 다른 웹사이트로 바꿀 수 있습니다. 위 예시에서 자신의 웹사이트로 바꾸었죠? 이와 같은 네비게이션을 적절히 이용하면 공격 포인트가 될 수 있다 이 말입니다.

다시 돌아오자면, 우리가 브라우저에서 볼 수 있는 모든 HTML document는 각각이 자바스크립트 실행 환경을 갖추고 있습니다. 지금까지 얘기했던 Context 얘기가 되겠지요. 한 context에서 실행되는 모든 스크립트는 브라우저에서 제공하는 API들을 활용하여 다른 context들과 상호 작용할 수 있습니다. 그러나 origin이 같지 않은 분리된 두 자바스크립트 실행 context에서는, 하나가 다른 context의 DOM에 접근하지 못합니다. 이렇게 하나의 보호된 영역에서 프로그램을 동작시키는 개념을 보안에서 Sandboxing이라고 합니다.

Window

자바스크립트에서 윈도우, 즉 창을 띄우는 API는 다음과 같습니다.

window.open(URL, name [,specs][.replace])

인자의 name 영역에는 다음과 같은 값이 들어갈 수 있습니다.

  • _blank : 기본값입니다. 완전히 새로운 윈도우(창)을 만들어 띄웁니다.
  • self : 현재 프레임에 URL이 적용됩니다.
  • _parent : 부모 프레임에 URL이 적용됩니다.
  • _top : 최상위인 현재의 페이지 에 URL이 적용됩니다.

Frame

그러면 프레임을 사용한 공격법들을 살펴보도록 합시다.

CSS를 사용하면 하나의 iframe 페이지를 완전하게 투명히 하는 것이 가능합니다. 공격자는 희생자를 혹 하게 만들만한 페이지를 만들어 클릭을 유도합니다. 그러나, 그 클릭이 있는 자리에는 ebay의 비딩 버튼이 있는 것이지요. 그러면 자연스럽게 앞에 나와있는 비딩 버튼을 클릭한 것으로 처리됩니다. 이런 치사한(?) 은닉 공격을 UI Redress라고 합니다.

이런 문제로, 웹사이트 개발자는 자신의 소스가 다른 사이트에서 프레임으로 로드되는 것을 막을 수 있습니다. 이를 Framebusting이라고 합니다.

<script>
  if(top.location != self.location)
  	{top.location = self.location }
</script>

위와 같이 top의 location과 self의 location이 서로 다르면 프레임 된 것으로, 코드를 통해 찾을 수 있습니다.

2008년에는 이를 위해 새로운 HTTP reponse 헤더 필드가 추가되었습니다. 이 필드의 이름은 X-Frmae-Options입니다.

X-Frame-Options : DENY
X-Frame-Options : SAMEORIGIN
X-Frame-Options : ALLOW_FROM https://example.com/

DENY 값은 어떤 웹사이트도 나의 웹사이트를 프레임으로 로드하는 것을 허용하지 않는 것입니다. SAMEORIGIN은 같은 origin이라면 허용하는 것이며, ALLOW_FROM은 명시된 특정 웹사이트만을 허용하는 것이죠.

프레임을 왜 막으려고들 할까요? 말씀드렸다시피, 프레임은 네비게이션이 가능합니다. 공격자가 실제로 어떤 사이트의 프레임을 가져와서 사용자를 해당 사이트로 착각하게 만든 뒤, 로그인을 할 때만 똑같이 디자인된 공격자의 프레임으로 네비게이션 하는 겁니다. 그렇다면 아이디와 패스워드는 원하는 사이트가 아닌 공격자로 향하게 되겠지요...

예전 인터넷 초창기 시절에는 심지어 서로 다른 윈도우의 프레임을 네비게이션 할 수도 있었습니다. 그래서 다음과 같은 공격도 벌어질 수 있었지요.

  1. 희생자는 유명한 파워블로거의 블로그를 구경하던 중 공격자 사이트의 플래시 광고를 맞이하게 됩니다.
  2. 희생자는 새로운 창을 열어 은행 사이트에 접속을 하는데, 그 사이트는 비밀번호 창이 프레임으로 되어있던 것이죠.
  3. 공격자의 광고창이 그 프레임을 똑같이 모방한 자신의 프레임으로 네비게이션 하는 겁니다.
  4. 아무것도 모르는 희생자는 비밀번호를 치고 그것이 공격자에게 전송되게 됩니다.

이러한 공격을 Cross-Window(Guninski) 공격이라고 합니다. 이제는 다 완벽히 패치되서 효과가 없어진 공격이지만요.😁

SOP for Cookies

쿠키는 DOM과는 origin의 정의가 다릅니다!

쿠키는 A와 B가 같은 도메인과 경로를 가지고 있을 때, origin A가 origin B의 쿠키에 접근할 수 있습니다.

프로토콜과 포트 번호는 무의미한 것입니다.

특이한 점은, Domain이 명시되어 있다면 그것의 Subdomain 또한 포함되게 됩니다. 만약 쿠키에서 Domain 필드가 안써져 있다면 자동적으로 HTTP 헤더의 host 필드의 값이 여기에 붙게 됩니다. host는 현재 document의 location입니다. 이 때는 또 Subdomain은 제외됩니다.

서브 도메인은 또한 기본 도메인에서 top-level domain(TLD)는 제외됩니다. 예를 들어 www.skku.edu가 있을 때, TLD인 .edu는 해당이 되지 않고, skku.edu까지가 서브 도메인에 해당되게 되는 거지요.

위의 예시는 login.site.com에서 쿠키를 세팅, 즉 쓰는 것에 대한 예시입니다. 쿠키 1은 도메인이 같기 때문에 세팅될 수 있고 쿠키 2는 서브 도메인이 때문에 역시나 세팅될 수 있습니다. 그러나 쿠키 3은 아예 다른 도메인이기 때문에, 쿠키 설정이 허용되지 않습니다. 그렇다면 서브 도메인은 왜 허용해줄까요? 예를 들어, login.amazon.com에서 로그인해서 받은 쿠키를 amazon.com에서 사용해야 되지 않겠어요?😝

위의 예시는 쿠키를 읽는 것에 대한 예시입니다.

  • http://checkout.site.com/
    서브 도메인인 쿠키 2만을 읽을 수 있습니다.
  • http://login.site.com/
    secure 때문에 쿠키1은 안되고 서브 도메인인 쿠키 2만을 읽을 수 있습니다.
  • https://login.site.com/
    https이기 때문에 쿠키 1, 쿠키 2를 모두 읽을 수 있습니다.

브라우저가 쿠키를 받을 때는 다음과 같이 name, value 뿐 아니라 domain, path, secure, expires 등 여러 attritube들이 추가로 붙습니다. 그러나 쿠키를 서버로 보낼 때는 오직 name, value만 보내는 Asymmetry 가 존재합니다. 이렇게 되면 서버는 쿠키를 받아도 도메인이 포함되어 있지 않기 때문에 누가 발행해주었던 쿠키였는 지를 알 수 없는 것입니다! 이로 인한 문제들을 알아보도록 합시다.

Overwrite에 관한 예시를 하나 들어보죠.

  1. Alice가 login.site.com에서 로그인을 했습니다. 해당 사이트는 session-id=Alicedomain=.site.com과 같은 attribute와 함께 Alice에게 쿠키를 발행하죠.
  2. 그 이후에 Alice가 evil.site.com에 접속하게 됩니다 . 그럼 이 사이트는 서브 도메인의 원칙 때문에 session-id=badguy로 세션 아이디를 바꿔서 쿠키를 쓸 수 잇습니다.
  3. 이후 Alice가 숙제 제출을 위해 cs142hw.site.com에 접속하지만 동시에 보낸 쿠키의 session-id 필드에는 badguy로 명시 되어 있기 때문에 숙제 사이트는 이 제출이 badguy가 한 것이라고 생각하게 되는 것이죠. 숙제 제출을 강탈하는 것입니다.

Secure 필드에 관한 다른 예시를 하나 들어보겠습니다.

  1. Alice가 https://www.google.com에 로그인하여 다음과 같은 5개의 쿠키를 받았고, 이들 중 마지막 LSID, GAUSR이 "secure" 쿠키입니다.
  2. 이후에 Alice가 공격자에 의해 강제적으로 http://www.google.com을 방문하게 됩니다. 그러면 공격자는 쉽게 HTTP response로 다음과 같은 헤더를 Injection 할 수 있습니다.
Set-Cookie : LSID = badguy; secure

결과적으로 secure가 설정되어 있어도, http 사이트가 도메인은 같기 때문에 쿠키를 쓰는 데는 문제가 되지 않습니다. 하지만, 지금까지 설명한 공격들은 모두 Overwrite, 즉 쿠키를 쓰는 공격들이었습니다. 쿠키를 훔치는, 즉 쿠키를 읽는 공격은 SOP에 의해 막힙니다. 왜냐하면, 공격자에 의해 삽입된 코드가 다른 도메인이 달라 쿠키를 건들 수 없기 때문이죠. 결국 A라는 사이트에 받은 쿠키는 A에서 다운로드 받은 자바스크립트만이 접근할 수 있다는 것입니다. 그래서 공격자들은 직접 사이트 A에게 스크립트를 주고 A가 이를 희생자에게 주도록 하여 쿠키를 빼내는 기발한 아이디어를 내게 됩니다. 이를 Cross-site Scripting(XSS)라고 하지요.

Cross-site Scripting(XSS)

이와 같은 방식의 XSS에는 다음과 같은 세 가지 타입이 있습니다.

  • Reflected (Non-persistent)
  • Stored (Persistent)
  • DOM-based (Non-persistent)

Reflected

첫 번째는 Reflected 방식입니다. 반사한다는 뜻이죠?

공격자는 오류가 있는 한 사이트를 찾습니다. 그 사이트는 페이지에서 요청된 URL의 일부를 페이지에 씁니다. 로그인을 할 때 URL에 아이디가 넘어가면 그걸 파싱해서 "안녕하세요, 영석님!" 과 같이 커스터마이즈 하는 것이죠. 즉, URL의 일부부를 리턴하는 페이지에 쓰는 것이며, 우리가 사용하는 많은 웹사이트가 이렇게 동작하고 있습니다. 이를 이용해서, 공격자는 URL에서 서버가 사용하는 그 일부에 자신의 스크립트를 쓰는 것입니다. 그 링크를 혹하는 내용과 함께 희생자들에게 이메일과 같은 형식을 통해서 돌리는 겁니다!

희생자가 그 링크를 누르면 서버는 뭣도 모르고 그 URL에서 스크립트 부분을 페이지에 추가하여 사용자의 GET 요청에 응답 페이지를 주는 것이죠. 그러면 사용자는 서버가 준 스크립트를 자동으로 실행하는데, 서버가 직접 줬기 때문에 쿠키 접근이 가능해지는 것이죠. 공격자는 이 쿠키를 자신에게 전달하게끔 스크립트를 짰기 때문에 쿠키가 털려버리게 됩니다...😫

Stored

Reflected는 희생자가 링크를 클릭하여 직접 스크립트를 전달한 뒤, GET을 하고 나면 모든 공격 과정이 끝납니다. 일시적인, non-persistent한 방식인거죠. 다음으로 설명드릴 Stored 방식은 이에 반해 persistent한 방식입니다.

다음과 같은 화면을 인터넷에서 흔히 보시죠?? 댓글을 남기는 인터페이스입니다. 우리가 여기에 쓰는 내용은 그대로 서버에 저장되어 많은 이용자들이 해당 내용을 받아서 브라우저에 띄움으로써 볼 수 있습니다. 즉, 이 내용을 보고자 하는 사용자드른 이 스크립트를 받아 브라우저에서 실행시키는 것이죠. 이 점을 이용해서 공격자가 내용에 자바스크립트 코드를 삽입한다면 어떻게 될까요? 희생자의 브라우저에서 돌아가 공격자로 쿠키를 전송하게 만들 수 있을 겁니다. 그 과정을 그림으로 표현한 것은 아래와 같습니다.

이러한 Stored 방식은 공격자 입장에서는 훨씬 쉽고 한 번 올려놓으면 반영구적이니 이보다 더 좋을 수 없을 겁니다. 또한, Reflected 처럼 공격자가 희생자에게 직접 접근해서 링크를 클릭하도록 유도할 필요조차 없죠...😟

DOM-based

위에서 설명한 ReflectedStored는 결국 서버에 공격 스크립트 내용이 들어간다는 한계가 있습니다. 그렇기 때문에 Web Application Firewall(WAF)에 의해 빈번히 들통나서 차단당하게 되죠. 그래서 나온 방식이 DOM-based로, 이를 알아보기 전에 먼저 Location SearchFragment URL을 알아야 합니다. 먼저, Location Search를 알아보도록 하겠습니다.

만약, 현재의 URL이 https://www.w3schools.com/submit.html?email=someone@example.com이라고 합시다.

위와 같은 스크립트 코드를 시켰을 때 결과를 보면 우리는 Location Search가 URL에서 ?부터의 쿼리 내용임을 짐작할 수 있습니다. 즉, 이전 까지의 URL에서 뒤의 쿼리에 해당하는 위치를 찾아주기 위함이지요.

그리고 Fragment URL은 위와 같이 URL 뒤에 # 문자가 붙는 URL을 말합니다. # 뒤의 내용은 일종의 북마크라고 볼 수 있습니다. 브라우저가 실제로 서버에게 다운로드를 요청해서 받아오는 부분은 딱 # 전까지입니다. 그만큼을 통째로 받아온 뒤에, 브라우저에서 # 뒤의 내용을 보고 위치를 파고들어가 화면에 띄우는 것이지요.

즉, Fragment는 HTTP request에 포함되지 않습니다.

이 점을 중요하게 봐야합니다. 즉 Fragment ID는 오직 브라우저에서만 유효한 값이 되는 거지요.

이 두 가지를 이용해서 만약 희생자에게 다음과 같은 URL 링크를 클릭하도록 하면 어떨까요?

서버에서 받은 것이 아니라 사용자의 브라우저에서 자체적으로 위치를 찾아가기 위해 첫 번째 Location Search 영역과 두 번째 Fragment 영역을 실행시키겠죠? 그러면 해당 스크립트가 돌아가서 공격자가 쿠키를 훔칠 수 있게 되는 겁니다. 심지어 Fragment 방식은 해당 영역이 서버로 전송조차 되지 않기 때문에, WAF에 걸릴 일도 없어 더 강력하지요...😩

Defense Against XSS

이러한 XSS 공격들은 우리는 어떻게 막을 수 있을까요? XSS 방식은 결국 공격자가 심은 스크립트를 희생자가 실행시켜서 문제가 되는 것이잖아요? 그리고 스크립트는 코드는 <script>와 같이 시작합니다. 따라서 우리는 <, > 문자를 거르자 이 말입니다. 가장 좋은 방법은 모든 HTML, URL, 그리고 자바스크립트 엔티티들을 거르는 것이지요.

여기서 조금 더 발전되서 나온 것이 바로 Content Security Policy(CSP)입니다. 실제로 XSS 공격은 희생자가, 즉 희생자의 브라우저가 모르는 소스로부터 받은 것을 믿고 돌려버리기 때문에 발생합니다. 그래서 이를 위해 나온 것이 바로 CSP인 것입니다.

위와 같은 내용이 HTTP response 한 페이지의 메타 데이터에 포함되거나 헤더의 한 필드로 들어갑니다. 위의 예시와 같은 내용은 서버가 브라우저에게 스크립트 소스는 나와 같은 도메인(self)이나 cdn.trustedorigin.net에서 받고, 이미지 소스는 아무데서나(*) 받아라는 의미입니다. 화이트 리스트를 만들어서 주는 이 방법을 이용하면 브라우저는 제 3자가 제공한 스크립트는 돌리지 않게 됩니다. 그러나 이는 상식적으로 생각을 해보면 공격자가 서버를 통해 삽입하는 스크립트까지 막을 수는 없기 때문에 완벽한 솔루션이 되지는 못합니다. 실제로 XSS는 다른 방법을 통해 잠식될 수 있었죠...😄

profile
백엔드 개발자

0개의 댓글