서버가 이상하다는 얘기가 나와서 access_log 를 보다가 못 보던 요청을 발견했다. 업로드 폴더 아래의 .php 를 부르고 있었다.
Table of contents
Open Table of contents
증상 — 업로드 폴더에 스크립트가 있었다
그 폴더에는 사용자가 올린 이미지나 문서가 들어간다. 실행되는 파일이 있을 자리가 아니다.
파일을 열어 보니 압축되고 인코딩돼 있어서 그대로는 읽을 수 없고 한 줄이 아주 길었다. 디코딩해서 일부를 보니 파일 목록을 보고 읽고 쓰고 명령을 실행하고 DB 에 접속하는 기능이 들어 있었다.
서버를 조작하는 도구였다. 어떻게 올라왔는지부터 봐야 했다.
확장자만 검사하고 있었다
업로드 처리 코드에서 move_uploaded_file 앞을 열었다.
$ext = end(explode('.', $_FILES['file']['name']));
if (in_array($ext, ['jpg', 'png', 'gif'])) {
move_uploaded_file(...);
}
explode 로 자른 뒤 end 가 집는 마지막 조각만 in_array 로 대조하고 내용은 안 본다. 이름을 바꾸면 통과한다는 뜻이다.
찾아보니 속이는 방법이 몇 가지 있었다. 확장자를 둘로 두면 서버 설정에 따라 앞의 것으로 실행되는 경우가 있고 목록에 소문자만 적혀 있으면 대문자로 올려서 빠져나갈 수 있으며 이름 중간에 특수 문자를 넣어 뒤를 잘라내는 방법도 있었다.
하나만 막으면 다른 것으로 들어온다. 막을 것을 나열하는 방식 자체가 빠진 것을 만들어 낸다.
세 층으로 막았다
이름을 검사해서 거르는 방향 대신 아예 다르게 하기로 했다. 먼저 올린 이름을 안 쓰고 우리가 새로 짓는다.
$newName = uniqid() . '.jpg';
확장자도 우리가 판단한 것으로 붙인다. 그다음 이름이 아니라 내용을 본다.
if (getimagesize($tmp) === false) {
// 이미지가 아님
}
이미지로 열리는지 실제로 확인해서 getimagesize 가 false 를 주면 거른다. finfo_open 으로 형식을 읽어도 같은 판정이 된다.
마지막으로 업로드 폴더에서 스크립트가 아예 안 돌게 .htaccess 에 서버 설정을 넣었다. 이러면 올라와도 실행이 안 된다.
셋이 다른 자리에서 막는다는 것이 요점이다. 이름 층에서 한 번 내용 층에서 한 번 실행 권한 층에서 한 번이라 하나가 뚫려도 다음이 막는다.
셋 중에는 실행 차단이 가장 확실해 보였다. 앞의 둘은 우회할 방법이 있을 수 있는데 실행 자체가 안 되면 우회할 것이 없다.
그래도 앞의 둘을 뺄 수는 없었다. 설정 파일이 바뀌거나 다른 서버로 옮길 때 빠질 수 있어서 마지막 층만 믿으면 그게 없어지는 순간 뚫린다.
이미 올라온 것과 백업
새로 막는 것과 별개로 이미 올라와 있는 것을 찾아야 했다.
$ find uploads/ -type f -name '*.php'
여러 개가 나왔다. find 에 -newermt 를 붙여 시각을 좁혀 보니 만들어진 때가 흩어져 있어서 한 번이 아니라 여러 번 올라온 것이었다.
access_log 에서 그 시각 근처를 보니 업로드 요청이 있고 그 뒤에 그 파일을 부르는 요청이 이어져 있었다.
다만 그 파일을 통해 무엇을 했는지는 로그로 알 수 없었다. 요청 줄은 남지만 POST 본문은 안 남는다.
무엇을 당했는지 모르니 다른 파일이 바뀌었는지 계정이 늘었는지 DB 에 이상한 것이 있는지를 시각을 기준으로 넓게 훑어야 했다.
$ find . -type f -exec md5sum {} \; | sort > /tmp/now.txt
$ diff /tmp/backup.txt /tmp/now.txt
양쪽에서 md5sum 목록을 뽑아 sort 한 뒤 diff 로 비교하니 바뀐 파일 목록이 나왔다.
백업을 복구용으로만 생각했는데 무엇이 바뀌었는지 가르는 대조 기준으로도 쓰였다. 백업이 없었으면 전부 의심해야 했을 것이다.
그 뒤로 백업을 두 곳에 뒀다. 같은 서버에 두면 빠른 대신 서버가 당할 때 백업도 같이 바뀔 수 있어서 침입 상황에서는 다른 곳에 있는 것이 필요하다. md5sum 목록을 미리 만들어 두고 정기적으로 md5sum -c 로 대조하는 것도 넣었다.
정리
- 업로드 폴더에 실행되는 파일이 있으면 안 된다
- 확장자만 검사하면 이중 확장자나 대소문자나 특수 문자로 통과한다
- 막을 것을 나열하는 방식은 빠진 것을 만든다
- 이름을 우리가 짓고 내용을 확인하고 실행을 막는 세 층으로 간다
- 셋이 다른 층이라 하나가 뚫려도 다음이 막는다
- 실행 차단이 가장 확실한데 설정이 빠질 수 있어 앞의 둘도 남긴다
- 새로 막는 것과 별개로 이미 올라온 것을 찾아야 한다
find -newermt로 시각을 좁혀 언제 들어왔는지 본다- 로그에 요청 줄은 남지만
POST본문은 안 남는다 md5sum목록을 백업과diff하면 무엇이 바뀌었는지 나온다- 같은 서버의 백업은 함께 바뀔 수 있어 다른 곳에도 둔다