module WPScan::Target::Platform::WordPress

Some WordPress specific implementation

wp-content & plugins directory implementation

Constants

WORDPRESS_HOSTED_PATTERN
WORDPRESS_PATTERN
WP_ADMIN_AJAX_PATTERN
WP_JSON_OEMBED_PATTERN

Attributes

mu_plugins[RW]

These methods are used in the associated interesting_findings finders to keep the boolean state of the finding rather than re-check the whole thing again

mu_plugins?[RW]

These methods are used in the associated interesting_findings finders to keep the boolean state of the finding rather than re-check the whole thing again

multisite[RW]

These methods are used in the associated interesting_findings finders to keep the boolean state of the finding rather than re-check the whole thing again

multisite?[RW]

These methods are used in the associated interesting_findings finders to keep the boolean state of the finding rather than re-check the whole thing again

registration_enabled[RW]

These methods are used in the associated interesting_findings finders to keep the boolean state of the finding rather than re-check the whole thing again

registration_enabled?[RW]

These methods are used in the associated interesting_findings finders to keep the boolean state of the finding rather than re-check the whole thing again

Public Instance Methods

content_dir() click to toggle source

@return [ String ] The wp-content directory

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 17
def content_dir
  unless @content_dir
    # scope_url_pattern is from CMSScanner::Target
    pattern = %r{#{scope_url_pattern}([\w\s\-/]+?)\\?/(?:themes|plugins|uploads|cache)\\?/}i

    [homepage_res, error_404_res].each do |page_res|
      in_scope_uris(page_res, '//link/@href|//script/@src|//img/@src') do |uri|
        return @content_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
      end

      # Checks for the pattern in raw JS code, as well as @content attributes of meta tags
      xpath_pattern_from_page('//script[not(@src)]|//meta/@content', pattern, page_res) do |match|
        return @content_dir = match[1]
      end
    end

    return @content_dir = 'wp-content' if default_content_dir_exists?
  end

  @content_dir
end
content_dir=(dir) click to toggle source
# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 8
def content_dir=(dir)
  @content_dir = dir.chomp('/')
end
content_uri() click to toggle source

@return [ Addressable::URI ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 46
def content_uri
  uri.join("#{content_dir}/")
end
content_url() click to toggle source

@return [ String ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 51
def content_url
  content_uri.to_s
end
default_content_dir_exists?() click to toggle source
# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 39
def default_content_dir_exists?
  # url('wp-content') can't be used here as the folder has not yet been identified
  # and the method would try to replace it by nil which would raise an error
  [200, 401, 403].include?(Browser.forge_request(uri.join('wp-content/').to_s, head_or_get_params).run.code)
end
do_login(username, password) click to toggle source

@param [ String ] username @param [ String ] password

@return [ Typhoeus::Response ]

# File lib/wpscan/target/platform/wordpress.rb, line 119
def do_login(username, password)
  login_request(username, password).run
end
login_request(username, password) click to toggle source

@param [ String ] username @param [ String ] password

@return [ Typhoeus::Request ]

# File lib/wpscan/target/platform/wordpress.rb, line 127
def login_request(username, password)
  Browser.instance.forge_request(
    login_url,
    method: :post,
    cache_ttl: 0,
    body: { log: username, pwd: password }
  )
end
login_url() click to toggle source

The login page is checked for a potential redirection (from http to https) the first time the method is called, and the effective_url is then used if suitable, otherwise the default wp-login will be.

If the login_uri CLI option has been provided, it will be returne w/o redirection check.

@return [ String, false ] The URL to the login page or false if not detected

# File lib/wpscan/target/platform/wordpress.rb, line 143
def login_url
  return @login_url unless @login_url.nil?
  return @login_url = url(ParsedCli.login_uri) if ParsedCli.login_uri

  @login_url = url('wp-login.php')

  res = Browser.get_and_follow_location(@login_url)

  @login_url = res.effective_url if res.effective_url =~ /wp-login\.php\z/i && in_scope?(res.effective_url)
  @login_url = false if res.code == 404

  @login_url
end
maybe_add_cookies() click to toggle source

Sometimes there is a mechanism in place on the blog, which requires a specific cookie and value to be added to requests. Lets try to detect and add them

# File lib/wpscan/target/platform/wordpress.rb, line 75
def maybe_add_cookies
  COOKIE_PATTERNS.each do |cookie_key, pattern|
    next unless homepage_res.body =~ pattern

    browser = Browser.instance

    cookie_string = "#{cookie_key}=#{Regexp.last_match[:c_value]}"

    cookie_string += "; #{browser.cookie_string}" if browser.cookie_string

    browser.cookie_string = cookie_string

    # Force recheck of the homepage when retying wordpress?
    # No need to clear the cache, as the request (which will contain the cookies)
    # will be different
    @homepage_res = nil
    @homepage_url = nil

    break
  end
end
plugin_url(slug) click to toggle source

@param [ String ] slug

@return [ String ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 73
def plugin_url(slug)
  plugins_uri.join("#{Addressable::URI.encode(slug)}/").to_s
end
plugins_dir() click to toggle source

@return [ String ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 56
def plugins_dir
  @plugins_dir ||= "#{content_dir}/plugins"
end
plugins_dir=(dir) click to toggle source
# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 12
def plugins_dir=(dir)
  @plugins_dir = dir.chomp('/')
end
plugins_uri() click to toggle source

@return [ Addressable::URI ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 61
def plugins_uri
  uri.join("#{plugins_dir}/")
end
plugins_url() click to toggle source

@return [ String ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 66
def plugins_url
  plugins_uri.to_s
end
registration_url() click to toggle source

@return [ String ]

# File lib/wpscan/target/platform/wordpress.rb, line 98
def registration_url
  multisite? ? url('wp-signup.php') : url('wp-login.php?action=register')
end
sub_dir() click to toggle source

@return [ String, False ] String of the sub_dir found, false otherwise @note: nil can not be returned here, otherwise if there is no sub_dir

the check would be done each time, which would make enumeration of
long list of items very slow to generate
# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 103
def sub_dir
  return @sub_dir unless @sub_dir.nil?

  # url_pattern is from CMSScanner::Target
  pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp-includes/)}i
  xpath = '(//@src|//@href|//@data-src)[contains(., "xmlrpc.php") or contains(., "wp-includes/")]'

  [homepage_res, error_404_res].each do |page_res|
    in_scope_uris(page_res, xpath) do |uri|
      return @sub_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
    end
  end

  @sub_dir = false
end
theme_url(slug) click to toggle source

@param [ String ] slug

@return [ String ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 95
def theme_url(slug)
  themes_uri.join("#{Addressable::URI.encode(slug)}/").to_s
end
themes_dir() click to toggle source

@return [ String ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 78
def themes_dir
  @themes_dir ||= "#{content_dir}/themes"
end
themes_uri() click to toggle source

@return [ Addressable::URI ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 83
def themes_uri
  uri.join("#{themes_dir}/")
end
themes_url() click to toggle source

@return [ String ]

# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 88
def themes_url
  themes_uri.to_s
end
url(path = nil) click to toggle source

Override of the WebSite#url to consider the custom WP directories

@param [ String ] path Optional path to merge with the uri

@return [ String ]

Calls superclass method
# File lib/wpscan/target/platform/wordpress/custom_directories.rb, line 124
def url(path = nil)
  return @uri.to_s unless path

  if %r{wp-content/plugins}i.match?(path)
    new_path = path.gsub('wp-content/plugins', plugins_dir)
  elsif /wp-content/i.match?(path)
    new_path = path.gsub('wp-content', content_dir)
  elsif path[0] != '/' && sub_dir
    new_path = "#{sub_dir}/#{path}"
  end

  super(new_path || path)
end
wordpress?(detection_mode) click to toggle source

@param [ Symbol ] detection_mode

@return [ Boolean ] Whether or not the target is running WordPress

# File lib/wpscan/target/platform/wordpress.rb, line 29
def wordpress?(detection_mode)
  [homepage_res, error_404_res].each do |page_res|
    return true if wordpress_from_meta_comments_or_scripts?(page_res)
  end

  if %i[mixed aggressive].include?(detection_mode)
    %w[wp-admin/install.php wp-login.php].each do |path|
      res = Browser.get_and_follow_location(url(path))

      next unless res.code == 200

      in_scope_uris(res, '//link/@href|//script/@src') do |uri|
        return true if WORDPRESS_PATTERN.match?(uri.path)
      end
    end
  end

  false
end
wordpress_from_meta_comments_or_scripts?(response) click to toggle source

@param [ Typhoeus::Response ] response @return [ Boolean ]

# File lib/wpscan/target/platform/wordpress.rb, line 51
def wordpress_from_meta_comments_or_scripts?(response)
  in_scope_uris(response, '//link/@href|//script/@src') do |uri|
    return true if WORDPRESS_PATTERN.match?(uri.path) || WP_JSON_OEMBED_PATTERN.match?(uri.path)
  end

  return true if response.html.css('meta[name="generator"]').any? do |node|
    /wordpress/i.match?(node['content'])
  end

  return true unless comments_from_page(/wordpress/i, response).empty?

  return true if response.html.xpath('//script[not(@src)]').any? do |node|
    WP_ADMIN_AJAX_PATTERN.match?(node.text)
  end

  false
end
wordpress_hosted?() click to toggle source

@return [ Boolean ] Whether or not the target is hosted on wordpress.com

# File lib/wpscan/target/platform/wordpress.rb, line 103
def wordpress_hosted?
  return true if /\.wordpress\.com$/i.match?(uri.host)

  unless content_dir
    uris_from_page(homepage_res, '(//@href|//@src)[contains(., "wp.com")]') do |uri|
      return true if uri.to_s.match?(WORDPRESS_HOSTED_PATTERN)
    end
  end

  false
end