Python实战:从零构建Web漏洞扫描器,实现SQL注入自动化检测

Python实战:从零构建Web漏洞扫描器,实现SQL注入自动化检测 1. 项目概述从零构建一个“会思考”的漏洞扫描器每次看到新闻里某某网站因为SQL注入漏洞导致数据泄露作为开发者心里总会咯噔一下。我们写的代码真的安全吗那些看似简单的表单背后是不是也藏着能被轻易攻破的入口与其被动等待不如主动出击。今天我们就来动手实现一个最基础的Web漏洞扫描器它不只是一个工具更是理解Web安全攻防逻辑的绝佳实践。这个项目将分步进行本篇聚焦于两大核心模块网站爬虫和SQL注入检测。简单来说我们要做的扫描器其工作流程就像一个智能的“安全巡检员”。首先它会像搜索引擎的蜘蛛一样自动浏览目标网站收集所有可能的用户输入点比如登录框、搜索栏、商品ID参数。然后它会模拟攻击者的手法向这些输入点发送精心构造的测试数据分析服务器的响应从而判断是否存在SQL注入漏洞。整个过程完全由Python驱动代码清晰逻辑直接非常适合想要深入理解Web安全原理、或为自动化安全测试打下基础的开发者。无论你是刚学完Python语法的新手还是有一定经验想拓展安全视野的开发者这个项目都能让你获益匪浅。你将不再只是漏洞的“听闻者”而是成为其“发现者”和“理解者”。我们将从最基础的HTTP请求开始一步步搭建起这个扫描器的骨架。2. 核心思路与技术选型为什么是“爬虫”“基于响应的检测”在动手写代码之前我们必须想清楚两件事扫描器如何发现目标以及如何判断漏洞存在。这直接决定了我们的技术方案。2.1 网站爬虫扫描器的“眼睛”与“地图”一个盲目的扫描器是无效的。我们必须先为它绘制一张目标网站的“地图”这张地图上需要标注出所有可能接受用户输入的“城门”也就是潜在的漏洞点。这些点通常对应着HTML中的表单form和带有查询参数的链接a href”?id1″。为什么不直接用一个现成的爬虫库比如Scrapy对于我们的学习目的和这个基础扫描器而言Scrapy显得过于重型了。它的学习曲线较陡且其强大的异步架构和中间件机制对于初识爬虫和安全测试的开发者来说可能会分散我们对核心安全逻辑的注意力。我们的需求很明确提取链接、发现表单。因此我选择使用轻量级的requests库来发送HTTP请求配合BeautifulSoup库来解析HTML文档。这个组合足够灵活、直观能让我们清晰地掌控每一个步骤从发送请求到解析标签再到数据处理整个过程一目了然。注意在实际操作中我们必须严格遵守robots.txt协议并且控制请求速率避免对目标服务器造成压力。本项目仅用于授权的安全测试和学习环境如DVWA、Pikachu靶场严禁对未授权的任何网站进行扫描。2.2 SQL注入检测基于响应差异的“逻辑探针”检测SQL注入的方法有很多比如基于报错的、基于布尔的、基于时间的盲注等。对于入门级的自动化扫描器基于响应内容的差异检测布尔盲注的一种简化应用是一个在复杂度和效果上取得很好平衡的方案。它的核心思想非常巧妙我们向目标参数提交两段精心构造的Payload。触发真条件提交一个让原始SQL语句逻辑恒成立的Payload例如在数字型参数后附加OR 11。触发假条件提交一个让原始SQL语句逻辑恒不成立的Payload例如附加AND 12。如果目标存在SQL注入漏洞那么服务器处理这两条不同的SQL语句后返回的页面内容通常会有显著差异比如一个返回了正常数据一个没有。如果目标不存在漏洞或者进行了有效的过滤那么这两次请求返回的页面内容应该基本一致因为注入的语句未被成功执行。这种方法的好处是原理简单直观不需要解析复杂的数据库错误信息。误报率相对较低通过对比两次响应比单纯匹配某个错误字符串更可靠。易于实现我们只需要比较两个HTTP响应体的相似度即可。我们将使用difflib库中的SequenceMatcher来计算两个响应内容的相似度比率从而做出判断。3. 环境准备与核心库安装工欲善其事必先利其器。我们首先需要搭建Python开发环境并安装必要的库。我强烈建议使用virtualenv或conda创建一个独立的虚拟环境以避免不同项目间的库版本冲突。# 1. 创建并激活虚拟环境 (以 virtualenv 为例) # 如果你还没有安装 virtualenv可以先运行: pip install virtualenv virtualenv venv_scanner # Windows 激活 venv_scanner\Scripts\activate # Linux/Mac 激活 source venv_scanner/bin/activate # 2. 安装核心库 pip install requests beautifulsoup4 # requests: 用于发送HTTP请求 # beautifulsoup4: 用于解析HTML提取链接和表单除了这两个核心库我们可能还会用到Python标准库中的urllib.parse用于URL拼接与解析和difflib用于内容比较它们无需额外安装。关于开发工具你可以使用任何你喜欢的文本编辑器或IDE例如 VS Code、PyCharm 甚至 Jupyter Notebook。确保你的Python版本在3.6以上即可。4. 核心模块一轻量级定向网站爬虫实现我们的爬虫不需要爬取全网它应该是一个“定向爬虫”专注于为安全扫描服务。它的核心任务是从给定的起始URL开始收集本站点内的所有链接并从中找出所有包含表单的页面以及所有带有查询参数的URL。4.1 网页请求与HTML解析这是爬虫的基石。我们使用requests.get()来获取页面内容并用BeautifulSoup进行解析。import requests from bs4 import BeautifulSoup from urllib.parse import urljoin, urlparse def fetch_page(url): 获取指定URL的页面内容。 Args: url (str): 目标网址 Returns: str: 页面的HTML文本如果请求失败则返回None headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } try: # 设置一个合理的超时时间避免长时间等待 response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 # 通常我们关心的是文本内容用于解析链接和表单 return response.text except requests.exceptions.RequestException as e: print(f[-] 请求 {url} 失败: {e}) return None def parse_links(html, base_url): 从HTML中解析出所有属于同一站点的链接。 Args: html (str): HTML文本 base_url (str): 当前页面的基础URL用于将相对路径转换为绝对路径 Returns: set: 一个去重后的绝对URL集合 if not html: return set() soup BeautifulSoup(html, html.parser) found_links set() for tag in soup.find_all([a, link], hrefTrue): href tag[href] # 拼接完整的URL full_url urljoin(base_url, href) # 解析URL检查是否属于同一网域简化处理相同主机名 parsed_full urlparse(full_url) parsed_base urlparse(base_url) # 只收集同一主机名的链接并且过滤掉javascript:等非HTTP链接 if parsed_full.netloc parsed_base.netloc and parsed_full.scheme in (http, https): # 去掉URL中的片段标识符#后面的部分 full_url full_url.split(#)[0] if full_url: found_links.add(full_url) return found_links4.2 表单与URL参数提取这是安全爬虫的关键。我们需要从页面中提取出所有form元素分析其提交方法GET/POST和输入字段input,select,textarea。同时也要从已有的链接中提取出查询参数如?id1nameadmin。def extract_forms(html, url): 从HTML中提取所有表单信息。 Args: html (str): HTML文本 url (str): 表单所在页面的URL Returns: list: 表单字典列表。每个字典包含表单的action、method和inputs列表。 forms [] soup BeautifulSoup(html, html.parser) for form in soup.find_all(form): form_info {} # 获取表单提交地址如果没有则使用当前页面URL action form.get(action) form_info[action] urljoin(url, action) if action else url # 获取表单提交方法默认是GET form_info[method] form.get(method, get).lower() # 获取所有输入字段 form_info[inputs] [] for input_tag in form.find_all([input, textarea, select]): input_info {type: input_tag.name, name: None, value: } if input_tag.get(name): input_info[name] input_tag.get(name) # 对于input标签获取type和value if input_tag.name input: input_info[type] input_tag.get(type, text) input_info[value] input_tag.get(value, ) # 对于select和textarea暂时简单处理实际可扩展 forms.append(form_info) return forms def extract_urls_with_params(links): 从链接集合中筛选出带有查询参数的URL并解析其参数。 Args: links (set): 链接集合 Returns: list: 参数化URL字典列表。每个字典包含url和params字典。 param_urls [] for link in links: parsed urlparse(link) if parsed.query: # 如果URL有查询字符串 params {} # 解析查询字符串如 ?id1nametest from urllib.parse import parse_qs query_params parse_qs(parsed.query) for key, value in query_params.items(): # parse_qs返回列表取第一个值作为测试用 params[key] value[0] if value else param_urls.append({ url: link, params: params, method: GET # 来自URL的参数通常是GET请求 }) return param_urls4.3 爬虫调度与去重控制我们需要一个调度器来管理待爬取的URL队列并避免重复爬取和陷入死循环。class SimpleScannerCrawler: def __init__(self, start_url, max_pages50): self.start_url start_url self.max_pages max_pages # 防止爬取过多页面 self.to_crawl [start_url] self.crawled set() self.all_links set([start_url]) self.target_forms [] # 收集到的所有表单 self.target_param_urls [] # 收集到的所有带参数的URL def crawl(self): 主爬取循环 while self.to_crawl and len(self.crawled) self.max_pages: current_url self.to_crawl.pop(0) if current_url in self.crawled: continue print(f[*] 正在爬取: {current_url}) html fetch_page(current_url) if html: self.crawled.add(current_url) # 1. 解析并收集新链接 new_links parse_links(html, current_url) for link in new_links: if link not in self.all_links and link not in self.crawled: self.all_links.add(link) self.to_crawl.append(link) # 2. 提取表单 forms extract_forms(html, current_url) self.target_forms.extend(forms) # 3. 从当前页面的链接中提取带参数的URL (注意去重) param_urls_from_page extract_urls_with_params(new_links) for pu in param_urls_from_page: # 简单的基于URL字符串的去重 if pu[url] not in [p[url] for p in self.target_param_urls]: self.target_param_urls.append(pu) print(f[] 爬取结束。共爬取 {len(self.crawled)} 个页面。) print(f[] 发现 {len(self.target_forms)} 个表单。) print(f[] 发现 {len(self.target_param_urls)} 个带参数的URL。) return self.target_forms, self.target_param_urls实操心得在实际测试中你会发现很多网站有动态加载的内容通过JavaScript我们的简单爬虫无法抓取到这些内容。这对于安全测试来说是一个重要的遗漏点。更高级的爬虫会集成无头浏览器如Selenium、Playwright但这会大大增加复杂度和资源消耗。对于入门项目我们暂时聚焦于静态内容这已经能覆盖很多传统网站和靶场如Pikachu、DVWA的漏洞点了。5. 核心模块二SQL注入漏洞检测引擎爬虫为我们找到了“攻击面”表单和参数化URL接下来就需要我们的检测引擎上场了。我们将实现前面提到的基于响应差异的检测方法。5.1 检测原理与Payload设计我们的检测逻辑围绕一个核心函数展开test_sql_injection。它会针对一个具体的输入点一个表单的某个字段或一个URL的某个参数进行测试。首先我们需要设计测试用的Payload。根据参数类型数字型、字符型Payload的构造方式不同。我们可以先尝试通用Payload然后根据响应情况判断类型。# 一些基础的测试Payload SQLI_PAYLOADS { # 通用逻辑测试Payload generic: [ OR 11, # 字符型-恒真 OR 12, # 字符型-恒假 OR 11, # 双引号字符型-恒真 OR 12, # 双引号字符型-恒假 OR 11, # 数字型-恒真 (注意前导空格有时需要) OR 12, # 数字型-恒假 AND 11, AND 12, ], # 更具体的测试可以后续扩展 # error_based: [, \, ), ...], # time_based: [ OR SLEEP(5)--, ...], }5.2 响应差异度计算与漏洞判定发送Payload后我们需要比较原始响应提交正常数据如空值或默认值与注入Payload后响应的差异。这里使用difflib.SequenceMatcher来计算相似度。import difflib def calculate_similarity(seq1, seq2): 计算两个字符串序列的相似度比率。 Args: seq1 (str): 字符串1 seq2 (str): 字符串2 Returns: float: 相似度0.0到1.0之间 return difflib.SequenceMatcher(None, seq1, seq2).ratio() def test_sql_injection(url, params, methodGET, dataNone): 测试一个特定的端点是否存在SQL注入漏洞。 Args: url (str): 目标URL params (dict): 要测试的参数键值对。对于GET是查询参数对于POST是表单数据。 method (str): GET 或 POST data (dict): 当method为POST时额外的表单数据可选。 Returns: bool: 是否疑似存在漏洞 str: 测试详情 # 1. 获取原始响应使用正常或空参数 original_response send_request(url, params, method, data) if not original_response: return False, 无法获取原始响应 # 2. 对每个参数进行测试 for param_name, original_value in params.items(): for payload in SQLI_PAYLOADS[generic]: # 构造测试参数将Payload附加到原始值后或直接替换根据情况 # 这里采用附加的方式适用于大多数情况 test_params params.copy() test_params[param_name] original_value payload test_response send_request(url, test_params, method, data) if not test_response: continue # 3. 计算相似度 similarity calculate_similarity(original_response, test_response) # 设置一个阈值比如相似度低于0.9即差异超过10%则认为可能存在问题 # 这个阈值需要根据目标网站调整有些网站动态内容多基线相似度本身就低 if similarity 0.9: # 进一步确认发送一个“恒假”Payload看是否与“恒真”Payload响应不同 false_params params.copy() false_params[param_name] original_value get_false_payload(payload) false_response send_request(url, false_params, method, data) if false_response and calculate_similarity(original_response, false_response) 0.95: # 原始响应与“恒假”响应相似但与“恒真”响应不相似这是典型的注入迹象 return True, f疑似SQL注入漏洞参数: {param_name}, Payload: {payload}, 相似度: {similarity:.2f} return False, 未发现明显的SQL注入漏洞 def send_request(url, params, methodGET, dataNone): 封装请求发送支持GET和POST headers {User-Agent: Mozilla/5.0 ...} try: if method.upper() GET: response requests.get(url, paramsparams, headersheaders, timeout10) else: # POST # 注意有些表单可能有其他固定字段data参数可以传入这些固定值 post_data params if data is None else {**data, **params} response requests.post(url, datapost_data, headersheaders, timeout10) response.raise_for_status() return response.text except Exception as e: print(f[-] 请求发送失败: {e}) return None def get_false_payload(true_payload): 根据一个真Payload返回一个对应的假Payload简化逻辑 if 11 in true_payload: return true_payload.replace(11, 12) elif 11 in true_payload: return true_payload.replace(11, 12) elif 11 in true_payload: return true_payload.replace(11, 12) # 默认返回一个假Payload return AND 125.3 整合爬虫与检测引擎最后我们将两个模块串联起来形成一个完整的扫描流程。def main_scan(target_url): print(f[*] 开始对目标 {target_url} 进行扫描) print([*] 阶段一爬取网站结构...) crawler SimpleScannerCrawler(target_url, max_pages30) forms, param_urls crawler.crawl() print(\n[*] 阶段二开始SQL注入漏洞检测...) vulnerabilities [] # 1. 检测表单POST请求 for form in forms: print(f [] 检测表单: {form[action]} [Method: {form[method].upper()}]) # 构建测试数据将表单中所有输入字段的值设为测试字符串如test test_data {} for inp in form[inputs]: if inp.get(name): # 对于隐藏字段或已有值的字段可以保留原值这里简单置为test test_data[inp[name]] test # 根据表单方法调用检测函数 if form[method] post: is_vuln, detail test_sql_injection(form[action], test_data, methodPOST) else: # get form is_vuln, detail test_sql_injection(form[action], test_data, methodGET) if is_vuln: vulnerabilities.append((FORM, form[action], detail)) print(f [!] {detail}) # 2. 检测URL参数GET请求 for param_url_info in param_urls: url param_url_info[url] params param_url_info[params] print(f [] 检测URL参数: {url}) is_vuln, detail test_sql_injection(url, params, methodGET) if is_vuln: vulnerabilities.append((URL, url, detail)) print(f [!] {detail}) # 输出最终报告 print(\n *50) print([] 扫描完成) if vulnerabilities: print([!] 发现潜在漏洞) for vuln_type, target, info in vulnerabilities: print(f - 类型: {vuln_type}) print(f 目标: {target}) print(f 详情: {info}) print() else: print([] 未发现明显的SQL注入漏洞。) return vulnerabilities # 使用示例请在授权的靶场环境中测试例如本地搭建的DVWA if __name__ __main__: # 目标URL请替换为你的测试靶场地址 TARGET http://localhost/dvwa/vulnerabilities/sqli/ # 注意DVWA需要登录我们的简单扫描器未处理会话(cookie)所以直接测试会失败。 # 更完整的实现需要集成会话管理。这里仅为演示流程。 # main_scan(TARGET) print(请确保已配置好靶场环境并处理好会话后运行扫描。)6. 实战测试、常见问题与优化方向理论说得再多不如一次实战。让我们在一个受控环境中测试我们的扫描器。6.1 靶场环境搭建与测试我强烈建议使用Pikachu或DVWA (Damn Vulnerable Web Application)作为测试靶场。它们都是专门为Web安全学习设计的包含了SQL注入等各类漏洞。以Pikachu为例通常你只需要下载源码配置一个PHP运行环境如XAMPP、PHPStudy将源码放入Web目录访问安装页面即可。靶场启动后你可以访问http://localhost/pikachu/vul/sqli/sqli_str.php这样的页面进行手工注入练习这也是我们扫描器的绝佳测试目标。测试步骤确保你的靶场如Pikachu正常运行。修改上面main_scan函数中的TARGET变量为靶场中SQL注入模块的入口URL。运行脚本。由于我们的扫描器目前没有处理登录状态Session/Cookie对于需要登录的靶场如DVWA它会直接失败。对于Pikachu的部分无需登录的漏洞页面则可以尝试扫描。踩坑记录我第一次测试DVWA时扫描器毫无收获。排查后发现所有请求都返回了登录页面。这是因为DVWA使用了会话认证。我们的扫描器需要具备会话保持能力。解决方法是在requests中使用Session对象并在扫描开始前手动登录一次让Session自动管理Cookies。这是一个非常重要的补充功能。6.2 常见问题与排查技巧在开发和测试过程中你肯定会遇到各种问题。下面是一个速查表问题现象可能原因排查与解决思路爬虫卡住或报连接错误1. 目标网站屏蔽了请求。2. 网络问题或目标不可达。3. 爬取到了非HTTP链接如javascript:void(0)。1. 检查User-Agent头模拟更真实的浏览器。2. 增加try-except和超时设置跳过错误链接。3. 在parse_links函数中严格过滤URL协议。检测引擎误报率高1. 相似度阈值设置不合理。2. 网站页面本身动态内容多每次请求响应都有差异如包含时间戳、随机令牌。3. Payload触发了WAFWeb应用防火墙返回了特定拦截页面。1. 调整calculate_similarity的判定阈值如从0.9调到0.8。2. 尝试在计算相似度前使用正则表达式移除页面中动态变化的部分如时间戳、CSRF令牌。3. 观察“恒真”和“恒假”Payload的响应是否都不同于原始响应如果都不同可能是触发了其他机制而非注入成功。检测不到已知漏洞1. Payload被服务器转义或过滤。2. 漏洞是数字型但Payload以字符型构造。3. 漏洞点在HTTP头部如User-Agent、Cookie中而非我们检测的表单或URL参数。1. 尝试更多变种的Payload如大小写混淆、URL编码、注释符--、/* */等。2. 改进检测逻辑先尝试判断参数类型如提交一个单引号看是否报错。3. 扩展扫描器的检测范围将Cookie、HTTP头也纳入考虑。这是高级扫描器的功能。扫描速度非常慢对每个参数发送了多次HTTP请求。1. 优化Payload集合优先使用最有可能成功的Payload。2. 引入异步请求如aiohttp库来并发测试这是大幅提升速度的关键。6.3 项目优化与扩展思路我们这个基础扫描器只是一个起点它有很多可以强化和扩展的方向会话管理集成requests.Session()并实现自动登录功能提供用户名密码或Cookie以扫描需要认证的页面。Payload库强化建立更完善的Payload字典包含报错注入、时间盲注等更高级的测试向量。启发式类型判断在检测前先通过提交特殊字符如单引号、双引号观察响应智能判断参数是数字型还是字符型以及是否被过滤从而选择更精准的Payload。并发扫描使用asyncio和aiohttp重写请求部分使爬取和检测可以并发进行效率提升不止一个数量级。报告生成将扫描结果输出为结构化的报告如JSON、HTML或PDF格式便于存档和分析。漏洞验证对于发现的疑似漏洞可以尝试进行进一步的验证例如尝试获取数据库版本 (version)、当前用户 (user()) 等信息以降低误报。支持更多漏洞类型将框架扩展加入XSS跨站脚本、文件包含、命令注入等模块的检测逻辑。构建这样一个工具的过程其价值远超工具本身。它强迫你去理解HTTP协议、HTML结构、SQL语法以及服务器如何与数据库交互。每一个你绕过的坑都是对Web安全更深一层的认识。当你下次再写一个从数据库查询数据的PHP或Python后端函数时你会自然而然地想到参数化查询Parameterized Query或ORM而不是简单的字符串拼接。这才是安全工程师思维模式的起点。