root/msfmods/trunk/modules/auxiliary/scanner/http/webdav_test.rb

Revision 37, 10.7 KB (checked in by sussurro, 3 years ago)

added some suggestions from CG

  • Property svn:keywords set to Id
Line 
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# Framework web site for more information on licensing and terms of use.
5# http://metasploit.com/framework/
6##
7
8
9require 'msf/core'
10require 'pp'
11
12
13class Metasploit3 < Msf::Auxiliary
14       
15        # Exploit mixins should be called first
16        include Msf::Exploit::Remote::HttpClient
17        include Msf::Auxiliary::WMAPScanServer
18        # Scanner mixin should be near last
19        include Msf::Auxiliary::Scanner
20        include Msf::Auxiliary::Report
21
22        def initialize
23                super(
24                        'Name'        => 'HTTP WebDAV Tester',
25                        'Version'     => '$Id$',
26                        'Description' => 'Evaluate a path to determine what can be created/uploaded',
27                        'Author'       => ['Ryan Linn <sussurro[at]happypacket.net'],
28                        'License'     => MSF_LICENSE
29                )
30                register_options(
31                        [
32                                OptString.new('PATH', [ true,  "The URI Path", '/testpath/'])
33                        ], self.class)
34
35               
36        end
37
38        @@jpg_file = Rex::Text.decode_base64("/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAA//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AR//Z")
39
40        # that can be found at http://code.google.com/p/davtest
41        @@checks = {
42                'asp' => '<html><body><% response.write (!N1! * !N2!) %>',
43                'aspx' => '<html><body><% response.write (!N1! * !N2!) %>',
44                'cfm' => '<cfscript>WriteOutput(!N1!*!N2!);</cfscript>',
45                'cgi' => "#!/usr/bin/perl\nprint \"Content-Type: text/html\n\r\n\r\" . !N1! * !N2!;",
46                'html' => '!S1!<br />',
47                'jhtml' => '<%= System.out.println(!N1! * !N2!); %>',
48                'jsp' => '<%= System.out.println(!N1! * !N2!); %>',
49                'php' => '<?php print !N1! * !N2!;?>',
50                'pl' => "#!/usr/bin/perl\nprint \"Content-Type: text/html\n\r\n\r\" . !N1! * !N2!;",
51                'shtml' => '<!--#echo var="DOCUMENT_URI"--><br /><!--#exec cmd="echo !S1!"-->',
52                'txt' => '!S1!'
53        }
54
55
56        def get_options(target_url)
57                begin
58                        res = send_request_raw({
59                                'uri'          => target_url,                                 
60                                'method'       => 'OPTIONS'
61                        }, 10)         
62
63                        if res and res.code == 200
64                                ret = {}       
65                                ret[:server_type] = res.headers['Server']
66                                ret[:options_allowed] = res.headers['Allow']
67                                ret[:options_public] = res.headers['Public']
68                                ret[:webdav] = false
69                               
70                                if (res.headers['DAV'] and res.headers['MS-Author-Via'].match('DAV'))
71                                        ret[:webdav] = true
72                                        ret[:webdav_type] = "unknown"
73                                        if res.headers['X-MSDAVEXT']
74                                                ret[:webdav_type] = 'SHAREPOINT DAV'
75                                        end
76                                        if res.headers['DAV'].match("apache")
77                                                ret[:webdav_type] = "Apache DAV"
78                                        end
79                                end
80                                return ret
81                        end
82                rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
83                rescue ::Timeout::Error, ::Errno::EPIPE
84                return false
85                end
86                return false
87       
88        end
89
90        def check_propfind(target_url)
91                begin
92                        res = send_request_raw({
93                                'uri'          => target_url,
94                                'method'       => 'PROPFIND',
95                                'headers' => { 'Depth' => 1 , 'Content-Length' => 0}
96                        })
97
98                        return false if res and res.code != 207
99                        ret = {}
100                        doc = REXML::Document.new(res.body)
101                        ret[:success] = false
102                        doc.elements.each('D:multistatus/D:response/D:propstat/D:status') do |e|
103                                ret[:success] = true if(e.to_a.to_s.index("200"))
104                        end
105
106                        #find internal IPs..
107                        intipregex = /(192\.168\.[0-9]{1,3}\.[0-9]{1,3}|10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|172\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/i
108
109                        #find directories..
110                        urlregex = /<.:href[^>]*>(.*?)<\/.:href>/i
111
112                        ret[:ips] = res.body.scan(intipregex).uniq
113                        ret[:paths] = res.body.scan(urlregex).uniq
114                        return ret
115
116
117                rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
118                rescue ::Timeout::Error, ::Errno::EPIPE
119                return false
120                end
121                return false
122       
123        end
124
125        def check_createdir(target_url)
126                begin
127                        res = send_request_raw({
128                                'uri'          => target_url,
129                                'method'       => 'MKCOL',
130                                'headers' => { 'Content-Length' => 0}
131                        })
132
133                        return true if res and res.code >= 200 and res.code < 300
134                rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
135                rescue ::Timeout::Error, ::Errno::EPIPE
136                return false
137                end
138       
139        end
140
141        def cleanup_dir(target_url)
142                begin
143                        res = send_request_raw({
144                                'uri'          => target_url + "/",
145                                'method'       => 'DELETE',
146                                'headers' => { 'Content-Length' => 0}
147                        })
148
149                        return true if res and res.code >= 200 and res.code < 300
150
151                rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
152                rescue ::Timeout::Error, ::Errno::EPIPE
153                return false
154                end
155       
156        end
157
158        def check_extensions(target_url)
159                result = []
160                # These checks are based off of Chris Sullo's davtest perl script
161                @@checks.each do |ext,payload|
162                        begin
163                                answer = nil
164                               
165                                fnr = Rex::Text.rand_text_alphanumeric(15)
166                                fn = target_url + "/" + fnr + "." + ext
167                                #print_status("Trying #{fn}")
168                                if(payload.index("!N1!"))
169                                        r1 = rand(10000)/100 * 10
170                                        r2 = rand(10000)/100 * 10
171                                        answer = (r1 *r2).to_s
172                                        payload = payload.gsub("!N1!",r1.to_s) 
173                                        payload = payload.gsub("!N2!",r2.to_s) 
174                                else
175                                        answer = Rex::Text.rand_text_alphanumeric(25)
176                                        payload = payload.gsub("!S1!",answer)
177                                end
178                                payload += "\n\n"
179                                res = send_request_raw({
180                                        'uri'           => fn,
181                                        'method'        => 'PUT',
182                                        'data'          => payload,
183                                        'headers' => { 'Content-Length' => payload.length }
184                                },5)
185                                if(not res or res.code != 201)
186                                        result << [ext,false,false]
187                                        next
188                                end
189                                res = send_request_raw({
190                                        'uri'           => fn,
191                                        'method'        => 'GET'
192                                })
193                                if(not res or res.code != 200 or not res.body.index(answer) or res.body.index("#exec"))
194                                        result << [ext,true,false]
195                                        next
196                                end
197
198                                result << [ext,true,true]
199                                next
200                               
201                        rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
202                        rescue ::Timeout::Error, ::Errno::EPIPE
203                        end
204                        result[ext] = false
205                end
206                result
207        end
208
209        def check_rename(hostname,target_url)
210                result = []
211                # These checks are based off of Chris Sullo's davtest perl script
212                @@checks.each do |ext,payload|
213                        begin
214                                answer = nil
215                               
216                                fnr = Rex::Text.rand_text_alphanumeric(15)
217                                fn = target_url + "/" + fnr + "." + 'txt'
218                                fnd = "http://" + hostname + target_url + "/" + fnr + "." + ext + ';.jpg'
219                                #print_status("Trying #{fnd}")
220                                if(payload.index("!N1!"))
221                                        r1 = rand(10000)/100 * 10
222                                        r2 = rand(10000)/100 * 10
223                                        answer = (r1 *r2).to_s
224                                        payload = payload.gsub("!N1!",r1.to_s) 
225                                        payload = payload.gsub("!N2!",r2.to_s) 
226                                else
227                                        answer = Rex::Text.rand_text_alphanumeric(25)
228                                        payload = payload.gsub("!S1!",answer)
229                                end
230                                payload = @@jpg_file + payload + "\n\n"
231                                res = send_request_raw({
232                                        'uri'           => fn,
233                                        'method'        => 'PUT',
234                                        'data'          => payload,
235                                        'headers' => { 'Content-Length' => payload.length }
236                                },5)
237                                if(not res or res.code != 201)
238                                        result << [ext,false,false]
239                                        next
240                                end
241                                res = send_request_raw({
242                                        'uri'           => fn,
243                                        'method'        => 'MOVE',
244                                        'headers'       => { 'Destination' => fnd }
245                                })
246                                if(not res or res.code != 201 )
247                                        result << [ext,true,false]
248                                        next
249                                end
250                               
251
252                                res = send_request_raw({
253                                        'uri'           => fnd,
254                                        'method'        => 'GET'
255                                })
256                                if(not res or res.code != 200 or not res.body.index(answer) or res.body.index("#exec"))
257                                        result << [ext,true,false]
258                                        next
259                                end
260
261                                result << [ext,true,true]
262                                next
263                               
264                        rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
265                        rescue ::Timeout::Error, ::Errno::EPIPE
266                        end
267                        result[ext] = false
268                end
269                result
270        end
271
272        def run_host(target_host)
273                path = datastore['PATH']
274                info = get_options(path)
275                enabled = false
276
277                if(info)
278                        if(info[:webdav])
279                                enabled = true
280                                print_status("#{target_host}#{path} (#{info[:server_type]}) has #{info[:webdav_type]} ENABLED")
281                                print_status("#{target_host}#{path} (#{info[:server_type]}) Allows Methods: #{info[:options_allowed]}")
282                                if(info[:options_public])
283                                        print_status("#{target_host}#{path} (#{info[:server_type]}) Has Public Methods: #{info[:options_public]}")
284                                end
285                        else
286                                print_status("#{target_host}#{path} (#{info[:server_type]}) is not reporting WEBDAV methods")
287                        end
288                        report_note(
289                                :host   => target_host,
290                                :proto  => 'HTTP',
291                                :port   => rport,
292                                :type   => "SERVER OPTIONS",
293                                :data   => info
294                        )
295                end
296                               
297                davinfo = check_propfind(path)
298                if(davinfo)
299                        if(davinfo[:success] and !enabled)
300                                print_status("#{target_host}#{path} has DAV ENABLED")
301                        end
302                        if(davinfo[:ips].length > 0 )
303                                print_status("#{target_host}#{path} exposed ips #{davinfo[:ips].join(",")}")
304                        end
305                        if(davinfo[:paths])
306                                print_status("#{target_host}#{path} exposed paths #{davinfo[:paths].join(",")}")
307                        end
308                        report_note(
309                                :host   => target_host,
310                                :proto  => 'HTTP',
311                                :port   => rport,
312                                :type   => "DAV_DISCLOSURE",
313                                :data   => davinfo
314                        )
315                       
316                else
317                        print_status("#{target_host}#{path} has DAV DISABLED")
318                        return
319                end
320
321                randstr = Rex::Text.rand_text_alphanumeric(10)
322                testdir = path + "WebDavTest_" + randstr
323                print_status("Attempting to create #{testdir}")
324                if(check_createdir(testdir))
325                        print_status("#{target_host}#{path} is WRITEABLE")
326                else
327                        print_status("#{target_host}#{path} is NOT WRITEABLE")
328                        return
329                end
330                print_status("Checking extensions for upload and execution")
331                results = check_extensions(testdir)
332
333                print_status("Attempting to use IIS rename/copy to bypass restrictions")
334                results2 = check_rename(target_host,testdir)
335
336                print_status("Attempting to cleanup #{testdir}")
337                cleanup_dir(testdir)
338                uploadable = []
339                executable = []
340                iis_renameable = []
341                results.each do |ext,upl,exe|
342                        if(upl)
343                                uploadable << ext
344                        end
345                        if(exe)
346                                executable << ext
347                        end
348                end
349                results2.each do |ext,upl,exe|
350                        iis_renameable << ext if (exe)
351                end
352                print_status("Uploadable files are: #{uploadable.join(",")}")
353                print_status("Executable files are: #{executable.join(",")}")
354                print_status("IIS rename/executable files are: #{iis_renameable.join(",")}")
355                ndata = {}
356                ndata[:executable] = executable
357                ndata[:uploadable] = uploadable
358                ndata[:iis_renamable] = iis_renameable
359
360                               
361                report_note(
362                        :host   => target_host,
363                        :proto  => 'HTTP',
364                        :port   => rport,
365                        :type   => "WRITABLE/EXECUTABLE DAV",
366                        :data   => ndata
367                )
368                       
369        end
370end
371
Note: See TracBrowser for help on using the browser.