Website WordPress của bạn nhìn vẫn bình thường khi admin đăng nhập, nhưng khách truy cập thật lại bị chèn JavaScript lạ thì sao? Đây là tình huống mình gặp khá nhiều trong các ca xử lý bảo mật: chủ website kiểm tra bằng mắt thường và không thấy gì, nhưng malware đang chủ động né admin.
Case dưới đây dựa trên một mẫu WordPress backdoor có persistence PHP và JavaScript loader điều khiển từ xa qua BSC testnet. Bài này không biến bạn thành malware analyst trong một buổi, nhưng sẽ cho bạn thấy một dịch vụ bảo mật website nghiêm túc phải kiểm tra những gì, vì sao chỉ xóa tài khoản admin lạ là chưa đủ và đâu là checklist cần làm trước khi mở lại website. Cùng xem nhé!

Tóm tắt nhanh
- Mẫu malware trong case này tạo hoặc nâng quyền tài khoản
sys_maintlên administrator, sau đó đánh dấu bằng user meta để tìm lại. - Backdoor dùng hook WordPress để tự tái tạo account, giấu user khỏi màn hình Users và che một phần kết quả qua REST API.
- JavaScript chỉ chạy với visitor thường, né admin đăng nhập, bot, crawler, URL tĩnh và các request kỹ thuật.
- Stage 2 không hard-code domain trong file PHP; loader gọi
eth_calltới BSC testnet RPC để lấy URL động rồi chèn script từ xa. - Dịch vụ bảo mật website trong tình huống này phải làm sạch theo quy trình: lưu bằng chứng, cô lập, tìm persistence, rotate credential, restore hoặc thay file sạch, rồi giám sát sau xử lý.
Case này nguy hiểm ở điểm nào?
Điểm nguy hiểm nhất là malware không chỉ chèn script, mà còn giữ quyền quản trị lâu dài và che dấu hiệu khỏi chính người quản trị website. Đây là khác biệt lớn giữa một đoạn code lỗi thời vô hại và một backdoor đã được thiết kế để tồn tại sau khi bị phát hiện.
Trong mẫu phân tích, phần PHP tạo một tài khoản cố định:
- Username:
sys_maint - Email:
sys@localhost.local - Password:
ChangeMe_Str0ng! - User meta:
_wp_ip = 1
WordPress có API hợp lệ để tạo user mới bằng username, password và email qua wp_create_user(). Tài liệu WordPress mô tả hàm này là cách đơn giản để chèn user vào database. [1] Bản thân API không xấu; vấn đề là malware đang dùng API hợp lệ cho mục tiêu trái phép.
Thêm một chi tiết đáng chú ý: nếu user đã tồn tại theo username hoặc email, malware không bỏ qua. Nó nâng quyền user đó thành administrator. WordPress roles and capabilities cho phép kiểm tra quyền như current_user_can() và quản lý vai trò người dùng; với role administrator, attacker có vùng quyền rất rộng trong dashboard. [2]
Bạn đang đọc bài viết thuộc chuyên mục Bảo mật website của VietnamTutor, nơi mình chia sẻ cách bảo vệ, vận hành và khôi phục website doanh nghiệp theo hướng thực chiến.

