wordstress 0.50.0 → 0.70.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Changelog.md +15 -0
- data/README.md +40 -7
- data/Rakefile +0 -34
- data/bin/wordstress +59 -56
- data/lib/wordstress/site.rb +28 -135
- data/lib/wordstress/utils.rb +46 -20
- data/lib/wordstress/version.rb +1 -1
- metadata +4 -5
- data/lib/wordstress/models/plugins.rb +0 -60
- data/lib/wordstress/models/themes.rb +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 671f7b006dbdafbbb6121ceef8e02955cfc7e911
|
4
|
+
data.tar.gz: b7cff46bb7c7450ddc56616df38b73a2b3010f72
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ccdd7c979267c9d39ff5de0b403dcf11f47f27e832f0732ee0b47b636ed423b3fea6a4117d9ce0d458474d21f9d6df9d28b4c585ea55f52b539c901742ef714d
|
7
|
+
data.tar.gz: 9f9421817bb5b75fac260bafd1fcdf21c6d7dc2d82f6747c2cac1fd52f16b99898da233ea141478d48afb277c44e537dc7b2b54809eba8a8dfdcdb1364278a86
|
data/Changelog.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Wordstress - changelog
|
2
|
+
|
3
|
+
Wordstress is a whitebox security scanner for wordpress powered blogs. It needs
|
4
|
+
[the wordstress](https://wordpress.org/plugins/wordstress/) plugin to be
|
5
|
+
installed on the target server and an access key (stored server side) in order
|
6
|
+
to works.
|
7
|
+
|
8
|
+
It uses [wpvulndb.com](http://wpvulndb.com) vulnerability database.
|
9
|
+
|
10
|
+
_latest update: Tue Mar 3 08:09:56 CET 2015_
|
11
|
+
|
12
|
+
## Version 0.60 (2015-xx-xx) - First release with a Changelog
|
13
|
+
|
14
|
+
* major cleanup for all greybox/blackbox scanning facilities. wordpress now
|
15
|
+
works only in whitebox mode
|
data/README.md
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# Wordstress
|
2
2
|
|
3
|
-
wordstress is
|
4
|
-
websites
|
5
|
-
|
3
|
+
wordstress is an opensource whitebox security scanner for wordpress powered
|
4
|
+
websites.
|
6
5
|
|
7
6
|
## Description
|
8
7
|
|
@@ -64,8 +63,8 @@ wordpress](https://wordpress.org/plugins/wordstress/) you may must:
|
|
64
63
|
generated, to increase entropy you may want to reload the page a couple of
|
65
64
|
times. When you're comfortable with the generated key, press the "Save Changes"
|
66
65
|
button.
|
67
|
-
The virtual page is now available at the url http://youblogurl/wordstress?
|
68
|
-
* from the command line, use wordstress security scanner this way: `
|
66
|
+
The virtual page is now available at the url http://youblogurl/wordstress?wordstress-key=the_key
|
67
|
+
* from the command line, use wordstress security scanner this way: `wordstress -u http://yourblogurl/wordstress -k the_key`
|
69
68
|
* enjoy results
|
70
69
|
|
71
70
|
## Why another tool?
|
@@ -93,6 +92,40 @@ themes and plugins and their version. Since an authenticated check is necessary
|
|
93
92
|
to match scan results with installed plugin (or theme) version, I tought it was
|
94
93
|
a better idea to start authenticated from the beginning.
|
95
94
|
|
95
|
+
## Usage
|
96
|
+
|
97
|
+
Using wordstress from command line is pretty easy. There are 2 mandatory
|
98
|
+
arguments, the key to use to query the wordpress plugin and the target url.
|
99
|
+
|
100
|
+
`$ wordstress -k d4a34e43b5d74c822830b5c4690eccbb621aa372 http://mywordpressblog.com`
|
101
|
+
|
102
|
+
By default, wordstress doesn't look for inactive themes or inactive plugins
|
103
|
+
vulnerabilities. This means that if `foobar_plugin` installed version is
|
104
|
+
vulnerable to privilege escalation, wordstress scanner by default won't raise
|
105
|
+
an alarm if the `foobar_plugin` **is not active**.
|
106
|
+
|
107
|
+
If you want to include vulnerabilities for all themes and vulnerabilities you
|
108
|
+
can use -T and -P flags.
|
109
|
+
|
110
|
+
`$ wordstress -k d4a34e43b5d74c822830b5c4690eccbb621aa372 -T -P http://mywordpressblog.com`
|
111
|
+
|
112
|
+
Examples:
|
113
|
+
$ wordstress -k d4a34e43b5d74c822830b5c4690eccbb621aa372 -B basic_user:basic_password http://mywordpressblog.com
|
114
|
+
|
115
|
+
-k, --key uses the key to access wordstress plugin content on target website
|
116
|
+
-B, --basic-auth user:pwd uses 'user' and 'pwd' as basic auth credentials to target website
|
117
|
+
|
118
|
+
Plugins and themes specific flags
|
119
|
+
|
120
|
+
-T, --fetch-all-themes-vulns retrieves vulnerabilities also for inactive themes
|
121
|
+
-P, --fetch-all-plugins-vulns retrieves vulnerabilities also for inactive plugins
|
122
|
+
|
123
|
+
Service flags
|
124
|
+
|
125
|
+
-D, --debug enters dawn debug mode
|
126
|
+
-v, --version shows version information
|
127
|
+
-h, --help shows this help
|
128
|
+
|
96
129
|
## Online resource
|
97
130
|
|
98
131
|
[Wordstress homepage](http://wordstress.org)
|
@@ -114,6 +147,6 @@ a better idea to start authenticated from the beginning.
|
|
114
147
|
|
115
148
|
1. Fork it ( https://github.com/[my-github-username]/wordstress/fork )
|
116
149
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
117
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
150
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
118
151
|
4. Push to the branch (`git push origin my-new-feature`)
|
119
|
-
5. Create a new Pull Request
|
152
|
+
5. Create a new Pull Request
|
data/Rakefile
CHANGED
@@ -1,35 +1 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
-
|
3
|
-
namespace :update do
|
4
|
-
desc 'Update themes'
|
5
|
-
task :themes, :name do |t,args|
|
6
|
-
|
7
|
-
end
|
8
|
-
|
9
|
-
desc 'Update plugins'
|
10
|
-
task :plugins, :name do |t, args|
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
end
|
15
|
-
namespace :import do
|
16
|
-
desc 'Import themes'
|
17
|
-
task :themes, :name do |t,args|
|
18
|
-
require 'wordstress/models/themes'
|
19
|
-
name = args.name
|
20
|
-
puts "reading themes from #{name}"
|
21
|
-
t = Wordstress::Models::Themes.new({:dbname=>"themes.db"})
|
22
|
-
t.import_from_file(name)
|
23
|
-
end
|
24
|
-
|
25
|
-
desc 'Import plugins'
|
26
|
-
task :plugins, :name do |t, args|
|
27
|
-
require 'wordstress/models/plugins'
|
28
|
-
name = args.name
|
29
|
-
puts "reading themes from #{name}"
|
30
|
-
p = Wordstress::Models::Plugins.new({:dbname=>"plugins.db"})
|
31
|
-
p.import_from_file(name)
|
32
|
-
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
data/bin/wordstress
CHANGED
@@ -7,38 +7,35 @@ require 'codesake-commons'
|
|
7
7
|
|
8
8
|
require 'wordstress'
|
9
9
|
|
10
|
-
# Scanning modes for plugins and themes
|
11
|
-
# + gentleman: wordstress will try to fetch plugins and themes only using
|
12
|
-
# info in the HTML page (this is very polite but also very inaccurate).
|
13
|
-
# + whitebox: wordstress will use a target installed plugin to fetch
|
14
|
-
# installed plugins and themes with their version
|
15
|
-
# + aggressive: wordstress will use enumeration to find installed plugins and
|
16
|
-
# themes. This will lead to false positives.
|
17
|
-
MODES = [:gentleman,:whitebox,:aggressive]
|
18
10
|
APPNAME = File.basename($0)
|
11
|
+
trap("INT") { $logger.die('[INTERRUPTED]') }
|
19
12
|
|
20
13
|
$logger = Codesake::Commons::Logging.instance
|
21
|
-
$logger.debug = false
|
22
|
-
|
23
|
-
@output_root = File.join(Dir.home, '/wordstress')
|
14
|
+
# $logger.debug = false
|
15
|
+
@output_root = File.join(Dir.home, 'wordstress')
|
24
16
|
|
25
|
-
|
26
|
-
|
27
|
-
basic_auth = {:user=>"", :pwd=>""}
|
17
|
+
whitebox = {:url=>"", :key=>""}
|
18
|
+
basic_auth = {:user=>"", :pwd=>""}
|
28
19
|
|
29
20
|
opts = GetoptLong.new(
|
30
|
-
|
31
|
-
[ '--
|
32
|
-
|
33
|
-
[ '--
|
34
|
-
|
35
|
-
[ '--
|
36
|
-
[ '--
|
37
|
-
[ '--
|
38
|
-
[ '--
|
21
|
+
#deprecated
|
22
|
+
[ '--gentleman', '-G', GetoptLong::NO_ARGUMENT],
|
23
|
+
#deprecated
|
24
|
+
[ '--whitebox' , '-W', GetoptLong::NO_ARGUMENT],
|
25
|
+
#deprecated
|
26
|
+
[ '--wordstress-url', '-u', GetoptLong::REQUIRED_ARGUMENT],
|
27
|
+
[ '--fetch-all-themes-vulns', '-T', GetoptLong::NO_ARGUMENT],
|
28
|
+
[ '--fetch-all-plugins-vulns','-P', GetoptLong::NO_ARGUMENT],
|
29
|
+
[ '--basic-auth', '-B', GetoptLong::REQUIRED_ARGUMENT],
|
30
|
+
[ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT],
|
31
|
+
[ '--csv', '-C', GetoptLong::NO_ARGUMENT],
|
32
|
+
[ '--debug', '-D', GetoptLong::NO_ARGUMENT],
|
33
|
+
[ '--version', '-v', GetoptLong::NO_ARGUMENT],
|
34
|
+
[ '--help', '-h', GetoptLong::NO_ARGUMENT]
|
39
35
|
)
|
40
36
|
|
41
37
|
opts.quiet=true
|
38
|
+
deprecation_warning = false
|
42
39
|
|
43
40
|
begin
|
44
41
|
opts.each do |opt, val|
|
@@ -47,10 +44,11 @@ begin
|
|
47
44
|
basic_auth[:user] = val.split(':')[0]
|
48
45
|
basic_auth[:pwd] = val.split(':')[1]
|
49
46
|
when '--whitebox'
|
50
|
-
|
47
|
+
deprecation_warning = true
|
51
48
|
when '--wordstress-url'
|
49
|
+
deprecation_warning = true
|
52
50
|
whitebox[:url] = val
|
53
|
-
when '--
|
51
|
+
when '--key'
|
54
52
|
whitebox[:key] = val
|
55
53
|
when '--version'
|
56
54
|
puts "#{Wordstress::VERSION}"
|
@@ -58,6 +56,7 @@ begin
|
|
58
56
|
when '--debug'
|
59
57
|
$logger.debug = true
|
60
58
|
when '--help'
|
59
|
+
Wordstress::Utils.help
|
61
60
|
Kernel.exit(0)
|
62
61
|
end
|
63
62
|
end
|
@@ -67,66 +66,70 @@ rescue GetoptLong::InvalidOption => e
|
|
67
66
|
Kernel.exit(-1)
|
68
67
|
end
|
69
68
|
|
70
|
-
target=ARGV.shift unless scanning_mode == :whitebox
|
71
|
-
target=whitebox[:url] if scanning_mode == :whitebox
|
72
|
-
|
73
|
-
|
74
69
|
$logger.helo APPNAME, Wordstress::VERSION
|
70
|
+
$logger.warn "-G, -W, -u flags are deprecated and they will be soon removed. whitebox scanning will be the only supported option" if deprecation_warning
|
71
|
+
|
72
|
+
if whitebox[:url].empty?
|
73
|
+
whitebox[:url]="#{ARGV.shift}/wordstress"
|
74
|
+
$logger.warn "assuming wordstress virtual page to be found at #{whitebox[:url]}"
|
75
|
+
end
|
75
76
|
|
76
77
|
unless Dir.exists?(@output_root)
|
77
78
|
$logger.log "creating output dir #{@output_root}"
|
78
79
|
Dir.mkdir @output_root
|
79
80
|
end
|
80
81
|
|
81
|
-
@output_dir = Wordstress::Utils.build_output_dir(@output_root,
|
82
|
+
@output_dir = Wordstress::Utils.build_output_dir(@output_root, whitebox[:url])
|
82
83
|
$logger.log "storing results to #{@output_dir}"
|
83
84
|
FileUtils::mkdir_p(@output_dir)
|
84
85
|
|
85
|
-
|
86
|
-
$logger.die("missing target") if target.nil?
|
86
|
+
$logger.die("missing target") if whitebox[:url].nil?
|
87
87
|
|
88
|
-
$logger.log "scanning #{
|
89
|
-
site = Wordstress::Site.new({:
|
88
|
+
$logger.log "scanning #{whitebox[:url]}"
|
89
|
+
site = Wordstress::Site.new({:whitebox=>whitebox,:basic_auth=>basic_auth, :output_dir=>@output_dir})
|
90
90
|
|
91
|
-
|
92
|
-
$logger.err "can't detect wordpress version running on #{target}. Giving up!"
|
93
|
-
Kernel.exit(-2)
|
94
|
-
end
|
95
|
-
|
96
|
-
$logger.ok "#{target} is a wordpress version #{site.version[:version]} with #{site.themes.count} themes and #{site.plugins.count} plugins"
|
97
|
-
$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
|
91
|
+
$logger.ok "#{Wordstress::Utils.url_to_target(whitebox[:url])} is a wordpress version #{site.version[:version]} with #{site.themes.count} themes and #{site.plugins.count} plugins"
|
98
92
|
|
99
93
|
if site.online?
|
100
94
|
site.wp_vuln["wordpress"]["vulnerabilities"].each do |v|
|
101
95
|
$logger.err "#{v["title"]}. Detected: #{site.version[:version]}. Safe: #{v["fixed_in"]}" if Gem::Version.new(site.version[:version]) <= Gem::Version.new(v["fixed_in"])
|
102
96
|
end
|
103
97
|
site.themes.each do |t|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
98
|
+
|
99
|
+
if t[:status] == "inactive"
|
100
|
+
$logger.debug "Theme #{t[:name]} is inactive no looking for vulns. Use '--fetch-all-themes-vulns' flag to fetch inactive theme vulnerabilities"
|
101
|
+
else
|
102
|
+
v = site.get_theme_vulnerabilities(t[:name])
|
103
|
+
unless v["theme"].nil?
|
104
|
+
v["theme"]["vulnerabilities"].each do |vv|
|
105
|
+
if Gem::Version.new(t[:version]) <= Gem::Version.new(vv["fixed_in"])
|
106
|
+
$logger.err "Theme #{vv["title"]} is vulnerable. Detected: #{t[:version]}. Safe: #{vv["fixed_in"]}"
|
107
|
+
site.theme_vulns << {:title=>vv["title"], :cve=>vv["cve"], :url=>vv["url"], :detected=>t[:version], :fixed_in=>vv["fixed_in"]}
|
108
|
+
end
|
110
109
|
end
|
111
110
|
end
|
112
|
-
end
|
111
|
+
end # if t[:status] == "inactive"
|
113
112
|
end
|
114
113
|
|
115
114
|
site.plugins.each do |t|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
115
|
+
if t[:status] == "inactive"
|
116
|
+
$logger.debug "Plugin #{t[:name]} is inactive no looking for vulns. Use '--fetch-all-plugins-vulns' flag to fetch inactive plugin vulnerabilities"
|
117
|
+
else
|
118
|
+
v = site.get_plugin_vulnerabilities(t[:name])
|
119
|
+
unless v["plugin"].nil?
|
120
|
+
v["plugin"]["vulnerabilities"].each do |vv|
|
121
|
+
if Gem::Version.new(t[:version]) <= Gem::Version.new(vv["fixed_in"])
|
122
|
+
$logger.err "Plugin #{vv["title"]} is vulnerable. Detected: #{t[:version]}. Safe: #{vv["fixed_in"]}"
|
123
|
+
site.plugin_vulns << {:title=>vv["title"], :cve=>vv["cve"], :url=>vv["url"], :detected=>t[:version], :fixed_in=>vv["fixed_in"]}
|
124
|
+
end
|
122
125
|
end
|
123
126
|
end
|
124
|
-
end
|
127
|
+
end # if t[:status] == "inactive"
|
125
128
|
end
|
126
129
|
else
|
127
130
|
site.online = false
|
128
131
|
$logger.err "it seems we are offline. wordstress can't reach https://wpvulndb.com"
|
129
|
-
$logger.err "
|
132
|
+
$logger.err "wordstress can't enumerate vulnerabilities"
|
130
133
|
end
|
131
134
|
|
132
135
|
site.stop_scan
|
data/lib/wordstress/site.rb
CHANGED
@@ -4,19 +4,17 @@ require 'terminal-table'
|
|
4
4
|
module Wordstress
|
5
5
|
class Site
|
6
6
|
|
7
|
-
attr_reader :version, :
|
7
|
+
attr_reader :version, :wp_vuln, :plugins, :themes
|
8
8
|
attr_accessor :theme_vulns, :plugin_vulns, :online
|
9
9
|
|
10
|
-
def initialize(options={:
|
11
|
-
@target = options[:target]
|
10
|
+
def initialize(options={:whitebox=>{:url=>"http://localhost/wordstress", :key=>""}, :basic_auth=>{:user=>"", :pwd=>""}, :output_dir=>"./"})
|
12
11
|
begin
|
13
|
-
@uri = URI(options[:
|
14
|
-
@raw_name = options[:target]
|
12
|
+
@uri = URI(options[:whitebox][:url])
|
15
13
|
@valid = true
|
16
14
|
rescue
|
17
15
|
@valid = false
|
18
16
|
end
|
19
|
-
@
|
17
|
+
@target = Wordstress::Utils.url_to_target(options[:whitebox][:url])
|
20
18
|
|
21
19
|
@basic_auth_user = options[:basic_auth][:user]
|
22
20
|
@basic_auth_pwd = options[:basic_auth][:pwd]
|
@@ -25,16 +23,8 @@ module Wordstress
|
|
25
23
|
@start_time = Time.now
|
26
24
|
@end_time = Time.now # I hate init variables to nil...
|
27
25
|
|
28
|
-
|
29
|
-
|
30
|
-
@robots_txt = get(@raw_name + "/robots.txt")
|
31
|
-
@readme_html = get(@raw_name + "/readme.html")
|
32
|
-
@homepage = get(@raw_name)
|
33
|
-
@version = detect_version(@homepage, false)
|
34
|
-
else
|
35
|
-
@wordstress_page = get("#{options[:whitebox][:url]}?wordstress-key=#{options[:whitebox][:key]}") if options[:scanning_mode] == :whitebox
|
36
|
-
@version = detect_version(@wordstress_page, true)
|
37
|
-
end
|
26
|
+
@wordstress_page = get("#{options[:whitebox][:url]}?wordstress-key=#{options[:whitebox][:key]}")
|
27
|
+
@version = detect_version(@wordstress_page)
|
38
28
|
|
39
29
|
@online = true
|
40
30
|
|
@@ -58,7 +48,7 @@ module Wordstress
|
|
58
48
|
return JSON.parse(json)
|
59
49
|
rescue => e
|
60
50
|
$logger.err e.message
|
61
|
-
@online = false
|
51
|
+
@online = false unless e.message.include?"403"
|
62
52
|
return JSON.parse("{}")
|
63
53
|
end
|
64
54
|
end
|
@@ -70,7 +60,7 @@ module Wordstress
|
|
70
60
|
return JSON.parse(json)
|
71
61
|
rescue => e
|
72
62
|
$logger.err e.message
|
73
|
-
@online = false
|
63
|
+
@online = false unless e.message.include?"403"
|
74
64
|
return JSON.parse("{}")
|
75
65
|
end
|
76
66
|
end
|
@@ -82,7 +72,7 @@ module Wordstress
|
|
82
72
|
return JSON.parse("{\"wordpress\":{\"vulnerabilities\":[]}}") if page.class == Net::HTTPNotFound
|
83
73
|
rescue => e
|
84
74
|
$logger.err e.message
|
85
|
-
@online = false
|
75
|
+
@online = false unless e.message.include?"403"
|
86
76
|
return JSON.parse("{}")
|
87
77
|
end
|
88
78
|
end
|
@@ -92,12 +82,7 @@ module Wordstress
|
|
92
82
|
# return version.gsub('.', '')+'0' if version.split('.').count == 2
|
93
83
|
end
|
94
84
|
|
95
|
-
def detect_version(page
|
96
|
-
detect_version_blackbox(page) unless whitebox
|
97
|
-
detect_version_whitebox(page) if whitebox
|
98
|
-
end
|
99
|
-
|
100
|
-
def detect_version_whitebox(page)
|
85
|
+
def detect_version(page)
|
101
86
|
v_meta = '0.0.0'
|
102
87
|
doc = Nokogiri::HTML(page.body)
|
103
88
|
doc.css('#wp_version').each do |link|
|
@@ -107,50 +92,6 @@ module Wordstress
|
|
107
92
|
return {:version => v_meta, :accuracy => 1.0}
|
108
93
|
end
|
109
94
|
|
110
|
-
def detect_version_blackbox(page)
|
111
|
-
|
112
|
-
#
|
113
|
-
# 1. trying to detect wordpress version from homepage body meta generator
|
114
|
-
# tag
|
115
|
-
|
116
|
-
v_meta = ""
|
117
|
-
doc = Nokogiri::HTML(page.body)
|
118
|
-
doc.xpath("//meta[@name='generator']/@content").each do |attr|
|
119
|
-
v_meta = attr.value.split(' ')[1]
|
120
|
-
end
|
121
|
-
|
122
|
-
#
|
123
|
-
# 2. trying to detect wordpress version from readme.html in the root
|
124
|
-
# directory
|
125
|
-
#
|
126
|
-
# Not available if scanning
|
127
|
-
|
128
|
-
unless whitebox
|
129
|
-
v_readme = ""
|
130
|
-
doc = Nokogiri::HTML(@readme_html.body)
|
131
|
-
v_readme = doc.at_css('h1').children.last.text.chop.lstrip.split(' ')[1]
|
132
|
-
end
|
133
|
-
|
134
|
-
#
|
135
|
-
# 3. Detect from RSS link
|
136
|
-
#
|
137
|
-
v_rss = ""
|
138
|
-
rss_doc = Nokogiri::HTML(page.body)
|
139
|
-
begin
|
140
|
-
rss = Nokogiri::HTML(get(rss_doc.css('link[type="application/rss+xml"]').first.attr('href')).body) unless l.nil?
|
141
|
-
v_rss= rss.css('generator').text.split('=')[1]
|
142
|
-
rescue => e
|
143
|
-
v_rss = "0.0.0"
|
144
|
-
end
|
145
|
-
|
146
|
-
|
147
|
-
return {:version => v_meta, :accuracy => 1.0} if v_meta == v_readme && v_meta == v_rss
|
148
|
-
return {:version => v_meta, :accuracy => 0.8} if v_meta == v_readme || v_meta == v_rss
|
149
|
-
|
150
|
-
# we failed detecting wordpress version
|
151
|
-
return {:version => "0.0.0", :accuracy => 0}
|
152
|
-
end
|
153
|
-
|
154
95
|
def get(page)
|
155
96
|
return get_http(page) if @uri.scheme == "http"
|
156
97
|
return get_https(page) if @uri.scheme == "https"
|
@@ -163,22 +104,26 @@ module Wordstress
|
|
163
104
|
return @online
|
164
105
|
end
|
165
106
|
|
166
|
-
def find_themes
|
167
|
-
return find_themes_gentleman if @scanning_mode == :gentleman
|
168
|
-
return find_themes_whitebox if @scanning_mode == :whitebox
|
169
|
-
return []
|
170
|
-
end
|
171
107
|
def find_plugins
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
108
|
+
ret = []
|
109
|
+
doc = Nokogiri::HTML(@wordstress_page.body)
|
110
|
+
doc.css('#all_plugin').each do |link|
|
111
|
+
l=link.text.split(',')
|
112
|
+
ret << {:name=>l[2].split('/')[0], :version=>l[1], :status=>l[3]} unless is_already_detected?(ret, l[2])
|
113
|
+
end
|
114
|
+
ret
|
115
|
+
end
|
116
|
+
def find_themes
|
117
|
+
ret = []
|
118
|
+
doc = Nokogiri::HTML(@wordstress_page.body)
|
119
|
+
doc.css('#all_theme').each do |link|
|
120
|
+
l=link.text.split(',')
|
121
|
+
ret << {:name=>l[2], :version=>l[1], :status=>l[3]} unless is_already_detected?(ret, l[2])
|
122
|
+
end
|
123
|
+
ret
|
180
124
|
end
|
181
125
|
|
126
|
+
|
182
127
|
def ascii_report
|
183
128
|
# 0_Executive summary
|
184
129
|
rows = []
|
@@ -271,36 +216,6 @@ module Wordstress
|
|
271
216
|
return (!a.nil?)
|
272
217
|
end
|
273
218
|
|
274
|
-
def find_plugins_whitebox
|
275
|
-
ret = []
|
276
|
-
doc = Nokogiri::HTML(@wordstress_page.body)
|
277
|
-
doc.css('#all_plugin').each do |link|
|
278
|
-
l=link.text.split(',')
|
279
|
-
ret << {:name=>l[2].split('/')[0], :version=>l[1], :status=>l[3]} unless is_already_detected?(ret, l[2])
|
280
|
-
end
|
281
|
-
ret
|
282
|
-
end
|
283
|
-
def find_themes_whitebox
|
284
|
-
ret = []
|
285
|
-
doc = Nokogiri::HTML(@wordstress_page.body)
|
286
|
-
doc.css('#all_theme').each do |link|
|
287
|
-
l=link.text.split(',')
|
288
|
-
ret << {:name=>l[2], :version=>l[1], :status=>l[3]} unless is_already_detected?(ret, l[2])
|
289
|
-
end
|
290
|
-
ret
|
291
|
-
end
|
292
|
-
|
293
|
-
def find_themes_gentleman
|
294
|
-
ret = []
|
295
|
-
doc = Nokogiri::HTML(@homepage.body)
|
296
|
-
doc.css('link').each do |link|
|
297
|
-
if link.attr('href').include?("wp-content/themes")
|
298
|
-
theme = theme_name(link.attr('href'))
|
299
|
-
ret << {:name=>theme, :version=>""} unless is_already_detected?(ret, theme)
|
300
|
-
end
|
301
|
-
end
|
302
|
-
ret
|
303
|
-
end
|
304
219
|
|
305
220
|
def theme_name(url)
|
306
221
|
url.match(/\/wp-content\/themes\/(\w)+/)[0].split('/').last
|
@@ -309,28 +224,6 @@ module Wordstress
|
|
309
224
|
url.match(/\/wp-content\/plugins\/(\w)+/)[0].split('/').last
|
310
225
|
end
|
311
226
|
|
312
|
-
def find_plugins_gentleman
|
313
|
-
ret = []
|
314
|
-
doc = Nokogiri::HTML(@homepage.body)
|
315
|
-
doc.css('script').each do |link|
|
316
|
-
if ! link.attr('src').nil?
|
317
|
-
if link.attr('src').include?("wp-content/plugins")
|
318
|
-
plugin = plugin_name(link.attr('src'))
|
319
|
-
ret << {:name=>plugin, :version=>"", :status=>"active"} unless is_already_detected?(ret, plugin)
|
320
|
-
end
|
321
|
-
end
|
322
|
-
end
|
323
|
-
doc.css('link').each do |link|
|
324
|
-
if link.attr('href').include?("wp-content/plugins")
|
325
|
-
plugin = plugin_name(link.attr('href'))
|
326
|
-
ret << plugin if ret.index(plugin).nil?
|
327
|
-
end
|
328
|
-
|
329
|
-
end
|
330
|
-
|
331
|
-
ret
|
332
|
-
end
|
333
|
-
|
334
227
|
def get_http(page, use_ssl=false)
|
335
228
|
uri = URI(page)
|
336
229
|
req = Net::HTTP::Get.new(uri)
|
@@ -349,7 +242,7 @@ module Wordstress
|
|
349
242
|
when Net::HTTPRedirection then
|
350
243
|
location = res['location']
|
351
244
|
$logger.debug "redirected to #{location}"
|
352
|
-
get_http(location)
|
245
|
+
get_http(location, use_ssl)
|
353
246
|
when Net::HTTPNotFound
|
354
247
|
return res
|
355
248
|
else
|
data/lib/wordstress/utils.rb
CHANGED
@@ -1,32 +1,58 @@
|
|
1
1
|
module Wordstress
|
2
2
|
class Utils
|
3
|
+
def self.help
|
4
|
+
|
5
|
+
puts "wordstress v#{Wordstress::VERSION} (http://wordstress.org)"
|
6
|
+
|
7
|
+
puts "Usage: wordstress [options] url"
|
8
|
+
printf "\nExamples:\n"
|
9
|
+
puts "\t$ wordstress -k d4a34e43b5d74c822830b5c4690eccbb621aa372 http://mywordpressblog.com"
|
10
|
+
puts "\t$ wordstress -k d4a34e43b5d74c822830b5c4690eccbb621aa372 -B basic_user:basic_password http://mywordpressblog.com"
|
11
|
+
puts "\t$ wordstress -k d4a34e43b5d74c822830b5c4690eccbb621aa372 -T -P http://mywordpressblog.com"
|
12
|
+
printf "\n -k, --key\t\t\t\tuses the key to access wordstress plugin content on target website"
|
13
|
+
printf "\n -B, --basic-auth user:pwd\t\tuses 'user' and 'pwd' as basic auth credentials to target website"
|
14
|
+
printf "\n\nPlugins and themes specific flags\n"
|
15
|
+
printf "\n -T, --fetch-all-themes-vulns\t\tretrieves vulnerabilities also for inactive themes"
|
16
|
+
printf "\n -P, --fetch-all-plugins-vulns\tretrieves vulnerabilities also for inactive plugins"
|
17
|
+
printf "\n\nService flags\n"
|
18
|
+
printf "\n -D, --debug\t\t\t\tenters dawn debug mode"
|
19
|
+
printf "\n -v, --version\t\t\tshows version information"
|
20
|
+
printf "\n -h, --help\t\t\t\tshows this help\n"
|
21
|
+
|
22
|
+
true
|
23
|
+
end
|
3
24
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
25
|
+
def self.url_to_target(url)
|
26
|
+
uri = URI.parse(url)
|
27
|
+
"#{uri.scheme}://#{uri.host}#{uri.request_uri.gsub("/wordstress", "")}" if uri.port == 80
|
28
|
+
"#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.request_uri.gsub("/wordstress", "")}" unless uri.port == 80
|
29
|
+
end
|
30
|
+
# Transform a given URL into a directory name to be used to store data
|
31
|
+
def self.target_to_dirname(target)
|
32
|
+
uri = URI.parse(target)
|
33
|
+
path = uri.request_uri.split('/')
|
34
|
+
blog_path = ""
|
35
|
+
blog_path = "_#{path[1]}" if path.count >= 2
|
36
|
+
return "#{uri.host}_#{uri.port}#{blog_path}"
|
37
|
+
end
|
16
38
|
|
17
|
-
|
39
|
+
def self.build_output_dir(root, target)
|
40
|
+
attempt=0
|
41
|
+
today=Time.now.strftime("%Y%m%d")
|
18
42
|
|
19
|
-
|
20
|
-
if attempt != 0
|
21
|
-
proposed += "_#{attempt}"
|
22
|
-
end
|
43
|
+
while 1 do
|
23
44
|
|
24
|
-
|
25
|
-
|
45
|
+
proposed = File.join(root, Wordstress::Utils.target_to_dirname(target), today)
|
46
|
+
if attempt != 0
|
47
|
+
proposed += "_#{attempt}"
|
26
48
|
end
|
27
49
|
|
28
|
-
|
50
|
+
return proposed unless Dir.exists?(proposed)
|
51
|
+
attempt +=1 if Dir.exists?(proposed)
|
29
52
|
end
|
30
53
|
|
54
|
+
|
31
55
|
end
|
56
|
+
|
57
|
+
end
|
32
58
|
end
|
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.70.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:
|
11
|
+
date: 2016-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -105,14 +105,13 @@ extensions: []
|
|
105
105
|
extra_rdoc_files: []
|
106
106
|
files:
|
107
107
|
- ".gitignore"
|
108
|
+
- Changelog.md
|
108
109
|
- Gemfile
|
109
110
|
- LICENSE.txt
|
110
111
|
- README.md
|
111
112
|
- Rakefile
|
112
113
|
- bin/wordstress
|
113
114
|
- lib/wordstress.rb
|
114
|
-
- lib/wordstress/models/plugins.rb
|
115
|
-
- lib/wordstress/models/themes.rb
|
116
115
|
- lib/wordstress/site.rb
|
117
116
|
- lib/wordstress/utils.rb
|
118
117
|
- lib/wordstress/version.rb
|
@@ -137,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
136
|
version: '0'
|
138
137
|
requirements: []
|
139
138
|
rubyforge_project:
|
140
|
-
rubygems_version: 2.
|
139
|
+
rubygems_version: 2.4.8
|
141
140
|
signing_key:
|
142
141
|
specification_version: 4
|
143
142
|
summary: wordstress is a security scanner for wordpress powered websites
|
@@ -1,60 +0,0 @@
|
|
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
|
@@ -1,62 +0,0 @@
|
|
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
|