module Wpxf::WordPress::Fingerprint

Provides functionality for fingerprinting WordPress and its components.

Constants

WORDPRESS_ATOM_VERSION_PATTERN
WORDPRESS_GENERATOR_VERSION_PATTERN
WORDPRESS_OPML_VERSION_PATTERN
WORDPRESS_RDF_VERSION_PATTERN
WORDPRESS_README_VERSION_PATTERN
WORDPRESS_RSS_VERSION_PATTERN
WORDPRESS_SITEMAP_VERSION_PATTERN
WORDPRESS_VERSION_PATTERN

Public Instance Methods

check_plugin_version_from_changelog(plugin_name, file_name, fixed = nil, introduced = nil) click to toggle source

Checks a plugin's changelog for a vulnerable version. @param plugin_name [String] the name of the plugin. @param file_name [String] the name of the file that contains the changelog. @param fixed [String] the version the vulnerability was fixed in. @param introduced [String] the version the vulnerability was introduced in. @return [Symbol] :unknown, :vulnerable or :safe.

# File lib/wpxf/wordpress/fingerprint.rb, line 65
def check_plugin_version_from_changelog(plugin_name, file_name, fixed = nil, introduced = nil)
  changelog = normalize_uri(wordpress_url_plugins, plugin_name, file_name)
  check_version_from_custom_file(changelog, /=\s([\d\.]+)\s=/, fixed, introduced)
end
check_plugin_version_from_readme(name, fixed = nil, introduced = nil) click to toggle source

Checks a plugin's readme for a vulnerable version. @param name [String] the name of the plugin. @param fixed [String] the version the vulnerability was fixed in. @param introduced [String] the version the vulnerability was introduced in. @return [Symbol] :unknown, :vulnerable or :safe.

# File lib/wpxf/wordpress/fingerprint.rb, line 55
def check_plugin_version_from_readme(name, fixed = nil, introduced = nil)
  _check_version_from_readme(:plugin, name, fixed, introduced)
end
check_theme_version_from_readme(name, fixed = nil, introduced = nil) click to toggle source

Checks a theme's readme for a vulnerable version. @param name [String] the name of the theme. @param fixed [String] the version the vulnerability was fixed in. @param introduced [String] the version the vulnerability was introduced in. @return [Symbol] :unknown, :vulnerable or :safe.

# File lib/wpxf/wordpress/fingerprint.rb, line 46
def check_theme_version_from_readme(name, fixed = nil, introduced = nil)
  _check_version_from_readme(:theme, name, fixed, introduced)
end
check_theme_version_from_style(name, fixed = nil, introduced = nil) click to toggle source

Checks the style.css file for a vulnerable version. @param name [String] the name of the theme. @param fixed [String] the version the vulnerability was fixed in. @param introduced [String] the version the vulnerability was introduced in. @return [Symbol] :unknown, :vulnerable or :safe.

# File lib/wpxf/wordpress/fingerprint.rb, line 30
def check_theme_version_from_style(name, fixed = nil, introduced = nil)
  style_uri = normalize_uri(wordpress_url_themes, name, 'style.css')
  res = execute_get_request(url: style_uri)

  # No style.css file present
  return :unknown if res.nil? || res.code != 200

  pattern = _extension_version_pattern(:style)
  _extract_and_check_version(res.body, pattern, fixed, introduced)
end
check_version_from_custom_file(url, regex, fixed = nil, introduced = nil) click to toggle source

Checks a custom file for a vulnerable version. @param url [String] the relative path of the file. @param regex [Regexp] the regular expression to extract the version. @param fixed [String] the version the vulnerability was fixed in. @param introduced [String] the version the vulnerability was introduced. @return [Symbol] :unknown, :vulnerable or :safe.

# File lib/wpxf/wordpress/fingerprint.rb, line 76
def check_version_from_custom_file(url, regex, fixed = nil, introduced = nil)
  res = execute_get_request(url: url)
  return :unknown unless res && res.code == 200
  _extract_and_check_version(res.body, regex, fixed, introduced)
end
wordpress_and_online?() click to toggle source

Check if the host is online and running WordPress. @return [Boolean] true if the host is online and running WordPress.

# File lib/wpxf/wordpress/fingerprint.rb, line 7
def wordpress_and_online?
  res = execute_get_request(url: full_uri)
  return false unless res && res.code == 200
  return true if _wordpress_fingerprint_regexes.any? { |r| res.body =~ r }
  false
end
wordpress_version() click to toggle source

Extract the WordPress version information from various sources. @return [Version, nil] the version if found, nil otherwise.

# File lib/wpxf/wordpress/fingerprint.rb, line 16
def wordpress_version
  _wordpress_version_fingerprint_sources.each do |url, pattern|
    res = execute_get_request(url: url)
    match = res.body.match(pattern) if res && res.code == 200
    return Gem::Version.new(match[1]) if match
  end
  nil
