배포할 것이 있어 서버에 접속했다. 업로드 폴더에 못 보던 파일이 있었다. 이름이 그럴듯해서 이미지들 사이에 섞이면 눈에 안 띈다. 끝이 .php 라 열어 봤다.
Table of contents
Open Table of contents
증상 — 브라우저에서만 막고 있었다
내용이 알아볼 수 없게 되어 있었다.
<?php eval(base64_decode("aWYoaXNzZXQoJF9QT1NUWy...")); ?>
base64_decode 로 문자열을 되돌리고 eval 로 실행한다. 요청으로 받은 것을 서버에서 그대로 돌리는 코드다.
먼저 어떻게 들어왔는지부터 봐야 했다. 지워도 다시 들어오면 소용없기 때문이다.
$name = $_FILES['file']['name'];
move_uploaded_file($_FILES['file']['tmp_name'], "upload/" . $name);
보내온 이름을 그대로 쓰고 확장자를 확인하는 코드가 없었다. 화면에서는 이미지만 고를 수 있었다. 그건 브라우저가 하는 일이고 서버는 무엇이 오든 받는다.
화면에서 막는 것은 사람이 화면을 쓸 때만 유효하다. 요청을 직접 만들어 보내면 그 검사를 지나간다. move_uploaded_file 을 부르는 자리에 검사가 있어야 했다.
확장자 검사만으로는 부족했다
먼저 서버에서 확장자를 확인하게 했다.
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
// 거부
}
그런데 이것만으로는 부족했다. 실행되는 확장자가 .php 하나가 아니라 .phtml 과 .php5 도 있었다.
더 걸리는 것이 있었다. AddHandler 로 .php 를 걸어 두면 그 확장자가 이름 끝이 아니어도 적용된다. 매뉴얼도 확장자가 여럿 붙으면 대개 핸들러 쪽이 이긴다고 적어 뒀다.
그러니 shell.php.jpg 는 끝이 .jpg 여도 PHP 로 실행된다. 마지막 조각만 보는 검사는 그것을 통과시킨다. 끝만 보게 하려면 AddHandler 대신 <FilesMatch \.php$> 와 SetHandler 를 쓰라고 매뉴얼이 안내한다.
# 업로드 폴더의 .htaccess
php_flag engine off
그래서 업로드 폴더에서 실행 자체를 껐다. 이게 제일 확실했다. 앞의 검사를 뚫고 들어와도 실행이 안 되면 그냥 파일이다.
이름과 내용이라는 두 자리
$_FILES 로 들어온 이름 대신 우리가 만든 이름으로 저장하게 바꿨다. 보내온 이름을 안 쓰면 .php 가 들어올 방법이 없다. 원본 이름은 따로 담아 보여줄 때만 쓴다.
내용도 봤다. 확장자가 .jpg 인데 이미지가 아닐 수 있어 getimagesize 로 확인했다. finfo_open(FILEINFO_MIME_TYPE) 으로 형식을 읽으면 확장자와 내용이 다른 것을 거른다.
다만 이미지 파일 안에 코드를 넣는 방법도 있다. 이미지로서도 유효하고 코드도 들어 있으면 getimagesize 도 통과한다.
그래서 확장자 확인만으로는 부족하고 실행을 막는 것이 확실하다는 결론이 됐다. 이름과 내용과 실행 세 곳을 다 막고서야 안심이 됐다. 여러 겹을 두는 이유를 이때 알았다.
지우는 것보다 경로가 먼저였다
발견한 파일을 바로 지우려다 멈췄다. 지워도 경로가 열려 있으면 다시 들어온다. 경로를 막은 다음에 지우는 순서가 맞았다.
다 하고 나서도 마음이 편하지 않았다. 언제 들어왔는지 정확히 모르고 무엇을 했는지도 모르며 다른 것이 더 있을 수도 있었다.
access_log 가 며칠 치밖에 없었다. logrotate 가 짧게 잡혀 그 전 것은 지워진 뒤였다. DB 접속 정보는 서버 안을 보면 알 수 있으니 최악을 가정해야 했다.
결국 비밀번호를 전부 바꿨다. 확실하지 않을 때 할 수 있는 것이 그것뿐이었다. 로그를 얼마나 보관할지도 정해 둬야 할 일로 남았다.
정리
- 화면에서 막는 것은 막는 것이 아니다. 서버는 무엇이 오든 받는다
- 검사는
move_uploaded_file을 부르는 자리에 있어야 한다 - 실행되는 확장자는
.php말고.phtml과.php5도 있다 AddHandler로 건 확장자는 이름 끝이 아니어도 적용된다- 그래서
shell.php.jpg가 PHP 로 실행된다 - 끝만 보게 하려면
FilesMatch와SetHandler를 쓴다 - 업로드 폴더에
php_flag engine off로 실행을 끈다 $_FILES의 이름을 그대로 쓰지 않고 새로 만들어 붙인다getimagesize와finfo_open으로 내용을 확인한다- 지우는 것보다 들어온 경로를 막는 것이 먼저다
logrotate가 짧으면 새어 나간 것을 확인할 수 없다