☰
WPScan 插件版本检测实战:以 bonaire 的 CHANGELOG.md 为例解读 ChangeLog 动态查找器
2026/9/25 12:47:39 网站建设 项目流程
  • 网络安全
  • 漏洞扫描
  • 渗透测试
  • 应用安全
  • CLI

【免费下载链接】wpscan

WPScan WordPress security scanner. Written for security professionals and blog maintainers to test the security of their WordPress websites. Contact us via contact@wpscan.com

项目地址:https://gitcode.com/gh_mirrors/wp/wpscan
点击查看免费下载

导读

WordPress 插件常常会随安装包附带一份CHANGELOG.md或changelog.txt,其中按版本号记录了变更历史。WPScan 充分利用了这一事实:在枚举插件版本时,扫描器会主动请求这类变更日志文件,并从其中的版本号行快速识别出插件当前版本。本文以仓库中 bonaire 插件版本检测的测试夹具(fixture)CHANGELOG.md 为切入点,结合expected.yml预期结果与dynamic_finders.yml配置、以及BodyPattern与 Readme 解析器的源码实现,完整讲解 WPScan 的 ChangeLog 动态查找器(Dynamic Finder)的工作原理、配置写法与检测置信度,帮助你理解扫描输出的版本号究竟从何而来。

bonaire 插件的 CHANGELOG.md:一份典型的版本检测目标

仓库中spec/fixtures/dynamic_finders/plugin_version/bonaire/change_log/CHANGELOG.md是用于验证 WPScan 动态查找器行为的测试夹具,完整内容如下:

# Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## Version 0.1.1 ### Changed - Code cleanup - Ensured compatibility with PHPMailer ## Version 0.1.0 First commit.

这份文件本身是虚构插件bonaire的变更日志,它遵循Keep a Changelog与Semantic Versioning(语义化版本)两个社区规范:每个版本用## Version x.y.z标题隔开,标题行中明确包含版本号。正是这种"标题行携带版本号"的固定格式,让 WPScan 可以在拿到该文件后,用一行正则即可稳定提取最新版本。

与这份夹具对应的预期检测结果记录在 spec/fixtures/dynamic_finders/expected.yml 中:

bonaire: ChangeLog: number: 0.1.1 found_by: Change Log (Aggressive Detection) interesting_entries: - 'http://wp.lab/wp-content/plugins/bonaire/CHANGELOG.md, Match: ''Version 0.1.1'''

该条目传达了三层关键信息:

  1. 检测类型为ChangeLog:WPScan 将"从插件自带的变更日志文件提取版本"视为一种独立的动态查找器类型;
  2. 命中版本号为0.1.1:即文件中以## Version 0.1.1标题出现、且按语义化版本排序后的最高版本;
  3. 命中方式为found_by: Change Log (Aggressive Detection):说明该检测属于主动(Aggressive)探测——扫描器会主动请求wp-content/plugins/bonaire/CHANGELOG.md这个 URL,并在响应正文中匹配到Version 0.1.1字样。

动态查找器机制:ChangeLog 是允许的查找器类型之一

ChangeLog并不是一个硬编码在扫描流程里的独立类,而是通过 WPScan 的**动态查找器(Dynamic Finder)**体系驱动的。其核心配置定义在 lib/wpscan/db/dynamic_finders/base.rb 中:

# @return [ Array<Symbol> ] def self.allowed_classes # The Readme is not put in there as it's not a Real DF, but rather using the DF system # to get the list of potential filenames for a given slug @allowed_classes ||= %i[Comment Xpath HeaderPattern BodyPattern JavascriptVar QueryParameter ConfigParser] end

从源码注释可以明确看到:Readme被刻意排除在动态查找器之外,因为readme.txt的解析是独立机制(见下文);而BodyPattern等类才是真正的动态查找器。ChangeLog检测正是通过配置为class: BodyPattern的条目来实现的——这一点可以在真实配置 spec/fixtures/db/dynamic_finders.yml 中看到典型写法(以2fas插件为例):

