From 9d5b961ea8ec28ea8665b548cb01fae831b76a67 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Mon, 4 Dec 2023 17:40:49 -0800 Subject: [PATCH] Add the `ronin-web vulns` command (closes #81). --- Gemfile | 8 + README.md | 2 + gemspec.yml | 2 + lib/ronin/web/cli/commands/vulns.rb | 463 +++++++++++++++++ man/ronin-web-vulns.1.md | 177 +++++++ spec/cli/commands/vulns_spec.rb | 777 ++++++++++++++++++++++++++++ spec/spec_helper.rb | 5 + 7 files changed, 1434 insertions(+) create mode 100644 lib/ronin/web/cli/commands/vulns.rb create mode 100644 man/ronin-web-vulns.1.md create mode 100644 spec/cli/commands/vulns_spec.rb diff --git a/Gemfile b/Gemfile index 9d810163..cefff2a0 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,13 @@ gem 'ronin-web-browser', '~> 0.1', github: 'ronin-rb/ronin-web-browser' gem 'ronin-web-session_cookie', '~> 0.1', github: 'ronin-rb/ronin-web-session_cookie', branch: 'main' +gem 'ronin-db', '~> 0.2', github: 'ronin-rb/ronin-db', + branch: '0.2.0' +gem 'ronin-db-activerecord', '~> 0.2', github: 'ronin-rb/ronin-db-activerecord', + branch: '0.2.0' +gem 'ronin-vulns', '~> 0.2', github: 'ronin-rb/ronin-vulns', + branch: '0.2.0' + group :development do gem 'rake' gem 'rubygems-tasks', '~> 0.1' @@ -45,6 +52,7 @@ group :development do gem 'rspec', '~> 3.0' gem 'simplecov', '~> 0.20' gem 'rack-test', '~> 0.6' + gem 'webmock', '~> 3.0' gem 'kramdown', '~> 2.0' gem 'redcarpet', platform: :mri diff --git a/README.md b/README.md index 2ba15980..fb176280 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Commands: session-cookie spider user-agent + vulns wordlist xml ``` @@ -600,6 +601,7 @@ For more examples, see [ronin-web-browser][ronin-web-browser-examples]. * [ronin-web-user_agents] ~> 0.1 * [ronin-web-session_cookie] ~> 0.1 * [ronin-core] ~> 0.2 +* [ronin-vulns] ~> 0.2 ## Install diff --git a/gemspec.yml b/gemspec.yml index efd4ffd0..6ea3b9b7 100644 --- a/gemspec.yml +++ b/gemspec.yml @@ -38,6 +38,7 @@ generated_files: - man/ronin-web-reverse-proxy.1 - man/ronin-web-session-cookie.1 - man/ronin-web-user-agent.1 + - man/ronin-web-vulns.1 - man/ronin-web-wordlist.1 - man/ronin-web-xml.1 @@ -56,6 +57,7 @@ dependencies: ronin-web-user_agents: ~> 0.1 ronin-web-session_cookie: ~> 0.1 ronin-core: ~> 0.2 + ronin-vulns: ~> 0.2 development_dependencies: bundler: ~> 2.0 diff --git a/lib/ronin/web/cli/commands/vulns.rb b/lib/ronin/web/cli/commands/vulns.rb new file mode 100644 index 00000000..53520a53 --- /dev/null +++ b/lib/ronin/web/cli/commands/vulns.rb @@ -0,0 +1,463 @@ +# frozen_string_literal: true +# +# ronin-web - A collection of useful web helper methods and commands. +# +# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com) +# +# ronin-web is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-web is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ronin-web. If not, see . +# + +require 'ronin/web/cli/command' +require 'ronin/web/cli/spider_options' +require 'ronin/core/cli/logging' +require 'ronin/vulns/url_scanner' +require 'ronin/vulns/cli/printing' +require 'ronin/vulns/cli/importable' + +module Ronin + module Web + class CLI + module Commands + # + # ## Usage + # + # ronin-web vulns [options] {--host HOST | --domain DOMAIN | --site URL} + # + # ## Options + # + # --host HOST Spiders the specific HOST + # --domain DOMAIN Spiders the whole domain + # --site URL Spiders the website, starting at the URL + # --open-timeout SECS Sets the connection open timeout + # --read-timeout SECS Sets the read timeout + # --ssl-timeout SECS Sets the SSL connection timeout + # --continue-timeout SECS Sets the continue timeout + # --keep-alive-timeout SECS Sets the connection keep alive timeout + # -P, --proxy PROXY Sets the proxy to use. + # -H, --header NAME: VALUE Sets a default header + # --host-header NAME=VALUE Sets a default header + # -u chrome-linux|chrome-macos|chrome-windows|chrome-iphone|chrome-ipad|chrome-android|firefox-linux|firefox-macos|firefox-windows|firefox-iphone|firefox-ipad|firefox-android|safari-macos|safari-iphone|safari-ipad|edge, + # --user-agent The User-Agent to use + # -U, --user-agent-string STRING The User-Agent string to use + # -R, --referer URL Sets the Referer URL + # --delay SECS Sets the delay in seconds between each request + # -l, --limit COUNT Only spiders up to COUNT pages + # -d, --max-depth DEPTH Only spiders up to max depth + # --enqueue URL Adds the URL to the queue + # --visited URL Marks the URL as previously visited + # --strip-fragments Enables/disables stripping the fragment component of every URL + # --strip-query Enables/disables stripping the query component of every URL + # --visit-host HOST Visit URLs with the matching host name + # --visit-hosts-like /REGEX/ Visit URLs with hostnames that match the REGEX + # --ignore-host HOST Ignore the host name + # --ignore-hosts-like /REGEX/ Ignore the host names matching the REGEX + # --visit-port PORT Visit URLs with the matching port number + # --visit-ports-like /REGEX/ Visit URLs with port numbers that match the REGEX + # --ignore-port PORT Ignore the port number + # --ignore-ports-like /REGEX/ Ignore the port numbers matching the REGEXP + # --visit-link URL Visit the URL + # --visit-links-like /REGEX/ Visit URLs that match the REGEX + # --ignore-link URL Ignore the URL + # --ignore-links-like /REGEX/ Ignore URLs matching the REGEX + # --visit-ext FILE_EXT Visit URLs with the matching file ext + # --visit-exts-like /REGEX/ Visit URLs with file exts that match the REGEX + # --ignore-ext FILE_EXT Ignore the URLs with the file ext + # --ignore-exts-like /REGEX/ Ignore URLs with file exts matching the REGEX + # -r, --robots Specifies whether to honor robots.txt + # -v, --verbose Enables verbose output + # --lfi-os unix|windows Sets the OS to test for + # --lfi-depth COUNT Sets the directory depth to escape up + # --lfi-filter-bypass null-byte|double-escape|base64|rot13|zlib + # Sets the filter bypass strategy to use + # --rfi-filter-bypass double-encode|suffix-escape|null-byte + # Optional filter-bypass strategy to use + # --rfi-script-lang asp|asp.net|coldfusion|jsp|php|perl + # Explicitly specify the scripting language to test for + # --rfi-test-script-url URL Use an alternative test script URL + # --sqli-escape-quote Escapes quotation marks + # --sqli-escape-parens Escapes parenthesis + # --sqli-terminate Terminates the SQL expression with a -- + # --ssti-test-expr {X*Y | X/Z | X+Y | X-Y} + # Optional numeric test to use + # --open-redirect-url URL Optional test URL to try to redirect to + # + # @since 2.0.0 + # + class Vulns < Command + + include Core::CLI::Logging + include SpiderOptions + include Ronin::Vulns::CLI::Printing + include Ronin::Vulns::CLI::Importable + + option :first, short: '-F', + desc: 'Stops spidering once the first vulnerability is found' do + @scan_mode = :first + end + + option :all, short: '-A', + desc: 'Spiders every URL and tests every param' do + @scan_mode = :all + end + + option :print_curl, desc: 'Also prints an example curl command for each vulnerability' + + option :print_http, desc: 'Also prints an example HTTP request for each vulnerability' + + option :import, desc: 'Imports discovered vulnerabilities into the database' + + option :lfi_os, value: { + type: [:unix, :windows] + }, + desc: 'Sets the OS to test for' do |os| + lfi_kwargs[:os] = os + end + + option :lfi_depth, value: { + type: Integer, + usage: 'COUNT' + }, + desc: 'Sets the directory depth to escape up' do |depth| + lfi_kwargs[:depth] = depth + end + + option :lfi_filter_bypass, value: { + type: { + 'null-byte' => :null_byte, + 'double-escape' => :double_escape, + 'base64' => :base64, + 'rot13' => :rot13, + 'zlib' => :zlib + } + }, + desc: 'Sets the filter bypass strategy to use' do |filter_bypass| + lfi_kwargs[:filter_bypass] = filter_bypass + end + + option :rfi_filter_bypass, value: { + type: { + 'double-encode' => :double_encode, + 'suffix-escape' => :suffix_escape, + 'null-byte' => :null_byte + } + }, + desc: 'Optional filter-bypass strategy to use' do |filter_bypass| + rfi_kwargs[:filter_bypass] = filter_bypass + end + + option :rfi_script_lang, value: { + type: { + 'asp' => :asp, + 'asp.net' => :asp_net, + 'coldfusion' => :cold_fusion, + 'jsp' => :jsp, + 'php' => :php, + 'perl' => :perl + } + }, + desc: 'Explicitly specify the scripting language to test for' do |script_lang| + rfi_kwargs[:script_lang] = script_lang + end + + option :rfi_test_script_url, value: { + type: String, + usage: 'URL' + }, + desc: 'Use an alternative test script URL' do |test_script_url| + rfi_kwargs[:test_script_url] = test_script_url + end + + option :sqli_escape_quote, desc: 'Escapes quotation marks' do + sqli_kwargs[:escape_quote] = true + end + + option :sqli_escape_parens, desc: 'Escapes parenthesis' do + sqli_kwargs[:escape_parens] = true + end + + option :sqli_terminate, desc: 'Terminates the SQL expression with a --' do + sqli_kwargs[:terminate] = true + end + + option :ssti_test_expr, value: { + type: %r{\A\d+\s*[\*/\+\-]\s*\d+\z}, + usage: '{X*Y | X/Z | X+Y | X-Y}' + }, + desc: 'Optional numeric test to use' do |expr| + ssti_kwargs[:test_expr] = Ronin::Vulns::SSTI::TestExpression.parse(expr) + end + + option :open_redirect_url, value: { + type: String, + usage: 'URL' + }, + desc: 'Optional test URL to try to redirect to' do |test_url| + open_redirect_kwargs[:test_url] = test_url + end + + description "Spiders a website and scans every URL for web vulnerabilities" + + man_page 'ronin-web-vulns.1' + + # The scan mode + # + # @return [:first, :all] + attr_reader :scan_mode + + # Keyword arguments for `Ronin::Vulns::URLScanner.scan`. + # + # @return [Hash{Symbol => Object}] + attr_reader :scan_kwargs + + # + # Initializes the `ronin-web vulns` command. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments. + # + def initialize(**kwargs) + super(**kwargs) + + @scan_mode = :all + @scan_kwargs = {} + end + + # + # Runs the `ronin-web vulns` command. + # + def run + db_connect if options[:import] + + vulns = [] + + begin + new_agent do |agent| + case @scan_mode + when :first + agent.every_url do |url| + log_info "Testing #{url}" + + if (vuln = test_url(url)) + process_vuln(vuln) + vulns << vuln + + agent.pause! + end + end + when :all + agent.every_url do |url| + log_info "Testing #{url}" + + scan_url(url) do |vuln| + process_vuln(vuln) + vulns << vuln + end + end + end + end + rescue Interrupt + puts + end + + puts unless vulns.empty? + print_vulns(vulns) + end + + # + # Logs and optioanlly imports a new discovered web vulnerability. + # + # @param [Ronin::Vulns::WebVuln] vuln + # The discovered web vulnerability. + # + def process_vuln(vuln) + log_vuln(vuln) + import_vuln(vuln) if options[:import] + end + + # + # Prints detailed information about a discovered web vulnerability. + # + # @param [Array] vulns + # The web vulnerability to log. + # + # @param [Boolean] print_curl + # Prints an example `curl` command to trigger the web vulnerability. + # + # @param [Boolean] print_http + # Prints an example HTTP request to trigger the web vulnerability. + # + def print_vulns(vulns, print_curl: options[:print_curl], + print_http: options[:print_http]) + super(vulns, print_curl: print_curl, print_http: print_http) + end + + # + # The default headers to send with every request. + # + # @return [Hash{String => String}] + # + # @since 2.0.0 + # + def default_headers + @scan_kwargs[:headers] ||= super + end + + # + # Sets the `User-Agent` header that will be sent with every request. + # + # @param [String] new_user_agent + # + # @return [String] + # + def user_agent=(new_user_agent) + @scan_kwargs[:user_agent] ||= super(new_user_agent) + end + + # + # Sets the `Referer` header that will be sent with every request. + # + # @param [String] new_referer + # + # @return [String, nil] + # + # @note + # Also sets the `Referer` header that will be used during web + # vulnerability scanning. + # + def referer=(new_referer) + @scan_kwargs[:referer] ||= super(new_referer) + end + + # + # @group URL Scanning Methods + # + + # + # Keyword arguments which will be passed to + # `Ronin::Vulns::URLScanner.scan` or `Ronin::Vulns::URLScanner.test` + # via the `lfi:` keyword. + # + # @return [Hash{Symbol => Object}] + # + def lfi_kwargs + @scan_kwargs[:lfi] ||= {} + end + + # + # Keyword arguments which will be passed to + # `Ronin::Vulns::URLScanner.scan` or `Ronin::Vulns::URLScanner.test` + # via the `rfi:` keyword. + # + # @return [Hash{Symbol => Object}] + # + def rfi_kwargs + @scan_kwargs[:rfi] ||= {} + end + + # + # Keyword arguments which will be passed to + # `Ronin::Vulns::URLScanner.scan` or `Ronin::Vulns::URLScanner.test` + # via the `sqli:` keyword. + # + # @return [Hash{Symbol => Object}] + # + def sqli_kwargs + @scan_kwargs[:sqli] ||= {} + end + + # + # Keyword arguments which will be passed to + # `Ronin::Vulns::URLScanner.scan` or `Ronin::Vulns::URLScanner.test` + # via the `ssti:` keyword. + # + # @return [Hash{Symbol => Object}] + # + def ssti_kwargs + @scan_kwargs[:ssti] ||= {} + end + + # + # Keyword arguments which will be passed to + # `Ronin::Vulns::URLScanner.scan` or `Ronin::Vulns::URLScanner.test` + # via the `open_redirect:` keyword. + # + # @return [Hash{Symbol => Object}] + # + def open_redirect_kwargs + @scan_kwargs[:open_redirect] ||= {} + end + + # + # Keyword arguments which will be passed to + # `Ronin::Vulns::URLScanner.scan` or `Ronin::Vulns::URLScanner.test` + # via the `reflected_xss:` keyword. + # + # @return [Hash{Symbol => Object}] + # + def reflected_xss_kwargs + @scan_kwargs[:reflected_xss] ||= {} + end + + # + # Scans the URL for web vulnerabilities. + # + # @param [URI::HTTP, String] url + # The URL to scan. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for `Ronin::Vulns::URLScanner.scan`. + # + # @yield [vuln] + # The given block will be yielded each discovered web vulnerability. + # + # @yieldparam [Ronin::Vulns::LFI, + # Ronin::Vulns::RFI, + # Ronin::Vulns::SQLI, + # Ronin::Vulns::SSTI, + # Ronin::Vulns::ReflectedXSS, + # Ronin::Vulns::OpenRedirect] vuln + # A discovered web vulnerability in the URL. + # + def scan_url(url,**kwargs,&block) + Ronin::Vulns::URLScanner.scan(url,**kwargs,**@scan_kwargs,&block) + end + + # + # Tests the URL for web vulnerabilities and prints the first + # vulnerability. + # + # @param [URI::HTTP, String] url + # The URL to scan. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for `Ronin::Vulns::URLScanner.test`. + # + # @return [Ronin::Vulns::LFI, + # Ronin::Vulns::RFI, + # Ronin::Vulns::SQLI, + # Ronin::Vulns::SSTI, + # Ronin::Vulns::ReflectedXSS, + # Ronin::Vulns::OpenRedirect, nil] + # The first discovered web vulnerability or `nil` if no + # vulnerabilities were discovered. + # + def test_url(url,**kwargs) + Ronin::Vulns::URLScanner.test(url,**kwargs,**@scan_kwargs) + end + + end + end + end + end +end diff --git a/man/ronin-web-vulns.1.md b/man/ronin-web-vulns.1.md new file mode 100644 index 00000000..0c459d4a --- /dev/null +++ b/man/ronin-web-vulns.1.md @@ -0,0 +1,177 @@ +# ronin-web-spider 1 "2022-01-01" Ronin Web "User Manuals" + +## SYNOPSIS + +`ronin-web spider` [*options*] {`--host` *HOST* \| `--domain` *DOMAIN* \| `--site` *URL*} + +## DESCRIPTION + +Spiders a website and tests every URL for web vulnerabilities. + +## OPTIONS + +`--host` *HOST* + Spiders the specific *HOST*. + +`--domain` *DOMAIN* + Spiders the whole *DOMAIN*. + +`--site` *URL* + Spiders the website, starting at the *URL*. + +`--open-timeout` *SECS* + Sets the connection open timeout. + +`--read-timeout` *SECS* + Sets the read timeout. + +`--ssl-timeout` *SECS* + Sets the SSL connection timeout. + +`--continue-timeout` *SECS* + Sets the continue timeout. + +`--keep-alive-timeout` *SECS* + Sets the connection keep alive timeout. + +`-P`, `--proxy` *PROXY* + Sets the proxy to use. + +`-H`, `--header` "*NAME*: *VALUE*" + Sets a default header. + +`--host-header` *NAME*=*VALUE* + Sets a default header. + +`-u`, `--user-agent` chrome-linux|chrome-macos|chrome-windows|chrome-iphone|chrome-ipad|chrome-android|firefox-linux|firefox-macos|firefox-windows|firefox-iphone|firefox-ipad|firefox-android|safari-macos|safari-iphone|safari-ipad|edge + The `User-Agent` to use. + +`-U`, `--user-agent-string` *STRING* + The raw `User-Agent` string to use. + +`-R`, `--referer` *URL* + Sets the `Referer` URL. + +`--delay` *SECS* + Sets the delay in seconds between each request. + +`-l`, `--limit` *COUNT* + Only spiders up to *COUNT* pages. + +`-d`, `--max-depth` *DEPTH* + Only spiders up to max depth. + +`--enqueue` *URL* + Adds the URL to the queue. + +`--visited` *URL* + Marks the URL as previously visited. + +`--strip-fragments` + Enables/disables stripping the fragment component of every URL. + +`--strip-query` + Enables/disables stripping the query component of every URL. + +`--visit-host` *HOST* + Visit URLs with the matching host name. + +`--visit-hosts-like` `/`*REGEX*`/` + Visit URLs with hostnames that match the *REGEX*. + +`--ignore-host` *HOST* + Ignore the host name. + +`--ignore-hosts-like` `/`*REGEX*`/` + Ignore the host names matching the *REGEX*. + +`--visit-port` *PORT* + Visit URLs with the matching port number. + +`--visit-ports-like` `/`*REGEX*`/` + Visit URLs with port numbers that match the *REGEX*. + +`--ignore-port` *PORT* + Ignore the port number. + +`--ignore-ports-like` `/`*REGEX*`/` + Ignore the port numbers matching the *REGEXP*. + +`--visit-link` *URL* + Visit the *URL*. + +`--visit-links-like` `/`*REGEX*`/` + Visit URLs that match the *REGEX*. + +`--ignore-link` *URL* + Ignore the *URL*. + +`--ignore-links-like` `/`*REGEX*`/` + Ignore URLs matching the *REGEX*. + +`--visit-ext` *FILE_EXT* + Visit URLs with the matching file ext. + +`--visit-exts-like` `/`*REGEX*`/` + Visit URLs with file exts that match the *REGEX*. + +`--ignore-ext` *FILE_EXT* + Ignore the URLs with the file ext. + +`--ignore-exts-like` `/`*REGEX*`/` + Ignore URLs with file exts matching the REGEX. + +`-r`, `--robots` + Specifies whether to honor `robots.txt`. + +`--lfi-os` `unix`\|`windows` +: Sets the OS to test for. + +`--lfi-depth` *NUM* +: Sets the directory depth to escape up. + +`--lfi-filter-bypass` `null-byte`\|`double-escape`\|`base64`\|`rot13`\|`zlib` +: Sets the filter bypass strategy to use. + +`--rfi-filter-bypass` `double-encode`\|`suffix-escape`\|`null-byte` +: Optional filter-bypass strategy to use. + +`--rfi-script-lang` `asp`\|`asp.net`\|`coldfusion`\|`jsp`\|`php`\|`perl` +: Explicitly specify the scripting language to test for. + +`--rfi-test-script-url` *URL* +: Use an alternative test script URL. + +`--sqli-escape-quote` +: Escapes quotation marks. + +`--sqli-escape-parens` +: Escapes parenthesis. + +`--sqli-terminate` +: Terminates the SQL expression with a `--`. + +`--ssti-test-expr` {*X*\**Y* \| *X*/*Z* \| *X*+*Y* \| *X*-*Y*} +: Optional numeric test to use. + +`--open-redirect-url` *URL* +: Optional test URL to try to redirect to. + +`-h`, `--help` + Print help information. + +## ENVIRONMENT + +*HTTP_PROXY* + Sets the global HTTP proxy. + +*RONIN_HTTP_PROXY* + Sets the HTTP proxy for Ronin. + +## AUTHOR + +Postmodern + +## SEE ALSO + +ronin-web-spider(1) diff --git a/spec/cli/commands/vulns_spec.rb b/spec/cli/commands/vulns_spec.rb new file mode 100644 index 00000000..1800838e --- /dev/null +++ b/spec/cli/commands/vulns_spec.rb @@ -0,0 +1,777 @@ +require 'spec_helper' +require 'ronin/web/cli/commands/vulns' +require_relative 'man_page_example' + +require 'sinatra/base' +require 'webmock/rspec' + +describe Ronin::Web::CLI::Commands::Vulns do + include_examples "man_page" + + describe "options" do + context "when the --header option is given" do + let(:header_name1) { 'X-Foo' } + let(:header_value1) { 'foo' } + let(:header1) { "#{header_name1}: #{header_value1}" } + + let(:header_name2) { 'X-Bar' } + let(:header_value2) { 'bar' } + let(:header2) { "#{header_name2}: #{header_value2}" } + + before do + subject.option_parser.parse(['--header', header1, '--header', header2]) + end + + it "must set :default_headers in #agent_kwargs" do + expect(subject.agent_kwargs[:default_headers]).to eq( + { + header_name1 => header_value1, + header_name2 => header_value2 + } + ) + end + + it "must set :headers in the #scan_kwargs" do + expect(subject.scan_kwargs[:headers]).to eq( + { + header_name1 => header_value1, + header_name2 => header_value2 + } + ) + end + end + + context "when the --user-agent-string option is given" do + let(:user_agent) { 'Foo Bot' } + + before do + subject.option_parser.parse(['--user-agent-string', user_agent]) + end + + it "must set :user_agent in #agent_kwargs" do + expect(subject.agent_kwargs[:user_agent]).to eq(user_agent) + end + + it "must set :user_agent in #scan_kwargs" do + expect(subject.scan_kwargs[:user_agent]).to eq(user_agent) + end + end + + context "when the --user-agent option is given" do + Ronin::Support::Network::HTTP::UserAgents::ALIASES.transform_keys { |key| + key.to_s.tr('_','-') + }.each do |user_agent_alias,user_agent_string| + context "and the value is '#{user_agent_alias}'" do + let(:user_agent) { user_agent_string } + + before do + subject.option_parser.parse(['--user-agent', user_agent_alias]) + end + + it "must set :user_agent in #agent_kwargs to '#{user_agent_string}'" do + expect(subject.agent_kwargs[:user_agent]).to eq(user_agent_string) + end + + it "must set :user_agent in #scan_kwargs to '#{user_agent_string}'" do + expect(subject.scan_kwargs[:user_agent]).to eq(user_agent_string) + end + end + end + end + + context "when the --referer option is given" do + let(:referer) { 'http://example.com/page' } + + before do + subject.option_parser.parse(['--referer', referer]) + end + + it "must set :referer in #agent_kwargs" do + expect(subject.agent_kwargs[:referer]).to eq(referer) + end + + it "must set :referer in #scan_kwargs" do + expect(subject.scan_kwargs[:referer]).to eq(referer) + end + end + + context "when the '--lfi-os' option is given" do + let(:os) { :windows } + let(:argv) { ['--lfi-os', os.to_s] } + + before { subject.option_parser.parse(argv) } + + it "must set the :os key in #lfi_kwargs" do + expect(subject.lfi_kwargs[:os]).to eq(os) + end + end + + context "when the '--lfi-depth' option is given" do + let(:depth) { 9 } + let(:argv) { ['--lfi-depth', depth.to_s] } + + before { subject.option_parser.parse(argv) } + + it "must set the :depth key in the Hash" do + expect(subject.lfi_kwargs[:depth]).to eq(depth) + end + end + + context "when the '--lfi-filter-bypass' option is given" do + let(:argv) { ['--lfi-filter-bypass', option_value] } + + before { subject.option_parser.parse(argv) } + + context "and it's value is 'null-byte'" do + let(:option_value) { 'null-byte' } + let(:filter_bypass) { :null_byte } + + it "must set the :filter_bypass key in #lfi_kwargs to :null_byte" do + expect(subject.lfi_kwargs[:filter_bypass]).to eq(filter_bypass) + end + end + + context "and it's value is 'double-escape'" do + let(:option_value) { 'double-escape' } + let(:filter_bypass) { :double_escape } + + it "must set the :filter_bypass key in #lfi_kwargs to :double_escape" do + expect(subject.lfi_kwargs[:filter_bypass]).to eq(filter_bypass) + end + end + + context "and it's value is 'base64'" do + let(:option_value) { 'base64' } + let(:filter_bypass) { :base64 } + + it "must set the :filter_bypass key in #lfi_kwargs to :base64" do + expect(subject.lfi_kwargs[:filter_bypass]).to eq(filter_bypass) + end + end + + context "and it's value is 'rot13'" do + let(:option_value) { 'rot13' } + let(:filter_bypass) { :rot13 } + + it "must set the :filter_bypass key in #lfi_kwargs to :rot13" do + expect(subject.lfi_kwargs[:filter_bypass]).to eq(filter_bypass) + end + end + + context "and it's value is 'zlib'" do + let(:option_value) { 'zlib' } + let(:filter_bypass) { :zlib } + + it "must set the :filter_bypass key in #lfi_kwargs" do + expect(subject.lfi_kwargs[:filter_bypass]).to eq(filter_bypass) + end + end + end + + context "when the '--rfi-filter-bypass' option is given" do + let(:argv) { ['--rfi-filter-bypass', option_value] } + + before { subject.option_parser.parse(argv) } + + context "when the option value is 'double-encode'" do + let(:option_value) { 'double-encode' } + let(:filter_bypass) { :double_encode } + + it "must set the :filter_bypass key in the #rfi_kwargs to :double_encode" do + expect(subject.rfi_kwargs[:filter_bypass]).to eq(filter_bypass) + end + end + + context "when the option value is 'suffix-escape'" do + let(:option_value) { 'suffix-escape' } + let(:filter_bypass) { :suffix_escape } + + it "must set the :filter_bypass key in the #rfi_kwargs to :suffix_escape" do + expect(subject.rfi_kwargs[:filter_bypass]).to eq(filter_bypass) + end + end + + context "when the option value is 'null-byte'" do + let(:option_value) { 'null-byte' } + let(:filter_bypass) { :null_byte } + + it "must set the :filter_bypass key in the #rfi_kwargs to :null_byte" do + expect(subject.rfi_kwargs[:filter_bypass]).to eq(filter_bypass) + end + end + end + + context "when the '--rfi-script-lang' option is given" do + let(:argv) { ['--rfi-script-lang', option_value] } + + before { subject.option_parser.parse(argv) } + + context "when the option value is 'asp'" do + let(:option_value) { 'asp' } + let(:script_lang) { :asp } + + it "must set the :script_lang key in #rfi_kwargs to :asp" do + expect(subject.rfi_kwargs[:script_lang]).to eq(script_lang) + end + end + + context "when the option value is 'asp.net'" do + let(:option_value) { 'asp.net' } + let(:script_lang) { :asp_net } + + it "must set the :script_lang key in #rfi_kwargs to :asp_net" do + expect(subject.rfi_kwargs[:script_lang]).to eq(script_lang) + end + end + + context "when the option value is 'coldfusion'" do + let(:option_value) { 'coldfusion' } + let(:script_lang) { :cold_fusion } + + it "must set the :script_lang key in #rfi_kwargs to :cold_fusion" do + expect(subject.rfi_kwargs[:script_lang]).to eq(script_lang) + end + end + + context "when the option value is 'jsp'" do + let(:option_value) { 'jsp' } + let(:script_lang) { :jsp } + + it "must set the :script_lang key in #rfi_kwargs to :jsp" do + expect(subject.rfi_kwargs[:script_lang]).to eq(script_lang) + end + end + + context "when the option value is 'php'" do + let(:option_value) { 'php' } + let(:script_lang) { :php } + + it "must set the :script_lang key in #rfi_kwargs to :php" do + expect(subject.rfi_kwargs[:script_lang]).to eq(script_lang) + end + end + + context "when the option value is 'perl'" do + let(:option_value) { 'perl' } + let(:script_lang) { :perl } + + it "must set the :script_lang key in #rfi_kwargs to :perl" do + expect(subject.rfi_kwargs[:script_lang]).to eq(script_lang) + end + end + end + + context "when the '--rfi-test-script-url' option is given" do + let(:test_script_url) { 'https://other-website.com/path/to/rfi_test.php' } + let(:argv) { ['--rfi-test-script-url', test_script_url] } + + before { subject.option_parser.parse(argv) } + + it "must set the :test_script_url key in the Hash" do + expect(subject.rfi_kwargs[:test_script_url]).to eq(test_script_url) + end + end + + context "when the '--sqli-escape-quote' option is given" do + let(:argv) { %w[--sqli-escape-quote] } + + before { subject.option_parser.parse(argv) } + + it "must set the :escape_quote key in the Hash" do + expect(subject.sqli_kwargs[:escape_quote]).to be(true) + end + end + + context "when the '--sqli-escape-parens' option is given" do + let(:argv) { %w[--sqli-escape-parens] } + + before { subject.option_parser.parse(argv) } + + it "must set the :escape_parens key in the Hash" do + expect(subject.sqli_kwargs[:escape_parens]).to be(true) + end + end + + context "when the '--sqli-terminate' option is given" do + let(:argv) { %w[--sqli-terminate] } + + before { subject.option_parser.parse(argv) } + + it "must set the :terminate key in the Hash" do + expect(subject.sqli_kwargs[:terminate]).to be(true) + end + end + + context "when the '--ssti-test-expr' option is given" do + let(:test) { '7*7' } + let(:argv) { ['--ssti-test-expr', test] } + + before { subject.option_parser.parse(argv) } + + it "must set the :test_expr key in the Hash" do + kwargs = subject.ssti_kwargs + + expect(kwargs[:test_expr]).to be_kind_of(Ronin::Vulns::SSTI::TestExpression) + expect(kwargs[:test_expr].string).to eq(test) + end + end + + context "when the '--open-redirect-url' option is given" do + let(:test_url) { 'https://example.com/test' } + let(:argv) { ['--open-redirect-url', test_url] } + + before { subject.option_parser.parse(argv) } + + it "must set the :test_url key in the Hash" do + expect(subject.open_redirect_kwargs[:test_url]).to eq(test_url) + end + end + end + + describe "#initialize" do + it "must default #scan_mode to :all" do + expect(subject.scan_mode).to eq(:all) + end + + it "must default #scan_kwargs to {}" do + expect(subject.scan_kwargs).to eq({}) + end + end + + describe "#run" do + module TestVulnsCommand + class App < Sinatra::Base + + set :host, 'example.com' + set :port, 80 + + get '/' do + <<~HTML + + + + + + HTML + end + + get '/page1' do + <<~HTML + + +

Page 1

+ +

Foo bar baz

+ + + HTML + end + + get '/page2' do + <<~HTML + + +

Page 2

+ +

Foo bar baz

+ + + HTML + end + + end + end + + let(:host) { 'example.com' } + let(:app) { TestVulnsCommand::App } + + before do + stub_request(:get, /#{Regexp.escape(host)}/).to_rack(app) + + subject.option_parser.parse(['--host', host]) + end + + it "must spider the website and test every URL" do + expect { + subject.run + }.to output( + <<~OUTPUT + >>> Testing http://#{host}/ + >>> Testing http://#{host}/page1?id=1 + >>> Testing http://#{host}/page2?q=foo + No vulnerabilities found + OUTPUT + ).to_stdout + + expect(WebMock).to have_requested(:get, "http://#{host}/") + expect(WebMock).to have_requested(:get, "http://#{host}/page1?id=1") + expect(WebMock).to have_requested(:get, "http://#{host}/page2?q=foo") + end + + context "when one of the URLs is vulnerable" do + module TestVulnsCommand + class VulnApp < Sinatra::Base + + set :host, 'example.com' + set :port, 80 + + get '/' do + <<~HTML + + + + + + HTML + end + + get '/page1' do + if params[:id] =~ /\A1'\) OR \d+=\d+--\z/ + <<~HTML + + +
    +
  • entry 1
  • +
  • entry 2
  • +
  • entry 3
  • +
+ + + HTML + else + <<~HTML + + +
    +
  • entry 1
  • +
+ + + HTML + end + end + + get '/page2' do + if params[:q] =~ %r{\A(\.\./){3,}etc/passwd\z} + <<~HTML + + +

Page 2

+ + root:x:0:0:Super User:/root:/bin/bash + + + HTML + else + <<~HTML + + +

Page 2

+ +

Foo bar baz

+ + + HTML + end + end + + end + end + + let(:app) { TestVulnsCommand::VulnApp } + + it "must print the vulnerabilities that are found" do + expect { + subject.run + }.to output( + <<~OUTPUT + >>> Testing http://#{host}/ + >>> Testing http://#{host}/page1?id=1 + /!\\ Found SQLi on http://#{host}/page1?id=1 via query param 'id'! + >>> Testing http://#{host}/page2?q=foo + /!\\ Found LFI on http://example.com/page2?q=foo via query param 'q'! + + Vulnerabilities found! + + SQLi on http://#{host}/page1?id=1 via query param 'id' + LFI on http://example.com/page2?q=foo via query param 'q' + + OUTPUT + ).to_stdout + + expect(WebMock).to have_requested(:get, "http://#{host}/") + expect(WebMock).to have_requested(:get, "http://#{host}/page1?id=1") + expect(WebMock).to have_requested(:get, "http://#{host}/page2?q=foo") + end + + context "but the --first option is given" do + before { subject.option_parser.parse(%w[--first]) } + + it "must stop spidering once the first vulnerability is found" do + expect { + subject.run + }.to output( + <<~OUTPUT + >>> Testing http://#{host}/ + >>> Testing http://#{host}/page1?id=1 + /!\\ Found SQLi on http://#{host}/page1?id=1 via query param 'id'! + + Vulnerabilities found! + + SQLi on http://#{host}/page1?id=1 via query param 'id' + + OUTPUT + ).to_stdout + + expect(WebMock).to have_requested(:get, "http://#{host}/") + end + end + end + end + + describe "#scan_kwargs" do + it "must default to an empty Hash" do + expect(subject.scan_kwargs).to eq({}) + end + end + + describe "#lfi_kwargs" do + it "must default to an empty Hash" do + expect(subject.lfi_kwargs).to eq({}) + end + + it "must also set :lfi in scan_kwargs to #lfi_kwargs" do + subject.lfi_kwargs + + expect(subject.scan_kwargs[:lfi]).to be(subject.lfi_kwargs) + end + end + + describe "#rfi_kwargs" do + it "must default to an empty Hash" do + expect(subject.rfi_kwargs).to eq({}) + end + + it "must also set :rfi in scan_kwargs to #rfi_kwargs" do + subject.rfi_kwargs + + expect(subject.scan_kwargs[:rfi]).to be(subject.rfi_kwargs) + end + end + + describe "#sqli_kwargs" do + it "must default to an empty Hash" do + expect(subject.sqli_kwargs).to eq({}) + end + + it "must also set :sqli in scan_kwargs to #sqli_kwargs" do + subject.sqli_kwargs + + expect(subject.scan_kwargs[:sqli]).to be(subject.sqli_kwargs) + end + end + + describe "#ssti_kwargs" do + it "must default to an empty Hash" do + expect(subject.ssti_kwargs).to eq({}) + end + + it "must also set :ssti in scan_kwargs to #ssti_kwargs" do + subject.ssti_kwargs + + expect(subject.scan_kwargs[:ssti]).to be(subject.ssti_kwargs) + end + end + + describe "#open_redirect_kwargs" do + it "must default to an empty Hash" do + expect(subject.open_redirect_kwargs).to eq({}) + end + + it "must also set :open_redirect in scan_kwargs to #open_redirect_kwargs" do + subject.open_redirect_kwargs + + expect(subject.scan_kwargs[:open_redirect]).to be(subject.open_redirect_kwargs) + end + end + + describe "#reflected_xss_kwargs" do + it "must return an empty Hash by default" do + expect(subject.reflected_xss_kwargs).to eq({}) + end + + it "must also set :reflected_xss in scan_kwargs to #reflected_xss_kwargs" do + subject.reflected_xss_kwargs + + expect(subject.scan_kwargs[:reflected_xss]).to be(subject.reflected_xss_kwargs) + end + end + + describe "#process_vuln" do + let(:vuln) { double('WebVuln object') } + + it "must call #log_vuln with the given vuln object" do + expect(subject).to receive(:log_vuln).with(vuln) + + subject.process_vuln(vuln) + end + + context "when options[:import] is true" do + before { subject.options[:import] = true } + + it "must call #log_vuln and then #import_vuln with the vuln object" do + expect(subject).to receive(:log_vuln).with(vuln) + expect(subject).to receive(:import_vuln).with(vuln) + + subject.process_vuln(vuln) + end + end + end + + describe "#default_headers" do + it "must return a Hash" do + expect(subject.default_headers).to eq({}) + end + + before { subject.default_headers } + + it "must set :default_headers in #agent_kwargs to an empty Hash" do + expect(subject.agent_kwargs[:default_headers]).to be(subject.default_headers) + end + + it "must set :headers in #scan_kwargs to an empty Hash" do + subject.default_headers + + expect(subject.scan_kwargs[:headers]).to be(subject.default_headers) + expect(subject.agent_kwargs[:default_headers]).to be(subject.default_headers) + end + end + + describe "#user_agent=" do + let(:user_agent) { 'Foo Bar' } + + before { subject.user_agent = user_agent } + + it "must set :user_agent in #agent_kwargs" do + expect(subject.agent_kwargs[:user_agent]).to eq(user_agent) + end + + it "must set :user_agent in #scan_kwargs" do + expect(subject.scan_kwargs[:user_agent]).to eq(user_agent) + end + end + + describe "#referer=" do + let(:referer) { 'https://example.com/' } + + before { subject.referer = referer } + + it "must set :referer in #agent_kwargs" do + expect(subject.agent_kwargs[:referer]).to eq(referer) + end + + it "must set :referer in #scan_kwargs" do + expect(subject.scan_kwargs[:referer]).to eq(referer) + end + end + + describe "#lfi_kwargs" do + it "must return a Hash" do + expect(subject.lfi_kwargs).to eq({}) + end + + it "must set :lfi_kwargs in #scan_kwargs to an empty Hash" do + subject.lfi_kwargs + + expect(subject.scan_kwargs[:lfi]).to eq({}) + end + end + + describe "#rfi_kwargs" do + it "must return a Hash" do + expect(subject.rfi_kwargs).to eq({}) + end + + it "must set :rfi_kwargs in #scan_kwargs to an empty Hash" do + subject.rfi_kwargs + + expect(subject.scan_kwargs[:rfi]).to eq({}) + end + end + + describe "#sqli_kwargs" do + it "must return a Hash" do + expect(subject.sqli_kwargs).to eq({}) + end + + it "must set :sqli_kwargs in #scan_kwargs to an empty Hash" do + subject.sqli_kwargs + + expect(subject.scan_kwargs[:sqli]).to eq({}) + end + end + + describe "#ssti_kwargs" do + it "must return a Hash" do + expect(subject.ssti_kwargs).to eq({}) + end + + it "must set :ssti_kwargs in #scan_kwargs to an empty Hash" do + subject.ssti_kwargs + + expect(subject.scan_kwargs[:ssti]).to eq({}) + end + end + + describe "#open_redirect_kwargs" do + it "must return a Hash" do + expect(subject.open_redirect_kwargs).to eq({}) + end + + it "must set :open_redirect_kwargs in #scan_kwargs to an empty Hash" do + subject.open_redirect_kwargs + + expect(subject.scan_kwargs[:open_redirect]).to eq({}) + end + end + + describe "#reflected_xss_kwargs" do + it "must return a Hash" do + expect(subject.reflected_xss_kwargs).to eq({}) + end + + it "must set :reflected_xss_kwargs in #scan_kwargs to an empty Hash" do + subject.reflected_xss_kwargs + + expect(subject.scan_kwargs[:reflected_xss]).to eq({}) + end + end + + let(:url) { 'https://example.com/page.php?id=1' } + + describe "#scan_url" do + it "must call Ronin::Vulns::URLScanner.scan with the URL and #scan_kwargs" do + expect(Ronin::Vulns::URLScanner).to receive(:scan).with( + url, **subject.scan_kwargs + ) + + subject.scan_url(url) + end + end + + describe "#test_url" do + it "must call Ronin::Vulns::URLScanner.scan with the URL and #scan_kwargs" do + expect(Ronin::Vulns::URLScanner).to receive(:test).with( + url, **subject.scan_kwargs + ) + + subject.test_url(url) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 777b2759..e93cd8b8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,13 @@ require 'rspec' require 'simplecov' +require 'webmock/rspec' SimpleCov.start RSpec.configure do |specs| specs.filter_run_excluding :network + + specs.before(:suite) do + WebMock.disable_net_connect!(allow_localhost: true) + end end