Commit a71bd9a6 authored by Nong Hoang Tu's avatar Nong Hoang Tu
Browse files

New upstream version 0.13.7

parents
Pipeline #5768 failed with stages
version: 2
updates:
- package-ecosystem: bundler
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
- dependency-name: rubocop-performance
versions:
- 1.10.0
- dependency-name: rubocop
versions:
- 1.9.0
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: [2.5, 2.6, 2.7, '3.0', 3.1]
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Set up Ruby ${{ matrix.ruby }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: Install GEMs
run: |
gem install bundler
bundle config force_ruby_platform true
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: rubocop
run: |
bundle exec rubocop
- name: rspec
run: |
bundle exec rspec
- name: Coveralls
uses: coverallsapp/github-action@master
continue-on-error: true
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: Ruby Gem
on:
release:
types: [published]
jobs:
build:
name: Build + Publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Ruby 2.6
uses: actions/setup-ruby@v1
with:
ruby-version: 2.6.x
#- name: Publish to GPR
# run: |
# mkdir -p $HOME/.gem
# touch $HOME/.gem/credentials
# chmod 0600 $HOME/.gem/credentials
# printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
# gem build *.gemspec
# gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
# env:
# GEM_HOST_API_KEY: ${{secrets.GITHUB_TOKEN}}
# OWNER: wpscanteam
- name: Publish to RubyGems
run: |
mkdir -p $HOME/.gem
touch $HOME/.gem/credentials
chmod 0600 $HOME/.gem/credentials
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
gem build *.gemspec
gem push *.gem
env:
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
*.gem
*.rbc
.bundle
.config
coverage
pkg
Gemfile.lock
--color
--fail-fast
--require spec_helper
\ No newline at end of file
require: rubocop-performance
AllCops:
NewCops: enable
SuggestExtensions: false
TargetRubyVersion: 2.5
Exclude:
- '*.gemspec'
- 'vendor/**/*'
- 'example/**/*'
Layout/LineLength:
Max: 120
Lint/ConstantDefinitionInBlock:
Enabled: false
Lint/FloatComparison:
Exclude:
- spec/app/models/version_spec.rb
Lint/MissingSuper:
Enabled: false
Lint/UriEscapeUnescape:
Enabled: false
Lint/UselessMethodDefinition:
Exclude:
- spec/lib/finders/same_type_finder_spec.rb
- spec/lib/finders/unique_finder_spec.rb
Metrics/AbcSize:
Max: 28
Metrics/BlockLength:
Exclude:
- 'spec/**/*'
Metrics/CyclomaticComplexity:
Max: 10
Metrics/MethodLength:
Max: 18
Exclude:
- app/controllers/core/cli_options.rb
Metrics/ParameterLists:
Max: 6
MaxOptionalParameters: 4
Metrics/PerceivedComplexity:
Max: 9
Style/ClassVars:
Enabled: false
Style/CombinableLoops:
Exclude:
- spec/lib/controllers_spec.rb
Style/Documentation:
Enabled: false
Style/FormatStringToken:
Exclude:
- lib/cms_scanner/finders/finder.rb
Style/MixinUsage:
Exclude:
- lib/cms_scanner/formatter.rb
# frozen_string_literal: true
if ENV['GITHUB_ACTION']
require 'simplecov-lcov'
SimpleCov::Formatter::LcovFormatter.config do |c|
c.single_report_path = 'coverage/lcov.info'
c.report_with_single_file = true
end
SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
end
SimpleCov.start do
enable_coverage :branch # Only supported for Ruby >= 2.5
add_filter '/example/'
add_filter '/spec/'
add_filter 'helper'
end
# frozen_string_literal: true
source 'https://rubygems.org'
gemspec
Copyright (C) 2014-2015 - WPScanTeam
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# CMSScanner
[![Gem Version](https://badge.fury.io/rb/cms_scanner.svg)](https://badge.fury.io/rb/cms_scanner)
![Build](https://github.com/wpscanteam/CMSScanner/workflows/Build/badge.svg)
[![Coverage Status](https://img.shields.io/coveralls/wpscanteam/CMSScanner.svg)](https://coveralls.io/r/wpscanteam/CMSScanner)
[![Code Climate](https://api.codeclimate.com/v1/badges/b90b7f9f6982792ef8d6/maintainability)](https://codeclimate.com/github/wpscanteam/CMSScanner/maintainability)
The goal of this gem is to provide a quick and easy way to create a CMS/WebSite Scanner by acting like a Framework and providing classes, formatters etc.
## /!\ This gem is currently Experimental /!\
## A basic implementation example is available in the example folder.
To start to play with it, copy all its files and folders into a new git repository and run `bundle install && rake install` inside it.
It will create a `cmsscan` command that you can run against a target, ie `cmsscan --url https://www.google.com`
Install Dependencies: `bundle install`
## Contributing
1. Fork it ( https://github.com/wpscanteam/CMSScanner/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
# frozen_string_literal: true
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rubocop/rake_task'
RuboCop::RakeTask.new
RSpec::Core::RakeTask.new(:spec)
# Run rubocop & rspec before the build
task build: %i[rubocop spec]
# frozen_string_literal: true
# Formatters
require_relative 'formatters/cli'
require_relative 'formatters/cli_no_colour'
require_relative 'formatters/cli_no_color'
require_relative 'formatters/json'
# Controllers
require_relative 'controllers/core'
require_relative 'controllers/interesting_findings'
# Models
require_relative 'models/interesting_finding'
require_relative 'models/robots_txt'
require_relative 'models/fantastico_fileslist'
require_relative 'models/search_replace_db_2'
require_relative 'models/headers'
require_relative 'models/xml_rpc'
require_relative 'models/version'
require_relative 'models/user'
# Finders
require_relative 'finders/interesting_findings'
# frozen_string_literal: true
require_relative 'core/cli_options'
module CMSScanner
module Controller
# Core Controller
class Core < Base
def setup_cache
return unless NS::ParsedCli.cache_dir
storage_path = File.join(NS::ParsedCli.cache_dir, Digest::MD5.hexdigest(target.url))
Typhoeus::Config.cache = Cache::Typhoeus.new(storage_path)
Typhoeus::Config.cache.clean if NS::ParsedCli.clear_cache
end
def before_scan
maybe_output_banner_help_and_version
setup_cache
check_target_availability
end
def maybe_output_banner_help_and_version
output('banner') if NS::ParsedCli.banner
output('help', help: option_parser.simple_help, simple: true) if NS::ParsedCli.help
output('help', help: option_parser.full_help, simple: false) if NS::ParsedCli.hh
output('version') if NS::ParsedCli.version
exit(NS::ExitCode::OK) if NS::ParsedCli.help || NS::ParsedCli.hh || NS::ParsedCli.version
end
# Checks that the target is accessible, raises related errors otherwise
#
# @return [ Void ]
def check_target_availability
res = NS::Browser.get(target.url)
case res.code
when 0
raise Error::TargetDown, res
when 401
raise Error::HTTPAuthRequired
when 403
raise Error::AccessForbidden, NS::ParsedCli.random_user_agent unless NS::ParsedCli.force
when 407
raise Error::ProxyAuthRequired
end
# Checks for redirects
# An out of scope redirect will raise an Error::HTTPRedirect
effective_url = target.homepage_res.effective_url
return if target.in_scope?(effective_url)
raise Error::HTTPRedirect, effective_url unless NS::ParsedCli.ignore_main_redirect
target.homepage_res = res
end
def run
@start_time = Time.now
@start_memory = NS.start_memory
output('started', url: target.url, ip: target.ip, effective_url: target.homepage_url)
end
def after_scan
@stop_time = Time.now
@elapsed = @stop_time - @start_time
@used_memory = GetProcessMem.new.bytes - @start_memory
output('finished',
cached_requests: NS.cached_requests,
requests_done: NS.total_requests,
data_sent: NS.total_data_sent,
data_received: NS.total_data_received)
end
end
end
end
# frozen_string_literal: true
module CMSScanner
module Controller
# CLI Options for the Core Controller
class Core < Base
def cli_options
formats = NS::Formatter.availables
[
OptURL.new(['-u', '--url URL', 'The URL to scan'],
required_unless: %i[help hh version],
default_protocol: 'http'),
OptBoolean.new(['--force', 'Do not check if target returns a 403'])
] + mixed_cli_options + [
OptFilePath.new(['-o', '--output FILE', 'Output to FILE'], writable: true, exists: false),
OptChoice.new(['-f', '--format FORMAT',
'Output results in the format supplied'], choices: formats),
OptChoice.new(['--detection-mode MODE'],
choices: %w[mixed passive aggressive],
normalize: :to_sym,
default: :mixed),
OptArray.new(['--scope DOMAINS',
'Comma separated (sub-)domains to consider in scope. ',
'Wildcard(s) allowed in the trd of valid domains, e.g: *.target.tld'], advanced: true)
] + cli_browser_options
end
def mixed_cli_options
[
OptBoolean.new(['-h', '--help', 'Display the simple help and exit']),
OptBoolean.new(['--hh', 'Display the full help and exit']),
OptBoolean.new(['--version', 'Display the version and exit']),
OptBoolean.new(['--ignore-main-redirect', 'Ignore the main redirect (if any) and scan the target url'],
advanced: true),
OptBoolean.new(['-v', '--verbose', 'Verbose mode']),
OptBoolean.new(['--[no-]banner', 'Whether or not to display the banner'], default: true),
OptPositiveInteger.new(['--max-scan-duration SECONDS',
'Abort the scan if it exceeds the time provided in seconds'],
advanced: true)
]
end
# @return [ Array<OptParseValidator::OptBase> ]
def cli_browser_options
cli_browser_headers_options + [
OptBoolean.new(['--random-user-agent', '--rua',
'Use a random user-agent for each scan']),
OptFilePath.new(['--user-agents-list FILE-PATH',
'List of agents to use with --random-user-agent'],
exists: true,
advanced: true,
default: APP_DIR.join('user_agents.txt')),
OptCredentials.new(['--http-auth login:password']),
OptPositiveInteger.new(['-t', '--max-threads VALUE', 'The max threads to use'],
default: 5),
OptPositiveInteger.new(['--throttle MilliSeconds', 'Milliseconds to wait before doing another web request. ' \
'If used, the max threads will be set to 1.']),
OptPositiveInteger.new(['--request-timeout SECONDS', 'The request timeout in seconds'],
default: 60),
OptPositiveInteger.new(['--connect-timeout SECONDS', 'The connection timeout in seconds'],
default: 30),
OptBoolean.new(['--disable-tls-checks',
'Disables SSL/TLS certificate verification, and downgrade to TLS1.0+ ' \
'(requires cURL 7.66 for the latter)'])
] + cli_browser_proxy_options + cli_browser_cookies_options + cli_browser_cache_options
end
# @return [ Array<OptParseValidator::OptBase> ]
def cli_browser_headers_options
[
OptString.new(['--user-agent VALUE', '--ua']),
OptHeaders.new(['--headers HEADERS', 'Additional headers to append in requests'], advanced: true),
OptString.new(['--vhost VALUE', 'The virtual host (Host header) to use in requests'], advanced: true)
]
end
# @return [ Array<OptParseValidator::OptBase> ]
def cli_browser_proxy_options
[
OptProxy.new(['--proxy protocol://IP:port',
'Supported protocols depend on the cURL installed']),
OptCredentials.new(['--proxy-auth login:password'])
]
end
# @return [ Array<OptParseValidator::OptBase> ]
def cli_browser_cookies_options
[
OptString.new(['--cookie-string COOKIE',
'Cookie string to use in requests, ' \
'format: cookie1=value1[; cookie2=value2]']),
OptFilePath.new(['--cookie-jar FILE-PATH', 'File to read and write cookies'],
writable: true,
readable: true,
create: true,
default: File.join(tmp_directory, 'cookie_jar.txt'))
]
end
# @return [ Array<OptParseValidator::OptBase> ]
def cli_browser_cache_options
[
OptInteger.new(['--cache-ttl TIME_TO_LIVE', 'The cache time to live in seconds'],
default: 600, advanced: true),
OptBoolean.new(['--clear-cache', 'Clear the cache before the scan'], advanced: true),
OptDirectoryPath.new(['--cache-dir PATH'],
readable: true,
writable: true,
create: true,
default: File.join(tmp_directory, 'cache'),
advanced: true)
]
end
end
end
end
# frozen_string_literal: true
module CMSScanner
module Controller
# InterestingFindings Controller
class InterestingFindings < Base
def cli_options
[
OptChoice.new(
['--interesting-findings-detection MODE',
'Use the supplied mode for the interesting findings detection. '],
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
)
]
end
def run
mode = NS::ParsedCli.interesting_findings_detection || NS::ParsedCli.detection_mode
findings = target.interesting_findings(mode: mode)
output('findings', findings: findings) unless findings.empty?
end
end
end
end
# frozen_string_literal: true
require_relative 'interesting_findings/headers'
require_relative 'interesting_findings/robots_txt'
require_relative 'interesting_findings/fantastico_fileslist'
require_relative 'interesting_findings/search_replace_db_2'
require_relative 'interesting_findings/xml_rpc'
module CMSScanner
module Finders
module InterestingFindings
# Interesting Files Finder
class Base
include IndependentFinder
# @param [ CMSScanner::Target ] target
def initialize(target)
%w[Headers RobotsTxt FantasticoFileslist SearchReplaceDB2 XMLRPC].each do |f|
finders << NS::Finders::InterestingFindings.const_get(f).new(target)
end
end
end
end
end
end
# frozen_string_literal: true
module CMSScanner
module Finders
module InterestingFindings
# FantasticoFileslist finder
class FantasticoFileslist < Finder
# @return [ InterestingFinding ]
def aggressive(_opts = {})
path = 'fantastico_fileslist.txt'
res = target.head_and_get(path)
return if res.body.strip.empty?
return unless res.headers && res.headers['Content-Type']&.start_with?('text/plain')
NS::Model::FantasticoFileslist.new(target.url(path), confidence: 70, found_by: found_by)
end
end
end
end
end
# frozen_string_literal: true
module CMSScanner
module Finders
module InterestingFindings
# Interesting Headers finder
class Headers < Finder
# @return [ InterestingFinding ]
def passive(_opts = {})
r = NS::Model::Headers.new(target.homepage_url, confidence: 100, found_by: found_by)
r.interesting_entries.empty? ? nil : r
end
end
end
end
end
# frozen_string_literal: true
module CMSScanner
module Finders
module InterestingFindings
# Robots.txt finder
class RobotsTxt < Finder
# @return [ InterestingFinding ]
def aggressive(_opts = {})
path = 'robots.txt'
res = target.head_and_get(path)
return unless res&.code == 200 && res.body =~ /(?:user-agent|(?:dis)?allow):/i
NS::Model::RobotsTxt.new(target.url(path), confidence: 100, found_by: found_by)
end
end
end
end
end
# frozen_string_literal: true
module CMSScanner
module Finders
module InterestingFindings
# SearchReplaceDB2 finder
class SearchReplaceDB2 < Finder
# @return [ InterestingFinding ]
def aggressive(_opts = {})
path = 'searchreplacedb2.php'
return unless /by interconnect/i.match?(target.head_and_get(path).body)
NS::Model::SearchReplaceDB2.new(target.url(path), confidence: 100, found_by: found_by)
end
end
end
end