2fas: ChangeLog: class: BodyPattern path: changelog.txt pattern: !ruby/regexp /^= (?<v>\d+\.[\.\d]+)/i version: true Readme: path: readme.txt TranslationFile: class: BodyPattern path: languages/2fas-pt_BR.po pattern: !ruby/regexp '/ion: 2FAS [^\s]+ Two Factor Authentication (?<v>\d+\.[\.\d]+)/i' version: true

这里展示了动态查找器配置的通用结构,每一项字段含义如下:

字段含义示例值
ChangeLog/TranslationFile等动态查找器名称,用于定位配置ChangeLog
class实际使用的查找器实现类,必须属于allowed_classesBodyPattern
path相对插件目录(wp-content/plugins/<slug>/)的检测文件路径changelog.txt、CHANGELOG.md、languages/2fas-pt_BR.po
pattern提取版本号的正则,必须包含(?<v>...)命名捕获组/^= (?<v>\d+\.[\.\d]+)/i
version标记该查找器用于提取版本号(而非用于确认插件存在)true

可以看出,ChangeLog 类查找器的本质是:"在插件目录下某个已知路径的变更日志文件中,用正则匹配版本号"。只要path指向changelog.txt、CHANGELOG.md等约定文件名,并给出适配该文件格式的pattern,即可完成一次版本探测。

配置的分发由 lib/wpscan/db/dynamic_finders/base.rb 中的method_missing完成:当代码调用aggressive_xxx_finder_configs之类的接口时,会根据allowed_classes校验类名,再返回对应查找器配置。而expected.yml中 bonaire 条目的found_by: Change Log (Aggressive Detection),正是这种Aggressive + ChangeLog组合在输出层呈现出来的形式。

BodyPattern 底层实现:一次请求 + 一次正则匹配

ChangeLog 检测最终由BodyPattern类执行。其实现位于 lib/wpscan/finders/dynamic_finder/version/body_pattern.rb:

class BodyPattern < Finders::DynamicFinder::Version::Finder # @return [ Hash ] def self.child_class_constants @child_class_constants ||= super.merge(PATTERN: nil, CONFIDENCE: 60) end # @param [ Typhoeus::Response ] response # @param [ Hash ] opts # @return [ Version ] def find(response, _opts = {}) return unless response.code != 404 && response.body =~ self.class::PATTERN create_version( Regexp.last_match[:v], interesting_entries: ["#{response.effective_url}, Match: '#{Regexp.last_match}'"] ) end end

其工作流程可以拆解为四步:

  1. 发起请求:aggressive模式会根据配置中的PATH拼出完整 URL 并发起请求,调用链见 lib/wpscan/finders/dynamic_finder/finder.rb:

    def aggressive(opts = {}) return unless self.class::PATH find(Browser.get(target.url(self.class::PATH)), opts) end
  2. 排除 404:只有响应状态码不是 404 时才继续匹配,避免把"文件不存在"误判为版本信息。

  3. 正则提取:将响应正文与配置中的PATTERN进行匹配,并通过命名捕获组(?<v>...)取出版本号。

  4. 生成 Version 模型:调用 lib/wpscan/finders/dynamic_finder/version/finder.rb 的create_version,把正则匹配片段写入interesting_entries——这与expected.yml中Match: 'Version 0.1.1'的格式完全吻合;同时BodyPattern类默认置信度为60(CONFIDENCE: 60)。

每个插件的查找器子类都由create_child_class(见 lib/wpscan/finders/dynamic_finder/finder.rb)在运行时按dynamic_finders.yml中的配置动态生成,因此2fas的changelog.txt与 bonaire 的CHANGELOG.md虽然在文件路径与格式上不同,却可以复用同一套BodyPattern逻辑。

Readme 解析中的 ChangeLog Section:另一种版本来源

除了直接请求插件的CHANGELOG.md,WPScan 还会在解析插件标准的readme.txt时,把其中的 **Changelog 章节(ChangeLog Section)**作为第二重版本来源。相关实现位于 app/finders/plugin_version/readme.rb:

