WordPress XML-RPC: 3 cách vô hiệu hóa & cách nhận biết tấn công xmlrpc.php | Ngoại lệ Jetpack

Thông báo tiếp thị liên kết Ver 3.1
Thông báo tiếp thị liên kết Ver 3.2 (chỉ trên di động)

Nếu log truy cập của website WordPress có hàng trăm yêu cầu xmlrpc.php liên tiếp từ cùng một IP, đó là tấn công dò mật khẩu nhắm vào WordPress XML-RPC. Bài viết phân loại 8.624 dòng log truy cập trong một ngày của server Nginx + Apache chạy nhiều website WordPress, trực tiếp áp dụng ba cách vô hiệu hóa và kiểm tra mã phản hồi, từ đó tổng hợp xem phải chặn ở lớp nào thì mới giảm được cả việc chạy PHP.

XML-RPC là giao thức trao đổi tài liệu XML qua HTTP để gọi các chức năng của WordPress từ xa; từ WordPress 3.5, mục bật/tắt trong màn hình cài đặt đã bị bỏ, nên XML-RPC chạy ở trạng thái bật ngay sau khi cài đặt.

1️⃣ Vai trò của xmlrpc.php và nơi sử dụng

Từ khi có REST API, dễ nghĩ rằng [xmlrpc.php] là tệp không còn ai dùng, nhưng đến nay Jetpack vẫn kết nối website với WordPress.com qua tệp này. Tài liệu chính thức của Jetpack ghi rõ là phụ thuộc vào kết nối XML-RPC, và trên hosting chặn XML-RPC theo mặc định thì ngay cả việc kết nối Jetpack cũng thất bại. Cũng giống như ngày nay nhiều văn phòng vẫn giữ lại một số fax.

Tính đến WordPress 7.1, tệp này nhận 77 phương thức: 50 phương thức họ wp xử lý bài viết, media và bình luận, 23 phương thức của các giao thức blog cũ (MetaWeblog, MovableType, Blogger), 2 phương thức pingback và 2 phương thức demo để thử nghiệm.

Họ wp đăng nhập bằng tên người dùng và mật khẩu nằm trong phần thân yêu cầu, nên không đi qua màn hình đăng nhập. Viết và sửa bài, tải lên media, quản lý bình luận đều thuộc họ này; còn 23 phương thức của các giao thức blog cũ được giữ lại để trao đổi bài viết theo cùng cách với các công cụ blog khác.

demo.sayHello là lệnh gọi thử có sẵn trong WordPress XML-RPC. Gửi đi thì nó chỉ trả về đúng một câu [Hello!]; vì không cần đăng nhập nên được dùng để kiểm tra XML-RPC có hoạt động hay không. Đi cặp với nó là demo.addTwoNumbers, cộng hai số rồi trả về kết quả.

Tất cả chỉ phản hồi khi phần thân XML được gửi bằng POST; mở địa chỉ bằng trình duyệt thì sẽ nhận về thông báo [XML-RPC server accepts POST requests only.] cùng mã 405.

Pingback cũng là việc của tệp này. Khi xuất bản bài viết, WordPress gửi pingback tới xmlrpc.php của các website được liên kết trong nội dung, và server bên nhận tự mở địa chỉ gửi đến để kiểm tra liên kết có thật hay không.

