So long hombre
This commit is contained in:
587
CHANGELOG.md
587
CHANGELOG.md
@@ -1,587 +0,0 @@
|
||||
# Changelog
|
||||
## Master
|
||||
[Work in progress](https://github.com/wpscanteam/wpscan/compare/2.9.4...master)
|
||||
|
||||
## Version 2.9.4
|
||||
Released: 2018-06-15
|
||||
|
||||
* Updated dependencies and required ruby version
|
||||
* Improved CLI output
|
||||
* Only show readme.html output when wp <= 4.8 #1127
|
||||
* Cleanup README.md
|
||||
* Fix bug "undefined method 'identifier' for nil:NilClass" #1149
|
||||
* Since WP 4.7 readme.html only shows major version #1152
|
||||
* Add checks for humans.txt and security.text (Thank you @g0tmi1k!)
|
||||
* Add offline database update support (Thank you @g0tmi1k!)
|
||||
* Check for API access and /wp-json/'s users output (Thank you @g0tmi1k!)
|
||||
* Add RSS author information (Thank you @g0tmi1k!)
|
||||
* Check HTTP status of each value in /robots.txt (Thank you @g0tmi1k!)
|
||||
* Follow any redirections (e.g. http -> https) (Thank you @g0tmi1k!)
|
||||
* Lots of other enhancements by @g0tmi1k & WPScan Team
|
||||
* Database export file enumeration.
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total tracked wordpresses: 319
|
||||
* Total tracked plugins: 74896
|
||||
* Total tracked themes: 16666
|
||||
* Total vulnerable wordpresses: 305
|
||||
* Total vulnerable plugins: 1645
|
||||
* Total vulnerable themes: 286
|
||||
* Total wordpress vulnerabilities: 8327
|
||||
* Total plugin vulnerabilities: 2603
|
||||
* Total theme vulnerabilities: 352
|
||||
|
||||
## Version 2.9.3
|
||||
Released: 2017-07-19
|
||||
|
||||
* Updated dependencies and required ruby version
|
||||
* Made some changes so wpscan works in ruby 2.4
|
||||
* Added a Gemfile.lock to lock all dependencies
|
||||
* You can now pass a wordlist from stdin via "--wordlist -"
|
||||
* Improved version detection regexes
|
||||
* Added an optional paramter to --log to specify a filename
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total tracked wordpresses: 251
|
||||
* Total tracked plugins: 68818
|
||||
* Total tracked themes: 15132
|
||||
* Total vulnerable wordpresses: 243
|
||||
* Total vulnerable plugins: 1527
|
||||
* Total vulnerable themes: 280
|
||||
* Total wordpress vulnerabilities: 5263
|
||||
* Total plugin vulnerabilities: 2406
|
||||
* Total theme vulnerabilities: 349
|
||||
|
||||
## Version 2.9.2
|
||||
Released: 2016-11-15
|
||||
|
||||
* Fixed error when detecting plugins with UTF-8 characters
|
||||
* Use all possible finders to verify a detected version
|
||||
* Fix error when detecting a WordPress version not in our database
|
||||
* Added some additional clarification on error messages
|
||||
* Upgrade terminal-table gem
|
||||
* Add --cache-dir option
|
||||
* Add --disable-tls-checks options
|
||||
* Improve/add additional plugin passive detections
|
||||
* Remove scripts when calculating page hashes
|
||||
* Many other small bug fixes.
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total tracked wordpresses: 194
|
||||
* Total tracked plugins: 63703
|
||||
* Total tracked themes: 13835
|
||||
* Total vulnerable wordpresses: 177
|
||||
* Total vulnerable plugins: 1382
|
||||
* Total vulnerable themes: 379
|
||||
* Total wordpress vulnerabilities: 2617
|
||||
* Total plugin vulnerabilities: 2190
|
||||
* Total theme vulnerabilities: 452
|
||||
|
||||
## Version 2.9.1
|
||||
Released: 2016-05-06
|
||||
|
||||
* Update to Ruby 2.3.1, drop older ruby support
|
||||
* New data file location
|
||||
* Added experimental Windows support
|
||||
* Display WordPress metadata on the detected version
|
||||
* Several small fixes
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total vulnerable versions: 156
|
||||
* Total vulnerable plugins: 1324
|
||||
* Total vulnerable themes: 376
|
||||
* Total version vulnerabilities: 1998
|
||||
* Total plugin vulnerabilities: 2057
|
||||
* Total theme vulnerabilities: 449
|
||||
|
||||
## Version 2.9
|
||||
Released: 2015-10-15
|
||||
|
||||
New
|
||||
* GZIP Encoding in updater
|
||||
* Adds --throttle option to throttle requests
|
||||
* Uses new API and local database file structure
|
||||
* Adds last updated and latest version to plugins and themes
|
||||
|
||||
Removed
|
||||
* ArchAssault from README
|
||||
* APIv1 local databases
|
||||
|
||||
General core
|
||||
* Update to Ruby 2.2.3
|
||||
* Use yajl-ruby as JSON parser
|
||||
* New dependancy for Ubuntu 14.04 (libgmp-dev)
|
||||
* Use Travis container based infra and caching
|
||||
|
||||
Fixed issues
|
||||
* Fix #835 - Readme requests to wp root dir
|
||||
* Fix #836 - Critical icon output twice when the site is not running WP
|
||||
* Fix #839 - Terminal-table dependency is broken
|
||||
* Fix #841 - error: undefined method `cells' for #<Array:0x000000029cc2f8>
|
||||
* Fix #852 - GZIP Encoding in updater
|
||||
* Fix #853 - APIv2 integration
|
||||
* Fix #858 - Detection FP
|
||||
* Fix #873 - false positive "site has Must Use Plugins"
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total vulnerable versions: 132
|
||||
* Total vulnerable plugins: 1170
|
||||
* Total vulnerable themes: 368
|
||||
* Total version vulnerabilities: 1476
|
||||
* Total plugin vulnerabilities: 1913
|
||||
* Total theme vulnerabilities: 450
|
||||
|
||||
## Version 2.8
|
||||
Released: 2015-06-22
|
||||
|
||||
New
|
||||
* Warn the user to update his DB files
|
||||
* Added last db update to --version option (see #815)
|
||||
* Add db checksum to verbose logging during update
|
||||
* Option to hide banner
|
||||
* Continue if user chooses not to update + db exists
|
||||
* Don't update if user chooses default + no DBs exist
|
||||
* Updates request timeout values to realistic ones (and in seconds)
|
||||
|
||||
Removed
|
||||
* Removed `Time.parse('2000-01-01')` expedient
|
||||
* Removed unnecessary 'return' and '()'
|
||||
* Removed debug output
|
||||
* Removed wpstools
|
||||
|
||||
General core
|
||||
* Update to Ruby 2.2.2
|
||||
* Switch to mitre
|
||||
* Install bundler gem README
|
||||
* Switch from gnutls to openssl
|
||||
|
||||
Fixed issues
|
||||
* Fix #789 - Add blackarch to readme
|
||||
* Fix #790 - Consider the target down after 30 requests timed out requests instead of 10
|
||||
* Fix #791 - Rogue character causing the scan of non-wordpress site to crash
|
||||
* Fix #792 - Adds the HttpError exception
|
||||
* Fix #795 - Remove GHOST warning
|
||||
* Fix #796 - Do not swallow exit code
|
||||
* Fix #797 - Increases the timeout values
|
||||
* Fix #801 - Forces UTF-8 encoding when enumerating usernames
|
||||
* Fix #803 - Increases default connect-timeout to 10s
|
||||
* Fix #804 - Updates the Theme detection pattern
|
||||
* Fix #816 - Ignores potential non version chars in theme version detection
|
||||
* Fix #819 - Removes potential spaces in robots.txt entries
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total vulnerable versions: 98
|
||||
* Total vulnerable plugins: 1076
|
||||
* Total vulnerable themes: 361
|
||||
* Total version vulnerabilities: 1104
|
||||
* Total plugin vulnerabilities: 1763
|
||||
* Total theme vulnerabilities: 443
|
||||
|
||||
## Version 2.7
|
||||
Released: 2015-03-16
|
||||
|
||||
New
|
||||
* Detects version in release date format
|
||||
* Copyrights updated
|
||||
* WP version detection from stylesheets
|
||||
* New license
|
||||
* Global HTTP request counter
|
||||
* Add security-protection plugin detection
|
||||
* Add GHOST warning if XMLRPC enabled
|
||||
* Update databases from wpvulndb.com
|
||||
* Enumerate usernames from WP <= 3.0 (thanks berotti3)
|
||||
|
||||
Removed
|
||||
* README.txt
|
||||
|
||||
General core
|
||||
* Update to Ruby 2.2.1
|
||||
* Update to Ruby 2.2.0
|
||||
* Add addressable gem
|
||||
* Update Typhoeus gem to 0.7.0
|
||||
* IDN support: encode non-ascii domain names (thanks dctabuyz)
|
||||
* Improve page hash calculation (thanks dctabuyz)
|
||||
* Version detection regex improved
|
||||
|
||||
Fixed issues
|
||||
* Fix #745 - Plugin version pattern in readme.txt file not detected
|
||||
* Fix #746 - Add a global counter for all active requests to server.
|
||||
* Fix #747 - Add 'security-protection' plugin to wp_login_protection module
|
||||
* Fix #753 - undefined method `round' for "10":String for request or connect timeouts
|
||||
* Fix #760 - typhoeus issue (infinite loop)
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total vulnerable versions: 89
|
||||
* Total vulnerable plugins: 953
|
||||
* Total vulnerable themes: 329
|
||||
* Total version vulnerabilities: 1070
|
||||
* Total plugin vulnerabilities: 1451
|
||||
* Total theme vulnerabilities: 378
|
||||
|
||||
## Version 2.6
|
||||
Released: 2014-12-19
|
||||
|
||||
New
|
||||
* Updates the readmes to reflect the new --usernames option
|
||||
* Improves plugin/theme version detection by looking at the "Version:"
|
||||
* Solution to avoid mandatory blank newline at the end of the wordlist
|
||||
* Add check for valid credentials
|
||||
* Add Sucuri sponsor to banner
|
||||
* Add protocol to sucuri url in banner
|
||||
* Add response code to proxy error output
|
||||
* Add a statement about mandatory newlines at the end of list
|
||||
* Give warning if default username 'admin' is still used
|
||||
* License amendment to make it more clear about value added usage
|
||||
|
||||
Removed
|
||||
* remove malwares
|
||||
* remove malware folder
|
||||
* Removes the theme version check from the readme, unrealistic scenario
|
||||
|
||||
General core
|
||||
* Update to Ruby 2.1.5 and travis
|
||||
* Prevent parent theme infinite loop
|
||||
* Fixes the progressbar being overriden by next brute forcing attempts
|
||||
|
||||
Fixed issues
|
||||
* Fix UTF-8 encode on security db file download
|
||||
* Fix #703 - Disable logging by default. Implement log option.
|
||||
* Fix #705 - Installation instructions for Ubuntu < 14.04 apparently incomplete
|
||||
* Fix #717 - Expand on readme.html finding output
|
||||
* Fix #716 - Adds the --version in the help
|
||||
* Fix #715 - Add new updating info to docs
|
||||
* Fix #727 - WpItems detection: Perform the passive check and filter only vulnerable results at the end if required
|
||||
* Fix #737 - Adds some readme files to check for plugin versions
|
||||
* Fix #739 - Adds the --usernames option
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total vulnerable versions: 88
|
||||
* Total vulnerable plugins: 901
|
||||
* Total vulnerable themes: 313
|
||||
* Total version vulnerabilities: 1050
|
||||
* Total plugin vulnerabilities: 1355
|
||||
* Total theme vulnerabilities: 349
|
||||
|
||||
## Version 2.5.1
|
||||
Released: 2014-09-29
|
||||
|
||||
Fixes reference URL to WPVDB
|
||||
|
||||
## Version 2.5
|
||||
Released: 2014-09-26 (@ BruCON 2014)
|
||||
|
||||
New
|
||||
* Exit program after --update
|
||||
* Detect directory listing in upload folder
|
||||
* Be more verbose when no version can be detected
|
||||
* Added detection for Yoast Wordpress SEO plugin
|
||||
* Also ensure to not process empty Location headers
|
||||
* Ensures a nil location is not processed when enumerating usernames
|
||||
* Fix #626 - Detect 'Must_Use_Plugins'
|
||||
* better username extraction
|
||||
* Add a --cookie option. Ref #485
|
||||
* Add a --no-color option
|
||||
* Output: Give 'Fixed in' an informational tag
|
||||
* Added ArchAssault distro - WPScan comes pre-installed with this distro
|
||||
* Layout changes with new colors
|
||||
|
||||
Removed
|
||||
* Removes the source code updaters
|
||||
* Removes the ListGenerator plugin from WPStools
|
||||
* Removes all files from data/
|
||||
|
||||
General core
|
||||
* Update docs to reflect new updating logic
|
||||
* Little output change and coloring
|
||||
* Adds a missing verbose output
|
||||
* Re-build redirection url if begin with slash '/'
|
||||
* Fixes the remove_conditional_comments function
|
||||
* Ensures to give a string to Typhoeus
|
||||
* Fix wpstools check-vuln-ref-urls
|
||||
* Fix rspecs for new json
|
||||
* Only output if different from style_url
|
||||
* Add exception so 'ruby wpscan.rb http://domain.com' is detected
|
||||
* Added make to Debian installation, which is needed in minimal installation.
|
||||
* Add build-essentials requirement to Ubuntu > 14.04
|
||||
* Updated installation instr. for GNU/Linux Debian.
|
||||
* Changes VersionCompare#is_newer_or_same? by lesser_or_equal?
|
||||
* Fixes the location of the robots.txt check
|
||||
* Updates the recommended ruby version
|
||||
* Rspec 3.0 support
|
||||
* Adds ruby 2.1.2 to Travis
|
||||
* Updated ruby-progressbar to 1.5.0
|
||||
|
||||
WordPress Fingerprints
|
||||
* Adds WP 4.0 fingerprints
|
||||
* Adds WP 3.9.2, 3.8.4 & 3.7.4 fingerprints - Ref #652
|
||||
* Adds 3.9.1 fingerprints
|
||||
|
||||
Fixed issues
|
||||
* Fix #689 - Adds config file to check
|
||||
* Fix #694 - Output Arrays
|
||||
* Fix #693 - Adds pathname require statement
|
||||
* Fix #657 - generate method
|
||||
* Fix #685 - Potenial fix for 'marshal data too short' error
|
||||
* Fix #686 - Adds specs for relative URI in Location headers
|
||||
* Fix #435 - Update license
|
||||
* Fix #674 - Improves the Plugins & Themes passive detection
|
||||
* Fix #673 - Problem with the output
|
||||
* Fix #661 - Don't hash directories named like a file
|
||||
* Fix #653 - Fix for infinite loop in wpstools
|
||||
* Fix #625 - Only parse styles when needed
|
||||
* Fix #481 - Fix for Jetpack plugin false positive
|
||||
* Fix #480 - Properly removes the colour sequence from log
|
||||
* Fix #472 - WPScan stops after redirection if not WordPress website
|
||||
* Fix #464 - Readmes updated to reflect recent changes about the config file & batch mode
|
||||
|
||||
Vulnerabilities
|
||||
* geoplaces4 also uses name GeoPlaces4beta
|
||||
* Added metasploit module's
|
||||
* Added some timthumb detections
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total vulnerable versions: 87
|
||||
* Total vulnerable plugins: 854
|
||||
* Total vulnerable themes: 303
|
||||
* Total version vulnerabilities: 752
|
||||
* Total plugin vulnerabilities: 1351
|
||||
* Total theme vulnerabilities: 345
|
||||
|
||||
## Version 2.4
|
||||
Released: 2014-04-17
|
||||
|
||||
New
|
||||
* '--batch' switch option added - Fix #454
|
||||
* Add random-agent
|
||||
* Added more CLI options
|
||||
* Switch over to nist - Fix #301
|
||||
* New choice added when a redirection is detected - Fix #438
|
||||
|
||||
Removed
|
||||
* Removed 'Total WordPress Sites in the World' counter from stats
|
||||
* Old wpscan repo links removed - Fix #440
|
||||
* Fingerprinting Dev script removed
|
||||
* Useless code removed
|
||||
|
||||
General core
|
||||
* Rspecs update
|
||||
* Forcing Travis notify the team
|
||||
* Ruby 2.1.1 added to Travis
|
||||
* Equal output layout for interaction questions
|
||||
* Only output error trace if verbose if enabled
|
||||
* Memory improvements during wp-items enumerations
|
||||
* Fixed broken link checker, fixed some broken links
|
||||
* Couple more 404s fixed
|
||||
* Themes & Plugins list updated
|
||||
|
||||
WordPress Fingerprints
|
||||
* WP 3.8.2 & 3.7.2 Fingerprints added - Fix #448
|
||||
* WP 3.8.3 & 3.7.3 fingerprints
|
||||
* WP 3.9 fingerprints
|
||||
|
||||
Fixed issues
|
||||
* Fix #380 - Redirects in WP 3.6-3.0
|
||||
* Fix #413 - Check the version of the Timthumbs files found
|
||||
* Fix #429 - Error WpScan Cache Browser
|
||||
* Fix #431 - Version number comparison between '2.3.3' and '0.42b'
|
||||
* Fix #439 - Detect if the target goes down during the scan
|
||||
* Fix #451 - Do not rely only on files in wp-content for fingerprinting
|
||||
* Fix #453 - Documentation or inplemention of option parameters
|
||||
* Fix #455 - Fails with a message if the target returns a 403 during the wordpress check
|
||||
|
||||
Vulnerabilities
|
||||
* Update WordPress Vulnerabilities
|
||||
* Fixed some duplicate vulnerabilities
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total vulnerable versions: 79; 1 is new
|
||||
* Total vulnerable plugins: 748; 55 are new
|
||||
* Total vulnerable themes: 292; 41 are new
|
||||
* Total version vulnerabilities: 617; 326 are new
|
||||
* Total plugin vulnerabilities: 1162; 146 are new
|
||||
* Total theme vulnerabilities: 330; 47 are new
|
||||
|
||||
## Version 2.3
|
||||
Released: 2014-02-11
|
||||
|
||||
New
|
||||
* Brute forcing over https!
|
||||
* Detect and output parent theme!
|
||||
* Complete fingerprint script & hash search
|
||||
* New spell checker!
|
||||
* Added database modification dates in status report
|
||||
* Added 'Total WordPress Sites in the World' statistics
|
||||
* Added separator between Name and Version in Item
|
||||
* Added a "Work in progress" URL in the CHANGELOG
|
||||
|
||||
Removed
|
||||
* Removed "Exiting!" sentence
|
||||
* Removed Backtrack Linux. Not maintained anymore.
|
||||
|
||||
General core
|
||||
* Ruby 2.1.0 added to Travis
|
||||
* Updated the version of WebMock required
|
||||
* Better string concatenation in code (improves speed)
|
||||
* Some modifications in the output of an item
|
||||
* Output cosmetics
|
||||
* rspec-mocks version constraint released
|
||||
* Tabs replaced by spaces
|
||||
* Rspecs update
|
||||
* Indent code cleanup
|
||||
* Themes & Plugins lists regenerated
|
||||
|
||||
Vulnerabilities
|
||||
* Update WordPress Vulnerabilities
|
||||
* Disabled some fake reported vulnerabilities
|
||||
* Fixed some duplicate vulnerabilities
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total vulnerable versions: 78; 2 are new
|
||||
* Total vulnerable plugins: 693; 83 are new
|
||||
* Total vulnerable themes: 251; 55 are new
|
||||
* Total version vulnerabilities: 291 17 are new
|
||||
* Total plugin vulnerabilities: 1016; 236 are new
|
||||
* Total theme vulnerabilities: 283; 79 are new
|
||||
|
||||
WordPress Fingerprints
|
||||
* Better fingerprints
|
||||
* WP 3.8.1 Fingerprinting
|
||||
* WP 3.8 Fingerprinting
|
||||
|
||||
Fixed issues
|
||||
* Fix #404 - Brute forcing issue over https
|
||||
* Fix #398 - Removed a fake vuln in WP Super Cache
|
||||
* Fix #393 - sudo added to the bundle install cmd for Mac OSX
|
||||
* Fix #228, #327 - Infinite loop when self-redirect
|
||||
* Fix #201 - Incorrect Paramter Parsing when no url was supplied
|
||||
|
||||
## Version 2.2
|
||||
Released: 2013-11-12
|
||||
|
||||
New
|
||||
* Output the vulnerability fix if available
|
||||
* Added 'WordPress Version Vulnerability' statistics
|
||||
* Added Kali Linux on the list of pre-installed Linux distributions
|
||||
* Added hosted wordpress detection. See issue #343.
|
||||
* Add detection for all-in-one-seo-pack
|
||||
* Use less memory when brute forcing with a large wordlist
|
||||
* Memory Usage output
|
||||
* Added cve tag to xml file
|
||||
* Add documentation to readme
|
||||
* Add --version switch
|
||||
* Parse robots.txt
|
||||
* Show twitter usernames
|
||||
* Clean logfile on wpstools too
|
||||
* Added pingback header
|
||||
* Request_timeout and connect_timeout implemented
|
||||
* Output interesting http-headers
|
||||
* Kali Linux detection
|
||||
* Ensure that brute forcing results are output even if an error occurs or the user exits
|
||||
* Added debug output
|
||||
* Fixed Version compare for issue #179
|
||||
* Added ruby-progressbar version to Gemfile
|
||||
* Use the redirect_to parameter on bruteforce
|
||||
* Readded "junk removal" from usernames before output
|
||||
* Add license file
|
||||
* Output the timthumb version if found
|
||||
* New enumeration system
|
||||
* More error details for XSD checks
|
||||
* Added default wp-content dir detection, see Issue #141.
|
||||
* Added checks for well formed xml
|
||||
|
||||
Changed
|
||||
* Trying a fix for Kali Linux
|
||||
* Make a seperator between plugin name and vulnerability name
|
||||
* It's WordPress, not Wordpress
|
||||
* Changed wordpress.com scanning error to warning. See issue #343.
|
||||
* Make output lines consistent
|
||||
* Replace packetstormsecurity.org to packetstormsecurity.com
|
||||
* Same URL syntax for all Packet Storm Security URL's
|
||||
* Packet Storm Security URL's don't need the 'friendly part' of the URL. So it can be neglected.
|
||||
* Use online documentation
|
||||
* User prompt on same line
|
||||
* Don't skip passwords that start with a hash. This is fairly common (see RockYou list for example).
|
||||
* Updated Fedora install instructions as per Issue #92
|
||||
* Slight update to security plugin warning. Issue #212.
|
||||
* Ruby-progressbar Gemfile version bump
|
||||
* Fix error with the -U option (undefined method 'merge' for #WpTarget:)
|
||||
* Banner artwork
|
||||
* Fix hacks.rb conflict
|
||||
* Handle when there are 2 headers of the same name
|
||||
* Releasing the Typhoeus version constraint
|
||||
* Amended Arch Linux install instructions. See issue #183.
|
||||
|
||||
Updated
|
||||
* Plugins & Themes updated
|
||||
* Update README.md
|
||||
* Updated documentation
|
||||
|
||||
Removed
|
||||
* Removed 'smileys' in output messages
|
||||
* Removed 'for WordPress' and 'plugin' in title strings.
|
||||
* Removed reference
|
||||
* Removed useless code
|
||||
* Removed duplicate vulnerabilities
|
||||
|
||||
General core
|
||||
* Code cleaning
|
||||
* Fix typo's
|
||||
* Clean up rspecs
|
||||
* Themes & Plugins lists regenerated
|
||||
* Rspecs update
|
||||
* Code Factoring
|
||||
* Added checks for old ruby. Otherwise there will be syntax errors
|
||||
|
||||
Vulnerabilities
|
||||
* Update WordPress Vulnerabilities
|
||||
* Update timthumb due to Secunia #54801
|
||||
* Added WP vuln: 3.4 - 3.5.1 wp-admin/users.php FPD
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total vulnerable versions: 76; 4 are new
|
||||
* Total vulnerable plugins: 610; 201 are new
|
||||
* Total vulnerable themes: 196; 47 are new
|
||||
* Total version vulnerabilities: 274; 53 are new
|
||||
* Total plugin vulnerabilities: 780; 286 are new
|
||||
* Total theme vulnerabilities: 204; 52 are new
|
||||
|
||||
Add WP Fingerprints
|
||||
* WP 3.7.1 Fingerprinting
|
||||
* WP 3.7 Fingerprinting
|
||||
* Ref #280 WP 3.6.1 fingerprint
|
||||
* Added WP 3.6 advanced fingerprint hash. See Issue #255.
|
||||
* Updated MD5 hash of WP 3.6 detection. See Issue #277.
|
||||
* WP 3.5.2 Fingerprint
|
||||
* Bug Fix : Wp 3.5 & 3.5.1 not detected from advanced fingerprinting.
|
||||
|
||||
Fixed issues
|
||||
* Fix #249 - [ERROR] "\xF1" on US-ASCII
|
||||
* Fix #275 - [ERROR] "\xC3" on US-ASCII
|
||||
* Fix #271 - Further Instructions added to the Mac Install
|
||||
* Fix #266 - passive detection regex
|
||||
* Fix #265 - remove base64 images before passive detection
|
||||
* Fix #262 - [ERROR] bad component(expected absolute path component)
|
||||
* Fix #260 - Fixes Travis Fail, due to rspec-mock v2.14.3
|
||||
* Fix #208 - Fixed vulnerable plugins still appear in the results
|
||||
* Fix #245 - all theme enumeration error
|
||||
* Fix #241 - Cant convert array to string
|
||||
* Fix #232 - Crash while enumerating usernames
|
||||
* Fix #223 - New wordpress urls for most popular plugins & themes
|
||||
* Fix #177 - Passive Cache plugins detection (no spec)
|
||||
* Fix #169 - False reports
|
||||
* Fix #182 - Remove the progress-bar static length (120), and let it to automatic
|
||||
* Fix #181 - Don't exit if no usernames found during a simple enumeration (but exit if a brute force is asked)
|
||||
* Fix #200 - Log file not recording the list of username retireved
|
||||
* Fix #164 - README.txt detection
|
||||
* Fix #166 - ListGenerator using the old Browser#get method for full generation
|
||||
* Fix #153 - Disable error trace when it's from the main script
|
||||
* Fix #163 - in the proper way
|
||||
* Fix #144 - Use cookie jar to prevent infinite redirections loop
|
||||
* Fix #158 - Add the solution to 'no such file to load -- rubygems' in the README
|
||||
* Fix #152 - invalid ssl_certificate - response code 0
|
||||
* Fix #147 - can't modify frozen string
|
||||
* Fix #140 - xml_rpc_url in the body
|
||||
* Fix #153 - No error trace when 'No argument supplied'
|
||||
|
||||
## Version 2.1
|
||||
Released 2013-3-4
|
||||
@@ -1,2 +0,0 @@
|
||||
WPScan is not responsible for misuse or for any damage that you may cause!
|
||||
You agree that you use this software at your own risk.
|
||||
37
Dockerfile
37
Dockerfile
@@ -1,37 +0,0 @@
|
||||
FROM ruby:2.5-alpine
|
||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
||||
|
||||
ARG BUNDLER_ARGS="--jobs=8 --without test"
|
||||
|
||||
# Add a new user
|
||||
RUN adduser -h /wpscan -g WPScan -D wpscan
|
||||
|
||||
# Setup gems
|
||||
RUN echo "gem: --no-ri --no-rdoc" > /etc/gemrc
|
||||
|
||||
COPY Gemfile /wpscan
|
||||
COPY Gemfile.lock /wpscan
|
||||
|
||||
# Runtime dependencies
|
||||
RUN apk add --no-cache libcurl procps && \
|
||||
# build dependencies
|
||||
apk add --no-cache --virtual build-deps alpine-sdk ruby-dev libffi-dev zlib-dev && \
|
||||
bundle install --system --gemfile=/wpscan/Gemfile $BUNDLER_ARGS && \
|
||||
apk del --no-cache build-deps
|
||||
|
||||
# Copy over data & set permissions
|
||||
COPY . /wpscan
|
||||
RUN chown -R wpscan:wpscan /wpscan
|
||||
|
||||
# Switch directory
|
||||
WORKDIR /wpscan
|
||||
|
||||
# Switch users
|
||||
USER wpscan
|
||||
|
||||
# Update WPScan
|
||||
RUN /wpscan/wpscan.rb --update --verbose --no-color
|
||||
|
||||
# Run WPScan
|
||||
ENTRYPOINT ["/wpscan/wpscan.rb"]
|
||||
CMD ["--help"]
|
||||
16
Gemfile
16
Gemfile
@@ -1,16 +0,0 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'addressable', '>=2.5.0'
|
||||
gem 'nokogiri', '>=1.7.0.1'
|
||||
gem 'ruby-progressbar', '>=1.8.1'
|
||||
gem 'rubyzip', '>=1.2.1'
|
||||
gem 'terminal-table', '>=1.6.0'
|
||||
gem 'typhoeus', '>=1.1.2'
|
||||
gem 'yajl-ruby', '>=1.3.0' # Better JSON parser regarding memory usage
|
||||
|
||||
group :test do
|
||||
gem 'webmock', '>=2.3.2'
|
||||
gem 'simplecov', '>=0.13.0'
|
||||
gem 'rspec', '>=3.5.0'
|
||||
gem 'rspec-its', '>=1.2.0'
|
||||
end
|
||||
71
Gemfile.lock
71
Gemfile.lock
@@ -1,71 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
diff-lcs (1.3)
|
||||
docile (1.3.1)
|
||||
ethon (0.11.0)
|
||||
ffi (>= 1.3.0)
|
||||
ffi (1.9.25)
|
||||
hashdiff (0.3.7)
|
||||
json (2.1.0)
|
||||
mini_portile2 (2.3.0)
|
||||
nokogiri (1.8.4)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
public_suffix (3.0.2)
|
||||
rspec (3.7.0)
|
||||
rspec-core (~> 3.7.0)
|
||||
rspec-expectations (~> 3.7.0)
|
||||
rspec-mocks (~> 3.7.0)
|
||||
rspec-core (3.7.1)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-expectations (3.7.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-its (1.2.0)
|
||||
rspec-core (>= 3.0.0)
|
||||
rspec-expectations (>= 3.0.0)
|
||||
rspec-mocks (3.7.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-support (3.7.1)
|
||||
ruby-progressbar (1.9.0)
|
||||
rubyzip (1.2.1)
|
||||
safe_yaml (1.0.4)
|
||||
simplecov (0.16.1)
|
||||
docile (~> 1.1)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
typhoeus (1.3.0)
|
||||
ethon (>= 0.9.0)
|
||||
unicode-display_width (1.4.0)
|
||||
webmock (3.4.2)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
yajl-ruby (1.4.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
addressable (>= 2.5.0)
|
||||
nokogiri (>= 1.7.0.1)
|
||||
rspec (>= 3.5.0)
|
||||
rspec-its (>= 1.2.0)
|
||||
ruby-progressbar (>= 1.8.1)
|
||||
rubyzip (>= 1.2.1)
|
||||
simplecov (>= 0.13.0)
|
||||
terminal-table (>= 1.6.0)
|
||||
typhoeus (>= 1.1.2)
|
||||
webmock (>= 2.3.2)
|
||||
yajl-ruby (>= 1.3.0)
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.3
|
||||
74
LICENSE
74
LICENSE
@@ -1,74 +0,0 @@
|
||||
WPScan Public Source License
|
||||
|
||||
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2018 WPScan Team.
|
||||
|
||||
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1 “License” means this document.
|
||||
1.2 “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns WPScan.
|
||||
1.3 “WPScan Team” means WPScan’s core developers.
|
||||
|
||||
2. Commercialization
|
||||
|
||||
A commercial use is one intended for commercial advantage or monetary compensation.
|
||||
|
||||
Example cases of commercialization are:
|
||||
|
||||
- Using WPScan to provide commercial managed/Software-as-a-Service services.
|
||||
- Distributing WPScan as a commercial product or as part of one.
|
||||
- Using WPScan as a value added service/product.
|
||||
|
||||
Example cases which do not require a commercial license, and thus fall under the terms set out below, include (but are not limited to):
|
||||
|
||||
- Penetration testers (or penetration testing organizations) using WPScan as part of their assessment toolkit.
|
||||
- Penetration Testing Linux Distributions including but not limited to Kali Linux, SamuraiWTF, BackBox Linux.
|
||||
- Using WPScan to test your own systems.
|
||||
- Any non-commercial use of WPScan.
|
||||
|
||||
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
|
||||
|
||||
We may grant commercial licenses at no monetary cost at our own discretion if the commercial usage is deemed by the WPScan Team to significantly benefit WPScan.
|
||||
|
||||
Free-use Terms and Conditions;
|
||||
|
||||
3. Redistribution
|
||||
|
||||
Redistribution is permitted under the following conditions:
|
||||
|
||||
- Unmodified License is provided with WPScan.
|
||||
- Unmodified Copyright notices are provided with WPScan.
|
||||
- Does not conflict with the commercialization clause.
|
||||
|
||||
4. Copying
|
||||
|
||||
Copying is permitted so long as it does not conflict with the Redistribution clause.
|
||||
|
||||
5. Modification
|
||||
|
||||
Modification is permitted so long as it does not conflict with the Redistribution clause.
|
||||
|
||||
6. Contributions
|
||||
|
||||
Any Contributions assume the Contributor grants the WPScan Team the unlimited, non-exclusive right to reuse, modify and relicense the Contributor's content.
|
||||
|
||||
7. Support
|
||||
|
||||
WPScan is provided under an AS-IS basis and without any support, updates or maintenance. Support, updates and maintenance may be given according to the sole discretion of the WPScan Team.
|
||||
|
||||
8. Disclaimer of Warranty
|
||||
|
||||
WPScan is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the WPScan is free of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
|
||||
9. Limitation of Liability
|
||||
|
||||
To the extent permitted under Law, WPScan is provided under an AS-IS basis. The WPScan Team shall never, and without any limit, be liable for any damage, cost, expense or any other payment incurred as a result of WPScan's actions, failure, bugs and/or any other interaction between WPScan and end-equipment, computers, other software or any 3rd party, end-equipment, computer or services.
|
||||
|
||||
10. Disclaimer
|
||||
|
||||
Running WPScan against websites without prior mutual consent may be illegal in your country. The WPScan Team accept no liability and are not responsible for any misuse or damage caused by WPScan.
|
||||
|
||||
11. Trademark
|
||||
|
||||
The "wpscan" term is a registered trademark. This License does not grant the use of the "wpscan" trademark or the use of the WPScan logo.
|
||||
318
README.md
318
README.md
@@ -1,318 +0,0 @@
|
||||

|
||||
|
||||
[](https://travis-ci.org/wpscanteam/wpscan)
|
||||
[](https://codeclimate.com/github/wpscanteam/wpscan)
|
||||
[](https://hub.docker.com/r/wpscanteam/wpscan/)
|
||||
[](https://www.patreon.com/wpscan)
|
||||
|
||||

|
||||
|
||||
# LICENSE
|
||||
|
||||
## WPScan Public Source License
|
||||
|
||||
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2018 WPScan Team.
|
||||
|
||||
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
|
||||
|
||||
### 1. Definitions
|
||||
|
||||
1.1 "License" means this document.
|
||||
|
||||
1.2 "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns WPScan.
|
||||
|
||||
1.3 "WPScan Team" means WPScan’s core developers, an updated list of whom can be found within the CREDITS file.
|
||||
|
||||
### 2. Commercialization
|
||||
|
||||
A commercial use is one intended for commercial advantage or monetary compensation.
|
||||
|
||||
Example cases of commercialization are:
|
||||
|
||||
- Using WPScan to provide commercial managed/Software-as-a-Service services.
|
||||
- Distributing WPScan as a commercial product or as part of one.
|
||||
- Using WPScan as a value added service/product.
|
||||
|
||||
Example cases which do not require a commercial license, and thus fall under the terms set out below, include (but are not limited to):
|
||||
|
||||
- Penetration testers (or penetration testing organizations) using WPScan as part of their assessment toolkit.
|
||||
- Penetration Testing Linux Distributions including but not limited to Kali Linux, SamuraiWTF, BackBox Linux.
|
||||
- Using WPScan to test your own systems.
|
||||
- Any non-commercial use of WPScan.
|
||||
|
||||
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
|
||||
|
||||
We may grant commercial licenses at no monetary cost at our own discretion if the commercial usage is deemed by the WPScan Team to significantly benefit WPScan.
|
||||
|
||||
Free-use Terms and Conditions;
|
||||
|
||||
### 3. Redistribution
|
||||
|
||||
Redistribution is permitted under the following conditions:
|
||||
|
||||
- Unmodified License is provided with WPScan.
|
||||
- Unmodified Copyright notices are provided with WPScan.
|
||||
- Does not conflict with the commercialization clause.
|
||||
|
||||
### 4. Copying
|
||||
|
||||
Copying is permitted so long as it does not conflict with the Redistribution clause.
|
||||
|
||||
### 5. Modification
|
||||
|
||||
Modification is permitted so long as it does not conflict with the Redistribution clause.
|
||||
|
||||
### 6. Contributions
|
||||
|
||||
Any Contributions assume the Contributor grants the WPScan Team the unlimited, non-exclusive right to reuse, modify and relicense the Contributor's content.
|
||||
|
||||
### 7. Support
|
||||
|
||||
WPScan is provided under an AS-IS basis and without any support, updates or maintenance. Support, updates and maintenance may be given according to the sole discretion of the WPScan Team.
|
||||
|
||||
### 8. Disclaimer of Warranty
|
||||
|
||||
WPScan is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the WPScan is free of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
|
||||
### 9. Limitation of Liability
|
||||
|
||||
To the extent permitted under Law, WPScan is provided under an AS-IS basis. The WPScan Team shall never, and without any limit, be liable for any damage, cost, expense or any other payment incurred as a result of WPScan's actions, failure, bugs and/or any other interaction between WPScan and end-equipment, computers, other software or any 3rd party, end-equipment, computer or services.
|
||||
|
||||
### 10. Disclaimer
|
||||
|
||||
Running WPScan against websites without prior mutual consent may be illegal in your country. The WPScan Team accept no liability and are not responsible for any misuse or damage caused by WPScan.
|
||||
|
||||
### 11. Trademark
|
||||
|
||||
The "wpscan" term is a registered trademark. This License does not grant the use of the "wpscan" trademark or the use of the WPScan logo.
|
||||
|
||||
# INSTALL
|
||||
|
||||
WPScan comes pre-installed on the following Linux distributions:
|
||||
|
||||
- [BackBox Linux](http://www.backbox.org/)
|
||||
- [Kali Linux](http://www.kali.org/)
|
||||
- [Pentoo](http://www.pentoo.ch/)
|
||||
- [SamuraiWTF](http://samurai.inguardians.com/)
|
||||
- [BlackArch](http://blackarch.org/)
|
||||
|
||||
On macOS WPScan is packaged by [Homebrew](https://brew.sh/) as [`wpscan`](http://braumeister.org/formula/wpscan).
|
||||
|
||||
Windows is not supported
|
||||
|
||||
We suggest you use our official Docker image from https://hub.docker.com/r/wpscanteam/wpscan/ to avoid installation problems.
|
||||
|
||||
# DOCKER
|
||||
## Install Docker
|
||||
[https://docs.docker.com/engine/installation/](https://docs.docker.com/engine/installation/)
|
||||
|
||||
## Get the image
|
||||
Pull the repo with `docker pull wpscanteam/wpscan`
|
||||
|
||||
## Start WPScan
|
||||
|
||||
```
|
||||
docker run -it --rm wpscanteam/wpscan -u https://yourblog.com [options]
|
||||
```
|
||||
|
||||
For the available Options, please see https://github.com/wpscanteam/wpscan#wpscan-arguments
|
||||
|
||||
If you run the git version of wpscan we included some binstubs in ./bin for easier start of wpscan.
|
||||
|
||||
## Examples
|
||||
|
||||
Mount a local wordlist to the docker container and start a bruteforce attack for user admin
|
||||
|
||||
```
|
||||
docker run -it --rm -v ~/wordlists:/wordlists wpscanteam/wpscan --url https://yourblog.com --wordlist /wordlists/crackstation.txt --username admin
|
||||
```
|
||||
|
||||
(This mounts the host directory `~/wordlists` to the container in the path `/wordlists`)
|
||||
|
||||
Use logfile option
|
||||
```
|
||||
# the file must exist prior to starting the container, otherwise docker will create a directory with the filename
|
||||
touch ~/FILENAME
|
||||
docker run -it --rm -v ~/FILENAME:/wpscan/output.txt wpscanteam/wpscan --url https://yourblog.com --log /wpscan/output.txt
|
||||
```
|
||||
|
||||
Published on https://hub.docker.com/r/wpscanteam/wpscan/
|
||||
|
||||
# Manual install
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Ruby >= 2.1.9 - Recommended: 2.5.1
|
||||
- Curl >= 7.21 - Recommended: latest - FYI the 7.29 has a segfault
|
||||
- RubyGems - Recommended: latest
|
||||
- Git
|
||||
|
||||
### Installing dependencies on Ubuntu
|
||||
|
||||
sudo apt-get install libcurl4-openssl-dev libxml2 libxml2-dev libxslt1-dev ruby-dev build-essential libgmp-dev zlib1g-dev
|
||||
|
||||
### Installing dependencies on Debian
|
||||
|
||||
sudo apt-get install gcc git ruby ruby-dev libcurl4-openssl-dev make zlib1g-dev
|
||||
|
||||
### Installing dependencies on Fedora
|
||||
|
||||
sudo dnf install gcc ruby-devel libxml2 libxml2-devel libxslt libxslt-devel libcurl-devel patch rpm-build
|
||||
|
||||
### Installing dependencies on Arch Linux
|
||||
|
||||
pacman -Syu ruby
|
||||
pacman -Syu libyaml
|
||||
|
||||
### Installing dependencies on macOS
|
||||
|
||||
Apple Xcode, Command Line Tools and the libffi are needed (to be able to install the FFI gem), See [http://stackoverflow.com/questions/17775115/cant-setup-ruby-environment-installing-fii-gem-error](http://stackoverflow.com/questions/17775115/cant-setup-ruby-environment-installing-fii-gem-error)
|
||||
|
||||
## Installing with RVM (recommended when doing a manual install)
|
||||
|
||||
If you are using GNOME Terminal, there are some steps required before executing the commands. See here for more information:
|
||||
https://rvm.io/integration/gnome-terminal#integrating-rvm-with-gnome-terminal
|
||||
|
||||
# Install all prerequisites for your OS (look above)
|
||||
cd ~
|
||||
curl -sSL https://rvm.io/mpapis.asc | gpg --import -
|
||||
curl -sSL https://get.rvm.io | bash -s stable
|
||||
source ~/.rvm/scripts/rvm
|
||||
echo "source ~/.rvm/scripts/rvm" >> ~/.bashrc
|
||||
rvm install 2.5.1
|
||||
rvm use 2.5.1 --default
|
||||
echo "gem: --no-ri --no-rdoc" > ~/.gemrc
|
||||
git clone https://github.com/wpscanteam/wpscan.git
|
||||
cd wpscan
|
||||
gem install bundler
|
||||
bundle install --without test
|
||||
|
||||
## Installing manually (not recommended)
|
||||
|
||||
git clone https://github.com/wpscanteam/wpscan.git
|
||||
cd wpscan
|
||||
sudo gem install bundler && bundle install --without test
|
||||
|
||||
# KNOWN ISSUES
|
||||
|
||||
- no such file to load -- rubygems
|
||||
|
||||
```update-alternatives --config ruby```
|
||||
|
||||
And select your ruby version
|
||||
|
||||
See [https://github.com/wpscanteam/wpscan/issues/148](https://github.com/wpscanteam/wpscan/issues/148)
|
||||
|
||||
# WPSCAN ARGUMENTS
|
||||
|
||||
--update Update the database to the latest version.
|
||||
--url | -u <target url> The WordPress URL/domain to scan.
|
||||
--force | -f Forces WPScan to not check if the remote site is running WordPress.
|
||||
--enumerate | -e [option(s)] Enumeration.
|
||||
option :
|
||||
u usernames from id 1 to 10
|
||||
u[10-20] usernames from id 10 to 20 (you must write [] chars)
|
||||
p plugins
|
||||
vp only vulnerable plugins
|
||||
ap all plugins (can take a long time)
|
||||
tt timthumbs
|
||||
t themes
|
||||
vt only vulnerable themes
|
||||
at all themes (can take a long time)
|
||||
Multiple values are allowed : "-e tt,p" will enumerate timthumbs and plugins
|
||||
If no option is supplied, the default is "vt,tt,u,vp"
|
||||
|
||||
--exclude-content-based "<regexp or string>"
|
||||
Used with the enumeration option, will exclude all occurrences based on the regexp or string supplied.
|
||||
You do not need to provide the regexp delimiters, but you must write the quotes (simple or double).
|
||||
--config-file | -c <config file> Use the specified config file, see the example.conf.json.
|
||||
--user-agent | -a <User-Agent> Use the specified User-Agent.
|
||||
--cookie <string> String to read cookies from.
|
||||
--random-agent | -r Use a random User-Agent.
|
||||
--follow-redirection If the target url has a redirection, it will be followed without asking if you wanted to do so or not
|
||||
--batch Never ask for user input, use the default behaviour.
|
||||
--no-color Do not use colors in the output.
|
||||
--log [filename] Creates a log.txt file with WPScan's output if no filename is supplied. Otherwise the filename is used for logging.
|
||||
--no-banner Prevents the WPScan banner from being displayed.
|
||||
--disable-accept-header Prevents WPScan sending the Accept HTTP header.
|
||||
--disable-referer Prevents setting the Referer header.
|
||||
--disable-tls-checks Disables SSL/TLS certificate verification.
|
||||
--wp-content-dir <wp content dir> WPScan try to find the content directory (ie wp-content) by scanning the index page, however you can specify it.
|
||||
Subdirectories are allowed.
|
||||
--wp-plugins-dir <wp plugins dir> Same thing than --wp-content-dir but for the plugins directory.
|
||||
If not supplied, WPScan will use wp-content-dir/plugins. Subdirectories are allowed
|
||||
--proxy <[protocol://]host:port> Supply a proxy. HTTP, SOCKS4 SOCKS4A and SOCKS5 are supported.
|
||||
If no protocol is given (format host:port), HTTP will be used.
|
||||
--proxy-auth <username:password> Supply the proxy login credentials.
|
||||
--basic-auth <username:password> Set the HTTP Basic authentication.
|
||||
--wordlist | -w <wordlist> Supply a wordlist for the password brute forcer.
|
||||
If the "-" option is supplied, the wordlist is expected via STDIN.
|
||||
--username | -U <username> Only brute force the supplied username.
|
||||
--usernames <path-to-file> Only brute force the usernames from the file.
|
||||
--cache-dir <cache-directory> Set the cache directory.
|
||||
--cache-ttl <cache-ttl> Typhoeus cache TTL.
|
||||
--request-timeout <request-timeout> Request Timeout.
|
||||
--connect-timeout <connect-timeout> Connect Timeout.
|
||||
--threads | -t <number of threads> The number of threads to use when multi-threading requests.
|
||||
--throttle <milliseconds> Milliseconds to wait before doing another web request. If used, the --threads should be set to 1.
|
||||
--help | -h This help screen.
|
||||
--verbose | -v Verbose output.
|
||||
--version Output the current version and exit.
|
||||
|
||||
# WPSCAN EXAMPLES
|
||||
|
||||
Do 'non-intrusive' checks...
|
||||
|
||||
```ruby wpscan.rb --url www.example.com```
|
||||
|
||||
Do wordlist password brute force on enumerated users using 50 threads...
|
||||
|
||||
```ruby wpscan.rb --url www.example.com --wordlist darkc0de.lst --threads 50```
|
||||
|
||||
Do wordlist password brute force on enumerated users using STDIN as the wordlist...
|
||||
|
||||
```crunch 5 13 -f charset.lst mixalpha | ruby wpscan.rb --url www.example.com --wordlist -```
|
||||
|
||||
Do wordlist password brute force on the 'admin' username only...
|
||||
|
||||
```ruby wpscan.rb --url www.example.com --wordlist darkc0de.lst --username admin```
|
||||
|
||||
Enumerate installed plugins...
|
||||
|
||||
```ruby wpscan.rb --url www.example.com --enumerate p```
|
||||
|
||||
Run all enumeration tools...
|
||||
|
||||
```ruby wpscan.rb --url www.example.com --enumerate```
|
||||
|
||||
Use custom content directory...
|
||||
|
||||
```ruby wpscan.rb -u www.example.com --wp-content-dir custom-content```
|
||||
|
||||
Update WPScan's databases...
|
||||
|
||||
```ruby wpscan.rb --update```
|
||||
|
||||
Debug output...
|
||||
|
||||
```ruby wpscan.rb --url www.example.com --debug-output 2>debug.log```
|
||||
|
||||
# PROJECT HOME
|
||||
|
||||
[http://www.wpscan.org](http://www.wpscan.org)
|
||||
|
||||
# VULNERABILITY DATABASE
|
||||
|
||||
[https://wpvulndb.com](https://wpvulndb.com)
|
||||
|
||||
# GIT REPOSITORY
|
||||
|
||||
[https://github.com/wpscanteam/wpscan](https://github.com/wpscanteam/wpscan)
|
||||
|
||||
# ISSUES
|
||||
|
||||
[https://github.com/wpscanteam/wpscan/issues](https://github.com/wpscanteam/wpscan/issues)
|
||||
|
||||
# DEVELOPER DOCUMENTATION
|
||||
|
||||
[http://rdoc.info/github/wpscanteam/wpscan/frames](http://rdoc.info/github/wpscanteam/wpscan/frames)
|
||||
21
bin/rspec
21
bin/rspec
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
SOURCE="$(readlink "$SOURCE")"
|
||||
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
|
||||
done
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
cd $DIR/../
|
||||
# always rebuild and include all GEMs
|
||||
docker build --build-arg "BUNDLER_ARGS=--jobs=8" -t wpscan:rspec .
|
||||
# update all gems (this updates Gemfile.lock on the host)
|
||||
# this also needs some build dependencies
|
||||
docker run --rm -u root -v $DIR/../Gemfile.lock:/wpscan/Gemfile.lock --entrypoint "" wpscan:rspec sh -c 'apk add --no-cache alpine-sdk ruby-dev libffi-dev zlib-dev && bundle update'
|
||||
# rebuild image with latest GEMs
|
||||
docker build --build-arg "BUNDLER_ARGS=--jobs=8" -t wpscan:rspec .
|
||||
# run spec
|
||||
docker run --rm -v $DIR/../:/wpscan --entrypoint "" wpscan:rspec rspec
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
SOURCE="$(readlink "$SOURCE")"
|
||||
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
|
||||
done
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
cd $DIR/../
|
||||
docker run --rm -v "$DIR/../":/usr/src/app -w /usr/src/app ruby:2.5-alpine /bin/sh -c "gem install bundler; bundle lock --update"
|
||||
14
bin/wpscan
14
bin/wpscan
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
SOURCE="$(readlink "$SOURCE")"
|
||||
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
|
||||
done
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
cd $DIR/../
|
||||
docker build -q -t wpscan:git .
|
||||
docker run -it --rm wpscan:git "$@"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
SOURCE="${BASH_SOURCE[0]}"
|
||||
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
SOURCE="$(readlink "$SOURCE")"
|
||||
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
|
||||
done
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
cd $DIR/../
|
||||
if [[ -n "$WPSCAN_BUILD" ]]; then
|
||||
docker build -q -t wpscan:git .
|
||||
fi
|
||||
docker run -it --rm -v $DIR/../:/wpscan wpscan:git "$@"
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# from the top level dir:
|
||||
# ln -sf ../../dev/pre-commit-hook.rb .git/hooks/pre-commit
|
||||
|
||||
require 'pty'
|
||||
html_path = 'rspec_results.html'
|
||||
|
||||
begin
|
||||
PTY.spawn( "rspec spec --format h > #{html_path}" ) do |stdin, stdout, pid|
|
||||
begin
|
||||
stdin.each { |line| print line }
|
||||
rescue Errno::EIO => e
|
||||
puts "Error: #{e.to.s}"
|
||||
return 1
|
||||
end
|
||||
end
|
||||
rescue PTY::ChildExited
|
||||
puts 'Child process exit!'
|
||||
end
|
||||
|
||||
# find out if there were any errors
|
||||
html = open(html_path).read
|
||||
examples = html.match(/(\d+) examples/)[0].to_i rescue 0
|
||||
errors = html.match(/(\d+) errors/)[0].to_i rescue 0
|
||||
if errors == 0
|
||||
errors = html.match(/(\d+) failure/)[0].to_i rescue 0
|
||||
end
|
||||
pending = html.match(/(\d+) pending/)[0].to_i rescue 0
|
||||
|
||||
if errors.zero?
|
||||
puts "0 failed! #{examples} run, #{pending} pending"
|
||||
sleep 1
|
||||
exit 0
|
||||
else
|
||||
puts "\aCOMMIT FAILED!!"
|
||||
puts "View your rspec results at #{File.expand_path(html_path)}"
|
||||
puts
|
||||
puts "#{errors} failed! #{examples} run, #{pending} pending"
|
||||
exit 1
|
||||
end
|
||||
19
dev/stats.rb
19
dev/stats.rb
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
# encoding: UTF-8
|
||||
|
||||
require File.expand_path(File.join(__dir__, '..', 'lib', 'wpscan', 'wpscan_helper'))
|
||||
|
||||
wordpress_json = json(WORDPRESSES_FILE)
|
||||
plugins_json = json(PLUGINS_FILE)
|
||||
themes_json = json(THEMES_FILE)
|
||||
|
||||
puts 'WPScan Database Statistics:'
|
||||
puts "* Total tracked wordpresses: #{wordpress_json.count}"
|
||||
puts "* Total tracked plugins: #{plugins_json.count}"
|
||||
puts "* Total tracked themes: #{themes_json.count}"
|
||||
puts "* Total vulnerable wordpresses: #{wordpress_json.select { |item| !wordpress_json[item]['vulnerabilities'].empty? }.count}"
|
||||
puts "* Total vulnerable plugins: #{plugins_json.select { |item| !plugins_json[item]['vulnerabilities'].empty? }.count}"
|
||||
puts "* Total vulnerable themes: #{themes_json.select { |item| !themes_json[item]['vulnerabilities'].empty? }.count}"
|
||||
puts "* Total wordpress vulnerabilities: #{wordpress_json.map {|k,v| v['vulnerabilities'].count}.inject(:+)}"
|
||||
puts "* Total plugin vulnerabilities: #{plugins_json.map {|k,v| v['vulnerabilities'].count}.inject(:+)}"
|
||||
puts "* Total theme vulnerabilities: #{themes_json.map {|k,v| v['vulnerabilities'].count}.inject(:+)}"
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0) Gecko/20100101 Firefox/9.0",
|
||||
|
||||
/* Uncomment the "proxy" line to use the proxy
|
||||
SOCKS proxies (4, 4A, 5) are supported, ie : "proxy": "socks5://127.0.0.1:9000"
|
||||
If you do not specify the protocol, http will be used
|
||||
*/
|
||||
//"proxy": "127.0.0.1:3128",
|
||||
//"proxy_auth": "username:password",
|
||||
|
||||
"cache_ttl": 600, // 10 minutes, at this time the cache is cleaned before each scan. If this value is set to 0, the cache will be disabled
|
||||
|
||||
"request_timeout": 60, // 1min
|
||||
|
||||
"connect_timeout": 10, // 10s
|
||||
|
||||
"max_threads": 20
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/typhoeus_cache'
|
||||
require 'common/browser/actions'
|
||||
require 'common/browser/options'
|
||||
|
||||
class Browser
|
||||
extend Browser::Actions
|
||||
include Browser::Options
|
||||
|
||||
OPTIONS = [
|
||||
:basic_auth,
|
||||
:cache_ttl,
|
||||
:max_threads,
|
||||
:user_agent,
|
||||
:proxy,
|
||||
:proxy_auth,
|
||||
:request_timeout,
|
||||
:connect_timeout,
|
||||
:cookie,
|
||||
:throttle,
|
||||
:disable_accept_header,
|
||||
:disable_referer,
|
||||
:disable_tls_checks
|
||||
]
|
||||
|
||||
@@instance = nil
|
||||
|
||||
attr_reader :hydra, :cache_dir
|
||||
|
||||
attr_accessor :referer, :cookie, :vhost
|
||||
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ Browser ]
|
||||
def initialize(options = {})
|
||||
@cache_dir = options[:cache_dir] || CACHE_DIR + '/browser'
|
||||
|
||||
# sets browser defaults
|
||||
browser_defaults
|
||||
# load config file
|
||||
conf = options[:config_file]
|
||||
load_config(conf) if conf
|
||||
# overrides defaults with user supplied values (overwrite values from config)
|
||||
override_config(options)
|
||||
|
||||
unless @hydra
|
||||
@hydra = Typhoeus::Hydra.new(max_concurrency: self.max_threads)
|
||||
end
|
||||
|
||||
@cache = TyphoeusCache.new(@cache_dir)
|
||||
@cache.clean
|
||||
|
||||
Typhoeus::Config.cache = @cache
|
||||
end
|
||||
|
||||
private_class_method :new
|
||||
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ Browser ]
|
||||
def self.instance(options = {})
|
||||
unless @@instance
|
||||
@@instance = new(options)
|
||||
end
|
||||
@@instance
|
||||
end
|
||||
|
||||
def self.reset
|
||||
@@instance = nil
|
||||
end
|
||||
|
||||
# Override for setting the User-Agent
|
||||
# @param [ String ] user_agent
|
||||
def user_agent=(user_agent)
|
||||
Typhoeus::Config.user_agent = user_agent
|
||||
end
|
||||
|
||||
#
|
||||
# sets browser default values
|
||||
#
|
||||
def browser_defaults
|
||||
Typhoeus::Config.user_agent = "WPScan v#{WPSCAN_VERSION} (http://wpscan.org)"
|
||||
@max_threads = 20
|
||||
# 10 minutes, at this time the cache is cleaned before each scan.
|
||||
# If this value is set to 0, the cache will be disabled
|
||||
@cache_ttl = 600
|
||||
@request_timeout = 60 # 60s
|
||||
@connect_timeout = 10 # 10s
|
||||
@throttle = 0
|
||||
end
|
||||
|
||||
#
|
||||
# If an option was set but is not in the new config_file
|
||||
# it's value is kept
|
||||
#
|
||||
# @param [ String ] config_file
|
||||
#
|
||||
# @return [ void ]
|
||||
def load_config(config_file = nil)
|
||||
if File.symlink?(config_file)
|
||||
raise '[ERROR] Config file is a symlink.'
|
||||
else
|
||||
data = JSON.parse(File.read(config_file))
|
||||
end
|
||||
|
||||
OPTIONS.each do |option|
|
||||
option_name = option.to_s
|
||||
unless data[option_name].nil?
|
||||
self.send(:"#{option_name}=", data[option_name])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Request ]
|
||||
def forge_request(url, params = {})
|
||||
Typhoeus::Request.new(url, merge_request_params(params))
|
||||
end
|
||||
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def merge_request_params(params = {})
|
||||
if @proxy
|
||||
params.merge!(proxy: @proxy)
|
||||
params.merge!(proxyuserpwd: @proxy_auth) if @proxy_auth
|
||||
end
|
||||
|
||||
if @basic_auth
|
||||
params = Browser.append_params_header_field(
|
||||
params,
|
||||
'Authorization',
|
||||
@basic_auth
|
||||
)
|
||||
end
|
||||
|
||||
if vhost
|
||||
params = Browser.append_params_header_field(
|
||||
params,
|
||||
'Host',
|
||||
vhost
|
||||
)
|
||||
end
|
||||
|
||||
params.merge!(referer: referer)
|
||||
params.merge!(timeout: @request_timeout) if @request_timeout && !params.key?(:timeout)
|
||||
params.merge!(connecttimeout: @connect_timeout) if @connect_timeout && !params.key?(:connecttimeout)
|
||||
|
||||
# Used to enable the cache system if :cache_ttl > 0
|
||||
params.merge!(cache_ttl: @cache_ttl) unless params.key?(:cache_ttl)
|
||||
|
||||
# Prevent infinite self redirection
|
||||
params.merge!(maxredirs: 3) unless params.key?(:maxredirs)
|
||||
|
||||
# Disable SSL-Certificate checks
|
||||
if @disable_tls_checks
|
||||
# Cert validity check
|
||||
params.merge!(ssl_verifypeer: 0) unless params.key?(:ssl_verifypeer)
|
||||
# Cert hostname check
|
||||
params.merge!(ssl_verifyhost: 0) unless params.key?(:ssl_verifyhost)
|
||||
end
|
||||
|
||||
params.merge!(cookiejar: @cache_dir + '/cookie-jar')
|
||||
params.merge!(cookiefile: @cache_dir + '/cookie-jar')
|
||||
params.merge!(cookie: @cookie) if @cookie
|
||||
params = Browser.remove_params_header_field(params, 'Accept') if @disable_accept_header
|
||||
params = Browser.remove_params_header_field(params, 'Referer') if @disable_referer
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param [ Hash ] params
|
||||
# @param [ String ] field
|
||||
# @param [ Mixed ] field_value
|
||||
#
|
||||
# @return [ Array ]
|
||||
def self.append_params_header_field(params = {}, field, field_value)
|
||||
if !params.has_key?(:headers)
|
||||
params = params.merge(:headers => { field => field_value })
|
||||
elsif !params[:headers].has_key?(field)
|
||||
params[:headers][field] = field_value
|
||||
end
|
||||
params
|
||||
end
|
||||
|
||||
# @param [ Hash ] params
|
||||
# @param [ String ] field
|
||||
# @param [ Mixed ] field_value
|
||||
#
|
||||
# @return [ Array ]
|
||||
def self.remove_params_header_field(params = {}, field)
|
||||
if !params.has_key?(:headers)
|
||||
params = params.merge(:headers => { field => nil })
|
||||
elsif !params[:headers].has_key?(field)
|
||||
params[:headers][field] = nil
|
||||
end
|
||||
params
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Browser
|
||||
module Actions
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def get(url, params = {})
|
||||
process(url, params.merge(method: :get))
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def post(url, params = {})
|
||||
process(url, params.merge(method: :post))
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def head(url, params = {})
|
||||
process(url, params.merge(method: :head))
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def get_and_follow_location(url, params = {})
|
||||
params[:maxredirs] ||= 2
|
||||
|
||||
get(url, params.merge(followlocation: true))
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def process(url, params)
|
||||
Typhoeus::Request.new(url, Browser.instance.merge_request_params(params)).run
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,122 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Browser
|
||||
module Options
|
||||
|
||||
attr_accessor :request_timeout, :connect_timeout, :user_agent, :disable_accept_header, :disable_referer, :disable_tls_checks
|
||||
attr_reader :basic_auth, :cache_ttl, :proxy, :proxy_auth, :throttle
|
||||
|
||||
# Sets the Basic Authentification credentials
|
||||
# Accepted format:
|
||||
# login:password
|
||||
# Basic base_64_encoded
|
||||
#
|
||||
# @param [ String ] auth
|
||||
#
|
||||
# @return [ void ]
|
||||
def basic_auth=(auth)
|
||||
if auth.index(':')
|
||||
@basic_auth = "Basic #{Base64.encode64(auth).chomp}"
|
||||
elsif auth =~ /\ABasic [a-zA-Z0-9=]+\z/
|
||||
@basic_auth = auth
|
||||
else
|
||||
raise "Invalid basic authentication format, \"login:password\" or \"Basic base_64_encoded\" expected. Your input: #{auth}"
|
||||
end
|
||||
end
|
||||
|
||||
def cache_ttl=(ttl)
|
||||
@cache_ttl = ttl.to_i
|
||||
end
|
||||
|
||||
# @return [ Integer ]
|
||||
def max_threads
|
||||
@max_threads || 1
|
||||
end
|
||||
|
||||
def max_threads=(threads)
|
||||
if threads.is_a?(Integer) && threads > 0
|
||||
@max_threads = threads
|
||||
@hydra = Typhoeus::Hydra.new(max_concurrency: threads)
|
||||
else
|
||||
raise 'max_threads must be an Integer > 0'
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the proxy
|
||||
# Accepted format:
|
||||
# [protocol://]host:post
|
||||
#
|
||||
# Supported protocols:
|
||||
# Depends on the curl protocols, See curl --version
|
||||
#
|
||||
# @param [ String ] proxy
|
||||
#
|
||||
# @return [ void ]
|
||||
def proxy=(proxy)
|
||||
if proxy.index(':')
|
||||
@proxy = proxy
|
||||
else
|
||||
raise 'Invalid proxy format. Should be [protocol://]host:port.'
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the proxy credentials
|
||||
# Accepted format:
|
||||
# username:password
|
||||
# { proxy_username: username, :proxy_password: password }
|
||||
#
|
||||
# @param [ String ] auth
|
||||
#
|
||||
# @return [ void ]
|
||||
def proxy_auth=(auth)
|
||||
unless auth.nil?
|
||||
if auth.is_a?(Hash) && auth.include?(:proxy_username) && auth.include?(:proxy_password)
|
||||
@proxy_auth = auth[:proxy_username] + ':' + auth[:proxy_password]
|
||||
elsif auth.is_a?(String) && auth.index(':') != nil
|
||||
@proxy_auth = auth
|
||||
else
|
||||
raise invalid_proxy_auth_format
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the request timeout
|
||||
# @param [ Integer ] timeout Timeout in ms
|
||||
#
|
||||
# @return [ void ]
|
||||
def request_timeout=(timeout)
|
||||
@request_timeout = timeout.to_i
|
||||
end
|
||||
|
||||
# Sets the connect timeout
|
||||
# @param [ Integer ] timeout Timeout in ms
|
||||
#
|
||||
# @return [ void ]
|
||||
def connect_timeout=(timeout)
|
||||
@connect_timeout = timeout.to_i
|
||||
end
|
||||
|
||||
# @param [ String, Integer ] throttle
|
||||
def throttle=(throttle)
|
||||
@throttle = throttle.to_i.abs / 1000.0
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def invalid_proxy_auth_format
|
||||
'Invalid proxy auth format, expected username:password or {proxy_username: username, proxy_password: password}'
|
||||
end
|
||||
|
||||
# Override with the options if they are set
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ void ]
|
||||
def override_config(options = {})
|
||||
options.each do |option, value|
|
||||
if value != nil and OPTIONS.include?(option)
|
||||
self.send(:"#{option}=", value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,78 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
#
|
||||
# => @todo take consideration of the cache_timeout :
|
||||
# -> create 2 files per key : one for the data storage (key.store ?)
|
||||
# and the other for the cache timeout (key.expiration, key.timeout ?)
|
||||
# or 1 file for all timeouts ?
|
||||
# -> 2 dirs : 1 for storage, the other for cache_timeout ?
|
||||
#
|
||||
|
||||
require 'yaml'
|
||||
require 'fileutils'
|
||||
|
||||
class CacheFileStore
|
||||
attr_reader :storage_path, :cache_dir, :serializer
|
||||
|
||||
# The serializer must have the 2 methods .load and .dump
|
||||
# (Marshal and YAML have them)
|
||||
# YAML is Human Readable, contrary to Marshal which store in a binary format
|
||||
# Marshal does not need any "require"
|
||||
def initialize(storage_path, serializer = Marshal)
|
||||
@cache_dir = File.expand_path(storage_path)
|
||||
@storage_path = File.expand_path(File.join(storage_path, storage_dir))
|
||||
@serializer = serializer
|
||||
|
||||
unless Dir.exist?(@storage_path)
|
||||
FileUtils.mkdir_p(@storage_path)
|
||||
end
|
||||
|
||||
unless Pathname.new(@storage_path).writable?
|
||||
fail "#{@storage_path} is not writable"
|
||||
end
|
||||
end
|
||||
|
||||
def clean
|
||||
# clean old directories
|
||||
Dir[File.join(@cache_dir, '*')].each do |f|
|
||||
if File.directory?(f)
|
||||
# delete directory if create time is older than 4 hours
|
||||
FileUtils.rm_rf(f) if File.mtime(f) < (Time.now - (60*240))
|
||||
else
|
||||
File.delete(f) unless File.symlink?(f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def read_entry(key)
|
||||
begin
|
||||
@serializer.load(File.read(get_entry_file_path(key)))
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def write_entry(key, data_to_store, cache_ttl)
|
||||
if cache_ttl && cache_ttl > 0
|
||||
File.open(get_entry_file_path(key), 'w') do |f|
|
||||
begin
|
||||
f.write(@serializer.dump(data_to_store))
|
||||
rescue
|
||||
nil # spec fix for "can't dump hash with default proc" when stub_request with response headers
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_entry_file_path(key)
|
||||
File::join(@storage_path, key)
|
||||
end
|
||||
|
||||
def storage_dir
|
||||
time = Time.now
|
||||
random = (0...8).map { (65 + rand(26)).chr }.join
|
||||
|
||||
Digest::MD5.hexdigest("#{time}#{random}")
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/vulnerabilities/output'
|
||||
|
||||
class Vulnerabilities < Array
|
||||
include Vulnerabilities::Output
|
||||
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Vulnerabilities < Array
|
||||
module Output
|
||||
|
||||
def output(verbose = false)
|
||||
self.each do |v|
|
||||
v.output(verbose)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,75 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/wp_items/detectable'
|
||||
require 'common/collections/wp_items/output'
|
||||
|
||||
class WpItems < Array
|
||||
extend WpItems::Detectable
|
||||
include WpItems::Output
|
||||
|
||||
attr_accessor :wp_target
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
def initialize(wp_target = nil)
|
||||
self.wp_target = wp_target
|
||||
end
|
||||
|
||||
# @param [String] args
|
||||
#
|
||||
# @return [ void ]
|
||||
def add(*args)
|
||||
index = 0
|
||||
|
||||
until args[index].nil?
|
||||
arg = args[index]
|
||||
|
||||
if arg.is_a?(String)
|
||||
if (next_arg = args[index + 1]).is_a?(Hash)
|
||||
item = create_item(arg, next_arg)
|
||||
index += 1
|
||||
else
|
||||
item = create_item(arg)
|
||||
end
|
||||
elsif arg.is_a?(Item)
|
||||
item = arg
|
||||
else
|
||||
raise 'Invalid arguments'
|
||||
end
|
||||
|
||||
self << item
|
||||
index += 1
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] name
|
||||
# @param [ Hash ] attrs
|
||||
#
|
||||
# @return [ WpItem ]
|
||||
def create_item(name, attrs = {})
|
||||
raise 'wp_target must be set' unless wp_target
|
||||
|
||||
item_class.new(
|
||||
wp_target.uri,
|
||||
attrs.merge(
|
||||
name: name,
|
||||
wp_content_dir: wp_target.wp_content_dir,
|
||||
wp_plugins_dir: wp_target.wp_plugins_dir
|
||||
) { |key, oldval, newval| oldval }
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ WpItems ] other
|
||||
#
|
||||
# @return [ self ]
|
||||
def +(other)
|
||||
other.each { |item| self << item }
|
||||
self
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @return [ Class ]
|
||||
def item_class
|
||||
Object.const_get(self.class.to_s.gsub(/.$/, ''))
|
||||
end
|
||||
end
|
||||
@@ -1,240 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItems < Array
|
||||
module Detectable
|
||||
|
||||
attr_reader :vulns_file, :item_xpath
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
# @option options [ Boolean ] :show_progression Whether or not output the progress bar
|
||||
# @option options [ Boolean ] :only_vulnerable Only check for vulnerable items
|
||||
# @option options [ String ] :exclude_content
|
||||
#
|
||||
# @return [ WpItems ]
|
||||
def aggressive_detection(wp_target, options = {})
|
||||
browser = Browser.instance
|
||||
hydra = browser.hydra
|
||||
targets = targets_items(wp_target, options)
|
||||
progress_bar = progress_bar(targets.size, options)
|
||||
queue_count = 0
|
||||
exist_options = {
|
||||
error_404_hash: wp_target.error_404_hash,
|
||||
homepage_hash: wp_target.homepage_hash,
|
||||
exclude_content: options[:exclude_content] ? %r{#{options[:exclude_content]}} : nil
|
||||
}
|
||||
results = passive_detection(wp_target, options)
|
||||
|
||||
targets.each do |target_item|
|
||||
request = browser.forge_request(target_item.url, request_params)
|
||||
|
||||
request.on_complete do |response|
|
||||
progress_bar.progress += 1 if options[:show_progression]
|
||||
|
||||
if target_item.exists?(exist_options, response)
|
||||
results << target_item unless results.include?(target_item)
|
||||
end
|
||||
end
|
||||
|
||||
hydra.queue(request)
|
||||
queue_count += 1
|
||||
|
||||
if queue_count >= browser.max_threads
|
||||
hydra.run
|
||||
queue_count = 0
|
||||
puts "Sent #{browser.max_threads} requests ..." if options[:verbose]
|
||||
end
|
||||
end
|
||||
|
||||
# run the remaining requests
|
||||
hydra.run
|
||||
|
||||
results.select!(&:vulnerable?) if options[:type] == :vulnerable
|
||||
results.sort!
|
||||
|
||||
results # can't just return results.sort as it would return an array, and we want a WpItems
|
||||
end
|
||||
|
||||
# @param [ Integer ] targets_size
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ ProgressBar ]
|
||||
# :nocov:
|
||||
def progress_bar(targets_size, options)
|
||||
if options[:show_progression]
|
||||
ProgressBar.create(
|
||||
format: '%t %a <%B> (%c / %C) %P%% %e',
|
||||
title: ' ', # Used to craete a left margin
|
||||
total: targets_size
|
||||
)
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ WpItems ]
|
||||
def passive_detection(wp_target, options = {})
|
||||
results = new(wp_target)
|
||||
# improves speed
|
||||
body = remove_base64_images_from_html(Browser.get(wp_target.url).body)
|
||||
page = Nokogiri::HTML(body)
|
||||
names = []
|
||||
|
||||
page.css('link,script,style').each do |tag|
|
||||
%w(href src).each do |attribute|
|
||||
attr_value = tag.attribute(attribute).to_s
|
||||
next unless attr_value
|
||||
|
||||
names << Regexp.last_match[1] if attr_value.match(attribute_pattern(wp_target))
|
||||
end
|
||||
|
||||
next unless tag.name == 'script' || tag.name == 'style'
|
||||
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
if !code.valid_encoding?
|
||||
code = code.encode('UTF-16be', :invalid => :replace, :replace => '?').encode('UTF-8')
|
||||
end
|
||||
|
||||
code.scan(code_pattern(wp_target)).flatten.uniq.each do |item_name|
|
||||
names << item_name
|
||||
end
|
||||
end
|
||||
|
||||
names.uniq.each { |name| results.add(name) }
|
||||
|
||||
results.sort!
|
||||
results
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Regex ]
|
||||
def item_pattern(wp_target)
|
||||
type = to_s.gsub(/Wp/, '').downcase
|
||||
wp_content_dir = wp_target.wp_content_dir
|
||||
wp_content_url = wp_target.uri.merge(wp_content_dir).to_s
|
||||
|
||||
url = wp_content_url.gsub(%r{\A(?:http|https)://}, '(?:https?:)?//').gsub('/', '\\\\\?\/')
|
||||
content_dir = %r{(?:#{url}|\\?\/\\?\/?#{wp_content_dir})}i
|
||||
|
||||
%r{#{content_dir}\\?/#{type}\\?/}
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Regex ]
|
||||
def attribute_pattern(wp_target)
|
||||
/\A#{item_pattern(wp_target)}([^\/]+)/i
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Regex ]
|
||||
def code_pattern(wp_target)
|
||||
/["'\(]#{item_pattern(wp_target)}([^\\\/\)"']+)/i
|
||||
end
|
||||
|
||||
# The default request parameters
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def request_params; { cache_ttl: 0, followlocation: true } end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ options ] options
|
||||
# @option options [ Boolean ] :only_vulnerable
|
||||
# @option options [ String ] :file The path to the file containing the targets
|
||||
#
|
||||
# @return [ Array<WpItem> ]
|
||||
def targets_items(wp_target, options = {})
|
||||
item_class = self.item_class
|
||||
vulns_file = self.vulns_file
|
||||
|
||||
targets = target_items_from_type(wp_target, item_class, vulns_file, options[:type])
|
||||
|
||||
targets.uniq! { |t| t.name }
|
||||
targets.sort_by { rand }
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Class ] item_class
|
||||
# @param [ String ] vulns_file
|
||||
#
|
||||
# @return [ Array<WpItem> ]
|
||||
def target_items_from_type(wp_target, item_class, vulns_file, type)
|
||||
targets = []
|
||||
json = json(vulns_file)
|
||||
|
||||
case type
|
||||
when :vulnerable
|
||||
items = json.select { |item| !json[item]['vulnerabilities'].empty? }.keys
|
||||
when :popular
|
||||
items = json.select { |item| json[item]['popular'] == true }.keys
|
||||
when :all
|
||||
items = json.keys
|
||||
else
|
||||
raise "Unknown type #{type}"
|
||||
end
|
||||
|
||||
items.each do |item|
|
||||
targets << create_item(
|
||||
item_class,
|
||||
item,
|
||||
wp_target,
|
||||
vulns_file
|
||||
)
|
||||
end
|
||||
|
||||
targets
|
||||
end
|
||||
|
||||
# @param [ Class ] klass
|
||||
# @param [ String ] name
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @option [ String ] vulns_file
|
||||
#
|
||||
# @return [ WpItem ]
|
||||
def create_item(klass, name, wp_target, vulns_file = nil)
|
||||
klass.new(
|
||||
wp_target.uri,
|
||||
name: name,
|
||||
vulns_file: vulns_file,
|
||||
wp_content_dir: wp_target.wp_content_dir,
|
||||
wp_plugins_dir: wp_target.wp_plugins_dir
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ String ] file
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Class ] item_class
|
||||
# @param [ String ] vulns_file
|
||||
#
|
||||
# @return [ Array<WpItem> ]
|
||||
def targets_items_from_file(file, wp_target, item_class, vulns_file)
|
||||
targets = []
|
||||
|
||||
File.open(file, 'r') do |f|
|
||||
f.readlines.collect do |item_name|
|
||||
targets << create_item(
|
||||
item_class,
|
||||
item_name.strip,
|
||||
wp_target,
|
||||
vulns_file
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
targets
|
||||
end
|
||||
|
||||
# @return [ Class ]
|
||||
def item_class
|
||||
Object.const_get(self.to_s.gsub(/.$/, ''))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItems < Array
|
||||
module Output
|
||||
|
||||
def output(verbose = false)
|
||||
self.each { |item| item.output(verbose) }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/wp_plugins/detectable'
|
||||
|
||||
class WpPlugins < WpItems
|
||||
extend WpPlugins::Detectable
|
||||
|
||||
end
|
||||
@@ -1,77 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpPlugins < WpItems
|
||||
module Detectable
|
||||
# @return [ String ]
|
||||
def vulns_file
|
||||
PLUGINS_FILE
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ WpPlugins ]
|
||||
def passive_detection(wp_target, options = {})
|
||||
detected = super(wp_target, options)
|
||||
|
||||
detected += from_header(wp_target)
|
||||
detected += from_content(wp_target)
|
||||
|
||||
detected.uniq! { |i| i.name }
|
||||
detected
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# X-Powered-By: W3 Total Cache/0.9.2.5
|
||||
# WP-Super-Cache: Served supercache file from PHP
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ WpPlugins ]
|
||||
def from_header(wp_target)
|
||||
headers = Browser.get(wp_target.url).headers
|
||||
wp_plugins = WpPlugins.new(wp_target)
|
||||
|
||||
if headers
|
||||
powered_by = headers['X-Powered-By'].to_s
|
||||
wp_super_cache = headers['wp-super-cache'].to_s
|
||||
|
||||
if matches = /W3 Total Cache\/([0-9.]+)/i.match(powered_by)
|
||||
wp_plugins.add('w3-total-cache', version: matches[1])
|
||||
end
|
||||
|
||||
wp_plugins.add('wp-super-cache') if wp_super_cache =~ /supercache/i
|
||||
end
|
||||
|
||||
wp_plugins
|
||||
end
|
||||
|
||||
# <!-- Cached page generated by WP-Super-Cache on 2013-05-03 14:46:37 -->
|
||||
# <!-- Performance optimized by W3 Total Cache.
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ WpPlugins ]
|
||||
def from_content(wp_target)
|
||||
body = Browser.get(wp_target.url).body
|
||||
wp_plugins = WpPlugins.new(wp_target)
|
||||
|
||||
wp_plugins.add('wp-super-cache') if body =~ /wp-super-cache/i
|
||||
wp_plugins.add('w3-total-cache') if body =~ /w3 total cache/i
|
||||
|
||||
if body =~ /<!-- all in one seo pack ([^\s]+)/i
|
||||
wp_plugins.add('all-in-one-seo-pack', version: $1)
|
||||
end
|
||||
|
||||
if body =~ /<!-- This site is optimized with the Yoast (?:WordPress )?SEO plugin v([^\s]+) -/i
|
||||
wp_plugins.add('wordpress-seo', version: $1)
|
||||
end
|
||||
|
||||
if body =~ /<!-- Google Universal Analytics for WordPress v([^\s]+) -/i
|
||||
wp_plugins.add('google-universal-analytics', version: $1)
|
||||
end
|
||||
|
||||
wp_plugins
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/wp_themes/detectable'
|
||||
|
||||
class WpThemes < WpItems
|
||||
extend WpThemes::Detectable
|
||||
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpThemes < WpItems
|
||||
module Detectable
|
||||
|
||||
# @return [ String ]
|
||||
def vulns_file
|
||||
THEMES_FILE
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/wp_timthumbs/detectable'
|
||||
|
||||
class WpTimthumbs < WpItems
|
||||
extend WpTimthumbs::Detectable
|
||||
|
||||
end
|
||||
@@ -1,83 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTimthumbs < WpItems
|
||||
module Detectable
|
||||
|
||||
# No passive detection
|
||||
#
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ WpTimthumbs ]
|
||||
def passive_detection(wp_target, options = {})
|
||||
new
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
# @option options [ String ] :file The path to the file containing the targets
|
||||
# @option options [ String ] :theme_name
|
||||
#
|
||||
# @return [ Array<WpTimthumb> ]
|
||||
def targets_items(wp_target, options = {})
|
||||
targets = options[:theme_name] ? theme_timthumbs(options[:theme_name], wp_target) : []
|
||||
|
||||
if options[:file]
|
||||
targets += targets_items_from_file(options[:file], wp_target)
|
||||
end
|
||||
|
||||
targets.uniq { |i| i.url }
|
||||
end
|
||||
|
||||
# @param [ String ] theme_name
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Array<WpTimthumb> ]
|
||||
def theme_timthumbs(theme_name, wp_target)
|
||||
targets = []
|
||||
wp_timthumb = create_item(wp_target)
|
||||
|
||||
%w{
|
||||
timthumb.php lib/timthumb.php inc/timthumb.php includes/timthumb.php
|
||||
scripts/timthumb.php tools/timthumb.php functions/timthumb.php thumb.php
|
||||
}.each do |path|
|
||||
wp_timthumb.path = "$wp-content$/themes/#{theme_name}/#{path}"
|
||||
|
||||
targets << wp_timthumb.dup
|
||||
end
|
||||
targets
|
||||
end
|
||||
|
||||
# @param [ String ] file
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Array<WpTimthumb> ]
|
||||
def targets_items_from_file(file, wp_target)
|
||||
targets = []
|
||||
|
||||
File.open(file, 'r') do |f|
|
||||
f.readlines.collect do |path|
|
||||
targets << create_item(wp_target, path.strip)
|
||||
end
|
||||
end
|
||||
targets
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @option [ String ] path
|
||||
#
|
||||
# @return [ WpTimthumb ]
|
||||
def create_item(wp_target, path = nil)
|
||||
options = {
|
||||
wp_content_dir: wp_target.wp_content_dir,
|
||||
wp_plugins_dir: wp_target.wp_plugins_dir
|
||||
}
|
||||
|
||||
options.merge!(path: path) if path
|
||||
|
||||
WpTimthumb.new(wp_target.uri, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/wp_users/detectable'
|
||||
require 'common/collections/wp_users/output'
|
||||
require 'common/collections/wp_users/brute_forcable'
|
||||
|
||||
class WpUsers < WpItems
|
||||
extend WpUsers::Detectable
|
||||
include WpUsers::Output
|
||||
include WpUsers::BruteForcable
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpUsers < WpItems
|
||||
module BruteForcable
|
||||
|
||||
# Brute force each wp_user
|
||||
#
|
||||
# @param [ String ] wordlist The path to the wordlist
|
||||
# @param [ Hash ] options See WpUser::BruteForcable#brute_force
|
||||
#
|
||||
# @return [ void ]
|
||||
def brute_force(wordlist, options = {})
|
||||
self.each { |wp_user| wp_user.brute_force(wordlist, options) }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpUsers < WpItems
|
||||
module Detectable
|
||||
|
||||
# @return [ Hash ]
|
||||
def request_params; {} end
|
||||
|
||||
# No passive detection
|
||||
#
|
||||
# @return [ WpUsers ]
|
||||
def passive_detection(wp_target, options = {})
|
||||
new
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
# @option options [ Range ] :range ((1..10))
|
||||
#
|
||||
# @return [ Array<WpUser> ]
|
||||
def targets_items(wp_target, options = {})
|
||||
range = options[:range] || (1..10)
|
||||
targets = []
|
||||
|
||||
range.each do |user_id|
|
||||
targets << WpUser.new(wp_target.uri, id: user_id)
|
||||
end
|
||||
targets
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpUsers < WpItems
|
||||
module Output
|
||||
|
||||
# @param [ Hash ] options
|
||||
# @option options[ Boolean ] :show_password Output the password column
|
||||
#
|
||||
# @return [ void ]
|
||||
def output(options = {})
|
||||
rows = []
|
||||
headings = ['ID', 'Login', 'Name']
|
||||
headings << 'Password' if options[:show_password]
|
||||
|
||||
remove_junk_from_display_names
|
||||
|
||||
self.each do |wp_user|
|
||||
row = [wp_user.id, wp_user.login, wp_user.display_name]
|
||||
row << wp_user.password if options[:show_password]
|
||||
rows << row
|
||||
end
|
||||
|
||||
table = Terminal::Table.new(headings: headings,
|
||||
rows: rows,
|
||||
style: { margin_left: options[:margin_left] || '' }).to_s
|
||||
# variable needed for output
|
||||
puts table
|
||||
end
|
||||
|
||||
def remove_junk_from_display_names
|
||||
display_names = []
|
||||
self.each do |u|
|
||||
display_name = u.display_name
|
||||
unless display_name == 'empty'
|
||||
display_names << display_name
|
||||
end
|
||||
end
|
||||
junk = get_equal_string_end(display_names)
|
||||
unless junk.nil? or junk.empty?
|
||||
self.each do |u|
|
||||
u.display_name ||= ''
|
||||
u.display_name = u.display_name.sub(/#{Regexp.escape(junk)}$/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,332 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
# Location directories
|
||||
LIB_DIR = File.expand_path(File.join(__dir__, '..')) # wpscan/lib/
|
||||
ROOT_DIR = File.expand_path(File.join(LIB_DIR, '..')) # wpscan/ - expand_path is used to get "wpscan/" instead of "wpscan/lib/../"
|
||||
USER_DIR = File.expand_path(Dir.home) # ~/
|
||||
|
||||
# Core WPScan directories
|
||||
CACHE_DIR = File.join(USER_DIR, '.wpscan/cache') # ~/.wpscan/cache/
|
||||
DATA_DIR = File.join(USER_DIR, '.wpscan/data') # ~/.wpscan/data/
|
||||
CONF_DIR = File.join(USER_DIR, '.wpscan/conf') # ~/.wpscan/conf/ - Not used ATM (only ref via ./spec/ for travis)
|
||||
COMMON_LIB_DIR = File.join(LIB_DIR, 'common') # wpscan/lib/common/
|
||||
WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan') # wpscan/lib/wpscan/
|
||||
MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models') # wpscan/lib/common/models/
|
||||
|
||||
# Core WPScan files
|
||||
DEFAULT_LOG_FILE = File.join(USER_DIR, '.wpscan/log.txt') # ~/.wpscan/log.txt
|
||||
DATA_FILE = File.join(ROOT_DIR, 'data.zip') # wpscan/data.zip
|
||||
|
||||
# WPScan Data files (data.zip)
|
||||
LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update') # ~/.wpscan/data/.last_update
|
||||
PLUGINS_FILE = File.join(DATA_DIR, 'plugins.json') # ~/.wpscan/data/plugins.json
|
||||
THEMES_FILE = File.join(DATA_DIR, 'themes.json') # ~/.wpscan/data/themes.json
|
||||
TIMTHUMBS_FILE = File.join(DATA_DIR, 'timthumbs.txt') # ~/.wpscan/data/timthumbs.txt
|
||||
USER_AGENTS_FILE = File.join(DATA_DIR, 'user-agents.txt') # ~/.wpscan/data/user-agents.txt
|
||||
WORDPRESSES_FILE = File.join(DATA_DIR, 'wordpresses.json') # ~/.wpscan/data/wordpresses.json
|
||||
WP_VERSIONS_FILE = File.join(DATA_DIR, 'wp_versions.xml') # ~/.wpscan/data/wp_versions.xml
|
||||
|
||||
MIN_RUBY_VERSION = '2.1.9'
|
||||
|
||||
WPSCAN_VERSION = '2.9.5-dev'
|
||||
|
||||
$LOAD_PATH.unshift(LIB_DIR)
|
||||
$LOAD_PATH.unshift(WPSCAN_LIB_DIR)
|
||||
$LOAD_PATH.unshift(MODELS_LIB_DIR)
|
||||
|
||||
def kali_linux?
|
||||
begin
|
||||
File.readlines('/etc/debian_version').grep(/^kali/i).any?
|
||||
rescue
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Determins if installed on Windows OS
|
||||
def windows?
|
||||
Gem.win_platform?
|
||||
end
|
||||
|
||||
require 'environment'
|
||||
require 'zip'
|
||||
|
||||
def escape_glob(s)
|
||||
s.gsub(/[\\\{\}\[\]\*\?]/) { |x| '\\' + x }
|
||||
end
|
||||
|
||||
# TODO : add an exclude pattern ?
|
||||
def require_files_from_directory(absolute_dir_path, files_pattern = '*.rb')
|
||||
files = Dir[File.join(escape_glob(absolute_dir_path), files_pattern)]
|
||||
|
||||
# Files in the root dir are loaded first, then those in the subdirectories
|
||||
files.sort_by { |file| [file.count('/'), file] }.each do |f|
|
||||
f = File.expand_path(f)
|
||||
# puts "require #{f}" # Used for debug
|
||||
require f
|
||||
end
|
||||
end
|
||||
|
||||
require_files_from_directory(COMMON_LIB_DIR, '**/*.rb')
|
||||
|
||||
# Add protocol
|
||||
def add_http_protocol(url)
|
||||
url =~ /^https?:/ ? url : "http://#{url}"
|
||||
end
|
||||
|
||||
def add_trailing_slash(url)
|
||||
url =~ /\/$/ ? url : "#{url}/"
|
||||
end
|
||||
|
||||
def missing_db_files?
|
||||
DbUpdater::FILES.each do |db_file|
|
||||
return true unless File.exist?(File.join(DATA_DIR, db_file))
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Find data.zip?
|
||||
def has_db_zip?
|
||||
return File.exist?(DATA_FILE)
|
||||
end
|
||||
|
||||
# Extract data.zip
|
||||
def extract_db_zip
|
||||
# Create data folder
|
||||
FileUtils.mkdir_p(DATA_DIR)
|
||||
|
||||
Zip::File.open(DATA_FILE) do |zip_file|
|
||||
zip_file.each do |f|
|
||||
# Feedback to the user
|
||||
#puts "[+] Extracting: #{File.basename(f.name)}"
|
||||
f_path = File.join(DATA_DIR, File.basename(f.name))
|
||||
|
||||
# Delete if already there
|
||||
#puts "[+] Deleting: #{File.basename(f.name)}" if File.exist?(f_path)
|
||||
FileUtils.rm(f_path) if File.exist?(f_path)
|
||||
|
||||
# Extract
|
||||
zip_file.extract(f, f_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def last_update
|
||||
date = nil
|
||||
if File.exists?(LAST_UPDATE_FILE)
|
||||
content = File.read(LAST_UPDATE_FILE)
|
||||
date = Time.parse(content) rescue nil
|
||||
end
|
||||
date
|
||||
end
|
||||
|
||||
# Was it 5 days ago?
|
||||
def update_required?
|
||||
date = last_update
|
||||
day_seconds = 24 * 60 * 60
|
||||
five_days_ago = Time.now - (5 * day_seconds)
|
||||
(true if date.nil?) or (date < five_days_ago)
|
||||
end
|
||||
|
||||
# Define colors
|
||||
def colorize(text, color_code)
|
||||
if $COLORSWITCH
|
||||
"#{text}"
|
||||
else
|
||||
"\e[#{color_code}m#{text}\e[0m"
|
||||
end
|
||||
end
|
||||
|
||||
def bold(text)
|
||||
colorize(text, 1)
|
||||
end
|
||||
|
||||
def red(text)
|
||||
colorize(text, 31)
|
||||
end
|
||||
|
||||
def green(text)
|
||||
colorize(text, 32)
|
||||
end
|
||||
|
||||
def amber(text)
|
||||
colorize(text, 33)
|
||||
end
|
||||
|
||||
def blue(text)
|
||||
colorize(text, 34)
|
||||
end
|
||||
|
||||
def critical(text)
|
||||
$exit_code += 1 if defined?($exit_code) # hack for undefined var via rspec
|
||||
"#{red('[!]')} #{text}"
|
||||
end
|
||||
|
||||
def warning(text)
|
||||
$exit_code += 1 if defined?($exit_code) # hack for undefined var via rspec
|
||||
"#{amber('[!]')} #{text}"
|
||||
end
|
||||
|
||||
def info(text)
|
||||
"#{green('[+]')} #{text}"
|
||||
end
|
||||
|
||||
def notice(text)
|
||||
"#{blue('[i]')} #{text}"
|
||||
end
|
||||
|
||||
# our 1337 banner
|
||||
def banner
|
||||
puts '_______________________________________________________________'
|
||||
puts ' __ _______ _____ '
|
||||
puts ' \\ \\ / / __ \\ / ____| '
|
||||
puts ' \\ \\ /\\ / /| |__) | (___ ___ __ _ _ __ ®'
|
||||
puts ' \\ \\/ \\/ / | ___/ \\___ \\ / __|/ _` | \'_ \\ '
|
||||
puts ' \\ /\\ / | | ____) | (__| (_| | | | |'
|
||||
puts ' \\/ \\/ |_| |_____/ \\___|\\__,_|_| |_|'
|
||||
puts
|
||||
puts ' WordPress Security Scanner by the WPScan Team '
|
||||
puts " Version #{WPSCAN_VERSION}"
|
||||
puts ' Sponsored by Sucuri - https://sucuri.net'
|
||||
puts ' @_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_'
|
||||
puts '_______________________________________________________________'
|
||||
puts
|
||||
end
|
||||
|
||||
def xml(file)
|
||||
Nokogiri::XML(File.open(file)) do |config|
|
||||
config.noblanks
|
||||
end
|
||||
end
|
||||
|
||||
def json(file)
|
||||
content = File.open(file).read
|
||||
|
||||
begin
|
||||
JSON.parse(content)
|
||||
rescue => e
|
||||
puts "[ERROR] In JSON file parsing #{file} #{e}"
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def redefine_constant(constant, value)
|
||||
Object.send(:remove_const, constant)
|
||||
Object.const_set(constant, value)
|
||||
end
|
||||
|
||||
# Gets the string all elements in stringarray ends with
|
||||
def get_equal_string_end(stringarray = [''])
|
||||
already_found = ''
|
||||
looping = true
|
||||
counter = -1
|
||||
# remove nils (# Issue #232)
|
||||
stringarray = stringarray.compact
|
||||
if stringarray.kind_of? Array and stringarray.length > 1
|
||||
base = stringarray.first
|
||||
while looping
|
||||
character = base[counter, 1]
|
||||
stringarray.each do |s|
|
||||
if s[counter, 1] != character
|
||||
looping = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if looping == false or (counter * -1) > base.length
|
||||
break
|
||||
end
|
||||
already_found = "#{character if character}#{already_found}"
|
||||
counter -= 1
|
||||
end
|
||||
end
|
||||
already_found
|
||||
end
|
||||
|
||||
def remove_base64_images_from_html(html)
|
||||
# remove data:image/png;base64, images
|
||||
base64regex = %r{(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?}
|
||||
imageregex = %r{data\s*:\s*image/[^\s;]+\s*;\s*base64\s*,\s*}
|
||||
html.gsub(/["']\s*#{imageregex}#{base64regex}\s*["']/, '""')
|
||||
end
|
||||
|
||||
# @return [ Integer ] The memory of the current process in Bytes
|
||||
def get_memory_usage
|
||||
`ps -o rss= -p #{Process.pid}`.to_i * 1024 # ps returns the value in KB
|
||||
end
|
||||
|
||||
# Use the wc system command to count the number of lines in the file
|
||||
# instead of using File.open which will use to much memory for large file (10 times the size of the file)
|
||||
#
|
||||
# @return [ Integer ] The number of lines in the given file
|
||||
def count_file_lines(file)
|
||||
if windows?
|
||||
`findstr /R /N "^" #{file.shellescape} | find /C ":"`.split[0].to_i
|
||||
else
|
||||
`wc -l #{file.shellescape}`.split[0].to_i
|
||||
end
|
||||
end
|
||||
|
||||
# Truncates a string to a specific length and adds ... at the end
|
||||
def truncate(input, size, trailing = '...')
|
||||
size = size.to_i
|
||||
trailing ||= ''
|
||||
return input if input.nil? or size <= 0 or input.length <= size or
|
||||
trailing.length >= input.length or size-trailing.length-1 >= input.length
|
||||
return "#{input[0..size-trailing.length-1]}#{trailing}"
|
||||
end
|
||||
|
||||
# Gets a random User-Agent
|
||||
#
|
||||
# @return [ String ] A random user-agent from data/user-agents.txt
|
||||
def get_random_user_agent
|
||||
user_agents = []
|
||||
|
||||
# If we can't access the file, die
|
||||
raise('[ERROR] Missing user-agent data. Please re-run with just --update.') unless File.exist?(USER_AGENTS_FILE)
|
||||
|
||||
# Read in file
|
||||
f = File.open(USER_AGENTS_FILE, 'r')
|
||||
|
||||
# Read every line in the file
|
||||
f.each_line do |line|
|
||||
# Remove any End of Line issues, and leading/trailing spaces
|
||||
line = line.strip.chomp
|
||||
# Ignore empty files and comments
|
||||
next if line.empty? or line =~ /^\s*(#|\/\/)/
|
||||
# Add to array
|
||||
user_agents << line.strip
|
||||
end
|
||||
# Close file handler
|
||||
f.close
|
||||
|
||||
# Return random user-agent
|
||||
user_agents.sample
|
||||
end
|
||||
|
||||
# Directory listing enabled on url?
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def directory_listing_enabled?(url)
|
||||
Browser.get(url.to_s).body[%r{<title>Index of}] ? true : false
|
||||
end
|
||||
|
||||
def url_encode(str)
|
||||
CGI.escape(str).gsub("+", "%20")
|
||||
end
|
||||
|
||||
# Check valid JSON?
|
||||
def valid_json?(json)
|
||||
JSON.parse(json)
|
||||
return true
|
||||
rescue JSON::ParserError => e
|
||||
return false
|
||||
end
|
||||
|
||||
# Get the HTTP response code
|
||||
def get_http_status(url)
|
||||
Browser.get(url.to_s).code
|
||||
end
|
||||
|
||||
# Check to see if we need a "s"
|
||||
def grammar_s(size)
|
||||
size.to_i >= 2 ? "s" : ""
|
||||
end
|
||||
@@ -1,74 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class CustomOptionParser < OptionParser
|
||||
|
||||
attr_reader :symbols_used
|
||||
|
||||
def initialize(banner = nil, width = 32, indent = ' ' * 4)
|
||||
@results = {}
|
||||
@symbols_used = []
|
||||
super(banner, width, indent)
|
||||
end
|
||||
|
||||
|
||||
# param Array(Array) or Array options
|
||||
def add(options)
|
||||
if options.is_a?(Array)
|
||||
if options[0].is_a?(Array)
|
||||
options.each do |option|
|
||||
add_option(option)
|
||||
end
|
||||
else
|
||||
add_option(options)
|
||||
end
|
||||
else
|
||||
raise "Options must be at least an Array, or an Array(Array). #{options.class} supplied"
|
||||
end
|
||||
end
|
||||
|
||||
# param Array option
|
||||
def add_option(option)
|
||||
if option.is_a?(Array)
|
||||
option_symbol = CustomOptionParser::option_to_symbol(option)
|
||||
|
||||
if !@symbols_used.include?(option_symbol)
|
||||
@symbols_used << option_symbol
|
||||
|
||||
self.on(*option) do |arg|
|
||||
@results[option_symbol] = arg
|
||||
end
|
||||
else
|
||||
raise "The option #{option_symbol} is already used !"
|
||||
end
|
||||
else
|
||||
raise "The option must be an array, #{option.class} supplied : '#{option}'"
|
||||
end
|
||||
end
|
||||
|
||||
# return Hash
|
||||
def results(argv = default_argv)
|
||||
self.parse!(argv) if @results.empty?
|
||||
|
||||
@results
|
||||
end
|
||||
|
||||
protected
|
||||
# param Array option
|
||||
def self.option_to_symbol(option)
|
||||
option_name = nil
|
||||
|
||||
option.each do |option_attr|
|
||||
if option_attr =~ /^--/
|
||||
option_name = option_attr
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if option_name
|
||||
option_name = option_name.gsub(/^--/, '').gsub(/-/, '_').gsub(/ .*$/, '')
|
||||
:"#{option_name}"
|
||||
else
|
||||
raise "Could not find the option name for #{option}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,126 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
# DB Updater
|
||||
class DbUpdater
|
||||
FILES = %w(
|
||||
local_vulnerable_files.xml local_vulnerable_files.xsd
|
||||
timthumbs.txt user-agents.txt wp_versions.xml wp_versions.xsd
|
||||
wordpresses.json plugins.json themes.json LICENSE
|
||||
)
|
||||
|
||||
attr_reader :repo_directory
|
||||
|
||||
def initialize(repo_directory)
|
||||
@repo_directory = repo_directory
|
||||
|
||||
unless Dir.exist?(@repo_directory)
|
||||
FileUtils.mkdir_p(@repo_directory)
|
||||
end
|
||||
|
||||
unless Pathname.new(@repo_directory).writable?
|
||||
fail "#{@repo_directory} is not writable"
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ Hash ] The params for Typhoeus::Request
|
||||
def request_params
|
||||
{
|
||||
ssl_verifyhost: 2,
|
||||
ssl_verifypeer: true,
|
||||
accept_encoding: 'gzip, deflate',
|
||||
timeout: 300,
|
||||
connecttimeout: 20
|
||||
}
|
||||
end
|
||||
|
||||
# @return [ String ] The raw file URL associated with the given filename
|
||||
def remote_file_url(filename)
|
||||
"https://data.wpscan.org/#{filename}"
|
||||
end
|
||||
|
||||
# @return [ String ] The checksum of the associated remote filename
|
||||
def remote_file_checksum(filename)
|
||||
url = "#{remote_file_url(filename)}.sha512"
|
||||
|
||||
res = Browser.get(url, request_params)
|
||||
fail DownloadError, res if res.timed_out? || res.code != 200
|
||||
res.body.chomp
|
||||
end
|
||||
|
||||
def local_file_path(filename)
|
||||
File.join(repo_directory, "#{filename}")
|
||||
end
|
||||
|
||||
def local_file_checksum(filename)
|
||||
Digest::SHA512.file(local_file_path(filename)).hexdigest
|
||||
end
|
||||
|
||||
def backup_file_path(filename)
|
||||
File.join(repo_directory, "#{filename}.back")
|
||||
end
|
||||
|
||||
def create_backup(filename)
|
||||
return unless File.exist?(local_file_path(filename))
|
||||
FileUtils.cp(local_file_path(filename), backup_file_path(filename))
|
||||
end
|
||||
|
||||
def restore_backup(filename)
|
||||
return unless File.exist?(backup_file_path(filename))
|
||||
FileUtils.cp(backup_file_path(filename), local_file_path(filename))
|
||||
end
|
||||
|
||||
def delete_backup(filename)
|
||||
FileUtils.rm(backup_file_path(filename))
|
||||
end
|
||||
|
||||
# @return [ String ] The checksum of the downloaded file
|
||||
def download(filename)
|
||||
file_path = local_file_path(filename)
|
||||
file_url = remote_file_url(filename)
|
||||
|
||||
res = Browser.get(file_url, request_params)
|
||||
fail DownloadError, res if res.timed_out? || res.code != 200
|
||||
File.open(file_path, 'wb') { |f| f.write(res.body) }
|
||||
|
||||
local_file_checksum(filename)
|
||||
end
|
||||
|
||||
def update(verbose = false)
|
||||
FILES.each do |filename|
|
||||
begin
|
||||
puts "[+] Checking: #{filename}" if verbose
|
||||
db_checksum = remote_file_checksum(filename)
|
||||
|
||||
# Checking if the file needs to be updated
|
||||
if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
|
||||
puts ' [i] Already Up-To-Date' if verbose
|
||||
next
|
||||
end
|
||||
|
||||
puts ' [i] Needs to be updated' if verbose
|
||||
create_backup(filename)
|
||||
puts ' [i] Backup Created' if verbose
|
||||
puts " [i] Downloading new file: #{remote_file_url(filename)}" if verbose
|
||||
dl_checksum = download(filename)
|
||||
puts " [i] Downloaded File Checksum: #{dl_checksum}" if verbose
|
||||
puts " [i] Database File Checksum : #{db_checksum}" if verbose
|
||||
|
||||
unless dl_checksum == db_checksum
|
||||
raise ChecksumError.new(File.read(local_file_path(filename))), "#{filename}: checksums do not match (local: #{dl_checksum} remote: #{db_checksum})"
|
||||
end
|
||||
rescue => e
|
||||
puts ' [i] Restoring Backup due to error' if verbose
|
||||
restore_backup(filename)
|
||||
raise e
|
||||
ensure
|
||||
if File.exist?(backup_file_path(filename))
|
||||
puts ' [i] Deleting Backup' if verbose
|
||||
delete_backup(filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# write last_update date to file
|
||||
File.write(LAST_UPDATE_FILE, Time.now)
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
# HTTP Error
|
||||
class HttpError < StandardError
|
||||
attr_reader :response
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
def initialize(response)
|
||||
@response = response
|
||||
end
|
||||
|
||||
def failure_details
|
||||
msg = response.effective_url
|
||||
|
||||
if response.code == 0 || response.timed_out?
|
||||
msg += " (#{response.return_message})"
|
||||
else
|
||||
msg += " (status: #{response.code})"
|
||||
end
|
||||
|
||||
msg
|
||||
end
|
||||
|
||||
def message
|
||||
"HTTP Error: #{failure_details}"
|
||||
end
|
||||
end
|
||||
|
||||
# Used in the Updater
|
||||
class DownloadError < HttpError
|
||||
def message
|
||||
"Unable to get #{failure_details}"
|
||||
end
|
||||
end
|
||||
|
||||
class ChecksumError < StandardError
|
||||
attr_reader :file
|
||||
|
||||
def initialize(file)
|
||||
@file = file
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
# This is used in WpItem::Existable
|
||||
module Typhoeus
|
||||
class Response
|
||||
|
||||
# Compare the body hash to error_404_hash and homepage_hash
|
||||
# returns true if they are different, false otherwise
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_valid_hash?(error_404_hash, homepage_hash)
|
||||
body_hash = WebSite.page_hash(self)
|
||||
|
||||
body_hash != error_404_hash && body_hash != homepage_hash
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# Override for puts to enable logging
|
||||
def puts(o = '')
|
||||
if $log && o.respond_to?(:gsub)
|
||||
temp = o.gsub(/\e\[\d+m/, '') # remove color for logging
|
||||
File.open($log, 'a+') { |f| f.puts(temp) }
|
||||
end
|
||||
|
||||
super(o)
|
||||
end
|
||||
|
||||
class Numeric
|
||||
def bytes_to_human
|
||||
units = %w{B KB MB GB TB}
|
||||
e = (Math.log(abs)/Math.log(1024)).floor
|
||||
s = '%.3f' % (abs.to_f / 1024**e)
|
||||
s.sub(/\.?0*$/, ' ' + units[e])
|
||||
end
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'vulnerability/output'
|
||||
require 'vulnerability/urls'
|
||||
|
||||
class Vulnerability
|
||||
include Vulnerability::Output
|
||||
include Vulnerability::Urls
|
||||
|
||||
attr_accessor :title, :references, :type, :fixed_in
|
||||
|
||||
#
|
||||
# @param [ String ] title The title of the vulnerability
|
||||
# @param [ String ] type The type of the vulnerability
|
||||
# @param [ Hash ] references References
|
||||
# @param [ String ] fixed_in Vuln fixed in Version X
|
||||
#
|
||||
# @return [ Vulnerability ]
|
||||
def initialize(title, type, references = {}, fixed_in = '')
|
||||
@title = title
|
||||
@type = type
|
||||
@references = references
|
||||
@fixed_in = fixed_in
|
||||
end
|
||||
|
||||
# @param [ Vulnerability ] other
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
# :nocov:
|
||||
def ==(other)
|
||||
title == other.title &&
|
||||
type == other.type &&
|
||||
references == other.references &&
|
||||
fixed_in == other.fixed_in
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# Create the Vulnerability from the json_item
|
||||
#
|
||||
# @param [ Hash ] json_item
|
||||
#
|
||||
# @return [ Vulnerability ]
|
||||
def self.load_from_json_item(json_item)
|
||||
references = {}
|
||||
references['id'] = [json_item['id']]
|
||||
|
||||
%w(url cve secunia osvdb metasploit exploitdb).each do |key|
|
||||
if json_item['references'][key]
|
||||
json_item['references'][key] = [json_item['references'][key]] if json_item['references'][key].class != Array
|
||||
references[key] = json_item['references'][key]
|
||||
end
|
||||
end
|
||||
|
||||
new(
|
||||
json_item['title'],
|
||||
json_item['type'],
|
||||
references,
|
||||
json_item['fixed_in']
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Vulnerability
|
||||
module Output
|
||||
# output the vulnerability
|
||||
def output(verbose = false)
|
||||
puts
|
||||
puts critical("Title: #{title}")
|
||||
|
||||
references.each do |key, urls|
|
||||
methodname = "url_#{key}"
|
||||
|
||||
urls.each do |u|
|
||||
next unless respond_to?(methodname)
|
||||
url = send(methodname, u)
|
||||
puts " Reference: #{url}" if url
|
||||
end
|
||||
end
|
||||
|
||||
puts notice("Fixed in: #{fixed_in}") if fixed_in
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Vulnerability
|
||||
module Urls
|
||||
# @return [ String ] The url to the metasploit module page
|
||||
def url_metasploit(module_path)
|
||||
# remove leading slash
|
||||
module_path = module_path.sub(/^\//, '')
|
||||
"https://www.rapid7.com/db/modules/#{module_path}"
|
||||
end
|
||||
|
||||
def url_url(url)
|
||||
url
|
||||
end
|
||||
|
||||
def url_cve(id)
|
||||
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-#{id}"
|
||||
end
|
||||
|
||||
def url_osvdb(id)
|
||||
"http://osvdb.org/show/osvdb/#{id}"
|
||||
end
|
||||
|
||||
def url_secunia(id)
|
||||
"https://secunia.com/advisories/#{id}/"
|
||||
end
|
||||
|
||||
def url_exploitdb(id)
|
||||
"https://www.exploit-db.com/exploits/#{id}/"
|
||||
end
|
||||
|
||||
def url_id(id)
|
||||
"https://wpvulndb.com/vulnerabilities/#{id}"
|
||||
end
|
||||
|
||||
def url_packetstorm(id)
|
||||
"http://packetstormsecurity.com/files/#{id}/"
|
||||
end
|
||||
|
||||
def url_securityfocus(id)
|
||||
"http://www.securityfocus.com/bid/#{id}/"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,121 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'wp_item/findable'
|
||||
require 'wp_item/versionable'
|
||||
require 'wp_item/vulnerable'
|
||||
require 'wp_item/existable'
|
||||
require 'wp_item/infos'
|
||||
require 'wp_item/output'
|
||||
|
||||
class WpItem
|
||||
|
||||
extend WpItem::Findable
|
||||
include WpItem::Versionable
|
||||
include WpItem::Vulnerable
|
||||
include WpItem::Existable
|
||||
include WpItem::Infos
|
||||
include WpItem::Output
|
||||
|
||||
attr_reader :path
|
||||
attr_accessor :name, :wp_content_dir, :wp_plugins_dir
|
||||
|
||||
# @return [ Array ]
|
||||
# Make it private ?
|
||||
def allowed_options
|
||||
[:name, :wp_content_dir, :wp_plugins_dir, :path, :version, :db_file]
|
||||
end
|
||||
|
||||
# @param [ URI ] target_base_uri
|
||||
# @param [ Hash ] options See allowed_option
|
||||
#
|
||||
# @return [ WpItem ]
|
||||
def initialize(target_base_uri, options = {})
|
||||
options[:wp_content_dir] ||= 'wp-content'
|
||||
options[:wp_plugins_dir] ||= options[:wp_content_dir] + '/plugins'
|
||||
|
||||
set_options(options)
|
||||
forge_uri(target_base_uri)
|
||||
end
|
||||
|
||||
def identifier
|
||||
@identifier ||= name
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def db_data
|
||||
@db_data ||= json(db_file)[identifier] || {}
|
||||
end
|
||||
|
||||
def latest_version
|
||||
db_data['latest_version']
|
||||
end
|
||||
|
||||
def last_updated
|
||||
db_data['last_updated']
|
||||
end
|
||||
|
||||
def popular?
|
||||
db_data['popular']
|
||||
end
|
||||
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ void ]
|
||||
def set_options(options)
|
||||
allowed_options.each do |allowed_option|
|
||||
if options.has_key?(allowed_option)
|
||||
method = :"#{allowed_option}="
|
||||
|
||||
if self.respond_to?(method)
|
||||
self.send(method, options[allowed_option])
|
||||
else
|
||||
raise "#{self.class} does not respond to #{method}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
private :set_options
|
||||
|
||||
# @param [ URI ] target_base_uri
|
||||
#
|
||||
# @return [ void ]
|
||||
def forge_uri(target_base_uri)
|
||||
@uri = target_base_uri
|
||||
end
|
||||
|
||||
# @return [ URI ] The uri to the WpItem, with the path if present
|
||||
def uri
|
||||
path ? @uri.merge(path) : @uri
|
||||
end
|
||||
|
||||
# @return [ String ] The url to the WpItem
|
||||
def url; uri.to_s end
|
||||
|
||||
# Sets the path
|
||||
#
|
||||
# Variable, such as $wp-plugins$ and $wp-content$ can be used
|
||||
# and will be replace by their value
|
||||
#
|
||||
# @param [ String ] path
|
||||
#
|
||||
# @return [ void ]
|
||||
def path=(path)
|
||||
@path = path.gsub(/\$wp-plugins\$/i, wp_plugins_dir).gsub(/\$wp-content\$/i, wp_content_dir)
|
||||
end
|
||||
|
||||
# @param [ WpItem ] other
|
||||
def <=>(other)
|
||||
name <=> other.name
|
||||
end
|
||||
|
||||
# @param [ WpItem ] other
|
||||
def ==(other)
|
||||
name === other.name
|
||||
end
|
||||
|
||||
# @param [ WpItem ] other
|
||||
def ===(other)
|
||||
self == other && version === other.version
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItem
|
||||
module Existable
|
||||
|
||||
# Check the existence of the WpItem
|
||||
# If the response is supplied, it's used for the verification
|
||||
# Otherwise a new request is done
|
||||
#
|
||||
# @param [ Hash ] options See exists_from_response?
|
||||
# @param [ Typhoeus::Response ] response
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def exists?(options = {}, response = nil)
|
||||
unless response
|
||||
response = Browser.get(url)
|
||||
end
|
||||
exists_from_response?(response, options)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ options ] options
|
||||
#
|
||||
# @option options [ Hash ] :error_404_hash The hash of the error 404 page
|
||||
# @option options [ Hash ] :homepage_hash The hash of the homepage
|
||||
# @option options [ Hash ] :exclude_content A regexp with the pattern to exclude from the body of the response
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def exists_from_response?(response, options = {})
|
||||
# 301 included as some items do a self-redirect
|
||||
# Redirects to the 404 and homepage should be ignored (unless dynamic content is used)
|
||||
# by the page hashes (error_404_hash & homepage_hash)
|
||||
if [200, 401, 403, 301].include?(response.code)
|
||||
if response.has_valid_hash?(options[:error_404_hash], options[:homepage_hash])
|
||||
if options[:exclude_content]
|
||||
unless response.body.match(options[:exclude_content])
|
||||
return true
|
||||
end
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItem
|
||||
attr_reader :found_from
|
||||
|
||||
# Sets the found_from attribute
|
||||
#
|
||||
# @param [ String ] method The method which found the WpItem
|
||||
#
|
||||
# @return [ void ]
|
||||
def found_from=(method)
|
||||
@found_from = method.to_s.gsub(/find_from_/, '').gsub(/_/, ' ')
|
||||
end
|
||||
|
||||
module Findable
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItem
|
||||
|
||||
# @uri is used instead of #uri to avoid the presence of the :path into it
|
||||
module Infos
|
||||
|
||||
# Checks if the url status code is 200
|
||||
#
|
||||
# @param [ String ] url
|
||||
#
|
||||
# @return [ Boolean ] True if the url status is 200
|
||||
def url_is_200?(url)
|
||||
Browser.get(url).code == 200
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_readme?
|
||||
!readme_url.nil?
|
||||
end
|
||||
|
||||
# @return [ String,nil ] The url to the readme file, nil if not found
|
||||
def readme_url
|
||||
# See https://github.com/wpscanteam/wpscan/pull/737#issuecomment-66375445
|
||||
# for any question about the order
|
||||
%w{readme.txt README.txt README.md readme.md Readme.txt}.each do |readme|
|
||||
url = @uri.merge(readme).to_s
|
||||
return url if url_is_200?(url)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_changelog?
|
||||
!changelog_url.nil?
|
||||
end
|
||||
|
||||
# @return [ String ] The url to the changelog file
|
||||
def changelog_url
|
||||
%w{changelog.txt CHANGELOG.md changelog.md}.each do |changelog|
|
||||
url = @uri.merge(changelog).to_s
|
||||
return url if url_is_200?(url)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_directory_listing?
|
||||
directory_listing_enabled?(@uri)
|
||||
end
|
||||
|
||||
# Discover any error_log files created by WordPress
|
||||
# These are created by the WordPress error_log() function
|
||||
# They are normally found in the /plugins/ directory,
|
||||
# however can also be found in their specific plugin dir.
|
||||
# http://www.exploit-db.com/ghdb/3714/
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_error_log?
|
||||
WebSite.has_log?(error_log_url, %r{PHP Fatal error}i)
|
||||
end
|
||||
|
||||
# @return [ String ] The url to the error_log file
|
||||
def error_log_url
|
||||
@uri.merge('error_log').to_s
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItem
|
||||
module Output
|
||||
|
||||
# @return [ Void ]
|
||||
def output(verbose = false)
|
||||
outdated = VersionCompare.lesser?(version, latest_version) if latest_version
|
||||
|
||||
puts
|
||||
puts info("Name: #{self}") #this will also output the version number if detected
|
||||
puts " | Latest version: #{latest_version} #{'(up to date)' if version}" if latest_version && !outdated
|
||||
puts " | Last updated: #{last_updated}" if last_updated
|
||||
puts " | Location: #{url}"
|
||||
puts " | Readme: #{readme_url}" if has_readme?
|
||||
puts " | Changelog: #{changelog_url}" if has_changelog?
|
||||
puts warning("The version is out of date, the latest version is #{latest_version}") if latest_version && outdated
|
||||
|
||||
puts warning("Directory listing is enabled: #{url}") if has_directory_listing?
|
||||
puts warning("An error_log file has been found: #{error_log_url}") if has_error_log?
|
||||
|
||||
additional_output(verbose) if respond_to?(:additional_output)
|
||||
|
||||
if version.nil? && vulnerabilities.length > 0
|
||||
puts
|
||||
puts warning('We could not determine the version installed. All of the past known vulnerabilities will be output to allow you to do your own manual investigation.')
|
||||
end
|
||||
|
||||
vulnerabilities.output
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItem
|
||||
attr_writer :version
|
||||
|
||||
module Versionable
|
||||
|
||||
# Get the version from the readme.txt
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def version
|
||||
unless @version
|
||||
# This check is needed because readme_url can return nil
|
||||
if has_readme?
|
||||
response = Browser.get(readme_url)
|
||||
@version = extract_version(response.body)
|
||||
end
|
||||
end
|
||||
@version
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
item_version = self.version
|
||||
"#{@name}#{' - v' + item_version.strip if item_version}"
|
||||
end
|
||||
|
||||
# Extracts the version number from a given string/body
|
||||
#
|
||||
# @return [ String ] detected version
|
||||
def extract_version(body)
|
||||
version = body[/\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i, 1]
|
||||
if version.nil? || version !~ /[0-9]+/
|
||||
extracted_versions = body.scan(/[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.-]*[=]+/i)
|
||||
return if extracted_versions.nil? || extracted_versions.length == 0
|
||||
extracted_versions.flatten!
|
||||
# must contain at least one number
|
||||
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
|
||||
sorted = extracted_versions.sort { |x,y|
|
||||
begin
|
||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||
rescue
|
||||
0
|
||||
end
|
||||
}
|
||||
return sorted.last
|
||||
else
|
||||
return version
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItem
|
||||
module Vulnerable
|
||||
attr_accessor :db_file, :identifier
|
||||
|
||||
# Get the vulnerabilities associated to the WpItem
|
||||
# Filters out already fixed vulnerabilities
|
||||
#
|
||||
# @return [ Vulnerabilities ]
|
||||
def vulnerabilities
|
||||
return @vulnerabilities if @vulnerabilities
|
||||
|
||||
@vulnerabilities = Vulnerabilities.new
|
||||
|
||||
[*db_data['vulnerabilities']].each do |vulnerability|
|
||||
vulnerability = Vulnerability.load_from_json_item(vulnerability)
|
||||
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
|
||||
end
|
||||
|
||||
@vulnerabilities
|
||||
end
|
||||
|
||||
def vulnerable?
|
||||
vulnerabilities.empty? ? false : true
|
||||
end
|
||||
|
||||
# Checks if a item is vulnerable to a specific vulnerability
|
||||
#
|
||||
# @param [ Vulnerability ] vuln Vulnerability to check the item against
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def vulnerable_to?(vuln)
|
||||
if version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
|
||||
unless VersionCompare::lesser_or_equal?(vuln.fixed_in, version)
|
||||
return true
|
||||
end
|
||||
else
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,16 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpPlugin < WpItem
|
||||
# Sets the @uri
|
||||
#
|
||||
# @param [ URI ] target_base_uri The URI of the wordpress blog
|
||||
#
|
||||
# @return [ void ]
|
||||
def forge_uri(target_base_uri)
|
||||
@uri = target_base_uri.merge("#{wp_plugins_dir}/#{url_encode(name)}/")
|
||||
end
|
||||
|
||||
def db_file
|
||||
@db_file ||= PLUGINS_FILE
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'wp_theme/findable'
|
||||
require 'wp_theme/versionable'
|
||||
require 'wp_theme/info'
|
||||
require 'wp_theme/output'
|
||||
require 'wp_theme/childtheme'
|
||||
|
||||
class WpTheme < WpItem
|
||||
extend WpTheme::Findable
|
||||
include WpTheme::Versionable
|
||||
include WpTheme::Info
|
||||
include WpTheme::Output
|
||||
include WpTheme::Childtheme
|
||||
|
||||
attr_accessor :referenced_url
|
||||
|
||||
def allowed_options; super << :referenced_url end
|
||||
|
||||
# Sets the @uri
|
||||
#
|
||||
# @param [ URI ] target_base_uri The URI of the wordpress blog
|
||||
#
|
||||
# @return [ void ]
|
||||
def forge_uri(target_base_uri)
|
||||
@uri = target_base_uri.merge("#{wp_content_dir}/themes/#{url_encode(name)}/")
|
||||
end
|
||||
|
||||
# @return [ String ] The url to the theme stylesheet
|
||||
def style_url
|
||||
@uri.merge('style.css').to_s
|
||||
end
|
||||
|
||||
def db_file
|
||||
@db_file ||= THEMES_FILE
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTheme < WpItem
|
||||
module Childtheme
|
||||
|
||||
def parent_theme_limit
|
||||
3
|
||||
end
|
||||
|
||||
def is_child_theme?
|
||||
return true unless @theme_template.nil?
|
||||
false
|
||||
end
|
||||
|
||||
def get_parent_theme_style_url
|
||||
if is_child_theme?
|
||||
return style_url.sub("/#{name}/style.css", "/#{@theme_template}/style.css")
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def get_parent_theme
|
||||
if is_child_theme?
|
||||
base_url = @uri.clone
|
||||
base_url.path = base_url.path.sub(/(?<url>.*\/)#{Regexp.escape(@wp_content_dir)}\/.+/, '\k<url>')
|
||||
return WpTheme.new(base_url,
|
||||
{
|
||||
name: @theme_template,
|
||||
style_url: get_parent_theme_style_url,
|
||||
wp_content_dir: @wp_content_dir
|
||||
})
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,64 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTheme < WpItem
|
||||
module Findable
|
||||
|
||||
# Find the main theme of the blog
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ WpTheme ]
|
||||
def find(target_uri)
|
||||
methods.grep(/^find_from_/).each do |method|
|
||||
if wp_theme = self.send(method, target_uri)
|
||||
wp_theme.found_from = method.to_s
|
||||
|
||||
return wp_theme
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Discover the wordpress theme by parsing the css link rel
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ WpTheme ]
|
||||
def find_from_css_link(target_uri)
|
||||
response = Browser.get_and_follow_location(target_uri.to_s)
|
||||
|
||||
# https + domain is optional because of relative links
|
||||
return unless response.body =~ %r{(?:https?://[^"']+/)?([^"'/\s]+)/themes/([^"'/]+)[^"']*/style\.css}i
|
||||
|
||||
new(
|
||||
target_uri,
|
||||
name: Regexp.last_match[2],
|
||||
referenced_url: Regexp.last_match[0],
|
||||
wp_content_dir: Regexp.last_match[1]
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ WpTheme ]
|
||||
def find_from_wooframework(target_uri)
|
||||
body = Browser.get(target_uri.to_s).body
|
||||
regexp = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?" />\s+<meta name="generator" content="WooFramework\s?([^"]+)?" />}
|
||||
|
||||
if matches = regexp.match(body)
|
||||
woo_theme_name = matches[1]
|
||||
woo_theme_version = matches[2]
|
||||
#woo_framework_version = matches[3] # Not used at this time
|
||||
|
||||
return new(
|
||||
target_uri,
|
||||
name: woo_theme_name,
|
||||
version: woo_theme_version
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTheme < WpItem
|
||||
module Info
|
||||
|
||||
attr_reader :theme_name, :theme_uri, :theme_description,
|
||||
:theme_author, :theme_author_uri, :theme_template,
|
||||
:theme_license, :theme_license_uri, :theme_tags,
|
||||
:theme_text_domain
|
||||
|
||||
def parse_style
|
||||
style = Browser.get(style_url).body
|
||||
@theme_name = parse_style_tag(style, 'Theme Name')
|
||||
@theme_uri = parse_style_tag(style, 'Theme URI')
|
||||
@theme_description = parse_style_tag(style, 'Description')
|
||||
@theme_author = parse_style_tag(style, 'Author')
|
||||
@theme_author_uri = parse_style_tag(style, 'Author URI')
|
||||
@theme_template = parse_style_tag(style, 'Template')
|
||||
@theme_license = parse_style_tag(style, 'License')
|
||||
@theme_license_uri = parse_style_tag(style, 'License URI')
|
||||
@theme_tags = parse_style_tag(style, 'Tags')
|
||||
@theme_text_domain = parse_style_tag(style, 'Text Domain')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_style_tag(style, tag)
|
||||
value = style[/^\s*#{Regexp.escape(tag)}:\s*(.*)/i, 1]
|
||||
return value.strip if value
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTheme
|
||||
module Output
|
||||
|
||||
# @return [ Void ]
|
||||
def additional_output(verbose = false)
|
||||
parse_style
|
||||
|
||||
theme_desc = verbose ? @theme_description : truncate(@theme_description, 100)
|
||||
puts " | Style URL: #{style_url}"
|
||||
puts " | Referenced style.css: #{referenced_url}" if referenced_url && referenced_url != style_url
|
||||
puts " | Theme Name: #{@theme_name}" if @theme_name
|
||||
puts " | Theme URI: #{@theme_uri}" if @theme_uri
|
||||
puts " | Description: #{theme_desc}" if theme_desc
|
||||
puts " | Author: #{@theme_author}" if @theme_author
|
||||
puts " | Author URI: #{@theme_author_uri}" if @theme_author_uri
|
||||
puts " | Template: #{@theme_template}" if @theme_template and verbose
|
||||
puts " | License: #{@theme_license}" if @theme_license and verbose
|
||||
puts " | License URI: #{@theme_license_uri}" if @theme_license_uri and verbose
|
||||
puts " | Tags: #{@theme_tags}" if @theme_tags and verbose
|
||||
puts " | Text Domain: #{@theme_text_domain}" if @theme_text_domain and verbose
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTheme < WpItem
|
||||
module Versionable
|
||||
def version
|
||||
@version ||= Browser.get(style_url).body[%r{Version:\s*(?!trunk)([0-9a-z\.-]+)}i, 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'wp_timthumb/versionable'
|
||||
require 'wp_timthumb/existable'
|
||||
require 'wp_timthumb/output'
|
||||
require 'wp_timthumb/vulnerable'
|
||||
|
||||
class WpTimthumb < WpItem
|
||||
include WpTimthumb::Versionable
|
||||
include WpTimthumb::Existable
|
||||
include WpTimthumb::Output
|
||||
include WpTimthumb::Vulnerable
|
||||
|
||||
# @param [ WpTimthumb ] other
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def ==(other)
|
||||
url == other.url
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTimthumb < WpItem
|
||||
module Existable
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def exists_from_response?(response, options = {})
|
||||
response.code == 400 && response.body =~ /no image specified/i ? true : false
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTimthumb < WpItem
|
||||
module Output
|
||||
|
||||
def output(verbose = false)
|
||||
puts
|
||||
puts info("#{self}") #this will also output the version number if detected
|
||||
|
||||
vulnerabilities.output
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTimthumb < WpItem
|
||||
module Versionable
|
||||
|
||||
# Get the version from the body of an invalid request
|
||||
# See https://code.google.com/p/timthumb/source/browse/trunk/timthumb.php#426
|
||||
#
|
||||
# @return [ String ] The version
|
||||
def version
|
||||
unless @version
|
||||
response = Browser.get(url)
|
||||
@version = response.body[%r{TimThumb version\s*: ([^<]+)} , 1]
|
||||
end
|
||||
@version
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
"#{url}#{ ' v' + version if version}"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTimthumb < WpItem
|
||||
module Vulnerable
|
||||
# @return [ Vulnerabilities ]
|
||||
def vulnerabilities
|
||||
vulns = Vulnerabilities.new
|
||||
|
||||
[:check_rce_132, :check_rce_webshot].each do |method|
|
||||
vuln = self.send(method)
|
||||
|
||||
vulns << vuln if vuln
|
||||
end
|
||||
vulns
|
||||
end
|
||||
|
||||
def check_rce_132
|
||||
rce_132_vuln unless VersionCompare.lesser_or_equal?('1.33', version)
|
||||
end
|
||||
|
||||
# Vulnerable versions : > 1.35 (or >= 2.0) and < 2.8.14
|
||||
def check_rce_webshot
|
||||
return if VersionCompare.lesser_or_equal?('2.8.14', version) || VersionCompare.lesser_or_equal?(version, '1.35')
|
||||
|
||||
response = Browser.get(uri.merge('?webshot=1&src=http://' + default_allowed_domains.sample))
|
||||
|
||||
rce_webshot_vuln unless response.body =~ /WEBSHOT_ENABLED == true/
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
|
||||
def default_allowed_domains
|
||||
%w(flickr.com picasa.com img.youtube.com upload.wikimedia.org)
|
||||
end
|
||||
|
||||
# @return [ Vulnerability ] The RCE in the <= 1.32
|
||||
def rce_132_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 1.32 Remote Code Execution',
|
||||
'RCE',
|
||||
{ exploitdb: ['17602'] },
|
||||
'1.33'
|
||||
)
|
||||
end
|
||||
|
||||
# @return [ Vulnerability ] The RCE due to the WebShot in the <= 2.8.13
|
||||
def rce_webshot_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
|
||||
'RCE',
|
||||
{ url: ['http://seclists.org/fulldisclosure/2014/Jun/117'] },
|
||||
'2.8.14'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,81 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'wp_user/existable'
|
||||
require 'wp_user/brute_forcable'
|
||||
|
||||
class WpUser < WpItem
|
||||
include WpUser::Existable
|
||||
include WpUser::BruteForcable
|
||||
|
||||
attr_accessor :id, :login, :display_name, :password
|
||||
|
||||
# @return [ Array<Symbol> ]
|
||||
def allowed_options; [:id, :login, :display_name, :password] end
|
||||
|
||||
# @return [ URI ] The uri to the author page
|
||||
def uri
|
||||
if id
|
||||
@uri.merge("?author=#{id}")
|
||||
else
|
||||
raise 'The id is nil'
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def login_url
|
||||
unless @login_url
|
||||
@login_url = @uri.merge('wp-login.php').to_s
|
||||
|
||||
# Let's check if the login url is redirected (to https url for example)
|
||||
if redirection = redirection(@login_url)
|
||||
@login_url = redirection
|
||||
end
|
||||
end
|
||||
|
||||
@login_url
|
||||
end
|
||||
|
||||
def redirection(url)
|
||||
redirection = nil
|
||||
response = Browser.get(url)
|
||||
|
||||
if response.code == 301 || response.code == 302
|
||||
redirection = response.headers_hash['location']
|
||||
|
||||
# Let's check if there is a redirection in the redirection
|
||||
if other_redirection = redirection(redirection)
|
||||
redirection = other_redirection
|
||||
end
|
||||
end
|
||||
|
||||
redirection
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
s = "#{id}"
|
||||
s << " | #{login}" if login
|
||||
s << " | #{display_name}" if display_name
|
||||
s
|
||||
end
|
||||
|
||||
# @param [ WpUser ] other
|
||||
def <=>(other)
|
||||
id <=> other.id
|
||||
end
|
||||
|
||||
# @param [ WpUser ] other
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def ==(other)
|
||||
self === other
|
||||
end
|
||||
|
||||
# @param [ WpUser ] other
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def ===(other)
|
||||
id === other.id && login === other.login
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,148 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpUser < WpItem
|
||||
module BruteForcable
|
||||
|
||||
attr_reader :progress_bar
|
||||
|
||||
# Brute force the user with the wordlist supplied
|
||||
#
|
||||
# It can take a long time to queue 2 million requests,
|
||||
# for that reason, we queue browser.max_threads, send browser.max_threads,
|
||||
# queue browser.max_threads and so on.
|
||||
#
|
||||
# hydra.run only returns when it has recieved all of its, responses.
|
||||
# This means that while we are waiting for browser.max_threads,
|
||||
# responses, we are waiting...
|
||||
#
|
||||
# @param [ String ] wordlist The wordlist path
|
||||
# @param [ Hash ] options
|
||||
# @option options [ Boolean ] :verbose
|
||||
# @option options [ Boolean ] :show_progression
|
||||
# @param [ String ] redirect_url Override for redirect_url
|
||||
#
|
||||
# @return [ void ]
|
||||
def brute_force(wordlist, options = {}, redirect_url = nil)
|
||||
browser = Browser.instance
|
||||
hydra = browser.hydra
|
||||
queue_count = 0
|
||||
found = false
|
||||
|
||||
if wordlist == '-'
|
||||
words = ARGF
|
||||
passwords_size = 10
|
||||
options[:starting_at] = 0
|
||||
else
|
||||
words = File.open(wordlist)
|
||||
passwords_size = count_file_lines(wordlist)+1
|
||||
end
|
||||
|
||||
create_progress_bar(passwords_size, options)
|
||||
|
||||
words.each do |password|
|
||||
password.chomp!
|
||||
|
||||
# A successfull login will redirect us to the redirect_to parameter
|
||||
# Generate a random one on each request
|
||||
unless redirect_url
|
||||
random = (0...8).map { 65.+(rand(26)).chr }.join
|
||||
redirect_url = "#{@uri}#{random}/"
|
||||
end
|
||||
|
||||
request = login_request(password, redirect_url)
|
||||
|
||||
request.on_complete do |response|
|
||||
if options[:show_progression] && !found
|
||||
progress_bar.progress += 1
|
||||
percentage = progress_bar.progress.fdiv(progress_bar.total)
|
||||
if options[:starting_at] && percentage >= 0.8
|
||||
progress_bar.total *= 2
|
||||
end
|
||||
end
|
||||
|
||||
progress_bar.log(" Trying Username: #{login} Password: #{password}") if options[:verbose]
|
||||
|
||||
if valid_password?(response, password, redirect_url, options)
|
||||
found = true
|
||||
self.password = password
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
hydra.queue(request)
|
||||
queue_count += 1
|
||||
|
||||
if queue_count >= browser.max_threads
|
||||
hydra.run
|
||||
queue_count = 0
|
||||
progress_bar.log(" Sent #{browser.max_threads} request/s ...") if options[:verbose]
|
||||
end
|
||||
end
|
||||
|
||||
# run all of the remaining requests
|
||||
hydra.run
|
||||
puts if options[:show_progression] # mandatory to avoid the output of the progressbar to be overriden
|
||||
end
|
||||
|
||||
# @param [ Integer ] passwords_size
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ ProgressBar ]
|
||||
# :nocov:
|
||||
def create_progress_bar(passwords_size, options)
|
||||
if options[:show_progression]
|
||||
@progress_bar = ProgressBar.create(
|
||||
format: '%t %a <%B> (%c / %C) %P%% %e',
|
||||
title: " Brute Forcing '#{login}'",
|
||||
total: passwords_size,
|
||||
starting_at: options[:starting_at]
|
||||
)
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# @param [ String ] password
|
||||
# @param [ String ] redirect_url
|
||||
#
|
||||
# @return [ Typhoeus::Request ]
|
||||
def login_request(password, redirect_url)
|
||||
Browser.instance.forge_request(login_url,
|
||||
method: :post,
|
||||
body: { log: login, pwd: password, redirect_to: redirect_url },
|
||||
cache_ttl: 0
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] password
|
||||
# @param [ String ] redirect_url
|
||||
# @param [ Hash ] options
|
||||
# @option options [ Boolean ] :verbose
|
||||
# @option options [ Boolean ] :show_progression
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def valid_password?(response, password, redirect_url, options = {})
|
||||
if response.code == 302 && response.headers_hash && response.headers_hash['Location'] == redirect_url
|
||||
progression = "#{info('[SUCCESS]')} Login : #{login} Password : #{password}\n\n"
|
||||
valid = true
|
||||
elsif response.body =~ /login_error/i
|
||||
verbose = "Incorrect login and/or password."
|
||||
elsif response.timed_out?
|
||||
progression = critical('ERROR: Request timed out.')
|
||||
elsif response.code == 0
|
||||
progression = critical("ERROR: No response from remote server. WAF/IPS? (#{response.return_message})")
|
||||
elsif response.code.to_s =~ /^50/
|
||||
progression = critical('ERROR: Server error, try reducing the number of threads or use the --throttle option.')
|
||||
else
|
||||
progression = critical("ERROR: We received an unknown response for login: #{login} and password: #{password}")
|
||||
verbose = critical(" Code: #{response.code}\n Body: #{response.body}\n")
|
||||
end
|
||||
|
||||
progress_bar.log(" #{progression}") if progression && options[:show_progression]
|
||||
progress_bar.log(" #{verbose}") if verbose && options[:verbose]
|
||||
|
||||
valid || false
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpUser < WpItem
|
||||
module Existable
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def exists_from_response?(response, options = {})
|
||||
load_from_response(response)
|
||||
|
||||
@login ? true : false
|
||||
end
|
||||
|
||||
# Load the login and display_name from the response
|
||||
#
|
||||
# @param [ Typhoeus::Response ] response
|
||||
#
|
||||
# @return [ void ]
|
||||
def load_from_response(response)
|
||||
if response.code == 301 # login in location?
|
||||
location = response.headers_hash['Location']
|
||||
|
||||
return if location.nil? || location.empty?
|
||||
|
||||
@login = Existable.login_from_author_pattern(location)
|
||||
@display_name = Existable.display_name_from_body(
|
||||
Browser.get(location).body
|
||||
)
|
||||
elsif response.code == 200 # login in body?
|
||||
@login = Existable.login_from_body(response.body)
|
||||
@display_name = Existable.display_name_from_body(response.body)
|
||||
end
|
||||
end
|
||||
private :load_from_response
|
||||
|
||||
# @param [ String ] text
|
||||
#
|
||||
# @return [ String ] The login
|
||||
def self.login_from_author_pattern(text)
|
||||
return unless text =~ %r{/author/([^/\b"']+)/?}i
|
||||
|
||||
Regexp.last_match[1].force_encoding('UTF-8')
|
||||
end
|
||||
|
||||
# @param [ String ] body
|
||||
#
|
||||
# @return [ String ] The login
|
||||
def self.login_from_body(body)
|
||||
# Feed URL with Permalinks
|
||||
login = WpUser::Existable.login_from_author_pattern(body)
|
||||
|
||||
unless login
|
||||
# No Permalinks
|
||||
login = body[%r{<body class="archive author author-([^\s]+)[ "]}i, 1]
|
||||
login ? login.force_encoding('UTF-8') : nil
|
||||
end
|
||||
|
||||
login
|
||||
end
|
||||
|
||||
# @note Some bodies are encoded in ASCII-8BIT, and Nokogiri doesn't support it
|
||||
# So it's forced to UTF-8 when this encoding is detected
|
||||
#
|
||||
# @param [ String ] body
|
||||
#
|
||||
# @return [ String ] The display_name
|
||||
def self.display_name_from_body(body)
|
||||
if title_tag = body[%r{<title>([^<]+)</title>}i, 1]
|
||||
title_tag.force_encoding('UTF-8') if title_tag.encoding == Encoding::ASCII_8BIT
|
||||
title_tag = Nokogiri::HTML::DocumentFragment.parse(title_tag).to_s
|
||||
# & are not decoded with Nokogiri
|
||||
title_tag.gsub!('&', '&')
|
||||
|
||||
# replace UTF chars like » with dummy character
|
||||
title_tag.gsub!(/&#(\d+);/, '|')
|
||||
|
||||
name = title_tag[%r{([^|«»]+) }, 1]
|
||||
|
||||
return name.strip if name
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'wp_version/findable'
|
||||
require 'wp_version/output'
|
||||
|
||||
class WpVersion < WpItem
|
||||
extend WpVersion::Findable
|
||||
include WpVersion::Output
|
||||
|
||||
# The version number
|
||||
attr_accessor :number, :metadata
|
||||
alias_method :version, :number # Needed to have the right behaviour in Vulnerable#vulnerable_to?
|
||||
|
||||
# @return [ Array ]
|
||||
def allowed_options; super << :number << :found_from end
|
||||
|
||||
def identifier
|
||||
@identifier ||= number
|
||||
end
|
||||
|
||||
def db_file
|
||||
@db_file ||= WORDPRESSES_FILE
|
||||
end
|
||||
|
||||
# @param [ WpVersion ] other
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def ==(other)
|
||||
number == other.number
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] All the stable versions from version_file
|
||||
def self.all(versions_file = WP_VERSIONS_FILE)
|
||||
Nokogiri.XML(File.open(versions_file)).css('version').reduce([]) do |a, node|
|
||||
a << node.text.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ Hash ] Metadata for specific WP version from WORDPRESSES_FILE
|
||||
def metadata(version)
|
||||
json = json(db_file)
|
||||
|
||||
metadata = {}
|
||||
temp = json[version]
|
||||
if !temp.nil?
|
||||
metadata[:release_date] = temp['release_date']
|
||||
metadata[:changelog_url] = temp['changelog_url']
|
||||
end
|
||||
metadata
|
||||
end
|
||||
end
|
||||
@@ -1,240 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpVersion < WpItem
|
||||
|
||||
module Findable
|
||||
|
||||
# Find the version of the blog designated from target_uri
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
# @param [ String ] wp_content_dir
|
||||
# @param [ String ] wp_plugins_dir
|
||||
#
|
||||
# @return [ WpVersion ]
|
||||
def find(target_uri, wp_content_dir, wp_plugins_dir, versions_xml)
|
||||
versions = {}
|
||||
methods.grep(/^find_from_/).each do |method|
|
||||
|
||||
if method === :find_from_advanced_fingerprinting
|
||||
version = send(method, target_uri, wp_content_dir, wp_plugins_dir, versions_xml)
|
||||
else
|
||||
version = send(method, target_uri)
|
||||
end
|
||||
|
||||
if version
|
||||
if versions.key?(version)
|
||||
versions[version] << method.to_s
|
||||
else
|
||||
versions[version] = [ method.to_s ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if versions.length > 0
|
||||
determined_version = versions.max_by { |k, v| v.length }
|
||||
if determined_version
|
||||
return new(target_uri, number: determined_version[0], found_from: determined_version[1].join(', '))
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
# Used to check if the version is correct: must contain at least one dot.
|
||||
#
|
||||
# @return [ String ]
|
||||
def version_pattern
|
||||
'([^\r\n"\',]+\.[^\r\n"\',]+)'
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Returns the first match of <pattern> in the body of the url
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
# @param [ Regex ] pattern
|
||||
# @param [ String ] path
|
||||
#
|
||||
# @return [ String ]
|
||||
def scan_url(target_uri, pattern, path = nil)
|
||||
url = path ? target_uri.merge(path).to_s : target_uri.to_s
|
||||
response = Browser.get_and_follow_location(url)
|
||||
|
||||
response.body[pattern, 1]
|
||||
end
|
||||
|
||||
#
|
||||
# DO NOT Change the order of the following methods
|
||||
# unless you know what you are doing
|
||||
# See WpVersion.find
|
||||
#
|
||||
|
||||
# Attempts to find the wordpress version from,
|
||||
# the generator meta tag in the html source.
|
||||
#
|
||||
# The meta tag can be removed however it seems,
|
||||
# that it is reinstated on upgrade.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_meta_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{name="generator" content="wordpress #{version_pattern}.*"}i
|
||||
)
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from,
|
||||
# the generator tag in the RSS feed source.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_rss_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{<generator>http://wordpress.org/\?v=#{version_pattern}</generator>}i,
|
||||
'feed/'
|
||||
)
|
||||
end
|
||||
|
||||
# Attempts to find WordPress version from,
|
||||
# the generator tag in the RDF feed source.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_rdf_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{<admin:generatorAgent rdf:resource="http://wordpress.org/\?v=#{version_pattern}" />}i,
|
||||
'feed/rdf/'
|
||||
)
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from,
|
||||
# the generator tag in the Atom source.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_atom_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{<generator uri="http://wordpress.org/" version="#{version_pattern}">WordPress</generator>}i,
|
||||
'feed/atom/'
|
||||
)
|
||||
end
|
||||
|
||||
# Uses data/wp_versions.xml to try to identify a
|
||||
# wordpress version.
|
||||
#
|
||||
# It does this by using client side file hashing
|
||||
#
|
||||
# /!\ Warning : this method might return false positive if the file used for fingerprinting is part of a theme (they can be updated)
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
# @param [ String ] wp_content_dir
|
||||
# @param [ String ] wp_plugins_dir
|
||||
# @param [ String ] versions_xml The path to the xml containing all versions
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_advanced_fingerprinting(target_uri, wp_content_dir, wp_plugins_dir, versions_xml)
|
||||
xml = xml(versions_xml)
|
||||
|
||||
wp_item = WpItem.new(target_uri,
|
||||
wp_content_dir: wp_content_dir,
|
||||
wp_plugins_dir: wp_plugins_dir)
|
||||
|
||||
xml.xpath('//file').each do |node|
|
||||
wp_item.path = node.attribute('src').text
|
||||
|
||||
response = Browser.get(wp_item.url)
|
||||
md5sum = Digest::MD5.hexdigest(response.body)
|
||||
|
||||
node.search('hash').each do |hash|
|
||||
if hash.attribute('md5').text == md5sum
|
||||
return hash.search('version').text
|
||||
end
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from the readme.html file.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_readme(target_uri)
|
||||
version = scan_url(
|
||||
target_uri,
|
||||
%r{<br />\sversion #{version_pattern}}i,
|
||||
'readme.html'
|
||||
)
|
||||
|
||||
# Since WP >= 4.7, the Readme only contains the major version
|
||||
VersionCompare.lesser?(version, '4.7') ? version : nil
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from the sitemap.xml file.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_sitemap_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{generator="wordpress/#{version_pattern}"}i,
|
||||
'sitemap.xml'
|
||||
)
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from the p-links-opml.php file.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_links_opml(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{generator="wordpress/#{version_pattern}"}i,
|
||||
'wp-links-opml.php'
|
||||
)
|
||||
end
|
||||
|
||||
def find_from_stylesheets_numbers(target_uri)
|
||||
wp_versions = WpVersion.all
|
||||
found = {}
|
||||
pattern = /\bver=([0-9\.]+)/i
|
||||
|
||||
Nokogiri::HTML(Browser.get(target_uri.to_s).body).css('link,script').each do |tag|
|
||||
%w(href src).each do |attribute|
|
||||
attr_value = tag.attribute(attribute).to_s
|
||||
|
||||
next if attr_value.nil? || attr_value.empty?
|
||||
|
||||
begin
|
||||
uri = Addressable::URI.parse(attr_value)
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
next
|
||||
end
|
||||
|
||||
next unless uri.query && uri.query.match(pattern)
|
||||
|
||||
version = Regexp.last_match[1].to_s
|
||||
|
||||
found[version] ||= 0
|
||||
found[version] += 1
|
||||
end
|
||||
end
|
||||
|
||||
found.delete_if { |v, _| !wp_versions.include?(v) }
|
||||
|
||||
best_guess = found.sort_by(&:last).last
|
||||
# best_guess[0]: version number, [1] numbers of occurences
|
||||
best_guess && best_guess[1] > 1 ? best_guess[0] : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,31 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpVersion < WpItem
|
||||
module Output
|
||||
|
||||
def output(verbose = false)
|
||||
metadata = self.metadata(self.number)
|
||||
|
||||
puts
|
||||
if verbose
|
||||
puts info("WordPress version #{self.number} identified from #{self.found_from}")
|
||||
puts " | Released: #{metadata[:release_date]}"
|
||||
puts " | Changelog: #{metadata[:changelog_url]}"
|
||||
else
|
||||
puts info("WordPress version #{self.number} #{"(Released on #{metadata[:release_date]}) identified from #{self.found_from}" if metadata[:release_date]}")
|
||||
end
|
||||
|
||||
vulnerabilities = self.vulnerabilities
|
||||
|
||||
unless vulnerabilities.empty?
|
||||
if vulnerabilities.size == 1
|
||||
puts critical("#{vulnerabilities.size} vulnerability identified from the version number")
|
||||
else
|
||||
puts critical("#{vulnerabilities.size} vulnerabilities identified from the version number")
|
||||
end
|
||||
vulnerabilities.output
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Plugin
|
||||
|
||||
attr_reader :author, :registered_options
|
||||
|
||||
def initialize(infos = {})
|
||||
@author = infos[:author]
|
||||
end
|
||||
|
||||
def run(options = {})
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# param Array options
|
||||
def register_options(*options)
|
||||
options.each do |option|
|
||||
unless option.is_a?(Array)
|
||||
raise "Each option must be an array, #{option.class} supplied"
|
||||
end
|
||||
end
|
||||
@registered_options = options
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Plugins < Array
|
||||
|
||||
attr_reader :option_parser
|
||||
|
||||
def initialize(option_parser = nil)
|
||||
if option_parser
|
||||
if option_parser.is_a?(CustomOptionParser)
|
||||
@option_parser = option_parser
|
||||
else
|
||||
raise "The parser must be an instance of CustomOptionParser, #{option_parser.class} supplied"
|
||||
end
|
||||
else
|
||||
@option_parser = CustomOptionParser.new
|
||||
end
|
||||
end
|
||||
|
||||
# param Array(Plugin) plugins
|
||||
def register(*plugins)
|
||||
plugins.each do |plugin|
|
||||
register_plugin(plugin)
|
||||
end
|
||||
end
|
||||
|
||||
# param Plugin plugin
|
||||
def register_plugin(plugin)
|
||||
if plugin.is_a?(Plugin)
|
||||
self << plugin
|
||||
|
||||
# A plugin may not have options
|
||||
if plugin_options = plugin.registered_options
|
||||
@option_parser.add(plugin_options)
|
||||
end
|
||||
else
|
||||
raise "The argument must be an instance of Plugin, #{plugin.class} supplied"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/cache_file_store'
|
||||
|
||||
class TyphoeusCache < CacheFileStore
|
||||
|
||||
def get(request)
|
||||
read_entry(request.hash.to_s)
|
||||
end
|
||||
|
||||
def set(request, response)
|
||||
write_entry(request.hash.to_s, response, request.cache_ttl)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class VersionCompare
|
||||
|
||||
# Compares two version strings. Returns true if version1 <= version2
|
||||
# and false otherwise
|
||||
#
|
||||
# @param [ String ] version1
|
||||
# @param [ String ] version2
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def self.lesser_or_equal?(version1, version2)
|
||||
# Prepend a '0' if the version starts with a '.'
|
||||
version1 = prepend_zero(version1)
|
||||
version2 = prepend_zero(version2)
|
||||
|
||||
return true if (version1 == version2)
|
||||
# Both versions must be set
|
||||
return false unless (version1 and version2)
|
||||
return false if (version1.empty? or version2.empty?)
|
||||
begin
|
||||
return true if (Gem::Version.new(version1) < Gem::Version.new(version2))
|
||||
rescue ArgumentError => e
|
||||
# Example: ArgumentError: Malformed version number string a
|
||||
return false if e.message =~ /Malformed version number string/
|
||||
raise
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
# Compares two version strings. Returns true if version1 < version2
|
||||
# and false otherwise
|
||||
#
|
||||
# @param [ String ] version1
|
||||
# @param [ String ] version2
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def self.lesser?(version1, version2)
|
||||
# Prepend a '0' if the version starts with a '.'
|
||||
version1 = prepend_zero(version1)
|
||||
version2 = prepend_zero(version2)
|
||||
|
||||
return false if (version1 == version2)
|
||||
# Both versions must be set
|
||||
return false unless (version1 and version2)
|
||||
return false if (version1.empty? or version2.empty?)
|
||||
begin
|
||||
return true if (Gem::Version.new(version1) < Gem::Version.new(version2))
|
||||
rescue ArgumentError => e
|
||||
# Example: ArgumentError: Malformed version number string a
|
||||
return false if e.message =~ /Malformed version number string/
|
||||
raise
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def self.prepend_zero(version)
|
||||
return nil if version.nil?
|
||||
version[0,1] == '.' ? "0#{version}" : version
|
||||
end
|
||||
end
|
||||
@@ -1,61 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'rubygems'
|
||||
|
||||
version = RUBY_VERSION.dup
|
||||
|
||||
if Gem::Version.create(version) < Gem::Version.create(MIN_RUBY_VERSION)
|
||||
puts "Ruby >= #{MIN_RUBY_VERSION} required to run wpscan (You have #{version})"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
# Fix for issue #245 "invalid byte sequence in US-ASCII"
|
||||
Encoding.default_external = Encoding::UTF_8
|
||||
|
||||
begin
|
||||
# Standard libs
|
||||
require 'readline'
|
||||
require 'bundler/setup' unless kali_linux?
|
||||
require 'getoptlong'
|
||||
require 'optparse' # Will replace getoptlong
|
||||
require 'uri'
|
||||
require 'time'
|
||||
require 'resolv'
|
||||
require 'digest/md5'
|
||||
require 'digest/sha1'
|
||||
require 'base64'
|
||||
require 'rbconfig'
|
||||
require 'pp'
|
||||
require 'shellwords'
|
||||
require 'fileutils'
|
||||
require 'pathname'
|
||||
require 'cgi'
|
||||
# Third party libs
|
||||
require 'typhoeus'
|
||||
require 'yajl/json_gem'
|
||||
require 'nokogiri'
|
||||
require 'terminal-table'
|
||||
require 'ruby-progressbar'
|
||||
require 'addressable/uri'
|
||||
# Custom libs
|
||||
require 'common/browser'
|
||||
require 'common/custom_option_parser'
|
||||
rescue LoadError => e
|
||||
puts "[ERROR] #{e}"
|
||||
|
||||
missing_gem = e.to_s[%r{ -- ([^/]+)/?\z}, 1]
|
||||
if missing_gem
|
||||
if missing_gem =~ /nokogiri/i
|
||||
puts
|
||||
puts 'Nokogiri needs some packets, please run \'sudo apt-get install libxml2 libxml2-dev libxslt1-dev\' to install them. Then run the command below'
|
||||
puts
|
||||
end
|
||||
puts "[TIP] Try to run 'gem install #{missing_gem}' or 'gem install --user-install #{missing_gem}'. If you still get an error, Please see README file or https://github.com/wpscanteam/wpscan"
|
||||
end
|
||||
exit(1)
|
||||
end
|
||||
|
||||
if Typhoeus::VERSION == '0.4.0'
|
||||
puts 'Typhoeus 0.4.0 detected, please update the gem otherwise wpscan will not work correctly'
|
||||
exit(1)
|
||||
end
|
||||
@@ -1,143 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'web_site/humans_txt'
|
||||
require 'web_site/interesting_headers'
|
||||
require 'web_site/robots_txt'
|
||||
require 'web_site/security_txt'
|
||||
require 'web_site/sitemap'
|
||||
require 'web_site/sql_file_export'
|
||||
|
||||
class WebSite
|
||||
include WebSite::HumansTxt
|
||||
include WebSite::InterestingHeaders
|
||||
include WebSite::RobotsTxt
|
||||
include WebSite::SecurityTxt
|
||||
include WebSite::Sitemap
|
||||
include WebSite::SqlFileExport
|
||||
|
||||
attr_reader :uri
|
||||
|
||||
def initialize(site_url)
|
||||
self.url = site_url
|
||||
end
|
||||
|
||||
def url=(url)
|
||||
@uri = URI.parse(add_trailing_slash(add_http_protocol(url)))
|
||||
end
|
||||
|
||||
def url
|
||||
@uri.to_s
|
||||
end
|
||||
|
||||
# Checks if the remote website has ssl errors
|
||||
def ssl_error?
|
||||
return false unless @uri.scheme == 'https'
|
||||
c = get_root_path_return_code
|
||||
# http://www.rubydoc.info/github/typhoeus/ethon/Ethon/Easy:return_code
|
||||
return (
|
||||
c == :ssl_connect_error ||
|
||||
c == :peer_failed_verification ||
|
||||
c == :ssl_certproblem ||
|
||||
c == :ssl_cipher ||
|
||||
c == :ssl_cacert ||
|
||||
c == :ssl_cacert_badfile ||
|
||||
c == :ssl_issuer_error ||
|
||||
c == :ssl_crl_badfile ||
|
||||
c == :ssl_engine_setfailed ||
|
||||
c == :ssl_engine_notfound
|
||||
)
|
||||
end
|
||||
|
||||
def get_root_path_return_code
|
||||
Browser.get(@uri.to_s).return_code
|
||||
end
|
||||
|
||||
# Checks if the remote website is up.
|
||||
def online?
|
||||
Browser.get(@uri.to_s).code != 0
|
||||
end
|
||||
|
||||
def has_basic_auth?
|
||||
Browser.get(@uri.to_s).code == 401
|
||||
end
|
||||
|
||||
def has_xml_rpc?
|
||||
response = Browser.get_and_follow_location(xml_rpc_url)
|
||||
response.body =~ %r{XML-RPC server accepts POST requests only}i
|
||||
end
|
||||
|
||||
# See http://www.hixie.ch/specs/pingback/pingback-1.0#TOC2.3
|
||||
def xml_rpc_url
|
||||
unless @xmlrpc_url
|
||||
@xmlrpc_url = @uri.merge('xmlrpc.php').to_s
|
||||
end
|
||||
|
||||
@xmlrpc_url
|
||||
end
|
||||
|
||||
# See if the remote url returns 30x redirect
|
||||
# This method is recursive
|
||||
# Return a string with the redirection or nil
|
||||
def redirection(url = nil)
|
||||
redirection = nil
|
||||
url ||= @uri.to_s
|
||||
response = Browser.get(url)
|
||||
|
||||
redirected_uri = URI.parse(add_trailing_slash(add_http_protocol(url)))
|
||||
if response.code == 301 || response.code == 302
|
||||
redirection = redirected_uri.merge(response.headers_hash['location']).to_s
|
||||
|
||||
return redirection if url == redirection # prevents infinite loop
|
||||
|
||||
# Let's check if there is a redirection in the redirection
|
||||
if other_redirection = redirection(redirection)
|
||||
redirection = other_redirection
|
||||
end
|
||||
end
|
||||
|
||||
redirection
|
||||
end
|
||||
|
||||
# Compute the MD5 of the page
|
||||
# Comments and scripts are deleted from the page to avoid cache generation details
|
||||
#
|
||||
# @param [ String, Typhoeus::Response ] page The url of the response of the page
|
||||
#
|
||||
# @return [ String ] The MD5 hash of the page
|
||||
def self.page_hash(page)
|
||||
page = Browser.get(page, { followlocation: true, cache_ttl: 0 }) unless page.is_a?(Typhoeus::Response)
|
||||
# remove comments
|
||||
page = page.body.gsub(/<!--.*?-->/m, '')
|
||||
# remove javascript stuff
|
||||
page = page.gsub(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/m, '')
|
||||
Digest::MD5.hexdigest(page)
|
||||
end
|
||||
|
||||
def homepage_hash
|
||||
unless @homepage_hash
|
||||
@homepage_hash = WebSite.page_hash(@uri.to_s)
|
||||
end
|
||||
@homepage_hash
|
||||
end
|
||||
|
||||
# Return the MD5 hash of a 404 page
|
||||
def error_404_hash
|
||||
unless @error_404_hash
|
||||
non_existant_page = Digest::MD5.hexdigest(rand(999_999_999).to_s) + '.html'
|
||||
@error_404_hash = WebSite.page_hash(@uri.merge(non_existant_page).to_s)
|
||||
end
|
||||
@error_404_hash
|
||||
end
|
||||
|
||||
# Only the first 700 bytes are checked to avoid the download
|
||||
# of the whole file which can be very huge (like 2 Go)
|
||||
#
|
||||
# @param [ String ] log_url
|
||||
# @param [ RegEx ] pattern
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def self.has_log?(log_url, pattern)
|
||||
log_body = Browser.get(log_url, headers: {'range' => 'bytes=0-700'}).body
|
||||
log_body[pattern] ? true : false
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module HumansTxt
|
||||
|
||||
# Gets the humans.txt URL
|
||||
# @return [ String ]
|
||||
def humans_url
|
||||
@uri.clone.merge('humans.txt').to_s
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module InterestingHeaders
|
||||
|
||||
# Checks for interesting headers
|
||||
# @return [ Array ] Interesting Headers
|
||||
def interesting_headers
|
||||
response = Browser.head(@uri.to_s)
|
||||
headers = response.headers
|
||||
# Header Names are case insensitve so convert them to upcase
|
||||
headers_uppercase = headers.inject({}) do |hash, keys|
|
||||
hash[keys[0].upcase] = keys[1]
|
||||
hash
|
||||
end
|
||||
InterestingHeaders.known_headers.each do |h|
|
||||
headers_uppercase.delete(h.upcase)
|
||||
end
|
||||
headers_uppercase.to_a.compact.sort
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @return [ Array ]
|
||||
def self.known_headers
|
||||
%w{
|
||||
Location
|
||||
Date
|
||||
Content-Type
|
||||
Content-Length
|
||||
Connection
|
||||
Etag
|
||||
Expires
|
||||
Last-Modified
|
||||
Pragma
|
||||
Vary
|
||||
Cache-Control
|
||||
X-Pingback
|
||||
Accept-Ranges
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module RobotsTxt
|
||||
|
||||
# Checks if a robots.txt file exists
|
||||
# @return [ Boolean ]
|
||||
def has_robots?
|
||||
Browser.get(robots_url).code == 200
|
||||
end
|
||||
|
||||
# Gets a robots.txt URL
|
||||
# @return [ String ]
|
||||
def robots_url
|
||||
@uri.clone.merge('robots.txt').to_s
|
||||
end
|
||||
|
||||
# Parse robots.txt
|
||||
# @return [ Array ] URLs generated from robots.txt
|
||||
def parse_robots_txt
|
||||
return_object = []
|
||||
|
||||
# Make request
|
||||
response = Browser.get(robots_url.to_s)
|
||||
body = response.body
|
||||
|
||||
# Get all allow and disallow urls
|
||||
entries = body.scan(/^(?:dis)?allow:\s*(.*)$/i)
|
||||
|
||||
# Did we get something?
|
||||
if entries
|
||||
# Remove any rubbish
|
||||
entries = clean_uri(entries)
|
||||
|
||||
# Sort
|
||||
entries.sort!
|
||||
|
||||
# Wordpress URL
|
||||
wordpress_path = @uri.path
|
||||
|
||||
# Each "boring" value as defined below, remove
|
||||
RobotsTxt.known_dirs.each do |d|
|
||||
entries.delete(d)
|
||||
# Also delete when wordpress is installed in subdir
|
||||
dir_with_subdir = "#{wordpress_path}/#{d}".gsub(/\/+/, '/')
|
||||
entries.delete(dir_with_subdir)
|
||||
end
|
||||
|
||||
# Convert to full URIs
|
||||
return_object = full_uri(entries)
|
||||
end
|
||||
return return_object
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Useful ~ "function do_robots()" -> https://github.com/WordPress/WordPress/blob/master/wp-includes/functions.php
|
||||
#
|
||||
# @return [ Array ]
|
||||
def self.known_dirs
|
||||
%w{
|
||||
/
|
||||
/wp-admin/
|
||||
/wp-admin/admin-ajax.php
|
||||
/wp-includes/
|
||||
/wp-content/
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module SecurityTxt
|
||||
|
||||
# Gets the security.txt URL
|
||||
# @return [ String ]
|
||||
def security_url
|
||||
@uri.clone.merge('.well-known/security.txt').to_s
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module Sitemap
|
||||
|
||||
# Checks if a sitemap.txt file exists
|
||||
# @return [ Boolean ]
|
||||
def has_sitemap?
|
||||
# Make the request
|
||||
response = Browser.get(sitemap_url)
|
||||
|
||||
# Make sure its HTTP 200
|
||||
return false unless response.code == 200
|
||||
|
||||
# Is there a sitemap value?
|
||||
result = response.body.scan(/^sitemap\s*:\s*(.*)$/i)
|
||||
return true if result[0]
|
||||
return false
|
||||
end
|
||||
|
||||
# Get the robots.txt URL
|
||||
# @return [ String ]
|
||||
def sitemap_url
|
||||
@uri.clone.merge('robots.txt').to_s
|
||||
end
|
||||
|
||||
# Parse robots.txt
|
||||
# @return [ Array ] URLs generated from robots.txt
|
||||
def parse_sitemap
|
||||
return_object = []
|
||||
|
||||
# Make request
|
||||
response = Browser.get(sitemap_url.to_s)
|
||||
|
||||
# Get all allow and disallow urls
|
||||
entries = response.body.scan(/^sitemap\s*:\s*(.*)$/i)
|
||||
|
||||
# Did we get something?
|
||||
if entries
|
||||
# Remove any rubbish
|
||||
entries = clean_uri(entries)
|
||||
|
||||
# Sort
|
||||
entries.sort!
|
||||
|
||||
# Convert to full URIs
|
||||
return_object = full_uri(entries)
|
||||
end
|
||||
return return_object
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,35 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module SqlFileExport
|
||||
|
||||
# Checks if a .sql file exists
|
||||
# @return [ Array ]
|
||||
def sql_file_export
|
||||
export_files = []
|
||||
|
||||
self.sql_file_export_urls.each do |url|
|
||||
response = Browser.get(url)
|
||||
export_files << url if response.code == 200 && response.body =~ /INSERT INTO/
|
||||
end
|
||||
|
||||
export_files
|
||||
end
|
||||
|
||||
# Gets a .sql export file URL
|
||||
# @return [ Array ]
|
||||
def sql_file_export_urls
|
||||
urls = []
|
||||
host = @uri.host[/(^[\w|-]+)/,1]
|
||||
|
||||
files = ["#{host}.sql", "#{host}.sql.gz", "#{host}.zip", 'db.sql', 'site.sql', 'database.sql',
|
||||
'data.sql', 'backup.sql', 'dump.sql', 'db_backup.sql', 'dbdump.sql', 'wordpress.sql', 'mysql.sql']
|
||||
|
||||
files.each do |file|
|
||||
urls << @uri.clone.merge(file).to_s
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,184 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'web_site'
|
||||
require 'wp_target/wp_api'
|
||||
require 'wp_target/wp_config_backup'
|
||||
require 'wp_target/wp_custom_directories'
|
||||
require 'wp_target/wp_full_path_disclosure'
|
||||
require 'wp_target/wp_login_protection'
|
||||
require 'wp_target/wp_must_use_plugins'
|
||||
require 'wp_target/wp_readme'
|
||||
require 'wp_target/wp_registrable'
|
||||
require 'wp_target/wp_rss'
|
||||
|
||||
class WpTarget < WebSite
|
||||
include WpTarget::WpAPI
|
||||
include WpTarget::WpConfigBackup
|
||||
include WpTarget::WpCustomDirectories
|
||||
include WpTarget::WpFullPathDisclosure
|
||||
include WpTarget::WpLoginProtection
|
||||
include WpTarget::WpMustUsePlugins
|
||||
include WpTarget::WpReadme
|
||||
include WpTarget::WpRegistrable
|
||||
include WpTarget::WpRSS
|
||||
|
||||
attr_reader :verbose
|
||||
|
||||
def initialize(target_url, options = {})
|
||||
raise Exception.new('target_url can not be nil or empty') if target_url.nil? || target_url == ''
|
||||
super(target_url)
|
||||
|
||||
@verbose = options[:verbose]
|
||||
@wp_content_dir = options[:wp_content_dir]
|
||||
@wp_plugins_dir = options[:wp_plugins_dir]
|
||||
@multisite = nil
|
||||
@vhost = options[:vhost]
|
||||
|
||||
Browser.instance.referer = url
|
||||
if @vhost
|
||||
Browser.instance.vhost = @vhost
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# check if the target website is
|
||||
# actually running wordpress.
|
||||
def wordpress?
|
||||
wordpress = false
|
||||
|
||||
response = Browser.get_and_follow_location(@uri.to_s)
|
||||
|
||||
# Note: in the future major WPScan version, change the user-agent to see
|
||||
# if the response is a 200 ?
|
||||
fail "The target is responding with a 403, this might be due to a WAF or a plugin.\n" \
|
||||
'You should try to supply a valid user-agent via the --user-agent option or use the --random-agent option' if response.code == 403
|
||||
|
||||
dir = wp_content_dir ? wp_content_dir : 'wp-content'
|
||||
|
||||
if response.body =~ /["'][^"']*\/#{Regexp.escape(dir)}\/[^"']*["']/i
|
||||
wordpress = true
|
||||
else
|
||||
|
||||
if has_xml_rpc?
|
||||
wordpress = true
|
||||
else
|
||||
response = Browser.get_and_follow_location(login_url)
|
||||
|
||||
if response.code == 200 && response.body =~ %r{WordPress}i
|
||||
wordpress = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
wordpress
|
||||
end
|
||||
|
||||
def wordpress_hosted?
|
||||
@uri.to_s =~ /\.wordpress\.com/i
|
||||
end
|
||||
|
||||
def login_url
|
||||
url = @uri.merge('wp-login.php').to_s
|
||||
|
||||
# Let's check if the login url is redirected (to https url for example)
|
||||
redirection = redirection(url)
|
||||
url = redirection if redirection
|
||||
|
||||
url
|
||||
end
|
||||
|
||||
# Valid HTTP return codes
|
||||
def self.valid_response_codes
|
||||
[200, 301, 302, 401, 403, 500, 400]
|
||||
end
|
||||
|
||||
# @return [ WpTheme ]
|
||||
# :nocov:
|
||||
def theme
|
||||
WpTheme.find(@uri)
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# @param [ String ] versions_xml
|
||||
#
|
||||
# @return [ WpVersion ]
|
||||
# :nocov:
|
||||
def version(versions_xml)
|
||||
WpVersion.find(@uri, wp_content_dir, wp_plugins_dir, versions_xml)
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# The version is not yet considered
|
||||
#
|
||||
# @param [ String ] name
|
||||
# @param [ String ] version
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_plugin?(name, version = nil)
|
||||
WpPlugin.new(
|
||||
@uri,
|
||||
name: name,
|
||||
version: version,
|
||||
wp_content_dir: wp_content_dir,
|
||||
wp_plugins_dir: wp_plugins_dir
|
||||
).exists?
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_debug_log?
|
||||
WebSite.has_log?(debug_log_url, %r{\[[^\]]+\] PHP (?:Warning|Error|Notice):})
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def debug_log_url
|
||||
@uri.merge("#{wp_content_dir}/debug.log").to_s
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def upload_dir_url
|
||||
@uri.merge("#{wp_content_dir}/uploads/").to_s
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def includes_dir_url
|
||||
@uri.merge("wp-includes/").to_s
|
||||
end
|
||||
|
||||
# Script for replacing strings in wordpress databases
|
||||
# reveals database credentials after hitting submit
|
||||
# http://interconnectit.com/124/search-and-replace-for-wordpress-databases/
|
||||
#
|
||||
# @return [ String ]
|
||||
def search_replace_db_2_url
|
||||
@uri.merge('searchreplacedb2.php').to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def search_replace_db_2_exists?
|
||||
resp = Browser.get(search_replace_db_2_url)
|
||||
resp.code == 200 && resp.body[%r{by interconnect}i]
|
||||
end
|
||||
|
||||
# Script used to recover locked out admin users
|
||||
# http://yoast.com/emergency-wordpress-access/
|
||||
# https://codex.wordpress.org/User:MichaelH/Orphaned_Plugins_needing_Adoption/Emergency
|
||||
#
|
||||
# @return [ String ]
|
||||
def emergency_url
|
||||
@uri.merge('emergency.php').to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def emergency_exists?
|
||||
resp = Browser.get(emergency_url)
|
||||
resp.code == 200 && resp.body[%r{password}i]
|
||||
end
|
||||
|
||||
def upload_directory_listing_enabled?
|
||||
directory_listing_enabled?(upload_dir_url)
|
||||
end
|
||||
|
||||
def include_directory_listing_enabled?
|
||||
directory_listing_enabled?(includes_dir_url)
|
||||
end
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpAPI
|
||||
|
||||
# Checks to see if the REST API is enabled
|
||||
#
|
||||
# This by default in a WordPress installation since 4.5+
|
||||
# @return [ Boolean ]
|
||||
def has_api?(url)
|
||||
# Make the request
|
||||
response = Browser.get(url)
|
||||
|
||||
# Able to view the output?
|
||||
if valid_json?(response.body) && response.body != ''
|
||||
# Read in JSON
|
||||
data = JSON.parse(response.body)
|
||||
|
||||
# If there is nothing there, return false
|
||||
if data.empty?
|
||||
return false
|
||||
# WAF/API disabled response
|
||||
elsif data.include?('message') and data['message'] =~ /Only authenticated users can access the REST API/
|
||||
return false
|
||||
# Success!
|
||||
elsif response.code == 200
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
# Something went wrong
|
||||
return false
|
||||
end
|
||||
|
||||
# @return [ String ] The API/JSON URL
|
||||
def json_url
|
||||
@uri.merge('/wp-json/').to_s
|
||||
end
|
||||
|
||||
# @return [ String ] The API/JSON URL to show users
|
||||
def json_users_url
|
||||
@uri.merge('/wp-json/wp/v2/users').to_s
|
||||
end
|
||||
|
||||
# @return [ String ] The API/JSON URL to show users
|
||||
def json_get_users(url)
|
||||
# Variables
|
||||
users = []
|
||||
|
||||
# Make the request
|
||||
response = Browser.get(url)
|
||||
|
||||
# If not HTTP 200, return false
|
||||
return false unless response.code == 200
|
||||
|
||||
# Able to view the output?
|
||||
return false unless valid_json?(response.body)
|
||||
|
||||
# Read in JSON
|
||||
data = JSON.parse(response.body)
|
||||
|
||||
# If there is nothing there, return false
|
||||
return false if data.empty?
|
||||
|
||||
# Add to array
|
||||
data.each do |child|
|
||||
row = [ child['id'], child['name'], child['link'] ]
|
||||
users << row
|
||||
end
|
||||
|
||||
# Sort and uniq
|
||||
users = users.sort.uniq
|
||||
|
||||
if users and users.size >= 1
|
||||
# Feedback
|
||||
grammar = grammar_s(users.size)
|
||||
puts warning("#{users.size} user#{grammar} exposed via API: #{json_users_url}")
|
||||
|
||||
# Print results
|
||||
table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'],
|
||||
rows: users)
|
||||
puts table
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpConfigBackup
|
||||
|
||||
# Checks to see if wp-config.php has a backup
|
||||
# See http://www.feross.org/cmsploit/
|
||||
# @return [ Array ] Backup config files
|
||||
def config_backup
|
||||
found = []
|
||||
backups = WpConfigBackup.config_backup_files
|
||||
browser = Browser.instance
|
||||
hydra = browser.hydra
|
||||
queue_count = 0
|
||||
|
||||
backups.each do |file|
|
||||
file_url = @uri.merge(url_encode(file)).to_s
|
||||
request = browser.forge_request(file_url)
|
||||
|
||||
request.on_complete do |response|
|
||||
if response.body[%r{define}i] and not response.body[%r{<\s?html}i]
|
||||
found << file_url
|
||||
end
|
||||
end
|
||||
|
||||
hydra.queue(request)
|
||||
queue_count += 1
|
||||
|
||||
if queue_count == browser.max_threads
|
||||
hydra.run
|
||||
queue_count = 0
|
||||
end
|
||||
end
|
||||
|
||||
hydra.run
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ Array ]
|
||||
def self.config_backup_files
|
||||
%w{
|
||||
wp-config.php~ #wp-config.php# wp-config.php.save .wp-config.php.swp wp-config.php.swp wp-config.php.swo
|
||||
wp-config.php_bak wp-config.bak wp-config.php.bak wp-config.save wp-config.old wp-config.php.old
|
||||
wp-config.php.orig wp-config.orig wp-config.php.original wp-config.original wp-config.txt
|
||||
} # thanks to Feross.org for these
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,49 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpCustomDirectories
|
||||
|
||||
# @return [ String ] The wp-content directory
|
||||
def wp_content_dir
|
||||
unless @wp_content_dir
|
||||
index_body = Browser.get(@uri.to_s).body
|
||||
uri_path = @uri.path # Only use the path because domain can be text or an IP
|
||||
|
||||
if index_body[/\/wp-content\/(?:themes|plugins)\//i] || default_wp_content_dir_exists?
|
||||
@wp_content_dir = 'wp-content'
|
||||
else
|
||||
domains_excluded = '(?:www\.)?(facebook|twitter)\.com'
|
||||
@wp_content_dir = index_body[/(?:href|src)\s*=\s*(?:"|').+#{Regexp.escape(uri_path)}((?!#{domains_excluded})[^"']+)\/(?:themes|plugins)\/.*(?:"|')/i, 1]
|
||||
end
|
||||
end
|
||||
|
||||
@wp_content_dir
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def default_wp_content_dir_exists?
|
||||
response = Browser.get(@uri.merge('wp-content').to_s)
|
||||
|
||||
if WpTarget.valid_response_codes.include?(response.code)
|
||||
hash = WebSite.page_hash(response)
|
||||
return true if hash != error_404_hash and hash != homepage_hash
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# @return [ String ] The wp-plugins directory
|
||||
def wp_plugins_dir
|
||||
unless @wp_plugins_dir
|
||||
@wp_plugins_dir = "#{wp_content_dir}/plugins"
|
||||
end
|
||||
@wp_plugins_dir
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def wp_plugins_dir_exists?
|
||||
Browser.get(@uri.merge(wp_plugins_dir).to_s).code != 404
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpFullPathDisclosure
|
||||
# Check for Full Path Disclosure (FPD)
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_full_path_disclosure?
|
||||
Browser.get(full_path_disclosure_url).body[%r/Fatal error/i] ? true : false
|
||||
end
|
||||
|
||||
def full_path_disclosure_data
|
||||
return nil unless has_full_path_disclosure?
|
||||
Browser.get(full_path_disclosure_url).body[/Fatal error:.+? in (.+?) on/i, 1]
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def full_path_disclosure_url
|
||||
@uri.merge('wp-includes/rss-functions.php').to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,110 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpLoginProtection
|
||||
|
||||
LOGIN_PROTECTION_METHOD_PATTERN = /^has_(.*)_protection\?/i
|
||||
# Used as cache
|
||||
@login_protection_plugin = nil
|
||||
|
||||
def has_login_protection?
|
||||
!login_protection_plugin.nil?
|
||||
end
|
||||
|
||||
# Checks if a login protection plugin is enabled
|
||||
# return a WpPlugin object or nil if no one is found
|
||||
def login_protection_plugin
|
||||
unless @login_protection_plugin
|
||||
protected_methods.grep(LOGIN_PROTECTION_METHOD_PATTERN).each do |symbol_to_call|
|
||||
|
||||
if send(symbol_to_call)
|
||||
plugin_name = symbol_to_call[LOGIN_PROTECTION_METHOD_PATTERN, 1].gsub('_', '-')
|
||||
|
||||
return @login_protection_plugin = WpPlugin.new(
|
||||
@uri,
|
||||
name: plugin_name,
|
||||
wp_content_dir: wp_content_dir,
|
||||
wp_plugins_dir: wp_plugins_dir
|
||||
)
|
||||
end
|
||||
end
|
||||
@login_protection_plugin = nil
|
||||
end
|
||||
@login_protection_plugin
|
||||
end
|
||||
|
||||
protected
|
||||
# Thanks to Alip Aswalid for providing this method.
|
||||
# http://wordpress.org/extend/plugins/login-lockdown/
|
||||
def has_login_lockdown_protection?
|
||||
Browser.get(login_url).body =~ %r{Login LockDown}i ? true : false
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/login-lock/
|
||||
def has_login_lock_protection?
|
||||
Browser.get(login_url).body =~ %r{LOGIN LOCK} ? true : false
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/better-wp-security/
|
||||
def has_better_wp_security_protection?
|
||||
Browser.get(better_wp_security_url).code != 404
|
||||
end
|
||||
|
||||
def plugin_url(plugin_name)
|
||||
WpPlugin.new(
|
||||
@uri,
|
||||
name: plugin_name,
|
||||
wp_content_dir: wp_content_dir,
|
||||
wp_plugins_dir: wp_plugins_dir
|
||||
).url
|
||||
end
|
||||
|
||||
def better_wp_security_url
|
||||
plugin_url('better-wp-security/')
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/simple-login-lockdown/
|
||||
def has_simple_login_lockdown_protection?
|
||||
Browser.get(simple_login_lockdown_url).code != 404
|
||||
end
|
||||
|
||||
def simple_login_lockdown_url
|
||||
plugin_url('simple-login-lockdown/')
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/login-security-solution/
|
||||
def has_login_security_solution_protection?
|
||||
Browser.get(login_security_solution_url).code != 404
|
||||
end
|
||||
|
||||
def login_security_solution_url
|
||||
plugin_url('login-security-solution')
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/limit-login-attempts/
|
||||
def has_limit_login_attempts_protection?
|
||||
Browser.get(limit_login_attempts_url).code != 404
|
||||
end
|
||||
|
||||
def limit_login_attempts_url
|
||||
plugin_url('limit-login-attempts')
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/bluetrait-event-viewer/
|
||||
def has_bluetrait_event_viewer_protection?
|
||||
Browser.get(bluetrait_event_viewer_url).code != 404
|
||||
end
|
||||
|
||||
def bluetrait_event_viewer_url
|
||||
plugin_url('bluetrait-event-viewer')
|
||||
end
|
||||
|
||||
# https://wordpress.org/plugins/security-protection/
|
||||
def has_security_protection_protection?
|
||||
Nokogiri::HTML(Browser.get(login_url).body).css('script').each do |node|
|
||||
return true if node['src'] =~ /security-protection.js/i
|
||||
end
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpMustUsePlugins
|
||||
# Checks to see if the must use plugin folder exists
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_must_use_plugins?
|
||||
response = Browser.get(must_use_url)
|
||||
|
||||
if response && [200, 401, 403].include?(response.code)
|
||||
hash = WebSite.page_hash(response)
|
||||
return true if hash != error_404_hash && hash != homepage_hash
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# @return [ String ] The must use plugins directory URL
|
||||
def must_use_url
|
||||
@uri.merge("#{wp_content_dir}/mu-plugins/").to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,27 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpReadme
|
||||
|
||||
# Checks to see if the readme.html file exists
|
||||
#
|
||||
# This file comes by default in a wordpress installation,
|
||||
# and if deleted is reinstated with an upgrade.
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_readme?
|
||||
response = Browser.get(readme_url)
|
||||
|
||||
unless response.code == 404
|
||||
return response.body =~ %r{wordpress}i ? true : false
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# @return [ String ] The readme URL
|
||||
def readme_url
|
||||
@uri.merge('readme.html').to_s
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpRegistrable
|
||||
|
||||
# Should check wp-login.php if registration is enabled or not
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def registration_enabled?
|
||||
resp = Browser.get(registration_url)
|
||||
# redirect only on non multi sites
|
||||
if resp.code == 302 and resp.headers_hash['location'] =~ /wp-login\.php\?registration=disabled/i
|
||||
enabled = false
|
||||
# multi site registration form
|
||||
elsif resp.code == 200 and resp.body =~ /<form id="setupform" method="post" action="[^"]*wp-signup\.php[^"]*">/i
|
||||
enabled = true
|
||||
# normal registration form
|
||||
elsif resp.code == 200 and resp.body =~ /<form name="registerform" id="registerform" action="[^"]*wp-login\.php[^"]*"/i
|
||||
enabled = true
|
||||
# registration disabled
|
||||
else
|
||||
enabled = false
|
||||
end
|
||||
enabled
|
||||
end
|
||||
|
||||
# @return [ String ] The registration URL
|
||||
def registration_url
|
||||
multisite? ? @uri.merge('wp-signup.php').to_s : @uri.merge('wp-login.php?action=register').to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def multisite?
|
||||
unless @multisite
|
||||
# when multi site, there is no redirection or a redirect to the site itself
|
||||
# otherwise redirect to wp-login.php
|
||||
resp = Browser.get(@uri.merge('wp-signup.php').to_s)
|
||||
|
||||
if resp.code == 302 and resp.headers_hash['location'] =~ /wp-login\.php\?action=register/
|
||||
@multisite = false
|
||||
elsif resp.code == 302 and resp.headers_hash['location'] =~ /wp-signup\.php/
|
||||
@multisite = true
|
||||
elsif resp.code == 200
|
||||
@multisite = true
|
||||
else
|
||||
@multisite = false
|
||||
end
|
||||
end
|
||||
@multisite
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,73 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpRSS
|
||||
|
||||
# Checks to see if there is an rss feed
|
||||
# Will try to find the rss url in the homepage
|
||||
# Only the first one found is returned
|
||||
#
|
||||
# This file comes by default in a WordPress installation
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def rss_url
|
||||
homepage_body = Browser.get(@uri.to_s).body
|
||||
# Format: <link rel="alternate" type="application/rss+xml" title=".*" href=".*" />
|
||||
homepage_body[%r{<link\s*.*\s*type=['|"]application\/rss\+xml['|"]\s*.*\stitle=".*" href=['|"]([^"]+)['|"]\s*\/?>}i, 1]
|
||||
end
|
||||
|
||||
|
||||
# Gets all the authors from the RSS feed
|
||||
#
|
||||
# @return [ string ]
|
||||
def rss_authors(url)
|
||||
# Variables
|
||||
users = []
|
||||
|
||||
# Make the request
|
||||
response = Browser.get(url, followlocation: true)
|
||||
|
||||
# Valid repose to view? HTTP 200?
|
||||
return false unless response.code == 200
|
||||
|
||||
# Get output
|
||||
data = response.body
|
||||
|
||||
# If there is nothing there, return false
|
||||
return false if data.empty?
|
||||
|
||||
begin
|
||||
# Read in RSS/XML
|
||||
xml = Nokogiri::XML(data)
|
||||
rescue
|
||||
puts critical("Missformed XML")
|
||||
return false
|
||||
end
|
||||
|
||||
begin
|
||||
# Look for <dc:creator> item
|
||||
xml.xpath('//item/dc:creator').each do |node|
|
||||
#Format: <dc:creator><![CDATA[.*]]></dc:creator>
|
||||
users << [%r{.*}i.match(node).to_s]
|
||||
end
|
||||
rescue
|
||||
puts critical("Missing Author field. Maybe non-standard WordPress RSS feed?")
|
||||
return false
|
||||
end
|
||||
|
||||
# Sort and uniq
|
||||
users = users.sort_by { |user| user.to_s.downcase }.uniq
|
||||
|
||||
if users and users.size >= 1
|
||||
# Feedback
|
||||
grammar = grammar_s(users.size)
|
||||
puts warning("Detected #{users.size} user#{grammar} from RSS feed:")
|
||||
|
||||
# Print results
|
||||
table = Terminal::Table.new(headings: ['Name'],
|
||||
rows: users)
|
||||
puts table
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,193 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require File.expand_path(File.join(__dir__, '..', 'common', 'common_helper'))
|
||||
|
||||
require_files_from_directory(WPSCAN_LIB_DIR, '**/*.rb')
|
||||
|
||||
# wpscan usage
|
||||
def usage
|
||||
script_name = $0
|
||||
puts
|
||||
puts 'Examples :'
|
||||
puts
|
||||
puts '-Further help ...'
|
||||
puts "ruby #{script_name} --help"
|
||||
puts
|
||||
puts "-Do 'non-intrusive' checks ..."
|
||||
puts "ruby #{script_name} --url www.example.com"
|
||||
puts
|
||||
puts '-Do wordlist password brute force on enumerated users using 50 threads ...'
|
||||
puts "ruby #{script_name} --url www.example.com --wordlist darkc0de.lst --threads 50"
|
||||
puts
|
||||
puts "-Do wordlist password brute force on the 'admin' username only ..."
|
||||
puts "ruby #{script_name} --url www.example.com --wordlist darkc0de.lst --username admin"
|
||||
puts
|
||||
puts '-Enumerate installed plugins ...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate p"
|
||||
puts
|
||||
puts '-Enumerate installed themes ...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate t"
|
||||
puts
|
||||
puts '-Enumerate users (from 1 - 10)...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate u"
|
||||
puts
|
||||
puts '-Enumerate users (from 1 - 20)...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate u[1-20]"
|
||||
puts
|
||||
puts '-Enumerate installed timthumbs ...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate tt"
|
||||
puts
|
||||
puts '-Use a HTTP proxy ...'
|
||||
puts "ruby #{script_name} --url www.example.com --proxy 127.0.0.1:8118"
|
||||
puts
|
||||
puts '-Use a SOCKS5 proxy ... (cURL >= v7.21.7 needed)'
|
||||
puts "ruby #{script_name} --url www.example.com --proxy socks5://127.0.0.1:9000"
|
||||
puts
|
||||
puts '-Use custom content directory ...'
|
||||
puts "ruby #{script_name} -u www.example.com --wp-content-dir custom-content"
|
||||
puts
|
||||
puts '-Use custom plugins directory ...'
|
||||
puts "ruby #{script_name} -u www.example.com --wp-plugins-dir wp-content/custom-plugins"
|
||||
puts
|
||||
puts '-Update the Database ...'
|
||||
puts "ruby #{script_name} --update"
|
||||
puts
|
||||
puts '-Debug output ...'
|
||||
puts "ruby #{script_name} --url www.example.com --debug-output 2>debug.log"
|
||||
puts
|
||||
puts 'See README for further information.'
|
||||
puts
|
||||
end
|
||||
|
||||
# command help
|
||||
def help
|
||||
puts 'Help :'
|
||||
puts
|
||||
puts 'Some values are settable in a config file, see the example.conf.json'
|
||||
puts
|
||||
puts '--update Update the database to the latest version.'
|
||||
puts '--url | -u <target url> The WordPress URL/domain to scan.'
|
||||
puts '--force | -f Forces WPScan to not check if the remote site is running WordPress.'
|
||||
puts '--enumerate | -e [option(s)] Enumeration.'
|
||||
puts ' option :'
|
||||
puts ' u usernames from id 1 to 10'
|
||||
puts ' u[10-20] usernames from id 10 to 20 (you must write [] chars)'
|
||||
puts ' p plugins'
|
||||
puts ' vp only vulnerable plugins'
|
||||
puts ' ap all plugins (can take a long time)'
|
||||
puts ' tt timthumbs'
|
||||
puts ' t themes'
|
||||
puts ' vt only vulnerable themes'
|
||||
puts ' at all themes (can take a long time)'
|
||||
puts ' Multiple values are allowed : "-e tt,p" will enumerate timthumbs and plugins'
|
||||
puts ' If no option is supplied, the default is "vt,tt,u,vp"'
|
||||
puts
|
||||
puts '--exclude-content-based "<regexp or string>"'
|
||||
puts ' Used with the enumeration option, will exclude all occurrences based on the regexp or string supplied.'
|
||||
puts ' You do not need to provide the regexp delimiters, but you must write the quotes (simple or double).'
|
||||
puts '--config-file | -c <config file> Use the specified config file, see the example.conf.json.'
|
||||
puts '--user-agent | -a <User-Agent> Use the specified User-Agent.'
|
||||
puts '--cookie <string> String to read cookies from.'
|
||||
puts '--random-agent | -r Use a random User-Agent.'
|
||||
puts '--follow-redirection If the target url has a redirection, it will be followed without asking if you wanted to do so or not'
|
||||
puts '--batch Never ask for user input, use the default behaviour.'
|
||||
puts '--no-color Do not use colors in the output.'
|
||||
puts '--log [filename] Creates a log.txt file with WPScan\'s output if no filename is supplied. Otherwise the filename is used for logging.'
|
||||
puts '--no-banner Prevents the WPScan banner from being displayed.'
|
||||
puts '--disable-accept-header Prevents WPScan sending the Accept HTTP header.'
|
||||
puts '--disable-referer Prevents setting the Referer header.'
|
||||
puts '--disable-tls-checks Disables SSL/TLS certificate verification.'
|
||||
puts '--wp-content-dir <wp content dir> WPScan try to find the content directory (ie wp-content) by scanning the index page, however you can specify it.'
|
||||
puts ' Subdirectories are allowed.'
|
||||
puts '--wp-plugins-dir <wp plugins dir> Same thing than --wp-content-dir but for the plugins directory.'
|
||||
puts ' If not supplied, WPScan will use wp-content-dir/plugins. Subdirectories are allowed'
|
||||
puts '--proxy <[protocol://]host:port> Supply a proxy. HTTP, SOCKS4 SOCKS4A and SOCKS5 are supported.'
|
||||
puts ' If no protocol is given (format host:port), HTTP will be used.'
|
||||
puts '--proxy-auth <username:password> Supply the proxy login credentials.'
|
||||
puts '--basic-auth <username:password> Set the HTTP Basic authentication.'
|
||||
puts '--wordlist | -w <wordlist> Supply a wordlist for the password brute forcer.'
|
||||
puts '--username | -U <username> Only brute force the supplied username.'
|
||||
puts '--usernames <path-to-file> Only brute force the usernames from the file.'
|
||||
puts '--cache-dir <cache-directory> Set the cache directory.'
|
||||
puts '--cache-ttl <cache-ttl> Typhoeus cache TTL.'
|
||||
puts '--request-timeout <request-timeout> Request Timeout.'
|
||||
puts '--connect-timeout <connect-timeout> Connect Timeout.'
|
||||
puts '--threads | -t <number of threads> The number of threads to use when multi-threading requests.'
|
||||
puts '--throttle <milliseconds> Milliseconds to wait before doing another web request. If used, the --threads should be set to 1.'
|
||||
puts '--help | -h This help screen.'
|
||||
puts '--verbose | -v Verbose output.'
|
||||
puts '--version Output the current version and exit.'
|
||||
puts
|
||||
end
|
||||
|
||||
|
||||
def clean_uri(entries)
|
||||
# Extract elements
|
||||
entries.flatten!
|
||||
# Remove any leading/trailing spaces
|
||||
entries.collect{|x| x.strip || x }
|
||||
# End Of Line issues
|
||||
entries.collect{|x| x.chomp! || x }
|
||||
# Remove nil's
|
||||
entries.compact
|
||||
# Unique values only
|
||||
entries.uniq!
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
# Return the full URL
|
||||
def full_uri(entries)
|
||||
return_object = []
|
||||
# Each value now, try and make it a full URL
|
||||
entries.each do |d|
|
||||
begin
|
||||
temp = @uri.clone
|
||||
temp.path = d.strip
|
||||
rescue URI::Error
|
||||
temp = d.strip
|
||||
end
|
||||
return_object << temp.to_s
|
||||
end
|
||||
|
||||
return return_object
|
||||
end
|
||||
|
||||
# Parse humans.txt
|
||||
# @return [ Array ] URLs generated from humans.txt
|
||||
def parse_txt(url)
|
||||
return_object = []
|
||||
response = Browser.get(url.to_s)
|
||||
body = response.body
|
||||
|
||||
# Get all non-comments
|
||||
entries = body.split(/\n/)
|
||||
|
||||
# Did we get something?
|
||||
if entries
|
||||
# Remove any rubbish
|
||||
entries = clean_uri(entries)
|
||||
end
|
||||
return return_object
|
||||
end
|
||||
|
||||
|
||||
# Hook to check if the target if down during the scan
|
||||
# And have the number of requests performed to display at the end of the scan
|
||||
# The target is considered down after 30 requests with status = 0
|
||||
down = 0
|
||||
@total_requests_done = 0
|
||||
|
||||
Typhoeus.on_complete do |response|
|
||||
next if response.cached?
|
||||
|
||||
down += 1 if response.code == 0
|
||||
@total_requests_done += 1
|
||||
|
||||
fail 'The target seems to be down' if down >= 30
|
||||
|
||||
next unless Browser.instance.throttle > 0
|
||||
|
||||
sleep(Browser.instance.throttle)
|
||||
end
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpscanOptions
|
||||
ACCESSOR_OPTIONS = [
|
||||
:batch,
|
||||
:enumerate_plugins,
|
||||
:enumerate_only_vulnerable_plugins,
|
||||
:enumerate_all_plugins,
|
||||
:enumerate_themes,
|
||||
:enumerate_only_vulnerable_themes,
|
||||
:enumerate_all_themes,
|
||||
:enumerate_timthumbs,
|
||||
:enumerate_usernames,
|
||||
:enumerate_usernames_range,
|
||||
:no_color,
|
||||
:log,
|
||||
:proxy,
|
||||
:proxy_auth,
|
||||
:threads,
|
||||
:url,
|
||||
:vhost,
|
||||
:wordlist,
|
||||
:force,
|
||||
:update,
|
||||
:verbose,
|
||||
:username,
|
||||
:usernames,
|
||||
:password,
|
||||
:follow_redirection,
|
||||
:wp_content_dir,
|
||||
:wp_plugins_dir,
|
||||
:help,
|
||||
:config_file,
|
||||
:cookie,
|
||||
:exclude_content_based,
|
||||
:basic_auth,
|
||||
:debug_output,
|
||||
:version,
|
||||
:user_agent,
|
||||
:random_agent,
|
||||
:cache_ttl,
|
||||
:request_timeout,
|
||||
:connect_timeout,
|
||||
:max_threads,
|
||||
:no_banner,
|
||||
:throttle,
|
||||
:disable_accept_header,
|
||||
:disable_referer,
|
||||
:cache_dir,
|
||||
:disable_tls_checks
|
||||
]
|
||||
|
||||
attr_accessor *ACCESSOR_OPTIONS
|
||||
|
||||
def initialize
|
||||
ACCESSOR_OPTIONS.each do |option|
|
||||
instance_variable_set("@#{option}", nil)
|
||||
end
|
||||
end
|
||||
|
||||
def url=(url)
|
||||
raise Exception.new('Empty URL given') if url.nil? || url == ''
|
||||
|
||||
url = Addressable::URI.parse(url).normalize.to_s unless url.ascii_only?
|
||||
|
||||
@url = URI.parse(add_http_protocol(url)).to_s
|
||||
end
|
||||
|
||||
def vhost=(vhost)
|
||||
@vhost = vhost
|
||||
end
|
||||
|
||||
def threads=(threads)
|
||||
@threads = threads.is_a?(Integer) ? threads : threads.to_i
|
||||
end
|
||||
|
||||
def wordlist=(wordlist)
|
||||
if File.exists?(wordlist) || wordlist == '-'
|
||||
@wordlist = wordlist
|
||||
else
|
||||
raise "The file #{wordlist} does not exist"
|
||||
end
|
||||
end
|
||||
|
||||
def usernames=(file)
|
||||
fail "The file #{file} does not exist" unless File.exists?(file)
|
||||
|
||||
@usernames = file
|
||||
end
|
||||
|
||||
def proxy=(proxy)
|
||||
if proxy.index(':') == nil
|
||||
raise 'Invalid proxy format. Should be host:port.'
|
||||
else
|
||||
@proxy = proxy
|
||||
end
|
||||
end
|
||||
|
||||
def proxy_auth=(auth)
|
||||
if auth.index(':') == nil
|
||||
raise 'Invalid proxy auth format, username:password expected'
|
||||
else
|
||||
@proxy_auth = auth
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_plugins=(enumerate_plugins)
|
||||
if enumerate_plugins === true and (@enumerate_all_plugins === true or @enumerate_only_vulnerable_plugins === true)
|
||||
raise 'Please choose only one plugin enumeration option'
|
||||
else
|
||||
@enumerate_plugins = enumerate_plugins
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_only_vulnerable_plugins=(enumerate_only_vulnerable_plugins)
|
||||
if enumerate_only_vulnerable_plugins === true and (@enumerate_all_plugins === true or @enumerate_plugins === true)
|
||||
raise 'Please choose only one plugin enumeration option'
|
||||
else
|
||||
@enumerate_only_vulnerable_plugins = enumerate_only_vulnerable_plugins
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_all_plugins=(enumerate_all_plugins)
|
||||
if enumerate_all_plugins === true and (@enumerate_plugins === true or @enumerate_only_vulnerable_plugins === true)
|
||||
raise 'Please choose only one plugin enumeration option'
|
||||
else
|
||||
@enumerate_all_plugins = enumerate_all_plugins
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_themes=(enumerate_themes)
|
||||
if enumerate_themes === true and (@enumerate_all_themes === true or @enumerate_only_vulnerable_themes === true)
|
||||
raise 'Please choose only one theme enumeration option'
|
||||
else
|
||||
@enumerate_themes = enumerate_themes
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_only_vulnerable_themes=(enumerate_only_vulnerable_themes)
|
||||
if enumerate_only_vulnerable_themes === true and (@enumerate_all_themes === true or @enumerate_themes === true)
|
||||
raise 'Please choose only one theme enumeration option'
|
||||
else
|
||||
@enumerate_only_vulnerable_themes = enumerate_only_vulnerable_themes
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_all_themes=(enumerate_all_themes)
|
||||
if enumerate_all_themes === true and (@enumerate_themes === true or @enumerate_only_vulnerable_themes === true)
|
||||
raise 'Please choose only one theme enumeration option'
|
||||
else
|
||||
@enumerate_all_themes = enumerate_all_themes
|
||||
end
|
||||
end
|
||||
|
||||
def debug_output=(debug_output)
|
||||
Typhoeus::Config.verbose = debug_output
|
||||
end
|
||||
|
||||
def has_options?
|
||||
!to_h.empty?
|
||||
end
|
||||
|
||||
def random_agent=(useless)
|
||||
@user_agent = get_random_user_agent
|
||||
end
|
||||
|
||||
# return Hash
|
||||
def to_h
|
||||
options = {}
|
||||
|
||||
ACCESSOR_OPTIONS.each do |option|
|
||||
instance_variable = instance_variable_get("@#{option}")
|
||||
|
||||
unless instance_variable.nil?
|
||||
options[:"#{option}"] = instance_variable
|
||||
end
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
# Will load the options from ARGV
|
||||
# return WpscanOptions
|
||||
def self.load_from_arguments
|
||||
wpscan_options = WpscanOptions.new
|
||||
|
||||
if ARGV.length > 0
|
||||
WpscanOptions.get_opt_long.each do |opt, arg|
|
||||
wpscan_options.set_option_from_cli(opt, arg)
|
||||
end
|
||||
end
|
||||
|
||||
wpscan_options
|
||||
end
|
||||
|
||||
# string cli_option : --url, -u, --proxy etc
|
||||
# string cli_value : the option value
|
||||
def set_option_from_cli(cli_option, cli_value)
|
||||
|
||||
if WpscanOptions.is_long_option?(cli_option)
|
||||
self.send(
|
||||
WpscanOptions.option_to_instance_variable_setter(cli_option),
|
||||
cli_value
|
||||
)
|
||||
elsif cli_option === '--enumerate' # Special cases
|
||||
# Default value if no argument is given
|
||||
cli_value = 'vt,tt,u,vp' if cli_value.length == 0
|
||||
|
||||
enumerate_options_from_string(cli_value)
|
||||
else
|
||||
text = "Unknown option : #{cli_option}"
|
||||
text << " with value #{cli_value}" if (cli_value && !cli_value.empty?)
|
||||
raise text
|
||||
end
|
||||
end
|
||||
|
||||
# Will set enumerate_* from the string value
|
||||
# IE : if value = vp => :enumerate_only_vulnerable_plugins will be set to true
|
||||
# multiple enumeration are possible : 'u,p' => :enumerate_usernames and :enumerate_plugins
|
||||
# Special case for usernames, a range is possible : u[1-10] will enumerate usernames from 1 to 10
|
||||
def enumerate_options_from_string(value)
|
||||
# Usage of self is mandatory because there are overridden setters
|
||||
|
||||
value = value.split(',').map { |c| c.downcase }
|
||||
|
||||
self.enumerate_only_vulnerable_plugins = true if value.include?('vp')
|
||||
|
||||
self.enumerate_plugins = true if value.include?('p')
|
||||
|
||||
self.enumerate_all_plugins = true if value.include?('ap')
|
||||
|
||||
@enumerate_timthumbs = true if value.include?('tt')
|
||||
|
||||
self.enumerate_only_vulnerable_themes = true if value.include?('vt')
|
||||
|
||||
self.enumerate_themes = true if value.include?('t')
|
||||
|
||||
self.enumerate_all_themes = true if value.include?('at')
|
||||
|
||||
value.grep(/^u/) do |username_enum_value|
|
||||
@enumerate_usernames = true
|
||||
# Check for usernames range
|
||||
matches = %r{\[([\d]+)-([\d]+)\]}.match(username_enum_value)
|
||||
if matches
|
||||
@enumerate_usernames_range = (matches[1].to_i..matches[2].to_i)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
protected
|
||||
# Even if a short option is given (IE : -u), the long one will be returned (IE : --url)
|
||||
def self.get_opt_long
|
||||
GetoptLong.new(
|
||||
['--url', '-u', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--vhost',GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--enumerate', '-e', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--username', '-U', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--usernames', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--wordlist', '-w', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--threads', '-t', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--force', '-f', GetoptLong::NO_ARGUMENT],
|
||||
['--user-agent', '-a', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--random-agent', '-r', GetoptLong::NO_ARGUMENT],
|
||||
['--help', '-h', GetoptLong::NO_ARGUMENT],
|
||||
['--verbose', '-v', GetoptLong::NO_ARGUMENT],
|
||||
['--proxy', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--proxy-auth', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--update', GetoptLong::NO_ARGUMENT],
|
||||
['--follow-redirection', GetoptLong::NO_ARGUMENT],
|
||||
['--wp-content-dir', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--wp-plugins-dir', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--config-file', '-c', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--exclude-content-based', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--basic-auth', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--debug-output', GetoptLong::NO_ARGUMENT],
|
||||
['--version', GetoptLong::NO_ARGUMENT],
|
||||
['--cache-ttl', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--request-timeout', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--connect-timeout', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--batch', GetoptLong::NO_ARGUMENT],
|
||||
['--no-color', GetoptLong::NO_ARGUMENT],
|
||||
['--cookie', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--log', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--no-banner', GetoptLong::NO_ARGUMENT],
|
||||
['--throttle', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--disable-accept-header', GetoptLong::NO_ARGUMENT],
|
||||
['--disable-referer', GetoptLong::NO_ARGUMENT],
|
||||
['--cache-dir', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--disable-tls-checks', GetoptLong::NO_ARGUMENT],
|
||||
)
|
||||
end
|
||||
|
||||
def self.is_long_option?(option)
|
||||
ACCESSOR_OPTIONS.include?(:"#{WpscanOptions.clean_option(option)}")
|
||||
end
|
||||
|
||||
# Will removed the '-' or '--' chars at the beginning of option
|
||||
# and replace any remaining '-' by '_'
|
||||
#
|
||||
# param string option
|
||||
# return string
|
||||
def self.clean_option(option)
|
||||
cleaned_option = option.gsub(/^--?/, '')
|
||||
cleaned_option.gsub(/-/, '_')
|
||||
end
|
||||
|
||||
def self.option_to_instance_variable_setter(option)
|
||||
cleaned_option = WpscanOptions.clean_option(option)
|
||||
option_syms = ACCESSOR_OPTIONS.grep(%r{^#{cleaned_option}$})
|
||||
|
||||
option_syms.length == 1 ? :"#{option_syms.at(0)}=" : nil
|
||||
end
|
||||
|
||||
end
|
||||
4
spec/cache/.gitignore
vendored
4
spec/cache/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
@@ -1,264 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Browser do
|
||||
it_behaves_like 'Browser::Actions'
|
||||
it_behaves_like 'Browser::Options'
|
||||
|
||||
CONFIG_FILE_WITHOUT_PROXY = SPEC_FIXTURES_CONF_DIR + '/browser.conf.json'
|
||||
CONFIG_FILE_WITH_PROXY = SPEC_FIXTURES_CONF_DIR + '/browser.conf_proxy.json'
|
||||
#CONFIG_FILE_WITH_PROXY_AND_AUTH = SPEC_FIXTURES_CONF_DIR + '/browser.conf_proxy_auth.json'
|
||||
|
||||
subject(:browser) {
|
||||
Browser.reset
|
||||
Browser.instance(options)
|
||||
}
|
||||
let(:options) { {} }
|
||||
let(:instance_vars_to_check) {
|
||||
['proxy', 'max_threads', 'cache_ttl', 'request_timeout', 'connect_timeout']
|
||||
}
|
||||
let(:json_config_without_proxy) { JSON.parse(File.read(CONFIG_FILE_WITHOUT_PROXY)) }
|
||||
let(:json_config_with_proxy) { JSON.parse(File.read(CONFIG_FILE_WITH_PROXY)) }
|
||||
|
||||
def check_instance_variables(browser, json_expected_vars)
|
||||
json_expected_vars['max_threads'] ||= 20 # max_thread can not be nil
|
||||
|
||||
instance_vars_to_check.each do |variable_name|
|
||||
expect(browser.send(:"#{variable_name}")).to be === json_expected_vars[variable_name]
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Singleton' do
|
||||
it 'should not allow #new' do
|
||||
expect { Browser.new }.to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe '::instance' do
|
||||
after { check_instance_variables(browser, @json_expected_vars) }
|
||||
|
||||
context "when :config_file = #{CONFIG_FILE_WITH_PROXY}" do
|
||||
let(:options) { { config_file: CONFIG_FILE_WITH_PROXY } }
|
||||
|
||||
it 'will check the instance vars' do
|
||||
@json_expected_vars = json_config_with_proxy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when options[:cache_dir]' do
|
||||
let(:cache_dir) { CACHE_DIR + '/somewhere' }
|
||||
let(:options) { { cache_dir: cache_dir } }
|
||||
|
||||
after { expect(subject.cache_dir).to eq cache_dir }
|
||||
|
||||
it 'sets @cache_dir' do
|
||||
@json_expected_vars = json_config_without_proxy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#load_config' do
|
||||
context 'when config_file is a symlink' do
|
||||
let(:config_file) { './rspec_symlink' }
|
||||
|
||||
it 'raises an error' do
|
||||
File.symlink('./testfile', config_file)
|
||||
expect { browser.load_config(config_file) }.to raise_error('[ERROR] Config file is a symlink.')
|
||||
File.unlink(config_file)
|
||||
end
|
||||
end
|
||||
|
||||
context 'otherwise' do
|
||||
after do
|
||||
browser.load_config(@config_file)
|
||||
check_instance_variables(browser, @expected)
|
||||
end
|
||||
|
||||
it 'sets the correct variables' do
|
||||
@config_file = CONFIG_FILE_WITH_PROXY
|
||||
@expected = json_config_without_proxy.merge(json_config_with_proxy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '::append_params_header_field' do
|
||||
after :each do
|
||||
expect(Browser.append_params_header_field(
|
||||
@params,
|
||||
@field,
|
||||
@field_value
|
||||
)).to be === @expected
|
||||
end
|
||||
|
||||
context 'when there is no headers' do
|
||||
it 'create the header and set the field' do
|
||||
@params = { somekey: 'somevalue' }
|
||||
@field = 'User-Agent'
|
||||
@field_value = 'FakeOne'
|
||||
@expected = { somekey: 'somevalue', headers: { 'User-Agent' => 'FakeOne' } }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are headers' do
|
||||
context 'when the field already exists' do
|
||||
it 'does not replace it' do
|
||||
@params = { somekey: 'somevalue', headers: { 'Location' => 'SomeLocation' } }
|
||||
@field = 'Location'
|
||||
@field_value = 'AnotherLocation'
|
||||
@expected = @params
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the field is not present' do
|
||||
it 'sets the field' do
|
||||
@params = { somekey: 'somevalue', headers: { 'Auth' => 'user:pass' } }
|
||||
@field = 'UA'
|
||||
@field_value = 'FF'
|
||||
@expected = { somekey: 'somevalue', headers: { 'Auth' => 'user:pass', 'UA' => 'FF' } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#merge_request_params' do
|
||||
let(:params) { {} }
|
||||
let(:cookie_jar) { CACHE_DIR + '/browser/cookie-jar' }
|
||||
let(:user_agent) { 'SomeUA' }
|
||||
let(:default_expectation) {
|
||||
{
|
||||
cache_ttl: 250,
|
||||
cookiejar: cookie_jar, cookiefile: cookie_jar,
|
||||
timeout: 60, connecttimeout: 10,
|
||||
maxredirs: 3,
|
||||
referer: nil
|
||||
}
|
||||
}
|
||||
|
||||
after :each do
|
||||
browser.user_agent = user_agent
|
||||
browser.cache_ttl = 250
|
||||
|
||||
expect(browser.merge_request_params(params)).to eq @expected
|
||||
expect(Typhoeus::Config.user_agent).to eq user_agent
|
||||
end
|
||||
|
||||
it 'sets the User-Agent header field and cache_ttl' do
|
||||
@expected = default_expectation
|
||||
end
|
||||
|
||||
context 'when @user_agent' do
|
||||
let(:user_agent) { 'test' }
|
||||
|
||||
it 'sets the User-Agent' do
|
||||
@expected = default_expectation
|
||||
end
|
||||
end
|
||||
|
||||
context 'when @proxy' do
|
||||
let(:proxy) { '127.0.0.1:9050' }
|
||||
let(:proxy_expectation) { default_expectation.merge(proxy: proxy) }
|
||||
|
||||
it 'merges the proxy' do
|
||||
browser.proxy = proxy
|
||||
@expected = proxy_expectation
|
||||
end
|
||||
|
||||
context 'when @proxy_auth' do
|
||||
it 'sets the proxy_auth' do
|
||||
browser.proxy = proxy
|
||||
browser.proxy_auth = 'user:pass'
|
||||
@expected = proxy_expectation.merge(proxyuserpwd: 'user:pass')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when @request_timeout' do
|
||||
it 'gives an Integer' do
|
||||
browser.request_timeout = '10'
|
||||
|
||||
@expected = default_expectation.merge(timeout: 10)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when @basic_auth' do
|
||||
it 'appends the basic_auth' do
|
||||
browser.basic_auth = 'user:pass'
|
||||
@expected = default_expectation.merge(
|
||||
headers: { 'Authorization' => 'Basic ' + Base64.encode64('user:pass').chomp }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the cache_ttl is alreday set' do
|
||||
let(:params) { { cache_ttl: 500 } }
|
||||
|
||||
it 'does not override it' do
|
||||
@expected = default_expectation.merge(params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the maxredirs is alreday set' do
|
||||
let(:params) { { maxredirs: 100 } }
|
||||
|
||||
it 'does not override it' do
|
||||
@expected = default_expectation.merge(params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when @cookie' do
|
||||
let(:cookie) { 'foor=bar;bar=foo' }
|
||||
before { browser.cookie = cookie }
|
||||
|
||||
it 'sets the cookie' do
|
||||
@expected = default_expectation.merge(cookie: cookie)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when @disable_tls_checks' do
|
||||
it 'disables tls checks' do
|
||||
browser.disable_tls_checks = true
|
||||
@expected = default_expectation.merge(ssl_verifypeer: 0, ssl_verifyhost: 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#forge_request' do
|
||||
let(:url) { 'http://example.localhost' }
|
||||
|
||||
it 'returns the correct Typhoeus::Request' do
|
||||
allow(subject).to receive_messages(merge_request_params: { cache_ttl: 10 })
|
||||
|
||||
request = subject.forge_request(url)
|
||||
expect(request).to be_a Typhoeus::Request
|
||||
expect(request.url).to eq url
|
||||
expect(request.cache_ttl).to eq 10
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'testing caching' do
|
||||
it 'should only do 1 request, and retrieve the other one from the cache' do
|
||||
|
||||
url = 'http://example.localhost'
|
||||
|
||||
stub_request(:get, url).to_return(status: 200, body: 'Hello World !')
|
||||
|
||||
response1 = Browser.get(url)
|
||||
response2 = Browser.get(url)
|
||||
|
||||
expect(response1.body).to eq response2.body
|
||||
#WebMock.should have_requested(:get, url).times(1) # This one fail, dunno why :s (but it works without mock)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'testing UTF8' do
|
||||
it 'should not throw an encoding exception' do
|
||||
url = SPEC_FIXTURES_DIR + '/utf8.html'
|
||||
stub_request(:get, url).to_return(status: 200, body: File.read(url))
|
||||
|
||||
response = Browser.get(url)
|
||||
expect { response.body }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,102 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe CacheFileStore do
|
||||
let(:cache_dir) { SPEC_CACHE_DIR + '/cache_file_store' }
|
||||
|
||||
before :each do
|
||||
Dir.delete(cache_dir) rescue nil
|
||||
|
||||
@cache = CacheFileStore.new(cache_dir)
|
||||
end
|
||||
|
||||
after :each do
|
||||
@cache.clean
|
||||
end
|
||||
|
||||
describe '#storage_path' do
|
||||
it 'returns the storage path given in the #new' do
|
||||
expect(@cache.storage_path).to match(/#{cache_dir}/)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#serializer' do
|
||||
it 'should return the default serializer : Marshal' do
|
||||
expect(@cache.serializer).to eq Marshal
|
||||
expect(@cache.serializer).not_to eq YAML
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clean' do
|
||||
it "should remove all files from the cache dir (#{@cache_dir}" do
|
||||
# clean is executed by other tests before
|
||||
before = count_files_in_dir(@cache.cache_dir)
|
||||
test_dir = File.expand_path("#{@cache.cache_dir}/test")
|
||||
Dir.mkdir test_dir
|
||||
#change the modification date
|
||||
%x[ touch -t 200701310846.26 #{test_dir} ]
|
||||
expect(count_files_in_dir(@cache.cache_dir)).to eq (before + 1)
|
||||
@cache.clean
|
||||
expect(count_files_in_dir(@cache.cache_dir)).to eq before
|
||||
end
|
||||
end
|
||||
|
||||
describe '#read_entry' do
|
||||
after { expect(@cache.read_entry(key)).to eq @expected }
|
||||
|
||||
context 'when the entry does not exist' do
|
||||
let(:key) { Digest::SHA1.hexdigest('hello world') }
|
||||
|
||||
it 'should return nil' do
|
||||
@expected = nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file exist but is empty (marshal data too short error)' do
|
||||
let(:key) { 'empty-file' }
|
||||
|
||||
it 'returns nil' do
|
||||
File.new(File.join(@cache.storage_path, key), File::CREAT)
|
||||
|
||||
@expected = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#write_entry, #read_entry' do
|
||||
|
||||
after :each do
|
||||
@cache.write_entry(@key, @data, @timeout)
|
||||
expect(@cache.read_entry(@key)).to be === @expected
|
||||
end
|
||||
|
||||
it 'should get the correct entry (string)' do
|
||||
@timeout = 10
|
||||
@key = 'some_key'
|
||||
@data = 'Hello World !'
|
||||
@expected = @data
|
||||
end
|
||||
|
||||
it 'should not write the entry' do
|
||||
@timeout = 0
|
||||
@key = 'another_key'
|
||||
@data = 'Another Hello World !'
|
||||
@expected = nil
|
||||
end
|
||||
|
||||
## TODO write / read for an object
|
||||
end
|
||||
|
||||
describe '#storage_dir' do
|
||||
it 'should create a unique storage dir' do
|
||||
storage_dirs = []
|
||||
|
||||
(1..5).each do |_|
|
||||
storage_dirs << CacheFileStore.new(cache_dir).storage_path
|
||||
end
|
||||
|
||||
expect(storage_dirs.uniq.size).to eq 5
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
#encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe WpItems do
|
||||
it_behaves_like 'WpItems::Detectable' do
|
||||
subject(:wp_items) { WpItems }
|
||||
let(:item_class) { WpItem }
|
||||
let(:fixtures_dir) { COLLECTIONS_FIXTURES + '/wp_items/detectable' }
|
||||
|
||||
let(:expected) do
|
||||
{
|
||||
request_params: { cache_ttl: 0, followlocation: true },
|
||||
targets_items_from_file: [ WpItem.new(uri, name: 'item1'),
|
||||
WpItem.new(uri, name: 'item-2'),
|
||||
WpItem.new(uri, name: 'mr-smith')],
|
||||
|
||||
vulnerable_targets_items: [ WpItem.new(uri, name: 'mr-smith'),
|
||||
WpItem.new(uri, name: 'neo')],
|
||||
|
||||
passive_detection: (1..15).reduce(WpItems.new) { |o, i| o << WpItem.new(uri, name: "detect-me-#{i}") }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,116 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
require WPSCAN_LIB_DIR + '/wp_target'
|
||||
|
||||
describe 'WpPlugins::Detectable' do
|
||||
subject(:wp_plugins) { WpPlugins }
|
||||
let(:wp_content_dir) { 'wp-content' }
|
||||
let(:wp_plugins_dir) { wp_content_dir + '/plugins' }
|
||||
let(:wp_target) { WpTarget.new(url, wp_content_dir: wp_content_dir, wp_plugins_dir: wp_plugins_dir) }
|
||||
let(:url) { 'http://example.com/' }
|
||||
let(:uri) { URI.parse(url) }
|
||||
|
||||
describe '::from_header' do
|
||||
context 'when no header' do
|
||||
it 'returns an empty WpPlugins' do
|
||||
stub_request(:get, url).to_return(status: 200)
|
||||
expect(subject.send(:from_header, wp_target)).to eq subject.new
|
||||
end
|
||||
end
|
||||
|
||||
context 'when headers' do
|
||||
let(:headers) { { } }
|
||||
let(:expected) { subject.new(wp_target) }
|
||||
|
||||
after :each do
|
||||
stub_request(:get, url).to_return(status: 200, headers: headers, body: '')
|
||||
expect(subject.send(:from_header, wp_target)).to eq expected
|
||||
end
|
||||
|
||||
context 'when w3-total-cache detected' do
|
||||
it 'returns the w3-total-cache' do
|
||||
headers['X-Powered-BY'] = 'W3 Total Cache/0.9'
|
||||
expected.add('w3-total-cache', version: '0.9')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wp-super-cache detected' do
|
||||
it 'returns the wp-super-cache' do
|
||||
headers['WP-Super-Cache'] = 'Served supercache file from PHP'
|
||||
expected.add('wp-super-cache')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a header key with mutiple values' do
|
||||
let(:headers) { { 'X-Powered-BY' => ['PHP/5.4.9', 'ASP.NET'] } }
|
||||
|
||||
context 'when no cache plugin' do
|
||||
it 'returns an empty WpPlugins' do
|
||||
# Handled
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a cache plugin' do
|
||||
it 'returns the correct plugin' do
|
||||
headers['X-Powered-BY'] << 'W3 Total Cache/0.9.2.5'
|
||||
|
||||
expected.add('w3-total-cache', version: '0.9.2.5')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '::from_content' do
|
||||
context 'when no body' do
|
||||
it 'returns an empty WpPlugins' do
|
||||
stub_request(:get, url).to_return(status: 200, body: '')
|
||||
expect(subject.send(:from_content, wp_target)).to eq subject.new
|
||||
end
|
||||
end
|
||||
|
||||
context 'when body' do
|
||||
@body = ''
|
||||
let(:expected) { subject.new(wp_target) }
|
||||
|
||||
after :each do
|
||||
stub_request(:get, url).to_return(status: 200, body: @body)
|
||||
stub_request(:get, /readme\.txt/i).to_return(status: 404)
|
||||
expect(subject.send(:from_content, wp_target)).to eq expected
|
||||
end
|
||||
|
||||
context 'when w3 total cache detected' do
|
||||
it 'returns the w3-total-cache' do
|
||||
@body = 'w3 total cache'
|
||||
expected.add('w3-total-cache')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wp-super-cache detected' do
|
||||
it 'returns the wp-super-cache' do
|
||||
@body = 'wp-super-cache'
|
||||
expected.add('wp-super-cache')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when all-in-one-seo-pack detected' do
|
||||
it 'returns the all-in-one-seo-pack' do
|
||||
@body = '<!-- All in One SEO Pack 2.0.3.1 by Michael Torbert of Semper Fi Web Design[300,342] -->'
|
||||
expected.add('all-in-one-seo-pack', version: '2.0.3.1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when google-universal-analytics detected' do
|
||||
it 'returns google-universal-analytics' do
|
||||
@body = '<!-- Google Universal Analytics for WordPress v2.4.2 -->'
|
||||
expected.add('google-universal-analytics', version: '2.4.2')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '::passive_detection' do
|
||||
# TODO
|
||||
end
|
||||
end
|
||||
@@ -1,30 +0,0 @@
|
||||
#encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe WpPlugins do
|
||||
it_behaves_like 'WpItems::Detectable' do
|
||||
subject(:wp_plugins) { WpPlugins }
|
||||
let(:item_class) { WpPlugin }
|
||||
let(:fixtures_dir) { COLLECTIONS_FIXTURES + '/wp_plugins/detectable' }
|
||||
|
||||
let(:expected) do
|
||||
{
|
||||
request_params: { cache_ttl: 0, followlocation: true },
|
||||
vulns_file: PLUGINS_FILE,
|
||||
targets_items_from_file: [ WpPlugin.new(uri, name: 'plugin1'),
|
||||
WpPlugin.new(uri, name:'plugin-2'),
|
||||
WpPlugin.new(uri, name: 'mr-smith')],
|
||||
|
||||
vulnerable_targets_items: [ WpPlugin.new(uri, name: 'mr-smith'),
|
||||
WpPlugin.new(uri, name: 'neo')],
|
||||
|
||||
passive_detection: WpPlugins.new << WpPlugin.new(uri, name: 'escaped-url') <<
|
||||
WpPlugin.new(uri, name: 'link-tag') <<
|
||||
WpPlugin.new(uri, name: 'script-tag') <<
|
||||
WpPlugin.new(uri, name: 'style-tag') <<
|
||||
WpPlugin.new(uri, name: 'style-tag-import')
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,31 +0,0 @@
|
||||
#encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe WpThemes do
|
||||
before { stub_request(:get, /.+\/style.css$/).to_return(status: 200) }
|
||||
|
||||
it_behaves_like 'WpItems::Detectable' do
|
||||
subject(:wp_themes) { WpThemes }
|
||||
let(:item_class) { WpTheme }
|
||||
let(:fixtures_dir) { COLLECTIONS_FIXTURES + '/wp_themes/detectable' }
|
||||
|
||||
let(:expected) do
|
||||
{
|
||||
request_params: { cache_ttl: 0, followlocation: true },
|
||||
vulns_file: THEMES_FILE,
|
||||
targets_items_from_file: [ WpTheme.new(uri, name: '3colours'),
|
||||
WpTheme.new(uri, name:'42k'),
|
||||
WpTheme.new(uri, name: 'a-ri')],
|
||||
|
||||
vulnerable_targets_items: [ WpTheme.new(uri, name: 'shopperpress'),
|
||||
WpTheme.new(uri, name: 'webfolio')],
|
||||
|
||||
passive_detection: WpThemes.new << WpTheme.new(uri, name: 'theme1') <<
|
||||
WpTheme.new(uri, name: 'theme 2') <<
|
||||
WpTheme.new(uri, name: 'theme-3') <<
|
||||
WpTheme.new(uri, name: 'style-tag-import')
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,123 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
require WPSCAN_LIB_DIR + '/wp_target'
|
||||
|
||||
describe 'WpTimthumbs::Detectable' do
|
||||
subject(:wp_timthumbs) { WpTimthumbs }
|
||||
let(:fixtures_dir) { COLLECTIONS_FIXTURES + '/wp_timthumbs/detectable' }
|
||||
let(:targets_items_file) { fixtures_dir + '/targets.txt' }
|
||||
let(:wp_content_dir) { 'wp-content' }
|
||||
let(:wp_plugins_dir) { wp_content_dir + '/plugins' }
|
||||
let(:wp_target) { WpTarget.new(url, wp_content_dir: wp_content_dir, wp_plugins_dir: wp_plugins_dir) }
|
||||
let(:url) { 'http://example.com/' }
|
||||
let(:uri) { URI.parse(url) }
|
||||
let(:empty_file) { SPEC_FIXTURES_DIR + '/empty-file' }
|
||||
|
||||
let(:expected) do
|
||||
{
|
||||
targets_from_file: [WpTimthumb.new(uri, path: 'timthumb.php'),
|
||||
WpTimthumb.new(uri, path: '$wp-content$/timthumb.php'),
|
||||
WpTimthumb.new(uri, path: '$wp-plugins$/a-gallery/timthumb.php'),
|
||||
WpTimthumb.new(uri, path: '$wp-content$/themes/theme-name/timthumb.php')]
|
||||
|
||||
}
|
||||
end
|
||||
|
||||
def expected_targets_from_theme(theme_name)
|
||||
expected = []
|
||||
%w(
|
||||
timthumb.php lib/timthumb.php inc/timthumb.php includes/timthumb.php
|
||||
scripts/timthumb.php tools/timthumb.php functions/timthumb.php thumb.php
|
||||
).each do |file|
|
||||
path = "$wp-content$/themes/#{theme_name}/#{file}"
|
||||
expected << WpTimthumb.new(uri, path: path)
|
||||
end
|
||||
expected
|
||||
end
|
||||
|
||||
describe '::passive_detection' do
|
||||
it 'returns an empty WpTimthumbs' do
|
||||
expect(subject.passive_detection(wp_target)).to eq subject.new
|
||||
end
|
||||
end
|
||||
|
||||
describe '::targets_items_from_file' do
|
||||
after do
|
||||
targets = subject.send(:targets_items_from_file, file, wp_target)
|
||||
|
||||
expect(targets.map(&:url)).to eq @expected.map(&:url)
|
||||
end
|
||||
|
||||
context 'when an empty file' do
|
||||
let(:file) { empty_file }
|
||||
|
||||
it 'returns an empty Array' do
|
||||
@expected = []
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a non empty file' do
|
||||
let(:file) { targets_items_file }
|
||||
|
||||
it 'returns the correct Array of WpTimthumb' do
|
||||
@expected = expected[:targets_from_file]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '::theme_timthumbs' do
|
||||
it 'returns the correct Array of WpTimthumb' do
|
||||
theme = 'hello-world'
|
||||
targets = subject.send(:theme_timthumbs, theme, wp_target)
|
||||
|
||||
expect(targets.map(&:url)).to eq expected_targets_from_theme(theme).map(&:url)
|
||||
end
|
||||
end
|
||||
|
||||
describe '::targets_items' do
|
||||
let(:options) { {} }
|
||||
|
||||
after do
|
||||
targets = subject.send(:targets_items, wp_target, options)
|
||||
|
||||
expect(targets.map(&:url)).to match_array(@expected.map(&:url))
|
||||
end
|
||||
|
||||
context 'when no :theme_name' do
|
||||
context 'when no :file' do
|
||||
it 'returns an empty Array' do
|
||||
@expected = []
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :file' do
|
||||
let(:options) { { file: targets_items_file } }
|
||||
|
||||
it 'returns the targets from the file' do
|
||||
@expected = expected[:targets_from_file]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :theme_name' do
|
||||
let(:theme) { 'theme-name' }
|
||||
|
||||
context 'when no :file' do
|
||||
let(:options) { { theme_name: theme } }
|
||||
|
||||
it 'returns targets from the theme' do
|
||||
@expected = expected_targets_from_theme(theme)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :file' do
|
||||
let(:options) { { theme_name: theme, file: targets_items_file } }
|
||||
|
||||
it 'returns merged targets from theme and file' do
|
||||
@expected = (expected_targets_from_theme('theme-name') + expected[:targets_from_file]).uniq { |i| i.url }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,59 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
require WPSCAN_LIB_DIR + '/wp_target'
|
||||
|
||||
describe 'WpUsers::Detectable' do
|
||||
subject(:wp_users) { WpUsers }
|
||||
let(:wp_content_dir) { 'wp-content' }
|
||||
let(:wp_plugins_dir) { wp_content_dir + '/plugins' }
|
||||
let(:wp_target) { WpTarget.new(url, wp_content_dir: wp_content_dir, wp_plugins_dir: wp_plugins_dir) }
|
||||
let(:url) { 'http://example.com/' }
|
||||
let(:uri) { URI.parse(url) }
|
||||
|
||||
def create_from_range(range)
|
||||
result = []
|
||||
|
||||
range.each do |current_id|
|
||||
result << WpUser.new(uri, id: current_id)
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
describe '::request_params' do
|
||||
it 'return an empty Hash' do
|
||||
expect(subject.request_params).to be === {}
|
||||
end
|
||||
end
|
||||
|
||||
describe '::passive_detection' do
|
||||
it 'return an empty WpUsers' do
|
||||
expect(subject.passive_detection(wp_target)).to eq subject.new
|
||||
end
|
||||
end
|
||||
|
||||
describe '::targets_items' do
|
||||
after do
|
||||
targets = subject.send(:targets_items, wp_target, options)
|
||||
|
||||
expect(targets).to eq @expected
|
||||
end
|
||||
|
||||
context 'when no :range' do
|
||||
let(:options) { {} }
|
||||
|
||||
it 'returns Array<WpUser> with id from 1 to 10' do
|
||||
@expected = create_from_range((1..10))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when :range' do
|
||||
let(:options) { { range: (1..2) } }
|
||||
|
||||
it 'returns Array<WpUser> with id from 1 to 2' do
|
||||
@expected = create_from_range((1..2))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user