서버 요청 298만 건 중 36%가 404, 그 절반은 .git 폴더를 뒤지는 요청이었고 4,917번은 실제로 파일을 내줬다
워드프레스가 도는 서버의 접속 로그를 35일치(8월 9일~9월 12일) 모아 응답 코드별로 세어 봤습니다. 297만 7,553건 가운데 404가 106만 5,189건, 35.8%입니다. 정상 응답 200(49.2%)에 이어 두 번째로 많은 코드가 “없는 페이지”입니다. 그런데 이 404를 경로별로 나누자 절반에 가까운 49만 2,189건이 한 폴더를 향하고 있었습니다. /.git/입니다. 그리고 그 요청 가운데 4,917건은 404가 아니라 200이었습니다. 실제로 파일을 내준 것입니다. 무엇을 세었고, 무엇을 막았고, 무엇을 그대로 뒀는지 적습니다.
404 106만 건은 넷으로 나뉜다
이 서버는 워드프레스 사이트 3개 외에 다른 사이트도 여러 개 올려 두고 있고, 접속 로그는 한 파일에 섞여 쌓입니다. 그래서 아래 숫자는 서버 전체 값이며, 사이트 하나의 값이 아닙니다. 응답 코드 404인 요청을 경로 모양으로 나누면 다음과 같습니다.
| 경로 모양 | 건수 | 404 중 비율 | 대표 경로 |
|---|---|---|---|
/.git/로 시작 |
492,189 | 46.2% | /.git/objects/3e/…, /.git/logs/refs/… |
.php가 들어감 |
261,248 | 24.5% | /admin.php, /phpinfo.php, /1.php |
.env가 들어감 |
67,579 | 6.3% | /.env, /.env.production, /api/.env |
| 그 밖 | 244,173 | 22.9% | /wp-json/batch/v1, /wp/, /.aws/credentials |
| 합계 | 1,065,189 | 100% | 전체 요청의 35.8% |