🧭 Các yêu cầu vào xmlrpc.php và cách xử lý
Bảo mật wp-adminSơ đồ giải thích
📨 Bên gửi
↓
Lối vào
xmlrpc.php
Chỉ xử lý phần thân XML gửi bằng POST
405 · XML-RPC server accepts POST requests only.
77 phương thức
wp.*Bài viết · media · bình luận: 50
metaWeblog · mt · bloggerGiao thức blog cũ: 23
pingback.*Pingback: 2
demo.*Lệnh gọi thử để kiểm tra hoạt động: 2
Kết quả
Duy trì kết nối với WordPress.com
Kiểm tra tên người dùng · mật khẩu trong phần thân yêu cầu → viết · sửa bài
Server tự mở địa chỉ gửi đến để kiểm tra liên kết
Không cần đăng nhập: Hello! Phản hồi
Không xử lý
Jetpack duy trì kết nối với WordPress.com qua tệp này, nên chặn tệp thì kết nối sẽ bị ngắt.
Công cụ đăng bài bên ngoài đăng nhập bằng tên người dùng và mật khẩu đặt trong phần thân yêu cầu, không đi qua màn hình đăng nhập.
Pingback được gửi tới xmlrpc.php của website được liên kết trong bài viết, và server bên nhận tự mở địa chỉ gửi đến để kiểm tra liên kết.
demo.sayHello là lệnh gọi thử để kiểm tra XML-RPC có hoạt động không; nó trả lời bằng đúng một câu [Hello!] mà không cần đăng nhập.
Mở địa chỉ bằng trình duyệt thì sẽ nhận về thông báo [XML-RPC server accepts POST requests only.] cùng mã 405.

2️⃣ Tấn công xmlrpc.php qua log truy cập

Tấn công dò mật khẩu không đi qua màn hình đăng nhập mà vào qua [xmlrpc.php]. Trong log truy cập từ 0:00 đến 20:00 ngày 14 tháng 10 của server Nginx + Apache chạy nhiều website WordPress, có 8.624 yêu cầu POST /xmlrpc.php đến từ 72 IP, trong đó 19 IP gửi đúng 300 yêu cầu mỗi IP.

Một IP đã gửi 300 yêu cầu trong 28 phút, từ 14:09 đến 14:37, với khoảng cách 5–6 giây đều đặn đến cuối. 8.524 yêu cầu ghi Jetpack hoặc WordPress.com trong User-Agent, và cùng một IP luân phiên dùng nhiều giá trị bắt chước định dạng Jetpack thật, chẳng hạn [Jetpack by WordPress.com (Jetpack 13.0; WordPress 6.3)].

Cùng ngày, trước 14:00, cài đặt tắt XML-RPC của plugin bảo mật trả các yêu cầu này về trang chủ bằng 302; sau 14:00, khi đã chặn ở Nginx, chúng kết thúc bằng 403. 302 nghĩa là PHP đã chạy cho từng yêu cầu dù phần thân chỉ 0 byte, còn 403 là phản hồi không đi qua PHP.

🧭 Tấn công xmlrpc.php qua một dòng log truy cập
Sơ đồ giải thích
🛡️ Trạng thái server
Một dòng log truy cập (Nginx access.log)
203.0.113.25IP[14/Oct/2026:14:09:04 +0900]Thời điểm"POST /xmlrpc.php HTTP/1.1"Yêu cầu302403Trạng thái0153Byte"Jetpack by WordPress.com (Jetpack 13.0; WordPress 6.3)"User-Agent
PHP chạy · 302 về trang chủKhông qua PHP · trang 403 mặc định của Nginx
Khi plugin bảo mật đã tắt XML-RPC, log ghi lại mã 302 trả yêu cầu về trang chủ. Phần thân là 0 byte, nhưng PHP vẫn chạy để tạo ra mã 302 đó.
Khi chặn ở Nginx, log ghi lại 403 cùng trang lỗi mặc định 153 byte, và PHP không chạy.
Yêu cầu từ cùng một IP
14:09:0414:37:20
300 yêu cầu trong 28 phút, cách nhau 5–6 giây
8.624 POST từ 0:00 đến 20:0072 IP19 IP gửi 300 yêu cầu mỗi IP8.524 UA giả danh Jetpack · WordPress.com

3️⃣ system.multicall và dò mật khẩu

