文章总结: 这篇文章对万户OA系统进行了代码审计,发现了多个严重安全漏洞。核心发现包括:鉴权机制存在绕过漏洞,通过路径遍历可访问受限资源;多处SQL注入漏洞影响MySQL、Oracle和SQLServer数据库;存在SSRF漏洞允许服务器发起任意HTTP请求;文件读取漏洞允许未授权访问系统文件;getNextAutoCode功能也存在SQL注入风险。文章提供了详细的漏洞分析和代码片段,建议系统管理员及时修补这些漏洞,加强输入验证和访问控制,并对类似OA系统进行全面安全审计。
综合评分: 89
文章分类: 代码审计,漏洞分析,WEB安全,渗透测试,漏洞预警
万户OA系统代码审计
原创
1
韭要学JAVA安全
2025年11月22日 20:18
河南
一、万户 OA(ezOFFICE)系统背景速览
万户 OA 是面向政企场景的协同办公解决方案,覆盖流程审批、文档管理、人事协同、数据统计等核心办公场景,因其适配性强、部署灵活(支持私有化部署 / 云端部署),成为众多单位的首选办公系统。
二、核心配置文件:web.xml 作用与鉴权关联
web.xml 是 Java Web 应用的核心配置文件,主要负责 URL 映射、过滤器配置、会话管理、安全约束等核心功能,其配置直接影响系统鉴权机制的安全性:
三、鉴权分析
classes/com/whir/common/util/SetCharacterEncodingFilter.class
if (var11.indexOf("/iWebOfficeSign/OfficeServer.jsp") >= 0) {var3.doFilter(var1, var2); // 直接放行,跳过所有鉴权 }
indexOf是「字符串包含匹配」:只要变量var11(请求的相对路径)中存在/iWebOfficeSign/OfficeServer.jsp这个子串,就判定为 “合法路径”,直接放行。
- 它不关心这个子串在路径的开头、中间还是结尾,也不关心子串前后有没有其他字符;
- 对比「精确路径匹配」(如
equals方法):只有当var11完全等于/iWebOfficeSign/OfficeServer.jsp才放行,
2.var11 是「未做任何处理的原始相对路径」:
代码仅通过 var9.substring(var10.length()) 截取了根路径后的相对路径,没有对路径中的特殊字符(如 ../)、冗余分隔符(如 //)做任何 “规范化处理”—— 这是漏洞能成功利用的核心前提。
假设:
- 万户 OA 的项目根路径(
var10):/defaultroot(访问时 URL 前缀是http://目标IP/defaultroot/); - 我们要访问的敏感路径(需鉴权):
/admin/userList.jsp(相对路径,完整 URL 是http://目标IP/defaultroot/admin/userList.jsp); - 我们构造的绕过路径:
/iWebOfficeSign/OfficeServer.jsp/../../admin/userList.jsp(相对路径,完整 URL 是http://目标IP/defaultroot/iWebOfficeSign/OfficeServer.jsp/../../admin/userList.jsp)。
第二种:
HttpSession var17 = var5.getSession();if ((var16.equals("") || var16.equals(".jsp") || var16.equals(".vm")) && this.needSecurity && var11.indexOf("/evo/weixin/") < 0 && var11.indexOf("/portal/") < 0 && var11.indexOf("/upgrade/") < 0 && var11.indexOf("/public/edit/") < 0 && !var8.getNosessionWhiteList().contains(var11)) { if (var17.getAttribute("userId") == null || var17.getAttribute("userId").toString().equals("") || var17.getAttribute("userId").toString().equals("null")) { if (var11.indexOf("/evo/sp/") >= 0) { var4.sendRedirect(var6 + "/evo/sp/login.jsp"); } else { var4.sendRedirect(var6 + "/public/messages/overtime.jsp"); } logger.error("session 过滤 为空的请求:" + var11); return; } String var18 = var17.getAttribute("userId").toString(); String var19 = var5.getParameter("common_whir_formUserId"); if (var19 != null && !var19.equals("") && !var19.equals(var18)) { var4.sendRedirect(var6 + "/login.jsp"); logger.error("session里的userId不一致:" + var11); return; }}
这段代码是 Java Web 应用的 Session 鉴权 鉴权触发条件(所有条件同时满足才会进入校验)
只有当以下 8 个条件都成立 时,才会校验 Session 中的 userId:
- 请求后缀是 空字符串、
.jsp或.vm(拦截动态页面 / 无后缀请求); - 全局鉴权开关
needSecurity = true(鉴权功能已启用); - 请求 URI 不包含
/evo/weixin/(该路径免校验); - 请求 URI 不包含
/portal/(该路径免校验); - 请求 URI 不包含
/upgrade/(该路径免校验); - 请求 URI 不包含
/public/edit/(该路径免校验); - 请求 URI 不在
nosessionWhiteList配置的白名单中(额外配置的免校验路径);
1.SQL注入漏洞
// MySQL 数据库if(databaseType.indexOf("mysql")>=0) stmt = conn.prepareStatement("select accessorysavename, accessoryname, info.information_id, info.informationtype, ch.channel_id, (select orgname from org_organization where org_id = ch.channeltype), ch.channeltype,info.informationTitle,ch.userDefine from oa_information info, oa_informationchannel ch, oa_informationaccessory acc where ch.channel_id = info.channel_id and info.information_id = acc.information_id and (LOWER(accessorytype) = 'jpg' or LOWER(accessorytype) = 'bmp' or LOWER(accessorytype) = 'jpeg' or LOWER(accessorytype) = 'gif') and info.informationstatus = 0 and channelidstring like '%$" + channelId + "$%' order by info.ORDERCODE desc, case when info.informationmodifytime is null then info.INFORMATIONISSUETIME else info.informationmodifytime end desc limit 0,"+num);// Oracle 数据库else if(databaseType.indexOf("oracle")>=0) stmt = conn.prepareStatement("select * from (select accessorysavename, accessoryname, info.information_id, info.informationtype, ch.channel_id, (select orgname from org_organization where org_id = ch.channeltype), ch.channeltype,info.informationTitle,ch.userDefine from oa_information info, oa_informationchannel ch, oa_informationaccessory acc where ch.channel_id = info.channel_id and info.information_id = acc.information_id and (LOWER(accessorytype) = 'jpg' or LOWER(accessorytype) = 'bmp' or LOWER(accessorytype) = 'jpeg' or LOWER(accessorytype) = 'gif') and info.informationstatus = 0 and channelidstring like '%$" + channelId + "$%' order by info.ORDERCODE desc, case when info.informationmodifytime is null then info.INFORMATIONISSUETIME else info.informationmodifytime end desc) where rownum <= "+num);// SQL Server 数据库else if(databaseType.indexOf("mssqlserver")>=0) stmt = conn.prepareStatement("select top "+num+" accessorysavename, accessoryname, info.information_id, info.informationtype, ch.channel_id, (select orgname from org_organization where org_id = ch.channeltype), ch.channeltype,info.informationTitle,ch.userDefine from oa_information info, oa_informationchannel ch, oa_informationaccessory acc where ch.channel_id = info.channel_id and info.information_id = acc.information_id and (LOWER(accessorytype) = 'jpg' or LOWER(accessorytype) = 'bmp' or LOWER(accessorytype) = 'jpeg' or LOWER(accessorytype) = 'gif') and info.informationstatus = 0 and channelidstring like '%$" + channelId + "$%' order by informationissuetime desc");
可以看到这是支持三种数据库类型的
channelId 和 num 是用户通过 request.getParameter 直接获取的可控参数
2.ssrf漏洞
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%@ page import="java.io.*"%><%@ page import="java.util.*"%><%@ page import="java.net.*"%><%response.setHeader("Pragma","no-cache");response.setDateHeader ("Expires", 0);response.setContentType("text/xml; charset=GBK");String rssUrl = request.getParameter("rssUrl");String charEncode = request.getParameter("charEncode");URL url = new URL(rssUrl);HttpURLConnection con = (HttpURLConnection) url.openConnection();String line = null;InputStream in = con.getInputStream();BufferedReader br = new BufferedReader(new InputStreamReader(in,charEncode));out.clearBuffer();while((line=br.readLine())!=null){//System.out.println("=============="+line);out.println(line);}in.close();con.disconnect();%>
很明显接收客户端传入的 rssUrl 参数 → 服务器端向该 URL 发起 HTTP 请求 → 将响应结果返回给客户端
4.文件读取漏洞
public/download_ftp/download.jsp
String path = request.getParameter("path");String filename = request.getParameter("FileName");String filepath = HSR.getRealPath("/upload/") + "/" + path + "/";java.io.File file = new java.io.File(filepath + filename);if (file.exists()) { response.setContentType("application/x-msdownload"); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileNameTemp + "\"");
java.io.FileInputStream fileInputStream = new java.io.FileInputStream(filepath + filename); java.io.OutputStream os = response.getOutputStream(); byte[] buffer = new byte[1024]; int n; while ((n = fileInputStream.read(buffer)) != -1) { os.write(buffer, 0, n); } fileInputStream.close(); os.close();}
path 和 FileName 为用户可控请求参数,未过滤 ../、..\ 等路径遍历
5.sql注入(getNextAutoCode)
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%request.setCharacterEncoding("UTF-8");String fieldId = request.getParameter("fieldId");String fieldName = request.getParameter("fieldName");String tableId = request.getParameter("tableId");String orgName = request.getParameter("orgName");response.setContentType("text/html; charset=UTF-8");response.setHeader("Cache-Control","no-cache");com.whir.ezoffice.customdb.customdb.bd.AutoCode ac = new com.whir.ezoffice.customdb.customdb.bd.AutoCode();String ret = ac.getAutoCode(fieldId, fieldName, tableId, orgName, null);out.print(ret);%>
直接调用并进入 com.whir.ezoffice.customdb.customdb.bd.AutoCode 这个 Java 类然后使用该类直接执行sql方法
流程图
前端请求(传fieldId、tableId等参数)→ JSP页面 ↓JSP接收参数 → 创建AutoCode实例 → 调用ac.getAutoCode(参数) ↓进入AutoCode.class → 执行编码生成逻辑 ↓AutoCode返回生成的编码 → JSP接收返回值 ↓JSP输出编码 → 前端接收结果
进入类 classes/com/whir/ezoffice/customdb/customdb/bd/AutoCode.class
核心触发漏洞的代码:
// 1. 拼接用户可控参数 var1(对应JSP的 fieldId)String[][] var15 = var14.executeQueryToStrArr2("select field_value, field_codevalue from tfield where field_id=" + var1);// 2. 拼接用户可控参数 var3(对应JSP的 tableId)String var21 = var14.executeQueryToStr("select table_name from ttable where TABLE_ID=" + var3);
#
AutoCode 方法的形参按声明顺序(var1→var2→var3→var4→var5)接收实参,与变量名无关
免责声明
- 本文档所涉及的技术分析、漏洞拆解及相关内容,仅面向合法授权的网络安全测试场景(如已获得系统所有者书面授权的渗透测试、代码审计),旨在帮助安全从业者、开发人员了解系统潜在风险,提升产品安全防护能力。
- 严禁任何个人或组织在未获得合法授权的情况下,利用本文档中的信息对任何计算机系统、网络进行攻击、入侵、数据窃取等违法违规操作。
- 本文档中涉及的代码片段、配置文件等均来自公开可查的技术资料或授权测试环境,仅用于技术分析和学习交流,不涉及任何商业机密或敏感信息泄露。
- 对于违反本声明、滥用相关技术造成的任何法律责任(包括但不限于民事赔偿、行政处罚、刑事责任),均由违规者自行承担,本文档作者及相关方不承担任何直接或间接责任。
- 如需对特定系统进行安全测试,请务必提前获得系统所有者的书面授权,并严格遵守《网络安全法》《数据安全法》《个人信息保护法》等相关法律法规及行业规范。
- 本文档内容仅为技术探讨,不代表对相关产品的安全性背书或质疑,相关产品的安全防护能力请以官方说明及实际测试结果为准。
#
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:韭要学JAVA安全 1《万户OA系统代码审计》