目录导航
背景
就像小说“依赖混乱”供应链攻击一样,有可能接管在 wordpress.org 注册表中无人认领的内部开发的 WordPress 插件。更新插件可能会导致 RCE 或安装 PHP 后门。您可以使用wp_update_confusion.py扫描潜在目标。为了保护您的网站,请阅读此公告。
wordpress公告详情:
在 WordPress 5.8 中引入“更新 URI”插件标头
WordPress 5.8 引入了一个可供插件作者使用的新标头。这允许第三方插件避免被WordPress.org插件目录中的类似名称的插件更新意外覆盖。
以前,任何使用与 WordPress.org 上托管的插件相同的插件的自定义插件都冒着被后者更新覆盖的重大风险。
WordPress 5.8 引入了一个新的 Update URI
插件标题字段。如果此新字段的值与https://wordpress.org/plugins/{$slug}/
或以外的任何 URI 匹配 w.org/plugin/{$slug}
,WordPress 将不会尝试更新它。
如果设置, Update URI
标头字段应该是有效的 URI 并具有唯一的主机名。
请注意,由 WordPress.org 托管的插件作者不需要使用这个新标题。
一些例子包括:
Update URI: https://wordpress.org/plugins/example-plugin/
Update URI: https://example.com/my-plugin/
Update URI: my-custom-plugin-name
Update URI: false
也可以工作,除非有一些代码处理 false
主机名(参见下面介绍的钩子),否则插件不会更新。
如果在 w.org 托管插件上使用标头,WordPress.org API将仅返回匹配以下格式的插件更新:
https://wordpress.org/plugins/{$slug}/
w.org/plugin/{$slug}
如果标头有任何其他值,API 将不会返回任何结果。出于更新目的,它将忽略插件。
此外,WordPress 5.8 引入了 update_plugins_{$hostname}
filter,第三方插件可以使用它来为给定的主机名提供更新。
这个新钩子过滤给定插件主机名的更新响应。钩子名称的动态部分是$hostname
指在Update URI
标头字段中指定的 URI 的主机名。
钩子有四个参数:
$update
:插件用最新的细节更新数据。默认false
.$plugin_data
:插件提供的标题列表。$plugin_file
: 插件文件名。$locales
:已安装的语言环境以查找翻译。
它们可用于在多个用例中过滤更新响应。
作为参考,参见#32101,#23318,以及更改日志[50921] 。
起因
我最近对一个 WordPress 实例进行了定期安全代码审查,并注意到一个新的内部开发插件。我突然想到,如果攻击者能够冒充插件的 slug 并将恶意版本上传到 WordPress 插件目录,如果 SVN 版本高于使用的版本,我们可能会看到更新通知。
这将引入“混淆代理问题”攻击场景,其中特权用户被指示定期更新所有插件,无意中用恶意软件感染了实例。
有人可能会争辩说,困惑的副手不应该在没有先检查变更日志的情况下盲目地更新插件,但老实说,更新发布得太频繁,很快就会变得单调乏味。我们首先不应该将它们放在那个位置,因为它是通过默默无闻的安全性,相信外部攻击者无法弄清楚插件的名称。
为了证实这个假设,我开始研究它是否实际上是一个可能的攻击媒介以及它的广泛性。

