wordstress 0.30.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/wordstress +68 -43
- data/lib/wordstress/site.rb +185 -59
- data/lib/wordstress/utils.rb +23 -1
- data/lib/wordstress/version.rb +1 -1
- data/wordstress.gemspec +3 -2
- metadata +18 -4
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 8d1d25228ca184bd9640ee87d516d4f5ab6b4c73
         | 
| 4 | 
            +
              data.tar.gz: cbeb681f9ddf891f3a67f34b03960a49c63b9e57
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: b7163eef62c310c477d7d247446ab7484ecda6e45543d3fc7d2a5c678d11e758aec38f039ebfdfe57cbfb15242522d71ea70c3687dd33da79da06bdc83522e77
         | 
| 7 | 
            +
              data.tar.gz: 8dfceb6b7446fbb0282ddf24fc42ff6640df1238fb13971d65c3d7a4f855b41ca872d6a05f65e471e1953cbe687316e509bd9768ee6ea06c56643e582ec3ae66
         | 
    
        data/bin/wordstress
    CHANGED
    
    | @@ -2,6 +2,7 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            require 'getoptlong'
         | 
| 4 4 | 
             
            require 'json'
         | 
| 5 | 
            +
            require 'fileutils'
         | 
| 5 6 | 
             
            require 'codesake-commons'
         | 
| 6 7 |  | 
| 7 8 | 
             
            require 'wordstress'
         | 
| @@ -9,26 +10,32 @@ require 'wordstress' | |
| 9 10 | 
             
            # Scanning modes for plugins and themes
         | 
| 10 11 | 
             
            #   + gentleman: wordstress will try to fetch plugins and themes only using
         | 
| 11 12 | 
             
            #     info in the HTML page (this is very polite but also very inaccurate).
         | 
| 12 | 
            -
            #   +  | 
| 13 | 
            +
            #   + whitebox: wordstress will use a target installed plugin to fetch
         | 
| 13 14 | 
             
            #     installed plugins and themes with their version
         | 
| 14 15 | 
             
            #   + aggressive: wordstress will use enumeration to find installed plugins and
         | 
| 15 16 | 
             
            #     themes. This will lead to false positives.
         | 
