WP-CLI와 SQL로 워드프레스 은폐 백도어 찾는 법
٣٠ يوليو ٢٠٢٦
0
AI/미래기술Related Video
5:04이것이 워드프레스 역사상 최악의 해킹일까?
Better Stack
Comments (0)
Log in to leave a comment
No posts yet
5:04Better Stack
Log in to leave a comment
No posts yet
외주로 만든 워드프레스 사이트의 코어 업데이트 버튼을 누르기는 꽤나 망설여집니다. 테마나 커스텀 플러그인이 꼬여서 화면이 하얗게 질릴까 봐 미루다 보면 어느새 사이트는 취약점 투성이가 됩니다. 이미 털렸을지도 모른다는 불안감이 엄습할 때, 가장 빠르게 데이터베이스와 서버를 털어보는 실무 점검 절차입니다.
공격자는 보통 티 나지 않게 들어옵니다. 데이터베이스의 wp_usermeta 테이블을 직접 건드려 기존 계정에 은근슬쩍 관리자 권한을 부여하는 식입니다. 일반적인 로그인 목록에는 잘 안 보이지만 DB를 직접 찌르면 바로 나옵니다.
SELECT
u.ID,
u.user_login,
u.user_email,
u.user_registered,
m.meta_value AS capabilities
FROM
wp_users u
INNER JOIN
wp_usermeta m ON u.ID = m.user_id
WHERE
m.meta_key = 'wp_capabilities'
AND (
m.meta_value LIKE '%"administrator"%'
OR m.meta_value LIKE '%"administrator":true%'
)
ORDER BY
u.user_registered DESC;
그 다음은 파일 시스템입니다. 이미지나 문서만 들어있어야 할 wp-content/uploads 디렉토리에 PHP 파일이 굴러다닌다면 십중팔구 백도어입니다. SSH 터미널을 열고 위험한 파일이 있는지 훑어봅니다.
# 1. uploads 디렉토리 내 PHP 파일 수색
find wp-content/uploads/ -type f -name "*.php" -ls
# 2. 난독화 함수(eval, base64_decode 등)를 품은 파일 감지
find . -type f -name "*.php" -exec grep -HnE "(eval\(|base64_decode\(|gzinflate\(|passthru\(|shell_exec\()" {} \;
# 3. 코어 파일이 변질됐는지 체크섬 검증
wp core verify-checksums --include-root
로그도 봐야 합니다. REST API를 노린 공격은 보통 짧은 시간에 POST 요청을 쏟아내며 200이나 201 응답을 받아냅니다. Nginx access.log에서 수상한 반응을 보인 요청을 뽑아냅니다.
grep -E "POST|PUT" /var/log/nginx/access.log | grep -E "/wp-json/|rest_route=" | awk '$9 ~ /^(200|201|207)$/ {print $1, $4, $6, $7, $9}' | sort | uniq -c | sort -nr | head -n 30
운영 중인 사이트에 바로 패치를 지르는 건 배짱이 아니라 무모함에 가깝습니다. 데이터베이스와 파일 시스템을 통째로 복제한 스테이징 환경을 먼저 만듭니다.
# DB 백업 및 파일 복제
wp db export production_backup.sql --add-drop-table
rsync -avz --exclude='wp-content/cache' /var/www/html/ staging:/var/www/staging/
# 스테이징 서버 접속 후 DB 밀어넣기 및 도메인 교체
wp db import production_backup.sql
wp search-replace 'https://example.com' 'https://staging.example.com' --skip-columns=guid
# 호환성 안 맞는 플러그인 확인
wp plugin list --update=available --fields=name,version,update_version,requires_php --format=table
복제가 끝나면 업데이트를 치고 딱 5분만 주요 기능을 눌러봅니다. 회원가입, 로그인, 장바구니 담기, 결제 창 호출, 문의 양식 제출까지 직접 해봐야 마음이 편합니다.
플러그인 개발사가 업데이트를 안 해줘서 코어를 바로 못 올리는 답답한 상황도 있습니다. 그럴 땐 서버 레벨이나 WAF에서 특정 경로의 접근을 막아두는 걸로 시간을 벌어야 합니다. Nginx를 쓴다면 nginx.conf에 REST API 유저 생성 및 배치 처리 엔드포인트를 내 IP 제외하고 막아버립니다.
location ~* ^/wp-json/(batch/v1|wp/v2/users) {
limit_except GET {
allow 192.0.2.1;
deny all;
}
try_files $uri $uri/ /index.php?$args;
}
혹시 모를 사태를 대비해 DB와 업로드 폴더를 S3로 올려두는 백업 스크립트도 덤으로 걸어둡니다.
#!/bin/bash
WP_PATH="/var/www/html"
BACKUP_DIR="/tmp/wp_backups"
DATE=$(date +%Y%m%d_%H%M%S)
S3_BUCKET="s3://my-wordpress-secure-backups"
mkdir -p $BACKUP_DIR
wp db export $BACKUP_DIR/db_$DATE.sql --path=$WP_PATH --quiet
tar -czf $BACKUP_DIR/files_$DATE.tar.gz -C $WP_PATH wp-content/uploads/
aws s3 cp $BACKUP_DIR/db_$DATE.sql $S3_BUCKET/db_$DATE.sql
aws s3 cp $BACKUP_DIR/files_$DATE.tar.gz $S3_BUCKET/files_$DATE.tar.gz
find $BACKUP_DIR -type f -mtime +7 -delete
악성 코드를 지우고 보안 패치를 마쳤다고 끝이 아닙니다. 공격자가 훔쳐낸 세션 쿠키로 다시 들어올 수 있으니까요. 세션을 털어내고 wp-config.php 솔트 키를 전부 갈아엎습니다.
# 모든 계정의 활성 세션 한번에 날리기
wp user list --field=ID | xargs -n 1 wp user session destroy --all
# 솔트 키 재발급
wp config shuffle-salts
관리자 페이지 안에서 플러그인이나 테마 파일을 직접 수정하는 기능도 끕니다. 공격자가 권한을 잡더라도 코드 주입을 못 하게 막는 최소한의 안전장치입니다. wp-config.php에 아래 두 줄을 넣습니다.
define( 'DISALLOW_FILE_EDIT', true );
define( 'DISALLOW_FILE_MODS', true );
마지막으로 로그인 안 한 사용자가 REST API로 유저 목록을 훔쳐보지 못하게 mu-plugins 디렉토리에 전용 필터를 하나 던져넣습니다.
<?php
/**
* Plugin Name: REST API Security Hardening
*/
add_filter( 'rest_authentication_errors', function( $result ) {
if ( ! empty( $result ) ) return $result;
if ( ! is_user_logged_in() ) {
if ( strpos( $_SERVER['REQUEST_URI'], '/wp/v2/users' ) !== false ) {
return new WP_Error( 'rest_cannot_access', '접근이 거부되었습니다.', array( 'status' => 401 ) );
}
}
return $result;
});
add_filter( 'xmlrpc_enabled', '__return_false' );