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



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

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



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

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



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

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:



116
117
118
# File 'lib/wpscan/target/platform/wordpress.rb', line 116

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

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

Parameters:

  • username (String)
  • password (String)

Returns:

  • (Typhoeus::Request)


124
125
126
127
128
129
130
131
# File 'lib/wpscan/target/platform/wordpress.rb', line 124

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

#login_urlString

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.

Returns:

  • (String)

    The URL to the login page



138
139
140
141
142
143
144
145
146
147
148
# File 'lib/wpscan/target/platform/wordpress.rb', line 138

def 
  return @login_url if @login_url

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



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/wpscan/target/platform/wordpress.rb', line 70

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)


93
94
95
# File 'lib/wpscan/target/platform/wordpress.rb', line 93

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
# 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

  [homepage_res, error_404_res].each do |page_res|
    in_scope_uris(page_res) 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)


123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/wpscan/target/platform/wordpress/custom_directories.rb', line 123

def url(path = nil)
  return @uri.to_s unless path

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

  super(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



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/wpscan/target/platform/wordpress.rb', line 28

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|
      return true if in_scope_uris(Browser.get_and_follow_location(url(path))).any? do |uri|
        WORDPRESS_PATTERN.match?(uri.path)
      end
    end
  end

  false
end

#wordpress_from_meta_comments_or_scripts?(response) ⇒ Boolean

Parameters:

Returns:

  • (Boolean)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/wpscan/target/platform/wordpress.rb', line 46

def wordpress_from_meta_comments_or_scripts?(response)
  in_scope_uris(response) 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



98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/wpscan/target/platform/wordpress.rb', line 98

def wordpress_hosted?
  return true if /\.wordpress\.com$/i.match?(uri.host)

  unless content_dir
    pattern = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i.freeze

    uris_from_page(homepage_res) do |uri|
      return true if uri.to_s.match?(pattern)
    end
  end

  false
end