1
+ ##
2
+ # This file is part of the Metasploit Framework and may be subject to
3
+ # redistribution and commercial restrictions. Please see the Metasploit
4
+ # web site for more information on licensing and terms of use.
5
+ # http://metasploit.com/
6
+ ##
7
+
8
+ require 'msf/core'
9
+
10
+ class Metasploit3 < Msf ::Auxiliary
11
+ include Msf ::Exploit ::Remote ::HttpClient
12
+ include Msf ::Auxiliary ::Scanner
13
+
14
+ def initialize ( info = { } )
15
+ super ( update_info ( info ,
16
+ 'Name' => 'Wordpress Pingback Port Scanner' ,
17
+ 'Description' => %q{
18
+ This module will perform a port scan using the Pingback API.
19
+ You can even scan the server itself or discover some hosts on
20
+ the internal network this server is part of.
21
+ } ,
22
+ 'Author' =>
23
+ [
24
+ 'Brandon McCann "zeknox" <bmccann[at]accuvant.com>' ,
25
+ 'Thomas McCarthy "smilingraccoon" <smilingraccoon[at]gmail.com>' ,
26
+ 'FireFart' , # Original PoC
27
+ ] ,
28
+ 'License' => MSF_LICENSE ,
29
+ 'References' =>
30
+ [
31
+ [ 'URL' , 'http://www.securityfocus.com/archive/1/525045/30/30/threaded' ] ,
32
+ [ 'URL' , 'http://www.ethicalhack3r.co.uk/security/introduction-to-the-wordpress-xml-rpc-api/' ] ,
33
+ [ 'URL' , 'https://github.com/FireFart/WordpressPingbackPortScanner' ] ,
34
+ ] ,
35
+ ) )
36
+
37
+ register_advanced_options (
38
+ [
39
+ OptInt . new ( 'NUM_REDIRECTS' , [ true , "Number of HTTP redirects to follow" , 10 ] )
40
+ ] , self . class )
41
+ end
42
+
43
+ def setup ( )
44
+ # Check if database is active
45
+ if db ( )
46
+ @db_active = true
47
+ else
48
+ @db_active = false
49
+ end
50
+ end
51
+
52
+ def get_xml_rpc_url ( ip )
53
+ # code to find the xmlrpc url when passed in RHOST
54
+ vprint_status ( "Enumerating XML-RPC URI for #{ ip } ..." )
55
+
56
+ begin
57
+ res = send_request_cgi (
58
+ {
59
+ 'method' => 'HEAD' ,
60
+ } )
61
+ # Check if X-Pingback exists and return value
62
+ unless res . nil?
63
+ unless res [ 'X-Pingback' ] . nil?
64
+ return res [ 'X-Pingback' ]
65
+ else
66
+ print_error ( "X-Pingback header not found, quiting" )
67
+ return nil
68
+ end
69
+ else
70
+ return nil
71
+ end
72
+ rescue ::Rex ::ConnectionRefused , ::Rex ::HostUnreachable , ::Rex ::ConnectionTimeout
73
+ return nil
74
+ rescue ::Timeout ::Error , ::Errno ::EPIPE
75
+ return nil
76
+ end
77
+ end
78
+
79
+ # Creates the XML data to be sent
80
+ def generate_pingback_xml ( target , valid_blog_post )
81
+ xml = "<?xml version=\" 1.0\" encoding=\" iso-8859-1\" ?>"
82
+ xml << "<methodCall>"
83
+ xml << "<methodName>pingback.ping</methodName>"
84
+ xml << "<params>"
85
+ xml << "<param><value><string>#{ target } </string></value></param>"
86
+ xml << "<param><value><string>#{ valid_blog_post } </string></value></param>"
87
+ xml << "</params>"
88
+ xml << "</methodCall>"
89
+ return xml
90
+ end
91
+
92
+ def get_blog_posts ( xml_rpc , ip )
93
+ # find all blog posts within RHOST and determine if pingback is enabled
94
+ vprint_status ( "Enumerating Blog posts..." )
95
+ blog_posts = { }
96
+
97
+ # make http request to feed url
98
+ begin
99
+ res = send_request_cgi ( {
100
+ 'uri' => '/?feed=rss2' ,
101
+ 'method' => 'GET' ,
102
+ } )
103
+
104
+ resolve = true
105
+ count = datastore [ 'NUM_REDIRECTS' ]
106
+ while ( res . code == 301 || res . code == 302 ) and count != 0
107
+ if resolve
108
+ print_status ( "Resolving #{ ip } /?feed=rss2 to locate wordpress feed..." )
109
+ resolve = false
110
+ else
111
+ vprint_status ( "Web server returned a #{ res . code } ...following to #{ res . headers [ 'location' ] } " )
112
+ end
113
+ uri = res . headers [ 'location' ] . sub ( /.*?#{ datastore [ 'RHOST' ] } / , "" )
114
+ res = send_request_cgi ( {
115
+ 'uri' => "#{ uri } " ,
116
+ 'method' => 'GET' ,
117
+ } )
118
+
119
+ if res . code == 200
120
+ print_status ( "Feed located at http://#{ datastore [ 'RHOST' ] } #{ uri } " )
121
+ end
122
+ count = count - 1
123
+ end
124
+ rescue ::Rex ::ConnectionRefused , ::Rex ::HostUnreachable , ::Rex ::ConnectionTimeout
125
+ return nil
126
+ rescue ::Timeout ::Error , ::Errno ::EPIPE
127
+ return nil
128
+ end
129
+
130
+ # parse out links and place in array
131
+ links = res . to_s . scan ( /<link>([^<]+)<\/ link>/i )
132
+
133
+ if res . code != 200 or links . nil? or links . empty?
134
+ return blog_posts
135
+ end
136
+
137
+ links . each do |link |
138
+ blog_post = link [ 0 ]
139
+ pingback_request = get_pingback_request ( xml_rpc , 'http://127.0.0.1' , blog_post )
140
+
141
+ pingback_disabled_match = pingback_request . body . match ( /<value><int>33<\/ int><\/ value>/i )
142
+ if pingback_request . code == 200 and pingback_disabled_match . nil?
143
+ print_good ( "Pingback enabled: #{ link . join } " )
144
+ blog_posts = link . join
145
+ return blog_posts
146
+ else
147
+ vprint_status ( "Pingback disabled: #{ link . join } " )
148
+ end
149
+ end
150
+ return blog_posts
151
+ end
152
+
153
+ # method to send xml-rpc requests
154
+ def get_pingback_request ( xml_rpc , target , blog_post )
155
+ uri = xml_rpc . sub ( /.*?#{ datastore [ 'RHOST' ] } / , "" )
156
+ # create xml pingback request
157
+ pingback_xml = generate_pingback_xml ( target , blog_post )
158
+
159
+ # Send post request with crafted XML as data
160
+ begin
161
+ res = send_request_cgi ( {
162
+ 'uri' => "#{ uri } " ,
163
+ 'method' => 'POST' ,
164
+ 'data' => "#{ pingback_xml } " ,
165
+ } )
166
+ rescue ::Rex ::ConnectionRefused , ::Rex ::HostUnreachable , ::Rex ::ConnectionTimeout
167
+ print_error ( "Unable to connect to #{ uri } " )
168
+ return nil
169
+ rescue ::Timeout ::Error , ::Errno ::EPIPE
170
+ print_error ( "Unable to connect to #{ uri } " )
171
+ return nil
172
+ end
173
+ return res
174
+ end
175
+
176
+ # Save data to vuln table
177
+ def store_vuln ( ip , blog )
178
+ report_vuln (
179
+ :host => ip ,
180
+ :name => self . name ,
181
+ :info => "Module #{ self . fullname } found pingback at #{ blog } "
182
+ )
183
+ end
184
+
185
+ # main control method
186
+ def run_host ( ip )
187
+ # call method to get xmlrpc url
188
+ xmlrpc = get_xml_rpc_url ( ip )
189
+
190
+ # once xmlrpc url is found, get_blog_posts
191
+ if xmlrpc . nil?
192
+ print_error ( "#{ ip } does not appear to be vulnerable" )
193
+ else
194
+ hash = get_blog_posts ( xmlrpc , ip )
195
+
196
+ if hash
197
+ store_vuln ( ip , hash ) if @db_active
198
+ else
199
+ print_status ( "X-Pingback enabled but no vulnerable blogs found on #{ ip } ..." )
200
+ end
201
+ end
202
+ end
203
+ end
0 commit comments