워드프레스 보안
재설치 없이 해킹된 워드프레스를 살린 순서: mu-plugins 로더부터
세 번 감염된 가족 블로그를 재설치하지 않고 격리, 코어 원본 교체, mu-plugins 백도어 제거, 가짜 관리자 삭제, 솔트 재생성 순서로 정리해 글 79개와 첨부 2,443개를 지킨 기록입니다.
가족 블로그로 쓰던 워드프레스가 해킹돼 있었습니다. 500 에러를 따라가다 발견했는데(그 과정은 따로 적었습니다), 파 보니 2020년, 2026년 2월, 2026년 7월 세 번에 걸친 침투 흔적이 있었습니다.
이런 경우 보통 권하는 건 "새로 설치하고 글만 옮겨라"입니다. 근데 이 블로그는 글 79개, 첨부 파일 2,443개에 가족 사진이 잔뜩 들어 있어서 재설치 대신 악성 코드만 걷어내는 쪽을 택했습니다. 이 글은 그 순서입니다. 순서가 중요해서, 제가 한 그대로 적습니다.
1. 손대기 전에 통째로 떠 둡니다
제일 먼저 웹 루트 전체와 DB를 백업했습니다. 악성 파일도 그대로 들어 있는 백업이지만, 나중에 "이 파일 원래 있던 거였나?"를 확인하려면 감염 상태 그대로의 사본이 필요합니다.
tar czf www_full.tgz -C /volume2/web/family-blog www # 985MB
mysqldump --single-transaction family_db | gzip > family_db.sql.gz
경로와 DB 이름은 바꿔 적었습니다. 백업은 웹 루트 밖, 웹에서 접근할 수 없는 곳에 둡니다.
2. 격리 — 지우지 말고 옮깁니다
의심 파일은 삭제하지 않고 quarantine/ 폴더로 옮겼습니다. 이때 옮긴 것들입니다.
| 위치 | 내용 |
|---|---|
wp-content/uploads/ | 파일 업로더, phpMiniAdmin, 무작위 이름 빈 PHP, class-wp-cache-9a8e2a14.php 웹셸 |
wp-content/plugins/ | 가짜 플러그인 galex_13c3697f, security-headers-manager-a49be6, wordpress-cache-optimizer, wp-content-filter |
wp-content/themes/ | 가짜 테마 flavor_f697306b, 진짜 테마 안의 huqqkdmf.php, uaojkfle.php |
wp-content/mu-plugins/ | galex_patch.php(658바이트), wp-core-update-52499f.php(307바이트) |
| 그 밖 | ectoplasm/ectoplasm/, src/src/, files/files/, images/images/처럼 진짜 폴더 이름을 한 번 더 겹쳐 만든 드로퍼 폴더 |
격리한 파일은 4,321개였습니다. 대부분은 아래 3단계에서 통째로 교체한 옛 플러그인 폴더라, 악성 파일 자체가 이만큼이라는 뜻은 아닙니다.
3. mu-plugins를 먼저 봐야 하는 이유
wp-content/mu-plugins/는 "반드시 쓰는 플러그인(Must-Use)" 폴더입니다. 여기 넣은 PHP 파일은 관리자 화면의 플러그인 목록에서 끌 수도 없고, 워드프레스가 요청마다 자동으로 불러옵니다. 공격자한테는 가장 좋은 자리입니다.
여기서 나온 두 파일은 웹셸 자체가 아니라 다시 살아나게 하는 장치였습니다.
galex_patch.php—register_rest_route로 REST API에/batch/v1경로를 몰래 추가합니다. 워드프레스 코어에도/batch/v1이라는 정상 경로가 있어서 로그에서 봐도 눈에 잘 안 띕니다.wp-core-update-52499f.php— 파일을 복사해 쓰는 코드가 들어 있는 307바이트짜리 파일입니다. 웹셸을 지워도 다음 요청 때 다시 깔아 주는 역할입니다.
웹셸만 지우고 이 두 개를 남겨 두면, 다음 방문 한 번에 원상 복구됩니다. 그래서 mu-plugins는 제일 먼저, 파일 이름이 그럴듯해도 전부 열어 봐야 합니다. 이 블로그에서는 mu-plugins 안에 있던 두 파일이 전부 공격자 것이었습니다.
4. 코어는 고치지 말고 통째로 바꿉니다
감염된 코어 파일을 하나하나 비교해 고치는 건 놓치기 쉽습니다. wp-admin, wp-includes, 루트의 코어 PHP를 wordpress.org에서 받은 같은 버전(당시 6.9.1)으로 통째로 바꿨습니다. 받은 파일은 wordpress.org가 공개하는 SHA1과 대조한 뒤에 썼습니다.
curl -O https://wordpress.org/wordpress-6.9.1.tar.gz
curl -O https://wordpress.org/wordpress-6.9.1.tar.gz.sha1
echo "$(cat wordpress-6.9.1.tar.gz.sha1) wordpress-6.9.1.tar.gz" | sha1sum -c -
wp-content와 wp-config.php는 코어가 아니라서 건드리지 않았습니다. 플러그인 8개(Rank Math, Site Kit, Autoptimize 등)도 같은 방식으로 wordpress.org 최신본으로 새로 받아 교체했습니다. 받을 수 있는 원본이 있으면 "고치기"보다 "갈아 끼우기"가 확실합니다.
5. DB 안의 흔적을 지웁니다
파일만큼 중요한 게 DB입니다. 확인하고 고친 순서입니다.
- 가짜 관리자 삭제.
wpenginebot,wphiddenbot두 계정이 관리자 권한으로 있었습니다. 호스팅 업체 봇처럼 보이게 지은 이름입니다. active_plugins정리. 폴더를 지운 가짜 플러그인이 활성 목록에 남아 있으면 워드프레스가 계속 찾으려 합니다.wp_options의 이 값에서 빼 줬습니다.- 모든 로그인 세션 끊기. 공격자가 가지고 있을지 모를 로그인 쿠키를 무효로 만들려고 전체 사용자의 세션을 끊었습니다. 워드프레스는 세션을 사용자 메타의
session_tokens에 저장합니다. - 보안 키(솔트) 재생성.
wp-config.php의AUTH_KEY등 8개 값을 새로 만들어 넣었습니다. wordpress.org 생성기를 쓰면 한 번에 받을 수 있습니다. 이것만 해도 기존 로그인 쿠키는 전부 무효가 됩니다.
그리고 공격자가 wp-config.php를 읽었다고 봐야 하니 DB 비밀번호도 바꿨습니다. 문제는 이 NAS의 워드프레스 여러 개가 DB 계정 하나를 같이 쓰고 있었다는 겁니다. 비밀번호 하나 바꾸는 데 wp-config.php 11개를 고쳐야 했습니다. 사이트마다 DB 계정을 따로 두는 게 맞습니다.
6. .htaccess를 새로 씁니다
마지막으로 500의 원인이던 .htaccess를 Apache 2.4 문법으로 다시 썼습니다. 워드프레스 기본 규칙에 두 가지를 더했습니다.
# xmlrpc.php 막기
<Files "xmlrpc.php">
Require all denied
</Files>
그리고 wp-content/uploads/.htaccess에 PHP 실행을 막는 규칙을 넣었습니다. 이번에 나온 업로더와 웹셸이 uploads 안에 있었기 때문입니다.
<FilesMatch "\.(php|phtml|php[0-9]|phar|suspected)$">
Require all denied
</FilesMatch>
결과와 남은 것
사이트는 200을 돌려주고 제목도 정상으로 나옵니다. 코어 파일은 wordpress.org 체크섬과 일치하고, 글 79개와 첨부 2,443개, 쓰던 테마는 손상 없이 그대로입니다.
하나 타협한 게 있습니다. 정리하면서 코어 파일 소유자를 웹 서버가 못 쓰게 바꿨더니, 이번엔 워드프레스 자동 업데이트가 멈췄습니다. 보안 업데이트를 못 받는 게 더 위험하다고 보고 권한을 되돌렸습니다. 그 얘기는 따로 정리했습니다. 대신 코어가 바뀌면 바로 알 수 있게 체크섬 점검기를 매시간 돌리고 있습니다.
확인한 공식 자료
구현 과정에서 판단 기준을 교차 확인한 공식 문서입니다. 글의 사례와 결론은 운영자가 직접 겪은 작업을 바탕으로 작성했습니다.