# @return [ String, nil ] The best version number detected from the changelog section def from_changelog_section(body) extracted_versions = body.scan(/^=+\s+(?:v(?:ersion)?\s*)?([0-9.-]+)[^=]*=+$/i) return if extracted_versions.nil? || extracted_versions.empty? extracted_versions.flatten! # must contain at least one number extracted_versions = extracted_versions.grep(/[0-9]+/) sorted = extracted_versions.sort do |x, y| Gem::Version.new(x) <=> Gem::Version.new(y) rescue StandardError 0 end sorted.last end

这段逻辑与 bonaire 的CHANGELOG.md场景高度呼应:

  • 它扫描== 版本号 ==形式的章节标题行(兼容v/version前缀);
  • 过滤掉不含数字的行,再借助 Ruby 的Gem::Version对提取出的版本号做语义化排序;
  • 取排序后的最后一个(即最高版本)作为检测结果。

在 app/finders/plugin_version/readme.rb 的version_numbers方法中,两个来源会被合并返回,并携带不同的置信度:

来源found_by 消息置信度
Stable TagReadme - Stable Tag (Aggressive Detection)80
ChangeLog SectionReadme - ChangeLog Section (Aggressive Detection)50

置信度差异体现了工程上的审慎:Stable Tag是插件作者在 readme 头部声明的正式版本号,可信度更高;而从 Changelog 章节推断出的版本虽然实用,但由于历史版本章节可能残留、格式不规范等原因,可信度相对较低。

对应的测试位于 spec/app/finders/plugin_version/readme_spec.rb,其中用一个哈希表覆盖了大量真实插件的变化日志格式变体(如1.3、2.64、2.0.66.33、1.2.3、2.1.5、3.1、1.5.9、1.0.4、2.27等),逐一断言from_changelog_section的提取结果——这也为"ChangeLog 检测对格式差异有较强容忍度"提供了测试层面的佐证。

如何在真实扫描中验证

要在真实环境复现上述检测行为,只需对目标站点运行插件枚举(主动模式),WPScan 会依次请求目标wp-content/plugins/<slug>/下的readme.txt、CHANGELOG.md、changelog.txt等已知路径:

ruby wpscan.rb --url https://example.com --enumerate p --plugins-detection aggressive

当检测命中时,CLI 输出中会以[!]行列出插件版本,其found_by字段即本文所述来源之一(如Change Log (Aggressive Detection)或Readme - ChangeLog Section (Aggressive Detection))。需要说明的是:前提是目标站点允许通过 HTTP 直接访问这些文件——若服务器屏蔽了CHANGELOG.md这类路径或返回 404,BodyPattern#find会因response.code != 404判断失败而返回nil,该来源自然失效,这与源码中的守卫条件一致。

小结

通过 bonaire 的 CHANGELOG.md 这份测试夹具,可以梳理出 WPScan 插件版本检测的一条完整链路:

  1. 约定路径:变更日志文件(CHANGELOG.md/changelog.txt)作为可被主动探测的公开文件,是版本信息的天然泄露点;
  2. 动态配置:dynamic_finders.yml中以ChangeLog+BodyPattern+path+pattern描述检测方式,运行时通过create_child_class动态生成查找器;
  3. 底层匹配:BodyPattern#find通过一次 HTTP 请求与一次正则匹配提取(?<v>...)命名组,并记录interesting_entries与默认置信度 60;
  4. 双保险:readme 解析器的from_changelog_section另行从== 版本 ==章节提取版本,两者互为补充,最终汇总到Version模型供扫描结果输出。

理解这条链路,有助于安全研究者判断扫描报告中插件版本号的可信度来源,也能帮助插件开发者认识到:一份格式规范的 CHANGELOG 文件,本身就是站点安全信息暴露面的一部分。

  • 网络安全
  • 漏洞扫描
  • 渗透测试
  • 应用安全
  • CLI

【免费下载链接】wpscan

WPScan WordPress security scanner. Written for security professionals and blog maintainers to test the security of their WordPress websites. Contact us via contact@wpscan.com

项目地址:https://gitcode.com/gh_mirrors/wp/wpscan
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询