Nhiều bài giải thích rằng [system.multicall] cho phép thử hàng trăm mật khẩu trong một yêu cầu, nhưng trong một yêu cầu, chỉ cần đăng nhập thất bại một lần là WordPress không đối chiếu mật khẩu của các lệnh gọi còn lại nữa. Chỉ lệnh gọi đầu tiên đi qua wp_authenticate; các lệnh gọi sau được trả lời bằng cùng lỗi [Incorrect username or password.] mà không qua kiểm tra.

Vì số phản hồi trả về vẫn bằng số lệnh gọi, một yêu cầu chứa 100 mật khẩu sai sẽ nhận về 100 lỗi trong phản hồi 23KB chỉ sau 0,12 giây. Trong cùng một yêu cầu, nếu đặt mật khẩu đúng ở sau thì nó cũng bị từ chối theo vì lệnh gọi phía trước đã thất bại; nếu đặt ở trước thì chỉ lệnh gọi đó thành công. Mật khẩu chỉ được đối chiếu một lần, nhưng việc phân tích XML và tải WordPress lặp lại ở mỗi yêu cầu, nên tải cho server của kiểu tấn công này đến từ chính số lượng yêu cầu chứ không phải từ việc kiểm tra đăng nhập.

4️⃣ 3 cách vô hiệu hóa XML-RPC và mã phản hồi

Dù đã tắt XML-RPC bằng plugin, yêu cầu [xmlrpc.php] vẫn vào tới PHP. Khi đã tắt bằng filter, gửi một yêu cầu chứa 100 lệnh gọi vẫn nhận về nguyên HTTP 200 kèm 100 lỗi (24KB). Tùy lớp nào từ chối yêu cầu mà có ba cách, và mỗi cách cho mã phản hồi khác nhau.

1. Tắt filter xmlrpc_enabled bằng code

Chỉ cần thêm vào functions.php của theme một dòng add_filter trả filter [xmlrpc_enabled] về false. Ở trạng thái này, các lệnh gọi cần đăng nhập bị từ chối với lỗi [XML-RPC services are disabled on this site.] và HTTP 405, nhưng lệnh gọi thử demo.sayHello, vốn không cần đăng nhập, vẫn trả lời bằng 200.

2. Tắt trong cài đặt plugin: Perfmatters · Hide My WP

Đây là cách vô hiệu hóa bằng cài đặt Disable XML-RPC trong các plugin như Perfmatters hay Hide My WP. Hide My WP dùng cùng cách filter như trên nên phản hồi cũng giống, còn Perfmatters, ngoài filter đó, còn kết thúc các yêu cầu trực tiếp tới [xmlrpc.php] bằng 403 và gỡ cả header pingback lẫn liên kết pingback, nên sau khi tắt, cả GET và POST đều trả lời 403. Tuy nhiên, để trả về 403, WordPress vẫn chạy đến giai đoạn init, nên thời gian phản hồi là 0,13–0,15 giây, bằng lúc còn mở, và tải PHP vẫn còn.

