1. 项目概述为什么用C#写一个简单的漏洞扫描器最近在整理自己的安全工具库发现市面上的漏洞扫描器要么功能庞大复杂要么就是黑盒工具知其然不知其所以然。对于想深入理解Web安全检测原理的开发者或安全爱好者来说自己动手实现一个核心扫描功能是条绝佳的成长路径。我选择用C#来实现一方面是因为它在Windows桌面应用和后台服务开发上生态成熟HttpClient、HtmlAgilityPack、Newtonsoft.Json这些库用起来得心应手能快速搭建原型另一方面C#的强类型和面向对象特性能让扫描器的模块设计更清晰便于后续扩展和维护。这个项目不追求大而全目标是构建一个能理解HTTP请求、解析响应、并实现几种基础漏洞检测逻辑的轻量级扫描器核心在于理解“扫描”这个动作背后的技术细节。2. 整体设计与核心思路拆解2.1 核心需求与功能边界定义一个漏洞扫描器本质上是一个自动化的安全测试代理。它需要模拟攻击者的行为向目标Web应用发送精心构造的请求然后分析服务器的响应从中寻找安全漏洞的蛛丝马迹。对于我们的“简单”扫描器我将其核心功能边界定义如下目标发现与爬取给定一个起始URL能够发现该域名下的其他链接构建测试范围。请求引擎能够灵活地构建和发送HTTP/HTTPS请求并完整记录请求与响应。漏洞检测模块实现几种最常见漏洞的检测逻辑例如SQL注入、跨站脚本XSS、敏感信息泄露等。结果管理与报告将扫描发现的问题清晰、结构化地输出便于分析。之所以限定这几点是为了避免项目一开始就陷入复杂性泥潭。像认证状态保持、复杂的JavaScript渲染、分布式爬取等高级功能完全可以等核心流程跑通后再迭代。2.2 技术栈选型与理由.NET Framework / .NET Core 6作为开发平台。考虑到跨平台和未来生态强烈建议使用.NET 6或更高版本。它自带的System.Net.Http.HttpClient性能好且支持异步是网络请求的不二之选。HtmlAgilityPack一个强大的HTML解析库。相比于正则表达式它能以DOM方式更稳定、准确地从HTML响应中提取链接、表单、脚本等元素是爬取功能的核心。Newtonsoft.Json (Json.NET)或System.Text.Json用于处理可能遇到的JSON API响应。虽然.NET Core内置了System.Text.Json但Newtonsoft.Json在功能丰富度和灵活性上目前仍有优势例如更复杂的序列化控制。我们主要用它来解析和生成检测payload时的数据结构。并发处理使用async/await进行异步I/O操作并结合Parallel.ForEach或Task库实现有限的并发控制以提升扫描效率同时避免对目标站点造成拒绝服务攻击。注意在工具选型上务必通过NuGet等官方包管理器安装避免使用来路不明的第三方DLL以防引入后门或兼容性问题。2.3 架构设计草图扫描器将采用一种简单的管道Pipeline或插件化架构便于每个检测模块独立开发和扩展。1. 调度引擎 (Scheduler) | 2. 爬虫模块 (Crawler) - 提取URL队列 | 3. 请求引擎 (Request Engine) - 发送HTTP请求获取响应 | 4. 检测模块插件池 (Detection Modules) |-- SQL注入检测器 |-- XSS检测器 |-- 敏感路径探测器 |-- ... (可扩展) | 5. 结果分析器与报告生成器 (Reporter)这个流程是线性的但通过队列和事件驱动可以较好地组织代码。每个检测模块都是一个独立的类实现统一的接口如IDetectionModule接收请求和响应对象进行分析。3. 核心模块实现细节与实操要点3.1 请求引擎的实现稳定与灵活是关键请求引擎是扫描器与目标交互的桥梁其稳定性和灵活性直接决定扫描效果。using System.Net.Http.Headers; public class HttpRequestEngine { private readonly HttpClient _httpClient; private readonly Random _random new Random(); public HttpRequestEngine() { // 配置HttpClient建议使用IHttpClientFactory管理生命周期此处为简化示例 var handler new HttpClientHandler { UseCookies true, AllowAutoRedirect true, // 自动处理重定向重要 MaxAutomaticRedirections 5 }; _httpClient new HttpClient(handler); _httpClient.Timeout TimeSpan.FromSeconds(30); // 设置一个常见的User-Agent避免被简单屏蔽 _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36); } public async TaskHttpResponseMessage SendRequestAsync(HttpRequestMessage request) { try { // 随机延迟模拟人工操作避免触发WAF的速率限制 await Task.Delay(_random.Next(500, 1500)); return await _httpClient.SendAsync(request); } catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) { // 处理超时 throw new Exception($请求超时: {request.RequestUri}); } catch (HttpRequestException ex) { // 处理网络错误 throw new Exception($网络请求失败: {ex.Message}); } } // 一个便捷方法用于构建GET请求 public HttpRequestMessage BuildGetRequest(Uri uri, Dictionarystring, string? headers null) { var request new HttpRequestMessage(HttpMethod.Get, uri); if (headers ! null) { foreach (var header in headers) { request.Headers.TryAddWithoutValidation(header.Key, header.Value); } } return request; } // 类似地可以实现BuildPostRequest等方法 }实操要点连接复用与超时务必重用HttpClient实例而不是每次请求都创建新的否则会导致端口耗尽。超时时间不宜过短对于复杂的应用或慢速网络30秒是一个合理的起点。处理重定向将AllowAutoRedirect设置为true让引擎自动跟随301/302跳转。这对于爬取和检测至关重要因为很多漏洞可能出现在重定向后的页面上。同时要控制MaxAutomaticRedirections防止无限重定向。请求头伪装设置合理的User-Agent、Referer等请求头让请求看起来更像普通浏览器发出的。一些安全设备会过滤掉没有常见浏览器标识的请求。异常处理网络请求充满不确定性必须妥善处理超时、连接拒绝、SSL错误等异常避免整个扫描进程因单个请求失败而崩溃。3.2 爬虫模块如何高效、礼貌地发现目标爬虫的目标是从起始页开始尽可能发现属于同一域名的链接同时避免陷入爬虫陷阱或重复爬取。using HtmlAgilityPack; public class SimpleCrawler { private readonly Uri _baseUri; private readonly HashSetstring _visitedUrls new HashSetstring(); private readonly ConcurrentQueueUri _urlsToCrawl new ConcurrentQueueUri(); private readonly HttpRequestEngine _requestEngine; public SimpleCrawler(string startUrl, HttpRequestEngine engine) { _baseUri new Uri(startUrl); _requestEngine engine; _urlsToCrawl.Enqueue(_baseUri); } public async TaskListUri CrawlAsync(int maxPages 100) { var discoveredUrls new ListUri(); while (_urlsToCrawl.TryDequeue(out Uri? currentUrl) _visitedUrls.Count maxPages) { if (_visitedUrls.Contains(currentUrl.ToString())) continue; _visitedUrls.Add(currentUrl.ToString()); Console.WriteLine($[Crawling] {currentUrl}); try { var request _requestEngine.BuildGetRequest(currentUrl); var response await _requestEngine.SendRequestAsync(request); if (!response.IsSuccessStatusCode) continue; string htmlContent await response.Content.ReadAsStringAsync(); var newUris ExtractUrisFromHtml(htmlContent, currentUrl); foreach (var uri in newUris) { // 只关注同一域名下的链接避免爬取整个互联网 if (uri.Host _baseUri.Host !_visitedUrls.Contains(uri.ToString())) { _urlsToCrawl.Enqueue(uri); discoveredUrls.Add(uri); } } } catch (Exception ex) { Console.WriteLine($[Crawl Error] {currentUrl}: {ex.Message}); } } return discoveredUrls; } private ListUri ExtractUrisFromHtml(string html, Uri baseUrl) { var uris new ListUri(); var htmlDoc new HtmlDocument(); htmlDoc.LoadHtml(html); // 提取a标签的href var linkNodes htmlDoc.DocumentNode.SelectNodes(//a[href]); if (linkNodes ! null) { foreach (var node in linkNodes) { string href node.GetAttributeValue(href, ); if (Uri.TryCreate(baseUrl, href, out Uri? absoluteUri)) { uris.Add(absoluteUri); } } } // 还可以提取script src, link href, img src, form action等 // 提取表单对于漏洞检测尤其重要 var formNodes htmlDoc.DocumentNode.SelectNodes(//form[action]); if (formNodes ! null) { foreach (var node in formNodes) { string action node.GetAttributeValue(action, ); if (string.IsNullOrEmpty(action)) action baseUrl.ToString(); // 默认提交到当前页 if (Uri.TryCreate(baseUrl, action, out Uri? formActionUri)) { uris.Add(formActionUri); } } } return uris.Distinct().ToList(); } }注意事项去重使用HashSetstring存储已访问的URL字符串确保高效去重。注意规范化URL如去除末尾斜杠、统一大小写避免因格式不同导致重复爬取。限定范围严格限制爬取范围如uri.Host _baseUri.Host这是伦理和安全的要求避免无意中对其他网站造成骚扰。尊重robots.txt一个成熟的扫描器应该解析目标网站的robots.txt文件并遵守其规则。本项目为简化未实现但在实际对外扫描时必须加入此功能。并发控制上述示例是单线程的。在实际应用中应在CrawlAsync方法内部引入信号量SemaphoreSlim或使用Parallel.ForEach配合MaxDegreeOfParallelism来控制并发线程数通常3-5个并发线程是比较礼貌的设置。3.3 SQL注入检测模块实现原理与技巧SQL注入检测的核心思想是向请求参数GET查询字符串、POST表单数据、Cookie等中插入特殊的“payload”然后根据服务器响应内容、响应时间或错误信息来判断是否存在漏洞。检测逻辑设计识别注入点遍历所有发现的URL和表单提取其中的参数名如id,username,search。构造Payload为每个参数构造一组测试payload。这些payload通常包括逻辑真/假测试 AND 11与 AND 12。如果应用程序返回不同内容则可能存在注入。错误触发(单引号),(双引号),;(分号)。观察是否返回数据库错误信息如MySQL, SQL Server, Oracle的特定错误。时间盲注测试 OR SLEEP(5)--。如果响应延迟了大约5秒则可能存在基于时间的盲注。联合查询测试 UNION SELECT NULL, NULL--。通过不断增加NULL的数量直到页面正常显示来探测列数。发送与比对分别发送原始请求基线请求和携带payload的请求。对比两者的响应状态码、响应体长度、响应内容、响应时间。结果判定基于一套启发式规则判定。例如如果触发payload的响应包含了数据库错误关键词如“SQL syntax” “MySQL” “ORA-”则标记为“疑似SQL注入错误型”。如果真假逻辑测试返回的页面内容差异显著可通过计算字符串哈希或比较特定关键词实现则标记为“疑似SQL注入布尔型”。C#代码片段示例简化版public class SqlInjectionDetector : IDetectionModule { private readonly string[] _errorKeywords { sql, syntax, mysql, oracle, sqlite, odbc, driver }; public async TaskListVulnerability CheckAsync(HttpRequestMessage baselineRequest, HttpResponseMessage baselineResponse) { var vulnerabilities new ListVulnerability(); string baselineContent await baselineResponse.Content.ReadAsStringAsync(); int baselineLength baselineContent.Length; // 假设我们只检测URL中的查询参数 var uriBuilder new UriBuilder(baselineRequest.RequestUri); var query HttpUtility.ParseQueryString(uriBuilder.Query); foreach (string paramName in query.AllKeys) { if (string.IsNullOrEmpty(paramName)) continue; string originalValue query[paramName]; // 测试单引号错误触发 query[paramName] originalValue ; uriBuilder.Query query.ToString(); var testRequest new HttpRequestMessage(baselineRequest.Method, uriBuilder.Uri); CopyHeaders(baselineRequest, testRequest); // 复制原始请求头 var testResponse await _httpClient.SendAsync(testRequest); var testContent await testResponse.Content.ReadAsStringAsync(); // 规则1检查响应中是否包含数据库错误关键词 if (_errorKeywords.Any(kw testContent.ToLower().Contains(kw))) { vulnerabilities.Add(new Vulnerability { Type SQL Injection, Severity High, Url testRequest.RequestUri.ToString(), Parameter paramName, Evidence Triggered database error message., Payload }); } // 规则2布尔逻辑测试简化示例需更复杂的差异分析 string truePayload originalValue AND 11; string falsePayload originalValue AND 12; // ... 发送两个请求并比较响应内容差异 ... // 如果差异显著则记录漏洞 query[paramName] originalValue; // 恢复原始值准备测试下一个payload } return vulnerabilities; } }实操心得Payload库不要自己从头构造所有payload。可以参考开源项目如sqlmap的xml/payloads/目录下的文件那里有非常全面且分类清晰的payload。你可以将其转换为C#中的列表或从文件加载。差异化比对简单的字符串相等或长度比较在动态内容页面如包含时间戳、广告上会误报。更稳健的方法是1移除HTML标签和脚本后再比较文本内容2计算响应正文的哈希值如MD5进行比对3寻找页面中稳定的“指纹”区域如标题、导航栏进行比较。时间盲注检测检测时间延迟需要精确计时。使用Stopwatch类并注意网络波动。通常如果延迟时间与payload中指定的睡眠时间如5秒接近例如4.5-6秒且远大于基线请求时间如1秒才判定为可能的时间盲注。需要多次测试以减少误报。3.4 XSS检测模块实现思路跨站脚本攻击检测与SQL注入检测思路类似但payload和目标不同。XSS payload是试图在受害者浏览器中执行的JavaScript代码。检测逻辑反射型XSS检测将测试payload插入到参数中发送请求然后检查响应HTML中该payload是否被原样或仅有最小化转义地反射回来。常用Payloadscriptalert(XSS)/scriptimg srcx onerroralert(1)svg/onloadalert(1)。检测方法在响应HTML中搜索插入的payload字符串。如果找到并且其上下文比如没有被正确地HTML编码如变成lt;允许其执行则存在漏洞。存储型XSS检测需要找到数据提交点如表单提交包含payload的数据然后访问可能显示该数据的页面如留言板、个人资料页检查payload是否被存储并渲染。这需要扫描器具备状态保持会话和触发数据查看流程的能力比反射型复杂得多。我们的简单扫描器可能主要关注反射型。C#代码片段示例反射型XSS检测public class XssDetector : IDetectionModule { private readonly string[] _xssPayloads { scriptalert(xss)/script, \ onmouseover\alert(1), img srcx onerroralert(1), javascript:alert(1)// }; public async TaskListVulnerability CheckAsync(HttpRequestMessage baselineRequest, HttpResponseMessage baselineResponse) { var vulns new ListVulnerability(); string baselineContent await baselineResponse.Content.ReadAsStringAsync(); var uriBuilder new UriBuilder(baselineRequest.RequestUri); var query HttpUtility.ParseQueryString(uriBuilder.Query); foreach (string paramName in query.AllKeys) { if (string.IsNullOrEmpty(paramName)) continue; string originalValue query[paramName]; foreach (var payload in _xssPayloads) { query[paramName] payload; uriBuilder.Query query.ToString(); var testRequest new HttpRequestMessage(baselineRequest.Method, uriBuilder.Uri); CopyHeaders(baselineRequest, testRequest); var testResponse await _httpClient.SendAsync(testRequest); var testContent await testResponse.Content.ReadAsStringAsync(); // 关键检查payload是否被原样反射且未被转义 // 简单的检查payload是否出现在响应体中且其前后的字符可能构成可执行的HTML/JS上下文 // 这是一个非常基础的检查误报率高 if (testContent.Contains(payload)) { // 进一步分析上下文这里简化处理 // 可以检查payload是否出现在 script 标签内或者属性值中且没有被引号包裹或转义 int index testContent.IndexOf(payload); string context GetHtmlContext(testContent, index, payload.Length); if (IsExecutableContext(context, payload)) { vulns.Add(new Vulnerability { Type Reflected XSS, Parameter paramName, Payload payload, Url testRequest.RequestUri.ToString() }); break; // 发现一个payload即止避免重复报告同一参数 } } } query[paramName] originalValue; } return vulns; } // 需要实现 GetHtmlContext 和 IsExecutableContext 方法来更精确地判断 }注意事项上下文感知最关键的改进点是上下文感知。一个script标签内的alert和一段文本中的alert意义完全不同。需要分析payload在响应HTML中的位置是在HTML标签内、属性值内、JavaScript代码块内还是纯文本中只有在前三种情况下且没有被正确转义才构成真正的漏洞。Payload编码尝试对payload进行URL编码、HTML实体编码等测试应用程序的过滤和转义机制是否可以被绕过。误报与静默简单的字符串匹配会产生大量误报。成熟的XSS扫描器如Burp Suite的Scanner会结合静态分析和动态验证如使用内置浏览器引擎实际渲染页面观察弹窗。我们的简单版本可以标记为“疑似”供人工复核。4. 项目集成与扫描流程编排4.1 主程序流程控制将上述模块串联起来形成一个完整的扫描流程。public class SimpleVulnerabilityScanner { private readonly HttpRequestEngine _requestEngine; private readonly ListIDetectionModule _detectors; private readonly ListVulnerability _findings new ListVulnerability(); public SimpleVulnerabilityScanner() { _requestEngine new HttpRequestEngine(); _detectors new ListIDetectionModule { new SqlInjectionDetector(_requestEngine), new XssDetector(_requestEngine), new SensitivePathDetector(_requestEngine) // 可以添加其他检测器 }; } public async Task ScanAsync(string startUrl) { Console.WriteLine($[*] 开始扫描: {startUrl}); // 1. 爬取 var crawler new SimpleCrawler(startUrl, _requestEngine); var targetUrls await crawler.CrawlAsync(maxPages: 50); Console.WriteLine($[*] 爬取完成共发现 {targetUrls.Count} 个独特URL。); // 2. 对每个URL进行漏洞检测 var options new ParallelOptions { MaxDegreeOfParallelism 3 }; // 控制并发度 await Parallel.ForEachAsync(targetUrls, options, async (url, cancellationToken) { Console.WriteLine($[Testing] {url}); try { var baselineRequest _requestEngine.BuildGetRequest(url); var baselineResponse await _requestEngine.SendRequestAsync(baselineRequest); if (!baselineResponse.IsSuccessStatusCode) return; foreach (var detector in _detectors) { var vulns await detector.CheckAsync(baselineRequest, baselineResponse); if (vulns.Any()) { lock (_findings) // 线程安全地添加结果 { _findings.AddRange(vulns); } } } } catch (Exception ex) { Console.WriteLine($[Error testing {url}] {ex.Message}); } }); // 3. 生成报告 GenerateReport(_findings); } private void GenerateReport(ListVulnerability findings) { Console.WriteLine(\n 扫描报告 ); Console.WriteLine($扫描完成共发现 {findings.Count} 个潜在漏洞。); foreach (var vul in findings.GroupBy(v v.Type)) { Console.WriteLine($\n[{vul.Key}]); foreach (var item in vul) { Console.WriteLine($ URL: {item.Url}); Console.WriteLine($ 参数: {item.Parameter}); Console.WriteLine($ Payload: {item.Payload}); Console.WriteLine($ 证据: {item.Evidence}); Console.WriteLine($ ---); } } // 可以将报告输出为JSON、HTML或CSV文件 // File.WriteAllText(scan_report.json, JsonConvert.SerializeObject(findings, Formatting.Indented)); } } // 定义漏洞信息类 public class Vulnerability { public string Type { get; set; } string.Empty; public string Severity { get; set; } Medium; public string Url { get; set; } string.Empty; public string Parameter { get; set; } string.Empty; public string Payload { get; set; } string.Empty; public string Evidence { get; set; } string.Empty; }4.2 配置与扩展性设计为了让扫描器更实用可以考虑添加以下配置项通过appsettings.json或命令行参数来指定TargetUrl: 起始扫描地址。MaxCrawlPages: 最大爬取页面数。MaxConcurrentRequests: 最大并发请求数。ScanModules: 启用的检测模块列表如[SQLi, XSS]。CustomHeaders: 需要添加的自定义HTTP头如认证Token。OutputFormat: 报告输出格式Console, JSON, HTML。检测模块应设计为插件式。定义一个公共接口IDetectionModule所有检测器都实现它。主程序通过反射或依赖注入加载所有实现了该接口的类这样新增一种漏洞检测类型时只需添加一个新的类库项目或DLL而无需修改主程序代码。public interface IDetectionModule { string ModuleName { get; } TaskListVulnerability CheckAsync(HttpRequestMessage baselineRequest, HttpResponseMessage baselineResponse); }5. 常见问题、优化与避坑指南5.1 性能与礼貌性优化速率限制在HttpRequestEngine的SendRequestAsync方法中加入随机延迟Task.Delay是基本的礼貌。更高级的做法是实现一个令牌桶Token Bucket算法严格控制请求速率。连接池管理确保正确使用和释放HttpClient。对于长时间运行的扫描器建议使用IHttpClientFactory来管理HttpClient实例的生命周期以避免Socket耗尽问题。异步并行使用Parallel.ForEachAsync.NET 6或Task.WhenAll配合信号量控制并发数能显著提升I/O密集型扫描任务的效率。5.2 提高检测准确率降低误报/漏报误报处理建立白名单对于已知的、由框架或WAF返回的特定错误页面将其内容特征如特定字符串、哈希值加入白名单避免反复报告。差异分析智能化对于布尔型SQL注入或XSS不要简单比较整个页面。可以提取页面的“核心内容区域”如通过CSS选择器定位main,article或特定id/class的div只比较这部分内容的差异。人工验证开关对于高风险的疑似漏洞在报告中标记为“待确认”并提供一键重放请求的功能例如生成一个cURL命令或Burp Suite的Intruder payload方便手动验证。漏报处理丰富Payload库持续维护和更新SQL注入、XSS、路径遍历等漏洞的payload库。关注安全社区的最新绕过技巧。参数污染尝试对同一个参数提交多个值如id1id2测试后端如何处理这有时能绕过某些防护。编码绕过对payload进行多重编码如URL编码、HTML实体编码、Unicode编码后再发送。5.3 运行时常遇问题与排查扫描中途卡住或停止检查目标目标网站可能启用了反爬虫机制如验证码、IP封禁、JavaScript挑战。观察控制台输出看是否大量请求返回403/429状态码。检查网络与超时增加HttpClient的超时时间。检查本地网络或代理设置。检查并发过高的并发可能导致本地端口耗尽或目标拒绝服务。降低MaxDegreeOfParallelism。使用调试器在可能卡住的代码处如某个特定的URL请求或响应解析设置断点查看具体状态。大量误报分析响应将误报的请求和响应保存下来例如记录到文件。分析是哪种检测规则过于宽泛。调整该检测模块的判定逻辑增加更多约束条件。基线建立对于动态内容可以尝试对同一页面发送两次相同的基线请求比较响应差异了解其“自然波动”范围将此作为差异判定的阈值。内存占用过高及时释放资源确保HttpResponseMessage和响应流在使用后被正确释放Dispose或使用using语句。限制队列大小为待爬取URL队列ConcurrentQueue设置一个上限避免在大型网站上无限增长。流式处理响应如果只需要检查响应头或部分内容可以使用HttpCompletionOption.ResponseHeadersRead并流式读取响应体避免将整个大响应如文件下载加载到内存。5.4 法律与伦理红线这是最重要的一部分。仅扫描你有权测试的目标这包括你拥有书面授权如漏洞众测项目、企业内部测试的网站或者你自己搭建的测试环境如DVWA, WebGoat。切勿扫描任何未经授权的公共网站或系统这是非法的属于“未经授权的访问尝试”可能构成计算机犯罪会面临法律诉讼。控制扫描强度即使对授权目标也应避免使用具有破坏性的payload如DROP TABLE,rm -rf。我们的简单扫描器不应包含此类payload。明确免责声明在工具的README和输出报告中必须清晰声明“本工具仅用于安全学习与研究目的。使用者应确保在合法、授权的范围内使用。开发者对因滥用本工具造成的任何损失概不负责。”编写这样一个工具的过程其价值远超工具本身。它迫使你去深入理解HTTP协议、Web应用架构、常见漏洞的成因与表现形式以及如何自动化地识别它们。每一个模块的实现每一次与目标应用的交互分析都是对Web安全知识的一次巩固和升华。当你看到自己编写的扫描器成功识别出一个隐藏在参数中的SQL注入点时那种成就感是使用现成黑盒工具无法比拟的。记住保持好奇心但更要恪守边界让技术用于建设而非破坏。