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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 47711364bb49aa85f8a3e426d687bb3b628c69fa
4
- data.tar.gz: 3c2d000a48d7a35bf7edfdbb6f0096dfa44aced1
3
+ metadata.gz: 559b014cd3100530e2a30f54da9bbebcd37530c4
4
+ data.tar.gz: 0398ef2797138d181fba62c84609456e5a1e4d76
5
5
  SHA512:
6
- metadata.gz: 93e2d400927c84b8dc665e84a075f19616c7320a6e8fa4908c19942ca613cb08eec46e447dd0d8b0bc3679aa89f0c2d3935c78a7d4336b8e4662a142cb866f6f
7
- data.tar.gz: 10d005cbc9ca7effc097869e19daa4f5d65bc54956320f18ba1125b650c40872d3824e4d3358e4a7f3de091f168e817acc8fc07cfee3b2b8d08b8d3733794a07
6
+ metadata.gz: a1c1a877d30ef0557957ac1bc2f56bedc33653e78cb16b0c014ba667a38fb5ef3b8572cfe93e09ba06f97e0980de749270f815034d914cf159bae93371d50ab9
7
+ data.tar.gz: 0b9cc780b6a570919b72aa637b6f86c00c63ac6a91fc3eac59c5f547113cc7232d70e42f2d9af01aca9da9444d388989f1ee0f356f2257babf30b23ceb03eaa0
data/.gitignore CHANGED
@@ -1,4 +1,6 @@
1
1
  *.sw?
2
+ plugins.db
3
+ themes.db
2
4
  .rvmrc
3
5
  .DS_Store
4
6
  /.bundle/
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
- wp_vuln_hash = JSON.parse(site.wp_vuln_json)
65
- $logger.ok "#{wp_vuln_hash["wordpress"]["vulnerabilities"].size} vulnerabilities found due wordpress version"
66
- wp_vuln_hash["wordpress"]["vulnerabilities"].each do |v|
67
- $logger.log "#{v["id"]} - #{v["title"]}"
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
@@ -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
- def initialize(name="")
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(name)
10
- @raw_name = 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
- get_https("https://wpvulndb.com/api/v1/wordpresses/#{version_pad(@version[:version])}").body
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)
@@ -1,3 +1,3 @@
1
1
  module Wordstress
2
- VERSION = "0.10.3"
2
+ VERSION = "0.15.0"
3
3
  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.10.3
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-25 00:00:00.000000000 Z
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