📍 Settings › Perfmatters › General › Core › Disable XML-RPC
Bật công tắc rồi nhấn [Save Changes], màn hình của khách truy cập bên dưới sẽ thay đổi.
Màn hình quản trị
Settings saved.
perfmatters
⚙ GeneralVersion 2.6.8
Core
Disable Emojis?
Disable Dashicons?
Disable Embeds?
Disable XML-RPC?
Remove RSD Link?
Remove jQuery Migrate?
Hide WP Version?
Remove Shortlink?
Disable RSS Feeds?
Remove RSS Feed Links?
Disable Self Pingbacks?
Disable REST API?
Remove REST API Links?
Disable Google Maps?
Disable Password Meter?
Disable Comments?
Remove Comment URLs?
Add Blank Favicon?
Remove Global Styles?
Block Style Behavior?
Disable Heartbeat?
Heartbeat Frequency?
Limit Post Revisions?
Autosave Interval?
Login URL
Custom Login URL?
Disabled Behavior?
Message?
📍 Hide My WP › Settings › Cleanup › Disable XML RPC
Đánh dấu rồi nhấn [Save Settings], màn hình của khách truy cập bên dưới sẽ thay đổi.
Màn hình quản trị
Hide My WP
Settings was updated successfully!
Cleanup
Minify HTML
Remove comments and whitespaces from HTML (Use with cache).
Minify style
Remove theme info and other comments from stylesheet. (Require new style name)(Use with cache!).
Replace \/ URLs
Choose if you see Javascript URLs (e.g. \/wp-content\/themes)
Feed Meta
Other Meta
Default Tagline
Remove Version
Body Classes
Menu Classes
Post Classes
Clean up other
Replace in AJAX
Change Nonce
Disable XML RPC
Internal CSS
Useful to paste internal CSS code (Require New Theme Path)
Internal JS
Useful to paste internal JS code (Require New Theme Path)
Màn hình của khách chưa đăng nhập — gửi yêu cầu với cài đặt đã lưu
405
XML-RPC server accepts POST requests only.
Yêu cầu GET nhận 405 ở mọi cài đặt.
200
Hello!
Đây là lệnh gọi thử để kiểm tra hoạt động, nên trả lời mà không cần đăng nhập.
200
faultCode 403 · Incorrect username or password.
Mật khẩu vẫn được đối chiếu. Đổi địa chỉ đăng nhập cũng vậy.
403
Không có phần thân
Perfmatters kết thúc bằng 403 ở giai đoạn init. PHP vẫn chạy.
403
Không có phần thân
Perfmatters kết thúc bằng 403 ở giai đoạn init. PHP vẫn chạy.
403
Không có phần thân
Kết thúc bằng 403 mà không đối chiếu mật khẩu. PHP vẫn chạy.
200
Hello!
Vì dùng cách filter, lệnh gọi không cần đăng nhập vẫn trả lời như thường.
405
faultCode 405 · XML-RPC services are disabled on this site.
Chỉ các lệnh gọi cần đăng nhập bị từ chối.

3. Chặn ở web server: Nginx · Apache

Dù là Nginx hay Apache, yêu cầu đều bị từ chối trước khi tới PHP, nên khác với cách dùng plugin, WordPress không chạy.

# Nginx — trong khối server, đặt trên location xử lý PHP
location = /xmlrpc.php {
    deny all;
}

Khối location này đặt trên location xử lý PHP, và Nginx từ chối ngay các yêu cầu khớp chính xác với [xmlrpc.php]. Sau khi thêm, phải kiểm tra cấu hình rồi tải lại thì mới có hiệu lực.

nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
systemctl reload nginx

Từ sau khi tải lại, GET và POST kết thúc bằng 403 mà không tới PHP; phần thân phản hồi là trang 403 mặc định của Nginx, 153 byte, và thời gian phản hồi là 0,02 giây.

# Apache — .htaccess ở thư mục gốc của website (Apache 2.4)
<Files xmlrpc.php>
    Require all denied
</Files>

Với server chỉ dùng Apache, đặt chỉ thị Files ở trên vào .htaccess ở thư mục gốc của website là áp dụng ngay, và trang 403 mặc định của Apache (199 byte) được trả về trong cùng 0,02 giây.

curl -s -o /dev/null -w '%{http_code}' -X POST https://example.com/xmlrpc.php -d '<methodCall><methodName>demo.sayHello</methodName></methodCall>'
403

Lệnh trên gửi lệnh gọi thử demo.sayHello bằng POST để kiểm tra mã phản hồi; sau khi chặn ở web server thì sẽ nhận về 403. Bỏ các tùy chọn [-o /dev/null] và [-w] rồi gửi thì sẽ thấy phần thân: nhận được [Hello!] là đang mở, nhưng website chỉ tắt bằng filter cũng nhận cùng phản hồi đó, nên phải gửi thêm lệnh gọi đăng nhập mới xác nhận được 405.