Code gist mẫu của backdoor
Dưới đây là gist mẫu để bạn xem cấu trúc backdoor ở dạng code. Mình khuyên bạn chỉ đọc để đối chiếu IOC, không copy chạy trên website thật.
Sơ đồ kiến trúc malware
┌────────────────────────────┐
│ Infected WordPress PHP │
└─────────────┬──────────────┘
│
├── Create admin
│ sys_maint
│
├── Give administrator
│
├── Tag _wp_ip=1
│
├── Hide from Users screen
│
├── Hide from REST users
│
└── Recreate if removed
Visitor request
│
▼
┌────────────────────────────┐
│ Inject obfuscated JS │
│ wp_head + wp_footer │
└─────────────┬──────────────┘
│
├── Don't run for WP admin
├── Don't run for bots
├── Don't run on WP/static URLs
│
▼
BSC Testnet RPC
│
▼
eth_call contract
0x7Fd85c090f2b35071C57a3b9FeAF462aaEb0E437
│
▼
encoded response
│
hex decode
│
atob()
│
▼
attacker-controlled URL
│
▼
document.createElement("script")
│
▼
Stage-2 JSBackdoor tạo administrator ẩn ra sao?
Backdoor tạo hoặc khôi phục administrator bằng nhiều điểm bám: user meta, username, email và hook chạy trong vòng đời WordPress. Vì vậy, thao tác xóa sys_maint trong wp-admin chỉ xử lý phần nhìn thấy, không xử lý nguồn tạo lại account.
Luồng persistence trong case này có thể tóm tắt như sau:
- Malware chạy trên
admin_initvàinit. - Nó kiểm tra user ID đã lưu trong
_wp_ip_id. - Nếu ID không còn hợp lệ, nó tìm user có meta
_wp_ip = 1. - Nếu vẫn không thấy, nó tìm theo username
sys_mainthoặc emailsys@localhost.local. - Nếu không có kết quả, nó tạo user mới và gán administrator.
Đây là lý do mình luôn nói với khách hàng: “xóa admin lạ” không đồng nghĩa với “đã sạch malware”. Nếu file PHP độc hại còn nằm trong theme, plugin, mu-plugin, uploads, cache drop-in hoặc một vị trí include nào đó, account có thể quay lại sau vài request.
Phần né phát hiện cũng khá rõ. Malware hook vào pre_user_query để chỉnh SQL trước khi danh sách user được truy vấn. Tài liệu WordPress cho biết hook này chạy sau khi WP_User_Query được parse và trước khi query thực thi. [3] Khi malware thêm điều kiện loại user có meta _wp_ip = 1, tài khoản đó có thể biến mất khỏi Users → All Users.
Chưa hết, malware còn dùng filter rest_user_query để điều chỉnh tham số truy vấn user qua REST API. WordPress ghi rõ filter này áp dụng với argument của WP_User_Query khi query users qua REST API. [4] Nói đơn giản: kiểm tra bằng dashboard hoặc REST API đều có thể bị làm mù nếu chỉ nhìn từ lớp WordPress bình thường.
- Persistence
- Khả năng malware tồn tại hoặc tự khôi phục sau khi một dấu hiệu bị xóa.
- Evasion
- Kỹ thuật né phát hiện bằng cách giấu tài khoản, né admin, né bot hoặc thay đổi kết quả truy vấn.
JavaScript loader hoạt động như thế nào?
Loader chèn JavaScript vào frontend, né bot và admin, sau đó gọi blockchain RPC để lấy URL stage 2 động. Điều này khiến việc tìm domain độc hại bằng cách grep file PHP trở nên không đủ.
Trong WordPress, wp_head thường được dùng để in script hoặc dữ liệu trong phần head của frontend, còn wp_footer in script hoặc dữ liệu gần cuối trang. [5] [6] Malware tận dụng hai hook này với priority cao để inject payload vào nhiều trang.
Nhưng payload không chạy với mọi người. Nó bỏ qua wp-login.php, không inject cho user đang đăng nhập có quyền manage_options, và JavaScript tiếp tục né các User-Agent kiểu bot, crawler, Ahrefs, Semrush, curl, wget, python, java. Thú vị nhỉ! Admin mở frontend khi đang đăng nhập sẽ thấy website có vẻ bình thường, trong khi visitor thật có thể nhận stage 2.
Phần cốt lõi nằm ở bước gọi RPC. Payload có contract:
0x7Fd85c090f2b35071C57a3b9FeAF462aaEb0E437
Và call data:
0x6d4ce63c
Nó tạo request JSON-RPC tương đương:
{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{
"to": "0x7Fd85c090f2b35071C57a3b9FeAF462aaEb0E437",
"data": "0x6d4ce63c"
},
"latest"
],
"id": 1
}Ethereum Execution API mô tả eth_call là phương thức gọi một message call ngay lập tức mà không tạo giao dịch trên blockchain, và kết quả trả về là bytes hex-encoded. [7] BNB Chain cũng cung cấp danh sách endpoint JSON-RPC cho BSC testnet, bao gồm các endpoint public để kết nối BNB Smart Chain. [8]
Trong case này, smart contract đóng vai trò như dead-drop hoặc configuration server. Attacker không cần sửa file PHP trên website để đổi URL stage 2; họ chỉ cần thay đổi dữ liệu mà loader đọc được từ contract hoặc cơ chế đằng sau contract đó. Đây là điều khiến remediation phải đi xa hơn việc tìm một domain độc hại trong source code.

