항상 이슈가 되는 건 파일 업로드 취약점을 이용하는 공격입니다.


다중 파일 확장자 취약점

아파치 문서에 나와 있는 내용으로 지시어 extension 부분을 보면 아래와 같이 설명하고 있습니다. (출처: https://httpd.apache.org/docs/2.4/ko/mod/directive-dict.html)

일반적으로 filename에서 마지막 마침표 뒤에 나오는 부분이다. 그러나 아파치는 여러 확장자를 인식할 수 있기 때문에, filename에 마침표가 여러 개 포함된 경우 마침표로 구분된 모든 부분을 확장자(extension)로 처리한다. 예를 들어, 파일명 file.html.en은 .html과 .en이라는 두 가지 확장자를 가진다. 아파치 지시어에서 extension에 지정한 값 앞에 마침표가 있어도 되고 없어도 된다. 또, extension은 대소문자를 가리지 않는다.

일반적으로 habony.txt.php.shtml.en.html 인 파일은 html 로 인식해야 합니다. 하지만 이건 착각입니다. 아파치는 여러 확장자를 지원하므로 위 설명대로라면 마침표를 분리해 보면 .txt, .php, .shtml, .en, .html 확장자가 나오고 실행 가능한 마임 타입(mime-type)만 4개가 나옵니다.

확장자가 어디에 위치하든 .php 확장자를 포함하고 있으면 php 핸들러가 php로 해석하고 스크립트를 실행한다는 설명입니다. 아래 코드를 작성한 다음 파일 이름을 index.php.ko.kr 로 저장해서 실행해 봅시다.

<?php
phpinfo(); 
?>

그럼 habony.php.HelloWorld.shtml 는 어떨까요? 다중 확장자를 지원하는 아파치에서는 모든 스크립트를 php 로 해석합니다. 다행히 문제를 해결할 수 있는 몇 가지 제안이 있습니다.

파일 업로드를 허용할 때 마지막 확장자를 제외한 파일 이름에 점(.)이 포함되어 있으면 언더바(_)로 변경하거나 확장자 없는 파일로 저장하는 것입니다. 

점(.)을 explode() 로 분리하여 php 단어가 포함된 파일을 금지하는 방법도 괜찮습니다. php 4.x ~ 5.x 버전의 확장자는 .php 이고, php 7.x 버전 이상은 .php, .phps, .php3, php4, php5, .php7, .pht, .phtml 를 해석하므로 모두 막아야 합니다. 

<?php
$filename = 'habony.pHp.HelloWorld.shtml';

$arr = array('php', 'phps', 'php3', 'php4', 'php5', 'php7', 'pht', 'phtml');

$exts = explode('.', strtolower($filename));

foreach($arr as $val)
{
  if(in_array($val, $exts))
  {
    echo '<b>' . $val . '</b> 는 업로드 할 수 없습니다.';
    exit;
  }
}
if(move_uploaded_file($_FILES['fname']['tmp_name'], $filename))
{
  echo "파일 업로드 성공!";
}
?>


확장자를 변경하지 않고 저장하는 코드이면 .php만 막을게 아니라 .htaccess, .html, .htm 파일도 실행되지 않게 막아야 합니다.

<?php
$arr = array('php', 'phps', 'php3', 'php4', 'php5', 'php7', 'pht', 'phtml', ‘htaccess’, ‘html’, ‘htm’, 'inc');
?>


이미지 업로드 변조

이미지 파일은 어쩔 수 없이 업로드를 허용해야 하기 때문에 웹에서 파일 실행 권한이 주어지면 공격에 노출됩니다. 마지막 확장자를 확인하고 파일을 유효한 이미지로 받아들이는 일반적인 방법을 시도할 수 있습니다. 하지만 php 스크립트를 .gif, .png, .jpg 로 변경해서 업로드를 시도한다면 대부분 유효한 이미지로 판단할 것입니다. 과거에는 getimagesize() 함수로 이미지 여부를 판단했습니다.

<?php
$imagesizedata = getimagesize($file);
  if ($imagesizedata ) {
    // do something
  }
?>


PHP 문서는 주어진 파일이 유효한 이미지인지 확인하기 위해 getimagesize() 를 사용하지 말라고 권고합니다. 이뿐만이 아니라 info_file(), mime_content_type(), exif_imagetype() 함수도 올바로 검증하지 못합니다.

다음 내용을 myimage.gif 로 저장한 다음 getimagesize() 로 유효성을 검증하면 image/gif 를 반환할 것입니다.

GIF89a<
<?php
echo "Hello Habony";
?>

결국 이미지 내용의 앞머리만 검증하는 함수로는 우리를 계속 속일 것입니다. 가장 안전한 방법은 imagecreate() 함수를 이용하는 것입니다. 이 함수를 이용해 이미지 생성에 실패하면 false 를 반환합니다.

<?php
function chkImageFile($filename)
{
  $isimage = null;
  $chk = 'true';

  $imginfo = @getimagesize($filename);
  switch( $imginfo['mime'] ) 
  {             
    case 'image/gif':
      if(!$isimage = @imagecreatefromgif($filename))
        $chk = 'Not an gif';
    break;
             
    case 'image/jpeg':
      if(!$isimage = @imagecreatefromjpeg($filename))
        $chk = 'Not an jpg';
    break;
           
    case 'image/png':
      if(!$isimage = @imagecreatefrompng($filename))
        $chk = 'Not an png';
    break;

    case 'image/bmp':
      if(!$isimage = @imagecreatefromwbmp($filename))
        $chk = 'Not an bmp';
    break;
           
    default:
      $chk = "no Image";
  }
  return $chk;
}


$filename = "pacman.php.png";
if(chkImageFile($filename) === "true")
{
  if(move_uploaded_file($_FILES['fname']['tmp_name'], $filename))
  {
    echo "파일 업로드 성공!";
  }
} 
?>


웹 공격이 가능하려면 ‘<’와 ‘>’ 같은 문자가 있어야만 작성할 수 있는 스크립트 언어이기 때문에 파일을 이동하기 전에 파일 이름을 한 번 더 검증하는 것이 좋습니다.

<?php
$filename = "my<image>.gif";

// ex.1)
move_uploaded_file($_FILES['fname']['tmp_name'], htmlentities($filename));

// ex.2)
if(strlen($filename) === strcspn($filename, "\0\/:;*?\"'<>|"))
{
  move_uploaded_file($_FILES['fname']['tmp_name'], $filename);
}
?>


약간 응용해서 이미지 파일이면 확장자 변경 없이 그대로 저장하고, 나머지 파일은 모두 md5() 로 저장하는 것입니다.

<?php
if(strlen($file) === strcspn($file, "\0\/:;*?\"'<>|"))
{
  if(chkImageFile($file) === "true")
  {
    // 이미지는 그대로 저장합니다.
    move_uploaded_file($_FILES['fname']['tmp_name'], 'data/' . $file);
    echo "이미지를 data 에 저장하였습니다.";
  }else{
    // 나머지 파일은 md5 로 저장합니다.
    move_uploaded_file($_FILES['fname']['tmp_name'], 'upload/' . md5($file));
    echo "파일을 upload 에 저장하였습니다.";
  }
}else{
  echo "파일 이름에 문제가 있습니다.";
}
?>


0 댓글