wpscan 3.8.28 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +104 -30
- data/app/app.rb +26 -0
- data/app/controllers/aliases.rb +2 -2
- data/app/controllers/authenticated_inventory.rb +43 -0
- data/app/controllers/core/cli_options.rb +151 -0
- data/app/controllers/core.rb +200 -25
- data/app/controllers/custom_directories.rb +1 -1
- data/app/controllers/enumeration/cli_options.rb +21 -31
- data/app/controllers/enumeration/enum_methods.rb +145 -38
- data/app/controllers/enumeration.rb +26 -3
- data/app/controllers/interesting_findings.rb +25 -0
- data/app/controllers/main_theme.rb +1 -1
- data/app/controllers/password_attack.rb +14 -6
- data/app/controllers/vuln_api.rb +9 -3
- data/app/controllers/wp_version.rb +1 -1
- data/app/controllers.rb +1 -0
- data/app/finders/backup_folders/known_locations.rb +66 -0
- data/app/finders/backup_folders.rb +19 -0
- data/app/finders/config_backups/known_filenames.rb +6 -4
- data/app/finders/config_backups.rb +1 -1
- data/app/finders/db_exports/known_locations.rb +16 -14
- data/app/finders/db_exports.rb +1 -1
- data/app/finders/interesting_findings/backup_db.rb +1 -1
- data/app/finders/interesting_findings/debug_log.rb +1 -1
- data/app/finders/interesting_findings/duplicator_installer_log.rb +1 -1
- data/app/finders/interesting_findings/emergency_pwd_reset_script.rb +1 -1
- data/app/finders/interesting_findings/fantastico_fileslist.rb +21 -0
- data/app/finders/interesting_findings/full_path_disclosure.rb +1 -1
- data/app/finders/interesting_findings/headers.rb +17 -0
- data/app/finders/interesting_findings/mu_plugins.rb +1 -1
- data/app/finders/interesting_findings/multisite.rb +1 -1
- data/app/finders/interesting_findings/php_disabled.rb +2 -2
- data/app/finders/interesting_findings/readme.rb +1 -1
- data/app/finders/interesting_findings/registration.rb +1 -1
- data/app/finders/interesting_findings/robots_txt.rb +20 -0
- data/app/finders/interesting_findings/search_replace_db_2.rb +19 -0
- data/app/finders/interesting_findings/tmm_db_migrate.rb +1 -1
- data/app/finders/interesting_findings/upload_directory_listing.rb +1 -1
- data/app/finders/interesting_findings/upload_sql_dump.rb +2 -2
- data/app/finders/interesting_findings/wp_cron.rb +1 -1
- data/app/finders/interesting_findings/xml_rpc.rb +61 -0
- data/app/finders/interesting_findings.rb +13 -4
- data/app/finders/main_theme/css_style_in_homepage.rb +1 -1
- data/app/finders/main_theme/urls_in_homepage.rb +3 -7
- data/app/finders/main_theme/woo_framework_meta_generator.rb +4 -4
- data/app/finders/main_theme.rb +1 -1
- data/app/finders/medias/attachment_brute_forcing.rb +2 -2
- data/app/finders/medias.rb +1 -1
- data/app/finders/passwords/wp_login.rb +2 -2
- data/app/finders/passwords/xml_rpc.rb +2 -2
- data/app/finders/passwords/xml_rpc_multicall.rb +1 -1
- data/app/finders/plugin_version/readme.rb +1 -1
- data/app/finders/plugin_version.rb +1 -1
- data/app/finders/plugins/known_locations.rb +17 -7
- data/app/finders/plugins/urls_in_homepage.rb +3 -7
- data/app/finders/plugins/wp_json_api.rb +85 -0
- data/app/finders/plugins.rb +2 -1
- data/app/finders/theme_version/style.rb +1 -1
- data/app/finders/theme_version/woo_framework_meta_generator.rb +1 -1
- data/app/finders/theme_version.rb +1 -1
- data/app/finders/themes/known_locations.rb +12 -6
- data/app/finders/themes/urls_in_homepage.rb +3 -7
- data/app/finders/themes/wp_json_api.rb +74 -0
- data/app/finders/themes.rb +2 -1
- data/app/finders/timthumb_version/bad_request.rb +1 -1
- data/app/finders/timthumb_version.rb +1 -1
- data/app/finders/timthumbs/known_locations.rb +6 -4
- data/app/finders/timthumbs.rb +1 -1
- data/app/finders/users/author_id_brute_forcing.rb +11 -7
- data/app/finders/users/author_posts.rb +1 -1
- data/app/finders/users/author_sitemap.rb +1 -1
- data/app/finders/users/login_error_messages.rb +1 -1
- data/app/finders/users/oembed_api.rb +3 -1
- data/app/finders/users/wp_json_api.rb +11 -7
- data/app/finders/users.rb +1 -1
- data/app/finders/wp_version/atom_generator.rb +1 -1
- data/app/finders/wp_version/rdf_generator.rb +1 -1
- data/app/finders/wp_version/readme.rb +1 -1
- data/app/finders/wp_version/rss_generator.rb +1 -1
- data/app/finders/wp_version/unique_fingerprinting.rb +2 -2
- data/app/finders/wp_version.rb +1 -1
- data/app/finders.rb +1 -0
- data/app/formatters/cli.rb +79 -0
- data/app/formatters/cli_no_color.rb +9 -0
- data/app/formatters/cli_no_colour.rb +17 -0
- data/app/formatters/json.rb +14 -0
- data/app/formatters/jsonl.rb +29 -0
- data/app/formatters/sarif.rb +311 -0
- data/app/models/backup_folder.rb +39 -0
- data/app/models/fantastico_fileslist.rb +34 -0
- data/app/models/headers.rb +44 -0
- data/app/models/interesting_finding.rb +41 -2
- data/app/models/plugin.rb +8 -2
- data/app/models/robots_txt.rb +31 -0
- data/app/models/search_replace_db_2.rb +17 -0
- data/app/models/theme.rb +9 -2
- data/app/models/timthumb.rb +2 -2
- data/app/models/user.rb +35 -0
- data/app/models/version.rb +49 -0
- data/app/models/wp_item/wordpress_org_data.rb +55 -0
- data/app/models/wp_item.rb +109 -9
- data/app/models/wp_version.rb +2 -2
- data/app/models/xml_rpc.rb +73 -3
- data/app/models.rb +2 -1
- data/app/user_agents.txt +46 -0
- data/app/views/cli/core/banner.erb +3 -3
- data/app/views/cli/core/finished.erb +15 -0
- data/app/views/cli/core/help.erb +4 -0
- data/app/views/cli/core/started.erb +11 -0
- data/app/views/cli/enumeration/backup_folders.erb +11 -0
- data/app/views/cli/enumeration/plugin.erb +13 -0
- data/app/views/cli/enumeration/plugins.erb +1 -12
- data/app/views/cli/enumeration/theme.erb +4 -0
- data/app/views/cli/enumeration/themes.erb +1 -3
- data/app/views/cli/enumeration/user.erb +4 -0
- data/app/views/cli/enumeration/users.erb +1 -3
- data/app/views/cli/finding.erb +1 -1
- data/app/views/cli/interesting_findings/_array.erb +10 -0
- data/app/views/cli/interesting_findings/findings.erb +23 -0
- data/app/views/cli/scan_aborted.erb +5 -0
- data/app/views/cli/update_aborted.erb +5 -0
- data/app/views/cli/vuln_api/status.erb +2 -0
- data/app/views/cli/vulnerability.erb +6 -0
- data/app/views/cli/wp_item.erb +4 -1
- data/app/views/json/core/banner.erb +2 -8
- data/app/views/json/core/finished.erb +13 -0
- data/app/views/json/core/help.erb +4 -0
- data/app/views/json/core/started.erb +10 -0
- data/app/views/json/enumeration/backup_folders.erb +11 -0
- data/app/views/json/enumeration/plugin.erb +15 -0
- data/app/views/json/enumeration/theme.erb +5 -0
- data/app/views/json/enumeration/user.erb +6 -0
- data/app/views/json/finding.erb +8 -2
- data/app/views/json/interesting_findings/findings.erb +24 -0
- data/app/views/json/notice.erb +1 -0
- data/app/views/json/scan_aborted.erb +5 -0
- data/app/views/json/update_aborted.erb +5 -0
- data/app/views/json/vuln_api/status.erb +2 -0
- data/app/views/json/wp_item.erb +4 -1
- data/bin/wpscan +1 -0
- data/lib/opt_parse_validator/config_files_loader_merger/base.rb +26 -0
- data/lib/opt_parse_validator/config_files_loader_merger/json.rb +17 -0
- data/lib/opt_parse_validator/config_files_loader_merger/yml.rb +17 -0
- data/lib/opt_parse_validator/config_files_loader_merger.rb +62 -0
- data/lib/opt_parse_validator/errors.rb +9 -0
- data/lib/opt_parse_validator/hacks.rb +19 -0
- data/lib/opt_parse_validator/opts/alias.rb +28 -0
- data/lib/opt_parse_validator/opts/array.rb +34 -0
- data/lib/opt_parse_validator/opts/base.rb +142 -0
- data/lib/opt_parse_validator/opts/boolean.rb +19 -0
- data/lib/opt_parse_validator/opts/choice.rb +43 -0
- data/lib/opt_parse_validator/opts/credentials.rb +15 -0
- data/lib/opt_parse_validator/opts/directory_path.rb +17 -0
- data/lib/opt_parse_validator/opts/file_path.rb +34 -0
- data/lib/opt_parse_validator/opts/headers.rb +33 -0
- data/lib/opt_parse_validator/opts/integer.rb +15 -0
- data/lib/opt_parse_validator/opts/integer_range.rb +37 -0
- data/lib/opt_parse_validator/opts/multi_choices.rb +135 -0
- data/lib/opt_parse_validator/opts/path.rb +78 -0
- data/lib/opt_parse_validator/opts/positive_integer.rb +16 -0
- data/lib/opt_parse_validator/opts/proxy.rb +7 -0
- data/lib/opt_parse_validator/opts/regexp.rb +14 -0
- data/lib/opt_parse_validator/opts/smart_list.rb +30 -0
- data/lib/opt_parse_validator/opts/string.rb +8 -0
- data/lib/opt_parse_validator/opts/uri.rb +41 -0
- data/lib/opt_parse_validator/opts/url.rb +11 -0
- data/lib/opt_parse_validator/opts.rb +9 -0
- data/lib/opt_parse_validator/version.rb +6 -0
- data/lib/opt_parse_validator.rb +161 -0
- data/lib/wpscan/browser/actions.rb +48 -0
- data/lib/wpscan/browser/options.rb +92 -0
- data/lib/wpscan/browser.rb +87 -2
- data/lib/wpscan/browser_authenticator.rb +64 -0
- data/lib/wpscan/cache/file_store.rb +77 -0
- data/lib/wpscan/cache/typhoeus.rb +25 -0
- data/lib/wpscan/controller.rb +100 -4
- data/lib/wpscan/controllers.rb +78 -3
- data/lib/wpscan/db/dynamic_finders/base.rb +3 -7
- data/lib/wpscan/db/dynamic_finders/plugin.rb +2 -2
- data/lib/wpscan/db/dynamic_finders/wordpress.rb +1 -1
- data/lib/wpscan/db/fingerprints.rb +2 -2
- data/lib/wpscan/db/updater.rb +23 -13
- data/lib/wpscan/db/vuln_api.rb +19 -7
- data/lib/wpscan/db/wp_item.rb +2 -2
- data/lib/wpscan/errors/enumeration.rb +4 -4
- data/lib/wpscan/errors/http.rb +82 -3
- data/lib/wpscan/errors/saml.rb +28 -0
- data/lib/wpscan/errors/scan.rb +14 -0
- data/lib/wpscan/errors/update.rb +11 -3
- data/lib/wpscan/errors/vuln_api.rb +24 -0
- data/lib/wpscan/errors/wordpress.rb +2 -2
- data/lib/wpscan/errors/wp_auth.rb +37 -0
- data/lib/wpscan/errors.rb +4 -3
- data/lib/wpscan/exit_code.rb +25 -0
- data/lib/wpscan/finders/base_finders.rb +45 -0
- data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/body_pattern.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/comment.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/header_pattern.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/javascript_var.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +3 -5
- data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/wp_items/finder.rb +3 -3
- data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
- data/lib/wpscan/finders/finder/breadth_first_dictionary_attack.rb +257 -0
- data/lib/wpscan/finders/finder/enumerator.rb +77 -0
- data/lib/wpscan/finders/finder/fingerprinter.rb +48 -0
- data/lib/wpscan/finders/finder/smart_url_checker/findings.rb +33 -0
- data/lib/wpscan/finders/finder/smart_url_checker.rb +60 -0
- data/lib/wpscan/finders/finder/wp_version/smart_url_checker.rb +1 -1
- data/lib/wpscan/finders/finder.rb +78 -0
- data/lib/wpscan/finders/finding.rb +54 -0
- data/lib/wpscan/finders/findings.rb +33 -0
- data/lib/wpscan/finders/independent_finder.rb +33 -0
- data/lib/wpscan/finders/independent_finders.rb +26 -0
- data/lib/wpscan/finders/same_type_finder.rb +19 -0
- data/lib/wpscan/finders/same_type_finders.rb +28 -0
- data/lib/wpscan/finders/unique_finder.rb +19 -0
- data/lib/wpscan/finders/unique_finders.rb +47 -0
- data/lib/wpscan/finders.rb +11 -12
- data/lib/wpscan/formatter/buffer.rb +17 -0
- data/lib/wpscan/formatter.rb +152 -0
- data/lib/wpscan/helper.rb +7 -1
- data/lib/wpscan/http_status_tracking.rb +128 -0
- data/lib/wpscan/numeric.rb +13 -0
- data/lib/wpscan/parsed_cli.rb +31 -2
- data/lib/wpscan/progressbar_null_output.rb +23 -0
- data/lib/wpscan/public_suffix/domain.rb +44 -0
- data/lib/wpscan/references.rb +118 -4
- data/lib/wpscan/scan.rb +127 -0
- data/lib/wpscan/target/hashes.rb +45 -0
- data/lib/wpscan/target/platform/php.rb +124 -0
- data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
- data/lib/wpscan/target/platform/wordpress.rb +7 -8
- data/lib/wpscan/target/platform.rb +3 -0
- data/lib/wpscan/target/scope.rb +103 -0
- data/lib/wpscan/target/server/apache.rb +27 -0
- data/lib/wpscan/target/server/generic.rb +72 -0
- data/lib/wpscan/target/server/iis.rb +29 -0
- data/lib/wpscan/target/server/nginx.rb +27 -0
- data/lib/wpscan/target/server.rb +6 -0
- data/lib/wpscan/target.rb +129 -9
- data/lib/wpscan/typhoeus/hydra.rb +12 -0
- data/lib/wpscan/typhoeus/response.rb +24 -1
- data/lib/wpscan/version.rb +1 -1
- data/lib/wpscan/vulnerability.rb +49 -3
- data/lib/wpscan/vulnerability_filter.rb +68 -0
- data/lib/wpscan/vulnerable.rb +13 -1
- data/lib/wpscan/web_site.rb +152 -0
- data/lib/wpscan.rb +126 -29
- metadata +362 -20
data/lib/wpscan/references.rb
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module WPScan
|
|
4
|
-
# References
|
|
5
|
-
# to allow the use of the wpvulndb reference.
|
|
4
|
+
# References related to a vulnerability / finding.
|
|
6
5
|
module References
|
|
7
6
|
extend ActiveSupport::Concern
|
|
8
7
|
|
|
@@ -10,22 +9,137 @@ module WPScan
|
|
|
10
9
|
module ClassMethods
|
|
11
10
|
# @return [ Array<Symbol> ]
|
|
12
11
|
def references_keys
|
|
13
|
-
@references_keys ||=
|
|
12
|
+
@references_keys ||= %i[cve exploitdb url metasploit packetstorm securityfocus youtube wpvulndb]
|
|
14
13
|
end
|
|
15
14
|
end
|
|
16
15
|
|
|
16
|
+
# @param [ Hash ] refs
|
|
17
|
+
def references=(refs)
|
|
18
|
+
@references = {}
|
|
19
|
+
|
|
20
|
+
self.class.references_keys.each do |key|
|
|
21
|
+
next unless refs.key?(key)
|
|
22
|
+
|
|
23
|
+
@references[key] = if key == :youtube
|
|
24
|
+
Array(refs[:youtube]).map { |id| youtube_url(id) }
|
|
25
|
+
else
|
|
26
|
+
Array(refs[key]).map(&:to_s)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [ Hash ]
|
|
32
|
+
def references
|
|
33
|
+
@references ||= {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [ Array<String> ] All the references URLs
|
|
17
37
|
def references_urls
|
|
18
|
-
wpvulndb_urls +
|
|
38
|
+
wpvulndb_urls + cve_urls + exploitdb_urls + urls + msf_urls +
|
|
39
|
+
packetstorm_urls + securityfocus_urls + youtube_urls
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [ Array<String> ] The CVEs
|
|
43
|
+
def cves
|
|
44
|
+
references[:cve] || []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [ Array<String> ]
|
|
48
|
+
def cve_urls
|
|
49
|
+
cves.reduce([]) { |acc, elem| acc << cve_url(elem) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [ String ] The URL to the CVE
|
|
53
|
+
def cve_url(cve)
|
|
54
|
+
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-#{cve}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [ Array<String> ] The ExploitDB ID
|
|
58
|
+
def exploitdb_ids
|
|
59
|
+
references[:exploitdb] || []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [ Array<String> ]
|
|
63
|
+
def exploitdb_urls
|
|
64
|
+
exploitdb_ids.reduce([]) { |acc, elem| acc << exploitdb_url(elem) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [ String ]
|
|
68
|
+
def exploitdb_url(id)
|
|
69
|
+
"https://www.exploit-db.com/exploits/#{id}/"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [ Array<String> ]
|
|
73
|
+
def urls
|
|
74
|
+
references[:url] || []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [ Array<String> ] The metasploit modules
|
|
78
|
+
def msf_modules
|
|
79
|
+
references[:metasploit] || []
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [ Array<String> ]
|
|
83
|
+
def msf_urls
|
|
84
|
+
msf_modules.reduce([]) { |acc, elem| acc << msf_url(elem) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [ String ] The URL to the metasploit module page
|
|
88
|
+
def msf_url(mod)
|
|
89
|
+
"https://www.rapid7.com/db/modules/#{mod.sub(%r{^/}, '')}/"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [ Array<String> ] The Packetstormsecurity IDs
|
|
93
|
+
def packetstorm_ids
|
|
94
|
+
@packetstorm_ids ||= references[:packetstorm] || []
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [ Array<String> ]
|
|
98
|
+
def packetstorm_urls
|
|
99
|
+
packetstorm_ids.reduce([]) { |acc, elem| acc << packetstorm_url(elem) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [ String ]
|
|
103
|
+
def packetstorm_url(id)
|
|
104
|
+
"https://packetstormsecurity.com/files/#{id}/"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [ Array<String> ] The Security Focus IDs
|
|
108
|
+
def securityfocus_ids
|
|
109
|
+
references[:securityfocus] || []
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [ Array<String> ]
|
|
113
|
+
def securityfocus_urls
|
|
114
|
+
securityfocus_ids.reduce([]) { |acc, elem| acc << securityfocus_url(elem) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [ String ]
|
|
118
|
+
def securityfocus_url(id)
|
|
119
|
+
"https://www.securityfocus.com/bid/#{id}/"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [ Array<String> ]
|
|
123
|
+
def youtube_urls
|
|
124
|
+
references[:youtube] || []
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [ String ]
|
|
128
|
+
def youtube_url(id)
|
|
129
|
+
"https://www.youtube.com/watch?v=#{id}"
|
|
19
130
|
end
|
|
20
131
|
|
|
132
|
+
# @return [ Array<String> ] wpvulndb (now WPScan) reference IDs
|
|
21
133
|
def wpvulndb_ids
|
|
22
134
|
references[:wpvulndb] || []
|
|
23
135
|
end
|
|
24
136
|
|
|
137
|
+
# @return [ Array<String> ]
|
|
25
138
|
def wpvulndb_urls
|
|
26
139
|
wpvulndb_ids.reduce([]) { |acc, elem| acc << wpvulndb_url(elem) }
|
|
27
140
|
end
|
|
28
141
|
|
|
142
|
+
# @return [ String ]
|
|
29
143
|
def wpvulndb_url(id)
|
|
30
144
|
"https://wpscan.com/vulnerability/#{id}"
|
|
31
145
|
end
|
data/lib/wpscan/scan.rb
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
# Scan
|
|
5
|
+
class Scan
|
|
6
|
+
attr_reader :run_error
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
WPScan.start_memory = GetProcessMem.new.bytes
|
|
10
|
+
|
|
11
|
+
# Capture the original command line arguments with sensitive data masked
|
|
12
|
+
WPScan.command_line = mask_sensitive_arguments(ARGV)
|
|
13
|
+
|
|
14
|
+
controllers << WPScan::Controller::Core.new
|
|
15
|
+
|
|
16
|
+
exit_hook
|
|
17
|
+
|
|
18
|
+
yield self if block_given?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Masks sensitive arguments in the command line to prevent exposing secrets
|
|
22
|
+
# @param [ Array<String> ] args The command line arguments
|
|
23
|
+
# @return [ String ] The sanitized command line string
|
|
24
|
+
def mask_sensitive_arguments(args)
|
|
25
|
+
# List of sensitive arguments that contain actual secrets (not file paths)
|
|
26
|
+
# File paths like --passwords and --cookie-jar are not masked as they're
|
|
27
|
+
# not secrets themselves, just references to files
|
|
28
|
+
sensitive_args = %w[
|
|
29
|
+
--api-token
|
|
30
|
+
--http-auth
|
|
31
|
+
--proxy-auth
|
|
32
|
+
--cookie-string
|
|
33
|
+
--wp-auth
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
masked_args = args.dup
|
|
37
|
+
args.each_with_index do |arg, index|
|
|
38
|
+
# Check if this argument is sensitive
|
|
39
|
+
if sensitive_args.include?(arg)
|
|
40
|
+
# Mask the next argument (the value)
|
|
41
|
+
masked_args[index + 1] = '[REDACTED]' if index + 1 < args.length
|
|
42
|
+
elsif arg.start_with?('--') && arg.include?('=')
|
|
43
|
+
# Handle --arg=value format
|
|
44
|
+
arg_name = arg.split('=').first
|
|
45
|
+
masked_args[index] = "#{arg_name}=[REDACTED]" if sensitive_args.include?(arg_name)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
masked_args.join(' ')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [ Controllers ]
|
|
53
|
+
def controllers
|
|
54
|
+
@controllers ||= WPScan::Controllers.new
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def run
|
|
58
|
+
controllers.run
|
|
59
|
+
rescue OptParseValidator::NoRequiredOption => e
|
|
60
|
+
@run_error = e
|
|
61
|
+
|
|
62
|
+
formatter.output('@usage', msg: e.message)
|
|
63
|
+
rescue NoMemoryError, ScriptError, SecurityError, SignalException, StandardError, SystemStackError => e
|
|
64
|
+
@run_error = e
|
|
65
|
+
|
|
66
|
+
output_params = {
|
|
67
|
+
reason: e.is_a?(Interrupt) ? 'Canceled by User' : e.message,
|
|
68
|
+
trace: e.backtrace,
|
|
69
|
+
verbose: WPScan::ParsedCli.verbose || run_error_exit_code == WPScan::ExitCode::EXCEPTION
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
output_params[:url] = controllers.first.target.url if WPScan::ParsedCli.url
|
|
73
|
+
|
|
74
|
+
formatter.output(aborted_view, output_params)
|
|
75
|
+
ensure
|
|
76
|
+
formatter.beautify
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Used for convenience
|
|
80
|
+
# @See Formatter
|
|
81
|
+
def formatter
|
|
82
|
+
controllers.first.formatter
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [ String ] The global view to render when the run is aborted
|
|
86
|
+
def aborted_view
|
|
87
|
+
core = controllers.first
|
|
88
|
+
core.respond_to?(:updating_db?) && core.updating_db? ? '@update_aborted' : '@scan_aborted'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [ Hash ]
|
|
92
|
+
def datastore
|
|
93
|
+
controllers.first.datastore
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Hook to be able to have an exit code returned
|
|
97
|
+
# depending on the findings / errors
|
|
98
|
+
# :nocov:
|
|
99
|
+
def exit_hook
|
|
100
|
+
# Avoid hooking the exit when rspec is running, otherwise it will always return 0
|
|
101
|
+
# and Travis won't detect failed builds. Couldn't find a better way, even though
|
|
102
|
+
# some people managed to https://github.com/rspec/rspec-core/pull/410
|
|
103
|
+
return if defined?(RSpec)
|
|
104
|
+
|
|
105
|
+
at_exit do
|
|
106
|
+
exit(run_error_exit_code) if run_error
|
|
107
|
+
|
|
108
|
+
# The parsed_option[:url] must be checked to avoid raising erros when only -h/-v are given
|
|
109
|
+
exit(WPScan::ExitCode::VULNERABLE) if WPScan::ParsedCli.url && controllers.first.target.vulnerable?
|
|
110
|
+
exit(WPScan::ExitCode::OK)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
# :nocov:
|
|
114
|
+
|
|
115
|
+
# @return [ Integer ] The exit code related to the run_error
|
|
116
|
+
def run_error_exit_code
|
|
117
|
+
return WPScan::ExitCode::CLI_OPTION_ERROR if run_error.is_a?(OptParseValidator::Error) ||
|
|
118
|
+
run_error.is_a?(OptionParser::ParseError)
|
|
119
|
+
|
|
120
|
+
return WPScan::ExitCode::INTERRUPTED if run_error.is_a?(Interrupt)
|
|
121
|
+
|
|
122
|
+
return WPScan::ExitCode::ERROR if run_error.is_a?(WPScan::Error::Standard)
|
|
123
|
+
|
|
124
|
+
WPScan::ExitCode::EXCEPTION
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
# Scope system logic
|
|
5
|
+
class Target < WebSite
|
|
6
|
+
# @note Comments are deleted to avoid cache generation details
|
|
7
|
+
#
|
|
8
|
+
# @param [ Typhoeus::Response, String ] page
|
|
9
|
+
#
|
|
10
|
+
# @return [ String ] The md5sum of the page
|
|
11
|
+
def self.page_hash(page)
|
|
12
|
+
page = WPScan::Browser.get(page, followlocation: true, maxredirs: 10) unless page.is_a?(Typhoeus::Response)
|
|
13
|
+
|
|
14
|
+
# Removes comments and script tags before computing the hash
|
|
15
|
+
# to remove any potential cached stuff
|
|
16
|
+
html = Nokogiri::HTML(page.body)
|
|
17
|
+
html.xpath('//script|//comment()').each(&:remove)
|
|
18
|
+
|
|
19
|
+
Digest::MD5.hexdigest(html)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [ String ] The hash of the homepage
|
|
23
|
+
def homepage_hash
|
|
24
|
+
@homepage_hash ||= self.class.page_hash(url)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @note This is used to detect potential custom 404 responding with a 200
|
|
28
|
+
# @return [ String ] The hash of a 404
|
|
29
|
+
def error_404_hash
|
|
30
|
+
@error_404_hash ||= self.class.page_hash(error_404_res)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param [ Typhoeus::Response, String ] page
|
|
34
|
+
# @return [ Boolean ] Wether or not the page is a the homepage or a 404 based on its md5sum
|
|
35
|
+
def homepage_or_404?(page)
|
|
36
|
+
homepage_and_404_hashes.include?(self.class.page_hash(page))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
def homepage_and_404_hashes
|
|
42
|
+
@homepage_and_404_hashes ||= [homepage_hash, error_404_hash].freeze
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
class Target < WebSite
|
|
5
|
+
module Platform
|
|
6
|
+
# Some PHP specific implementation
|
|
7
|
+
module PHP
|
|
8
|
+
DEBUG_LOG_PATTERN = /(?:\[\d{2}-[a-zA-Z]{3}-\d{4}\s\d{2}:\d{2}:\d{2}\s[A-Z]{3}\]|
|
|
9
|
+
PHP\s(?:Fatal|Warning|Strict|Error|Notice):)/x
|
|
10
|
+
FPD_PATTERN = /Fatal error:.+? in (.+?) on/
|
|
11
|
+
ERROR_LOG_PATTERN = /PHP Fatal error/i
|
|
12
|
+
|
|
13
|
+
# @param [ String ] path
|
|
14
|
+
# @param [ Regexp ] pattern
|
|
15
|
+
# @param [ Hash ] params The request params
|
|
16
|
+
#
|
|
17
|
+
# @return [ Boolean ]
|
|
18
|
+
def log_file?(path, pattern, params = {})
|
|
19
|
+
max_mib = WPScan::ParsedCli.max_log_file_size.to_i
|
|
20
|
+
max_size = max_mib.positive? ? max_mib * 1024 * 1024 : nil
|
|
21
|
+
|
|
22
|
+
head_res = WPScan::Browser.forge_request(url(path), head_or_get_params).run
|
|
23
|
+
return false unless head_res.code == 200
|
|
24
|
+
|
|
25
|
+
if max_size
|
|
26
|
+
content_length = head_res.headers&.[]('Content-Length').to_i
|
|
27
|
+
return false if content_length > max_size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
body = stream_capped_body(path, params, max_size)
|
|
31
|
+
return false if body.nil?
|
|
32
|
+
|
|
33
|
+
body.match?(pattern) || false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Performs a streaming GET of `path` and returns the body as a String, capped at
|
|
37
|
+
# `max_size` bytes. Returns nil if the transfer was aborted because it exceeded the cap.
|
|
38
|
+
#
|
|
39
|
+
# Only the first 700 bytes of the file are needed to match log-file patterns. Servers may
|
|
40
|
+
# ignore the Range header, and libcurl's maxfilesize is a no-op when the response has no
|
|
41
|
+
# Content-Length (chunked transfer encoding), so an on_body callback is used to abort the
|
|
42
|
+
# transfer once the accumulated body exceeds the configured limit. Streaming via on_body
|
|
43
|
+
# also keeps Typhoeus::Response#body empty, so the response cache cannot Marshal-dump a
|
|
44
|
+
# huge payload.
|
|
45
|
+
#
|
|
46
|
+
# @param [ String ] path
|
|
47
|
+
# @param [ Hash ] params The base request params
|
|
48
|
+
# @param [ Integer, nil ] max_size Size cap in bytes, or nil to disable
|
|
49
|
+
#
|
|
50
|
+
# @return [ String, nil ]
|
|
51
|
+
def stream_capped_body(path, params, max_size)
|
|
52
|
+
get_params = params.merge(
|
|
53
|
+
headers: { 'Range' => 'bytes=0-700' },
|
|
54
|
+
method: :get,
|
|
55
|
+
cache_ttl: 0
|
|
56
|
+
)
|
|
57
|
+
get_params[:maxfilesize] = max_size if max_size
|
|
58
|
+
|
|
59
|
+
req = WPScan::Browser.forge_request(url(path), get_params)
|
|
60
|
+
body = +''
|
|
61
|
+
aborted = install_body_cap(req, body, max_size)
|
|
62
|
+
req.run
|
|
63
|
+
|
|
64
|
+
aborted.call ? nil : body
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Installs an on_body callback on `req` that appends chunks to `body`. If `max_size` is
|
|
68
|
+
# set, the transfer is aborted once the buffer exceeds it. Returns a callable that
|
|
69
|
+
# reports whether the transfer was aborted.
|
|
70
|
+
def install_body_cap(req, body, max_size)
|
|
71
|
+
aborted = false
|
|
72
|
+
|
|
73
|
+
if max_size
|
|
74
|
+
req.on_body do |chunk|
|
|
75
|
+
body << chunk
|
|
76
|
+
next if body.bytesize <= max_size
|
|
77
|
+
|
|
78
|
+
aborted = true
|
|
79
|
+
:abort
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
req.on_body { |chunk| body << chunk }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
-> { aborted }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param [ String ] path
|
|
89
|
+
# @param [ Hash ] params The request params
|
|
90
|
+
#
|
|
91
|
+
# @return [ Boolean ] true if url(path) is a debug log, false otherwise
|
|
92
|
+
def debug_log?(path, params = {})
|
|
93
|
+
log_file?(path, DEBUG_LOG_PATTERN, params)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @param [ String ] path
|
|
97
|
+
# @param [ Hash ] params The request params
|
|
98
|
+
#
|
|
99
|
+
# @return [ Boolean ] Wether or not url(path) is an error log file
|
|
100
|
+
def error_log?(path, params = {})
|
|
101
|
+
log_file?(path, ERROR_LOG_PATTERN, params)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @param [ String ] path
|
|
105
|
+
# @param [ Hash ] params The request params
|
|
106
|
+
#
|
|
107
|
+
# @return [ Boolean ] true if url(path) contains a FPD, false otherwise
|
|
108
|
+
def full_path_disclosure?(path = nil, params = {})
|
|
109
|
+
!full_path_disclosure_entries(path, params).empty?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @param [ String ] path
|
|
113
|
+
# @param [ Hash ] params The request params
|
|
114
|
+
#
|
|
115
|
+
# @return [ Array<String> ] The FPD found, or an empty array if none
|
|
116
|
+
def full_path_disclosure_entries(path = nil, params = {})
|
|
117
|
+
res = WPScan::Browser.get(url(path), params)
|
|
118
|
+
|
|
119
|
+
res.body.scan(FPD_PATTERN).flatten
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module WPScan
|
|
4
|
-
class Target
|
|
4
|
+
class Target
|
|
5
5
|
module Platform
|
|
6
6
|
# wp-content & plugins directory implementation
|
|
7
7
|
module WordPress
|
|
@@ -16,7 +16,7 @@ module WPScan
|
|
|
16
16
|
# @return [ String ] The wp-content directory
|
|
17
17
|
def content_dir
|
|
18
18
|
unless @content_dir
|
|
19
|
-
# scope_url_pattern is from
|
|
19
|
+
# scope_url_pattern is from WPScan::Target
|
|
20
20
|
pattern = %r{#{scope_url_pattern}([\w\s\-/]+?)\\?/(?:themes|plugins|uploads|cache)\\?/}i
|
|
21
21
|
|
|
22
22
|
[homepage_res, error_404_res].each do |page_res|
|
|
@@ -103,7 +103,7 @@ module WPScan
|
|
|
103
103
|
def sub_dir
|
|
104
104
|
return @sub_dir unless @sub_dir.nil?
|
|
105
105
|
|
|
106
|
-
# url_pattern is from
|
|
106
|
+
# url_pattern is from WPScan::Target
|
|
107
107
|
pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp-includes/)}i
|
|
108
108
|
xpath = '(//@src|//@href|//@data-src)[contains(., "xmlrpc.php") or contains(., "wp-includes/")]'
|
|
109
109
|
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
module WPScan
|
|
8
|
-
class Target
|
|
8
|
+
class Target
|
|
9
9
|
module Platform
|
|
10
10
|
# Some WordPress specific implementation
|
|
11
11
|
module WordPress
|
|
12
|
-
include
|
|
12
|
+
include WPScan::Target::Platform::PHP
|
|
13
13
|
|
|
14
|
-
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu-)?plugins|uploads))|wp-includes)/}i
|
|
15
|
-
WORDPRESS_HOSTED_PATTERN = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i
|
|
16
|
-
WP_JSON_OEMBED_PATTERN = %r{/wp-json/oembed/}i
|
|
17
|
-
WP_ADMIN_AJAX_PATTERN = %r{\\?/wp-admin\\?/admin-ajax\.php}i
|
|
14
|
+
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu-)?plugins|uploads))|wp-includes)/}i
|
|
15
|
+
WORDPRESS_HOSTED_PATTERN = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i
|
|
16
|
+
WP_JSON_OEMBED_PATTERN = %r{/wp-json/oembed/}i
|
|
17
|
+
WP_ADMIN_AJAX_PATTERN = %r{\\?/wp-admin\\?/admin-ajax\.php}i
|
|
18
18
|
|
|
19
19
|
# These methods are used in the associated interesting_findings finders
|
|
20
20
|
# to keep the boolean state of the finding rather than re-check the whole thing again
|
|
@@ -87,8 +87,7 @@ module WPScan
|
|
|
87
87
|
# Force recheck of the homepage when retying wordpress?
|
|
88
88
|
# No need to clear the cache, as the request (which will contain the cookies)
|
|
89
89
|
# will be different
|
|
90
|
-
|
|
91
|
-
@homepage_url = nil
|
|
90
|
+
reset_homepage_cache!
|
|
92
91
|
|
|
93
92
|
break
|
|
94
93
|
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
# Scope system logic
|
|
5
|
+
class Target < WebSite
|
|
6
|
+
# @return [ Array<PublicSuffix::Domain, String> ]
|
|
7
|
+
def scope
|
|
8
|
+
@scope ||= Scope.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param [ String, Addressable::URI ] url An absolute URL or URI
|
|
12
|
+
#
|
|
13
|
+
# @return [ Boolean ] true if the url given is in scope
|
|
14
|
+
def in_scope?(url_or_uri)
|
|
15
|
+
url_or_uri = Addressable::URI.parse(url_or_uri.strip) unless url_or_uri.is_a?(Addressable::URI)
|
|
16
|
+
|
|
17
|
+
scope.include?(url_or_uri.host)
|
|
18
|
+
rescue StandardError
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param [ Typhoeus::Response ] res
|
|
23
|
+
# @param [ String ] xpath
|
|
24
|
+
#
|
|
25
|
+
# @yield [ Addressable::URI, Nokogiri::XML::Element ] The in scope url and its associated tag
|
|
26
|
+
#
|
|
27
|
+
# @return [ Array<Addressable::URI> ] The in scope absolute URIs detected in the response's body
|
|
28
|
+
#
|
|
29
|
+
# @note It is highly recommended to use the xpath parameter to focus on the uris needed, as this method can be quite
|
|
30
|
+
# time consuming when there are a lof of uris to check
|
|
31
|
+
def in_scope_uris(res, xpath = '//@href|//@src|//@data-src')
|
|
32
|
+
found = []
|
|
33
|
+
|
|
34
|
+
uris_from_page(res, xpath) do |uri, tag|
|
|
35
|
+
next unless in_scope?(uri)
|
|
36
|
+
|
|
37
|
+
yield uri, tag if block_given?
|
|
38
|
+
|
|
39
|
+
found << uri
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
found
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Similar to Target#url_pattern but considering the in scope domains as well
|
|
46
|
+
#
|
|
47
|
+
# @return [ Regexp ] The pattern related to the target url and in scope domains,
|
|
48
|
+
# it also matches escaped /, such as in JSON JS data: http:\/\/t.com\/
|
|
49
|
+
# rubocop:disable Metrics/AbcSize
|
|
50
|
+
def scope_url_pattern
|
|
51
|
+
return @scope_url_pattern if @scope_url_pattern
|
|
52
|
+
|
|
53
|
+
domains = [uri.host + uri.path]
|
|
54
|
+
|
|
55
|
+
domains += if scope.domains.empty?
|
|
56
|
+
Array(scope.invalid_domains[1..])
|
|
57
|
+
else
|
|
58
|
+
Array(scope.domains[1..]).map(&:to_s) + scope.invalid_domains
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
domains.map! { |d| Regexp.escape(d.delete_suffix('/')).gsub('\*', '.*').gsub('/', '\\\\\?/') }
|
|
62
|
+
|
|
63
|
+
domains[0].gsub!(Regexp.escape(uri.host), "#{Regexp.escape(uri.host)}(?::\\d+)?") if uri.port
|
|
64
|
+
|
|
65
|
+
@scope_url_pattern = %r{https?:\\?/\\?/(?:#{domains.join('|')})\\?/?}i
|
|
66
|
+
end
|
|
67
|
+
# rubocop:enable Metrics/AbcSize
|
|
68
|
+
|
|
69
|
+
# Scope Implementation
|
|
70
|
+
class Scope
|
|
71
|
+
# @return [ Array<PublicSuffix::Domain> ] The valid domains in scope
|
|
72
|
+
def domains
|
|
73
|
+
@domains ||= []
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [ Array<String> ] The invalid domains in scope (such as IP addresses etc)
|
|
77
|
+
def invalid_domains
|
|
78
|
+
@invalid_domains ||= []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def <<(element)
|
|
82
|
+
if PublicSuffix.valid?(element, ignore_private: true)
|
|
83
|
+
domains << PublicSuffix.parse(element, ignore_private: true)
|
|
84
|
+
else
|
|
85
|
+
invalid_domains << element
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [ Boolean ] Wether or not the host is in the scope
|
|
90
|
+
def include?(host)
|
|
91
|
+
if PublicSuffix.valid?(host, ignore_private: true)
|
|
92
|
+
domain = PublicSuffix.parse(host, ignore_private: true)
|
|
93
|
+
|
|
94
|
+
domains.each { |d| return true if domain.match(d) }
|
|
95
|
+
else
|
|
96
|
+
invalid_domains.each { |d| return true if host == d }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WPScan
|
|
4
|
+
class Target < WebSite
|
|
5
|
+
module Server
|
|
6
|
+
# Some Apche specific implementation
|
|
7
|
+
module Apache
|
|
8
|
+
# @param [ String ] path
|
|
9
|
+
# @param [ Hash ] params The request params
|
|
10
|
+
#
|
|
11
|
+
# @return [ Symbol ] :Apache
|
|
12
|
+
def server(_path = nil, _params = {})
|
|
13
|
+
:Apache
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param [ String ] path
|
|
17
|
+
# @param [ Hash ] params The request params
|
|
18
|
+
#
|
|
19
|
+
# @return [ Array<String> ] The first level of directories/files listed,
|
|
20
|
+
# or an empty array if none
|
|
21
|
+
def directory_listing_entries(path = nil, params = {})
|
|
22
|
+
super(path, params, 'td a')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|