WordPress.org 插件目录
WordPress.org 为希望开发插件的任何人提供免费托管。目录中的所有代码都应尽可能安全,但安全性是插件开发者的最终责任。
WordPress.org 插件目录是潜在用户下载和安装任何插件的最简单方法。一旦新插件获得批准,开发人员就可以访问(SVN)Subversion 存储库。SVN 存储库是一个发布存储库,而不是开发存储库。所有提交、代码或自述文件将触发与插件关联的 zip 文件的重新生成。
WordPress 与插件目录的集成意味着用户只需点击几下即可更新插件。当 SVN 中的插件版本增加时,用户会收到更新提醒。
理论上,任何遵循指南并通过审查过程的人都可以上传插件并在以后分发恶意版本。不幸的是,在阅读开发人员文档时,我发现了以下信息:
为什么我的提交失败说我的插件名称已经存在? 您正在尝试使用带有永久链接的插件,该插件存在于WordPress.org之外并且拥有大量用户群。 了解插件更新 API 的工作方式是将插件文件夹名称(即永久链接)与其托管在 WordPress.org 上的每个插件进行比较,了解这一点很重要。如果匹配,则它会检查更新并提示用户升级。 当这种情况发生时,“原始”插件(我们不托管的插件)的用户会从 WordPress.org 升级到插件,如果这不是您真正想要做的,您可能会破坏他们的网站。 有时,当公司或个人私下发布他们的插件(例如通过 Github)并决定要在 WordPress.org 上重新发布时,就会出现这种情况。在这些情况下,我们建议您给我们发送电子邮件,我们将引导您了解如何克服错误。
这意味着 WordPress 的安全团队正在内部跟踪所有在野外使用的无人认领的插件名称、安装数量,并且有一些神奇的阈值可以防止大规模供应链攻击。但它也验证了攻击向量确实可能存在的假设。
主题审批流程
大多数使用 WordPress 的商业网站都有自定义主题,并且 slug 名称几乎总是在 HTML 源代码中,用于从主题的资产目录加载 JS/CSS 文件。检测主题slug并接管无人认领的slug将是最简单的方法。
WordPress 主题目录确实有严格的主题审核流程和主题审核要求,以确保质量和安全性。新主题通常会以更高的标准来吸引新用户。
我很难开发出吸引普通观众的前端并通过审核,所以我决定不担心他们。
插件审批流程
另一方面,通过被动检查不可能检测到大多数插件,并且很难创建自定义词表。但是WordPress Plugin Directory的审核流程没有那么严格,指引也比较简单。
在阅读文档时,我主要对插件名称的限制感兴趣。已经熟悉 WordPress,我知道 slug 名称只能包含小写字母数字字符除以破折号,但我也了解到还有更多限制:
插件必须尊重商标、版权和项目名称。 名称不能“保留”以备将来使用或保护品牌。 除非可以确认合法所有权/代表的证明,否则禁止使用商标或其他项目作为插件 slug 的唯一或初始术语。 此政策扩展到插件 slug,我们不允许 slug 以另一个产品的术语开头。
这让我很失望,因为显然,对商标品牌进行了检查,这将保护大多数公司并禁止上传无人认领的插件(假设他们使用公司名称作为内部插件的前缀)。我还不想放弃,所以我开始四处寻找,结果发现 WordPress 团队是透明的粉丝,整个审批过程是自动化的,最重要的是,完全开源。
通过检查[class-upload-handler.php](https://meta.trac.wordpress.org/browser/sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-upload-handler.php)
检查,我们可以看到几个基本功能:
process_upload()
- 根据主插件文件中的插件名称确定插件 slug。(字母数字和破折号)
88 $this->plugin_slug = remove_accents( $this->plugin['Name'] );
89 $this->plugin_slug = preg_replace( '/[^a-z0-9 _.-]/i', '', $this->plugin_slug );
90 $this->plugin_slug = str_replace( '_', '-', $this->plugin_slug );
91 $this->plugin_slug = sanitize_title_with_dashes( $this->plugin_slug );
has_reserved_slug()
- 确保它不使用被认为不会被公众使用的 slug。(不允许使用 wp-admin 等常见的 WordPress 路径)
388 public function has_reserved_slug() {
389 $reserved_slugs = array(
390 // Plugin Directory URL parameters.
391 'about',
392 'admin',
393 'browse',
394 'category',
395 'developers',
396 'developer',
has_trademarked_slug()
- 确保它没有使用受商标保护的 slug。(不允许使用商标公司名称)
430 public function has_trademarked_slug() {
431 $trademarked_slugs = array(
432 'adobe-',
433 'adsense-',
434 'advanced-custom-fields-',
435 'adwords-',
436 'akismet-',
437 'all-in-one-wp-migration',
438 'amazon-',
439 'android-',
440 'apple-',
441 'applenews-',
442 'aws-',
令人惊讶的是,足够大 (FAANG) 风格的公司可以要求将自己添加到$trademarked_slugs
禁止术语列表 ( ) 中,任何包含该名称的插件的上传都会自动失败。最重要的是,还有一个检查阻止使用已经在野外使用的流行插件名称上传:
231 if ( function_exists( 'wporg_stats_get_plugin_name_install_count' ) ) {
232 $installs = wporg_stats_get_plugin_name_install_count( $this->plugin['Name'] );
233
234 if ( $installs && $installs->count >= 100 ) {
235 $error = __( 'Error: That plugin name is already in use.', 'wporg-plugins' );
综上所述,针对企业网站内部开发插件的WordPress Plugin Confusion攻击确实是可能的。尽管如此,安全机制仍可防止针对安装在 100 多个网站上的无人认领的插件的大规模供应链攻击。
扫描器检测易受攻击的插件
我写了一个简单的工具wp_update_confusion.py,它会被动地从首页响应中检测任何插件,并检查插件名称是否只包含允许的字符。然后它 ping WordPress SVN 以验证 slug 是否已经被声明。
在扫描 HackerOne 公共漏洞赏金计划的网站时,它报告了大量误报。我很快意识到,如果没有付费 WordPress 插件数据库,就不可能获得任何有意义的输出。而自己构建一个需要大量时间。
我没有重新发明轮子并从审查流程代码中知道 WordPress 团队已经拥有数据,而是开始环顾网站。每个插件都有一个“高级视图”选项卡,可以在其中查看各种图表,例如活动版本、每天的下载量、安装增长等。

仔细研究 API 发现了一个公开可用的端点,实际上,它返回任何无人认领的插件 slug 的all_time下载次数:
https://api.wordpress.org/stats/plugin/1.0/downloads.php?slug={plugin_name}&historical_summary=1
最后,拥有验证我们是否可以接管插件所需的所有信息会产生更好的结果。虽然我没有实施暴力破解选项,这将是获得漏洞赏金的最佳方式,所以现在,它只能检测低悬的果实 🙂
它的工作方式是:
1) 查询首页并找到所有插件re.findall("wp-content/plugins/(.*?)/", html)
2) 检查是否允许使用 slug
3) 检查 slug 是否存在于 SVN 注册表中
4) 检查 slug 是否安装在 100 多个网站上
5) 利润?

使用 Burp 调试 WP 更新
下一步是调试插件机制是如何工作的。最简单的解决方案是检查代码,但由于我对 PHP 不太熟悉,黑盒式测试听起来是个更好的主意。
由于我非常喜欢 Burp Suite,我决定拦截网站之间的请求,并wordpress.org API
通过其代理。在 Docker 容器中运行 WordPress 很容易,但安装 SSL 证书并通过 Burp 路由所有外部流量并不那么简单。
经过大量调试,我想出了以下内容:
1)配置Proxy Listener
监听所有接口
2)代理的添加IP地址extra_hosts
在docker-compose.yml
3)运行docker
并安装WordPress通过wp-cli
4) 下载并安装 Burp Suite 证书
#!/usr/bin/env sh
# Download & install Burp Suite certificate
wget -q http://burp:8080/cert -O cert.der
openssl x509 -in cert.der -inform DER -out cert.pem
mkdir /usr/local/share/ca-certificates/extra
cp cert.pem /usr/local/share/ca-certificates/extra/cert.crt
update-ca-certificates
rm cert.der cert.pem
5)将侦听器收到的所有请求重定向到我的主机
6) 将所有出现的 替换*api.wordpress.org*
为我的主机
你可以在这里看到最终的脚本:github.com/vavkamil/wp2burp
之后,我可以看到当网站检查是否有任何可用更新时,它会发出以下请求:
POST /plugins/update-check/1.1/ HTTP/2
Host: api.wordpress.org
User-Agent: WordPress/5.3; http://127.0.0.1:31337/
Accept: */*
Accept-Encoding: gzip, deflate
Referer: https://api.wordpress.org/plugins/update-check/1.1/
Connection: close
Content-Length: 1778
Content-Type: application/x-www-form-urlencoded
Expect: 100-continue
plugins={...}
其中 JSON 数据包含所有已安装插件的列表,例如:
{
"akismet\/akismet.php":{
"Name":"Akismet Anti-Spam",
"PluginURI":"https:\/\/akismet.com\/",
"Version":"4.1.3",
"Description":"Used by millions, Akismet is quite possibly the best way in the world to <strong>protect your blog from spam<\/strong>. It keeps your site protected even while you sleep. To get started: activate the Akismet plugin and then go to your Akismet Settings page to set up your API key.",
"Author":"Automattic",
"AuthorURI":"https:\/\/automattic.com\/wordpress-plugins\/",
"TextDomain":"akismet",
"DomainPath":"",
"Network":false,
"RequiresWP":"",
"RequiresPHP":"",
"Title":"Akismet Anti-Spam",
"AuthorName":"Automattic"
},
如果有可用的新版本,网站会收到以下回复:
HTTP/2 200 OK
Date: Sun, 10 Oct 2021 19:13:57 GMT
Content-Type: application/json
Access-Control-Allow-Origin: *
Cf-Cache-Status: DYNAMIC
Expect-Ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=NpTdm3rT6Twj%2FvltPnM2Lb627HEIH4tcXE%2FTqUW0ZSqB7QQlDef1ttcmizy5cx2qcGwpKR%2BmudmYA0tp0G5QVEJ8G4%2Fu%2Bh07GKDQfbBYlJext3lDiKXRNB0EHIi3lD35oLk%3D"}],"group":"cf-nel","max_age":604800}
Nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Server: cloudflare
Cf-Ray: 69c22b4018442794-PRG
Alt-Svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400
{"plugins":{"akismet\/akismet.php":{"new_version":"4.2.1","package":"https:\/\/downloads.wordpress.org\/plugin\/akismet.4.2.1.zip"}}}
如果您单击更新按钮,WordPress 会删除旧插件目录的内容,并下载并解压缩包含新版本的 .zip 文件,从而有效地替换所有文件。从道德上讲,利用这一点很棘手,因为使用任何概念证明,您都会破坏网站。
但是使用 docker 容器拦截和模拟攻击可能就足够了,因为您可以修改响应,因此它将包含您想要的任何版本和远程 zip 文件:

发布 WordPress 插件 PoC
我不想声明任何人的插件,因为更新会无意中破坏网站(旧插件文件将被删除)。尽管如此,我还是必须模拟攻击以确认它有效。
对我来说幸运的是,两年前我已经写了一个简单的 WP 插件“ XML-RPC 设置”来了解各种“xmlrpc.php”攻击向量的工作原理以及如何防御它们。我从来没有费心把它上传到 WordPress 插件目录,但有可能有人从我的 GitHub 上安装了它。例如,在我迁移到 GitHub 页面之前,它在这个博客上。可以肯定的是,不久前,当我考虑开始这项研究时,我在六个不同的网站上安装了该插件。
在阅读了 WordPress 开发者指南并更新自述文件后,我提交了插件以供审核。确切的时间表是:
- 2021 年 10 月 3 日
- 插件提交成功
- 2021 年 10 月 5 日
- 正在审核中,插件代码问题(错误的稳定版本标签,不够唯一的函数名称)
- 2021 年 10 月 5 日
- 修复了版本并要求再次审核
- 2021 年 10 月 7 日
- 您的审核已成功完成。
- 2021 年 10 月 7 日
- 恭喜,XML-RPC 设置的插件托管请求已获批准。
插件获得批准一小时后,我获得了对您的 Subversion (SVN) 存储库的提交访问权限并上传了我的插件:
$ sudo apt-get install subversion
$ svn co https://plugins.svn.wordpress.org/xml-rpc-settings wordpress/
$ cp xml-rpc-settings/* wordpress/.
$ cd wordpress
$ svn add trunk/*
$ svn ci -m 'feat(xml-rpc-settings): Add plugin files'
就这样,我发布了插件;现在,我不得不等到 WordPress 插件目录与 SVN 同步。过了一会儿,我注意到一个 Wordfence Slack 通知,告诉我它在几个网站上发现了一个问题,并且有一个新的插件更新可用;答对了!

我能够劫持安装在几个网站上的插件。虽然没有任何后门,但实际上你可以查看插件:

另一方面,攻击者现在可以在网站上立足。更糟糕的是,如果网站管理员启用了 WordPress 5.5 中引入的自动插件更新。这将允许攻击者随时更改插件文件,而无需任何用户交互。
如何保护自己
由于这在设计上本质上是易受攻击的,因此我不希望 WordPress 方面进行修复。如果您是一家足够大的公司,您可以联系他们并在$trademarked_slugs
列表中获取您的商标。由于不允许将“虚拟”插件上传到注册表,因此必须另辟蹊径。您可以遵循一些建议来确保您的网站安全。
保护主题
主题标头是小写的主题名称,空格替换为连字符 (-)。它也是主题的文件夹名称。主题团队可以根据名称拒绝主题,如果他们认为名称不合适或与现有主题或品牌过于相似,则可以要求更改名称。
- 主题名称不得使用:WordPress, Theme, Twenty*
- 在所有面向公众的文本中正确拼写“WordPress”:所有一个单词,同时带有大写的 W 和 P
- 没有侵犯商标权。
这意味着您可以通过以下方式重命名自定义主题:theme-internal_name
安全起见。
保护插件
2021 年 7 月 20 日发布的 WordPress 5.8 引入了一个新的“更新 URI”插件标头。
这允许第三方插件避免被 WordPress.org 插件目录中的类似名称的插件更新意外覆盖。
它有效地为网站维护者提供了一种防止供应链攻击的有效方法,因为内部插件永远不会要求更新。
主 PHP 文件应包含标题注释Update URI: false
,例如:
<?php
/**
* Plugin Name: Internal Plugin
* Version: 1.0
* Update URI: false
*/
您可以在此处阅读完整的公告:
make.wordpress.org../introducing-update-uri-plugin-header..
如果您由于任何遗留原因无法更新到 WordPress 5.8 并使用Update URI
缓解措施,您仍然有一些选择:
插件 slug 只能包含小写字母数字字符和破折号作为分隔符。一些关键字也被禁止,这在这种情况下可以帮助我们。这意味着您可以通过以下方式重命名自定义插件:
internal_plugin_name
InternalPluginName
wp-internal-plugin-name
还可以利用钩子编写自定义更新函数,阻止内部 WP API 调用并将其替换为您自己的调用,类似于付费插件从其服务器提供自定义更新的方式。
某些插件会为您执行此操作,例如Easy Updates Manager,它允许您阻止特定插件的更新。
话虽如此,您应该始终创建一个新备份并在更新任何插件之前阅读更改日志。
漏洞赏金狩猎
由于我不再拥有自己的侦察数据库,我从Chaos(带有赏金的公共 HackerOne 程序)中转储了大约 20 万个子域,并使用httpx扫描它们:
httpx -random-agent -l chaos_all_hackerone.txt -match-string "wp-content" -o httpx_wordpress.txt
这导致了大约 427 个 WordPress 网站。在使用概念验证wp_update_confusion.py
工具对其进行验证后,我发现了 13 个潜在目标。考虑到[8.2 High](https://chandanbn.github.io/cvss/#CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:L)
严重性,这还不错。

在仔细阅读每个漏洞赏金计划的政策后,我发现超过一半 (7) 的潜在目标超出了范围。这真是太糟糕了,因为有些属于知名公司,在我发布这项研究后,他们可能无论如何都会修复它。
不管怎样,我最终提交了六份报告。其中之一是 VDP 计划(不提供赏金)。

在 Twitter 上分享带有 #0day 标签的已编辑提交报告的屏幕截图后,我收到了来自@naglinagli的消息,提供了合作。这个提议是访问他广泛的侦察数据库,用我的有效载荷扫描主机,以换取未来的任何漏洞赏金支出。

由于 Nagli 通常是 HackerOne 声誉最高的前 5 名研究人员,我决定接受这个提议。主要是看有多少易受攻击的网站可能存在。赠送 50% 的潜在赏金可能有点贵,但无论如何我已经完成了扫描。
第一次尝试时,我们发现易受攻击的主机数量是我之前的扫描尝试的两倍多。我必须说合作很棒,他能够找到多少备受瞩目的目标给我留下了深刻的印象。
总而言之,我们提交了接近 25 份报告。不幸的是,其中很多都被关闭为信息性的。有些人认为他们的发布过程不会更新插件,这很可能要归功于 CI/CD 自动化。有些人不理解该报告,有些人将其关闭,因为它缺少明确的概念证明,认为这只是一个理论问题。

但其他人对提交的内容表示赞赏,并进行了修复,一家公司甚至因报告质量而奖励我们。您可以在此处查看报告示例:hackerone.com/reports/1364851
我们最终获得了大约 4000 美元的赏金,但最重要的是,我们让互联网变得更安全了。令人大开眼界的是,有多少顶级科技公司和世界知名公司可能受到影响。

这项研究在2021 年 11 月 25 日的OWASP 捷克分会会议上以演讲形式发表。