摘要:在本文中,我们将把XSS检测能力提升到新的层次,着手开发用于发现存储型XSS(Stored XSS)的自动化扫描模块。与需要诱骗用户点击链接的反射型XSS不同,存储型XSS的恶意载荷被永久保存在服务器数据库中,能对所有访问受感染页面的用户发起无差别攻击,危害性极大。我们将设计一个“两阶段”的自动化扫描流程:首先,利用一个基础爬虫来发现网站上的所有表单(如评论区、留言板),然后向这些表单的每一个输入字段提交一个带有唯一标识的XSS Payload;接着,再次爬取整个网站,检查我们之前提交的Payload是否出现在任何一个页面的HTML源码中。

关键词:Python, XSS, 存储型XSS, 漏洞扫描, Web安全, 爬虫, 自动化测试


正文

⚠️ 警告:高风险主动注入,务必授权使用

存储型XSS的检测,意味着你的工具会向目标数据库写入测试数据。这是一种侵入性更强的测试行为。你必须、也只能在你拥有完全所有权或已获得明确书-面授权的测试环境(如DVWA的留言板功能)中使用它

1. 存储型XSS:潜伏的“水雷”

存储型XSS(也称持久型XSS)被认为是危害最严重的XSS类型。

攻击流程

  1. 注入阶段:攻击者在一个允许用户输入并会存入数据库的地方(如文章评论、用户个人资料、商品评价),提交一段恶意的JavaScript代码。

  2. 存储阶段:Web应用后端未对输入进行充分过滤,将这段恶意代码原封不动地存入了数据库。

  3. 触发阶段:当任何一个普通用户(包括管理员)访问包含这条“带毒”数据的页面时,服务器会从数据库中读出恶意代码,并将其作为正常内容嵌入到HTML页面中。

  4. 执行阶段:用户的浏览器在渲染页面时,执行了这段恶意脚本,导致用户的Cookie被窃取、账户被劫持等。

由于其“一次注入,持续伤害”的特性,一个成功的存储型XSS可以造成大规模的用户信息泄露。

2. “播种与收获”:检测算法设计

检测存储型XSS,就像一个农夫在测试一片土地的肥力。我们不能只看一眼,而是需要一个完整的“播种-生长-收获”的周期。

我们的两阶段算法

  • 阶段一:播种(Submission Phase)

    1. 发现表单:使用一个简单的爬虫遍历目标站点,找到所有<form>标签。

    2. 识别输入:解析每个表单,找出其中所有可供输入的字段(<input>, <textarea>等)。

    3. 注入唯一Payload:为每个输入字段,都生成一个独一无二的XSS探测Payload。这个唯一性至关重要,它能让我们在后续阶段准确地追溯到是哪个表单的哪个字段存在漏洞。

      • Payload示例<xss_test_tag id="form1_field_username">

    4. 提交表单:模拟用户填写并提交这个“带毒”的表单。

  • 阶段二:收获(Verification Phase)

    1. 再次爬取:在完成所有表单的注入后,再次启动爬虫,遍历网站的所有页面。

    2. 检查“果实”:在每个爬取到的页面HTML源码中,搜索我们之前“播种”下去的唯一Payloads(例如,搜索是否存在<xss_test_tag)。

    3. 溯源与报告:如果在一个页面上发现了我们注入的Payload id="form1_field_username",我们就可以确认:

      • 存在存储型XSS漏洞

      • 注入点:是第一个表单的username字段。

      • 触发点:是当前被爬取到的这个页面。

3. 完整代码实现 (stored_xss_scanner.py)

这个实现会比之前的扫描器复杂,因为它需要管理状态(已提交的Payloads)和一个多阶段的工作流。

环境准备

Bash

pip install requests beautifulsoup4

Python

# stored_xss_scanner.py
import requests
from bs4 import BeautifulSoup
import argparse
import time
from urllib.parse import urljoin, urlparse
from collections import deque

