## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # web site for more information on licensing and terms of use. # http://metasploit.com/ ## require 'msf/core' class Metasploit3 < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Scanner def initialize(info = {}) super(update_info(info, 'Name' => 'Wordpress Pingback Locator', 'Description' => %q{ This module will scan for wordpress sites with the Pingback API enabled. By interfacing with the API an attacker can cause the wordpress site to port scan an external target and return results. Refer to the wordpress_pingback_portscanner module. This issue was fixed in wordpress 3.5.1 }, 'Author' => [ 'Thomas McCarthy "smilingraccoon" ', 'Brandon McCann "zeknox" ' , 'Christian Mehlmauer "FireFart" ' # Original PoC ], 'License' => MSF_LICENSE, 'References' => [ [ 'URL', 'http://www.securityfocus.com/archive/1/525045/30/30/threaded'], [ 'URL', 'http://www.ethicalhack3r.co.uk/security/introduction-to-the-wordpress-xml-rpc-api/'], [ 'URL', 'https://github.com/FireFart/WordpressPingbackPortScanner'] ] )) register_options( [ OptString.new('TARGETURI', [ true, 'The path to wordpress installation (e.g. /wordpress/)', '/']) ], self.class) register_advanced_options( [ OptInt.new('NUM_REDIRECTS', [ true, "Number of HTTP redirects to follow", 10]) ], self.class) end def setup() # Check if database is active if db() @db_active = true else @db_active = false end end def get_xml_rpc_url(ip) # code to find the xmlrpc url when passed in IP vprint_status("#{ip} - Enumerating XML-RPC URI...") begin uri = target_uri.path uri << '/' if uri[-1,1] != '/' res = send_request_cgi( { 'method' => 'HEAD', 'uri' => "#{uri}" }) # Check if X-Pingback exists and return value if res if res['X-Pingback'] return res['X-Pingback'] else vprint_status("#{ip} - X-Pingback header not found") return nil end else return nil end rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout vprint_error("#{ip} - Unable to connect") return nil rescue ::Timeout::Error, ::Errno::EPIPE vprint_error("#{ip} - Unable to connect") return nil end end # Creates the XML data to be sent def generate_pingback_xml(target, valid_blog_post) xml = "" xml << "" xml << "pingback.ping" xml << "" xml << "#{target}" xml << "#{valid_blog_post}" xml << "" xml << "" return xml end def get_blog_posts(xml_rpc, ip) # find all blog posts within IP and determine if pingback is enabled vprint_status("#{ip} - Enumerating Blog posts on...") blog_posts = nil uri = target_uri.path uri << '/' if uri[-1,1] != '/' # make http request to feed url begin vprint_status("#{ip} - Resolving #{uri}?feed=rss2 to locate wordpress feed...") res = send_request_cgi({ 'uri' => "#{uri}?feed=rss2", 'method' => 'GET' }) count = datastore['NUM_REDIRECTS'] # Follow redirects while (res.code == 301 || res.code == 302) and res.headers['Location'] and count != 0 vprint_status("#{ip} - Web server returned a #{res.code}...following to #{res.headers['Location']}") uri = res.headers['Location'].sub(/(http|https):\/\/.*?\//, "/") res = send_request_cgi({ 'uri' => "#{uri}", 'method' => 'GET' }) if res.code == 200 vprint_status("#{ip} - Feed located at #{uri}") else vprint_status("#{ip} - Returned a #{res.code}...") end count = count - 1 end rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout vprint_error("#{ip} - Unable to connect") return nil rescue ::Timeout::Error, ::Errno::EPIPE vprint_error("#{ip} - Unable to connect") return nil end if res.nil? or res.code != 200 vprint_status("#{ip} - Did not recieve HTTP response from #{ip}") return blog_posts end # parse out links and place in array links = res.body.scan(/([^<]+)<\/link>/i) if links.nil? or links.empty? vprint_status("#{ip} - Feed at #{ip} did not have any links present") return blog_posts end links.each do |link| blog_post = link[0] pingback_response = get_pingback_request(xml_rpc, 'http://127.0.0.1', blog_post) if pingback_response pingback_disabled_match = pingback_response.body.match(/33<\/int><\/value>/i) if pingback_response.code == 200 and pingback_disabled_match.nil? print_good("#{ip} - Pingback enabled: #{link.join}") blog_posts = link.join return blog_posts else vprint_status("#{ip} - Pingback disabled: #{link.join}") end end end return blog_posts end # method to send xml-rpc requests def get_pingback_request(xml_rpc, target, blog_post) uri = xml_rpc.sub(/.*?#{target}/,"") # create xml pingback request pingback_xml = generate_pingback_xml(target, blog_post) # Send post request with crafted XML as data begin res = send_request_cgi({ 'uri' => "#{uri}", 'method' => 'POST', 'data' => "#{pingback_xml}" }) rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout vprint_error("Unable to connect to #{uri}") return nil rescue ::Timeout::Error, ::Errno::EPIPE vprint_error("Unable to connect to #{uri}") return nil end return res end # Save data to vuln table def store_vuln(ip, blog) report_vuln( :host => ip, :proto => 'tcp', :port => datastore['RPORT'], :name => self.name, :info => "Module #{self.fullname} found pingback at #{blog}", :sname => datastore['SSL'] ? "https" : "http" ) end # main control method def run_host(ip) # call method to get xmlrpc url xmlrpc = get_xml_rpc_url(ip) # once xmlrpc url is found, get_blog_posts if xmlrpc.nil? vprint_error("#{ip} - It doesn't appear to be vulnerable") else hash = get_blog_posts(xmlrpc, ip) if hash store_vuln(ip, hash) if @db_active else vprint_status("#{ip} - X-Pingback enabled but no vulnerable blogs found") end end end end