404를 받은 IP는 16,009개이고, 그중 6,852개(42.8%)는 35일 동안 404 외의 응답을 한 번도 받지 않았습니다. 존재하는 페이지는 하나도 열지 않고 없는 경로만 두드리다 간 주소입니다. 404 응답에 나간 전송량은 1.02GB로 전체 22.47GB의 4.5%입니다. 건수로는 3분의 1이지만 바이트로는 작습니다. 이유는 뒤에 나옵니다.
.git 요청 49만 건, 89%가 도구 하나
/.git/ 아래를 요청한 건수는 404 외의 응답까지 합쳐 497,946건, 서버 전체 요청의 16.7%입니다. 여섯 건 중 하나가 소스 저장소 폴더를 찾는 요청이었습니다. 경로를 한 단계 더 나누면 /.git/objects/가 404,775건, /.git/refs/ 42,108건, /.git/logs/ 40,371건입니다. 객체 해시(objects/3e/5c… 꼴)는 서로 다른 것이 3,350개였습니다. 저장소를 통째로 내려받으려는 순서입니다.
User-Agent를 세어 보면 Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0 하나가 443,642건, 89.1%입니다. 2020년에 나온 파이어폭스 78 문자열을 그대로 박아 둔 도구가 IP를 바꿔 가며 돌린 것으로 보입니다. 두 번째는 git/2.40.0 28,448건인데, 이쪽은 위장 없이 git 명령 자체가 HTTP로 저장소를 긁은 흔적입니다. IP는 1,754개였고 상위 1개가 11.6%, 상위 10개가 51.5%를 차지했습니다. 가장 몰린 순간은 8월 12일 01시 11분의 7,308건, 초당 122건입니다.
4,917번은 200이었다
여기까지는 다른 서버에서도 흔히 보는 소음입니다. 문제는 응답 코드 분포입니다.
| 응답 | 건수 | 뜻 |
|---|---|---|
| 404 | 492,189 | 파일 없음 |
| 200 | 4,917 | 파일을 내줌 |
| 301 | 489 | https로 이동 |
| 403 | 335 | 거부 |
| 500 | 15 | 서버 오류 |
200이 나온 경로는 /.git/config 1,020건, /.git/HEAD 563건, /.git/refs/heads/master 226건, /.git/packed-refs 196건, /.git/logs/HEAD 193건, /.git/index 190건 순입니다. 200으로 나간 바이트를 합치면 64.4MB이고, 그중 /.git/index는 한 번에 338KB짜리였습니다. index 파일에는 저장소에 든 파일 이름 전부가 들어 있으니, 이것을 받아 가면 문서 루트에 어떤 파일이 있는지 목록을 손에 넣은 셈입니다.
어느 사이트가 내줬는지는 로그만으로는 알 수 없어서(로그에 호스트명이 없습니다) 직접 확인했습니다. 문서 루트 아래 .git 폴더가 있는 사이트가 9개였고, 그중 4개에 /.git/config를 요청하니 200과 266바이트가 돌아왔습니다. 워드프레스 사이트 3개에는 .git이 없어서 해당하지 않았습니다. 워드프레스가 아니라 옆에 올려 둔 다른 사이트가 구멍이었고, 스캐너는 호스트를 가리지 않고 같은 서버의 모든 이름에 같은 경로를 넣어 봅니다.
없는 PHP 파일 26만 건, 워드프레스 이름은 19%뿐
두 번째로 많은 404는 .php 파일입니다. 서로 다른 경로가 14,161개, IP가 4,114개였습니다. 파일 이름만 떼어 세면 다음과 같습니다.
| 파일 이름 | 건수 | 보통 뜻 |
|---|---|---|
index.php |
15,341 | /wp/, /blog/, /wordpress/ 등 설치 폴더 추측 |
phpinfo.php |
9,417 | 서버 정보 노출 여부 |
about.php |
5,213 | 업로드된 웹셸 이름 |
admin.php |
5,082 | 웹셸 또는 관리 화면 |
info.php |
3,566 | phpinfo 변형 |
wp-login.php |
2,037 | 다른 폴더의 워드프레스 로그인 |
wp_filemanager.php |
1,589 | 플러그인으로 위장한 파일 관리기 |
eval-stdin.php |
1,433 | PHPUnit 취약점(CVE-2017-9841) |
경로에 wp-나 wordpress가 들어간 것은 261,248건 중 50,701건, 19.4%입니다. 나머지 80%는 1.php, 222.php, k.php, 3PJcpMFsD8B.php처럼 워드프레스와 무관한 이름입니다. 누군가 이미 심어 둔 웹셸이 있는지 이름 목록을 돌려 보는 것입니다. 이 요청의 61.4%는 User-Agent가 비어 있었습니다. 브라우저도 검색 봇도 빈 UA를 보내지 않으니 이 값 하나로도 걸러집니다.
없는 .php를 요청하면 아파치는 파일이 없다는 것을 모르고 PHP-FPM에 넘기고, PHP-FPM이 Primary script unknown이라는 줄을 에러 로그에 남기며 404를 돌려줍니다. 같은 기간 에러 로그 197,501줄 중 188,834줄(95.6%)이 이 한 문장이었습니다. 에러 로그를 열었을 때 진짜 오류가 보이지 않는 이유가 이것입니다.
같은 404인데 값이 다르다: 워드프레스가 만드는 404는 75ms
404 106만 건이 서버에 얼마나 부담을 줬는지는 경로 모양에 따라 다릅니다. 워드프레스 사이트 하나(kilho.org)에 없는 경로 세 종류를 5번씩 요청해 평균을 냈습니다.
| 요청 | 응답 | 크기 | 시간 | 누가 처리했나 |
|---|---|---|---|---|
/zz-none.php |
404 | 16B | 17ms | PHP-FPM이 파일 없음 반환 |
/wp-includes/ID3/ |
403 | 199B | 20ms | 아파치(디렉터리 목록 금지) |
/zz-none.txt |
404 | 55,096B | 75ms | 워드프레스가 404 페이지를 렌더링 |
/.git/config (고치기 전) |
404 | 55,096B | 73ms | 워드프레스 |
/ (비교용 홈) |
200 | 103,403B | 149ms | 워드프레스 |
왜 워드프레스가 .git/objects 요청을 받나
워드프레스의 .htaccess 규칙은 “파일도 폴더도 아니면 index.php로 보내라”입니다. /.git/objects/3e/5c…는 파일도 폴더도 아니니 워드프레스로 넘어가고, 워드프레스는 테마를 불러 404 페이지 전체(55KB)를 만든 뒤 돌려줍니다. 홈 화면의 절반 값입니다. 반면 .php로 끝나는 요청은 아파치가 PHP-FPM으로 곧장 넘기므로 워드프레스가 뜨지 않고, 16바이트로 끝납니다. 즉 이 서버에서 가장 싼 404는 없는 PHP 파일이고, 가장 비싼 404는 워드프레스 사이트에 들어온 없는 텍스트 경로입니다.
로그의 응답 크기로 역산하면 404 중 1KB 이상은 41,867건(3.9%)이고 나머지 96.1%는 1KB 미만입니다. 워드프레스 404 페이지는 압축 후에도 1KB를 넘으니, 106만 건 중 워드프레스까지 도달해 페이지를 만든 것은 약 4만 건입니다. 75ms씩 잡아도 35일 동안 52분 남짓입니다. 부담이라기보다 낭비이고, 이번 조치로 이 낭비는 0에 가까워집니다.
막은 것과 막지 않은 것
이 글 이전에 서버 설정은 손대지 않았습니다. 이번에는 하나를 고쳤습니다. 이유와 결과를 표로 남깁니다.
| 항목 | 조치 | 이유 |
|---|---|---|
.git·.svn·.hg 폴더 |
서버 전체에서 거부(403) | 4개 사이트가 .git/config를 내주고 있었음. 저장소 내용은 어떤 사이트에서도 공개할 이유가 없음 |
.env 파일 |
서버 전체에서 거부(403) | 한 사이트에 실제 .env가 있었음(요청 시 500이 났지만 내용은 안 나갔음). 같은 규칙 한 줄로 해결 |
없는 .php 요청 |
그대로 둠 | 이미 16바이트·17ms로 가장 싼 응답. 이름 목록을 막아도 다음 이름이 옴 |
| 빈 User-Agent 차단 | 그대로 둠 | 404의 16%를 줄일 수 있지만, 자체 스크립트(크론 호출 등)까지 걸릴 수 있어 보류 |
| IP 차단(fail2ban) | 그대로 둠 | IP 1,754개가 1개당 평균 284건. 상위 10개를 막아도 절반이 남음 |
추가한 설정은 이것이 전부입니다. DirectoryMatch는 폴더 안 모든 파일에, FilesMatch는 문서 루트에 바로 놓인 .env나 .gitignore 같은 낱개 파일에 걸립니다.
<DirectoryMatch "/.(git|svn|hg)(/|$)">
Require all denied
</DirectoryMatch>
<FilesMatch "^.(env|git|svn|hg)">
Require all denied
</FilesMatch>
적용 후 같은 요청을 다시 보내니 .git/config가 200 → 403, .env가 500 → 403으로 바뀌었고, 워드프레스 사이트에서는 /.git/config가 55KB·73ms짜리 404 페이지에서 199바이트·17ms짜리 403으로 바뀌었습니다. 아파치가 워드프레스보다 먼저 끊기 때문입니다.
한 가지 덧붙이면, 이 로그에는 앞서 설정 파일 두 개가 겹쳐 같은 요청이 두 줄씩 남던 기간이 포함돼 있습니다. 그 중복분(common 형식 줄)은 제외하고 combined 형식 줄만 세었으므로 위 숫자는 실제 요청 수입니다. 처음에 형식을 가리지 않고 세었을 때는 .git 요청이 95만 건으로 나왔고, 그대로 썼다면 두 배 부풀린 글이 됐을 것입니다.
측정 방법 요약
- 대상:
/var/log/httpd/access_log와 회전 파일 4개, 2026-08-09 ~ 09-12(KST). combined 형식 줄만 정규식으로 파싱해 2,977,553건. - 분류: 응답 코드 404인 줄의 경로를
/.git/시작,.php포함,.env포함, 나머지로 나눔. 파일 이름은 경로의 마지막 조각을 소문자로 통일해 집계. - .git 세부: 응답 코드별 건수, UA·IP 분포, 분 단위 최대치,
objects/xx/hash고유 개수. 200 응답의 바이트 합계는 로그의 전송 크기를 더함. - 에러 로그: 같은 기간
error_log5개 파일 197,501줄에서AH01071 … Primary script unknown건수와AH01276 Cannot serve directory(디렉터리 목록 요청 거부, 4,675건 중 워드프레스 폴더 1,704건)를 셈. - 응답 비용: 서버 안에서
curl --resolve로 kilho.org에 직접 요청, 5회 평균. 외부 캐시를 거치지 않은 원 서버 값. - 노출 확인:
find /home/www -maxdepth 2 -name .git로 9개 확인 후, 4개 호스트명으로/.git/config를 요청해 200·266B 확인. 설정 추가 뒤apachectl configtest,systemctl reload httpd, 같은 요청 재확인.
재감염 경로를 다룬 악성코드를 지워도 다시 생기는 이유에서는 들어온 경로를 찾지 못하면 지워도 다시 감염된다는 점을 다뤘는데, 이번 로그의 .git/index 190건은 그 ‘어디’가 워드프레스가 아니라 옆 사이트의 저장소 폴더일 수 있다는 것을 보여 줍니다. 워드프레스만 잠가도 같은 서버의 다른 문서 루트가 열려 있으면 의미가 없습니다.