서버 요청 298만 건 중 36%가 404, 그 절반은 .git 폴더를 뒤지는 요청이었고 4,917번은 실제로 파일을 내줬다

남의 서버에서 .git 폴더부터 찾는 사람들

워드프레스가 도는 서버의 접속 로그를 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 응답 1,065,189건을 경로 모양으로 나눈 막대그래프. .git 폴더 492,189건, .php 파일 261,248건, .env 파일 67,579건, 그 밖 244,173건
404의 46%가 /.git/ 아래 경로였고, 없는 PHP 파일이 그다음입니다.

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_log 5개 파일 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건은 그 ‘어디’가 워드프레스가 아니라 옆 사이트의 저장소 폴더일 수 있다는 것을 보여 줍니다. 워드프레스만 잠가도 같은 서버의 다른 문서 루트가 열려 있으면 의미가 없습니다.

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다