class StoredXSSScanner:
    def __init__(self, start_url):
        self.start_url = start_url
        self.target_domain = urlparse(start_url).netloc
        self.session = requests.Session()
        self.submitted_payloads = {} # {payload_id: {"form_url": ..., "form_inputs": ...}}
        self.urls_to_visit = deque([start_url])
        self.visited_urls = set()

    def discover_forms(self):
        """爬取网站并发现所有表单。"""
        print("[*] 阶段一: 发现并提交表单...")
        forms_found = []
        
        while self.urls_to_visit:
            current_url = self.urls_to_visit.popleft()
            if current_url in self.visited_urls:
                continue
            
            self.visited_urls.add(current_url)
            print(f"  - 正在爬取: {current_url}")
            
            try:
                response = self.session.get(current_url)
                soup = BeautifulSoup(response.content, 'html.parser')

                # 发现新链接
                for link in soup.find_all('a', href=True):
                    full_url = urljoin(current_url, link['href'])
                    if urlparse(full_url).netloc == self.target_domain and full_url not in self.visited_urls:
                        self.urls_to_visit.append(full_url)
                
                # 发现表单并注入Payload
                for form in soup.find_all('form'):
                    self.submit_form_with_payload(current_url, form)
                    
            except requests.RequestException:
                continue

    def submit_form_with_payload(self, form_url, form):
        action = form.attrs.get('action', form_url)
        action_url = urljoin(form_url, action)
        method = form.attrs.get('method', 'get').lower()
        
        inputs = form.find_all(['input', 'textarea', 'select'])
        form_data = {}
        payload_id = f"xss_test_{int(time.time()*1000)}"
        payload = f"<xss_test_tag id='{payload_id}'>"

        has_text_input = False
        for i in inputs:
            name = i.attrs.get('name')
            input_type = i.attrs.get('type', 'text')
            value = i.attrs.get('value', '')
            if name:
                if input_type in ['text', 'textarea', 'search', 'email', 'url']:
                    form_data[name] = payload
                    has_text_input = True
                else:
                    form_data[name] = value
        
        if not has_text_input:
            return

        print(f"    - 发现表单在 {form_url},正在注入Payload ID: {payload_id}")
        try:
            if method == 'post':
                self.session.post(action_url, data=form_data)
            else:
                self.session.get(action_url, params=form_data)
            
            # 记录已成功提交的Payload
            self.submitted_payloads[payload_id] = {"form_url": form_url, "inputs": list(form_data.keys())}
        except requests.RequestException:
            pass

    def verify_payloads(self):
        """再次爬取网站,验证Payload是否被存储和反射。"""
        print("\n[*] 阶段二: 验证Payload是否在页面中出现...")
        
        # 清空爬虫队列,从头开始爬
        self.urls_to_visit = deque([self.start_url])
        self.visited_urls = set()
        
        while self.urls_to_visit:
            current_url = self.urls_to_visit.popleft()
            if current_url in self.visited_urls:
                continue
            self.visited_urls.add(current_url)
            
            try:
                response = self.session.get(current_url)
                
                # 在页面内容中检查所有已提交的Payload
                for payload_id, details in self.submitted_payloads.items():
                    if f"id='{payload_id}'" in response.text:
                        print("\n[+] 高危: 发现存储型XSS漏洞!")
                        print(f"    - 注入点 (表单所在页面): {details['form_url']}")
                        print(f"    - 注入的表单字段 (可能): {details['inputs']}")
                        print(f"    - 触发点 (Payload反射页面): {current_url}")
                        # 我们可以选择删除已发现的Payload,避免重复报告
                        # self.submitted_payloads.pop(payload_id)
                        # return # 发现一个后即可停止,或继续寻找
                
                # 发现新链接(在验证阶段也需要爬取)
                soup = BeautifulSoup(response.content, 'html.parser')
                for link in soup.find_all('a', href=True):
                    full_url = urljoin(current_url, link['href'])
                    if urlparse(full_url).netloc == self.target_domain and full_url not in self.visited_urls:
                        self.urls_to_visit.append(full_url)
                        
            except requests.RequestException:
                continue

    def run(self):
        self.discover_forms()
        self.verify_payloads()
        print("\n[*] 存储型XSS扫描完成。")

def main():
    parser = argparse.ArgumentParser(description="一个用于检测存储型XSS的自动化扫描器。")
    parser.add_argument("url", help="起始URL。")
    args = parser.parse_args()
    scanner = StoredXSSScanner(args.url)
    scanner.run()

if __name__ == "__main__":
    main()

总结

我们成功地构建了一个能够自动化探测存储型XSS的扫描器。它通过“注入-爬取-验证”的三步走策略,模拟了高级渗透测试人员的手工测试流程,能够发现比反射型XSS更隐蔽、危害更大的安全漏洞。

这个引擎的设计思想——通过爬虫全面探索应用,对所有输入点进行系统性测试,并通过再次爬取来验证异步或延迟的漏洞效果——是所有专业级Web应用扫描器的核心工作流。

我们的XSS检测引擎目前还停留在分析静态HTML的层面。然而,现代Web应用的大量逻辑都在客户端的JavaScript中执行,这催生了一种新的、更难检测的XSS类型。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