VulnAPI Implementation

This commit is contained in:
erwanlr
2019-07-18 20:32:15 +01:00
parent 84422b10c8
commit 463e77f0a5
60 changed files with 1126 additions and 223 deletions

View File

@@ -8,12 +8,12 @@ ClassVars:
Enabled: false
LineLength:
Max: 120
Lint/UriEscapeUnescape:
Enabled: false
MethodLength:
Max: 20
Exclude:
- 'app/controllers/enumeration/cli_options.rb'
Lint/UriEscapeUnescape:
Enabled: false
Metrics/AbcSize:
Max: 25
Metrics/BlockLength:
@@ -29,3 +29,6 @@ Style/Documentation:
Enabled: false
Style/FormatStringToken:
Enabled: false
Style/NumericPredicate:
Exclude:
- 'app/controllers/vuln_api.rb'

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
require_relative 'controllers/core'
require_relative 'controllers/api_token'
require_relative 'controllers/vuln_api'
require_relative 'controllers/custom_directories'
require_relative 'controllers/wp_version'
require_relative 'controllers/main_theme'

View File

@@ -1,19 +0,0 @@
# frozen_string_literal: true
module WPScan
module Controller
# Controller to handle the API token
class ApiToken < CMSScanner::Controller::Base
def cli_options
[
OptString.new(['--api-token TOKEN', 'The API Token to display vulnerability data'])
]
end
def before_scan(opts = {})
# TODO, validate the token
# res = browser.get()
end
end
end
end

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
module WPScan
module Controller
# Controller to handle the API token
class VulnApi < CMSScanner::Controller::Base
def cli_options
[
OptString.new(['--api-token TOKEN', 'The WPVulnDB API Token to display vulnerability data'])
]
end
def before_scan
return unless ParsedCli.api_token
DB::VulnApi.token = ParsedCli.api_token
api_status = DB::VulnApi.status
raise Error::InvalidApiToken if api_status['error']
raise Error::ApiLimitReached if api_status['requests_remaining'] == 0
raise api_status['http_error'] if api_status['http_error']
end
def after_scan
output('status', status: DB::VulnApi.status, api_requests: WPScan.api_requests)
end
end
end
end

View File

@@ -15,9 +15,16 @@ module WPScan
@uri = Addressable::URI.parse(blog.url(path_from_blog))
end
# @return [ JSON ]
# Retrieve the metadata from the vuln API if available (and a valid token is given),
# or the local metadata db otherwise
# @return [ Hash ]
def metadata
@metadata ||= DB::Plugin.metadata_at(slug)
@metadata ||= db_data.empty? ? DB::Plugin.metadata_at(slug) : db_data
end
# @return [ Hash ]
def db_data
@db_data ||= DB::VulnApi.plugin_data(slug)
end
# @param [ Hash ] opts

View File

@@ -21,9 +21,16 @@ module WPScan
parse_style
end
# Retrieve the metadata from the vuln API if available (and a valid token is given),
# or the local metadata db otherwise
# @return [ JSON ]
def metadata
@metadata ||= DB::Theme.metadata_at(slug)
@metadata ||= db_data.empty? ? DB::Theme.metadata_at(slug) : db_data
end
# @return [ Hash ]
def db_data
@db_data ||= DB::VulnApi.theme_data(slug)
end
# @param [ Hash ] opts

View File

@@ -39,11 +39,10 @@ module WPScan
@vulnerabilities = []
# TODO Get them from API
#[*db_data['vulnerabilities']].each do |json_vuln|
# vulnerability = Vulnerability.load_from_json(json_vuln)
# @vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
#end
[*db_data['vulnerabilities']].each do |json_vuln|
vulnerability = Vulnerability.load_from_json(json_vuln)
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
end
@vulnerabilities
end
@@ -67,7 +66,7 @@ module WPScan
# Not used anywhere ATM
# @return [ Boolean ]
def popular?
@popular ||= metadata['popular']
@popular ||= metadata['popular'] ? true : false
end
# @return [ String ]

View File

@@ -35,9 +35,16 @@ module WPScan
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
end
# @return [ JSON ]
# Retrieve the metadata from the vuln API if available (and a valid token is given),
# or the local metadata db otherwise
# @return [ Hash ]
def metadata
@metadata ||= DB::Version.metadata_at(number)
@metadata ||= db_data.empty? ? DB::Version.metadata_at(number) : db_data
end
# @return [ Hash ]
def db_data
@db_data ||= DB::VulnApi.wordpress_data(number)
end
# @return [ Array<Vulnerability> ]
@@ -46,10 +53,9 @@ module WPScan
@vulnerabilities = []
# TODO get them from API
#[*db_data['vulnerabilities']].each do |json_vuln|
# @vulnerabilities << Vulnerability.load_from_json(json_vuln)
#end
[*db_data['vulnerabilities']].each do |json_vuln|
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
end
@vulnerabilities
end

View File

@@ -0,0 +1,13 @@
<% unless @status.empty? -%>
<% if @status['http_error'] -%>
<%= critical_icon %> WPVulnDB API, <%= @status['http_error'].to_s %>
<% else -%>
<%= info_icon %> WPVulnDB API OK
| Plan: <%= @status['plan'] %>
| Requests Done (during the scan): <%= @api_requests %>
| Requests Remaining: <%= @status['requests_remaining'] %>
<% end -%>
<% else -%>
<%= warning_icon %> No WPVulnDB API Token given, as a result vulnerability data has not been output.
<%= warning_icon %> You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register.
<% end -%>

View File

