# frozen_string_literal: true # # ronin-vulns - A Ruby library for blind vulnerability testing. # # Copyright (c) 2022-2023 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/logging' require 'ronin/support/network/http/cookie' require 'set' module Ronin module Vulns class CLI # # Base class for all web vulnerability commands. # class WebVulnCommand < Command include Logging 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 :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 :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 :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 vulns_discovered = false if options[:input] File.open(options[:input]) do |file| file.each_line(chomp: true) do |url| vulns_discovered ||= process_url(url) end end elsif !urls.empty? urls.each do |url| vulns_discovered ||= process_url(url) end end unless vulns_discovered puts colors.green("No vulnerabilities found") end end # # Processes a URL. # # @param [String] url # A URL to scan. # # @return [Boolean] # Indicates whether a vulnerability was discovered in the URL. # 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 vuln_discovered = false if @scan_mode == :first if (first_vuln = test_url(url)) log_vuln(first_vuln) vuln_discovered = true end else scan_url(url) do |vuln| log_vuln(vuln) vuln_discovered = true end end return vuln_discovered end # # Additional headers. # # @return [Hash{String => String}] # def headers @scan_kwargs[:headers] ||= {} 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 # # 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