├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── bin └── sitecore-scan ├── install ├── lib └── sitecore_scan.rb └── sitecore_scan.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /vendor/bundle 26 | /lib/bundler/man/ 27 | 28 | # for a library or gem, you might want to ignore these files since the code is 29 | # intended to run in multiple environments; otherwise, check them in: 30 | # Gemfile.lock 31 | # .ruby-version 32 | # .ruby-gemset 33 | 34 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 35 | .rvmrc 36 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | sitecore_scan (0.0.4) 5 | logger (~> 1.6) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | logger (1.6.0) 11 | 12 | PLATFORMS 13 | ruby 14 | 15 | DEPENDENCIES 16 | sitecore_scan! 17 | 18 | BUNDLED WITH 19 | 2.3.19 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Brendan Coles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SitecoreScan 2 | 3 | ## Description 4 | 5 | SitecoreScan is a simple remote scanner for Sitecore CMS. 6 | 7 | ## Installation 8 | 9 | Install from RubyGems.org: 10 | 11 | ``` 12 | gem install sitecore_scan 13 | ``` 14 | 15 | Install from GitHub: 16 | 17 | ``` 18 | git clone https://github.com/bcoles/sitecore_scan 19 | cd sitecore_scan 20 | bundle install 21 | gem build sitecore_scan.gemspec 22 | gem install --local sitecore_scan-0.0.4.gem 23 | ``` 24 | 25 | ## Usage (command line) 26 | 27 | ``` 28 | % sitecore-scan -h 29 | Usage: sitecore-scan [options] 30 | -u, --url URL Sitecore URL to scan 31 | -s, --skip Skip check for Sitecore 32 | -i, --insecure Skip SSL/TLS validation 33 | -v, --verbose Enable verbose output 34 | -h, --help Show this help 35 | ``` 36 | 37 | ## Usage (ruby) 38 | 39 | ```ruby 40 | #!/usr/bin/env ruby 41 | require 'sitecore_scan' 42 | url = 'https://sitecore.example.local/' 43 | SitecoreScan::detectSitecore(url) # Check if a URL is Sitecore (using all methods) 44 | SitecoreScan::detectSitecoreEditMode(url) # Check if a URL is Sitecore (detect edit mode) 45 | SitecoreScan::detectSitecoreErrorRedirect(url) # Check if a URL is Sitecore (detect error redirect) 46 | SitecoreScan::glimpseDebugging(url) # Check if Glimpse debugging is enabled 47 | SitecoreScan::soapApi(url) # Check if SOAP API is accessible 48 | SitecoreScan::mvcDeviceSimulatorFileDisclosure(url) # Check if MVC Device Simulator allows file disclosure 49 | SitecoreScan::dashboardReporting(url) # Check if Executive Insight Dashboard reporting is accessible 50 | SitecoreScan::telerikWebUi(url) # Check if Telerik Web UI is accessible 51 | SitecoreScan::getVersionFromLogin(url) # Retrieve Sitecore version from Login page 52 | ``` 53 | -------------------------------------------------------------------------------- /bin/sitecore-scan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file is part of SitecoreScan 4 | # https://github.com/bcoles/sitecore_scan 5 | # 6 | 7 | require 'sitecore_scan' 8 | require 'optparse' 9 | require 'resolv' 10 | 11 | def banner 12 | puts " 13 | _____ _ _ _____ 14 | / ____(_) | / ____| 15 | | (___ _| |_ ___ ___ ___ _ __ ___| (___ ___ __ _ _ __ 16 | \\___ \\| | __/ _ \\/ __/ _ \\| '__/ _ \\\\___ \\ / __/ _` | '_ \\ 17 | ____) | | || __/ (_| (_) | | | __/____) | (_| (_| | | | | 18 | |_____/|_|\\__\\___|\\___\\___/|_| \\___|_____/ \\___\\__,_|_| |_| 19 | version #{SitecoreScan::VERSION}" 20 | puts 21 | puts '-' * 60 22 | end 23 | 24 | banner 25 | options = {} 26 | opts = OptionParser.new do |o| 27 | o.banner = "Usage: sitecore-scan [options]" 28 | 29 | o.on('-u URL', '--url URL', 'Sitecore URL to scan') do |v| 30 | unless v.match(%r{\Ahttps?://}) 31 | puts "- Invalid URL: #{v}" 32 | exit(1) 33 | end 34 | options[:url] = v 35 | end 36 | 37 | o.on('-s', '--skip', 'Skip check for Sitecore') do 38 | options[:skip] = true 39 | end 40 | 41 | o.on('-i', '--insecure', 'Skip SSL/TLS validation') do 42 | options[:insecure] = true 43 | end 44 | 45 | o.on('-v', '--verbose', 'Enable verbose output') do 46 | options[:verbose] = true 47 | end 48 | 49 | o.on('-h', '--help', 'Show this help') do 50 | puts opts 51 | exit 52 | end 53 | end 54 | 55 | opts.parse! 56 | 57 | if options[:url].nil? 58 | puts opts 59 | exit(1) 60 | end 61 | 62 | def scan(url, check: true, insecure: false, verbose: false) 63 | SitecoreScan.logger = ::Logger.new($stdout).tap do |log| 64 | log.progname = 'sitecore-scan' 65 | log.level = verbose ? ::Logger::INFO : ::Logger::WARN 66 | log.datetime_format = '%Y-%m-%d %H:%M:%S ' 67 | end 68 | 69 | SitecoreScan.insecure = insecure 70 | 71 | puts "Scan started at #{Time.now.getutc}" 72 | puts "URL: #{url}" 73 | 74 | # parse URL 75 | target = nil 76 | begin 77 | target = URI::parse(url.split('?').first) 78 | rescue 79 | puts "- Could not parse target URL: #{url}" 80 | end 81 | exit(1) if target.nil? 82 | 83 | # resolve IP address 84 | begin 85 | ip = Resolv.getaddress(target.host).to_s 86 | puts "IP: #{ip}" unless ip.nil? 87 | rescue 88 | puts "- Could not resolve hostname #{target.host}" 89 | end 90 | 91 | puts "Port: #{target.port}" 92 | puts '-' * 60 93 | 94 | # Check if the URL is Sitecore 95 | if check 96 | unless SitecoreScan::detectSitecore(url) 97 | puts '- Sitecore not found' 98 | exit(1) 99 | end 100 | puts '+ Found Sitecore' 101 | end 102 | 103 | # Retrieve Sitecore version from Login page 104 | version = SitecoreScan::getVersionFromLogin(url) 105 | puts "+ Version: #{version}" if version 106 | 107 | # Check if Glimpse debugging is enabled 108 | puts "+ Glimpse debugging is enabled" if SitecoreScan::glimpseDebugging(url) 109 | 110 | # Check if SOAP API is accessible 111 | puts "+ SOAP API is available" if SitecoreScan::soapApi(url) 112 | 113 | # Check if MVC Device Simulator allows file disclosure 114 | puts "+ MVC Device Simulator allows file disclosure" if SitecoreScan::mvcDeviceSimulatorFileDisclosure(url) 115 | 116 | # Check if Executive Insight Dashboard reporting is accessible 117 | puts "+ Executive Insight Dashboard reporting is available" if SitecoreScan::dashboardReporting(url) 118 | 119 | # Check if Telerik Web UI is accessible 120 | puts "+ Telerik Web Ui is available" if SitecoreScan::telerikWebUi(url) 121 | 122 | puts "Scan finished at #{Time.now.getutc}" 123 | puts '-' * 60 124 | end 125 | 126 | scan( 127 | options[:url], 128 | insecure: options[:insecure], 129 | check: !options[:skip], 130 | verbose: options[:verbose] 131 | ) 132 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | bundle install 3 | gem build sitecore_scan.gemspec 4 | gem install --local sitecore_scan-0.0.4.gem 5 | -------------------------------------------------------------------------------- /lib/sitecore_scan.rb: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of SitecoreScan 3 | # https://github.com/bcoles/sitecore_scan 4 | # 5 | 6 | require 'uri' 7 | require 'cgi' 8 | require 'logger' 9 | require 'net/http' 10 | require 'openssl' 11 | require 'stringio' 12 | 13 | class SitecoreScan 14 | VERSION = '0.0.4'.freeze 15 | 16 | def self.logger 17 | @logger 18 | end 19 | 20 | def self.logger=(logger) 21 | @logger = logger 22 | end 23 | 24 | def self.insecure 25 | @insecure ||= false 26 | end 27 | 28 | def self.insecure=(insecure) 29 | @insecure = insecure 30 | end 31 | 32 | # 33 | # Check if URL is running Sitecore 34 | # 35 | # @param [String] URL 36 | # 37 | # @return [Boolean] 38 | # 39 | def self.detectSitecore(url) 40 | return true if detectSitecoreEditMode(url) 41 | return true if detectSitecoreErrorRedirect(url) 42 | 43 | false 44 | end 45 | 46 | # 47 | # Check if URL is running Sitecore using edit mode 48 | # 49 | # @param [String] URL 50 | # 51 | # @return [Boolean] 52 | # 53 | def self.detectSitecoreEditMode(url) 54 | url += '/' unless url.to_s.end_with? '/' 55 | res = sendHttpRequest("#{url}?sc_mode=edit") 56 | 57 | return false unless res 58 | 59 | return true if res['sitecore-item'] 60 | return true if res['set-cookies'].to_s.include?('sc_mode=edit') 61 | return true if res.code.to_i == 302 && (res['location'].to_s.downcase.include?('sitecore/login') || res['location'].to_s.downcase.include?('user=sitecore')) 62 | 63 | false 64 | end 65 | 66 | # 67 | # Check if URL is running Sitecore using error redirect 68 | # 69 | # @param [String] URL 70 | # 71 | # @return [Boolean] 72 | # 73 | def self.detectSitecoreErrorRedirect(url) 74 | url += '/' unless url.to_s.end_with? '/' 75 | res = sendHttpRequest("#{url}#{('a'..'z').to_a.shuffle[0,8].join}.aspx") 76 | 77 | return false unless res 78 | 79 | return true if res['sitecore-item'] 80 | return true if res.code.to_i == 302 && res['location'].to_s.downcase.include?('sitecore/service/notfound.aspx') 81 | 82 | false 83 | end 84 | 85 | # 86 | # Retrieve Sitecore version from Login page 87 | # 88 | # @param [String] URL 89 | # 90 | # @return [String] Sitecore version 91 | # 92 | def self.getVersionFromLogin(url) 93 | url += '/' unless url.to_s.end_with? '/' 94 | res = sendHttpRequest("#{url}sitecore/login") 95 | 96 | return unless res 97 | 98 | version = res.body.to_s.scan(%r{(Sitecore\.NET [\d\.]+ \(rev\. \d+\))}).flatten.first 99 | 100 | return version if version 101 | 102 | version = res.body.to_s.scan(%r{