@@ -0,0 +1,13 @@
"vuln_api": {
<% unless @status.empty? -%>
<% if @status['http_error'] -%>
"http_error": <%= @status['http_error'].to_s.to_json %>
<% else -%>
"plan": <%= @status['plan'].to_json %>,
"requests_done_during_scan": <%= @api_requests.to_json %>,
"requests_remaining": <%= @status['requests_remaining'].to_json %>
<% end -%>
<% else -%>
"error": "No WPVulnDB API Token given, as a result vulnerability data has not been output.\nYou can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register."
<% end -%>
},

View File

@@ -5,7 +5,7 @@ require 'wpscan'
WPScan::Scan.new do |s|
s.controllers <<
WPScan::Controller::ApiToken.new <<
WPScan::Controller::VulnApi.new <<
WPScan::Controller::CustomDirectories.new <<
WPScan::Controller::InterestingFindings.new <<
WPScan::Controller::WpVersion.new <<

View File

@@ -7,6 +7,7 @@ require 'wpscan'
report = MemoryProfiler.report(top: 15) do
WPScan::Scan.new do |s|
s.controllers <<
WPScan::Controller::VulnApi.new <<
WPScan::Controller::CustomDirectories.new <<
WPScan::Controller::InterestingFindings.new <<
WPScan::Controller::WpVersion.new <<

View File

@@ -12,6 +12,7 @@ StackProf.run(mode: :cpu, out: '/tmp/stackprof-cpu.dump', interval: 500) do
# require_relative 'wpscan' doesn't work
WPScan::Scan.new do |s|
s.controllers <<
WPScan::Controller::VulnApi.new <<
WPScan::Controller::CustomDirectories.new <<
WPScan::Controller::InterestingFindings.new <<
WPScan::Controller::WpVersion.new <<

View File

@@ -13,7 +13,8 @@ require 'uri'
require 'time'
require 'readline'
require 'securerandom'
# Monkey Patches/Fixes/Override
require 'wpscan/typhoeus/response' # Adds a from_vuln_api? method
# Custom Libs
require 'wpscan/helper'
require 'wpscan/db'
@@ -38,12 +39,28 @@ module WPScan
APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path
DB_DIR = Pathname.new(Dir.home).join('.wpscan', 'db')
Typhoeus.on_complete do |response|
next if response.cached? || !response.from_vuln_api?
self.api_requests += 1
end
# Override, otherwise it would be returned as 'wp_scan'
#
# @return [ String ]
def self.app_name
'wpscan'
end
# @return [ Integer ]
def self.api_requests
@@api_requests ||= 0
end
# @param [ Integer ] value
def self.api_requests=(value)
@@api_requests = value
end
end
require "#{WPScan::APP_DIR}/app"

View File

@@ -7,7 +7,7 @@ module WPScan
# @return [ String ]
def default_user_agent
"WPScan v#{VERSION} (https://wpscan.org/)"
@default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.org/)"
end
end
end

View File

@@ -10,6 +10,8 @@ require_relative 'db/theme'
require_relative 'db/wp_version'
require_relative 'db/fingerprints'
require_relative 'db/vuln_api'
require_relative 'db/dynamic_finders/base'
require_relative 'db/dynamic_finders/plugin'
require_relative 'db/dynamic_finders/theme'

View File

@@ -14,7 +14,7 @@ module WPScan
OLD_FILES = %w[
wordpress.db user-agents.txt dynamic_finders_01.yml
wordpressess.json plugins.json themes.json
wordpresses.json plugins.json themes.json
].freeze
attr_reader :repo_directory

78
lib/wpscan/db/vuln_api.rb Normal file
View File

@@ -0,0 +1,78 @@
# frozen_string_literal: true
module WPScan
module DB
# WPVulnDB API
class VulnApi
NON_ERROR_CODES = [200, 401, 404].freeze
class << self
attr_accessor :token
end
# @return [ Addressable::URI ]
def self.uri
@uri ||= Addressable::URI.parse('https://wpvulndb.com/api/v3/')
end
# @param [ String ] path
# @param [ Hash ] params
#
# @return [ Hash ]
def self.get(path, params = {})
return {} unless token
res = Browser.get(uri.join(path), params.merge(request_params))
return JSON.parse(res.body) if NON_ERROR_CODES.include?(res.code)
raise Error::HTTP, res
rescue Error::HTTP => e
retries ||= 0
if (retries += 1) <= 3
sleep(1)
retry
end
{ 'http_error' => e }
end
# @return [ Hash ]
def self.plugin_data(slug)
get("plugins/#{slug}")&.dig(slug) || {}
end
# @return [ Hash ]
def self.theme_data(slug)
get("themes/#{slug}")&.dig(slug) || {}
end
# @return [ Hash ]
def self.wordpress_data(version_number)
get("wordpresses/#{version_number.tr('.', '')}")&.dig(version_number) || {}
end
# @return [ Hash ]
def self.status
json = get('status', params: { version: WPScan::VERSION }, cache_ttl: 0)
json['requests_remaining'] = 'Unlimited' if json['requests_remaining'] == -1
json
end
# @return [ Hash ]
def self.request_params
{
headers: {
'Host' => uri.host, # Reset in case user provided a --vhost for the target
'Referer' => nil, # Removes referer set by the cmsscanner to the target url
'User-Agent' => Browser.instance.default_user_agent,
'Authorization' => "Token token=#{token}"
}
}
end
end
end
end

View File

@@ -12,5 +12,6 @@ end
require_relative 'errors/enumeration'
require_relative 'errors/http'
require_relative 'errors/update'
require_relative 'errors/vuln_api'
require_relative 'errors/wordpress'
require_relative 'errors/xmlrpc'

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module WPScan
module Error
# Error raised when the token given via --api-token is invalid
class InvalidApiToken < Standard
def to_s
'The API token provided is invalid'
end
end
# Error raised when the number of API requests has been reached
# currently not implemented on the API side
class ApiLimitReached < Standard
def to_s
'Your API limit has been reached'
end
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module Typhoeus
# Custom Response class
class Response
# @note: Ignores requests done to the /status endpoint of the API
#
# @return [ Boolean ]
def from_vuln_api?
effective_url.start_with?(WPScan::DB::VulnApi.uri.to_s) && !effective_url.include?('/status')
end
end
end

