'''
spiderMan.py
Copyright 2006 Andres Riancho
This file is part of w3af, w3af.sourceforge.net .
w3af is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 2 of the License.
w3af is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with w3af; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
'''
import time
import threading
import socket
import signal
import select
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import core.controllers.outputManager as om
from core.controllers.basePlugin.baseDiscoveryPlugin import baseDiscoveryPlugin
import core.data.url.httpResponse as httpResponse
from core.data.request.httpPostDataRequest import httpPostDataRequest
from core.data.request.httpQsRequest import httpQsRequest
import core.data.parsers.urlParser as urlParser
from core.data.getResponseType import *
import cgi
from core.controllers.w3afException import w3afException
from core.controllers.w3afException import w3afRunOnce
LISTENADDRESS = '127.0.0.1'
LISTENPORT = 8998
URLOPENER = None
mutants = []
createFuzzableRequests = None
class spiderMan(baseDiscoveryPlugin):
'''
SpiderMan is a local proxy that will collect new URL's and POSTDATA.
@author: Andres Riancho ( andres.riancho@gmail.com )
'''
def __init__(self):
self._run = True
def discover(self, freq ):
global mutants
if not self._run:
# This will remove the plugin from the discovery plugins to be runned.
raise w3afRunOnce()
else:
self._run = False
global URLOPENER
global spiderManProxy
global createFuzzableRequests
createFuzzableRequests = self._createFuzzableRequests
URLOPENER = self._urlOpener
spiderManProxy = proxy()
om.out.information('To exit spiderMan plugin please navigate to http://w3af/spiderMan?terminate or press ctrl+c .')
spiderManProxy.run()
return mutants
def getOptionsXML(self):
'''
This method returns a XML containing the Options that the plugin has.
Using this XML the framework will build a window, a menu, or some other input method to retrieve
the info from the user. The XML has to validate against the xml schema file located at :
w3af/core/ui/userInterface.dtd
@return: XML with the plugin options.
'''
return '\
\
\
\
\
'
def setOptions( self, optionsMap ):
'''
This method sets all the options that are configured using the user interface
generated by the framework using the result of getOptionsXML().
@parameter OptionList: A dictionary with the options for the plugin.
@return: No value is returned.
'''
global LISTENADDRESS
global LISTENPORT
LISTENADDRESS = optionsMap['listenAddress']
LISTENPORT = optionsMap['listenPort']
def getPluginDeps( self ):
'''
@return: A list with the names of the plugins that should be runned before the
current one.
'''
return []
def getLongDesc( self ):
'''
@return: A DETAILED description of the plugin functions and features.
'''
return '''
This plugin is a local proxy that can be used to give the framework knowledge about the web
application when it has a lot of "client side code" like Flash or Java applets. Whenever a w3af needs to
test an application with flash, he will see that w3af can't read that files, the solution is to use a web browser
to navigate the site using spiderMan proxy.
The proxy will extract information from the user navigation and generate the necesary injection points for the
audit plugins.
Two configurable parameters exist:
- listenAddress
- listenPort
'''
class myHTTPServer(HTTPServer):
def handle_error(self, e ):
# This overrides the default way that SocketServer.py handles exceptions
# I dont want that "fancy" errors, just raise them all
raise e
def handle_request(self):
"""Handle one request, possibly blocking."""
try:
request, client_address = self.get_request()
except socket.error:
return
if self.verify_request(request, client_address):
try:
self.process_request(request, client_address)
except Exception, e:
# Here I changed the order of this two calls
self.close_request(request)
self.handle_error( e )
class proxy(threading.Thread):
#class proxy:
'''
This class is the Proxy.
@author: Andres Riancho ( andres.riancho@gmail.com )
'''
def __init__( self ):
threading.Thread.__init__( self )
self._proxy = None
self._go = True
self._running = False
def stop(self):
if self._running:
self._proxy.server_close()
self._go = False
self._running = False
def run(self):
'''
Starts the http server that will become a proxy.
'''
try:
self._proxy = myHTTPServer((LISTENADDRESS, LISTENPORT ), proxyHandler )
except socket.error, e:
raise w3afException('spiderMan error: ' + str( e[1] ) )
else:
message = 'spiderMan running on '+ LISTENADDRESS + ':'+ str(LISTENPORT) +' .'
om.out.information( message )
self._running = True
while self._go:
try:
self._proxy.handle_request()
except KeyboardInterrupt,e:
om.out.information('spiderMan plugin has been ended by user request.')
self.stop()
except Exception, e:
om.out.error('spiderMan error: '+ str(e) + ' calling self.stop().' )
self.stop()
class proxyHandler(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
self._version = 'spiderMan-w3af/1.0'
BaseHTTPRequestHandler.__init__(self, request, client_address, server)
def _connect( self, host, port ):
'''
Connects to remote host and keeps the connection alive to transfer data.
'''
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
try:
try:
sock.connect((host, port))
finally:
signal.alarm(0)
except TimeoutError:
self.send_error(504, "Proxy timeout.")
return
self.wfile.write("%s %s %s\r\n" % (self.protocol_version, 200, "Connection established"))
self.wfile.write("Proxy-agent: %s\r\n" % self._version)
self.wfile.write("\r\n")
self._read_write(sock, 300)
finally:
sock.close()
self.connection.close()
def _read_write(self, sock, max_idling=20):
rfile = self.rfile
if hasattr(rfile, '_rbuf'):
data = rfile._rbuf
else:
if self.headers.has_key('Content-Length'):
n = int(self.headers['Content-Length'])
data = rfile.read(n)
else:
self.connection.setblocking(0)
try: data = rfile.read()
except IOError: data = ''
self.connection.setblocking(1)
rfile.close()
if data:
sock.send(data)
iw = [self.connection, sock]
count = 0
self.headers_done = 0
while 1:
count += 1
(ins, _, exs) = select.select(iw, [], iw, 3)
if exs: break
if ins:
for i in ins:
if i is sock:
out = self.connection
else:
out = sock
data = i.recv(8192)
if data:
out.send(data)
count = 0
if count == max_idling: break
def _httpRequest( self, command, url, postData, headers ):
'''
Connects to remote host, sends data, receives response and closes connection to remote peer.
'''
functionReference = getattr( URLOPENER , command )
try:
response = functionReference( url, postData, headers )
except KeyboardInterrupt,e:
raise e
except Exception,e:
title = cgi.escape('Error when requesting: '+ url)
desc = cgi.escape('Description: ' + str(e))
om.out.error( title + '. ' +desc)
html = '
'+title+'spiderMan '+ title +'. ' + desc + ''
headers = {'Content-Length': str(len(html))}
response = httpResponse( 400, html, headers, url )
return response
def handle_one_request(self):
'''
Handle a single HTTP request.
'''
self.raw_requestline = self.rfile.readline()
if not self.raw_requestline:
self.close_connection = 1
return
if not self.parse_request():
# An error code has been sent, just exit
return
words = self.raw_requestline.split('\n')[0].split()
if len( words ) == 3:
command, url, version = words
# This is a fix
if command.lower() == 'HEAD':
command = 'GET'
if url == 'http://w3af/spiderMan?terminate':
self._sendEnd( version )
spiderManProxy.stop()
else:
if command == 'CONNECT':
host, port = url.split(':')
port = int(port)
self._connect( host, port )
else:
# Make this proxy as transparent as possible
del self.headers['Proxy-Connection']
del self.headers['keep-alive']
self.headers['connection'] = 'close'
# Copy the headers
headers = {}
for header in self.headers.keys():
headers[header] = self.headers.getheader(header)
postData = ''
try:
length = int(self.headers.getheader('content-length'))
except:
pass
else:
postData = self.rfile.read(length)
global mutants
global createFuzzableRequests
response = self._httpRequest( command, url, postData, headers )
freq = self._createFuzzFromRequest( command, url, postData, headers )
mutants.append( freq )
self._writeResponse( response, version )
if isTextOrHtml( response.getHeaders() ):
mutants.extend( createFuzzableRequests( response ) )
else:
# Client send some garbage, not the 3 words.
return
def _createFuzzFromRequest( self, command, url, postData, headers ):
'''
Creates a fuzzable request based on a query sent FROM the browser.
'''
res = None
if len( postData ):
pdr = httpPostDataRequest()
pdr.setURL( url )
pdr.setMethod( command )
if 'content-length' in headers.keys():
headers.pop('content-length')
pdr.setHeaders( headers )
if 'content-Type' in headers.keys() and headers['content-Type'] == 'multipart/form-data':
dc = cgi.parse_multipart( postData, headers )
for i in dc.keys():
dc = dc[ i ][0]
pdr.setDc( dc )
else:
dc = urlParser.getQueryString( 'http://a/?' + postData )
pdr.setDc( dc )
res = pdr
else:
# Just a query string request ! no postdata
qsr = httpQsRequest()
qsr.setURL( url )
qsr.setMethod( command )
qsr.setHeaders( headers )
dc = urlParser.getQueryString( url )
qsr.setDc( dc )
res = qsr
return res
def _sendEnd( self, version ):
'''
Sends an HTML indicating that w3af spiderMan plugin has finished its execution.
'''
html = 'spiderMan plugin finished its execution.'
headers = {'Content-Length': str(len(html))}
r = httpResponse( 200, html, headers, 'http://w3af/spiderMan?terminate' )
self._writeResponse( r , version )
def _writeResponse( self, response, version ):
'''
Writes the response passed as param to the user:
HTTP/1.1 200 OK
Date: Tue, 05 Dec 2006 20:50:11 GMT
Server: Apache/2.2.3 (Debian) mod_python/3.2.10 Python/2.4.4 PHP/5.2.0-7 mod_perl/2.0.2 Perl/v5.8.8
Content-Length: 2459
Connection: close
Content-Type: text/html; charset=UTF-8
HTML-GOES-HERE
'''
om.out.debug('spiderMan navigated to: ' + response.getURL() )
#om.out.debug( 'HTML\n' + response.getHtml() )
try:
# big try, small coder :P
self.wfile.write( version + ' ' + str(response.getCode()) + ' OK\r\n' )
for header in response.getHeaders().keys():
# remove this header! Transfer-Encoding: chunked
if header.lower() != 'transfer-encoding':
self.wfile.write(header + ': ' + response.getHeaders()[header] +'\r\n' )
self.wfile.write( '\r\n' )
self.wfile.write( response.getBody() )
except Exception,e:
om.out.error('Error while sending data to client. Error: '+ str(e) )
self.close_connection = 1
# This is commonly caused when the user click on the "stop" button
pass