Vì sao không thể chỉ xóa một file?
Vì file bạn nhìn thấy có thể chỉ là một điểm bám trong chuỗi compromise; entry point, account ẩn, database injection, cron và stage 2 vẫn có thể còn tồn tại. Xóa nhầm hoặc xóa thiếu đều khiến website tái nhiễm.
Một dịch vụ bảo mật website đúng nghĩa không nên bắt đầu bằng câu “đưa quyền admin để em cài plugin quét”. Với case này, mình sẽ coi toàn bộ WordPress installation đã compromise cho đến khi chứng minh ngược lại.
Các vị trí cần kiểm tra gồm mu-plugins, theme, plugin, uploads, wp-config.php, drop-ins, database options, cron event, usermeta và access layer như SFTP, SSH key, API token hoặc application password.
Các chiến dịch WordPress gần đây cũng cho thấy rogue administrator và backdoor không phải chuyện hiếm. The Hacker News đưa tin tháng 06/2026 về một chiến dịch tamper JavaScript của các plugin phổ biến, trong đó payload có thể tạo admin account và cài hidden plugin khi admin đăng nhập tải script bị sửa. [9] Chi tiết chiến dịch khác case này, nhưng bài học giống nhau: khi attacker có đường chạy code trong ngữ cảnh WordPress, tạo tài khoản quản trị và giữ cửa hậu là kịch bản thực tế.
Vì vậy, mình không khuyên bạn “dọn tay” trực tiếp trên production nếu website đang tạo doanh thu. Hãy clone ra môi trường điều tra, lưu hash và timestamp, rồi mới quyết định restore, thay file sạch hoặc rebuild.

Dịch vụ bảo mật website nên xử lý theo quy trình nào?
Quy trình xử lý nên đi từ bảo toàn bằng chứng, cô lập tác động, phân tích persistence, làm sạch có kiểm soát, rotate credential và giám sát sau khôi phục. Mục tiêu không chỉ là “hết cảnh báo” mà là giảm xác suất tái nhiễm.
- Lưu hiện trạng: ghi nhận URL, timestamp, user nghi vấn, file nghi vấn, outbound request, log và snapshot filesystem/database nếu có thể.
- Cô lập tác động: bật maintenance, chặn outbound đáng ngờ hoặc route sang bản sạch, nhưng chưa xóa log và file nghi vấn.
- Tìm persistence: rà
sys_maint,_wp_ip, hook WordPress, contract address, endpoint BSC testnet và biến thể obfuscated. - Làm sạch bằng bản chuẩn: thay core/plugin/theme phổ biến bằng bản chính thức; với custom code, so diff với repository gốc.
- Rotate credential: đổi mật khẩu admin, hosting, database, SFTP/SSH, API key, application password và tài khoản dịch vụ.
- Kiểm thử như visitor thật: dùng trình duyệt sạch, user chưa login, User-Agent thật, referrer khác nhau và mobile.
- Theo dõi sau khôi phục: giám sát file modification, admin creation, outbound request, Search Console, logs và blacklist trong 7-14 ngày.
Nếu bạn cần checklist nền trước khi có sự cố, bài scan malware WordPress và monitoring sẽ phù hợp hơn. Còn nếu đang có admin lạ, hãy đọc thêm bảo mật tài khoản quản trị WordPress để rà lại quyền và 2FA.

