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