wordstress 0.10.3 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|