IOC và lệnh quét nhanh cho quản trị viên
IOC giúp bạn rà soát nhanh dấu hiệu trùng với mẫu này, nhưng không thay thế phân tích toàn hệ thống. Nếu kết quả âm tính, website vẫn có thể nhiễm biến thể khác hoặc obfuscation khác.
Các IOC trực tiếp từ case:
| Loại | Giá trị | Ý nghĩa |
|---|---|---|
| Username | sys_maint | Administrator cố định malware tạo hoặc nâng quyền |
sys@localhost.local | Email dùng để tìm lại hoặc tạo account | |
| User meta | _wp_ip, _wp_ip_id | Dấu đánh dấu account backdoor |
| WordPress hooks | pre_user_query, rest_user_query, wp_head, wp_footer | Giấu user và inject payload |
| Contract | 0x7Fd85c090f2b35071C57a3b9FeAF462aaEb0E437 | Địa chỉ contract loader gọi để lấy config |
| Call data | 0x6d4ce63c | Selector/call data trong request RPC |
| RPC | bsc-testnet, data-seed-prebsc, drpc.org, publicnode.com | Endpoint được dùng để gọi BSC testnet |
Một số lệnh kiểm tra nhanh trên server Linux:
grep -RIn -E "sys_maint|sys@localhost\.local|_wp_ip|0x7Fd85c090f2b35071C57a3b9FeAF462aaEb0E437|0x6d4ce63c|bsc-testnet|data-seed-prebsc|publicnode|drpc" .
grep -RIn --include="*.php" -E "pre_user_query|rest_user_query|wp_create_user|set_role|wp_head|wp_footer" wp-content
find wp-content/uploads -type f \( -name "*.php" -o -name "*.phtml" -o -name "*.phar" \) -print
wp db query "SELECT user_id, meta_key, meta_value FROM wp_usermeta WHERE meta_key IN ('_wp_ip','_wp_ip_id');"
wp user get sys_maint --field=IDMình khuyên bạn chạy các lệnh này trên bản copy hoặc trong phiên có quyền đọc phù hợp. Với shared hosting, hãy cẩn thận prefix database và đường dẫn WordPress; không phải site nào cũng dùng wp_.
Nếu đang xử lý website có doanh thu, hãy đọc thêm bài audit bảo mật WordPress hàng tháng để biến IOC check thành lịch vận hành, thay vì chỉ chạy khi có sự cố.
Khi nào doanh nghiệp nên thuê đội xử lý?
Bạn nên thuê đội xử lý khi website có dữ liệu khách hàng, doanh thu, traffic SEO quan trọng, hoặc malware có persistence/evasion như case này. Đây không còn là việc “xóa vài dòng code lạ” nữa.
Dấu hiệu nên escalation ngay: admin lạ bị xóa rồi quay lại, payload chỉ xuất hiện với visitor, website có thanh toán hoặc dữ liệu cá nhân, chưa có backup sạch, chưa biết entry point, hoặc Search Console đã ghi nhận redirect/spam.
Một dịch vụ bảo mật website đáng thuê nên bàn giao tối thiểu các phần sau: timeline sự cố, IOC đã tìm thấy, file/database đã sửa, credential cần rotate, khuyến nghị hardening, kết quả kiểm thử sau cleanup và lịch monitoring sau 7-14 ngày. Nếu chỉ nhận lại câu “đã xóa mã độc” mà không có báo cáo, bạn rất khó biết site đã thật sự sạch hay chỉ tạm im.
Kết luận ngắn gọn: case này là một persistent WordPress backdoor + cloaked remote JavaScript loader. Nó có admin access, persistence, stealth và khả năng chạy JavaScript từ xa. Với mức độ này, hãy coi website đã compromise toàn phần cho đến khi điều tra đủ sâu.
Nếu website của bạn đang có dấu hiệu tương tự, hãy bắt đầu bằng việc lưu hiện trạng, tạm dừng thao tác xóa vội và kiểm tra IOC ở trên. Cần hỗ trợ chuyên sâu thì đội VietnamTutor có thể giúp bạn audit, làm sạch và thiết lập monitoring để giảm nguy cơ tái nhiễm.
Nguồn tham khảo
- WordPress Developer Reference: wp_create_user()
- WordPress Plugin Handbook: Roles and Capabilities
- WordPress Developer Reference: pre_user_query hook
- WordPress Developer Reference: rest_user_query hook
- WordPress Developer Reference: wp_head hook
- WordPress Developer Reference: wp_footer hook
- Ethereum Execution APIs: eth_call
- BNB Chain Docs: JSON-RPC endpoints for BNB Smart Chain
- The Hacker News: Popular WordPress Plugin Scripts Tampered to Plant Hidden Backdoors on Sites
Các câu hỏi thường gặp
Dịch vụ bảo mật website có nên xóa ngay tài khoản admin lạ không?
Có thể khóa hoặc cô lập tài khoản để giảm rủi ro, nhưng không nên xem đó là bước xử lý duy nhất. Nếu backdoor còn trong PHP hoặc database, tài khoản admin lạ có thể được tạo lại.
Vì sao admin đăng nhập không thấy JavaScript độc hại?
Nhiều malware cố tình né người quản trị bằng cách kiểm tra trạng thái đăng nhập hoặc capability như manage_options. Vì vậy, cần kiểm thử bằng trình duyệt sạch, user chưa đăng nhập và User-Agent giống visitor thật.
Blockchain RPC trong malware WordPress có nghĩa là gì?
Trong case này, JavaScript gọi RPC tới BSC testnet để đọc dữ liệu từ smart contract, sau đó decode thành URL stage 2. Blockchain đóng vai trò nơi lấy cấu hình động, không phải bằng chứng website đang dùng blockchain hợp pháp.
Có thể dùng plugin scan malware WordPress để xử lý case này không?
Plugin scan có thể giúp phát hiện dấu hiệu ban đầu, nhưng case có persistence và evasion cần kiểm tra filesystem, database, logs, credential và outbound behavior. Plugin không thay thế quy trình incident response.
Sau khi làm sạch backdoor WordPress cần theo dõi bao lâu?
Tối thiểu nên theo dõi 7-14 ngày sau khôi phục, gồm file modification, admin creation, request outbound, Search Console, log lỗi và cảnh báo blacklist. Nếu dấu hiệu cũ quay lại, vẫn còn persistence hoặc credential bị lộ.