end

Private Instance Methods

_check_version_from_readme(type, name, fixed = nil, introduced = nil) click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 128
def _check_version_from_readme(type, name, fixed = nil, introduced = nil)
  readme = _get_first_readme(name, type)
  if readme.nil?
    # No readme present for plugin
    return :unknown if type == :plugin

    # Try again using the style.css file, if it is a theme.
    return check_theme_version_from_style(name, fixed, introduced) if type == :theme
  end

  # If all versions are vulnerable and we found a file, end the process here
  # and return a vulnerable state, as some readmes will not have a version.
  return :vulnerable if fixed.nil? && introduced.nil?

  state = _extension_is_vulnerable(type, readme, fixed, introduced)
  if state == :no_version_found
    # If no version could be found in readme.txt for a theme, try style.css
    return check_theme_version_from_style(name, fixed, introduced)
  end

  state
end
_content_directory_name(type) click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 190
def _content_directory_name(type)
  case type
  when :plugin
    'plugins'
  when :theme
    'themes'
  else
    raise("Unknown readme type #{type}")
  end
end
_extension_is_vulnerable(type, readme, fixed, introduced) click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 151
def _extension_is_vulnerable(type, readme, fixed, introduced)
  pattern = _extension_version_pattern(:readme)
  vuln = _extract_and_check_version(readme, pattern, fixed, introduced)
  return :no_version_found if vuln == :unknown && type == :theme
  vuln
end
_extension_version_pattern(type) click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 224
def _extension_version_pattern(type)
  case type
  when :readme
    # Example line:
    # Stable tag: 2.6.6
    /(?:stable tag):\s*(?!trunk)([0-9a-z.-]+)/i
  when :style
    # Example line:
    # Version: 1.5.2
    /(?:Version):\s*([0-9a-z.-]+)/i
  else
    raise("Unknown file type #{type}")
  end
end
_extract_and_check_version(body, pattern, fixed = nil, introduced = nil) click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 212
def _extract_and_check_version(body, pattern, fixed = nil, introduced = nil)
  version = _extract_highest_version(body, pattern)
  return :unknown if version.nil?

  version = Gem::Version.new(version)
  fixed = Gem::Version.new(fixed) unless fixed.nil?
  introduced = Gem::Version.new(introduced) unless introduced.nil?

  emit_info "Found version #{version}", true
  _version_vulnerable?(version, fixed, introduced)
end
_extract_highest_version(body, pattern) click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 201
def _extract_highest_version(body, pattern)
  version = nil

  body.scan(pattern) do |match|
    match_version = Gem::Version.new(match[0])
    version = match_version if version.nil? || match_version > version
  end

  version
end
_get_first_readme(name, type) click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 158
def _get_first_readme(name, type)
  res = nil
  folder = _content_directory_name(type)
  readmes = ['readme.txt', 'Readme.txt', 'README.txt']
  readmes.each do |readme|
    readme_url = normalize_uri(wordpress_url_wp_content, folder, name, readme)
    res = execute_get_request(url: readme_url)
    break if res && res.code == 200
  end

  return res.body if res && res.code == 200
  nil
end
_version_vulnerable?(version, fixed, introduced) click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 172
def _version_vulnerable?(version, fixed, introduced)
  return :vulnerable if fixed.nil? && introduced.nil?

  if fixed && !introduced
    return :vulnerable if version < fixed
  end

  if !fixed && introduced
    return :vulnerable if version >= introduced
  end

  if fixed && introduced
    return :vulnerable if version >= introduced && version < fixed
  end

  :safe
end
_wordpress_fingerprint_regexes() click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 119
def _wordpress_fingerprint_regexes
  [
    %r{["'][^"']*\/#{Regexp.escape(wp_content_dir)}\/[^"']*["']}i,
    %r{<link rel=["']wlwmanifest["'].*href=["'].*\/wp-includes\/
      wlwmanifest\.xml["'] \/>}i,
    %r{<link rel=["']pingback["'].*href=["'].*\/xmlrpc\.php["'](?: \/)*>}i
  ]
end
_wordpress_version_fingerprint_sources() click to toggle source
# File lib/wpxf/wordpress/fingerprint.rb, line 107
def _wordpress_version_fingerprint_sources
  {
    full_uri => WORDPRESS_GENERATOR_VERSION_PATTERN,
    wordpress_url_readme => WORDPRESS_README_VERSION_PATTERN,
    wordpress_url_rss => WORDPRESS_RSS_VERSION_PATTERN,
    wordpress_url_rdf => WORDPRESS_RDF_VERSION_PATTERN,
    wordpress_url_atom => WORDPRESS_ATOM_VERSION_PATTERN,
    wordpress_url_sitemap => WORDPRESS_SITEMAP_VERSION_PATTERN,
    wordpress_url_opml => WORDPRESS_OPML_VERSION_PATTERN
  }
end