华夏 ERP CMS v2.3代码审计
前言
项目地址:华夏ERP_v2.3
代码审计是每个安全从业者必须要会的技能,遂来学习一波
前置知识
工欲善其事,必先利其器
先推荐一些对这个项目比较好用的插件
MyBatisCodeHelperPro 插件有收费功能,但是我们使用免费的功能就够了,可以很方便的跳转到mapper绑定的接口
Rainbow Brackets Lite 彩虹括号,可以提到代码阅读效率
Spring常见注解
注解 | 作用 | 使用场景 |
---|---|---|
@Component | 标记一个类为 Spring 的组件,Spring 会自动检测并将其作为 Bean 注册到 Spring 容器中。 | 一般用于服务类、工具类、或任何希望被 Spring 容器管理的类。 |
@Controller | 标记一个类为 Spring MVC 控制器,处理 HTTP 请求并返回视图。 | 用于 Spring Web 应用中的控制器类,处理前端请求并返回数据或视图。 |
@Service | 标记一个类为服务层组件,用于定义业务逻辑。 | 用于业务层的服务类(例如,处理数据库操作或业务逻辑)。 |
@Repository | 标记一个类为数据访问层组件,通常用于 DAO 类,表示该类主要用于访问数据库。 | 用于数据层或持久层的组件,Spring 会提供数据库访问的相关异常翻译功能。 |
@Autowired | 自动注入依赖,Spring 会根据类型来注入 Bean。 | 用于自动注入 Bean,简化依赖注入的过程。 |
@Qualifier | 指定要注入的 Bean 的名称,当存在多个同类型的 Bean 时,通过该注解来明确指定注入哪一个 Bean。 | 当一个类型的 Bean 有多个实例时,用于指定注入特定的 Bean。 |
@Resource | 与 @Autowired 类似,但基于 Java EE 注解,通常根据 Bean 的名称进行注入。 |
用于 Java EE 环境,或者希望通过名称注入的场景。 |
@Value | 用于注入外部配置文件的值(例如,application.properties 或 application.yml 中的配置)。 |
注入配置文件中的值到类的属性、构造方法或方法参数中。 |
@Scope | 定义 Bean 的作用域(例如:singleton、prototype 等)。 | 根据实际需求定义 Bean 的生命周期。 |
@PostConstruct | 标记一个方法,在 Bean 初始化之后自动执行。 | 在 Bean 初始化完成后执行某些初始化工作。 |
@PreDestroy | 标记一个方法,在容器销毁之前执行,用于做清理工作。 | 在 Bean 销毁之前执行某些清理操作(例如关闭资源、连接等)。 |
@Configuration | 标记一个类为 Spring 配置类,替代 XML 配置文件,通常用于定义 @Bean 方法。 |
用于替代传统的 XML 配置文件,定义 Java 配置类。 |
@ComponentScan | 指定扫描的包路径,Spring 容器会自动扫描并注册该路径下的类。 | 与 @Configuration 一起使用,指定自动扫描 Bean 的路径。 |
@Bean | 用于在 Java 配置类中定义一个 Bean,相当于 XML 配置中的 <bean> 元素。 |
在 Java 配置类中手动定义一个 Bean,并将其加入到 Spring 容器中。 |
@PropertySource | 指定外部属性文件的位置,Spring 会将文件中的属性值加载到 Spring 环境中。 | 用于加载配置文件并将文件中的值注入到 Spring 容器中的 Bean 中。 |
@Import | 导入一个或多个配置类或组件类,使它们成为当前配置类的一部分。 | 用于导入其他配置类,方便模块化和分离不同的配置。 |
JAVAWEB 常见项目结构
View(视图层):
- 负责用户界面展示,直接与用户交互。
- 显示数据或接受用户输入。
Controller(控制层):
- 处理视图层的请求,调用服务层完成业务逻辑。
- 通常对请求参数进行校验,并将结果返回到视图层。
Service(服务层):
- 封装具体的业务逻辑。
- 调用 DAO 层与数据库交互,或者与外部服务进行集成。
DAO(持久层):
- 专门负责与数据库的交互,提供数据的增删改查功能。
- 通常使用 ORM 框架(如 MyBatis、Hibernate)实现。
Entity(实体类):
- 数据的实体类,通常与数据库表一一对应。
- 在持久化操作中充当数据传递的载体。
这样说也许还是有点抽象,我们就拿这个项目来给举例一下
数据走向:
用户通过 视图层(View) 提交请求,数据流入 **控制层(Controller)**。
控制层(Controller) 接收请求后,调用 服务层(Service) 执行业务逻辑。
服务层(Service) 调用 DAO 层 进行数据库操作(例如查询、插入、更新等)。
DAO 层 通过 实体类(Entity) 与数据库交互,最终返回数据。
数据经过多层传递和处理,最终在 视图层 展示给用户。
审计准备
审计思路
JAVA的项目一般来讲比较庞大,当我们拿到一个JAVA的项目应该先看什么?代码审计中这个”方向感”特别重要
- 上手第一时间先判断技术栈,可以让我们快速了解程序架构,想到潜在的安全漏洞。
- 其次这是个maven项目,查看pom.xml文件,它定义了项目的所有依赖、插件以及版本信息,查看项目所依赖的第三方库和它们的版本,以确认是否存在已知的漏洞。 比如这里使用了fastjson,还是有漏洞的版本。
- 另外看Filter,为什么看Filter 因为可以看到全局的路由,另外filter是所有请求处理的第一站,这意味着所有潜在的恶意请求在到达Controller之前,都会经过 Filter 进行处理和过滤。另外通过查看filter可以判断应用程序是否存在对用户输入的有效验证和过滤。
1 |
|
com.jsh.erp.filter.LogCostFilter#init
方法中对上面@WebInitParam
定义的参数用”#”分割
com.jsh.erp.filter.LogCostFilter#doFilter
方法是整个filter的核心部分,这里有几种情况是不阻止的:url包含register.html
,login.html
,以及 doc.html
。com.jsh.erp.filter.LogCostFilter#verify
是自己定义的一个工具方法,
1 |
|
将ignoredUrls中的逐个元素拼接成正则表达式后与当前url进行匹配,匹配成功即返回true,例如第一个元素形成的正则表达式为^.*.css.*$
,即只要包含ignoredUrls中的任意一个元素即可在不登录的情况下访问
用于检查请求 URL 是否在忽略列表中;也就是看当前访问的连接是不是.css|.js|.jpg|.png|.gif|.ico
文件,这些文件也是不阻止的。如果请求的路由是/user/login
或/user/registerUser
或/v2/api-docs
,也是不阻止的。
- 如果想更清楚的了解网站的架构,那路由分析不可少
- 接下来可以结合网站的功能点,结合代码,寻找脆弱点,当然这个比较靠经验。
路由分析
大部分请求路径都包含在Controller文件夹中,这里有一个特殊的类,即com.jsh.erp.controller.ResourceController
,它的请求路径中包含{apiName},代码中使用@Resource
注解通过名称注入使com.jsh.erp.service.CommonQueryManager类
对其进行处理,以com.jsh.erp.service.CommonQueryManager#select
方法为例:
1 |
|
它调用了com.jsh.erp.service.InterfaceContainer
,其中InterfaceContainer 的init方法会在Spring 容器启动时自动初始化。
1 |
|
configComponentMap存放的是ICommonQuery的实现类,即如图所示:
@Autowired(required = false)
:Spring 在初始化InterfaceContainer
时,会自动收集所有实现了ICommonQuery
接口的 Bean,注入到init
方法的configComponents
参数中。init
方法逻辑:- 遍历所有注入的
ICommonQuery
实现类。 - 通过
AnnotationUtils.getAnnotation
检查每个类是否包含@ResourceInfo
注解。 - 如果找到注解,则将注解的
value
值作为 key,对应的组件实例作为 value,存入configComponentMap
。
- 遍历所有注入的
- 最终结果:
configComponentMap
其中 key 是@ResourceInfo
的value
(如"account"
),value 是对应的组件实例(如AccountComponent
)(这就是一些文章说的与目录对应)。
初始化完成后,接着以com.jsh.erp.service.account.AccountComponent#select
为例。
1 |
|
调用了com.jsh.erp.service.account.AccountService#select
,接着ctrl+鼠标左键select
方法就看到到了dao层,进行真正的数据处理。
总结一下就是每个Component
类(这里以AccountComponent举例)的@AccountResource
注解将@ResourceInfo(value = "account")
和com.jsh.erp.service.account.AccountComponent
绑定,在InterfaceContainer
类中Spring 启动时会扫描所有实现了 ICommonQuery
接口的组件。通过反射读取每个组件上的 @ResourceInfo
注解的 value
值,将其作为键(apiName
),组件实例作为值,存入 configComponentMap
。当我们访问接口,比如:GET /account/list
。在 Controller
中,apiName
从路径参数中解析出来(@PathVariable("apiName")
),值为 "account"
。CommonQueryManager
将 apiName
传递给 InterfaceContainer
。InterfaceContainer
使用 configComponentMap.get(apiName)
查找对应的组件实例,找到 AccountComponent
。调用其 select
方法,执行对应的业务逻辑。
数据流:
1 |
|
未授权访问
阅读filter代码我们发现加了白,对应的资源加白在 ignoredUrl
中,以 #
分割,看似没什么问题;还有对应的 Path 加白,加了 /user/login
,/user/registerUser
以及 /v2/api-docs
。
对于加白的思索:是否会存在潜在的未授权访问?对于资源加白 ———— ignoredList
的判断只是进行了正则的判断,这并不符合开发的安全性,正确的写法应该使用 endsWith()
来判断 URL 是否以 .css
;.js
等资源后缀结尾
我们去找Controller(上面在介绍常见项目结构的时候也介绍过了,Controller是JavaWeb的入口点)。
直接使用.css|.js|.jpg|.png|.gif|.ico
绕过
这个接口能看到所有的信息
对于 URL 加白的思考:使用 startsWith()
方法来判断 URL 是否是白名单开头的时候,可以使用目录穿越来骗过判断,导致可以绕过认证请求。
存储型XSS
其实之前在看filter的时候我们就可以发现,并没有做过滤。我平常的习惯也就是见框就x
SQL注入
因为组件是Mybatis,SQL注入在Mybatis框架下一般寻找mapper下的xml文件中的${}、like、in、order by
,因为${}
是字符串替换意思就是咱们输入的字符可以直接作为sql的查询语句,like in order by
不能进行预编译,在过滤不严的情况下也有可能导致sql注入。
在mapper_xml文件 ctrl+shift+f 搜索${
然后就到了这里,这时候插件的好处就来了,ctrl+鼠标左键点击蓝色的小鸟
直接跳转到对应的接口方法com.jsh.erp.datasource.mappers.UserMapperEx#selectByConditionUser
,然后ctrl+鼠标左键找上层调用
然后就找到了com.jsh.erp.service.user.UserService#select
继续往上找
按住CTRL+鼠标左键
然后点击函数,即可看到调用位置为com.jsh.erp.service.user.UserComponent#getUserList
可以看到这里的username
和password
是直接通过com.jsh.erp.utils.StringUtil#getInfo
函数获取到的
可以看到在这个函数中调用了fastjson
中的com.alibaba.fastjson.JSON#parseObject(java.lang.String)
方法,去解析一个变量叫做search
还是没有到Controller层,继续Ctrl+B,来到com.jsh.erp.service.CommonQueryManager
1 |
|
继续往上,终于来到ResourceController
1 |
|
通过前面的路由分析可知,这里的apiname为user
正向数据链:
/user/list——>ResourceController.getList——>CommonQueryManager.select——>UserComponent.select——>UserComponent.getUserList——>UserService.select——>UserMapperEx.selectByConditionUser——>UserMapperEx.xml中id为selectByConditionUser的查询
同样的道理,在这个getList方法中,还有一个select查询,对应的数据链:
/user/list——>ResourceController.getList——>CommonQueryManager.counts——>UserComponent.counts——>UserService.countUser——>UserMapperEx.countsByUser——>UserMapperEx.xml中id为countsByUser的查询
触发页面
抓个包可以看到确实有userName、loginName参数
我们可以直接来验证一下
1 |
|
Fastjson漏洞
刚开始我们看组件的时候就发现这个fastjson是有漏洞的版本,且在上面sql注入分析的时候就发现com.jsh.erp.utils.StringUtil#getInfo
使用了fastjson
1 |
|
详细利用可以看https://github.com/safe6Sec/Fastjson
当我们不知道具体版本的情况下利用下面的代码探测版本
1 |
|
虽然这里没有回显,但是其实报错已经有了版本
使用{“@type”:”java.net.Inet4Address”,”val”:”01z8zqebq8y495se6ypru6f08rei2ez2o.oastify.com”}探测一下
可以看到DNS是有记录的,证明存在 Fastjson 漏洞,然后我们进一步构造 payload。
使用https://github.com/cckuailong/JNDI-Injection-Exploit-Plus
构造
1 |
|
但是我们看到autoType
没有开启
越权
重置密码
jsh账户有重置密码的权限,登录jsh账户抓一个重置密码的包
普通账户没有重置密码的功能,不过我们可以利用未授权访问中的方法来绕过。也就是说,我们可以利用普通用户构造如下数据包来越权修改他人的密码为初始密码123456
,我们先登录用户jsh
/123456
,然后抓包改包为如下内容(注意这里删除cookie,这样就是在未授权的情况下重置他人的密码):
可以看到密码修改成功了
让我们跟进代码逻辑看看
1 |
|
可以看到写的非常简单,除了admin用户的密码不能更改,其他的都可以通过这个方式更改。可以结合前面的未授权访问综合利用。
修改密码
登录自己的用户抓一个修改密码的包
1 |
|
我这个用户的id是137,当我把id改为138也是成功了
实战场景下,可以看有没有用户没有修改默认密码的。
删除用户
我们先用 admin 的账户抓一个 deleteUser
的包
可以用同样的方式绕过(删除cookie)
看一下代码逻辑,接收参数ids,使用逗号,分割,调用userMapperEx.batDeleteOrUpdateUser()
方法将ids参数拼接进sql语句进行删除,这里没有对当前执行删除用户操作的用户身份做判断。