mirror of
https://github.com/urbanadventurer/WhatWeb
synced 2026-06-21 14:12:19 +00:00
29aa301273
Changelog:
* Updated version from 0.5.5 to 0.6.1
* Improved SSL/TLS security with comprehensive cipher suite support (TLSv1, TLSv1.1, TLSv1.2, SSLv3, SSLv2)
- Added SSL_ATTRIBUTES constant for verify_mode
- Replaced dynamic SSL parameter collection with explicit SSLContext configuration
- Set verify_mode to VERIFY_NONE for maximum compatibility
- Enabled negotiation of all SSL/TLS versions including legacy SSLv2/SSLv3
* Enhanced HTTPS connection handling with optimized verification settings
- Cleared minimum version restrictions and disabled protocol exclusion flags
- Added fallback handling for different OpenSSL configurations
* Added dual-protocol feature: simple hostnames (without paths or ports) are now scanned using both HTTP and HTTPS protocols
- Automatically creates and tests both HTTP and HTTPS versions of simple hostnames
- Shows informational message when dual-protocol scanning is performed
- Intelligently detects and avoids duplicate URL scanning with redirect handling
* Added robust input validation and error handling
- Automatically aborts processing after 10 consecutive parsing errors
- Prevents wasted resources when processing invalid data sources
- Provides helpful error messages to guide users in troubleshooting input issues
- Handles various error cases including malformed URLs and non-URL input data
* Improved dependency management with Bundler group support
- Reorganized Gemfile with proper optional group definitions for mongo and rchardet
- Added clear installation instructions for optional dependencies
- Set explicit version constraints for test dependencies
- Added support for older Bundler versions through GEMFILE_GROUPS environment variable
* Expanded documentation for optional dependencies
- Added detailed installation instructions for MongoDB support
- Added clear steps for character set detection configuration
- Provided combined installation commands for all optional features
- Improved explanation of performance implications
This release introduces three new search contexts for plugin authors to use:
- uri.path
- uri.query
- uri.extension
An example of how to use this is from the PHP plugin.
```
{ :name=>"File extension", :regexp=>/^(php|phtml|php3|php4|php5|phps)$/, :search=>"uri.extension" }
```
* #311 - JSON Logging fails... UTF-8 can't modify frozen String (@juananpe)
* Refactored Helper::convert_to_utf8. Droped Ruby 2.0 support (@urbanadventurer)
* Added new search contexts for plugins: uri.path, uri.query, uri.extension (@urbanadventurer)
* Plex Media Server (@urbanadventurer)
* Meta-Facebook-Infrastructure (@urbanadventurer)
* Netflix-Platform (@urbanadventurer)
* VKontakte-Platform (@urbanadventurer)
* Distributed-Tracing (@urbanadventurer)
* Modern-Security-Headers (@urbanadventurer)
* Baidu-Platform (@urbanadventurer)
* Alibaba-Aliyun (@urbanadventurer)
* Weibo-Platform (@urbanadventurer)
* Adobe-Flash (@urbanadventurer)
* ASP_NET (@urbanadventurer)
* ColdFusion (@urbanadventurer)
* Drupal (@gboddin)
* Java (@urbanadventurer)
* Perl (@urbanadventurer)
* PHP (@urbanadventurer)
* Python (@urbanadventurer)
* Ruby (@urbanadventurer)
* TYPO3 (@definity)
* WordPress (@juananpe)
* Shopify (enhanced HTTP header detection) (@urbanadventurer)
* CloudFlare (enhanced with CF-RAY and other headers) (@urbanadventurer)
* CloudFront (renamed to Amazon-CloudFront and enhanced) (@urbanadventurer)
* Tengine Web Server (added Alibaba-specific headers) (@urbanadventurer)
* Content-Security-Policy (added modern CSP headers) (@urbanadventurer)
* Azure (renamed to Microsoft-Azure and enhanced) (@urbanadventurer)
401 lines
12 KiB
Ruby
401 lines
12 KiB
Ruby
# Copyright 2009 to 2025 Andrew Horton and Brendan Coles
|
|
#
|
|
# This file is part of WhatWeb.
|
|
#
|
|
# WhatWeb is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 2 of the License, or
|
|
# at your option) any later version.
|
|
#
|
|
# WhatWeb is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with WhatWeb. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
# this class contains stuff related to plugins but not necessary to repeat in each plugin we create
|
|
class PluginSupport
|
|
# this is used by load_plugins
|
|
def self.load_plugin(f)
|
|
load f
|
|
|
|
rescue ArgumentError => err
|
|
if err.message =~ /wrong number of arguments \(given 1, expected 0\)/
|
|
# this error can be caused by other reasons
|
|
error("Error loading plugin #{f}. This plugin may be using a deprecated plugin format for WhatWeb version < 0.5.0. Error message: #{err.message}")
|
|
end
|
|
raise if $WWDEBUG == true
|
|
|
|
rescue SyntaxError => err
|
|
error("Error loading plugin #{f}. Error details: #{err.message}")
|
|
raise if $WWDEBUG == true
|
|
|
|
rescue Interrupt
|
|
error("Interrupt detected. Failed to load plugin #{f}.")
|
|
raise if $WWDEBUG == true
|
|
exit 1 # exit with ctrl-c
|
|
end
|
|
|
|
# precompile regular expressions in plugins for performance
|
|
def self.precompile_regular_expressions
|
|
Plugin.registered_plugins.each do |thisplugin|
|
|
matches = thisplugin[1].matches
|
|
next if matches.nil?
|
|
matches.each do |thismatch|
|
|
unless thismatch[:regexp].nil?
|
|
# pp thismatch
|
|
thismatch[:regexp_compiled] = Regexp.new(thismatch[:regexp])
|
|
end
|
|
|
|
[:version, :os, :string, :account, :model, :firmware, :module, :filepath].each do |label|
|
|
if !thismatch[label].nil? && thismatch[label].class == Regexp
|
|
thismatch[:regexp_compiled] = Regexp.new(thismatch[label])
|
|
# pp thismatch
|
|
end
|
|
end
|
|
|
|
unless thismatch[:text].nil?
|
|
thismatch[:regexp_compiled] = Regexp.new(Regexp.escape(thismatch[:text]))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# for adding/removing sets of plugins.
|
|
#
|
|
# --plugins +plugins-disabled,-foobar (+ adds to the full set, -removes from the fullset. items can be directories, files or plugin names)
|
|
# --plugins +/tmp/moo.rb
|
|
# --plugins foobar (only select foobar)
|
|
# --plugins ./plugins-disabled,-md5 (select only plugins from the plugins-disabled folder, remove the md5 plugin from the selected list)
|
|
#
|
|
# does not work correctly with mixed plugin names and files
|
|
#
|
|
def self.load_plugins(list = nil)
|
|
a = []
|
|
b = []
|
|
|
|
plugin_dirs = PLUGIN_DIRS.clone
|
|
plugin_dirs.map { |p| p = File.expand_path(p) }
|
|
|
|
# no selection, so it's default
|
|
unless list
|
|
plugin_dirs.each do |d|
|
|
Dir.glob("#{d}/*.rb").each { |x| PluginSupport.load_plugin(x) }
|
|
end
|
|
return Plugin.registered_plugins
|
|
end
|
|
|
|
# separate list into a and b
|
|
# a = make list of dir & filenames
|
|
# b = make list of assumed pluginnames
|
|
|
|
list.split(',').each do |p|
|
|
choice = PluginChoice.new
|
|
choice.fill(p)
|
|
a << choice if choice.type == 'file'
|
|
b << choice if choice.type == 'plugin'
|
|
end
|
|
|
|
# puts "a: list of dir + filenames"
|
|
# pp a
|
|
# puts "b: list of plugin names"
|
|
# pp b
|
|
# puts "Plugin Dirs"
|
|
# pp plugin_dirs
|
|
|
|
# sort by neither, add, minus
|
|
a = a.sort
|
|
|
|
# plugin_dirs gets wiped out if no modifier is used on a file/folder
|
|
plugin_dirs = [] if a.map(&:modifier).include?(nil)
|
|
|
|
minus_files = [] # make list of files not to load
|
|
a.map do |c|
|
|
plugin_dirs << c.name if c.modifier.nil? || c.modifier == '+'
|
|
plugin_dirs -= [c.name] if c.modifier == '-' # for Dirs
|
|
minus_files << c.name if c.modifier == '-' # for files
|
|
end
|
|
|
|
# puts "Plugin Dirs"
|
|
# pp plugin_dirs
|
|
|
|
# puts "before plugin_dirs.each "
|
|
# pp Plugin.registered_plugins.size
|
|
|
|
# load files from plugin_dirs unless a file is minused
|
|
plugin_dirs.each do |d|
|
|
# if a folder, then load all files
|
|
if File.directory?(d)
|
|
(Dir.glob("#{d}/*.rb") - minus_files).each { |x| PluginSupport.load_plugin(x) }
|
|
elsif File.exist?(d)
|
|
PluginSupport.load_plugin(d)
|
|
else
|
|
error("Error: #{d} is not Dir or File")
|
|
end
|
|
end
|
|
|
|
# puts "after plugin_dirs.each "
|
|
# pp Plugin.registered_plugins.size
|
|
|
|
# make list of plugins to run
|
|
# go through all plugins, remove from list any that match b minus
|
|
selected_plugin_names = []
|
|
|
|
if b.map(&:modifier).include?(nil)
|
|
selected_plugin_names = []
|
|
else
|
|
selected_plugin_names = Plugin.registered_plugins.map { |n, _p| n.downcase }
|
|
end
|
|
|
|
b.map do |c|
|
|
selected_plugin_names << c.name if c.modifier.nil? || c.modifier == '+'
|
|
selected_plugin_names -= [c.name] if c.modifier == '-'
|
|
end
|
|
|
|
# pp selected_plugin_names
|
|
# Plugin.registered_plugins is getting wiped out
|
|
|
|
plugins_to_use = Plugin.registered_plugins.map do |n, p|
|
|
[n, p] if selected_plugin_names.include?(n.downcase)
|
|
end.compact
|
|
# puts "after "
|
|
|
|
# report on plugins that couldn't be found
|
|
unfound_plugins = selected_plugin_names - plugins_to_use.map { |n, _p| n.downcase }
|
|
unless unfound_plugins.empty?
|
|
puts 'Error: The following plugins were not found: ' + unfound_plugins.join(',')
|
|
end
|
|
|
|
# puts "-" * 80
|
|
# pp plugins_to_use
|
|
|
|
plugins_to_use
|
|
end
|
|
|
|
def self.custom_plugin(c, *option)
|
|
if option == ['grep']
|
|
plugin_name = 'Grep'
|
|
|
|
matches = if c[0] == '/' && c[-1] == '/'
|
|
# it's a regex. this might cause some issues?
|
|
"matches([:regexp=>/#{c[1..-2]}/])"
|
|
else
|
|
"matches([:text=>\"#{c}\"])"
|
|
end
|
|
else
|
|
# define a custom plugin on the cmdline
|
|
# ":text=>'powered by abc'" or
|
|
# "{:text=>'powered by abc'},{:regexp=>/abc [ ]?1/i}"
|
|
|
|
# then it's ok..
|
|
if c =~ /:(text|ghdb|md5|regexp|tagpattern)=>[\/'"].*/
|
|
matches = "matches([\{#{c}\}])"
|
|
end
|
|
|
|
# this isn't checked for sanity... loading plugins = cmd exec anyway
|
|
matches = "matches([#{c}])" if c =~ /\{.*\}/
|
|
|
|
plugin_name = 'Custom-Plugin'
|
|
|
|
abort("Invalid custom plugin syntax: #{c}") if matches.nil?
|
|
end
|
|
|
|
custom = %(
|
|
Plugin.define do
|
|
name "#{plugin_name}"
|
|
authors ["WhatWeb"]
|
|
description "User defined"
|
|
website "User defined"
|
|
#{matches}
|
|
end
|
|
)
|
|
|
|
begin
|
|
pp custom if $verbose > 2
|
|
eval(custom)
|
|
true
|
|
rescue SyntaxError
|
|
error('Error: Cannot load custom plugin')
|
|
false
|
|
end
|
|
end
|
|
|
|
### some UI stuff
|
|
def self.plugin_list
|
|
terminal_width = 80
|
|
puts 'WhatWeb Plugin List'
|
|
puts
|
|
puts "Plugin" + " " * 24 + "Website"
|
|
puts '-' * terminal_width
|
|
Plugin.registered_plugins.sort_by { |a, _b| a.downcase }.each do |n, p|
|
|
next if n == "?" # skip the Easter Egg plugin
|
|
|
|
# output fits more description onto a line
|
|
line = "#{n}"
|
|
spaces = terminal_width - 50 - n.size
|
|
spaces = 1 if spaces <= 0
|
|
line += " " * spaces
|
|
line += p.website if p.website
|
|
puts line
|
|
end
|
|
puts '-' * terminal_width
|
|
puts
|
|
puts "Total: #{Plugin.registered_plugins.size} Plugins"
|
|
puts
|
|
puts 'Hint:'
|
|
puts 'For complete plugin descriptions use : whatweb --info-plugins <SEARCH>'
|
|
puts 'Use it without a search term for a complete description of all plugins.'
|
|
puts
|
|
end
|
|
|
|
# Show Google Dorks
|
|
def self.plugin_dorks(plugin_name)
|
|
dorks = []
|
|
|
|
# Loop through plugins
|
|
Plugin.registered_plugins.each do |n, p|
|
|
if n.casecmp(plugin_name.downcase).zero?
|
|
pp "Google Dorks for #{n}:" if $verbose > 2
|
|
dorks << p.dorks unless p.dorks.empty?
|
|
end
|
|
end
|
|
|
|
# Show results if present, else show error message
|
|
if !dorks.empty?
|
|
puts dorks
|
|
else
|
|
error('Google dork lookup failed: Invalid plugin name or no dorks available')
|
|
end
|
|
end
|
|
|
|
# Show plugin information
|
|
def self.plugin_info(keywords = nil)
|
|
terminal_width = 80
|
|
|
|
puts 'WhatWeb Detailed Plugin List'
|
|
puts 'Searching for ' + keywords.join(',') unless keywords.empty?
|
|
|
|
count = { plugins: 0, version_detection: 0, matches: 0, dorks: 0, aggressive: 0, passive: 0 }
|
|
|
|
Plugin.registered_plugins.sort_by { |a, _b| a.downcase }.each do |name, plugin|
|
|
next if name == "?" # skip the Easter Egg plugin
|
|
dump = [name, plugin.authors, plugin.description, plugin.website, plugin.matches].flatten.compact.to_a.join.downcase
|
|
|
|
# this will fail is an expected variable is not defined or empty
|
|
next unless keywords.empty? || keywords.map { |k| dump.include?(k.downcase) }.compact.include?(true)
|
|
puts '=' * terminal_width
|
|
puts 'Plugin:'.ljust(16) + name
|
|
puts '-' * terminal_width
|
|
|
|
if plugin.description
|
|
Helper::word_wrap(plugin.description, terminal_width - 16).each_with_index do |line, index|
|
|
if index == 0
|
|
print 'Description:'.ljust(16)
|
|
else
|
|
print ' ' * 16
|
|
end
|
|
puts line
|
|
end
|
|
else
|
|
puts 'Description:'.ljust(16) + '<Not defined>'
|
|
end
|
|
|
|
puts 'Website:'.ljust(16) + (plugin.website || '<Not defined>')
|
|
puts
|
|
|
|
authors = (if plugin.authors.empty?
|
|
'<Not defined>'
|
|
else
|
|
plugin.authors.join(', ')
|
|
end)
|
|
puts 'Authors:'.ljust(16) + authors
|
|
|
|
puts 'Version:'.ljust(16) + (plugin.version || '<Not defined>')
|
|
puts
|
|
print 'Features:'.ljust(16)
|
|
|
|
print "[#{plugin.matches.any? ? 'Yes' : 'No'}]".ljust(7) + 'Pattern Matching'
|
|
|
|
if plugin.matches.any?
|
|
puts " (#{plugin.matches.size})"
|
|
else
|
|
puts
|
|
end
|
|
|
|
puts ' ' * 16 + "[#{plugin.version_detection? ? 'Yes' : 'No'}]".ljust(7) + 'Version detection from pattern matching'
|
|
puts ' ' * 16 + "[#{plugin.passive ? 'Yes' : 'No'}]".ljust(7) + 'Function for passive matches'
|
|
puts ' ' * 16 + "[#{plugin.aggressive ? 'Yes' : 'No'}]".ljust(7) + 'Function for aggressive matches'
|
|
|
|
count[:version_detection] += 1 if plugin.version_detection?
|
|
count[:passive] += 1 if plugin.passive
|
|
count[:aggressive] += 1 if plugin.aggressive
|
|
|
|
print ' ' * 16 + "[#{plugin.dorks.any? ? 'Yes' : 'No'}]".ljust(7) + 'Google Dorks'
|
|
if plugin.dorks.empty?
|
|
puts
|
|
else
|
|
puts " (#{plugin.dorks.size})"
|
|
end
|
|
|
|
puts
|
|
|
|
unless plugin.dorks.empty?
|
|
puts 'Google Dorks:'
|
|
plugin.dorks.each_with_index do |dork, index|
|
|
puts "[#{index + 1}] #{dork}"
|
|
end
|
|
puts
|
|
count[:dorks] += plugin.dorks.size
|
|
end
|
|
|
|
count[:matches] += plugin.matches.size if plugin.matches
|
|
count[:plugins] += 1
|
|
end
|
|
|
|
puts '=' * terminal_width
|
|
puts "Total plugins: #{count[:plugins]}"
|
|
puts "Total plugins with version detection from pattern matching: #{count[:version_detection]}"
|
|
puts "Total patterns (regular expressions, text, MD5 hashes, etc): #{count[:matches]}"
|
|
puts "Total Google dorks: #{count[:dorks]}"
|
|
puts "Total aggressive functions: #{count[:aggressive]}"
|
|
puts "Total passive functions: #{count[:passive]}"
|
|
puts
|
|
end
|
|
end
|
|
|
|
|
|
# This is used in plugin selection by load_plugins
|
|
class PluginChoice
|
|
attr_accessor :modifier, :type, :name
|
|
|
|
def <=>(_s)
|
|
x = -1 if modifier.nil?
|
|
x = 0 if modifier == '+'
|
|
x = 1 if modifier == '-'
|
|
x
|
|
end
|
|
|
|
def fill(s)
|
|
self.modifier = nil
|
|
self.modifier = s[0].chr if ['+', '-'].include?(s[0].chr)
|
|
|
|
self.name = if modifier
|
|
s[1..-1]
|
|
else
|
|
s
|
|
end
|
|
|
|
# figure out and store the filename or pluginname
|
|
if File.exist?(File.expand_path(name))
|
|
self.type = 'file'
|
|
self.name = File.expand_path(name)
|
|
else
|
|
name.downcase!
|
|
self.type = 'plugin'
|
|
end
|
|
end
|
|
end
|