From e5010a1de9100b84a87ecca649d3ebac5fc49672 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Sun, 28 Apr 2024 16:29:33 -0700 Subject: [PATCH 1/2] Refactored `CLI::Logging` to `CLI::Printing` (closes #70). --- lib/ronin/vulns/cli/importable.rb | 15 ++--- .../vulns/cli/{logging.rb => printing.rb} | 53 ++++++++++------ lib/ronin/vulns/cli/web_vuln_command.rb | 4 +- .../cli/{logging_spec.rb => printing_spec.rb} | 62 +++++++++++++++---- 4 files changed, 93 insertions(+), 41 deletions(-) rename lib/ronin/vulns/cli/{logging.rb => printing.rb} (57%) rename spec/cli/{logging_spec.rb => printing_spec.rb} (88%) diff --git a/lib/ronin/vulns/cli/importable.rb b/lib/ronin/vulns/cli/importable.rb index 85ed90c..2f1e099 100644 --- a/lib/ronin/vulns/cli/importable.rb +++ b/lib/ronin/vulns/cli/importable.rb @@ -19,7 +19,7 @@ # require 'ronin/vulns/importer' -require 'ronin/vulns/cli/logging' +require 'ronin/vulns/cli/printing' require 'ronin/db/cli/database_options' require 'ronin/db/cli/printing' @@ -36,7 +36,7 @@ class CLI # module Importable include DB::CLI::Printing - include Logging + include Printing # # Includes `Ronin::DB::CLI::DatabaseOptions` into the including command @@ -60,13 +60,14 @@ def self.included(command) def import_vuln(vuln) Importer.import(vuln) - vuln_name = vuln_type(vuln) - location = vuln_location(vuln) + vuln_type = vuln_type(vuln) + param_type = vuln_param_type(vuln) + param_name = vuln_param_name(vuln) - if location - log_info "Imported #{vuln_name} vulnerability on URL #{vuln.url} and #{location}" + if (param_type && param_name) + log_info "Imported #{vuln_type} vulnerability on URL #{vuln.url} and #{param_type} '#{param_name}'" else - log_info "Imported #{vuln_name} vulnerability on URL #{vuln.url}" + log_info "Imported #{vuln_type} vulnerability on URL #{vuln.url}" end end end diff --git a/lib/ronin/vulns/cli/logging.rb b/lib/ronin/vulns/cli/printing.rb similarity index 57% rename from lib/ronin/vulns/cli/logging.rb rename to lib/ronin/vulns/cli/printing.rb index a1b0061..f3e4551 100644 --- a/lib/ronin/vulns/cli/logging.rb +++ b/lib/ronin/vulns/cli/printing.rb @@ -24,9 +24,12 @@ module Ronin module Vulns class CLI # - # Mixin that adds methods for logging discovered web vulnerabilities. + # Mixin that adds methods for logging and printing discovered web + # vulnerabilities. # - module Logging + # @since 0.2.0 + # + module Printing include Core::CLI::Logging # Known vulnerability types and their printable names. @@ -53,40 +56,50 @@ def vuln_type(vuln) end # - # Determines the location of the web vulnerability. + # Determines the param type that the web vulnerability occurs in. # # @param [WebVuln] vuln # # @return [String, nil] # - # @since 0.2.0 + def vuln_param_type(vuln) + if vuln.query_param then 'query param' + elsif vuln.header_name then 'Header' + elsif vuln.cookie_param then 'Cookie param' + elsif vuln.form_param then 'form param' + end + end + + # + # Determines the param name that the web vulnerability occurs in. + # + # @param [WebVuln] vuln + # + # @return [String, nil] # - def vuln_location(vuln) - if vuln.query_param - "query param '#{vuln.query_param}'" - elsif vuln.header_name - "Header '#{vuln.header_name}'" - elsif vuln.cookie_param - "Cookie param '#{vuln.cookie_param}'" - elsif vuln.form_param - "form param '#{vuln.form_param}'" + def vuln_param_name(vuln) + if vuln.query_param then vuln.query_param + elsif vuln.header_name then vuln.header_name + elsif vuln.cookie_param then vuln.cookie_param + elsif vuln.form_param then vuln.form_param end end # - # Prints a web vulnerability. + # Prints a log message about a newly discovered web vulnerability. # # @param [WebVuln] vuln - # The web vulnerability to print. + # The web vulnerability to log. # def log_vuln(vuln) - vuln_name = vuln_type(vuln) - location = vuln_location(vuln) + vuln_type = vuln_type(vuln) + param_type = vuln_param_type(vuln) + param_name = vuln_param_name(vuln) - if location - log_warn "Found #{vuln_name} on #{vuln.url} via #{location}!" + if (param_type && param_name) + log_warn "Found #{vuln_type} on #{vuln.url} via #{param_type} '#{param_name}'!" else - log_warn "Found #{vuln_name} on #{vuln.url}!" + log_warn "Found #{vuln_type} on #{vuln.url}!" end end end diff --git a/lib/ronin/vulns/cli/web_vuln_command.rb b/lib/ronin/vulns/cli/web_vuln_command.rb index e683eb2..4664eb9 100644 --- a/lib/ronin/vulns/cli/web_vuln_command.rb +++ b/lib/ronin/vulns/cli/web_vuln_command.rb @@ -20,7 +20,7 @@ require 'ronin/vulns/cli/command' require 'ronin/vulns/cli/importable' -require 'ronin/vulns/cli/logging' +require 'ronin/vulns/cli/printing' require 'ronin/support/network/http/cookie' require 'ronin/support/network/http/user_agents' @@ -35,7 +35,7 @@ class CLI # class WebVulnCommand < Command - include Logging + include Printing include Importable option :import, desc: 'Imports discovered vulnerabilities into the database' diff --git a/spec/cli/logging_spec.rb b/spec/cli/printing_spec.rb similarity index 88% rename from spec/cli/logging_spec.rb rename to spec/cli/printing_spec.rb index 56025f5..0800939 100644 --- a/spec/cli/logging_spec.rb +++ b/spec/cli/printing_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -require 'ronin/vulns/cli/logging' +require 'ronin/vulns/cli/printing' require 'ronin/vulns/cli/command' require 'ronin/vulns/lfi' @@ -9,12 +9,12 @@ require 'ronin/vulns/reflected_xss' require 'ronin/vulns/open_redirect' -describe Ronin::Vulns::CLI::Logging do +describe Ronin::Vulns::CLI::Printing do let(:url) { 'https://example.com/page.php?id=1' } module TestCLIPrinting class TestCommand < Ronin::Vulns::CLI::Command - include Ronin::Vulns::CLI::Logging + include Ronin::Vulns::CLI::Printing end end @@ -71,13 +71,13 @@ class TestCommand < Ronin::Vulns::CLI::Command end end - describe "#vuln_location" do + describe "#vuln_param_type" do context "and the #query_param attribute is set" do let(:query_param) { 'id' } let(:vuln) { Ronin::Vulns::WebVuln.new(url, query_param: query_param) } - it "must log 'Found LFI on via query param !'" do - expect(subject.vuln_location(vuln)).to eq("query param '#{query_param}'") + it "must return 'query param''" do + expect(subject.vuln_param_type(vuln)).to eq("query param") end end @@ -85,8 +85,8 @@ class TestCommand < Ronin::Vulns::CLI::Command let(:header_name) { 'X-Foo' } let(:vuln) { Ronin::Vulns::LFI.new(url, header_name: header_name) } - it "must log 'Found LFI on via Header !'" do - expect(subject.vuln_location(vuln)).to eq("Header '#{header_name}'") + it "must return 'Header'" do + expect(subject.vuln_param_type(vuln)).to eq("Header") end end @@ -94,8 +94,8 @@ class TestCommand < Ronin::Vulns::CLI::Command let(:cookie_param) { 'X-Foo' } let(:vuln) { Ronin::Vulns::LFI.new(url, cookie_param: cookie_param) } - it "must log 'Found LFI on via Cookie param !'" do - expect(subject.vuln_location(vuln)).to eq("Cookie param '#{cookie_param}'") + it "must return 'Cookie param'" do + expect(subject.vuln_param_type(vuln)).to eq("Cookie param") end end @@ -103,8 +103,46 @@ class TestCommand < Ronin::Vulns::CLI::Command let(:form_param) { 'X-Foo' } let(:vuln) { Ronin::Vulns::LFI.new(url, form_param: form_param) } - it "must log 'Found LFI on via form param !'" do - expect(subject.vuln_location(vuln)).to eq("form param '#{form_param}'") + it "must return 'form param'" do + expect(subject.vuln_param_type(vuln)).to eq("form param") + end + end + end + + describe "#vuln_param_name" do + context "and the #query_param attribute is set" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::WebVuln.new(url, query_param: query_param) } + + it "must return the vuln's #query_param" do + expect(subject.vuln_param_name(vuln)).to eq(query_param) + end + end + + context "and the #header_name attribute is set" do + let(:header_name) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::LFI.new(url, header_name: header_name) } + + it "must return the vuln's #header_name" do + expect(subject.vuln_param_name(vuln)).to eq(header_name) + end + end + + context "and the #cookie_param attribute is set" do + let(:cookie_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::LFI.new(url, cookie_param: cookie_param) } + + it "must return the vuln's #cookie_param" do + expect(subject.vuln_param_name(vuln)).to eq(cookie_param) + end + end + + context "and the #form_param attribute is set" do + let(:form_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::LFI.new(url, form_param: form_param) } + + it "must return the vuln's #form_param" do + expect(subject.vuln_param_name(vuln)).to eq(form_param) end end end From fa44084a21789a2f1fdc3d9342f8457e66956ec3 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Sun, 28 Apr 2024 19:53:59 -0700 Subject: [PATCH 2/2] Added separate `print_vuln` and `print_vulns` methods. * Added `CLI::Printing#print_vuln`. * Added `CLI::Printing#print_vulns`. * Refactored `WebVulnCommand` to collect all discovered web vulnerabilities and print them after scanning. * Override `WebVulnCommand#print_vuln` and `#print_vulns` to pass in the `--print-curl` and `--print-http` options. --- lib/ronin/vulns/cli/printing.rb | 76 ++++ lib/ronin/vulns/cli/web_vuln_command.rb | 99 +++-- spec/cli/printing_spec.rb | 538 ++++++++++++++++++++++++ spec/cli/web_vuln_command_spec.rb | 243 ++++++++++- 4 files changed, 913 insertions(+), 43 deletions(-) diff --git a/lib/ronin/vulns/cli/printing.rb b/lib/ronin/vulns/cli/printing.rb index f3e4551..dc1445c 100644 --- a/lib/ronin/vulns/cli/printing.rb +++ b/lib/ronin/vulns/cli/printing.rb @@ -20,6 +20,8 @@ require 'ronin/core/cli/logging' +require 'command_kit/printing/indent' + module Ronin module Vulns class CLI @@ -31,6 +33,7 @@ class CLI # module Printing include Core::CLI::Logging + include CommandKit::Printing::Indent # Known vulnerability types and their printable names. VULN_TYPES = { @@ -102,6 +105,79 @@ def log_vuln(vuln) log_warn "Found #{vuln_type} on #{vuln.url}!" end 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: false, print_http: false) + vuln_type = vuln_type(vuln) + param_type = vuln_param_type(vuln) + param_name = vuln_param_name(vuln) + + if (param_type && param_name) + puts "#{colors.bold(colors.bright_red(vuln_type))} on #{colors.bold(colors.bright_white(vuln.url))} via #{colors.bold(colors.bright_white(param_type))} '#{colors.bold(colors.bright_red(param_name))}'" + else + puts "#{colors.bold(colors.red(vuln_type))} on #{colors.bold(colors.bright_white(vuln.url))}" + end + + if print_curl || print_http + puts + + if print_curl + puts " #{vuln.to_curl}" + puts + end + + if print_http + vuln.to_http.each_line(chomp: true) do |line| + puts " #{line}" + end + puts + end + end + 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: false, print_http: false) + if vulns.empty? + puts colors.green("No vulnerabilities found") + else + puts colors.bold(colors.bright_red('Vulnerabilities found!')) + puts + + indent do + vulns.each do |vuln| + print_vuln(vuln, print_curl: print_curl, + print_http: print_http) + end + end + puts unless (print_curl || print_http) + end + end end end end diff --git a/lib/ronin/vulns/cli/web_vuln_command.rb b/lib/ronin/vulns/cli/web_vuln_command.rb index 4664eb9..356decd 100644 --- a/lib/ronin/vulns/cli/web_vuln_command.rb +++ b/lib/ronin/vulns/cli/web_vuln_command.rb @@ -24,6 +24,7 @@ require 'ronin/support/network/http/cookie' require 'ronin/support/network/http/user_agents' +require 'command_kit/printing/indent' require 'set' @@ -35,6 +36,7 @@ class CLI # class WebVulnCommand < Command + include CommandKit::Printing::Indent include Printing include Importable @@ -250,23 +252,66 @@ def run(*urls) db_connect if options[:import] - vulns_discovered = false + vulns = [] if options[:input] File.open(options[:input]) do |file| file.each_line(chomp: true) do |url| - vulns_discovered ||= process_url(url) + process_url(url) do |vuln| + vulns << vuln + end end end elsif !urls.empty? urls.each do |url| - vulns_discovered ||= process_url(url) + process_url(url) do |vuln| + vulns << vuln + end end end - unless vulns_discovered - puts colors.green("No vulnerabilities found") - 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 # @@ -275,8 +320,12 @@ def run(*urls) # @param [String] url # A URL to scan. # - # @return [Boolean] - # Indicates whether a vulnerability was discovered in the URL. + # @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://') @@ -284,23 +333,17 @@ def process_url(url) exit(-1) end - vuln_discovered = false - if @scan_mode == :first if (first_vuln = test_url(url)) process_vuln(first_vuln) - - vuln_discovered = true + yield first_vuln end else scan_url(url) do |vuln| process_vuln(vuln) - - vuln_discovered = true + yield vuln end end - - return vuln_discovered end # @@ -316,30 +359,6 @@ def process_vuln(vuln) import_vuln(vuln) if options[:import] end - # - # Logs a discovered web vulnerability. - # - # @param [WebVuln] vuln - # The discovered web vulnerability. - # - # @since 0.2.0 - # - def log_vuln(vuln) - super(vuln) - - if options[:print_curl] - puts - puts " #{vuln.to_curl}" - puts - elsif options[:print_http] - puts - vuln.to_http.each_line do |line| - puts " #{line}" - end - puts - end - end - # # The HTTP request method to use. # diff --git a/spec/cli/printing_spec.rb b/spec/cli/printing_spec.rb index 0800939..ec869ed 100644 --- a/spec/cli/printing_spec.rb +++ b/spec/cli/printing_spec.rb @@ -9,6 +9,8 @@ require 'ronin/vulns/reflected_xss' require 'ronin/vulns/open_redirect' +require 'stringio' + describe Ronin::Vulns::CLI::Printing do let(:url) { 'https://example.com/page.php?id=1' } @@ -472,4 +474,540 @@ class TestCommand < Ronin::Vulns::CLI::Command end end end + + describe "#print_vulns" do + let(:stdout) { StringIO.new } + + subject { command_class.new(stdout: stdout) } + before { allow(stdout).to receive(:tty?).and_return(true) } + + let(:bright_red) { CommandKit::Colors::ANSI::BRIGHT_RED } + let(:bright_white) { CommandKit::Colors::ANSI::BRIGHT_WHITE } + let(:bold) { CommandKit::Colors::ANSI::BOLD } + let(:bold_bright_red) { bold + bright_red } + let(:bold_bright_white) { bold + bright_white } + let(:reset_intensity) { CommandKit::Colors::ANSI::RESET_INTENSITY } + let(:reset_color) { CommandKit::Colors::ANSI::RESET_COLOR } + let(:reset) { reset_color + reset_intensity } + + context "when given an empty Array" do + let(:vulns) { [] } + + let(:green) { CommandKit::Colors::ANSI::GREEN } + + it "must print 'No vulnerabilities found' in green" do + subject.print_vulns(vulns) + + expect(stdout.string).to eq( + "#{green}No vulnerabilities found#{reset_color}#{$/}" + ) + end + end + + context "when given an Array of Ronin::Vulns::WebVuln objects" do + let(:query_param1) { 'a' } + let(:query_param2) { 'b' } + let(:url) { URI.parse("https://example.com/page.php?#{query_param1}=foo&#{query_param2}=bar") } + + let(:vuln1) { Ronin::Vulns::SQLI.new(url, query_param: query_param1) } + let(:vuln2) { Ronin::Vulns::SQLI.new(url, query_param: query_param2) } + let(:vulns) { [vuln1, vuln2] } + + it "must print 'Vulnerabilities found!' in bold bright red and list the individual vulnerabilities" do + subject.print_vulns(vulns) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}Vulnerabilities found!#{reset}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param1}#{reset}'", + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param2}#{reset}'", + '', + '' + ].join($/) + ) + end + + context "and when given `print_curl: true`" do + it "must print an indented example curl command for each web vulnerability" do + subject.print_vulns(vulns, print_curl: true) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}Vulnerabilities found!#{reset}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param1}#{reset}'", + '', + " #{vuln1.to_curl}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param2}#{reset}'", + '', + " #{vuln2.to_curl}", + '', + '' + ].join($/) + ) + end + end + + context "and when `print_http: true` is given" do + it "must print an indented example HTTP request for each web vulnerability" do + subject.print_vulns(vulns, print_http: true) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}Vulnerabilities found!#{reset}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param1}#{reset}'", + '', + *vuln1.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param2}#{reset}'", + '', + *vuln2.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + '' + ].join($/) + ) + end + end + + context "and when `print_curl: true` and `print_http: true` are given" do + it "must print an indented example curl command and then an example HTTP request for each web vulnerability" do + subject.print_vulns(vulns, print_curl: true, print_http: true) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}Vulnerabilities found!#{reset}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param1}#{reset}'", + '', + " #{vuln1.to_curl}", + '', + *vuln1.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param2}#{reset}'", + '', + " #{vuln2.to_curl}", + '', + *vuln2.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + '' + ].join($/) + ) + end + end + end + end + + describe "#print_vuln" do + let(:stdout) { StringIO.new } + + subject { command_class.new(stdout: stdout) } + before { allow(stdout).to receive(:tty?).and_return(true) } + + let(:bright_red) { CommandKit::Colors::ANSI::BRIGHT_RED } + let(:bright_white) { CommandKit::Colors::ANSI::BRIGHT_WHITE } + let(:bold) { CommandKit::Colors::ANSI::BOLD } + let(:bold_bright_red) { bold + bright_red } + let(:bold_bright_white) { bold + bright_white } + let(:reset_intensity) { CommandKit::Colors::ANSI::RESET_INTENSITY } + let(:reset_color) { CommandKit::Colors::ANSI::RESET_COLOR } + let(:reset) { reset_color + reset_intensity } + + context "when given a Ronin::Vulns::LFI object" do + context "and the #query_param attribute is set" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::LFI.new(url, query_param: query_param) } + + it "must print \"LFI on via query param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}LFI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'#{$/}" + ) + end + end + + context "and the #header_name attribute is set" do + let(:header_name) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::LFI.new(url, header_name: header_name) } + + it "must print \"LFI on via Header ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}LFI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Header#{reset} '#{bold_bright_red}#{header_name}#{reset}'#{$/}" + ) + end + end + + context "and the #cookie_param attribute is set" do + let(:cookie_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::LFI.new(url, cookie_param: cookie_param) } + + it "must print \"LFI on via Cookie param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}LFI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Cookie param#{reset} '#{bold_bright_red}#{cookie_param}#{reset}'#{$/}" + ) + end + end + + context "and the #form_param attribute is set" do + let(:form_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::LFI.new(url, form_param: form_param) } + + it "must print \"LFI on via form param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}LFI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}form param#{reset} '#{bold_bright_red}#{form_param}#{reset}'#{$/}" + ) + end + end + end + + context "when given a Ronin::Vulns::RFI object" do + context "and the #query_param attribute is set" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::RFI.new(url, query_param: query_param) } + + it "must print \"RFI on via query param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}RFI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'#{$/}" + ) + end + end + + context "and the #header_name attribute is set" do + let(:header_name) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::RFI.new(url, header_name: header_name) } + + it "must print \"RFI on via Header ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}RFI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Header#{reset} '#{bold_bright_red}#{header_name}#{reset}'#{$/}" + ) + end + end + + context "and the #cookie_param attribute is set" do + let(:cookie_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::RFI.new(url, cookie_param: cookie_param) } + + it "must print \"RFI on via Cookie param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}RFI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Cookie param#{reset} '#{bold_bright_red}#{cookie_param}#{reset}'#{$/}" + ) + end + end + + context "and the #form_param attribute is set" do + let(:form_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::RFI.new(url, form_param: form_param) } + + it "must print \"RFI on via form param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}RFI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}form param#{reset} '#{bold_bright_red}#{form_param}#{reset}'#{$/}" + ) + end + end + end + + context "when given a Ronin::Vulns::SQLI object" do + context "and the #query_param attribute is set" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, query_param: query_param) } + + it "must print \"SQLi on via query param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'#{$/}" + ) + end + end + + context "and the #header_name attribute is set" do + let(:header_name) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, header_name: header_name) } + + it "must print \"SQLI on via Header ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Header#{reset} '#{bold_bright_red}#{header_name}#{reset}'#{$/}" + ) + end + end + + context "and the #cookie_param attribute is set" do + let(:cookie_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, cookie_param: cookie_param) } + + it "must print \"SQLi on via Cookie param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Cookie param#{reset} '#{bold_bright_red}#{cookie_param}#{reset}'#{$/}" + ) + end + end + + context "and the #form_param attribute is set" do + let(:form_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, form_param: form_param) } + + it "must print \"SQLi on via form param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}form param#{reset} '#{bold_bright_red}#{form_param}#{reset}'#{$/}" + ) + end + end + end + + context "when given a Ronin::Vulns::SSTI object" do + context "and the #query_param attribute is set" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::SSTI.new(url, query_param: query_param) } + + it "must print \"SSTI on via query param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}SSTI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'#{$/}" + ) + end + end + + context "and the #header_name attribute is set" do + let(:header_name) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::SSTI.new(url, header_name: header_name) } + + it "must print \"SSTI on via Header ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}SSTI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Header#{reset} '#{bold_bright_red}#{header_name}#{reset}'#{$/}" + ) + end + end + + context "and the #cookie_param attribute is set" do + let(:cookie_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::SSTI.new(url, cookie_param: cookie_param) } + + it "must print \"SSTI on via Cookie param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}SSTI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Cookie param#{reset} '#{bold_bright_red}#{cookie_param}#{reset}'#{$/}" + ) + end + end + + context "and the #form_param attribute is set" do + let(:form_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::SSTI.new(url, form_param: form_param) } + + it "must print \"SSTI on via form param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}SSTI#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}form param#{reset} '#{bold_bright_red}#{form_param}#{reset}'#{$/}" + ) + end + end + end + + context "when given a Ronin::Vulns::OpenRedirect object" do + context "and the #query_param attribute is set" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::OpenRedirect.new(url, query_param: query_param) } + + it "must print \"Open Redirect on via query param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}Open Redirect#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'#{$/}" + ) + end + end + + context "and the #header_name attribute is set" do + let(:header_name) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::OpenRedirect.new(url, header_name: header_name) } + + it "must print \"Open Redirect on via Header ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}Open Redirect#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Header#{reset} '#{bold_bright_red}#{header_name}#{reset}'#{$/}" + ) + end + end + + context "and the #cookie_param attribute is set" do + let(:cookie_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::OpenRedirect.new(url, cookie_param: cookie_param) } + + it "must print \"Open Redirect on via Cookie param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}Open Redirect#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Cookie param#{reset} '#{bold_bright_red}#{cookie_param}#{reset}'#{$/}" + ) + end + end + + context "and the #form_param attribute is set" do + let(:form_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::OpenRedirect.new(url, form_param: form_param) } + + it "must print \"Open Redirect on via form param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}Open Redirect#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}form param#{reset} '#{bold_bright_red}#{form_param}#{reset}'#{$/}" + ) + end + end + end + + context "when given a Ronin::Vulns::ReflectedXSS object" do + context "and the #query_param attribute is set" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::ReflectedXSS.new(url, query_param: query_param) } + + it "must print \"reflected XSS on via query param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}reflected XSS#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'#{$/}" + ) + end + end + + context "and the #header_name attribute is set" do + let(:header_name) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::ReflectedXSS.new(url, header_name: header_name) } + + it "must print \"reflected XSS on via Header ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}reflected XSS#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Header#{reset} '#{bold_bright_red}#{header_name}#{reset}'#{$/}" + ) + end + end + + context "and the #cookie_param attribute is set" do + let(:cookie_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::ReflectedXSS.new(url, cookie_param: cookie_param) } + + it "must print \"reflected XSS on via Cookie param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}reflected XSS#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}Cookie param#{reset} '#{bold_bright_red}#{cookie_param}#{reset}'#{$/}" + ) + end + end + + context "and the #form_param attribute is set" do + let(:form_param) { 'X-Foo' } + let(:vuln) { Ronin::Vulns::ReflectedXSS.new(url, form_param: form_param) } + + it "must print \"reflected XSS on via form param ''!\"" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + "#{bold_bright_red}reflected XSS#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}form param#{reset} '#{bold_bright_red}#{form_param}#{reset}'#{$/}" + ) + end + end + end + + context "when given `print_curl: true`" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, query_param: query_param) } + + it "must print an indented example curl command for the web vulnerability" do + subject.print_vuln(vuln, print_curl: true) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'", + '', + " #{vuln.to_curl}", + '', + '' + ].join($/) + ) + end + end + + context "when given `print_http: true`" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, query_param: query_param) } + + it "must print an indented example HTTP request for the web vulnerability" do + subject.print_vuln(vuln, print_http: true) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'", + '', + *vuln.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + '' + ].join($/) + ) + end + end + + context "when given both `print_curl: true` and `print_http: true`" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, query_param: query_param) } + + it "must print an indented example curl command and then an example HTTP request for the web vulnerability" do + subject.print_vuln(vuln, print_curl: true, print_http: true) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'", + '', + " #{vuln.to_curl}", + '', + *vuln.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + '' + ].join($/) + ) + end + end + end end diff --git a/spec/cli/web_vuln_command_spec.rb b/spec/cli/web_vuln_command_spec.rb index 8f8fc9e..7d5b84c 100644 --- a/spec/cli/web_vuln_command_spec.rb +++ b/spec/cli/web_vuln_command_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' require 'ronin/vulns/cli/web_vuln_command' +require 'ronin/vulns/sqli' +require 'stringio' require 'tempfile' describe Ronin::Vulns::CLI::WebVulnCommand do @@ -507,12 +509,243 @@ end end + describe "#print_vulns" do + let(:stdout) { StringIO.new } + + subject { described_class.new(stdout: stdout) } + before { allow(stdout).to receive(:tty?).and_return(true) } + + let(:green) { CommandKit::Colors::ANSI::GREEN } + let(:bright_red) { CommandKit::Colors::ANSI::BRIGHT_RED } + let(:bright_white) { CommandKit::Colors::ANSI::BRIGHT_WHITE } + let(:bold) { CommandKit::Colors::ANSI::BOLD } + let(:bold_bright_red) { bold + bright_red } + let(:bold_bright_white) { bold + bright_white } + let(:reset_intensity) { CommandKit::Colors::ANSI::RESET_INTENSITY } + let(:reset_color) { CommandKit::Colors::ANSI::RESET_COLOR } + let(:reset) { reset_color + reset_intensity } + + context "when given an empty Array" do + let(:vulns) { [] } + + it "must print 'No vulnerabilities found' in green" do + subject.print_vulns(vulns) + + expect(stdout.string).to eq( + "#{green}No vulnerabilities found#{reset_color}#{$/}" + ) + end + end + + context "when given an Array of Ronin::Vulns::WebVuln objects" do + let(:query_param1) { 'a' } + let(:query_param2) { 'b' } + let(:url) { URI.parse("https://example.com/page.php?#{query_param1}=foo&#{query_param2}=bar") } + + let(:vuln1) { Ronin::Vulns::SQLI.new(url, query_param: query_param1) } + let(:vuln2) { Ronin::Vulns::SQLI.new(url, query_param: query_param2) } + let(:vulns) { [vuln1, vuln2] } + + it "must print 'No vulnerabilities found' in green" do + subject.print_vulns(vulns) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}Vulnerabilities found!#{reset}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param1}#{reset}'", + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param2}#{reset}'", + '', + '' + ].join($/) + ) + end + + context "and when the '--print-curl' option is given" do + before { subject.options[:print_curl] = true } + + it "must print an indented example curl command for each web vulnerability" do + subject.print_vulns(vulns) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}Vulnerabilities found!#{reset}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param1}#{reset}'", + '', + " #{vuln1.to_curl}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param2}#{reset}'", + '', + " #{vuln2.to_curl}", + '', + '' + ].join($/) + ) + end + end + + context "and when the '--print'http' option is given" do + before { subject.options[:print_http] = true } + + it "must print an indented example HTTP request for each web vulnerability" do + subject.print_vulns(vulns) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}Vulnerabilities found!#{reset}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param1}#{reset}'", + '', + *vuln1.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param2}#{reset}'", + '', + *vuln2.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + '' + ].join($/) + ) + end + end + + context "and when given both the '--print'curl' and '--print-http' options are given" do + before do + subject.options[:print_curl] = true + subject.options[:print_http] = true + end + + it "must print an indented example curl command and then an example HTTP request for each web vulnerability" do + subject.print_vulns(vulns) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}Vulnerabilities found!#{reset}", + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param1}#{reset}'", + '', + " #{vuln1.to_curl}", + '', + *vuln1.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + " #{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param2}#{reset}'", + '', + " #{vuln2.to_curl}", + '', + *vuln2.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + '' + ].join($/) + ) + end + end + end + end + + describe "#print_vuln" do + let(:stdout) { StringIO.new } + + subject { described_class.new(stdout: stdout) } + before { allow(stdout).to receive(:tty?).and_return(true) } + + let(:bright_red) { CommandKit::Colors::ANSI::BRIGHT_RED } + let(:bright_white) { CommandKit::Colors::ANSI::BRIGHT_WHITE } + let(:bold) { CommandKit::Colors::ANSI::BOLD } + let(:bold_bright_red) { bold + bright_red } + let(:bold_bright_white) { bold + bright_white } + let(:reset_intensity) { CommandKit::Colors::ANSI::RESET_INTENSITY } + let(:reset_color) { CommandKit::Colors::ANSI::RESET_COLOR } + let(:reset) { reset_color + reset_intensity } + + context "when the '--print-curl' option is given" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, query_param: query_param) } + + before { subject.options[:print_curl] = true } + + it "must print an indented example curl command for the web vulnerability" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'", + '', + " #{vuln.to_curl}", + '', + '' + ].join($/) + ) + end + end + + context "when the '--print'http' option is given" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, query_param: query_param) } + + before { subject.options[:print_http] = true } + + it "must print an indented example HTTP request for the web vulnerability" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'", + '', + *vuln.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + '' + ].join($/) + ) + end + end + + context "when given both the '--print'curl' and '--print-http' options are given" do + let(:query_param) { 'id' } + let(:vuln) { Ronin::Vulns::SQLI.new(url, query_param: query_param) } + + before do + subject.options[:print_curl] = true + subject.options[:print_http] = true + end + + it "must print an indented example curl command and then an example HTTP request for the web vulnerability" do + subject.print_vuln(vuln) + + expect(stdout.string).to eq( + [ + "#{bold_bright_red}SQLi#{reset} on #{bold_bright_white}#{url}#{reset} via #{bold_bright_white}query param#{reset} '#{bold_bright_red}#{query_param}#{reset}'", + '', + " #{vuln.to_curl}", + '', + *vuln.to_http.each_line(chomp: true).map { |line| + " #{line}" + }, + '', + '' + ].join($/) + ) + end + end + end + describe "#process_url" do context "when #scan_mode is :first" do it "must call #test_url with the given URL" do expect(subject).to receive(:test_url).with(url) - subject.process_url(url) + expect { |b| + subject.process_url(url,&b) + }.to_not yield_control end context "and #test_url returns a WebVuln object" do @@ -522,7 +755,9 @@ expect(subject).to receive(:test_url).with(url).and_return(vuln) expect(subject).to receive(:process_vuln).with(vuln) - subject.process_url(url) + expect { |b| + subject.process_url(url,&b) + }.to yield_with_args(vuln) end end end @@ -546,7 +781,9 @@ expect(subject).to receive(:process_vuln).with(vuln1) expect(subject).to receive(:process_vuln).with(vuln2) - subject.process_url(url) + expect { |b| + subject.process_url(url,&b) + }.to yield_successive_args(vuln1,vuln2) end end end