괄호를 막아도 자바스크립트는 실행된다. 함수를 부르는 문법이 두 벌이고, 호출이 아닌데 실행되는 지점도 따로 있기 때문이다.
괄호 없이 실행되는 위치: 호출, 대입, 파서
| |
셋 다 소스에 괄호가 없는데 실행된다. 그러니 필터를 우회하기 전에 내가 노리는 sink가 셋 중 무엇을 요구하는지부터 봐야 한다. 대입만 요구하면 괄호가 막혀 있어도 그냥 된다.
태그드 템플릿: 괄호 없는 호출 문법
| |
함수 이름 뒤에 백틱 문자열을 바로 붙이면 그 함수가 호출된다. 엔진이 템플릿을 조각내서 넘기는데 변환 규칙은 이렇다.
| |
첫 인자가 문자열 조각들의 배열이고 ${} 안에 있던 값들이 뒤에 순서대로 붙는다. 배열에는 이스케이프 처리 전 원본을 담은 .raw 프로퍼티도 같이 달려온다. 원래는 SQL이나 HTML을 안전하게 조립하라고 만든 문법이고, 괄호를 안 쓰는 건 그 부수효과다.
그래서 alert`1` 은 실제로 alert(["1"]) 이다. alert이 인자를 문자열로 강제 변환하는데 길이 1짜리 배열은 "1" 이 되니까 결과가 같아 보인다. fetch`/write` 도 같은 이유로 동작한다.
쓸 수 있는 범위는 좁다. 인자를 하나만, 그것도 문자열 성격의 값으로만 넘길 수 있다. fetch 에 method와 body를 담은 옵션 객체를 주려면 두 번째 인자가 필요한데 이 문법으로는 못 준다. 괄호가 막힌 상황에서 POST 요청을 짜다 막히는 지점이 대부분 여기다.
백틱이 통하는 쪽과 안 통하는 쪽: eval과 Function
eval 은 인자가 문자열일 때만 코드로 해석한다. 문자열이 아니면 받은 값을 그대로 돌려주고 끝난다.
| |
Function 생성자는 인자를 문자열로 변환해서 함수 본문으로 삼기 때문에 배열을 줘도 통한다. 다만 함수를 만들기만 하고 부르지는 않으니 한 번 더 호출해야 한다.
| |
뒤에 붙은 백틱 두 개가 빈 템플릿이고, 그게 방금 만든 함수를 인자 없이 호출한다. 본문 안의 괄호는 \x28 과 \x29 로 넣었다. 문자열 리터럴 안에서는 16진 이스케이프가 유효하니까 원본 소스에는 괄호 문자가 등장하지 않는다.
setTimeout 과 setInterval 도 첫 인자가 문자열이면 코드로 해석한다. 다만 호출 자체에 괄호가 필요해서 괄호 우회용으로는 못 쓰고, 지연 실행이 필요할 때 쓰는 카드다.
괄호 우회 네 가지
태그드 템플릿은 앞에서 본 그대로다. 인자 하나짜리 호출이면 이걸로 끝난다.
예외를 던져서 브라우저가 대신 부르게 만들 수도 있다.
| |
window.onerror 는 처리되지 않은 예외가 생겼을 때 브라우저가 호출하는 핸들러다. 여기에 원하는 함수를 대입해두고 아무 값이나 던지면 브라우저가 그 함수를 불러준다. 대입에도 throw에도 괄호가 없다. 핸들러에 실제로 넘어가는 첫 인자는 던진 값 자체가 아니라 Uncaught 1337 같은 오류 메시지 문자열이라서, 값을 그대로 봐야 하면 접두사를 감안해야 한다. onerror 라는 이름이 필요하니까 필터가 on 을 통째로 막고 있으면 죽는 카드다.
대입만으로 실행되는 지점을 쓰는 방법도 있다.
| |
location 에 javascript: URL을 넣으면 브라우저가 그 코드를 현재 문서에서 실행한다. innerHTML 대입은 문자열을 HTML로 파싱하니까 이벤트 핸들러가 붙은 태그를 심을 수 있다.
location=name 은 조금 다르다. window.name 은 페이지를 옮겨도 값이 유지되는 성질이 있어서, 페이로드 본문을 공격자 페이지에 두고 피해자 페이지에서는 참조만 한다. 삽입 가능한 길이가 짧을 때 쓴다.
| |
피해자 페이지에 들어가는 건 location=name 열여섯 글자뿐이고, 실제 페이로드는 길이 제한을 안 받는다.
마지막은 괄호 자체를 복원하는 것이다. 문자열 리터럴 안에서는 \x28 이 ( 가 되고, HTML 속성값 안에서는 ( 이 ( 가 된다.
| |
문자 블랙리스트가 뚫리는 이유
필터는 저장 시점의 소스 문자열을 검사하는데, 실제 실행은 그 문자열이 여러 단계 변환을 거친 뒤에 일어난다.
\x28 은 문자열 리터럴을 파싱하는 단계에서 괄호가 된다. ( 은 HTML 파서 단계에서 괄호가 된다. 태그드 템플릿은 괄호가 아예 없는데도 호출 표현식으로 해석된다. 세 경우 모두 필터가 본 바이트열과 엔진이 실행하는 코드가 다르다. 그래서 특정 문자를 막는 방식은 변환 계층이 하나 늘어날 때마다 새 우회가 생기고, 그 계층을 전부 세어서 막는 건 실무에서 불가능하다.
방어: 컨텍스트별 이스케이프와 CSP
하나는 출력 컨텍스트에 맞춰 이스케이프하는 것이다. 같은 값이라도 어디에 박히느냐에 따라 막아야 할 문자가 달라진다.
| |
세 번째가 특히 잘 뚫린다. HTML 인코딩만 하고 스크립트 안에 넣으면 </script> 한 방에 컨텍스트를 빠져나간다.
다른 하나는 실행 권한 자체를 회수하는 CSP다.
| |
인라인 스크립트가 금지되므로 <svg onload=...> 도 <script> 도 실행되지 않고, unsafe-eval 이 없으니 eval 과 Function 도 막힌다. 입력을 걸러서 막는 게 아니라 실행을 못 하게 만드는 접근이고, 이게 현대적 방어의 기본선이다.
요약
| 기법 | 문법 | 전제 |
|---|---|---|
| 태그드 템플릿 | alert`1` | 인자 1개, 문자열 성격 |
| Function 생성자 | Function`code``` | eval은 불가, Function은 가능 |
| throw + onerror | {onerror=alert}throw 1337 | on 이름을 쓸 수 있어야 함 |
| 대입 sink | location='javascript:...' | 호출이 아니라 괄호 무관 |
| 이스케이프 복원 | \x28 · ( | 디코딩 계층이 있을 때 |