Module: WPScan::Target::Platform::WordPress

Includes:
CMSScanner::Target::Platform::PHP
Included in:
WPScan::Target
Defined in:
lib/wpscan/target/platform/wordpress.rb,
lib/wpscan/target/platform/wordpress/custom_directories.rb

Overview

wp-content & plugins directory implementation

Constant Summary collapse

WORDPRESS_PATTERN =
%r{/(?:(?:wp-content/(?:themes|(?:mu-)?plugins|uploads))|wp-includes)/}i.freeze
WORDPRESS_HOSTED_PATTERN =
%r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i.freeze
WP_JSON_OEMBED_PATTERN =
%r{/wp-json/oembed/}i.freeze
WP_ADMIN_AJAX_PATTERN =
%r{\\?/wp-admin\\?/admin-ajax\.php}i.freeze
{
  'vjs' => /createCookie\('vjs','(?<c_value>\d+)',\d+\);/i
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#mu_pluginsObject Also known as: mu_plugins?

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



21
22
23
# File 'lib/wpscan/target/platform/wordpress.rb', line 21

def mu_plugins
  @mu_plugins
end

#multisiteObject Also known as: multisite?

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



21
22
23
# File 'lib/wpscan/target/platform/wordpress.rb', line 21

def multisite
  @multisite
end

#registration_enabledObject Also known as: registration_enabled?

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



21
22
23
# File 'lib/wpscan/target/platform/wordpress.rb', line 21

def registration_enabled
  @registration_enabled
end

Instance Method Details

#content_dirString

Returns The wp-content directory.

Returns:

  • (String)

    The wp-content directory



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 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) ⇒ Object



8
9
10
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 8

def content_dir=(dir)
  @content_dir = dir.chomp('/')
end

#content_uriAddressable::URI

Returns:

  • (Addressable::URI)


46
47
48
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 46

def content_uri
  uri.join("#{content_dir}/")
end

#content_urlString

Returns:

  • (String)


51
52
53
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 51

def content_url
  content_uri.to_s
end

#default_content_dir_exists?Boolean

Returns:

  • (Boolean)


39
40
41
42
43
# 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) ⇒ Typhoeus::Response

Parameters:

  • username (String)
  • password (String)

Returns:



119
120
121
# File 'lib/wpscan/target/platform/wordpress.rb', line 119

def (username, password)
  (username, password).run
end

#login_request(username, password) ⇒ Typhoeus::Request

Parameters:

  • username (String)
  • password (String)

Returns:

  • (Typhoeus::Request)


127
128
129
130
131
132
133
134
# File 'lib/wpscan/target/platform/wordpress.rb', line 127

def (username, password)
  Browser.instance.forge_request(
    ,
    method: :post,
    cache_ttl: 0,
    body: { log: username, pwd: password }
  )
end

#login_urlString, false

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.

Returns:

  • (String, false)

    The URL to the login page or false if not detected



143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/wpscan/target/platform/wordpress.rb', line 143

def 
  return @login_url unless @login_url.nil?
  return @login_url = url(ParsedCli.) if ParsedCli.

  @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_cookiesObject

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



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 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) ⇒ String

Parameters:

  • slug (String)

Returns:

  • (String)


73
74
75
# 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_dirString

Returns:

  • (String)


56
57
58
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 56

def plugins_dir
  @plugins_dir ||= "#{content_dir}/plugins"
end

#plugins_dir=(dir) ⇒ Object



12
13
14
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 12

def plugins_dir=(dir)
  @plugins_dir = dir.chomp('/')
end

#plugins_uriAddressable::URI

Returns:

  • (Addressable::URI)


61
62
63
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 61

def plugins_uri
  uri.join("#{plugins_dir}/")
end

#plugins_urlString

Returns:

  • (String)


66
67
68
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 66

def plugins_url
  plugins_uri.to_s
end

#registration_urlString

Returns:

  • (String)


98
99
100
# 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_dirString, False

@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

Returns:

  • (String, False)

    String of the sub_dir found, false otherwise



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# 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) ⇒ String

Parameters:

  • slug (String)

Returns:

  • (String)


95
96
97
# 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_dirString

Returns:

  • (String)


78
79
80
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 78

def themes_dir
  @themes_dir ||= "#{content_dir}/themes"
end

#themes_uriAddressable::URI

Returns:

  • (Addressable::URI)


83
84
85
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 83

def themes_uri
  uri.join("#{themes_dir}/")
end

#themes_urlString

Returns:

  • (String)


88
89
90
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 88

def themes_url
  themes_uri.to_s
end

#url(path = nil) ⇒ String

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

Parameters:

  • path (String) (defaults to: nil)

    Optional path to merge with the uri

Returns:

  • (String)


124
125
126
127
128
129
130
131
132
133
134
135
136
# 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) ⇒ Boolean

Returns Whether or not the target is running WordPress.

Parameters:

  • detection_mode (Symbol)

Returns:

  • (Boolean)

    Whether or not the target is running WordPress



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 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) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 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?Boolean

Returns Whether or not the target is hosted on wordpress.com.

Returns:

  • (Boolean)

    Whether or not the target is hosted on wordpress.com



103
104
105
106
107
108
109
110
111
112
113
# 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