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 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