파일 업로드 취약점은 파일 업로드를 하는 곳에서 발생하게 된다. 파일 업로드 파트의 소스코드는 다음과 같다.
<?php
$targetDir = "uploads/"; // 업로드된 파일이 저장될 디렉토리 경로
// require_once "tool/chack_er.php";
// 파일이 업로드되었는지 확인
if(empty($_FILES["fileToUpload"]['name'])) {
}elseif(!empty($_FILES["fileToUpload"]['name'])) {
$targetFile = $targetDir . basename($_FILES["fileToUpload"]["name"]); // 업로드된 파일의 경로와 이름, basenaem함수는 파일경로에서 파일명만 가져옴.
$uploadOk = 1; // 파일 업로드가 성공적으로 이루어졌는지 여부를 나타내는 변수
// 파일 유형 검사 (예시: 이미지 파일만 업로드 가능하도록 제한)
$imageFileType = strtolower(pathinfo($targetFile,PATHINFO_EXTENSION)); // 확장자 추출 및 소문자 변환
if($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg" && $imageFileType != "gif" ) {
echo "Only JPG, JPEG, PNG & GIF files are allowed.";
$uploadOk = 0;
}
// 파일 업로드 실행여부 검사
if ($uploadOk == 0) {
echo "<script> alert('error [0]');
window.history.back();
</script>";
}
// 업로드될 파일의 정보를 MySQL 데이터베이스에 저장하는 코드 작성
$filename = basename($_FILES["fileToUpload"]["name"]);
$filesize = $_FILES["fileToUpload"]["size"];
$uploader = $_SESSION["name"];
$boardNO = "1";
$contentNO = $id; //upload_content_1.php에서 정의
if(strlen($filename) >= "25"){
echo "<script> alert('파일 이름의 길이는 확장자명 포함 25자 이내여야 합니다.');
window.history.back();
</script>";
}
// MySQL 데이터베이스 연결 및 파일 정보 저장
require_once "tool/db_conn.php";
if ($con->connect_error) {
die("Connection failed: " . $con->connect_error);
}
// 파일 정보를 데이터베이스 테이블에 삽입하는 SQL 쿼리 실행
$sql = "INSERT INTO board1_file (filename, filesize, uploader, uploadDate, boardNO, contentNO) VALUES (?, ?, ?, NOW(), ?, ?)";
$stmts = $con -> prepare($sql);
$stmts -> bind_param('sssii', $filename, $filesize, $uploader, $boardNO, $contentNO);
if ($stmts -> execute()) {
$file_id = mysqli_insert_id($con); // AUTO_INCREMENT로 생성된 primary key 값을 가져옴
$saveFile = $targetDir . $file_id . "." . pathinfo($targetFile,PATHINFO_EXTENSION); //저장될 파일명 및 디렉토리
} else {
echo "<script> alert('error [0]');
window.history.back();
</script>";
}
// MySQL 연결 종료
$con->close();
//파일 업로드 실행
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $saveFile)) {
} else {
// 파일 업로드 실패
echo "<script> alert('파일 업로드에 실패하였습니다.');</script>";
}
}
?>
서버에서 실행이 가능한 파일이 무엇인지를 확인해야 한다. 소스코드를 보면 파일의 확장자명을 소스코드 기반 필터링을 하고 있다.
확장자명을 가져오는 방법은 pathinfo() 함수를 사용하고 있다. 이 함수의 경우 작동 원리가 맨 뒤의 점으로 부터 그 이후의 문자를 확장자명으로 가져오는 것이다. 예를들어 1.2.3.4.56.78입력시 .78이 확장자명이 되게 된다. 이에 더해서 strtolower()함수를 사용해서 확장자명의 대소문자를 모두 소문자로 변환시켜 저장하고 있다.
따라서 다음과 같은 우회방법은 불가능하다.
1. PhP와같이 대소문자 혼합
2. jpg.php와 같은 이중확장자
3. phtml과 같은 다른 확장자의 사용
고려하지못한 우회 방법은 %00(null byte)사용을 통한 확장자명 우회이다.
따라서 다음 코드를 추가하여 필터링을 진행하였다.
// NULL byte 탐지
if(strpos($targetfile, "\0")){
$uploadOk = 0; //0일경우 업로드 진행 안함
}
저장 경로 변경 관련 보안책이다. 우선 알아야하는것이 파일 저장 경로는 실행이 제한되어있는 디렉토리이다. 따라서 공격자는 이 디렉토리를 벗어나는 경로순회 공격이 필요하다. "../"과 같이 다른 디렉토리로 이동을 하는 문자에 대한 유효성 검사 혹은 필터링이 존재하지 않는다. 따라서 파일명을 그대로 저장할 경우 원하지 않는 디렉토리의 저장이 발생 할 가능성이 존재한다.
다만 위 소스코드를 보게 되면 원래의 파일명은 DB에 저장이 되게 되고, id값으로 파일이 저장되게 된다.
아래는 다운로드 페이지의 소스코드이다.
<?php
$file_id = $_GET['file_id'];
//db 연결
require_once "tool/db_conn.php";
// require_once 'tool/chack_er.php';
$file_query = "SELECT * FROM board1_file WHERE id=?";
$stmt = $con -> prepare($file_query);
$stmt -> bind_param('i', $file_id);
$stmt -> execute();
$file_result = $stmt -> get_result();
$file_row = $file_result -> fetch_assoc();
// 다운로드할 파일의 경로 및 파일명
$pathinfo = pathinfo($file_row['filename'],PATHINFO_EXTENSION);
$filePath = "uploads/{$file_id}.{$pathinfo}";
// 파일이 존재하는지 확인
if (file_exists($filePath)) {
// 다운로드할 파일의 정보 설정
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $file_row['filename']);
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filePath));
// 파일을 읽고 출력 버퍼에 쓰기
readfile($filePath);
// 파일 다운로드 후 종료
exit;
} else {
// 파일이 존재하지 않는 경우 처리할 내용
echo "<script> alert('파일을 찾을 수 없습니다.');
window.history.back();
</script>";
}
?>
위 코드를 보면 파일 id를 통해 데이터를 가져와 id로 저장한 파일명을 다시 원래의 파일명으로 되돌려 다운로드가 가능하도록 하고 있다. 따라서 파일명에 경로 순회 공격을 가하더라도 저장되는 결과는 5.php로 실행권한이 없는 디렉토리에 저장되므로 파일이 실행되지 않는다.
다운로드 소스코드에서 한가지 문제점을 더 발견하였다. file id값을 get으로 받고 있다. 이는 다운로드의 요청/응답이나 소스코드를 확인 할 경우 저장되는 파일명을 사용자가 알 수 있다. 이를 모르게 하기 위해서는 게시글 번호를 이용하여 SQL문을 작성하고 데이터를 가져올 필요가 있다.
따라서 다음과 같이 수정하였다.
$board_id = $_GET['board_id'];
$boardNO="1";
//db 연결
require_once "tool/db_conn.php";
// require_once 'tool/chack_er.php';
$file_query = "SELECT * FROM board1_file WHERE boardNO=? AND contentNO=?";
$stmt = $con -> prepare($file_query);
$stmt -> bind_param('ii', $boardNO, $board_id);
$stmt -> execute();
$file_result = $stmt -> get_result();
$file_row = $file_result -> fetch_assoc();
위의 코드의 경우 데이터를 검색하는 소스가 게시글의 id이므로 파일이 어떠한 이름으로 저장되는지(파일id가 무엇인지) 알아내기 위해서는 SQL Injection이나 DB탈취 등의 공격이 필요해졌다.
'프로그래밍 및 코딩 > PHP' 카테고리의 다른 글
DB에 파일 업로드 및 다운로드 (CLOB/BLOB) (0) | 2023.06.14 |
---|---|
서버시간 가져오기 (0) | 2023.06.14 |
비관계형 데이터베이스 (0) | 2023.05.31 |
케빈 미트닉 해킹기법 (0) | 2023.05.30 |
게시판 만들기_파일 다운로드 (0) | 2023.05.30 |