now sends 1 request per form input

This commit is contained in:
Dan McInerney
2014-09-08 05:10:10 -04:00
parent 85907da7be
commit 7397ad8f7d
6 changed files with 106 additions and 92 deletions

View File

@@ -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
View 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()

View File

@@ -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

View File

@@ -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

View File

@@ -35,5 +35,5 @@ ITEM_PIPELINES = {'xsscrapy.pipelines.XSSCharFinder':100}
#FEED_FORMAT = 'csv'
#FEED_URI = 'vulnerable-urls.txt'
CONCURRENT_REQUESTS = 25
CONCURRENT_REQUESTS = 20

View File

@@ -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 '''