diff --git a/.gitignore b/.gitignore index 59013f4e..41dc3eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ log.txt .yardoc debug.log wordlist.txt +rspec_results.html diff --git a/dev/pre-commit-hook.rb b/dev/pre-commit-hook.rb new file mode 100755 index 00000000..925c80d4 --- /dev/null +++ b/dev/pre-commit-hook.rb @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby + +# ln -sf /Users/xxx/wpscan/dev/pre-commit-hook.rb /Users/xxx/wpscan/.git/hooks/pre-commit + +require 'pty' +html_path = 'rspec_results.html' + +begin + PTY.spawn( "rspec spec --format h > #{html_path}" ) do |stdin, stdout, pid| + begin + stdin.each { |line| print line } + rescue Errno::EIO => e + puts "Error: #{e.to.s}" + return 1 + end + end +rescue PTY::ChildExited + puts 'Child process exit!' +end + +# find out if there were any errors +html = open(html_path).read +examples = html.match(/(\d+) examples/)[0].to_i rescue 0 +errors = html.match(/(\d+) errors/)[0].to_i rescue 0 +if errors == 0 then + errors = html.match(/(\d+) failure/)[0].to_i rescue 0 +end +pending = html.match(/(\d+) pending/)[0].to_i rescue 0 + +if errors.zero? + puts "0 failed! #{examples} run, #{pending} pending" + sleep 1 + exit 0 +else + puts "\aCOMMIT FAILED!!" + puts "View your rspec results at #{File.expand_path(html_path)}" + puts + puts "#{errors} failed! #{examples} run, #{pending} pending" + exit 1 +end diff --git a/lib/common/collections/vulnerabilities/output.rb b/lib/common/collections/vulnerabilities/output.rb index d5130227..632aed88 100644 --- a/lib/common/collections/vulnerabilities/output.rb +++ b/lib/common/collections/vulnerabilities/output.rb @@ -3,9 +3,9 @@ class Vulnerabilities < Array module Output - def output + def output(verbose = false) self.each do |v| - v.output + v.output(verbose) end end diff --git a/lib/common/collections/wp_items/output.rb b/lib/common/collections/wp_items/output.rb index 44ff2f68..6981c96a 100644 --- a/lib/common/collections/wp_items/output.rb +++ b/lib/common/collections/wp_items/output.rb @@ -3,8 +3,8 @@ class WpItems < Array module Output - def output - self.each { |item| item.output } + def output(verbose = false) + self.each { |item| item.output(verbose) } end end diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 39ed7990..4f0a1cca 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -177,3 +177,12 @@ end def count_file_lines(file) `wc -l #{file.shellescape}`.split[0].to_i end + +# Truncates a string to a specific length and adds ... at the end +def truncate(input, size, trailing = '...') + size = size.to_i + trailing ||= '' + return input if input.nil? or size <= 0 or input.length <= size or + trailing.length >= input.length or size-trailing.length-1 >= input.length + return "#{input[0..size-trailing.length-1]}#{trailing}" +end diff --git a/lib/common/models/vulnerability/output.rb b/lib/common/models/vulnerability/output.rb index 1a71051b..8f4aac2c 100644 --- a/lib/common/models/vulnerability/output.rb +++ b/lib/common/models/vulnerability/output.rb @@ -4,7 +4,7 @@ class Vulnerability module Output # output the vulnerability - def output + def output(verbose = false) puts ' |' puts ' | ' + red("* Title: #{title}") references.each do |key, urls| diff --git a/lib/common/models/wp_item/output.rb b/lib/common/models/wp_item/output.rb index f9370eb1..80c6daf7 100644 --- a/lib/common/models/wp_item/output.rb +++ b/lib/common/models/wp_item/output.rb @@ -4,7 +4,7 @@ class WpItem module Output # @return [ Void ] - def output + def output(verbose = false) puts puts " | Name: #{self}" #this will also output the version number if detected puts " | Location: #{url}" @@ -13,6 +13,10 @@ class WpItem puts " | Readme: #{readme_url}" if has_readme? puts " | Changelog: #{changelog_url}" if has_changelog? + if respond_to?(:additional_output) + additional_output(verbose) + end + vulnerabilities.output if has_error_log? diff --git a/lib/common/models/wp_theme.rb b/lib/common/models/wp_theme.rb index b6abbbfb..6cafa982 100755 --- a/lib/common/models/wp_theme.rb +++ b/lib/common/models/wp_theme.rb @@ -3,16 +3,28 @@ require 'wp_theme/findable' require 'wp_theme/versionable' require 'wp_theme/vulnerable' +require 'wp_theme/info' +require 'wp_theme/output' +require 'wp_theme/childtheme' class WpTheme < WpItem extend WpTheme::Findable include WpTheme::Versionable include WpTheme::Vulnerable + include WpTheme::Info + include WpTheme::Output + include WpTheme::Childtheme attr_writer :style_url def allowed_options; super << :style_url end + def initialize(*args) + super(*args) + + parse_style + end + # Sets the @uri # # @param [ URI ] target_base_uri The URI of the wordpress blog diff --git a/lib/common/models/wp_theme/childtheme.rb b/lib/common/models/wp_theme/childtheme.rb new file mode 100644 index 00000000..3e0944e8 --- /dev/null +++ b/lib/common/models/wp_theme/childtheme.rb @@ -0,0 +1,33 @@ +# encoding: UTF-8 + +class WpTheme < WpItem + module Childtheme + + def is_child_theme? + return true unless @theme_template.nil? + false + end + + def get_parent_theme_style_url + if is_child_theme? + return style_url.sub("/#{name}/style.css", "/#@theme_template/style.css") + end + nil + end + + def get_parent_theme + if is_child_theme? + base_url = @uri.clone + base_url.path = base_url.path.sub(/(?.*\/)#{Regexp.escape(@wp_content_dir)}\/.+/, '\k') + return WpTheme.new(base_url, + { + name: @theme_template, + style_url: get_parent_theme_style_url, + wp_content_dir: @wp_content_dir + }) + end + nil + end + + end +end diff --git a/lib/common/models/wp_theme/info.rb b/lib/common/models/wp_theme/info.rb new file mode 100644 index 00000000..3c456c89 --- /dev/null +++ b/lib/common/models/wp_theme/info.rb @@ -0,0 +1,34 @@ +# encoding: UTF-8 + +class WpTheme < WpItem + module Info + + attr_reader :theme_name, :theme_uri, :theme_description, + :theme_author, :theme_author_uri, :theme_template, + :theme_license, :theme_license_uri, :theme_tags, + :theme_text_domain + + def parse_style + style = Browser.get(style_url).body + @theme_name = parse_style_tag(style, 'Theme Name') + @theme_uri = parse_style_tag(style, 'Theme URI') + @theme_description = parse_style_tag(style, 'Description') + @theme_author = parse_style_tag(style, 'Author') + @theme_author_uri = parse_style_tag(style, 'Author URI') + @theme_template = parse_style_tag(style, 'Template') + @theme_license = parse_style_tag(style, 'License') + @theme_license_uri = parse_style_tag(style, 'License URI') + @theme_tags = parse_style_tag(style, 'Tags') + @theme_text_domain = parse_style_tag(style, 'Text Domain') + end + + private + + def parse_style_tag(style, tag) + value = style[/^\s*#{Regexp.escape(tag)}:\s*(.*)/i, 1] + return value.strip if value + nil + end + + end +end diff --git a/lib/common/models/wp_theme/output.rb b/lib/common/models/wp_theme/output.rb new file mode 100644 index 00000000..fd3c6f25 --- /dev/null +++ b/lib/common/models/wp_theme/output.rb @@ -0,0 +1,23 @@ +# encoding: UTF-8 + +class WpTheme + module Output + + # @return [ Void ] + def additional_output(verbose = false) + puts " | Style URL: #{style_url}" + puts " | Theme Name: #@theme_name" if @theme_name + puts " | Theme URI: #@theme_uri" if @theme_uri + theme_desc = verbose ? @theme_description : truncate(@theme_description, 100) + puts " | Description: #{theme_desc}" + puts " | Author: #@theme_author" if @theme_author + puts " | Author URI: #@theme_author_uri" if @theme_author_uri + puts " | Template: #@theme_template" if @theme_template and verbose + puts " | License: #@theme_license" if @theme_license and verbose + puts " | License URI: #@theme_license_uri" if @theme_license_uri and verbose + puts " | Tags: #@theme_tags" if @theme_tags and verbose + puts " | Text Domain: #@theme_text_domain" if @theme_text_domain and verbose + end + + end +end diff --git a/lib/common/models/wp_timthumb/output.rb b/lib/common/models/wp_timthumb/output.rb index 3d4f07a1..4b523164 100644 --- a/lib/common/models/wp_timthumb/output.rb +++ b/lib/common/models/wp_timthumb/output.rb @@ -3,7 +3,7 @@ class WpTimthumb < WpItem module Output - def output + def output(verbose = false) puts ' | ' + red('[!]') + " #{self}" end diff --git a/lib/common/models/wp_version/output.rb b/lib/common/models/wp_version/output.rb index 1154a295..979f1e75 100644 --- a/lib/common/models/wp_version/output.rb +++ b/lib/common/models/wp_version/output.rb @@ -3,7 +3,7 @@ class WpVersion < WpItem module Output - def output + def output(verbose = false) puts green('[+]') + " WordPress version #{self.number} identified from #{self.found_from}" vulnerabilities = self.vulnerabilities diff --git a/spec/lib/common/collections/wp_themes_spec.rb b/spec/lib/common/collections/wp_themes_spec.rb index 31843115..d173e396 100644 --- a/spec/lib/common/collections/wp_themes_spec.rb +++ b/spec/lib/common/collections/wp_themes_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe WpThemes do + before { stub_request(:get, /.+\/style.css$/).to_return(status: 200) } + it_behaves_like 'WpItems::Detectable' do subject(:wp_themes) { WpThemes } let(:item_class) { WpTheme } diff --git a/spec/lib/common/common_helper_spec.rb b/spec/lib/common/common_helper_spec.rb index 17c98063..a423a573 100644 --- a/spec/lib/common/common_helper_spec.rb +++ b/spec/lib/common/common_helper_spec.rb @@ -88,4 +88,83 @@ describe 'common_helper' do @expected = @html end end -end \ No newline at end of file + + describe '#truncate' do + after :each do + output = truncate(@input, @length, @trailing) + output.should == @expected + end + + it 'returns nil on no input' do + @input = nil + @length = 1 + @expected = nil + @trailing = '...' + end + + it 'returns input when length > input' do + @input = '1234567890' + @length = 13 + @expected = @input + @trailing = '...' + end + + it 'truncates the input' do + @input = '1234567890' + @length = 6 + @expected = '123...' + @trailing = '...' + end + + it 'adds own trailing' do + @input = '1234567890' + @length = 7 + @expected = '123xxxx' + @trailing = 'xxxx' + end + + it 'accepts strings as length' do + @input = '1234567890' + @length = '6' + @expected = '123...' + @trailing = '...' + end + + it 'checks if trailing is longer than input' do + @input = '1234567890' + @length = 1 + @expected = @input + @trailing = 'A' * 20 + end + + it 'returns input on negative length' do + @input = '1234567890' + @length = -1 + @expected = @input + @trailing = '...' + end + + it 'returns input on length == input.length' do + @input = '1234567890' + @length = '10' + @expected = @input + @trailing = '...' + end + + it 'returns cut string on nil trailing' do + @input = '1234567890' + @length = 9 + @expected = '123456789' + @trailing = nil + end + + it 'trailing.length > length' do + @input = '1234567890' + @length = 1 + @expected = @input + @trailing = 'A' * 20 + end + + end + +end diff --git a/spec/lib/common/models/wp_theme/findable_spec.rb b/spec/lib/common/models/wp_theme/findable_spec.rb index 85bd6b7c..58ed03ef 100644 --- a/spec/lib/common/models/wp_theme/findable_spec.rb +++ b/spec/lib/common/models/wp_theme/findable_spec.rb @@ -7,6 +7,10 @@ describe 'WpTheme::Findable' do let(:uri) { URI.parse('http://example.com/') } describe '::find_from_css_link' do + before do + stub_request(:get, /.+\/style.css$/).to_return(status: 200) + end + after do @body ||= File.new(fixtures_dir + '/css_link/' + @file) stub_request(:get, uri.to_s).to_return(status: 200, body: @body) @@ -51,6 +55,10 @@ describe 'WpTheme::Findable' do end describe '::find_from_wooframework' do + before do + stub_request(:get, /.+\/style.css$/).to_return(status: 200) + end + after do @body ||= File.new(fixtures_dir + '/wooframework/' + @file) stub_request(:get, uri.to_s).to_return(status: 200, body: @body) @@ -119,6 +127,7 @@ describe 'WpTheme::Findable' do context 'when the theme is found' do it 'returns it, with the :found_from set' do stub_all_to_nil() + stub_request(:get, /.+\/the-oracle\/style.css$/).to_return(status: 200) expected = WpTheme.new(uri, name: 'the-oracle') WpTheme.stub(:find_from_css_link).and_return(expected) diff --git a/spec/lib/common/models/wp_theme_spec.rb b/spec/lib/common/models/wp_theme_spec.rb index 19cd1536..cf5174b3 100644 --- a/spec/lib/common/models/wp_theme_spec.rb +++ b/spec/lib/common/models/wp_theme_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe WpTheme do + before do + stub_request(:get, /.+\/style.css$/).to_return(status: 200) + end + it_behaves_like 'WpTheme::Versionable' it_behaves_like 'WpTheme::Vulnerable' it_behaves_like 'WpItem::Vulnerable' do diff --git a/wpscan.rb b/wpscan.rb index 5547535b..263322d8 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -186,14 +186,24 @@ def main } if wp_version = wp_target.version(WP_VERSIONS_FILE) - wp_version.output + wp_version.output(wpscan_options.verbose) end if wp_theme = wp_target.theme puts # Theme version is handled in #to_s puts green('[+]') + " WordPress theme in use: #{wp_theme}" - wp_theme.output + wp_theme.output(wpscan_options.verbose) + + # Check for parent Themes + while wp_theme.is_child_theme? + parent = wp_theme.get_parent_theme + puts + puts green('[+]') + " Detected parent theme: #{parent}" + parent.output(wpscan_options.verbose) + wp_theme = parent + end + end if wpscan_options.enumerate_plugins == nil and wpscan_options.enumerate_only_vulnerable_plugins == nil @@ -204,7 +214,7 @@ def main if !wp_plugins.empty? puts " | #{wp_plugins.size} plugins found:" - wp_plugins.output + wp_plugins.output(wpscan_options.verbose) else puts 'No plugins found' end @@ -226,7 +236,7 @@ def main if !wp_plugins.empty? puts green('[+]') + " We found #{wp_plugins.size} plugins:" - wp_plugins.output + wp_plugins.output(wpscan_options.verbose) else puts 'No plugins found' end @@ -248,7 +258,7 @@ def main if !wp_themes.empty? puts green('[+]') + " We found #{wp_themes.size} themes:" - wp_themes.output + wp_themes.output(wpscan_options.verbose) else puts 'No themes found' end @@ -270,7 +280,7 @@ def main puts green('[+]') + " We found #{wp_timthumbs.size} timthumb file/s:" puts - wp_timthumbs.output + wp_timthumbs.output(wpscan_options.verbose) puts puts red(' * Reference: http://www.exploit-db.com/exploits/17602/')