Advertisement
infodox

Damn Small XSS scanner

Nov 30th, 2011
379
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 7.13 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. import cookielib, optparse, random, re, string, urllib2, urlparse
  4.  
  5. NAME    = "Damn Small XSS Scanner (DSXS) < 100 LOC (Lines of Code)"
  6. VERSION = "0.1f"
  7. AUTHOR  = "Miroslav Stampar (http://unconciousmind.blogspot.com | @stamparm)"
  8. LICENSE = "Public domain (FREE)"
  9.  
  10. SMALLER_CHAR_POOL    = ('<', '>')                               # characters used for XSS tampering of parameter values (smaller set - for avoiding possible SQLi errors)
  11. LARGER_CHAR_POOL     = ('\'', '"', '>', '<')                    # characters used for XSS tampering of parameter values (larger set)
  12. GET, POST            = "GET", "POST"                            # enumerator-like values used for marking current phase
  13. PREFIX_SUFFIX_LENGTH = 5                                        # length of random prefix/suffix used in XSS tampering
  14. CONTEXT_DISPLAY_OFFSET = 10                                     # offset outside the affected context for displaying in vulnerability report
  15. COOKIE, UA, REFERER = "Cookie", "User-Agent", "Referer"         # optional HTTP header names
  16. REGEX_SPECIAL_CHARS = ('\\', '*', '.', '+', '[', ']', ')', '(') # characters reserved for regular expressions
  17.  
  18. XSS_PATTERNS = (                                                # each (pattern) item consists of ((context regex), (prerequisite unfiltered characters), "info text")
  19.     (r'\A[^<>]*%(chars)s[^<>]*\Z', ('<', '>'), "\"...\", pure text response, %(filtering)s filtering"),
  20.     (r"<script[^>]*>(?!.*<script).*'[^>']*%(chars)s[^>']*'.*</script>", ('\''), "\"<script>.'...'.</script>\", enclosed by script tags, inside single-quotes, %(filtering)s filtering"),
  21.     (r'<script[^>]*>(?!.*<script).*"[^>"]*%(chars)s[^>"]*".*</script>', ('"'), "'<script>.\"...\".</script>', enclosed by script tags, inside double-quotes, %(filtering)s filtering"),
  22.     (r'<script[^>]*>(?!.*<script).*?%(chars)s.*?</script>', (), "\"<script>...</script>\", enclosed by script tags, %s"),
  23.     (r'>[^<]*%(chars)s[^<]*(<|\Z)', ('<', '>'), "\">...<\", outside tags, %(filtering)s filtering"),
  24.     (r"<[^>]*'[^>']*%(chars)s[^>']*'[^>]*>", ('\'',), "\"<.'...'.>\", inside tag, inside single-quotes, %(filtering)s filtering"),
  25.     (r'<[^>]*"[^>"]*%(chars)s[^>"]*"[^>]*>', ('"',), "'<.\"...\".>', inside tag, inside double-quotes, %(filtering)s filtering"),
  26.     (r'<[^>]*%(chars)s[^>]*>', (), "\"<...>\", inside tag, %(filtering)s filtering")
  27. )
  28.  
  29. USER_AGENTS = (                                                 # items used for picking random HTTP User-Agent header value
  30.     "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_0; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.678.0 Safari/534.21",
  31.     "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
  32.     "Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:0.9.2) Gecko/20020508 Netscape6/6.1",
  33.     "Mozilla/5.0 (X11;U; Linux i686; en-GB; rv:1.9.1) Gecko/20090624 Ubuntu/9.04 (jaunty) Firefox/3.5",
  34.     "Opera/9.80 (X11; U; Linux i686; en-US; rv:1.9.2.3) Presto/2.2.15 Version/10.10"
  35. )
  36.  
  37. _headers = {}                                                   # used for storing dictionary with optional header values
  38.  
  39. def retrieve_content(url, data=None):
  40.     try:
  41.         req = urllib2.Request("".join(url[i].replace(' ', '%20') if i > url.find('?') else url[i] for i in xrange(len(url))), data, _headers)
  42.         retval = urllib2.urlopen(req).read()
  43.     except Exception, ex:
  44.         retval = ex.read() if hasattr(ex, "read") else getattr(ex, "msg", str())
  45.     return retval or ""
  46.  
  47. def scan_page(url, data=None):
  48.     def _contains(content, chars):
  49.         content = re.sub(r"\\[%s]" % "".join(chars), "", content, re.S) if chars else content
  50.         return all(char in content for char in chars)
  51.     retval, usable = False, False
  52.     try:
  53.         for phase in (GET, POST):
  54.             current = url if phase is GET else (data or "")
  55.             for match in re.finditer(r"((\A|[?&])(?P<parameter>[\w\[\]]+)=)(?P<value>[^&]+)", current):
  56.                 found, usable = False, True
  57.                 print "* scanning %s parameter '%s'" % (phase, match.group("parameter"))
  58.                 prefix, suffix = ["".join(random.sample(string.ascii_lowercase, PREFIX_SUFFIX_LENGTH)) for i in xrange(2)]
  59.                 for pool in (LARGER_CHAR_POOL, SMALLER_CHAR_POOL):
  60.                     if not found:
  61.                         tampered = current.replace(match.group(0), "%s%s%s%s" % (match.group(1), prefix, "".join(random.sample(pool, len(pool))), suffix))
  62.                         content = retrieve_content(tampered, data) if phase is GET else retrieve_content(url, tampered)
  63.                         for sample in re.finditer("%s(.+?)%s" % (prefix, suffix), content, re.I|re.S):
  64.                             for regex, condition, info in XSS_PATTERNS:
  65.                                 context = re.search(regex % dict((("chars",reduce(lambda filtered, char: filtered.replace(char, "\\%s" % char), REGEX_SPECIAL_CHARS, sample.group(0))),)), content, re.I|re.S)
  66.                                 if context and not found and sample.group(1).strip():
  67.                                     if _contains(sample.group(1), condition):
  68.                                         print " (i) %s parameter '%s' appears to be XSS vulnerable (%s)" % (phase, match.group("parameter"), info % dict((("filtering", "no" if all(char in sample.group(1) for char in LARGER_CHAR_POOL) else "some"),)))
  69.                                         found = retval = True
  70.                                     break
  71.         if not usable:
  72.             print " (x) no usable GET/POST parameters found"
  73.     except KeyboardInterrupt:
  74.         print "\r (x) Ctrl-C pressed"
  75.     return retval
  76.  
  77. def init_options(proxy=None, cookie=None, ua=None, referer=None):
  78.     if proxy:
  79.         urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler({'http': proxy})))
  80.     _headers.update(dict(filter(lambda item: item[1], [(COOKIE, cookie), (UA, ua), (REFERER, referer)])))
  81.  
  82. if __name__ == "__main__":
  83.     print "%s #v%s\n by: %s\n" % (NAME, VERSION, AUTHOR)
  84.     parser = optparse.OptionParser(version=VERSION)
  85.     parser.add_option("-u", "--url", dest="url", help="Target URL (e.g. \"http://www.target.com/page.htm?id=1\")")
  86.     parser.add_option("--data", dest="data", help="POST data (e.g. \"query=test\")")
  87.     parser.add_option("--cookie", dest="cookie", help="HTTP Cookie header value")
  88.     parser.add_option("--user-agent", dest="ua", help="HTTP User-Agent header value")
  89.     parser.add_option("--random-agent", dest="randomAgent", action="store_true", help="Use randomly selected HTTP User-Agent header value")
  90.     parser.add_option("--referer", dest="referer", help="HTTP Referer header value")
  91.     parser.add_option("--proxy", dest="proxy", help="HTTP proxy address (e.g. \"http://127.0.0.1:8080\")")
  92.     options, _ = parser.parse_args()
  93.     if options.url:
  94.         init_options(options.proxy, options.cookie, options.ua if not options.randomAgent else random.choice(USER_AGENTS), options.referer)
  95.         result = scan_page(options.url if options.url.startswith("http") else "http://%s" % options.url, options.data)
  96.         print "\nscan results: %s vulnerabilities found" % ("possible" if result else "no")
  97.     else:
  98.         parser.print_help()
  99.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement