JshERP v2.3 代码审计教学文档
文档概述
本教学文档基于对开源ERP系统JshERP v2.3的代码审计实践。通过本次审计,我们将深入探讨Java Web应用常见的安全漏洞,包括身份验证绕过、Fastjson反序列化、MyBatis SQL注入以及存储型XSS。文档将遵循“环境搭建 -> 项目结构分析 -> 漏洞审计详解”的逻辑,旨在为安全研究人员和开发者提供一套完整的代码审计思路和方法论。
第一章:环境搭建与项目结构分析
1.1 环境准备
- 项目地址: https://github.com/jishenghua/jshERP/releases/tag/2.3
- 必要环境:
- JDK 1.8: 项目编译和运行的基础环境。
- MySQL 5.7.26: 数据库,需创建名为
jsh_erp的数据库并导入项目SQL文件。 - Maven 3.9.1: 用于管理项目依赖和构建。
- IDEA: 集成开发环境,便于代码阅读和调试。
- 启动方式: 使用IDEA打开项目,配置好Maven和JDK后,直接运行
ErpApplication.java主类。默认测试账号为jsh/123456。
1.2 项目架构分析
JshERP采用典型的MVC架构,并增加了Service层,结构清晰,是学习Java Web应用架构的良好样本。
controller/: Web控制层,负责接收HTTP请求和返回响应。service/: 业务逻辑层,处理核心业务逻辑。datasource/mappers/: 数据持久层,包含MyBatis的Mapper接口和对应的XML映射文件(位于resources/mapper_xml),这些XML文件是SQL注入审计的关键。utils/: 工具类,其中StringUtil类包含了危险的反序列化操作。filter/: 过滤器,LogCostFilter是身份验证的核心。ErpApplication.java: 项目启动主类,使用@SpringBootApplication注解。
关键点: 理解MVC各层的职责和调用关系,是快速定位漏洞入口的前提。
第二章:漏洞审计详解
2.1 漏洞一:身份验证绕过(LogCostFilter.java)
-
漏洞文件:
com.jsh.erp.filter.LogCostFilter -
漏洞类型: 逻辑缺陷/权限控制缺失
-
漏洞原理:
该过滤器负责检查用户会话,未登录用户将被重定向到登录页面。但其放行逻辑存在缺陷:- 检查Session中是否存在
user对象,存在则放行。 - 检查请求URL是否包含
/doc.html,/register.html,/login.html,包含则放行。 - 检查请求URL是否匹配
ignoredList(如.css,.js等静态资源),匹配则放行。 - 检查请求URL是否以
allowUrls(如/user/login,/user/registerUser)开头,是则放行。 - 以上条件均不满足,则重定向到
/login.html。
- 检查Session中是否存在
-
漏洞点: 在第2和第3步的路径检查中,过滤器仅使用了简单的字符串包含(
contains)和正则匹配,未对路径进行规范化处理。攻击者可以利用目录遍历(../)绕过检查。 -
漏洞复现(Payload):
直接访问后台功能页面的请求会被拦截,但通过以下Payload可绕过:GET /doc.html/../home.html HTTP/1.1 GET /login.html/../account/list HTTP/1.1 GET /1.css/../user/list HTTP/1.1这些请求在进入过滤器时,
/doc.html/../home.html会被识别为包含doc.html从而被放行。但后续服务器在处理请求时,会将其规范化为/home.html,从而访问到本应受保护的资源。 -
修复建议:
在路径判断前,先对requestUrl进行规范化处理,使用request.getRequestURI()得到的路径已经是解过码的,但最好再使用normalize()方法(如Apache Commons IO的FilenameUtils.normalize)或直接使用request.getServletPath()来获取标准化后的路径,再进行白名单匹配。
2.2 漏洞二:Fastjson反序列化远程命令执行
-
漏洞文件:
com.jsh.erp.utils.StringUtil.getInfo -
相关依赖:
fastjson:1.2.55(存在已知反序列化漏洞的低版本) -
漏洞原理:
StringUtil.getInfo方法直接使用JSONObject.parseObject(search)来解析用户可控的search参数。public static String getInfo(String search, String key){ String value = ""; if(search != null) { JSONObject obj = JSONObject.parseObject(search); // 危险操作! value = obj.getString(key); } return value; }当恶意攻击者传入一个包含
@type属性的JSON字符串时,Fastjson会尝试实例化该属性指定的类。如果该类存在危险的构造器、getter、setter方法,就可能执行任意代码。 -
利用链分析:
- 入口点: 控制器
ResourceController.getList方法接收search参数。 - 传递: 调用
UserComponent.getUserList->StringUtil.getInfo。 - 触发:
JSONObject.parseObject(search)触发Fastjson反序列化流程。 - 利用: 通过
@type指定恶意类,如java.net.Inet4Address,可触发DNS查询,证明漏洞存在。更高版本的利用链可导致RCE。
- 入口点: 控制器
-
漏洞复现(DNSLog探测):
由于该版本Fastjson的默认配置可能限制了恶意类的加载,我们可以先使用DNSLog来验证漏洞存在。
Payload:GET /user/list?search={"@type":"java.net.Inet4Address","val":"your-dnslog-server.cn"} HTTP/1.1说明: 替换
your-dnslog-server.cn为您的DNSLog平台域名。如果收到DNS查询记录,则证明反序列化漏洞被成功触发。 -
修复建议:
- 首要方案: 升级Fastjson至最新安全版本(>= 1.2.83),并开启
safeMode(最彻底)或使用@JSONType注解进行严格的白名单控制。 - 临时方案: 如果无法升级,在调用
parseObject或parse时,指定一个安全的ParserConfig,并关闭autoTypeSupport。但这不是根本解决办法。
- 首要方案: 升级Fastjson至最新安全版本(>= 1.2.83),并开启
2.3 漏洞三:MyBatis SQL注入(多位置)
-
漏洞文件: 多个Mapper XML文件,如
AccountMapperEx.xml,DepotMapperEx.xml等。 -
漏洞类型: SQL注入
-
漏洞原理:
MyBatis中,#{}是预编译占位符,能有效防止SQL注入。而${}是字符串替换,会将参数值直接拼接到SQL语句中。审计发现,系统中多处模糊查询使用了危险的${}方式。漏洞代码示例(AccountMapperEx.xml):
<select id="selectByConditionAccount" ...> SELECT ... FROM jsh_account account ... WHERE 1=1 <if test="name != null"> and account.name LIKE '%${name}%' <!-- 这里存在SQL注入 --> </if> </select> -
利用链分析:
- 前端请求
GET /account/list?search={"name":"注入点","serialNo":"2","remark":"3"}。 - 后端通过统一接口
ResourceController.getList路由到AccountComponent.getAccountList。 StringUtil.getInfo解析出name参数。- 最终参数传入
AccountMapperEx.selectByConditionAccount方法,拼接成SQL语句。
- 前端请求
-
漏洞复现(时间盲注):
Payload:GET /account/list?search={"name":"123' OR SLEEP(5) -- "}¤tPage=1&pageSize=15 HTTP/1.1说明: 此Payload会使数据库执行延时5秒,从而判断注入是否存在。同样,也可以进行联合查询、数据提取等操作。
受影响的Mapper:AccountMapperEx.xml,DepotMapperEx.xml,LogMapperEx.xml,MaterialMapperEx.xml,PersonMapperEx.xml,RoleMapperEx.xml,UnitMapperEx.xml,UserMapperEx.xml等。 -
修复建议:
将${}替换为#{}。但模糊查询需要做特殊处理,因为LIKE '%#{name}%'是错误的语法。正确的修复方式有两种:- 在XML中使用SQL字符串连接函数(推荐):
and account.name LIKE CONCAT('%', #{name}, '%') - 在Java代码中拼接好通配符,再传入Mapper:
String nameParam = "%" + name + "%"; // ... 然后调用Mapper,Mapper中使用 #{name}
- 在XML中使用SQL字符串连接函数(推荐):
2.4 漏洞四:存储型跨站脚本(XSS)
-
漏洞位置: 多个新增/编辑功能点,如“用户管理”、“商品信息”、“收入单”。
-
漏洞原理:
后端Controller在接收用户输入的数据(如用户名、商品名、备注等)时,未对输入内容进行任何过滤或编码,直接保存到数据库中。当这些数据再次被查询并渲染到前端页面时,浏览器会执行其中的JavaScript代码,从而触发XSS。 -
漏洞复现:
- 在“用户管理”中,编辑一个用户,将用户名修改为:
<script>alert('XSS')</script>。 - 保存后,每当页面列表显示该用户名,或其他用户查看、编辑该用户信息时,都会触发弹窗。
- 在“用户管理”中,编辑一个用户,将用户名修改为:
-
修复建议:
- 输入过滤: 对用户输入进行严格的校验和过滤,只允许预期的字符集。
- 输出编码: 在将数据输出到HTML页面时,必须进行HTML编码。例如,将
<编码为<,>编码为>。现代前端框架(如Vue, React)通常默认提供输出编码。 - 内容安全策略(CSP): 在HTTP响应头中设置CSP,可以有效地缓解XSS的影响。
第三章:总结与审计方法论
本次对JshERP v2.3的代码审计,揭示了从前端权限控制到后端数据处理等多个层面的安全问题。总结出的Java代码审计方法论如下:
- 环境与架构理解: 快速搭建环境,理清项目技术栈(Spring Boot, MyBatis等)和代码结构(MVC),这是审计的基础。
- 入口点搜寻:
- Controller: 重点关注所有对外暴露的API接口,特别是接收用户参数的接口(
@RequestParam,@RequestBody)。 - 配置文件: 审查
pom.xml中的依赖版本,寻找已知漏洞的组件(如Fastjson, Log4j)。 - 过滤器/拦截器: 检查权限控制逻辑是否严谨。
- Controller: 重点关注所有对外暴露的API接口,特别是接收用户参数的接口(
- 数据流跟踪: 从用户输入点(Controller参数)开始,跟踪数据如何流经Service、Mapper,最终到达数据库或外部系统(如执行SQL、反序列化)。关注每一步是否有安全检查。
- 关键函数/模式识别:
- SQL注入: 在MyBatis XML中全局搜索
${。 - 反序列化: 搜索
JSON.parseObject,ObjectInputStream.readObject等。 - 命令执行: 搜索
Runtime.exec,ProcessBuilder.start。 - 文件操作: 搜索
FileInputStream,Paths.get(路径遍历)。 - XSS: 查看数据返回视图时是否经过编码。
- SQL注入: 在MyBatis XML中全局搜索
- 黑白盒结合: 结合动态测试(如Burp Suite抓包重放)验证静态代码分析的结果,提高漏洞发现的准确率。
通过本次实践,我们不仅发现了多个高危漏洞,更重要的建立了一套系统性的代码审计思维模式,这对于应对未来的安全挑战至关重要。