wordstress 0.10.3 → 0.15.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/.gitignore +2 -0
- data/Rakefile +21 -0
- data/bin/wordstress +36 -5
- data/lib/wordstress/models/plugins.rb +60 -0
- data/lib/wordstress/models/themes.rb +62 -0
- data/lib/wordstress/site.rb +107 -5
- data/lib/wordstress/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 559b014cd3100530e2a30f54da9bbebcd37530c4
|
4
|
+
data.tar.gz: 0398ef2797138d181fba62c84609456e5a1e4d76
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1c1a877d30ef0557957ac1bc2f56bedc33653e78cb16b0c014ba667a38fb5ef3b8572cfe93e09ba06f97e0980de749270f815034d914cf159bae93371d50ab9
|
7
|
+
data.tar.gz: 0b9cc780b6a570919b72aa637b6f86c00c63ac6a91fc3eac59c5f547113cc7232d70e42f2d9af01aca9da9444d388989f1ee0f356f2257babf30b23ceb03eaa0
|
data/.gitignore
CHANGED
data/Rakefile
CHANGED
@@ -1,2 +1,23 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
|
3
|
+
namespace :import do
|
4
|
+
desc 'Import themes'
|
5
|
+
task :themes, :name do |t,args|
|
6
|
+
require 'wordstress/models/themes'
|
7
|
+
name = args.name
|
8
|
+
puts "reading themes from #{name}"
|
9
|
+
t = Wordstress::Models::Themes.new({:dbname=>"themes.db"})
|
10
|
+
t.import_from_file(name)
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Import plugins'
|
14
|
+
task :plugins, :name do |t, args|
|
15
|
+
require 'wordstress/models/plugins'
|
16
|
+
name = args.name
|
17
|
+
puts "reading themes from #{name}"
|
18
|
+
p = Wordstress::Models::Plugins.new({:dbname=>"plugins.db"})
|
19
|
+
p.import_from_file(name)
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
data/bin/wordstress
CHANGED
@@ -6,12 +6,22 @@ require 'codesake-commons'
|
|
6
6
|
|
7
7
|
require 'wordstress'
|
8
8
|
|
9
|
+
# Scanning modes for plugins and themes
|
10
|
+
# + gentleman: wordstress will try to fetch plugins and themes only using
|
11
|
+
# info in the HTML page (this is very polite but also very inaccurate).
|
12
|
+
# + interactive: wordstress will use a target installed plugin to fetch
|
13
|
+
# installed plugins and themes with their version
|
14
|
+
# + aggressive: wordstress will use enumeration to find installed plugins and
|
15
|
+
# themes. This will lead to false positives.
|
16
|
+
MODES = [:gentleman,:interactive,:aggressive]
|
9
17
|
APPNAME = File.basename($0)
|
10
18
|
|
11
19
|
$logger = Codesake::Commons::Logging.instance
|
12
20
|
@output_root = File.join(Dir.home, '/wordstress')
|
21
|
+
scanning_mode = :gentleman
|
13
22
|
|
14
23
|
opts = GetoptLong.new(
|
24
|
+
[ '--gentleman','-G', GetoptLong::NO_ARGUMENT],
|
15
25
|
[ '--csv', '-C', GetoptLong::NO_ARGUMENT],
|
16
26
|
[ '--version', '-v', GetoptLong::NO_ARGUMENT],
|
17
27
|
[ '--help', '-h', GetoptLong::NO_ARGUMENT]
|
@@ -53,7 +63,7 @@ trap("INT") { $logger.die('[INTERRUPTED]') }
|
|
53
63
|
$logger.die("missing target") if target.nil?
|
54
64
|
|
55
65
|
$logger.log "scanning #{target}"
|
56
|
-
site = Wordstress::Site.new(target)
|
66
|
+
site = Wordstress::Site.new({:target=>target, :scanning_mode=>scanning_mode})
|
57
67
|
|
58
68
|
if site.version[:version] == "0.0.0"
|
59
69
|
$logger.err "can't detect wordpress version running on #{target}. Giving up!"
|
@@ -61,8 +71,29 @@ if site.version[:version] == "0.0.0"
|
|
61
71
|
end
|
62
72
|
|
63
73
|
$logger.ok "wordpress version #{site.version[:version]} detected"
|
64
|
-
|
65
|
-
|
66
|
-
wp_vuln_hash
|
67
|
-
$logger.
|
74
|
+
$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
|
75
|
+
if site.online?
|
76
|
+
wp_vuln_hash = JSON.parse(site.wp_vuln_json)
|
77
|
+
$logger.ok "#{wp_vuln_hash["wordpress"]["vulnerabilities"].size} vulnerabilities found due wordpress version"
|
78
|
+
wp_vuln_hash["wordpress"]["vulnerabilities"].each do |v|
|
79
|
+
$logger.log "#{v["id"]} - #{v["title"]}"
|
80
|
+
end
|
81
|
+
else
|
82
|
+
$logger.err "it seems we are offline. wordstress can't reach https://wpvulndb.com"
|
83
|
+
$logger.err "wordpress can't enumerate vulnerabilities"
|
84
|
+
end
|
85
|
+
$logger.log "#{target} has #{site.themes.count} themes"
|
86
|
+
site.themes.each do |t|
|
87
|
+
$logger.log "searching wpvulndb for theme #{t} vulnerabilities"
|
88
|
+
v = site.get_theme_vulnerabilities(t)
|
89
|
+
$logger.debug v
|
68
90
|
end
|
91
|
+
|
92
|
+
$logger.log "#{target} has #{site.plugins.count} plugins"
|
93
|
+
site.plugins.each do |t|
|
94
|
+
$logger.log "searching wpvulndb for plugin #{t} vulnerabilities"
|
95
|
+
v = site.get_plugin_vulnerabilities(t)
|
96
|
+
$logger.debug v
|
97
|
+
end
|
98
|
+
|
99
|
+
$logger.bye
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'data_mapper'
|
2
|
+
require 'dm-sqlite-adapter'
|
3
|
+
|
4
|
+
module Wordstress
|
5
|
+
module Models
|
6
|
+
|
7
|
+
class PluginInfo
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
property :id, Serial
|
11
|
+
property :revision, Integer
|
12
|
+
property :created_at, DateTime, :default=>DateTime.now
|
13
|
+
property :updated_at, DateTime, :default=>DateTime.now
|
14
|
+
end
|
15
|
+
|
16
|
+
class Plugin
|
17
|
+
include DataMapper::Resource
|
18
|
+
|
19
|
+
property :id, Serial
|
20
|
+
property :name, String
|
21
|
+
property :link, String
|
22
|
+
property :created_at, DateTime, :default=>DateTime.now
|
23
|
+
property :updated_at, DateTime, :default=>DateTime.now
|
24
|
+
end
|
25
|
+
|
26
|
+
class Plugins
|
27
|
+
|
28
|
+
def initialize(options={:dbname=>"plugins.db"})
|
29
|
+
DataMapper.setup(:default, "sqlite3://#{File.join(Dir.pwd, options[:dbname])}")
|
30
|
+
DataMapper.finalize
|
31
|
+
DataMapper.auto_migrate!
|
32
|
+
end
|
33
|
+
|
34
|
+
def import_from_file(filename)
|
35
|
+
doc = Nokogiri::HTML(File.read(filename))
|
36
|
+
title = doc.at_css('title').children.text
|
37
|
+
|
38
|
+
return nil unless title.include?"Revision"
|
39
|
+
revision = title.split("Revision ")[1].split(':')[0].to_i
|
40
|
+
links = doc.xpath('//li//a')
|
41
|
+
|
42
|
+
puts "Plugin SVN revision is: #{revision}"
|
43
|
+
puts "#{links.count} plugins found"
|
44
|
+
|
45
|
+
i = PluginInfo.new
|
46
|
+
i.revision = revision
|
47
|
+
i.save
|
48
|
+
|
49
|
+
links.each do |link|
|
50
|
+
p = Plugin.new
|
51
|
+
p.name = link.text.chop
|
52
|
+
p.link = 'https://plugins.svn.wordpress.org/'+link.attr('href')
|
53
|
+
p.save
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'data_mapper'
|
2
|
+
require 'dm-sqlite-adapter'
|
3
|
+
|
4
|
+
module Wordstress
|
5
|
+
module Models
|
6
|
+
|
7
|
+
class Info
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
property :id, Serial
|
11
|
+
property :revision, Integer
|
12
|
+
property :created_at, DateTime, :default=>DateTime.now
|
13
|
+
property :updated_at, DateTime, :default=>DateTime.now
|
14
|
+
end
|
15
|
+
|
16
|
+
class Theme
|
17
|
+
include DataMapper::Resource
|
18
|
+
|
19
|
+
property :id, Serial
|
20
|
+
property :name, String
|
21
|
+
property :link, String
|
22
|
+
property :created_at, DateTime, :default=>DateTime.now
|
23
|
+
property :updated_at, DateTime, :default=>DateTime.now
|
24
|
+
end
|
25
|
+
|
26
|
+
class Themes
|
27
|
+
|
28
|
+
def initialize(options={:dbname=>"themes.db"})
|
29
|
+
DataMapper.setup(:default, "sqlite3://#{File.join(Dir.pwd, options[:dbname])}")
|
30
|
+
DataMapper.finalize
|
31
|
+
DataMapper.auto_migrate!
|
32
|
+
end
|
33
|
+
|
34
|
+
def import_from_file(filename)
|
35
|
+
doc = Nokogiri::HTML(File.read(filename))
|
36
|
+
title = doc.at_css('title').children.text
|
37
|
+
|
38
|
+
return nil unless title.include?"Revision"
|
39
|
+
revision = title.split("Revision ")[1].split(':')[0].to_i
|
40
|
+
links = doc.xpath('//li//a')
|
41
|
+
|
42
|
+
|
43
|
+
puts "Theme SVN revision is: #{revision}"
|
44
|
+
puts "#{links.count} themes found"
|
45
|
+
|
46
|
+
i = Info.new
|
47
|
+
i.revision = revision
|
48
|
+
i.save
|
49
|
+
|
50
|
+
links.each do |link|
|
51
|
+
# puts "-> #{link.attr('href')} - #{link.text.chop}"
|
52
|
+
t = Theme.new
|
53
|
+
t.name = link.text.chop
|
54
|
+
t.link = 'https://themes.svn.wordpress.org/'+link.attr('href')
|
55
|
+
t.save
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/wordstress/site.rb
CHANGED
@@ -3,27 +3,71 @@ require 'net/http'
|
|
3
3
|
module Wordstress
|
4
4
|
class Site
|
5
5
|
|
6
|
-
attr_reader :version, :wp_vuln_json
|
7
|
-
|
6
|
+
attr_reader :version, :scanning_mode, :wp_vuln_json, :plugins, :themes, :themes_vuln_json
|
7
|
+
|
8
|
+
def initialize(options={:target=>"http://localhost", :scanning_mode=>:gentleman})
|
8
9
|
begin
|
9
|
-
@uri = URI(
|
10
|
-
@raw_name =
|
10
|
+
@uri = URI(options[:target])
|
11
|
+
@raw_name = options[:target]
|
11
12
|
@valid = true
|
12
13
|
rescue
|
13
14
|
@valid = false
|
14
15
|
end
|
16
|
+
@scanning_mode = options[:scanning_mode]
|
15
17
|
|
16
18
|
@robots_txt = get(@raw_name + "/robots.txt")
|
17
19
|
@readme_html = get(@raw_name + "/readme.html")
|
18
20
|
@homepage = get(@raw_name)
|
19
21
|
@version = detect_version
|
22
|
+
@online = true
|
20
23
|
|
21
24
|
@wp_vuln_json = get_wp_vulnerabilities unless @version[:version] == "0.0.0"
|
22
25
|
@wp_vuln_json = Hash.new.to_json if @version[:version] == "0.0.0"
|
26
|
+
|
27
|
+
@plugins = find_plugins
|
28
|
+
@themes = find_themes
|
29
|
+
# @themes_vuln_json = get_themes_vulnerabilities
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_themes_vulnerabilities
|
33
|
+
vuln = []
|
34
|
+
@themes.each do |t|
|
35
|
+
vuln << {:theme=>t, :vulns=>get_theme_vulnerabilities(t)}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_plugin_vulnerabilities(theme)
|
40
|
+
begin
|
41
|
+
json= get_https("https://wpvulndb.com/api/v1/plugins/#{theme}").body
|
42
|
+
return "Plugin #{theme} is not present on wpvulndb.com" if json.include?"The page you were looking for doesn't exist (404)"
|
43
|
+
return json
|
44
|
+
rescue => e
|
45
|
+
$logger.err e.message
|
46
|
+
@online = false
|
47
|
+
return []
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_theme_vulnerabilities(theme)
|
52
|
+
begin
|
53
|
+
json=get_https("https://wpvulndb.com/api/v1/themes/#{theme}").body
|
54
|
+
return "Theme #{theme} is not present on wpvulndb.com" if json.include?"The page you were looking for doesn't exist (404)"
|
55
|
+
return json
|
56
|
+
rescue => e
|
57
|
+
$logger.err e.message
|
58
|
+
@online = false
|
59
|
+
return []
|
60
|
+
end
|
23
61
|
end
|
24
62
|
|
25
63
|
def get_wp_vulnerabilities
|
26
|
-
|
64
|
+
begin
|
65
|
+
return get_https("https://wpvulndb.com/api/v1/wordpresses/#{version_pad(@version[:version])}").body
|
66
|
+
rescue => e
|
67
|
+
$logger.err e.message
|
68
|
+
@online = false
|
69
|
+
return ""
|
70
|
+
end
|
27
71
|
end
|
28
72
|
|
29
73
|
def version_pad(version)
|
@@ -74,8 +118,66 @@ module Wordstress
|
|
74
118
|
def is_valid?
|
75
119
|
return @valid
|
76
120
|
end
|
121
|
+
def online?
|
122
|
+
return @online
|
123
|
+
end
|
124
|
+
|
125
|
+
def find_themes
|
126
|
+
return find_themes_gentleman if @scanning_mode == :gentleman
|
127
|
+
return []
|
128
|
+
end
|
129
|
+
def find_plugins
|
130
|
+
return find_plugins_gentleman if @scanning_mode == :gentleman
|
131
|
+
|
132
|
+
# bruteforce check must start with error page discovery.
|
133
|
+
# the idea is to send 2 random plugin names (e.g. 2 sha256 of time seed)
|
134
|
+
# and see how webserver answers and then understand if we can rely on a
|
135
|
+
# pattern for the error page.
|
136
|
+
return []
|
137
|
+
end
|
77
138
|
|
78
139
|
private
|
140
|
+
def find_themes_gentleman
|
141
|
+
ret = []
|
142
|
+
doc = Nokogiri::HTML(@homepage.body)
|
143
|
+
doc.css('link').each do |link|
|
144
|
+
if link.attr('href').include?("wp-content/themes")
|
145
|
+
theme = theme_name(link.attr('href'))
|
146
|
+
ret << theme if ret.index(theme).nil?
|
147
|
+
end
|
148
|
+
end
|
149
|
+
ret
|
150
|
+
end
|
151
|
+
|
152
|
+
def theme_name(url)
|
153
|
+
url.match(/\/wp-content\/themes\/(\w)+/)[0].split('/').last
|
154
|
+
end
|
155
|
+
def plugin_name(url)
|
156
|
+
url.match(/\/wp-content\/plugins\/(\w)+/)[0].split('/').last
|
157
|
+
end
|
158
|
+
|
159
|
+
def find_plugins_gentleman
|
160
|
+
ret = []
|
161
|
+
doc = Nokogiri::HTML(@homepage.body)
|
162
|
+
doc.css('script').each do |link|
|
163
|
+
if ! link.attr('src').nil?
|
164
|
+
if link.attr('src').include?("wp-content/plugins")
|
165
|
+
plugin = plugin_name(link.attr('src'))
|
166
|
+
ret << plugin if ret.index(plugin).nil?
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
doc.css('link').each do |link|
|
171
|
+
if link.attr('href').include?("wp-content/plugins")
|
172
|
+
plugin = plugin_name(link.attr('href'))
|
173
|
+
ret << plugin if ret.index(plugin).nil?
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
ret
|
179
|
+
end
|
180
|
+
|
79
181
|
def get_http(page)
|
80
182
|
uri = URI.parse(page)
|
81
183
|
http = Net::HTTP.new(uri.host, uri.port)
|
data/lib/wordstress/version.rb
CHANGED
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.15.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: 2014-11-
|
11
|
+
date: 2014-11-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -95,6 +95,8 @@ files:
|
|
95
95
|
- Rakefile
|
96
96
|
- bin/wordstress
|
97
97
|
- lib/wordstress.rb
|
98
|
+
- lib/wordstress/models/plugins.rb
|
99
|
+
- lib/wordstress/models/themes.rb
|
98
100
|
- lib/wordstress/site.rb
|
99
101
|
- lib/wordstress/utils.rb
|
100
102
|
- lib/wordstress/version.rb
|