5️⃣ Khi không nên tắt và phương án thay thế

Chặn toàn bộ ở web server là cách hiệu quả nhất, nhưng không phù hợp với website kết nối với WordPress.com qua Jetpack. Jetpack phụ thuộc vào kết nối XML-RPC, và trên hosting chặn XML-RPC theo mặc định như WP Engine, phải mở ngoại lệ theo hướng dẫn của Jetpack thì mới kết nối được. Website muốn nhận pingback cũng vậy.

Những website này thu hẹp phạm vi bằng cách chỉ cho phép danh sách IP mà Jetpack công bố (jetpack.com/ips-v4.txt) và từ chối phần còn lại. Chỉ cần thêm vào khối location của Nginx một dòng allow cho mỗi dải IP và đặt deny all ở cuối.

Nếu không thể chặn hoàn toàn, vẫn còn cách đặt giới hạn số lần mỗi phút cho các yêu cầu chưa đăng nhập. Cách thiết lập giống mục giới hạn tốc độ Nginx trong bài Bảo mật wp-admin — vì chỉ đếm các yêu cầu không có cookie đăng nhập nên không ảnh hưởng đến thao tác quản trị khi đã đăng nhập.

🔢 FAQ & nội dung đề xuất

Dù xóa đi, khi cập nhật lõi WordPress, tệp có trong gói cài đặt sẽ được sao chép lại nguyên vẹn, nên tệp sẽ quay lại ở lần cập nhật tiếp theo. Thay vì xóa tệp, chặn địa chỉ đó ở web server hoặc vô hiệu hóa bằng filter thì vẫn giữ được hiệu lực sau khi cập nhật.

REST API là một lối vào riêng, đi qua địa chỉ [wp-json], nên dù đã vô hiệu hóa XML-RPC, nó vẫn mở. Trình soạn thảo khối dùng lối vào này, nên cách đúng là không vô hiệu hóa hoàn toàn mà chỉ chặn các yêu cầu khi chưa đăng nhập.

Chúng không kiểm tra mà cứ gửi. Trên mọi website WordPress, tệp này đều nằm ở cùng một đường dẫn, nên scanner chỉ việc đổi địa chỉ rồi gửi POST. WordPress ở trạng thái mặc định còn tự để lộ địa chỉ xmlrpc.php qua liên kết [EditURI] trong phần head của trang và header [X-Pingback] của các bài viết nhận pingback.

🛡️ Nội dung đề xuất về bảo mật WordPress 8 bài viết · 2 bài bên ngoài
🗂️ Bảo mật WordPress
📋 Bảo mật wp-admin 📄 9 cách chặn brute force 📄 Hide My WP 📄 Plugin bảo mật Best 5
📄 Vô hiệu hóa XML-RPC 📄 REST API 📄 Pingback · trackback 📄 Bảo mật wp-config.php
📡 Nanaco Japan: xử lý tấn công xmlrpc.php 📡 サイトの主治医: vô hiệu hóa xmlrpc.php và kiểm tra
Không có kết quả tìm kiếm.

ℹ️ Thông báo tiếp thị liên kết
Nội dung trên trang này có chứa liên kết affiliate (tiếp thị liên kết). Khi khách truy cập mua sản phẩm hoặc dịch vụ qua các liên kết này, trang sẽ nhận hoa hồng từ bên bán. Trong quá trình đó, số tiền người mua phải trả (sẽ giảm khi có chương trình khuyến mãi ↓) không hề tăng lên. Giá, khuyến mãi và tình trạng hàng được đăng theo thời điểm viết bài và có thể khác với thực tế, vì vậy hãy kiểm tra lại lần cuối với bên bán trước khi mua. Việc chọn và đánh giá sản phẩm dựa trên tiêu chí riêng của trang; việc có nhận hoa hồng hay không không ảnh hưởng đến thứ tự giới thiệu hay nội dung đánh giá.