View File

@@ -1,24 +0,0 @@
# frozen_string_literal: true
describe WPScan::Controller::ApiToken do
subject(:controller) { described_class.new }
let(:target_url) { 'http://ex.lo/' }
let(:cli_args) { "--url #{target_url}" }
before do
WPScan::ParsedCli.options = rspec_parsed_options(cli_args)
end
describe '#cli_options' do
its(:cli_options) { should_not be_empty }
its(:cli_options) { should be_a Array }
it 'contains to correct options' do
expect(controller.cli_options.map(&:to_sym)).to eq %i[api_token]
end
end
describe '#before_scan' do
xit
end
end

View File

@@ -0,0 +1,93 @@
# frozen_string_literal: true
describe WPScan::Controller::VulnApi do
subject(:controller) { described_class.new }
let(:target_url) { 'http://ex.lo/' }
let(:cli_args) { "--url #{target_url}" }
before do
WPScan::ParsedCli.options = rspec_parsed_options(cli_args)
end
describe '#cli_options' do
its(:cli_options) { should_not be_empty }
its(:cli_options) { should be_a Array }
it 'contains to correct options' do
expect(controller.cli_options.map(&:to_sym)).to eq %i[api_token]
end
end
describe '#before_scan' do
context 'when no --api-token provided' do
its(:before_scan) { should be nil }
end
context 'when --api-token given' do
let(:cli_args) { "#{super()} --api-token token" }
context 'when the token is invalid' do
before { expect(WPScan::DB::VulnApi).to receive(:status).and_return('error' => 'HTTP Token: Access denied.') }
it 'raise an InvalidApiToken error' do
expect { controller.before_scan }.to raise_error(WPScan::Error::InvalidApiToken)
end
end
context 'when the token is valid' do
context 'when the limit has been reached' do
before do
expect(WPScan::DB::VulnApi)
.to receive(:status)
.and_return('success' => true, 'plan' => 'free', 'requests_remaining' => 0)
end
it 'raises an ApiLimitReached error' do
expect { controller.before_scan }.to raise_error(WPScan::Error::ApiLimitReached)
end
end
context 'when a HTTP error, like a timeout' do
before do
expect(WPScan::DB::VulnApi)
.to receive(:status)
.and_return(
'http_error' => WPScan::Error::HTTP.new(
Typhoeus::Response.new(effective_url: 'mock-url', return_code: 28)
)
)
end
it 'raises an HTTP error' do
expect { controller.before_scan }
.to raise_error(WPScan::Error::HTTP, 'HTTP Error: mock-url (Timeout was reached)')
end
end
context 'when the token is valid and no HTTP error' do
before do
expect(WPScan::DB::VulnApi)
.to receive(:status)
.and_return('success' => true, 'plan' => 'free', 'requests_remaining' => requests)
end
context 'when limited requests' do
let(:requests) { 100 }
it 'does not raise an error' do
expect { controller.before_scan }.to_not raise_error
end
context 'when unlimited requests' do
let(:requests) { 'Unlimited' }
it 'does not raise an error' do
expect { controller.before_scan }.to_not raise_error
end
end
end
end
end
end
end
end

View File

