Ở Phần 1, tôi đã dọn một website WordPress doanh nghiệp tại Việt Nam sau khi bị đăng 119 bài spam casino và cài 4 tài khoản admin lạ. Cuộc dọn trông như đã xong. Sáng hôm sau, Wordfence lại báo có admin lạ mới, và lần này nguyên nhân là ba lớp: 16 webshell giấu trong thư mục vendor của plugin, hai plugin giả, và một backdoor kích hoạt bằng cookie trong functions.php của theme. Tôi xử lý xong, vá PHP và đổi các bí mật. Chín giờ sau, site bị nhiễm lần thứ ba, qua một plugin tự phục hồi được cài bằng mật khẩu admin bị lộ. Bài này ghi lại cách tìm ra từng lớp, điều gì cuối cùng chặn được vòng lặp, và những kiểm tra nào lẽ ra giúp tìm sớm hơn.
Điều gì quay lại sau lần dọn đầu tiên
Một con bot tạo lại hai tài khoản admin khoảng 2 giờ 15 phút một lần. Chặn IP kẻ tấn công trong Wordfence không thay đổi gì: mười địa chỉ khác nhau gọi thẳng vào file độc hại, và tường lửa miễn phí của Wordfence chỉ chạy khi chính WordPress được nạp, nên một request đi thẳng vào file PHP lạ không bao giờ chạm tới nó.
Không ai biết site bị xâm nhập từ khi nào. Log máy chủ chỉ giữ từ ngày 24/10, nên manh mối đáng tin duy nhất là thứ tự các file bị cài vào.
Lớp 1: 16 webshell nằm trong thư mục vendor của plugin
wp core verify-checksums và wp plugin verify-checksums --all so từng file với bản phát hành chính thức. Bên cạnh các dòng “doesn’t verify”, chúng in ra “File was added”, và đây mới là những dòng quan trọng. Chúng lộ ra 15 file PHP trong thư mục vendor/ của bốn plugin phổ biến (một plugin sao lưu, một page builder, một plugin cache và một plugin bảo mật), cộng thêm một file accept.php lạ ở thư mục gốc của site.
Tên file trông như mã thư viện, ví dụ album_rate.php, admin.modules.php hay qtype_calculated.php. Mỗi file nhận request POST và chạy bất cứ thứ gì bên trong. Thư mục vendor/ là nơi giấu lý tưởng: hàng nghìn file hợp lệ mà không ai mở ra xem.
Lớp 2: hai plugin giả và một file plugin bị sửa
- Một plugin giả “tiện ích lõi” (
wp-core-util) thực chất là máy khách của mạng spam. Nó chèn bài ảo, thêm URL vào sitemap, giấu liên kết ở footer, hiển thị nội dung khác cho Googlebot, và nhận lệnh qua REST API bằng một khoá sinh ra từ chính salts của site. - Một plugin giả “mail debugger” (
wp-mail-debugger) chép phần header của một plugin thật của tác giả có tên tuổi. Mười file.mp3của nó thực ra là script PHP, và nó có hàm tự xoá plugin theo yêu cầu để che dấu vết. - Một plugin lazy-load thật đã bị sửa. 119 dòng được thêm vào file chính để ẩn một user ID khỏi truy vấn, danh sách Users và REST API, cộng một cron “heartbeat” 5 phút một lần. Không có
eval, không gọi mạng. Rất dễ lọt khi đọc code, và đúng là loại thay đổi màverify-checksumssinh ra để bắt.
Lớp 3: backdoor kích hoạt bằng cookie trong theme
verify-checksums không áp dụng cho theme. Vì vậy tôi tải đúng phiên bản GeneratePress từ wordpress.org và chạy diff -rq với theme đang cài. Chỉ đúng một file khác biệt: functions.php.
Dòng 1 là <?php theo sau là hơn một trăm dấu cách, nên ở hầu hết trình soạn thảo, mã độc nằm lệch xa về bên phải ngoài màn hình. Nó không làm gì trừ khi có đủ bốn cookie đánh số. Khi đó, nó giải mã cookie số 3 (ROT13 rồi Base64), ghi PHP thu được vào một file tạm, nạp bằng require, rồi xoá. Các chỉ số cookie được viết dưới dạng phép tính (-87+87) để né việc tìm chuỗi đơn giản.
Hệ quả: thực thi mã từ xa ở mọi request, không có URL mới để tìm và không có POST đặc trưng trong access log. Dấu vết duy nhất nằm ở thư mục tạm của site: 536 file nhỏ, mỗi file một lần kích hoạt, còn sót lại vì payload thoát trước khi dòng dọn dẹp chạy. File đầu tiên mang mốc 06:47 ngày 01/11. Có những đợt 198 và 128 lượt gọi trong một giờ khớp với các đợt đăng spam, sau đó là nhịp đều 3 lượt mỗi 2 giờ, chính là con bot tạo lại admin.
Ngày sửa của file ghi 10/07/2026. Dấu thời gian có thể bị làm giả, nên tôi coi mốc đó là chưa xác nhận.
head -40, làm mất những kết quả xếp sau. Và tôi tin vào các công cụ checksum chỉ phủ lõi WordPress và plugin trên wordpress.org, chứ không phủ theme, mu-plugins, uploads hay plugin trả phí. Một giả định sai về ngày bắt đầu khiến lần quét “mù” đúng với file quan trọng nhất.
Lần thứ ba: cửa vào là một mật khẩu bị lộ
Lần kiểm sạch cuối của tôi là 14:10 ngày 3/11. Đến 22:51 cùng tối, một plugin mới xuất hiện với cái tên vô hại như “content sync connector”: 21 file PHP được ghi trong đúng một phút, ba mu-plugin mới, functions.php của theme bị sửa lần nữa kèm một file “guard” trong theme và trong uploads, và cấu hình cache bị chỉnh. Tên file chứa các từ như guard, watchdog, persistence. Đó là dấu hiệu của mã độc tự phục hồi: xoá một file thì file khác ghi lại.
Danh sách user không đổi (bốn người) và các file kích hoạt cũ trong thư mục tạm không tăng, nên đây không phải loader cũ trong theme. Bằng chứng chỉ về một lần đăng nhập admin bình thường rồi upload plugin qua trang quản trị. Ở lần hai tôi đã tạo lại salts và thu hồi Application Password, nhưng chưa đổi mật khẩu của chính tài khoản admin, và mật khẩu đó đã bị lộ. Đổi salt làm mất hiệu lực phiên đăng nhập hiện có, không đổi mật khẩu mà kẻ tấn công đã biết.
Plugin sau đó hoạt động như máy khách của một máy chủ điều khiển. Trong 27 giờ nó nhận 13 lệnh: đăng ký site, tạo rồi xoá một bài thử, rồi mười lệnh đăng bài casino, cứ 30 đến 60 phút một bài. Cơ sở dữ liệu có 15 option và transient cùng tiền tố, hai cron hook, 11 bài spam mới và một Application Password được tạo lại.
Điều thực sự chặn được nó
- Dừng PHP trước. Tôi dừng dịch vụ PHP-FPM và web server trước khi chạm vào bất kỳ file nào. Khi PHP còn chạy, các file guard tạo lại mọi thứ bạn xoá; khi PHP dừng, không gì thực thi được trong lúc bạn dọn.
- Lập bản đồ điểm bám trước khi xoá. Plugin tự lưu bản đồ chín nơi nó có thể phục hồi, gồm một file zip khôi phục giấu trong thư mục cache. Tôi đọc bản đồ đó, cách ly bảy file và hai thư mục cache, rồi tìm theo tên file và theo kích thước cộng SHA-256. Không còn gì ngoài thư mục bằng chứng.
- Dọn cơ sở dữ liệu riêng. Xoá 15 option và transient, hai cron hook, đưa 12 bài spam vào thùng rác, thu hồi Application Password vừa được tạo lại.
- Đổi các mật khẩu mà con người thực sự gõ: admin và mọi user khác, salts một lần nữa, và mật khẩu cơ sở dữ liệu.
--quiet. Coi mọi bí mật đã hiện trong terminal, chat hay log là đã bị lộ.
Thứ tự dựng lại đã hiệu quả
- Giữ bằng chứng trước. Sao chép mọi file đáng ngờ, băm chúng, và export cơ sở dữ liệu trước khi đổi bất cứ thứ gì.
- Di chuyển, đừng xoá. Dùng
mvđưa từng file độc hại vào thư mục bằng chứng giữ nguyên đường dẫn gốc, để luôn biết nó từng nằm ở đâu. - Cắt đường vào trước khi xoá tài khoản. Xoá admin lạ là vô nghĩa khi webshell tạo lại chúng vẫn đang chạy.
- Thay plugin bằng file zip chính thức. Giải nén từng cái vào thư mục tạm và chỉ hoán đổi khi thấy đúng thư mục mong đợi. Đừng dùng
wp plugin delete: lệnh này chạy hook gỡ cài đặt có thể xoá dữ liệu của plugin. Sau đó chạy lại checksum: 29 trên 30 khớp, và plugin trả phí duy nhất được xem tay. - Thay file theme bằng bản chính thức, rồi so lại toàn bộ theme.
- Quét toàn bộ cây thư mục theo chữ ký, không chỉ các file mới sửa gần đây.
- Đổi mọi bí mật: tạo lại salts, thu hồi mọi Application Password, đổi mật khẩu cơ sở dữ liệu và admin, bật 2FA.
- Vá và khởi động lại: PHP 8.3.8 lên 8.3.36, rồi reboot để nạp bản cập nhật kernel đang chờ.
- Đóng cửa chính. Bật 2FA cho mọi admin, gỡ plugin có lỗ hổng hoặc bị bỏ rơi, và cân nhắc
DISALLOW_FILE_MODSđể không ai upload được plugin từ trang quản trị. Kiểm tra phiên bản đang cài trước khi áp dụng bất kỳ file zip nào: bản add-on trả phí tôi được đưa lại cũ hơn bản đang chạy trên site (bản có lỗ hổng upload file điểm CVSS 8,8), nên cài vào là hạ cấp chứ không phải vá.
Các lệnh phát hiện bạn có thể dùng lại
Chạy từ thư mục gốc của site bằng WP-CLI. Các lệnh này tìm đúng họ mã độc này, nên kết quả sạch chưa phải bằng chứng site đã sạch.
# File không được phép có trong lõi hoặc plugin ("File was added")
wp core verify-checksums
wp plugin verify-checksums --all
# Theme không được phủ: so với đúng phiên bản trên wordpress.org
diff -rq /tmp/theme-official/generatepress wp-content/themes/generatepress
# Mánh độn dấu cách: thẻ PHP theo sau là hơn 40 dấu cách
grep -rlE '^<\?php {40,}' --include='*.php' .
# Mã chạy theo cookie, giấu chỉ số dưới dạng phép tính
grep -rlE '\$_COOKIE\[-?[0-9]+[-+][0-9]+\]' --include='*.php' .
# Hàng trăm file nhỏ trong thư mục tạm của site = loader đang bị kích hoạt
ls -la /path/to/site/tmp | wc -l
Kết quả cho đến hiện tại
- File backdoor đã cách ly: 16 file ở lần hai và 7 file cùng 2 thư mục cache ở lần ba, đều giữ làm bằng chứng thay vì xoá.
- Plugin đã xác minh: 29 trên 30 khớp checksum chính thức; theme khớp hoàn toàn với bản chính thức.
- PHP: 8.3.8 lên 8.3.36, xác nhận từ cả dòng lệnh lẫn header phản hồi HTTP.
- Kiểm từ bên ngoài ngày 6/11: trang chủ có 0 từ khoá spam (kể cả khi gọi bằng user agent Googlebot), các URL spam kiểm mẫu và thư mục plugin độc đều trả 404, REST API công khai không có bài nào đăng sau 28/10, và danh sách user không đọc được khi chưa đăng nhập.
Một cuộc dọn chỉ được coi là xong khi không có gì quay lại sau nhiều chu kỳ của kẻ tấn công. Tôi tiếp tục kiểm ba thứ vài giờ một lần trong 7 ngày: danh sách Users, mọi file PHP mới, và số file trong thư mục tạm. Bài spam nằm trong thùng rác đủ 7 ngày rồi mới xoá hẳn, và Google Search Console được gửi lại sitemap cùng yêu cầu xoá URL.
Website của bạn cứ bị nhiễm lại sau khi dọn?
Gửi tôi tên miền và các triệu chứng. Tôi sẽ cho bạn biết nên kiểm lớp nào trước.