- 网络安全
- 漏洞扫描
- 渗透测试
- 应用安全
- 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
导读
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'''该条目传达了三层关键信息:
- 检测类型为
ChangeLog:WPScan 将"从插件自带的变更日志文件提取版本"视为一种独立的动态查找器类型; - 命中版本号为
0.1.1:即文件中以## Version 0.1.1标题出现、且按语义化版本排序后的最高版本; - 命中方式为
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_classes | BodyPattern |
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其工作流程可以拆解为四步:
发起请求:
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排除 404:只有响应状态码不是 404 时才继续匹配,避免把"文件不存在"误判为版本信息。
正则提取:将响应正文与配置中的
PATTERN进行匹配,并通过命名捕获组(?<v>...)取出版本号。生成 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 Tag | Readme - Stable Tag (Aggressive Detection) | 80 |
ChangeLog Section | Readme - 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 插件版本检测的一条完整链路:
- 约定路径:变更日志文件(
CHANGELOG.md/changelog.txt)作为可被主动探测的公开文件,是版本信息的天然泄露点; - 动态配置:
dynamic_finders.yml中以ChangeLog+BodyPattern+path+pattern描述检测方式,运行时通过create_child_class动态生成查找器; - 底层匹配:
BodyPattern#find通过一次 HTTP 请求与一次正则匹配提取(?<v>...)命名组,并记录interesting_entries与默认置信度 60; - 双保险: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
相关推荐
WPScan 插件版本检测实战:以 Clear Floats Button 的 CHANGELOG.md 动态查找器为例
WPScan 插件版本检测实战:以 Clear Floats Button 的 CHANGELOG.md 动态查找器为例 本文以 WPScan 仓库中 clea
网络安全漏洞扫描渗透测试应用安全CLIWPScan 插件版本检测实战:以 get-the-image 的 changelog.md 为例解析 ChangeLog 动态查找机制
WPScan 插件版本检测实战:以 get the image 的 changelog.md 为例解析 ChangeLog 动态查找机制 导读 WordPres
网络安全漏洞扫描渗透测试应用安全CLIWPScan 动态识别插件版本:以 BlockMeister 的 changelog.md 为例解读 ChangeLog 探测机制
WPScan 动态识别插件版本:以 BlockMeister 的 changelog.md 为例解读 ChangeLog 探测机制 导读 本文围绕 WPScan
网络安全漏洞扫描渗透测试应用安全CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考