@@ -81,24 +81,39 @@ describe WPScan::Model::Plugin do
end
describe '#latest_version, #last_updated, #popular' do
context 'when none' do
let(:slug) { 'vulnerable-not-popular' }
before { allow(plugin).to receive(:db_data).and_return(db_data) }
context 'when no db_data and no metadata' do
let(:slug) { 'not-known' }
let(:db_data) { {} }
its(:latest_version) { should be_nil }
its(:last_updated) { should be_nil }
its(:popular?) { should be false }
end
context 'when values' do
context 'when no db_data but metadata' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { {} }
its(:latest_version) { should eql WPScan::Model::Version.new('2.0') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z' }
its(:popular?) { should be true }
end
context 'when db_data' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('plugins/no-vulns-popular') }
its(:latest_version) { should eql WPScan::Model::Version.new('2.1') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z-via-api' }
its(:popular?) { should be true }
end
end
describe '#outdated?' do
before { allow(plugin).to receive(:db_data).and_return({}) }
context 'when last_version' do
let(:slug) { 'no-vulns-popular' }
@@ -116,13 +131,13 @@ describe WPScan::Model::Plugin do
.and_return(WPScan::Model::Version.new(version_number))
end
context 'when version < last_version' do
context 'when version < latest_version' do
let(:version_number) { '1.2' }
its(:outdated?) { should eql true }
end
context 'when version >= last_version' do
context 'when version >= latest_version' do
let(:version_number) { '3.0' }
its(:outdated?) { should eql false }
@@ -130,7 +145,7 @@ describe WPScan::Model::Plugin do
end
end
context 'when no last_version' do
context 'when no latest_version' do
let(:slug) { 'vulnerable-not-popular' }
context 'when no version' do
@@ -153,13 +168,16 @@ describe WPScan::Model::Plugin do
end
describe '#vulnerabilities' do
before { allow(plugin).to receive(:db_data).and_return(db_data) }
after do
expect(plugin.vulnerabilities).to eq @expected
expect(plugin.vulnerable?).to eql @expected.empty? ? false : true
end
context 'when plugin not in the DB' do
let(:slug) { 'not-in-db' }
let(:slug) { 'not-in-db' }
let(:db_data) { {} }
it 'returns an empty array' do
@expected = []
@@ -168,7 +186,8 @@ describe WPScan::Model::Plugin do
context 'when in the DB' do
context 'when no vulnerabilities' do
let(:slug) { 'no-vulns-popular' }
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('plugins/no-vulns-popular') }
it 'returns an empty array' do
@expected = []
@@ -176,11 +195,13 @@ describe WPScan::Model::Plugin do
end
context 'when vulnerabilities' do
let(:slug) { 'vulnerable-not-popular' }
let(:slug) { 'vulnerable-not-popular' }
let(:db_data) { vuln_api_data_for('plugins/vulnerable-not-popular') }
let(:all_vulns) do
[
WPScan::Vulnerability.new(
'First Vuln',
'First Vuln <= 6.3.10 - LFI',
{ wpvulndb: '1' },
'LFI',
'6.3.10'

View File

@@ -86,8 +86,179 @@ describe WPScan::Model::Theme do
end
end
describe '#latest_version, #last_updated, #popular' do
before do
stub_request(:get, /.*\.css\z/)
allow(theme).to receive(:db_data).and_return(db_data)
end
context 'when no db_data and no metadata' do
let(:slug) { 'not-known' }
let(:db_data) { {} }
its(:latest_version) { should be_nil }
its(:last_updated) { should be_nil }
its(:popular?) { should be false }
end
context 'when no db_data but metadata' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { {} }
its(:latest_version) { should eql WPScan::Model::Version.new('2.0') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z' }
its(:popular?) { should be true }
end
context 'when db_data' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('themes/no-vulns-popular') }
its(:latest_version) { should eql WPScan::Model::Version.new('2.2') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z-via-api' }
its(:popular?) { should be true }
end
end
describe '#outdated?' do
before do
stub_request(:get, /.*\.css\z/)
allow(theme).to receive(:db_data).and_return({})
end
context 'when last_version' do
let(:slug) { 'no-vulns-popular' }
context 'when no version' do
before { expect(theme).to receive(:version).at_least(1).and_return(nil) }
its(:outdated?) { should eql false }
end
context 'when version' do
before do
expect(theme)
.to receive(:version)
.at_least(1)
.and_return(WPScan::Model::Version.new(version_number))
end
context 'when version < latest_version' do
let(:version_number) { '1.2' }
its(:outdated?) { should eql true }
end
context 'when version >= latest_version' do
let(:version_number) { '3.0' }
its(:outdated?) { should eql false }
end
end
end
context 'when no latest_version' do
let(:slug) { 'vulnerable-not-popular' }
context 'when no version' do
before { expect(theme).to receive(:version).at_least(1).and_return(nil) }
its(:outdated?) { should eql false }
end
context 'when version' do
before do
expect(theme)
.to receive(:version)
.at_least(1)
.and_return(WPScan::Model::Version.new('1.0'))
end
its(:outdated?) { should eql false }
end
end
end
describe '#vulnerabilities' do
xit
before do
stub_request(:get, /.*\.css\z/)
allow(theme).to receive(:db_data).and_return(db_data)
end
after do
expect(theme.vulnerabilities).to eq @expected
expect(theme.vulnerable?).to eql @expected.empty? ? false : true
end
context 'when theme not in the DB' do
let(:slug) { 'not-in-db' }
let(:db_data) { {} }
it 'returns an empty array' do
@expected = []
end
end
context 'when in the DB' do
context 'when no vulnerabilities' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('themes/no-vulns-popular') }
it 'returns an empty array' do
@expected = []
end
end
context 'when vulnerabilities' do
let(:slug) { 'vulnerable-not-popular' }
let(:db_data) { vuln_api_data_for('themes/vulnerable-not-popular') }
let(:all_vulns) do
[
WPScan::Vulnerability.new(
'First Vuln',
{ wpvulndb: '1' },
'LFI',
'6.3.10'
),
WPScan::Vulnerability.new('No Fixed In', wpvulndb: '2')
]
end
context 'when no theme version' do
before { expect(theme).to receive(:version).at_least(1).and_return(false) }
it 'returns all the vulnerabilities' do
@expected = all_vulns
end
end
context 'when theme version' do
before do
expect(theme)
.to receive(:version)
.at_least(1)
.and_return(WPScan::Model::Version.new(number))
end
context 'when < to a fixed_in' do
let(:number) { '5.0' }
it 'returns it' do
@expected = all_vulns
end
end
context 'when >= to a fixed_in' do
let(:number) { '6.3.10' }
it 'does not return it ' do
@expected = [all_vulns.last]
end
end
end
end
end
end
describe '#parent_theme' do

View File

@@ -40,11 +40,13 @@ describe WPScan::Model::WpVersion do
describe '#vulnerabilities' do
subject(:version) { described_class.new(number) }
before { allow(version).to receive(:db_data).and_return(db_data) }
context 'when no vulns' do
let(:number) { '4.4' }
let(:db_data) { { 'vulnerabilities' => [] } }
its(:vulnerabilities) { should eql([]) }
its(:vulnerabilities) { should be_empty }
end
context 'when vulnerable' do
@@ -53,8 +55,25 @@ describe WPScan::Model::WpVersion do
expect(version).to be_vulnerable
end
let(:all_vulns) do
[
WPScan::Vulnerability.new(
'WP 3.8.1 - Vuln 1',
{ wpvulndb: '1' },
'SQLI'
),
WPScan::Vulnerability.new(
'WP 3.8.1 - Vuln 2',
{ url: %w[url-2 url-3], osvdb: %w[10], cve: %w[2014-0166], wpvulndb: '2' },
nil,
'3.8.2'
)
]
end
context 'when a signle vuln' do
let(:number) { '3.8' }
let(:number) { '3.8.1' }
let(:db_data) { vuln_api_data_for('wordpresses/38') }
it 'returns the expected result' do
@expected = [WPScan::Vulnerability.new(
@@ -67,6 +86,7 @@ describe WPScan::Model::WpVersion do
context 'when multiple vulns' do
let(:number) { '3.8.1' }
let(:db_data) { vuln_api_data_for('wordpresses/381') }
it 'returns the expected results' do
@expected = [
@@ -87,27 +107,30 @@ describe WPScan::Model::WpVersion do
end
end
describe '#release_date' do
describe '#metadata, #release_date, #status' do
subject(:version) { described_class.new('3.8.1') }
its(:release_date) { should eql '2014-01-23' }
before { allow(version).to receive(:db_data).and_return(db_data) }
context 'when the version is not in the DB' do
subject(:version) { described_class.new('3.8.2') }
context 'when no db_data' do
let(:db_data) { {} }
its(:release_date) { should eql 'Unknown' }
its(:release_date) { should eql '2014-01-23' }
its(:status) { should eql 'outdated' }
context 'when the version is not in the metadata' do
subject(:version) { described_class.new('3.8.2') }
its(:release_date) { should eql 'Unknown' }
its(:status) { should eql 'Unknown' }
end
end
end
describe '#status' do
subject(:version) { described_class.new('3.8.1') }
context 'when db_data' do
let(:db_data) { vuln_api_data_for('wordpresses/381') }
its(:status) { should eql 'outdated' }
context 'when the version is not in the DB' do
subject(:version) { described_class.new('3.8.2') }
its(:release_date) { should eql 'Unknown' }
its(:release_date) { should eql '2014-01-23-via-api' }
its(:status) { should eql 'outdated-via-api' }
end
end
end

View File

@@ -9,6 +9,7 @@ describe 'App::Views' do
# in the expected output.
%i[JSON CliNoColour].each do |formatter|
context "when #{formatter}" do
it_behaves_like 'App::Views::VulnApi'
it_behaves_like 'App::Views::WpVersion'
it_behaves_like 'App::Views::MainTheme'
it_behaves_like 'App::Views::Enumeration'

View File

@@ -34,6 +34,12 @@
"last_updated": "2015-05-16T00:00:00.000Z",
"vulnerabilities": false
},
"vulnerable-not-popular": {
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities": true
},
"dignitas-themes": {
"popular": true,
"latest_version": null,

View File

@@ -1,25 +0,0 @@
{
"no-vulns-popular": {
"vulnerabilities": [],
"popular": true,
"latest_version": "2.0",
"last_updated": "2015-05-16T00:00:00.000Z"
},
"vulnerable-not-popular": {
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities" : [
{
"title" : "First Vuln",
"fixed_in" : "6.3.10",
"id" : 1,
"vuln_type": "LFI"
},
{
"title": "No Fixed In",
"id": 2
}
]
}
}

View File

@@ -1,48 +0,0 @@
{
"no-vulns-popular": {
"popular": true,
"latest_version": "2.0",
"last_updated": "2015-05-16T00:00:00.000Z",
"vulnerabilities": []
},
"dignitas-themes": {
"popular": true,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : [
{
"created_at" : "2015-03-05T19:25:59.000Z",
"updated_at" : "2015-03-05T19:37:47.000Z",
"references": {
"url" : [
"http://research.evex.pw/?vuln=6",
"http://packetstormsecurity.com/files/130652/"
]
},
"title" : "Dignitas 1.1.9 - Privilage Escalation",
"id" : 7825,
"vuln_type" : "AUTHBYPASS"
}
]
},
"yaaburnee-themes": {
"popular": false,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : [
{
"created_at" : "2015-03-05T19:25:44.000Z",
"updated_at" : "2015-03-05T19:41:14.000Z",
"references": {
"url" : [
"http://research.evex.pw/?vuln=6",
"http://packetstormsecurity.com/files/130652/"
]
},
"title" : "Ya'aburnee 1.0.7 - Privilage Escalation",
"id" : 7824,
"vuln_type" : "AUTHBYPASS"
}
]
}
}

View File

@@ -0,0 +1,6 @@
{
"vulnerabilities": [],
"popular": true,
"latest_version": "2.1",
"last_updated": "2015-05-16T00:00:00.000Z-via-api"
}

View File

@@ -0,0 +1,17 @@
{
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities" : [
{
"title" : "First Vuln \u003c= 6.3.10 - LFI",
"fixed_in" : "6.3.10",
"id" : 1,
"vuln_type": "LFI"
},
{
"title": "No Fixed In",
"id": 2
}
]
}

View File

@@ -0,0 +1,20 @@
{
"popular": true,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : [
{
"created_at" : "2015-03-05T19:25:59.000Z",
"updated_at" : "2015-03-05T19:37:47.000Z",
"references": {
"url" : [
"http://research.evex.pw/?vuln=6",
"http://packetstormsecurity.com/files/130652/"
]
},
"title" : "Dignitas 1.1.9 - Privilage Escalation",
"id" : 7825,
"vuln_type" : "AUTHBYPASS"
}
]
}

View File

@@ -0,0 +1,6 @@
{
"popular": true,
"latest_version": "2.2",
"last_updated": "2015-05-16T00:00:00.000Z-via-api",
"vulnerabilities": []
}

View File

@@ -0,0 +1,17 @@
{
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities" : [
{
"title" : "First Vuln",
"fixed_in" : "6.3.10",
"id" : 1,
"vuln_type": "LFI"
},
{
"title": "No Fixed In",
"id": 2
}
]
}

View File

@@ -0,0 +1,20 @@
{
"popular": false,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : [
{
"created_at" : "2015-03-05T19:25:44.000Z",
"updated_at" : "2015-03-05T19:41:14.000Z",
"references": {
"url" : [
"http://research.evex.pw/?vuln=6",
"http://packetstormsecurity.com/files/130652/"
]
},
"title" : "Ya'aburnee 1.0.7 - Privilage Escalation",
"id" : 7824,
"vuln_type" : "AUTHBYPASS"
}
]
}

View File

@@ -0,0 +1,17 @@
{
"release_date" : "2013-12-12",
"status": "insecure",
"vulnerabilities" : [
{
"references": {
"url" : ["url-4"],
"osvdb" : ["11"]
},
"created_at" : "2014-08-01T10:58:19.000Z",
"updated_at" : "2014-09-16T15:45:26.000Z",
"title" : "WP 3.8 - Vuln 1",
"id" : 3,
"vuln_type" : "AUTHBYPASS"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"release_date" : "2014-01-23-via-api",
"status": "outdated-via-api",
"vulnerabilities" : [
{
"created_at" : "2014-08-01T10:58:19.000Z",
"updated_at" : "2014-09-16T13:52:17.000Z",
"title" : "WP 3.8.1 - Vuln 1",
"id" : 1,
"vuln_type" : "SQLI",
"published_date" : null,
"fixed_in" : null
},
{
"references" : {
"cve" : ["2014-0166"],
"osvdb" : ["10"],
"url" : ["url-2","url-3"]
},
"fixed_in" : "3.8.2",
"created_at" : "2014-08-01T10:58:19.000Z",
"updated_at" : "2014-09-16T13:53:11.000Z",
"id" : 2,
"title" : "WP 3.8.1 - Vuln 2"
}
]
}

View File

@@ -0,0 +1,4 @@
{
"release_date" : "2014-09-04",
"status": "latest"
}

View File

@@ -1,50 +0,0 @@
{
"4.0": {
"release_date" : "2014-09-04",
"status": "latest"
},
"3.8.1": {
"release_date" : "2014-01-23",
"status": "outdated",
"vulnerabilities" : [
{
"created_at" : "2014-08-01T10:58:19.000Z",
"updated_at" : "2014-09-16T13:52:17.000Z",
"title" : "WP 3.8.1 - Vuln 1",
"id" : 1,
"vuln_type" : "SQLI",
"published_date" : null,
"fixed_in" : null
},
{
"references" : {
"cve" : ["2014-0166"],
"osvdb" : ["10"],
"url" : ["url-2","url-3"]
},
"fixed_in" : "3.8.2",
"created_at" : "2014-08-01T10:58:19.000Z",
"updated_at" : "2014-09-16T13:53:11.000Z",
"id" : 2,
"title" : "WP 3.8.1 - Vuln 2"
}
]
},
"3.8": {
"release_date" : "2013-12-12",
"status": "insecure",
"vulnerabilities" : [
{
"references": {
"url" : ["url-4"],
"osvdb" : ["11"]
},
"created_at" : "2014-08-01T10:58:19.000Z",
"updated_at" : "2014-09-16T15:45:26.000Z",
"title" : "WP 3.8 - Vuln 1",
"id" : 3,
"vuln_type" : "AUTHBYPASS"
}
]
}
}

View File

@@ -4,7 +4,7 @@ describe WPScan::DB::Themes do
subject(:themes) { described_class }
describe '#all_slugs' do
its(:all_slugs) { should eql %w[no-vulns-popular dignitas-themes yaaburnee-themes] }
its(:all_slugs) { should eql %w[no-vulns-popular vulnerable-not-popular dignitas-themes yaaburnee-themes] }
end
describe '#popular_slugs' do
@@ -12,6 +12,6 @@ describe WPScan::DB::Themes do
end
describe '#vulnerable_slugs' do
its(:vulnerable_slugs) { should eql %w[dignitas-themes yaaburnee-themes] }
its(:vulnerable_slugs) { should eql %w[vulnerable-not-popular dignitas-themes yaaburnee-themes] }
end
end

View File

@@ -0,0 +1,281 @@
# frozen_string_literal: true
describe WPScan::DB::VulnApi do
subject(:api) { described_class }
describe '#uri' do
its(:uri) { should be_a Addressable::URI }
end
describe '#token, @token=' do
context 'when no token set' do
before { api.token = nil } # In case it was set by a previous spec
its(:token) { should be nil }
end
context 'when token set' do
it 'returns it' do
api.token = 's3cRet'
expect(api.token).to eql 's3cRet'
end
end
end
describe '#get' do
context 'when no token' do
before { api.token = nil }
it 'returns an empty hash' do
expect(api.get('test')).to eql({})
end
end
context 'when a token' do
before { api.token = 's3cRet' }
context 'when no timeouts' do
before do
stub_request(:get, api.uri.join('path'))
.with(headers: { 'Host' => api.uri.host, 'Expect' => nil, 'Referer' => nil,
'User-Agent' => WPScan::Browser.instance.default_user_agent,
'Authorization' => 'Token token=s3cRet' })
.to_return(status: code, body: body)
end
context 'when 200' do
let(:code) { 200 }
let(:body) { { data: 'something' }.to_json }
it 'returns the expected hash' do
result = api.get('path')
expect(result).to eql('data' => 'something')
end
end
context 'when 401' do
let(:code) { 401 }
let(:body) { { error: 'HTTP Token: Access denied.' }.to_json }
it 'returns the expected hash' do
result = api.get('path')
expect(result).to eql('error' => 'HTTP Token: Access denied.')
end
end
context 'when 404' do
let(:code) { 404 }
let(:body) { { error: 'Not found' }.to_json }
it 'returns an empty hash' do
result = api.get('path')
expect(result).to eql('error' => 'Not found')
end
end
end
context 'when timeouts' do
context 'when all requests timeout' do
before do
stub_request(:get, api.uri.join('path'))
.with(headers: { 'Host' => api.uri.host, 'Expect' => nil, 'Referer' => nil,
'User-Agent' => WPScan::Browser.instance.default_user_agent,
'Authorization' => 'Token token=s3cRet' })
.to_return(status: 0)
end
it 'tries 3 times and returns the hash with the error' do
expect(api).to receive(:sleep).with(1).exactly(3).times
result = api.get('path')
expect(result['http_error']).to be_a WPScan::Error::HTTP
end
end
context 'when only the first request timeout' do
before do
stub_request(:get, api.uri.join('path'))
.with(headers: { 'Host' => api.uri.host, 'Expect' => nil, 'Referer' => nil,
'User-Agent' => WPScan::Browser.instance.default_user_agent,
'Authorization' => 'Token token=s3cRet' })
.to_return(status: 0).then
.to_return(status: 200, body: { data: 'test' }.to_json)
end
it 'tries 1 time and returns expected data' do
expect(api).to receive(:sleep).with(1).exactly(1).times
result = api.get('path')
expect(result).to eql('data' => 'test')
end
end
end
end
end
describe '#plugin_data' do
before { api.token = api_token }
context 'when no --api-token' do
let(:api_token) { nil }
it 'returns an empty hash' do
expect(api.plugin_data('slug')).to eql({})
end
end
context 'when valid --api-token' do
let(:api_token) { 's3cRet' }
context 'when the slug exist' do
it 'calls the correct URL' do
stub_request(:get, api.uri.join('plugins/slug'))
.to_return(status: 200, body: { slug: { p: 'aa' } }.to_json)
expect(api.plugin_data('slug')).to eql('p' => 'aa')
end
end
context 'when the slug does not exist' do
it 'returns an empty hash' do
stub_request(:get, api.uri.join('plugins/slug-404')).to_return(status: 404, body: '{}')
expect(api.plugin_data('slug-404')).to eql({})
end
end
end
end
describe '#theme_data' do
before { api.token = api_token }
context 'when no --api-token' do
let(:api_token) { nil }
it 'returns an empty hash' do
expect(api.theme_data('slug')).to eql({})
end
end
context 'when valid --api-token' do
let(:api_token) { 's3cRet' }
context 'when the slug exist' do
it 'calls the correct URL' do
stub_request(:get, api.uri.join('themes/slug'))
.to_return(status: 200, body: { slug: { t: 'aa' } }.to_json)
expect(api.theme_data('slug')).to eql('t' => 'aa')
end
end
context 'when the slug does not exist' do
it 'returns an empty hash' do
stub_request(:get, api.uri.join('themes/slug-404')).to_return(status: 404, body: '{}')
expect(api.theme_data('slug-404')).to eql({})
end
end
end
end
describe '#wordpress_data' do
before { api.token = api_token }
context 'when no --api-token' do
let(:api_token) { nil }
it 'returns an empty hash' do
expect(api.wordpress_data('1.2')).to eql({})
end
end
context 'when valid --api-token' do
let(:api_token) { 's3cRet' }
context 'when the version exist' do
it 'calls the correct URL' do
stub_request(:get, api.uri.join('wordpresses/522'))
.to_return(status: 200, body: { '5.2.2' => { w: 'aa' } }.to_json)
expect(api.wordpress_data('5.2.2')).to eql('w' => 'aa')
end
end
context 'when the version does not exist' do
it 'returns an empty hash' do
stub_request(:get, api.uri.join('wordpresses/11')).to_return(status: 404, body: '{}')
expect(api.wordpress_data('1.1')).to eql({})
end
end
end
end
describe '#status' do
before do
api.token = 's3cRet'
stub_request(:get, api.uri.join('status'))
.with(query: { version: WPScan::VERSION },
headers: { 'Host' => api.uri.host, 'Expect' => nil, 'Referer' => nil,
'User-Agent' => WPScan::Browser.instance.default_user_agent,
'Authorization' => 'Token token=s3cRet' })
.to_return(status: code, body: return_body.to_json)
end
let(:code) { 200 }
let(:return_body) { {} }
context 'when 200' do
let(:return_body) { { success: true, plan: 'free', requests_remaining: 100 } }
it 'returns the expected hash' do
status = api.status
expect(status['success']).to be true
expect(status['plan']).to eql 'free'
expect(status['requests_remaining']).to eql 100
end
context 'when unlimited requests' do
let(:return_body) { super().merge(plan: 'enterprise', requests_remaining: -1) }
it 'returns the expected hash, witht he correct requests_remaining' do
status = api.status
expect(status['success']).to be true
expect(status['plan']).to eql 'enterprise'
expect(status['requests_remaining']).to eql 'Unlimited'
end
end
end
context 'when 401' do
let(:code) { 401 }
let(:return_body) { { error: 'HTTP Token: Access denied.' } }
it 'returns the expected hash' do
status = api.status
expect(status['error']).to eql 'HTTP Token: Access denied.'
end
end
context 'otherwise' do
let(:code) { 0 }
it 'returns the expected hash with the response' do
status = api.status
expect(status['http_error']).to be_a WPScan::Error::HTTP
end
end
end
end

View File

@@ -83,14 +83,22 @@ describe WPScan::Target do
end
context 'when wp_version found' do
before do
expect(wp_version)
.to receive(:db_data)
.and_return(vuln_api_data_for("wordpresses/#{wp_version.number.tr('.', '')}"))
target.instance_variable_set(:@wp_version, wp_version)
end
context 'when not vulnerable' do
before { target.instance_variable_set(:@wp_version, WPScan::Model::WpVersion.new('4.4')) }
let(:wp_version) { WPScan::Model::WpVersion.new('4.0') }
it { should_not be_vulnerable }
end
context 'when vulnerable' do
before { target.instance_variable_set(:@wp_version, WPScan::Model::WpVersion.new('3.8.1')) }
let(:wp_version) { WPScan::Model::WpVersion.new('3.8.1') }
it { should be_vulnerable }
end

View File

@@ -0,0 +1,4 @@
[+] WPVulnDB API OK
| Plan: paid
| Requests Done (during the scan): 3
| Requests Remaining: 120

View File

@@ -0,0 +1,7 @@
{
"vuln_api": {
"plan": "paid",
"requests_done_during_scan": 3,
"requests_remaining": 120
}
}

View File

@@ -0,0 +1 @@
[!] WPVulnDB API, HTTP Error: url (Timeout was reached)

View File

@@ -0,0 +1,5 @@
{
"vuln_api": {
"http_error": "HTTP Error: url (Timeout was reached)"
}
}

View File

@@ -0,0 +1,4 @@
[+] WPVulnDB API OK
| Plan: free
| Requests Done (during the scan): 3
| Requests Remaining: 0

View File

@@ -0,0 +1,7 @@
{
"vuln_api": {
"plan": "free",
"requests_done_during_scan": 3,
"requests_remaining": 0
}
}

View File

@@ -0,0 +1,2 @@
[!] No WPVulnDB API Token given, as a result vulnerability data has not been output.
[!] You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register.

View File

@@ -0,0 +1,5 @@
{
"vuln_api": {
"error": "No WPVulnDB API Token given, as a result vulnerability data has not been output.\nYou can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register."
}
}

View File

@@ -0,0 +1,4 @@
[+] WPVulnDB API OK
| Plan: enterprise
| Requests Done (during the scan): 3
| Requests Remaining: Unlimited

View File

@@ -0,0 +1,7 @@
{
"vuln_api": {
"plan": "enterprise",
"requests_done_during_scan": 3,
"requests_remaining": "Unlimited"
}
}

View File

@@ -1,4 +1,4 @@
[+] WordPress version 3.8.1 identified (Outdated, released on 2014-01-23).
[+] WordPress version 3.8.1 identified (Outdated-via-api, released on 2014-01-23-via-api).
| Detected By: rspec
|
| [!] 2 vulnerabilities identified:

View File

@@ -1,8 +1,8 @@
{
"version": {
"number": "3.8.1",
"release_date": "2014-01-23",
"status": "outdated",
"release_date": "2014-01-23-via-api",
"status": "outdated-via-api",
"found_by": "rspec",
"confidence": 0,
"interesting_entries": [

View File

@@ -1,5 +1,6 @@
# frozen_string_literal: true
require 'shared_examples/views/vuln_api'
require 'shared_examples/views/wp_version'
require 'shared_examples/views/main_theme'
require 'shared_examples/views/enumeration'

View File

@@ -60,6 +60,8 @@ shared_examples 'App::Views::MainTheme' do
it 'outputs the expected string' do
expect(theme).to receive(:version).at_least(1)
allow(theme).to receive(:db_data).and_return(vuln_api_data_for('themes/dignitas-themes'))
@tpl_vars = tpl_vars.merge(theme: theme, verbose: true)
end
end

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
shared_examples 'App::Views::VulnApi' do
let(:controller) { WPScan::Controller::VulnApi.new }
let(:tpl_vars) { { url: target_url } }
describe 'status' do
let(:view) { 'status' }
context 'when no api token is given' do
let(:expected_view) { 'no_token' }
it 'outputs the expected string' do
@tpl_vars = tpl_vars.merge(status: {})
end
end
context 'when http error' do
let(:expected_view) { 'http_error' }
it 'outputs the expected string' do
@tpl_vars = tpl_vars.merge(
status: {
'http_error' => WPScan::Error::HTTP.new(Typhoeus::Response.new(effective_url: 'url', return_code: 28))
}
)
end
end
context 'when no more remaining requests' do
let(:expected_view) { 'no_more_requests' }
it 'outputs the expected string' do
@tpl_vars = tpl_vars.merge(
status: { 'success': true, 'plan' => 'free', 'requests_remaining' => 0 },
api_requests: 3
)
end
end
context 'when everything is fine' do
let(:expected_view) { 'all_ok' }
it 'outputs the expected string' do
@tpl_vars = tpl_vars.merge(
status: { 'success': true, 'plan' => 'paid', 'requests_remaining' => 120 },
api_requests: 3
)
end
end
context 'when unlimited requests' do
let(:expected_view) { 'unlimited_requests' }
it 'outputs the expected string' do
@tpl_vars = tpl_vars.merge(
status: { 'success': true, 'plan' => 'enterprise', 'requests_remaining' => 'Unlimited' },
api_requests: 3
)
end
end
end
end

View File

@@ -17,6 +17,7 @@ shared_examples 'App::Views::WpVersion' do
context 'when the version is not nil' do
let(:version) { WPScan::Model::WpVersion.new('4.0', found_by: 'rspec') }
before { allow(version).to receive(:db_data).and_return(vuln_api_data_for('wordpresses/40')) }
context 'when confirmed_by is empty' do
context 'when no interesting_entries' do
@@ -77,9 +78,12 @@ shared_examples 'App::Views::WpVersion' do
context 'when the version is vulnerable' do
let(:expected_view) { 'with_vulns' }
let(:version) { WPScan::Model::WpVersion.new('3.8.1', found_by: 'rspec') }
before { allow(version).to receive(:db_data).and_return(vuln_api_data_for('wordpresses/381')) }
it 'outputs the expected string' do
@tpl_vars = tpl_vars.merge(version: WPScan::Model::WpVersion.new('3.8.1', found_by: 'rspec'))
@tpl_vars = tpl_vars.merge(version: version)
end
end
end

View File

@@ -44,6 +44,10 @@ def df_stubbed_response(fixture, finder_super_class)
end
end
def vuln_api_data_for(path)
JSON.parse(File.read(FIXTURES.join('db', 'vuln_api', "#{path}.json")))
end
require 'wpscan'
require 'shared_examples'