now sends 1 request per form input
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
xsscrapy
|
||||
========
|
||||
|
||||
Fast, thorough, XSS spider. Give it a URL and it'll test every link it finds for cross-site scripting vulnerabilities.
|
||||
|
||||
From within the main folder run:
|
||||
|
||||
```
|
||||
scrapy crawl xsscrapy -a url='http://something.com'
|
||||
./xsscrapy.py -u http://something.com
|
||||
```
|
||||
|
||||
|
||||
If you wish to login then crawl:
|
||||
|
||||
```
|
||||
scrapy crawl xsscrapy -a url='http://something.com/login_page' -a login=username \
|
||||
-a pw=secret_password
|
||||
./xsscrapy.py -u http://something.com/login_page -l loginname -p pa$$word
|
||||
```
|
||||
|
||||
Output is stored in formatted-urls.txt.
|
||||
|
||||
29
xsscrapy.py
Executable file
29
xsscrapy.py
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
from scrapy.cmdline import execute
|
||||
from xsscrapy.spiders.xss_spider import XSSspider
|
||||
|
||||
__author__ = 'Dan McInerney'
|
||||
__license__ = 'BSD'
|
||||
__version__ = '1.0.0'
|
||||
__email__ = 'danhmcinerney@gmail.com'
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('-u', '--url', help="URL to scan; -u http://example.com")
|
||||
parser.add_argument('-l', '--login', help="Login name; -l danmcinerney")
|
||||
parser.add_argument('-p', '--password', help="Password; -p pa$$w0rd")
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
args = get_args()
|
||||
url = args.url
|
||||
user = args.login
|
||||
password = args.password
|
||||
|
||||
try:
|
||||
execute(['scrapy', 'crawl', 'xsscrapy', '-a', 'url=%s' % url, '-a', 'user=%s' % user, '-a', 'pw=%s' % password])
|
||||
except KeyboardInterrupt:
|
||||
sys.exit()
|
||||
@@ -46,42 +46,29 @@ class InjectedDupeFilter(object):
|
||||
url = request.url.replace(delim, '')
|
||||
if url in URLS_SEEN:
|
||||
raise IgnoreRequest
|
||||
|
||||
if request.callback == spider.xss_chars_finder:
|
||||
spider.log('Sending payloaded URL: %s' % url)
|
||||
|
||||
spider.log('Sending payloaded URL: %s' % url)
|
||||
URLS_SEEN.add(url)
|
||||
return
|
||||
|
||||
# Injected form dupe handling
|
||||
elif meta['xss_place'] == 'form':
|
||||
stripped_vals = [v[0].replace(delim, '') for v in meta['values']]
|
||||
u = request.url
|
||||
v = stripped_vals
|
||||
m = request.method
|
||||
# URL, input, payload, values
|
||||
u_v_m = (u, v, m)
|
||||
if u_v_m in FORMS_SEEN:
|
||||
u = meta['POST_to']
|
||||
p = meta['xss_param']
|
||||
u_p = (u, p)
|
||||
if u_p in FORMS_SEEN:
|
||||
raise IgnoreRequest
|
||||
|
||||
if request.callback == spider.xss_chars_finder:
|
||||
spider.log('Sending request for possibly vulnerable form to %s' % u)
|
||||
|
||||
FORMS_SEEN.add(u_v_m)
|
||||
spider.log('Sending payloaded form param %s to: %s' % (p, u))
|
||||
FORMS_SEEN.add(u_p)
|
||||
return
|
||||
|
||||
# Injected header dupe handling
|
||||
elif meta['xss_place'] == 'header':
|
||||
u = request.url
|
||||
h = meta['xss_param']
|
||||
p = meta['payload'].replace(delim, '')
|
||||
# URL, changed header, payload
|
||||
u_h_p = (u, h, p)
|
||||
if u_h_p in HEADERS_SEEN:
|
||||
u_h = (u, h)
|
||||
if u_h in HEADERS_SEEN:
|
||||
raise IgnoreRequest
|
||||
|
||||
elif request.callback == spider.xss_chars_finder:
|
||||
spider.log('Sending payloaded %s header, payload: %s' % (h, p))
|
||||
|
||||
HEADERS_SEEN.add(u_h_p)
|
||||
spider.log('Sending payloaded %s header' % h)
|
||||
HEADERS_SEEN.add(u_h)
|
||||
return
|
||||
|
||||
@@ -87,7 +87,7 @@ class XSSCharFinder(object):
|
||||
item['error'] = 'Payload delims do not surround this injection point. Found via search for entire payload.'
|
||||
self.write_to_file(item, spider)
|
||||
|
||||
raise DropItem('No XSS vulns in %s. type = %s, %s, %s'% (resp_url, meta['xss_place'], meta['xss_param'], meta['payload']))
|
||||
raise DropItem('No XSS vulns in %s. type = %s, %s' % (resp_url, meta['xss_place'], meta['xss_param']))
|
||||
|
||||
def xss_logic(self, injection, meta, resp_url, error):
|
||||
''' XSS logic. Returns None if vulnerability not found
|
||||
@@ -249,9 +249,8 @@ class XSSCharFinder(object):
|
||||
if tag in ['script', 'frame', 'iframe']:
|
||||
breakout_chars.append(set([':', ')', '(']))
|
||||
else:
|
||||
print '\n\n\n\n\n NO QUOTeS OPEN BUT FOUND INJECTION IN ATTRIBUTE TAG', tag, attr, attr_val, delim, payload, '\n\n\n\n\n'
|
||||
# Hail mary, no quotes found but definitely inside attr
|
||||
breakout_chars.append(set(['"', "'", ">", "<"]))
|
||||
#No quotes found but definitely inside attr
|
||||
breakout_chars.append(set([">", "<"]))
|
||||
|
||||
return breakout_chars
|
||||
|
||||
|
||||
@@ -35,5 +35,5 @@ ITEM_PIPELINES = {'xsscrapy.pipelines.XSSCharFinder':100}
|
||||
#FEED_FORMAT = 'csv'
|
||||
#FEED_URI = 'vulnerable-urls.txt'
|
||||
|
||||
CONCURRENT_REQUESTS = 25
|
||||
CONCURRENT_REQUESTS = 20
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import requests
|
||||
import string
|
||||
import random
|
||||
|
||||
from IPython import embed
|
||||
|
||||
__author__ = 'Dan McInerney danhmcinerney@gmail.com'
|
||||
|
||||
'''
|
||||
@@ -53,7 +55,12 @@ class XSSspider(CrawlSpider):
|
||||
self.test_str = '\'"(){}<x>:'
|
||||
|
||||
self.login_user = kwargs.get('user')
|
||||
if self.login_user == 'None':
|
||||
self.login_user = None
|
||||
self.login_pass = kwargs.get('pw')
|
||||
if self.login_pass == 'None':
|
||||
self.login_pass = None
|
||||
print ' login', self.login_user, 'passw', self.login_pass
|
||||
|
||||
def parse_start_url(self, response):
|
||||
''' Creates the XSS tester requests for the start URL as well as the request for robots.txt '''
|
||||
@@ -184,6 +191,21 @@ class XSSspider(CrawlSpider):
|
||||
|
||||
return payloads
|
||||
|
||||
def url_valid(self, url, orig_url):
|
||||
# Make sure there's a form action url
|
||||
if url == None:
|
||||
self.log('No form action URL found')
|
||||
return
|
||||
|
||||
# Sometimes lxml doesn't read the form.action right
|
||||
if '://' not in url:
|
||||
self.log('Form URL contains no scheme, attempting to put together a working form submissions URL')
|
||||
proc_url = self.url_processor(orig_url)
|
||||
url = proc_url[1]+proc_url[0]+url
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def check_form_validity(self, values, url, payload, orig_url):
|
||||
''' Make sure the form action url and values are valid/exist '''
|
||||
|
||||
@@ -304,9 +326,7 @@ class XSSspider(CrawlSpider):
|
||||
return iframe_reqs
|
||||
|
||||
def make_form_reqs(self, orig_url, forms, payload):
|
||||
''' Logic: Get forms, find injectable input values, confirm at least one value has been injected,
|
||||
confirm that value + url + POST/GET has not been made into a request before, finally send the request
|
||||
Note: if you see lots and lots of errors when POSTing, it is probably because of captchas'''
|
||||
''' Payload each form input in each input's own request '''
|
||||
reqs = []
|
||||
vals_urls_meths = []
|
||||
|
||||
@@ -315,67 +335,45 @@ class XSSspider(CrawlSpider):
|
||||
payload = delim_str + payload + delim_str + ';9'
|
||||
|
||||
for form in forms:
|
||||
#payloads = self.encode_payloads(new_payloads, form.method)
|
||||
values, url, method = self.fill_form(orig_url, form, payload)
|
||||
url = self.check_form_validity(values, url, payload, orig_url)
|
||||
if not url:
|
||||
continue
|
||||
# Get form field names
|
||||
form_fields = ', '.join([f for f in form.fields])
|
||||
|
||||
# POST to both the orig url and the specified form action='http://url.com' url
|
||||
# Just to prevent false negatives
|
||||
if url != orig_url:
|
||||
urls = [url, orig_url]
|
||||
else:
|
||||
urls = [url]
|
||||
|
||||
# Make the payloaded requests
|
||||
req = [FormRequest(url,
|
||||
formdata=values,
|
||||
method=method,
|
||||
meta={'payload':payload,
|
||||
'xss_param':form_fields,
|
||||
'orig_url':orig_url,
|
||||
'forms':forms,
|
||||
'xss_place':'form',
|
||||
'POST_to':url,
|
||||
'values':values,
|
||||
'delim':delim_str},
|
||||
dont_filter=True,
|
||||
callback=self.xss_chars_finder)
|
||||
for url in urls]
|
||||
|
||||
reqs += req
|
||||
if form.inputs:
|
||||
method = form.method
|
||||
url = form.action or form.base_url
|
||||
if self.url_valid(url, orig_url) and method:
|
||||
for i in form.inputs:
|
||||
if i.name:
|
||||
#value = self.fill_form(orig_url, i, payload)
|
||||
if type(i).__name__ not in ['InputElement', 'TextareaElement']:
|
||||
continue
|
||||
if type(i).__name__ == 'InputElement':
|
||||
# Don't change values for the below types because they
|
||||
# won't be strings and lxml will complain
|
||||
nonstrings = ['checkbox', 'radio', 'submit']
|
||||
if i.type in nonstrings:
|
||||
continue
|
||||
orig_val = form.fields[i.name]
|
||||
if orig_val == None:
|
||||
orig_val = ''
|
||||
form.fields[i.name] = payload
|
||||
xss_param = i.name
|
||||
values = form.form_values()
|
||||
req = FormRequest(url,
|
||||
formdata=values,
|
||||
method=method,
|
||||
meta={'payload':payload,
|
||||
'xss_param':xss_param,
|
||||
'orig_url':orig_url,
|
||||
'xss_place':'form',
|
||||
'POST_to':url,
|
||||
'delim':delim_str},
|
||||
dont_filter=True,
|
||||
callback=self.xss_chars_finder)
|
||||
reqs.append(req)
|
||||
# Reset the value
|
||||
form.fields[i.name] = orig_val
|
||||
|
||||
if len(reqs) > 0:
|
||||
return reqs
|
||||
|
||||
def make_form_payloads(self, response):
|
||||
''' Create the payloads based on the injection points from the first test request'''
|
||||
orig_url = response.meta['orig_url']
|
||||
payload = response.meta['payload']
|
||||
#quote_enclosure = response.meta['quote']
|
||||
xss_place = response.meta['xss_place']
|
||||
forms = response.meta['forms']
|
||||
delim = response.meta['delim']
|
||||
body = response.body
|
||||
resp_url = response.url
|
||||
try:
|
||||
doc = lxml.html.fromstring(body)
|
||||
except lxml.etree.XMLSyntaxError:
|
||||
self.log('XML Syntax Error on %s' % resp_url)
|
||||
return
|
||||
|
||||
injections = self.xss_params(payload, doc)
|
||||
if injections:
|
||||
payloads = self.xss_str_generator(injections, delim)
|
||||
if payloads:
|
||||
form_reqs = self.make_form_reqs(orig_url, forms, payloads, injections)
|
||||
if form_reqs:
|
||||
return form_reqs
|
||||
return
|
||||
|
||||
def make_cookie_reqs(self, url, payload, xss_param):
|
||||
''' Generate payloaded cookie header requests '''
|
||||
|
||||
|
||||
Reference in New Issue
Block a user