# frozen_string_literal: true # # ronin-vulns - A Ruby library for blind vulnerability testing. # # Copyright (c) 2022-2024 Hal Brodigan (postmodern.mod3 at gmail.com) # # ronin-vulns is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ronin-vulns 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ronin-vulns. If not, see . # require 'ronin/vulns/cli/command' require 'ronin/vulns/cli/importable' require 'ronin/vulns/cli/printing' require 'ronin/support/network/http/cookie' require 'ronin/support/network/http/user_agents' require 'set' module Ronin module Vulns class CLI # # Base class for all web vulnerability commands. # class WebVulnCommand < Command include Printing include Importable option :import, desc: 'Imports discovered vulnerabilities into the database' option :first, short: '-F', desc: 'Only find the first vulnerability for each URL' do @scan_mode = :first end option :all, short: '-A', desc: 'Find all vulnerabilities for each URL' 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 :request_method, short: '-M', value: { type: { 'COPY' => :copy, 'DELETE' => :delete, 'GET' => :get, 'HEAD' => :head, 'LOCK' => :lock, 'MKCOL' => :mkcol, 'MOVE' => :move, 'OPTIONS' => :options, 'PATCH' => :patch, 'POST' => :post, 'PROPFIND' => :propfind, 'PROPPATCH' => :proppatch, 'PUT' => :put, 'TRACE' => :trace, 'UNLOCK' => :unlock } }, desc: 'The HTTP request method to use' do |verb| self.request_method = verb end option :header, short: '-H', value: { type: /[A-Za-z0-9-]+:\s*\w+/, usage: '"Name: value"' }, desc: 'Sets an additional header' do |header| name, value = header.split(/:\s*/,2) self.headers[name] = value end option :user_agent_string, short: '-U', value: { type: String, usage: 'STRING' }, desc: 'Sets the User-Agent header' do |ua| self.user_agent = ua end option :user_agent, short: '-u', value: { type: Support::Network::HTTP::UserAgents::ALIASES.transform_keys { |key| key.to_s.tr('_','-') } }, desc: 'Sets the User-Agent to use' do |name| self.user_agent = name end option :cookie, short: '-C', value: { type: String, usage: 'COOKIE' }, desc: 'Sets the raw Cookie header' do |cookie| cookie = Support::Network::HTTP::Cookie.parse(cookie) self.cookie.merge!(cookie) end option :cookie_param, short: '-c', value: { type: /[^\s=]+=\w+/, usage: 'NAME=VALUE' }, desc: 'Sets an additional cookie param' do |param| name, value = param.split('=',2) self.cookie[name] = value end option :referer, short: '-R', value: { type: String, usage: 'URL' }, desc: 'Sets the Referer header' do |referer| self.referer = referer end option :form_param, short: '-F', value: { type: /[^\s=]+=\w+/, usage: 'NAME=VALUE' }, desc: 'Sets an additional form param' do |param| name, value = param.split('=',2) self.form_data[name] = value end option :test_query_param, value: { type: String, usage: 'NAME' }, desc: 'Tests the URL query param name' do |name| case (test_query_params = self.test_query_params) when true # no-op, test all query params when Set test_query_params << name end end option :test_all_query_params, desc: 'Test all URL query param names' do self.test_query_params = true end option :test_header_name, value: { type: String, usage: 'NAME' }, desc: 'Tests the HTTP Header name' do |name| self.test_header_names << name end option :test_cookie_param, value: { type: String, usage: 'NAME' }, desc: 'Tests the HTTP Cookie name' do |name| case (test_cookie_params = self.test_cookie_params) when true # no-op, test all query params when Set test_cookie_params << name end end option :test_all_cookie_params, desc: 'Test all Cookie param names' do self.test_cookie_params = true end option :test_form_param, value: { type: String, usage: 'NAME' }, desc: 'Tests the form param name' do |name| self.test_form_params << name end option :test_all_form_params, desc: 'Tests all form param names' do self.test_form_params = true end option :input, short: '-i', value: { type: String, usage: 'FILE' }, desc: 'Reads URLs from the list file' argument :url, required: false, repeats: true, desc: 'The URL(s) to scan' # The scan mode. # # @return [:first, :all] # * `:first` - Only find the first vulnerability for each URL. # * `:all` - Find all vulnerabilities for each URL. attr_reader :scan_mode # Keywrod arguments that will be used in {#scan_url} and {#test_url} to # call {WebVuln.scan} or {WebVuln.test}. # # @return [Hash{Symbol => Object}] attr_reader :scan_kwargs # # Initializes the command. # # @param [Hash{Symbol => Object}] kwargs # Additional keyword arguments. # def initialize(**kwargs) super(**kwargs) @scan_mode = :first @scan_kwargs = {} end # # Runs the command. # # @param [Array] urls # The URL(s) to scan. # def run(*urls) unless (options[:input] || !urls.empty?) print_error "must specify URL(s) or --input" exit(-1) end db_connect if options[:import] vulns = [] if options[:input] File.open(options[:input]) do |file| file.each_line(chomp: true) do |url| process_url(url) do |vuln| vulns << vuln end end end elsif !urls.empty? urls.each do |url| process_url(url) do |vuln| vulns << vuln end end end puts unless vulns.empty? print_vulns(vulns) end # # Print a summary of all web vulnerabilities found. # # @param [Array] vulns # The discovered web vulnerabilities. # # @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. # # @since 0.2.0 # 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 # # Prints detailed information about a discovered web vulnerability. # # @param [WebVuln] vuln # 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. # # @since 0.2.0 # def print_vuln(vuln, print_curl: options[:print_curl], print_http: options[:print_http]) super(vuln, print_curl: print_curl, print_http: print_http) end # # Processes a URL. # # @param [String] url # A URL to scan. # # @yield [vuln] # The given block will be passed each newly discovered web # vulnerability. # # @yieldparam [WebVuln] vuln # A newly discovered web vulnerability. # def process_url(url) unless url.start_with?('http://') || url.start_with?('https://') print_error("URL must start with http:// or https://: #{url.inspect}") exit(-1) end if @scan_mode == :first if (first_vuln = test_url(url)) process_vuln(first_vuln) yield first_vuln end else scan_url(url) do |vuln| process_vuln(vuln) yield vuln end end end # # Logs and optioanlly imports a new discovered web vulnerability. # # @param [WebVuln] vuln # The discovered web vulnerability. # # @since 0.2.0 # def process_vuln(vuln) log_vuln(vuln) import_vuln(vuln) if options[:import] end # # The HTTP request method to use. # # @return [:copy, :delete, :get, :head, :lock, :mkcol, :move, # :options, :patch, :post, :propfind, :proppatch, :put, # :trace, :unlock] # # @since 0.2.0 # def request_method @scan_kwargs[:request_method] end # # Sets the HTTP request method to use. # # @param [:copy, :delete, :get, :head, :lock, :mkcol, :move, # :options, :patch, :post, :propfind, :proppatch, :put, # :trace, :unlock] new_request_method # # @return [:copy, :delete, :get, :head, :lock, :mkcol, :move, # :options, :patch, :post, :propfind, :proppatch, :put, # :trace, :unlock] # # @since 0.2.0 # def request_method=(new_request_method) @scan_kwargs[:request_method] = new_request_method end # # Additional headers. # # @return [Hash{String => String}] # def headers @scan_kwargs[:headers] ||= {} end # # The optional HTTP `User-Agent` header to send. # # @return [String, :random, :chrome, :chrome_linux, :chrome_macos, # :chrome_windows, :chrome_iphone, :chrome_ipad, # :chrome_android, :firefox, :firefox_linux, :firefox_macos, # :firefox_windows, :firefox_iphone, :firefox_ipad, # :firefox_android, :safari, :safari_macos, :safari_iphone, # :safari_ipad, :edge, :linux, :macos, :windows, :iphone, # :ipad, :android, nil] # # @since 0.2.0 # def user_agent @scan_kwargs[:user_agent] end # # Sets the HTTP `User-Agent` header. # # @param [String, :random, :chrome, :chrome_linux, :chrome_macos, # :chrome_windows, :chrome_iphone, :chrome_ipad, # :chrome_android, :firefox, :firefox_linux, :firefox_macos, # :firefox_windows, :firefox_iphone, :firefox_ipad, # :firefox_android, :safari, :safari_macos, :safari_iphone, # :safari_ipad, :edge, :linux, :macos, :windows, :iphone, # :ipad, :android] new_user_agent # The new `User-Agent` value to send. # # @return [String, :random, :chrome, :chrome_linux, :chrome_macos, # :chrome_windows, :chrome_iphone, :chrome_ipad, # :chrome_android, :firefox, :firefox_linux, :firefox_macos, # :firefox_windows, :firefox_iphone, :firefox_ipad, # :firefox_android, :safari, :safari_macos, :safari_iphone, # :safari_ipad, :edge, :linux, :macos, :windows, :iphone, # :ipad, :android] # # @since 0.2.0 # def user_agent=(new_user_agent) @scan_kwargs[:user_agent] = new_user_agent end # # The optional `Cookie` header to send. # # @return [Ronin::Support::Network::HTTP::Cookie] # def cookie @scan_kwargs[:cookie] ||= Support::Network::HTTP::Cookie.new end # # The optional HTTP `Referer` header to send. # # @return [String, nil] # def referer @scan_kwargs[:referer] end # # Sets the HTTP `Referer` header to send. # # @param [String, nil] new_referer # The new `Referer` header to send. # # @return [String, nil] # def referer=(new_referer) @scan_kwargs[:referer] = new_referer end # # Additional form params. # # @return [Hash{String => String}, nil] # def form_data @scan_kwargs[:form_data] ||= {} end # # The URL query params to test. # # @return [Set, true] # def test_query_params @scan_kwargs[:query_params] ||= Set.new end # # Sets the URL query params to test. # # @param [Set, true] new_query_params # The query params to test. # # @return [Set, true] # def test_query_params=(new_query_params) @scan_kwargs[:query_params] = new_query_params end # # The HTTP Header names to test. # # @return [Set] # def test_header_names @scan_kwargs[:header_names] ||= Set.new end # # The HTTP Cookie to test. # # @return [Set, true] # def test_cookie_params @scan_kwargs[:cookie_params] ||= Set.new end # # Sets the HTTP Cookie to test. # # @param [Set, true] new_cookie_params # The new cookie param names to test. # # @return [Set, true] # def test_cookie_params=(new_cookie_params) @scan_kwargs[:cookie_params] = new_cookie_params end # # The form params to test. # # @return [Set, nil] # def test_form_params @scan_kwargs[:form_params] ||= Set.new end # # Sets the form params to test. # # @param [Set, true] new_form_params # The new form param names to test. # # @return [Set, true] # def test_form_params=(new_form_params) @scan_kwargs[:form_params] = new_form_params end # # Scans a URL for web vulnerabilities. # # @param [String] url # The URL to scan. # # @yield [vuln] # The given block will be passed each discovered web vulnerability. # # @yieldparam [WebVuln] vuln # A web vulnerability discovered on the URL. # # @abstract # def scan_url(url,&block) raise(NotImplementedError,"#{self.class}#scan_url was not defined") end # # Tests a URL for web vulnerabilities. # # @param [String] url # The URL to test. # # @return [WebVuln, nil] vuln # The first web vulnerability discovered on the URL. # # @abstract # def test_url(url) raise(NotImplementedError,"#{self.class}#test_url was not defined") end end end end end