| 16 | 
            -
            MODES = [:gentleman,: | 
| 17 | 
            +
            MODES = [:gentleman,:whitebox,:aggressive]
         | 
| 17 18 | 
             
            APPNAME = File.basename($0)
         | 
| 18 19 |  | 
| 19 20 | 
             
            $logger  = Codesake::Commons::Logging.instance
         | 
| 21 | 
            +
            $logger.debug = false
         | 
| 22 | 
            +
            # $logger.toggle_silence
         | 
| 20 23 | 
             
            @output_root = File.join(Dir.home, '/wordstress')
         | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 24 | 
            +
             | 
| 25 | 
            +
            scanning_mode = :whitebox
         | 
| 26 | 
            +
            whitebox = {:url=>"", :key=>""}
         | 
| 27 | 
            +
            basic_auth  = {:user=>"", :pwd=>""}
         | 
| 23 28 |  | 
| 24 29 | 
             
            opts    = GetoptLong.new(
         | 
| 25 | 
            -
              [ '--gentleman', | 
| 26 | 
            -
              [ '-- | 
| 27 | 
            -
              [ '-- | 
| 28 | 
            -
              [ '-- | 
| 29 | 
            -
              [ '-- | 
| 30 | 
            -
              [ '-- | 
| 31 | 
            -
              [ '-- | 
| 30 | 
            +
              [ '--gentleman',          '-G',   GetoptLong::NO_ARGUMENT],
         | 
| 31 | 
            +
              [ '--basic-auth',         '-B',   GetoptLong::REQUIRED_ARGUMENT],
         | 
| 32 | 
            +
              [ '--whitebox' ,          '-W',   GetoptLong::NO_ARGUMENT],
         | 
| 33 | 
            +
              [ '--wordstress-url',     '-u', GetoptLong::REQUIRED_ARGUMENT],
         | 
| 34 | 
            +
              [ '--wordstress-api-key', '-k', GetoptLong::REQUIRED_ARGUMENT],
         | 
| 35 | 
            +
              [ '--csv',                '-C',   GetoptLong::NO_ARGUMENT],
         | 
| 36 | 
            +
              [ '--debug',              '-D',   GetoptLong::NO_ARGUMENT],
         | 
| 37 | 
            +
              [ '--version',            '-v',   GetoptLong::NO_ARGUMENT],
         | 
| 38 | 
            +
              [ '--help',               '-h',   GetoptLong::NO_ARGUMENT]
         | 
| 32 39 | 
             
            )
         | 
| 33 40 |  | 
| 34 41 | 
             
            opts.quiet=true
         | 
| @@ -36,15 +43,20 @@ opts.quiet=true | |
| 36 43 | 
             
            begin
         | 
| 37 44 | 
             
              opts.each do |opt, val|
         | 
| 38 45 | 
             
                case opt
         | 
| 39 | 
            -
                when '-- | 
| 40 | 
            -
                   | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 46 | 
            +
                when '--basic-auth'
         | 
| 47 | 
            +
                  basic_auth[:user] = val.split(':')[0]
         | 
| 48 | 
            +
                  basic_auth[:pwd] = val.split(':')[1]
         | 
| 49 | 
            +
                when '--whitebox'
         | 
| 50 | 
            +
                  scanning_mode = :whitebox
         | 
| 51 | 
            +
                when '--wordstress-url'
         | 
| 52 | 
            +
                  whitebox[:url] = val
         | 
| 53 | 
            +
                when '--wordstress-api-key'
         | 
| 54 | 
            +
                  whitebox[:key] = val
         | 
| 45 55 | 
             
                when '--version'
         | 
| 46 56 | 
             
                  puts "#{Wordstress::VERSION}"
         | 
| 47 57 | 
             
                  Kernel.exit(0)
         | 
| 58 | 
            +
                when '--debug'
         | 
| 59 | 
            +
                  $logger.debug = true
         | 
| 48 60 | 
             
                when '--help'
         | 
| 49 61 | 
             
                  Kernel.exit(0)
         | 
| 50 62 | 
             
                end
         | 
| @@ -55,55 +67,68 @@ rescue GetoptLong::InvalidOption => e | |
| 55 67 | 
             
              Kernel.exit(-1)
         | 
| 56 68 | 
             
            end
         | 
| 57 69 |  | 
| 58 | 
            -
            target=ARGV.shift
         | 
| 70 | 
            +
            target=ARGV.shift unless scanning_mode == :whitebox
         | 
| 71 | 
            +
            target=whitebox[:url] if scanning_mode == :whitebox
         | 
| 72 | 
            +
             | 
| 73 | 
            +
             | 
| 59 74 | 
             
            $logger.helo APPNAME, Wordstress::VERSION
         | 
| 60 | 
            -
            $logger.toggle_syslog
         | 
| 61 | 
            -
            @output_dir = File.join(@output_root, Wordstress::Utils.target_to_dirname(target))
         | 
| 62 75 |  | 
| 63 76 | 
             
            unless Dir.exists?(@output_root)
         | 
| 64 | 
            -
              $logger. | 
| 77 | 
            +
              $logger.log "creating output dir #{@output_root}"
         | 
| 65 78 | 
             
              Dir.mkdir @output_root
         | 
| 66 79 | 
             
            end
         | 
| 67 80 |  | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 81 | 
            +
            @output_dir = Wordstress::Utils.build_output_dir(@output_root, target)
         | 
| 82 | 
            +
            $logger.log "storing results to #{@output_dir}"
         | 
| 83 | 
            +
            FileUtils::mkdir_p(@output_dir)
         | 
| 71 84 |  | 
| 72 85 | 
             
            trap("INT")   { $logger.die('[INTERRUPTED]') }
         | 
| 73 86 | 
             
            $logger.die("missing target") if target.nil?
         | 
| 74 87 |  | 
| 75 88 | 
             
            $logger.log "scanning #{target}"
         | 
| 76 | 
            -
            site = Wordstress::Site.new({:target=>target, :scanning_mode=>scanning_mode, : | 
| 89 | 
            +
            site = Wordstress::Site.new({:target=>target, :scanning_mode=>scanning_mode, :whitebox=>whitebox,:basic_auth=>basic_auth, :output_dir=>@output_dir})
         | 
| 77 90 |  | 
| 78 91 | 
             
            if site.version[:version] == "0.0.0"
         | 
| 79 92 | 
             
              $logger.err "can't detect wordpress version running on #{target}. Giving up!"
         | 
| 80 93 | 
             
              Kernel.exit(-2)
         | 
| 81 94 | 
             
            end
         | 
| 82 95 |  | 
| 83 | 
            -
            $logger.ok "wordpress version #{site.version[:version]}  | 
| 96 | 
            +
            $logger.ok "#{target} is a wordpress version #{site.version[:version]} with #{site.themes.count} themes and #{site.plugins.count} plugins"
         | 
| 84 97 | 
             
            $logger.warn "scan mode is set to 'gentleman'. We are using only information found on resulting HTML. This can be lead to undetected plugins or themes" if site.scanning_mode == :gentleman
         | 
| 98 | 
            +
             | 
| 85 99 | 
             
            if site.online?
         | 
| 86 | 
            -
               | 
| 87 | 
            -
             | 
| 88 | 
            -
               | 
| 89 | 
            -
             | 
| 100 | 
            +
              site.wp_vuln["wordpress"]["vulnerabilities"].each do |v|
         | 
| 101 | 
            +
                $logger.err "#{v["title"]}. Detected: #{site.version[:version]}. Safe: #{v["fixed_in"]}" if Gem::Version.new(site.version[:version]) <= Gem::Version.new(v["fixed_in"])
         | 
| 102 | 
            +
              end
         | 
| 103 | 
            +
              site.themes.each do |t|
         | 
| 104 | 
            +
                v = site.get_theme_vulnerabilities(t[:name])
         | 
| 105 | 
            +
                unless v["theme"].nil?
         | 
| 106 | 
            +
                  v["theme"]["vulnerabilities"].each do |vv|
         | 
| 107 | 
            +
                    if Gem::Version.new(t[:version]) <= Gem::Version.new(vv["fixed_in"])
         | 
| 108 | 
            +
                      $logger.err "#{vv["title"]}. Detected: #{t[:version]}. Safe: #{vv["fixed_in"]}"
         | 
| 109 | 
            +
                      site.theme_vulns << {:title=>vv["title"], :cve=>vv["cve"], :url=>vv["url"], :detected=>t[:version], :fixed_in=>vv["fixed_in"]}
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
              end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
              site.plugins.each do |t|
         | 
| 116 | 
            +
                v = site.get_plugin_vulnerabilities(t[:name])
         | 
| 117 | 
            +
                unless v["plugin"].nil?
         | 
| 118 | 
            +
                  v["plugin"]["vulnerabilities"].each do |vv|
         | 
| 119 | 
            +
                    if Gem::Version.new(t[:version]) <= Gem::Version.new(vv["fixed_in"])
         | 
| 120 | 
            +
                      $logger.err "#{vv["title"]}. Detected: #{t[:version]}. Safe: #{vv["fixed_in"]}"
         | 
| 121 | 
            +
                      site.plugin_vulns << {:title=>vv["title"], :cve=>vv["cve"], :url=>vv["url"], :detected=>t[:version], :fixed_in=>vv["fixed_in"]}
         | 
| 122 | 
            +
                    end
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                end
         | 
| 90 125 | 
             
              end
         | 
| 91 126 | 
             
            else
         | 
| 127 | 
            +
              site.online = false
         | 
| 92 128 | 
             
              $logger.err "it seems we are offline. wordstress can't reach https://wpvulndb.com"
         | 
| 93 129 | 
             
              $logger.err "wordpress can't enumerate vulnerabilities"
         | 
| 94 130 | 
             
            end
         | 
| 95 | 
            -
            $logger.log "#{target} has #{site.themes.count} themes"
         | 
| 96 | 
            -
            site.themes.each do |t|
         | 
| 97 | 
            -
              $logger.log "searching wpvulndb for theme #{t[:name]} vulnerabilities"
         | 
| 98 | 
            -
              v = site.get_theme_vulnerabilities(t[:name])
         | 
| 99 | 
            -
              $logger.debug v
         | 
| 100 | 
            -
            end
         | 
| 101 | 
            -
             | 
| 102 | 
            -
            $logger.log "#{target} has #{site.plugins.count} plugins"
         | 
| 103 | 
            -
            site.plugins.each do |t|
         | 
| 104 | 
            -
              $logger.log "searching wpvulndb for plugin #{t[:name]} vulnerabilities"
         | 
| 105 | 
            -
              v = site.get_plugin_vulnerabilities(t[:name])
         | 
| 106 | 
            -
              $logger.debug v
         | 
| 107 | 
            -
            end
         | 
| 108 131 |  | 
| 132 | 
            +
            site.stop_scan
         | 
| 133 | 
            +
            site.ascii_report
         | 
| 109 134 | 
             
            $logger.bye
         | 
    
        data/lib/wordstress/site.rb
    CHANGED
    
    | @@ -1,11 +1,14 @@ | |
| 1 1 | 
             
            require 'net/http'
         | 
| 2 | 
            +
            require 'terminal-table'
         | 
| 2 3 |  | 
| 3 4 | 
             
            module Wordstress
         | 
| 4 5 | 
             
              class Site
         | 
| 5 6 |  | 
| 6 | 
            -
                attr_reader :version, :scanning_mode, : | 
| 7 | 
            +
                attr_reader :version, :scanning_mode, :wp_vuln, :plugins, :themes
         | 
| 8 | 
            +
                attr_accessor :theme_vulns, :plugin_vulns, :online
         | 
| 7 9 |  | 
| 8 | 
            -
                def initialize(options={:target=>"http://localhost", :scanning_mode=>:gentleman, : | 
| 10 | 
            +
                def initialize(options={:target=>"http://localhost", :scanning_mode=>:gentleman, :whitebox=>{}, :basic_auth=>{:user=>"", :pwd=>""}, :output_dir=>"./"})
         | 
| 11 | 
            +
                  @target     = options[:target]
         | 
| 9 12 | 
             
                  begin
         | 
| 10 13 | 
             
                    @uri      = URI(options[:target])
         | 
| 11 14 | 
             
                    @raw_name = options[:target]
         | 
| @@ -13,80 +16,105 @@ module Wordstress | |
| 13 16 | 
             
                  rescue
         | 
| 14 17 | 
             
                    @valid = false
         | 
| 15 18 | 
             
                  end
         | 
| 16 | 
            -
                  @scanning_mode | 
| 19 | 
            +
                  @scanning_mode    = options[:scanning_mode]
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  @basic_auth_user  = options[:basic_auth][:user]
         | 
| 22 | 
            +
                  @basic_auth_pwd   = options[:basic_auth][:pwd]
         | 
| 23 | 
            +
                  @output_dir       = options[:output_dir]
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  @start_time       = Time.now
         | 
| 26 | 
            +
                  @end_time         = Time.now # I hate init variables to nil...
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  unless scanning_mode == :whitebox
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    @robots_txt   = get(@raw_name + "/robots.txt")
         | 
| 31 | 
            +
                    @readme_html  = get(@raw_name + "/readme.html")
         | 
| 32 | 
            +
                    @homepage     = get(@raw_name)
         | 
| 33 | 
            +
                    @version      = detect_version(@homepage, false)
         | 
| 34 | 
            +
                  else
         | 
| 35 | 
            +
                    @wordstress_page  = get("#{options[:whitebox][:url]}?wordstress-key=#{options[:whitebox][:key]}") if options[:scanning_mode] == :whitebox
         | 
| 36 | 
            +
                    @version          = detect_version(@wordstress_page, true)
         | 
| 37 | 
            +
                  end
         | 
| 17 38 |  | 
| 18 | 
            -
                  @robots_txt   = get(@raw_name + "/robots.txt")
         | 
| 19 | 
            -
                  @readme_html  = get(@raw_name + "/readme.html")
         | 
| 20 | 
            -
                  @homepage     = get(@raw_name)
         | 
| 21 | 
            -
                  @version      = detect_version
         | 
| 22 39 | 
             
                  @online       = true
         | 
| 23 40 |  | 
| 24 | 
            -
                  @ | 
| 25 | 
            -
                  @ | 
| 41 | 
            +
                  @wp_vuln = get_wp_vulnerabilities  unless @version[:version] == "0.0.0"
         | 
| 42 | 
            +
                  @wp_vuln = JSON.parse("{}")        if @version[:version] == "0.0.0"
         | 
| 26 43 |  | 
| 27 | 
            -
                  @wordstress_page = post_and_get(options[:interactive][:url], options[:interactive][:pwd]) if options[:scanning_mode] == :interactive && !options[:interactive][:pwd].empty?
         | 
| 28 | 
            -
                  @wordstress_page = get(options[:interactive][:url]) if options[:scanning_mode] == :interactive && options[:interactive][:pwd].empty?
         | 
| 29 44 | 
             
                  @plugins      = find_plugins
         | 
| 30 45 | 
             
                  @themes       = find_themes
         | 
| 31 | 
            -
                   | 
| 46 | 
            +
                  @theme_vulns  = []
         | 
| 47 | 
            +
                  @plugin_vulns = []
         | 
| 32 48 | 
             
                end
         | 
| 33 49 |  | 
| 34 | 
            -
                def  | 
| 35 | 
            -
                   | 
| 36 | 
            -
                  @themes.each do |t|
         | 
| 37 | 
            -
                    vuln << {:theme=>t, :vulns=>get_theme_vulnerabilities(t)}
         | 
| 38 | 
            -
                  end
         | 
| 50 | 
            +
                def stop_scan
         | 
| 51 | 
            +
                  @end_time = Time.now
         | 
| 39 52 | 
             
                end
         | 
| 40 53 |  | 
| 41 54 | 
             
                def get_plugin_vulnerabilities(theme)
         | 
| 42 55 | 
             
                  begin
         | 
| 43 56 | 
             
                    json= get_https("https://wpvulndb.com/api/v1/plugins/#{theme}").body
         | 
| 44 | 
            -
                    return " | 
| 45 | 
            -
                    return json
         | 
| 57 | 
            +
                    return JSON.parse("{\"plugin\":{\"vulnerabilities\":[]}}") if json.include?"The page you were looking for doesn't exist (404)"
         | 
| 58 | 
            +
                    return JSON.parse(json)
         | 
| 46 59 | 
             
                  rescue => e
         | 
| 47 60 | 
             
                    $logger.err e.message
         | 
| 48 61 | 
             
                    @online = false
         | 
| 49 | 
            -
                    return  | 
| 62 | 
            +
                    return JSON.parse("{}")
         | 
| 50 63 | 
             
                  end
         | 
| 51 64 | 
             
                end
         | 
| 52 65 |  | 
| 53 66 | 
             
                def get_theme_vulnerabilities(theme)
         | 
| 54 67 | 
             
                  begin
         | 
| 55 68 | 
             
                    json=get_https("https://wpvulndb.com/api/v1/themes/#{theme}").body
         | 
| 56 | 
            -
                    return " | 
| 57 | 
            -
                    return json
         | 
| 69 | 
            +
                    return JSON.parse("{\"theme\":{\"vulnerabilities\":[]}}") if json.include?"The page you were looking for doesn't exist (404)"
         | 
| 70 | 
            +
                    return JSON.parse(json)
         | 
| 58 71 | 
             
                  rescue => e
         | 
| 59 72 | 
             
                    $logger.err e.message
         | 
| 60 73 | 
             
                    @online = false
         | 
| 61 | 
            -
                    return  | 
| 74 | 
            +
                    return JSON.parse("{}")
         | 
| 62 75 | 
             
                  end
         | 
| 63 76 | 
             
                end
         | 
| 64 77 |  | 
| 65 78 | 
             
                def get_wp_vulnerabilities
         | 
| 66 79 | 
             
                  begin
         | 
| 67 | 
            -
                     | 
| 80 | 
            +
                    page= get_https("https://wpvulndb.com/api/v1/wordpresses/#{version_pad(@version[:version])}")
         | 
| 81 | 
            +
                    return JSON.parse(page.body) unless page.class == Net::HTTPNotFound
         | 
| 82 | 
            +
                    return JSON.parse("{\"wordpress\":{\"vulnerabilities\":[]}}") if page.class == Net::HTTPNotFound
         | 
| 68 83 | 
             
                  rescue => e
         | 
| 69 84 | 
             
                    $logger.err e.message
         | 
| 70 85 | 
             
                    @online = false
         | 
| 71 | 
            -
                    return ""
         | 
| 86 | 
            +
                    return JSON.parse("{}")
         | 
| 72 87 | 
             
                  end
         | 
| 73 88 | 
             
                end
         | 
| 74 89 |  | 
| 75 90 | 
             
                def version_pad(version)
         | 
| 76 | 
            -
                  #  | 
| 77 | 
            -
                  #  | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 91 | 
            +
                  return version.gsub('.', '')      #if version.split('.').count == 3
         | 
| 92 | 
            +
                  # return version.gsub('.', '')+'0'  if version.split('.').count == 2
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                def detect_version(page, whitebox=false)
         | 
| 96 | 
            +
                  detect_version_blackbox(page) unless whitebox
         | 
| 97 | 
            +
                  detect_version_whitebox(page) if whitebox
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def detect_version_whitebox(page)
         | 
| 101 | 
            +
                  v_meta = '0.0.0'
         | 
| 102 | 
            +
                  doc = Nokogiri::HTML(page.body)
         | 
| 103 | 
            +
                  doc.css('#wp_version').each do |link|
         | 
| 104 | 
            +
                    v_meta = link.text
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  return {:version => v_meta, :accuracy => 1.0}
         | 
| 80 108 | 
             
                end
         | 
| 81 109 |  | 
| 82 | 
            -
                def  | 
| 110 | 
            +
                def detect_version_blackbox(page)
         | 
| 83 111 |  | 
| 84 112 | 
             
                  #
         | 
| 85 113 | 
             
                  # 1. trying to detect wordpress version from homepage body meta generator
         | 
| 86 114 | 
             
                  # tag
         | 
| 87 115 |  | 
| 88 116 | 
             
                  v_meta = ""
         | 
| 89 | 
            -
                  doc = Nokogiri::HTML( | 
| 117 | 
            +
                  doc = Nokogiri::HTML(page.body)
         | 
| 90 118 | 
             
                  doc.xpath("//meta[@name='generator']/@content").each do |attr|
         | 
| 91 119 | 
             
                    v_meta = attr.value.split(' ')[1]
         | 
| 92 120 | 
             
                  end
         | 
| @@ -94,13 +122,20 @@ module Wordstress | |
| 94 122 | 
             
                  #
         | 
| 95 123 | 
             
                  # 2. trying to detect wordpress version from readme.html in the root
         | 
| 96 124 | 
             
                  # directory
         | 
| 125 | 
            +
                  #
         | 
| 126 | 
            +
                  # Not available if scanning 
         | 
| 97 127 |  | 
| 98 | 
            -
                   | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 128 | 
            +
                  unless whitebox
         | 
| 129 | 
            +
                    v_readme = ""
         | 
| 130 | 
            +
                    doc = Nokogiri::HTML(@readme_html.body)
         | 
| 131 | 
            +
                    v_readme = doc.at_css('h1').children.last.text.chop.lstrip.split(' ')[1]
         | 
| 132 | 
            +
                  end
         | 
| 101 133 |  | 
| 134 | 
            +
                  #
         | 
| 135 | 
            +
                  # 3. Detect from RSS link
         | 
| 136 | 
            +
                  #
         | 
| 102 137 | 
             
                  v_rss = ""
         | 
| 103 | 
            -
                  rss_doc = Nokogiri::HTML( | 
| 138 | 
            +
                  rss_doc = Nokogiri::HTML(page.body)
         | 
| 104 139 | 
             
                  begin
         | 
| 105 140 | 
             
                    rss = Nokogiri::HTML(get(rss_doc.css('link[type="application/rss+xml"]').first.attr('href')).body) unless l.nil?
         | 
| 106 141 | 
             
                    v_rss= rss.css('generator').text.split('=')[1]
         | 
| @@ -130,12 +165,12 @@ module Wordstress | |
| 130 165 |  | 
| 131 166 | 
             
                def find_themes
         | 
| 132 167 | 
             
                  return find_themes_gentleman  if @scanning_mode == :gentleman
         | 
| 133 | 
            -
                  return  | 
| 168 | 
            +
                  return find_themes_whitebox if @scanning_mode == :whitebox
         | 
| 134 169 | 
             
                  return []
         | 
| 135 170 | 
             
                end
         | 
| 136 171 | 
             
                def find_plugins
         | 
| 137 172 | 
             
                  return find_plugins_gentleman if @scanning_mode == :gentleman
         | 
| 138 | 
            -
                  return  | 
| 173 | 
            +
                  return find_plugins_whitebox if @scanning_mode == :whitebox
         | 
| 139 174 |  | 
| 140 175 | 
             
                  # bruteforce check must start with error page discovery.
         | 
| 141 176 | 
             
                  # the idea is to send 2 random plugin names (e.g. 2 sha256 of time seed)
         | 
| @@ -144,11 +179,89 @@ module Wordstress | |
| 144 179 | 
             
                  return []
         | 
| 145 180 | 
             
                end
         | 
| 146 181 |  | 
| 147 | 
            -
                def  | 
| 148 | 
            -
                   | 
| 149 | 
            -
                   | 
| 150 | 
            -
                   | 
| 151 | 
            -
                   | 
| 182 | 
            +
                def ascii_report
         | 
| 183 | 
            +
                  # 0_Executive summary
         | 
| 184 | 
            +
                  rows = []
         | 
| 185 | 
            +
                  rows << ['Wordstress version', Wordstress::VERSION]
         | 
| 186 | 
            +
                  rows << ['Scan started',@start_time]
         | 
| 187 | 
            +
                  rows << ['Scan duration', "#{(@end_time - @start_time).round(3)} sec"]
         | 
| 188 | 
            +
                  rows << ['Target', @target]
         | 
| 189 | 
            +
                  rows << ['Wordpress version', version[:version]]
         | 
| 190 | 
            +
                  unless @online
         | 
| 191 | 
            +
                    rows << ['Scan status', 'During scan wordstress went offline. Results are incomplete / unreliable. Please make sure you are connected to the Internet']
         | 
| 192 | 
            +
                  else
         | 
| 193 | 
            +
                    rows << ['Scan status', 'Scan completed successfully']
         | 
| 194 | 
            +
                  end
         | 
| 195 | 
            +
                  table = Terminal::Table.new :title=>'Scan summary', :rows => rows
         | 
| 196 | 
            +
                  puts table
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                  return table unless @online
         | 
| 199 | 
            +
                  # 1_vulnerability summary
         | 
| 200 | 
            +
                  rows = []
         | 
| 201 | 
            +
                  rows << ['Wordpress version', @wp_vuln["wordpress"]["vulnerabilities"].count]
         | 
| 202 | 
            +
                  rows << ['Plugins installed', @plugin_vulns.count]
         | 
| 203 | 
            +
                  rows << ['Themes installed', @theme_vulns.count]
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                  table = Terminal::Table.new :title=>'Vulnerabilities found', :rows => rows
         | 
| 206 | 
            +
                  puts table
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                  # 2_vulnerabilities detail
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                  if @wp_vuln["wordpress"]["vulnerabilities"].count != 0
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                    rows = []
         | 
| 213 | 
            +
                    @wp_vuln["wordpress"]["vulnerabilities"].each do |v|
         | 
| 214 | 
            +
                      rows << [v[:title], v[:cve], v[:url], v[:fixed_in]]
         | 
| 215 | 
            +
                      rows << :separator
         | 
| 216 | 
            +
                    end
         | 
| 217 | 
            +
                    table = Terminal::Table.new :title=>"Vulnerabilities in Wordpress version #{version[:version]}", :headings=>['Issue', 'CVE', 'Url', 'Fixed in version'], :rows=>rows
         | 
| 218 | 
            +
                    puts table
         | 
| 219 | 
            +
                  end
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                  if @plugin_vulns.count != 0
         | 
| 222 | 
            +
                    rows = []
         | 
| 223 | 
            +
                    @plugin_vulns.each do |v|
         | 
| 224 | 
            +
                      rows << [v[:title], v[:cve], v[:detected], v[:fixed_in]]
         | 
| 225 | 
            +
                      rows << :separator
         | 
| 226 | 
            +
                    end
         | 
| 227 | 
            +
                    table = Terminal::Table.new :title=>"Vulnerabilities in installed plugins", :headings=>['Issue', 'CVE', 'Detected version', 'Fixed version'], :rows=>rows
         | 
| 228 | 
            +
                    puts table
         | 
| 229 | 
            +
                  end
         | 
| 230 | 
            +
             | 
| 231 | 
            +
                  if @theme_vulns.count != 0
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                    rows = []
         | 
| 234 | 
            +
                    @theme_vulns.each do |v|
         | 
| 235 | 
            +
                      rows << [v[:title], v[:cve], v[:detected], v[:fixed_in]]
         | 
| 236 | 
            +
                      rows << :separator
         | 
| 237 | 
            +
                    end
         | 
| 238 | 
            +
                    table = Terminal::Table.new :title=>"Vulnerabilities in installed themes", :headings=>['Issue', 'CVE', 'Detected version', 'Fixed in version'], :rows=>rows
         | 
| 239 | 
            +
                    puts table
         | 
| 240 | 
            +
                  end
         | 
| 241 | 
            +
             | 
| 242 | 
            +
             | 
| 243 | 
            +
             | 
| 244 | 
            +
             | 
| 245 | 
            +
             | 
| 246 | 
            +
                  # File.open(File.join(@output_dir, "report.txt"), 'w') do |file|
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                  # file.puts("target: #{@target}")
         | 
| 249 | 
            +
                  # file.puts("wordpress version: #{version[:version]}")
         | 
| 250 | 
            +
                  # file.puts("themes found: #{@themes.count}")
         | 
| 251 | 
            +
                    # file.puts("plugins found: #{@plugins.count}")
         | 
| 252 | 
            +
                    # file.puts("Vulnerabilities in wordpress")
         | 
| 253 | 
            +
                    # @wp_vuln["wordpress"]["vulnerabilities"].each do |v|
         | 
| 254 | 
            +
                      # file.puts "#{v[:title]} - fixed in #{v[:fixed_in]}"
         | 
| 255 | 
            +
                    # end
         | 
| 256 | 
            +
                    # file.puts("Vulnerabilities in themes")
         | 
| 257 | 
            +
                    # @theme_vulns.each do |v|
         | 
| 258 | 
            +
                      # file.puts "#{v[:title]} - fixed in #{v[:fixed_in]}"
         | 
| 259 | 
            +
                    # end
         | 
| 260 | 
            +
                    # file.puts("Vulnerabilities in plugins")
         | 
| 261 | 
            +
                    # @plugins_vulns.each do |v|
         | 
| 262 | 
            +
                      # file.puts "#{v[:title]} - fixed in #{v[:fixed_in]}"
         | 
| 263 | 
            +
                    # end
         | 
| 264 | 
            +
                  # end
         | 
| 152 265 | 
             
                end
         | 
| 153 266 |  | 
| 154 267 | 
             
                private
         | 
| @@ -158,24 +271,22 @@ module Wordstress | |
| 158 271 | 
             
                  return (!a.nil?)
         | 
| 159 272 | 
             
                end
         | 
| 160 273 |  | 
| 161 | 
            -
                def  | 
| 274 | 
            +
                def find_plugins_whitebox
         | 
| 162 275 | 
             
                  ret = []
         | 
| 163 276 | 
             
                  doc = Nokogiri::HTML(@wordstress_page.body)
         | 
| 164 277 | 
             
                  doc.css('#all_plugin').each do |link|
         | 
| 165 278 | 
             
                    l=link.text.split(',')
         | 
| 166 | 
            -
                    ret << {:name=>l[2], :version=>l[1], :status=>l[3]} unless is_already_detected?(ret, l[2])
         | 
| 279 | 
            +
                    ret << {:name=>l[2].split('/')[0], :version=>l[1], :status=>l[3]} unless is_already_detected?(ret, l[2])
         | 
| 167 280 | 
             
                  end
         | 
| 168 | 
            -
                  $logger.debug ret
         | 
| 169 281 | 
             
                  ret
         | 
| 170 282 | 
             
                end
         | 
| 171 | 
            -
                def  | 
| 283 | 
            +
                def find_themes_whitebox
         | 
| 172 284 | 
             
                  ret = []
         | 
| 173 285 | 
             
                  doc = Nokogiri::HTML(@wordstress_page.body)
         | 
| 174 286 | 
             
                  doc.css('#all_theme').each do |link|
         | 
| 175 287 | 
             
                    l=link.text.split(',')
         | 
| 176 | 
            -
                    ret << {:name=>l[2], :version=>l[1]} unless is_already_detected?(ret, l[2])
         | 
| 288 | 
            +
                    ret << {:name=>l[2], :version=>l[1], :status=>l[3]} unless is_already_detected?(ret, l[2])
         | 
| 177 289 | 
             
                  end
         | 
| 178 | 
            -
                  $logger.debug ret
         | 
| 179 290 | 
             
                  ret
         | 
| 180 291 | 
             
                end
         | 
| 181 292 |  | 
| @@ -220,19 +331,34 @@ module Wordstress | |
| 220 331 | 
             
                  ret
         | 
| 221 332 | 
             
                end
         | 
| 222 333 |  | 
| 223 | 
            -
                def get_http(page)
         | 
| 224 | 
            -
                  uri = URI | 
| 225 | 
            -
                   | 
| 226 | 
            -
                   | 
| 227 | 
            -
             | 
| 334 | 
            +
                def get_http(page, use_ssl=false)
         | 
| 335 | 
            +
                  uri = URI(page)
         | 
| 336 | 
            +
                  req = Net::HTTP::Get.new(uri)
         | 
| 337 | 
            +
                  req.basic_auth @basic_auth_user, @basic_auth_pwd unless @basic_auth_user == ""
         | 
| 338 | 
            +
             | 
| 339 | 
            +
             | 
| 340 | 
            +
                  http = Net::HTTP.new(uri.hostname, uri.port)
         | 
| 341 | 
            +
                  http.use_ssl = use_ssl
         | 
| 342 | 
            +
             | 
| 343 | 
            +
                  res = http.start {|h|
         | 
| 344 | 
            +
                    h.request(req)
         | 
| 345 | 
            +
                  }
         | 
| 346 | 
            +
                  case res
         | 
| 347 | 
            +
                  when Net::HTTPSuccess then
         | 
| 348 | 
            +
                    return res
         | 
| 349 | 
            +
                  when Net::HTTPRedirection then
         | 
| 350 | 
            +
                    location = res['location']
         | 
| 351 | 
            +
                    $logger.debug "redirected to #{location}"
         | 
| 352 | 
            +
                    get_http(location)
         | 
| 353 | 
            +
                  when Net::HTTPNotFound
         | 
| 354 | 
            +
                    return res
         | 
| 355 | 
            +
                  else
         | 
| 356 | 
            +
                    return res.value
         | 
| 357 | 
            +
                  end
         | 
| 228 358 | 
             
                end
         | 
| 229 | 
            -
                def get_https(page)
         | 
| 230 | 
            -
                  uri = URI.parse(page)
         | 
| 231 | 
            -
                  http = Net::HTTP.new(uri.host, uri.port)
         | 
| 232 | 
            -
                  http.use_ssl = true
         | 
| 233 | 
            -
                  request = Net::HTTP::Get.new(uri.request_uri)
         | 
| 234 | 
            -
                  return http.request(request)
         | 
| 235 359 |  | 
| 360 | 
            +
                def get_https(page)
         | 
| 361 | 
            +
                  get_http(page, true)
         | 
| 236 362 | 
             
                end
         | 
| 237 363 | 
             
              end
         | 
| 238 364 | 
             
            end
         | 
    
        data/lib/wordstress/utils.rb
    CHANGED
    
    | @@ -3,7 +3,29 @@ module Wordstress | |
| 3 3 |  | 
| 4 4 | 
             
                # Transform a given URL into a directory name to be used to store data
         | 
| 5 5 | 
             
                def self.target_to_dirname(target)
         | 
| 6 | 
            -
                   | 
| 6 | 
            +
                  uri = URI.parse(target)
         | 
| 7 | 
            +
                  path = uri.request_uri.split('/')
         | 
| 8 | 
            +
                  blog_path = ""
         | 
| 9 | 
            +
                  blog_path = "_#{path[1]}" if path.count >= 2
         | 
| 10 | 
            +
                  return "#{uri.host}_#{uri.port}#{blog_path}"
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def self.build_output_dir(root, target)
         | 
| 14 | 
            +
                  attempt=0
         | 
| 15 | 
            +
                  today=Time.now.strftime("%Y%m%d")
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  while 1 do
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    proposed = File.join(root, Wordstress::Utils.target_to_dirname(target), today)
         | 
| 20 | 
            +
                    if attempt != 0
         | 
| 21 | 
            +
                      proposed += "_#{attempt}"
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    return proposed unless Dir.exists?(proposed)
         | 
| 25 | 
            +
                    attempt +=1 if Dir.exists?(proposed)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
             | 
| 7 29 | 
             
                end
         | 
| 8 30 |  | 
| 9 31 | 
             
              end
         | 
    
        data/lib/wordstress/version.rb
    CHANGED
    
    
    
        data/wordstress.gemspec
    CHANGED
    
    | @@ -7,10 +7,10 @@ Gem::Specification.new do |spec| | |
| 7 7 | 
             
              spec.name          = "wordstress"
         | 
| 8 8 | 
             
              spec.version       = Wordstress::VERSION
         | 
| 9 9 | 
             
              spec.authors       = ["Paolo Perego"]
         | 
| 10 | 
            -
              spec.email         = [" | 
| 10 | 
            +
              spec.email         = ["paolo@wordstress.org"]
         | 
| 11 11 | 
             
              spec.summary       = %q{wordstress is a security scanner for wordpress powered websites}
         | 
| 12 12 | 
             
              spec.description   = %q{wordstress is a security scanner for wordpress powered websites}
         | 
| 13 | 
            -
              spec.homepage      = " | 
| 13 | 
            +
              spec.homepage      = "http://wordstress.org"
         | 
| 14 14 | 
             
              spec.license       = "MIT"
         | 
| 15 15 |  | 
| 16 16 | 
             
              spec.files         = `git ls-files -z`.split("\x0")
         | 
| @@ -24,5 +24,6 @@ Gem::Specification.new do |spec| | |
| 24 24 | 
             
              spec.add_dependency 'codesake-commons'
         | 
| 25 25 | 
             
              spec.add_dependency 'json'
         | 
| 26 26 | 
             
              spec.add_dependency 'ciphersurfer'
         | 
| 27 | 
            +
              spec.add_dependency 'terminal-table'
         | 
| 27 28 |  | 
| 28 29 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: wordstress
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.40.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Paolo Perego
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2015- | 
| 11 | 
            +
            date: 2015-02-25 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| @@ -80,9 +80,23 @@ dependencies: | |
| 80 80 | 
             
                - - ">="
         | 
| 81 81 | 
             
                  - !ruby/object:Gem::Version
         | 
| 82 82 | 
             
                    version: '0'
         | 
| 83 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 84 | 
            +
              name: terminal-table
         | 
| 85 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 86 | 
            +
                requirements:
         | 
| 87 | 
            +
                - - ">="
         | 
| 88 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 89 | 
            +
                    version: '0'
         | 
| 90 | 
            +
              type: :runtime
         | 
| 91 | 
            +
              prerelease: false
         | 
| 92 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 93 | 
            +
                requirements:
         | 
| 94 | 
            +
                - - ">="
         | 
| 95 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 96 | 
            +
                    version: '0'
         | 
| 83 97 | 
             
            description: wordstress is a security scanner for wordpress powered websites
         | 
| 84 98 | 
             
            email:
         | 
| 85 | 
            -
            -  | 
| 99 | 
            +
            - paolo@wordstress.org
         | 
| 86 100 | 
             
            executables:
         | 
| 87 101 | 
             
            - wordstress
         | 
| 88 102 | 
             
            extensions: []
         | 
| @@ -101,7 +115,7 @@ files: | |
| 101 115 | 
             
            - lib/wordstress/utils.rb
         | 
| 102 116 | 
             
            - lib/wordstress/version.rb
         | 
| 103 117 | 
             
            - wordstress.gemspec
         | 
| 104 | 
            -
            homepage:  | 
| 118 | 
            +
            homepage: http://wordstress.org
         | 
| 105 119 | 
             
            licenses:
         | 
| 106 120 | 
             
            - MIT
         | 
| 107 121 | 
             
            metadata: {}
         |