IPFxxFileService 文件上传限制不当漏洞
问题发生在ServiceDispatcherServlet路由,这也是用友U8cloud反序列化漏洞主要接口
WEB-INF/web.xml
<servlet>
<servlet-name>CommonServletDispatcher</servlet-name>
<servlet-class>nc.bs.framework.comn.serv.CommonServletDispatcher</servlet-class>
<init-param>
<param-name>service</param-name>
<param-value>nc.bs.framework.comn.serv.ServiceDispatcher</param-value>
</init-param>
<load-on-startup>10</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CommonServletDispatcher</servlet-name>
<url-pattern>/ServiceDispatcherServlet</url-pattern>
</servlet-mapping>
找到对应的servlet为CommonServletDispatcher
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long time = System.currentTimeMillis();
if (Profiler.log.isInfoEnabled()) {
Profiler.log.info("ServletDispatcher is starting to service......");
}
response.setContentType("application/x-java-serialized-object");
...
var12 = true;
this.serviceHandler.execCall(request, response);
var12 = false;
break label118;
....
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
doPost直接重用了doGet的逻辑,可以直接看doGet
首先会通过this.serviceHandler处理请求,根据web.xml可以知道serviceHandler为nc.bs.framework.comn.serv.ServiceDispatcher,看它的execCall方法
public void execCall(HttpServletRequest request, HttpServletResponse response) throws Throwable {
....
invInfo = (InvocationInfo) NetObjectInputStream.readObject(request.getInputStream(), streamRet);
invInfo.setServerName(ServerConfiguration.getServerConfiguration().getServerName());
invInfo.setServerHost(request.getServerName());
invInfo.setServerPort(request.getServerPort());
invInfo.setRemoteHost(request.getRemoteAddr());
invInfo.setRemotePort(request.getRemotePort());
....
TokenUtil.getInstance().vertifyToken(invInfo.getToken(), invInfo.getServiceName(), this.getAddr(request));
result.result = this.invokeBeanMethod(
invInfo.getModule(),
invInfo.getServiceName(),
invInfo.getMethodName(),
invInfo.getParametertypes(),
invInfo.getParameters()
);
....
}
execCall首先对POST传来的序列化流进行readObject反序列化然后获取InvocationInfo的每个属性,然后经过一层token验证传给invokeBeanMethod进行调用。
public void vertifyToken(String token, String service, String clientIP) {
if (ServerConfiguration.getServerConfiguration().isSingle() ||
!ServerConfiguration.getServerConfiguration().isMaster()) {
if (!this.isTrustService(service)) {
if (!this.isTustIP(clientIP)) {
this.vertifyTokenIllegal(token, service);
}
}
}
}
会调用vertifyTokenIllegal验证token
private void vertifyTokenIllegal(String token, String service) {
if (StringUtil.isEmptyWithTrim(token)) {
throw new BusinessRuntimeException("invalid orginal token(null), please login!" + service);
} else {
String userCode = InvocationInfoProxy.getInstance().getUserCode();
String curToken = this.genToken(userCode);
if (!curToken.equalsIgnoreCase(token)) {
throw new BusinessRuntimeException("token error!, please login!" + service);
}
}
}
首先就是从InvocationInfo接收usercode然后 计算token
public String genToken(String userCode) {
byte[] md5 = this.md5(this.getTokenSeed(), userCode.getBytes());
return MD5Util.byteToHexString(md5);
}
读取一个seed和usercode进行md5加密,如果服务端计算得到token和用户传来的一样,则验证通过。
private byte[] getTokenSeed() {
return this.tokenSeed;
}
829补丁更新后,tokenseed不再硬编码,tokenseed文件在/ierp/bin/Tenant.properties所以伪造token走不通,但是发现在进行token验证前会验证服务是否可信,可信就不会验证token
if (!this.isTrustService(service)) {
private boolean isTrustService(String service) {
if (StringUtil.isEmptyWithTrim(service)) {
return false;
} else {
return this.trustServiceList.contains(service);
}
}
可信服务配置在
private static final String srvFilePath = "/ierp/bin/token/trustServiceList.conf";


829补丁加了一个nc.bs.framework.core.service.IHRMultiServicesInvorker这个可信服务可以用来绕过token验证。

所以可以传入的servicename为IHRMultiServicesInvorker
private Object invokeBeanMethod(String module, String beanName, String methodName, Class[] parameterTypes, Object[] beanParameters) throws Throwable {
...
if (module == null) {
o = this.remoteCtx.lookup(beanName);
}
else {
Context moduleCtx = (Context)this.ctxMap.get(module);
if (moduleCtx == null) {
Properties props = new Properties();
props.put("nc.targetModule", module);
props.put("nc.locator.provider", "nc.bs.framework.server.ModuleNCLocator");
moduleCtx = NCLocator.getInstance(props);
this.ctxMap.put(module, moduleCtx);
}
o = ((Context)moduleCtx).lookup(beanName);
}
...
...
Method bm = o.getClass().getMethod(methodName, parameterTypes);
bm.setAccessible(true);
if (bm == null) {
}
Object result = bm.invoke(o, beanParameters);
}
payload里面的module为空,则会使用nc.bs.framework.server.ModuleNCLocator根据beanName查找对应的bean,找到它的实现类IHRMultiServicesInvorker,然后获取到它的multiStrServicesInvorker方法然后执行
ModuleNCLocator#lookup
AbstractContainer#lookup

Method bm = o.getClass().getMethod(methodName, parameterTypes);
bm.setAccessible(true);
if (bm == null) {
}
Object result = bm.invoke(o, beanParameters);
public String multiStrServicesInvorker(List<String> servicenames,
List<List<String>> methodnames,
List<List<Class[]>> paramsClass,
List<List<Object[]>> params,
List<Map> assinfo) throws Exception {
Object serviceObject = null;
new ArrayList();
List smname = null;
int len = servicenames.size();
String resultjson = null;
Expression e = null;
Method sm = null;
for (int i = 0; i < len; ++i) {
serviceObject = NCLocator.getInstance().lookup((String) servicenames.get(i));
smname = (List) methodnames.get(i);
int mlen = smname.size();
for (int j = 0; j < mlen; ++j) {
sm = serviceObject.getClass().getMethod(
(String) smname.get(j),
(Class[]) ((Class[]) ((List) paramsClass.get(i)).get(j))
);
sm.setAccessible(true);
Object o = sm.invoke(
serviceObject,
(Object[]) ((Object[]) ((List) params.get(i)).get(j))
);
resultjson = JSON.toJSONString(o);
}
}
return resultjson;
}
payload传入的服务名是nc.itf.uap.pfxx.IPFxxFileService,方法名为writeDocToXMLFile,参数为列表,键是字节列表,值的字符串对应文件数据和文件路径,然后执行。
跟踪到nc.itf.uap.pfxx.IPFxxFileService#writeDocToXMLFile找到实现类PFxxFileServiceImpl#writeDocToXMLFile
public File writeDocToXMLFile(byte[] filedata, String filename) throws BusinessException {
try {
return FileUtils.writeBytesToFile(filedata, filename);
} catch (Exception var4) {
Exception e = var4;
Logger.error("Writing File error!", e);
throw new BusinessException("Writing File error!");
}
}
public static File writeBytesToFile(byte[] filedata, String filename) throws IOException {
File file = new File(filename);
File pFile = file.getParentFile();
if (!pFile.exists()) {
pFile.mkdirs();
}
Debug.debug(NCLangResOnserver.getInstance().getStrByID("uffactory_hyeaa", "UPPuffactory_hyeaa-000521", (String)null, new String[]{file.getAbsolutePath()}));
FileOutputStream outStream = new FileOutputStream(file);
outStream.write(filedata);
outStream.close();
Debug.debug(NCLangResOnserver.getInstance().getStrByID("uffactory_hyeaa", "UPPuffactory_hyeaa-000522", (String)null, new String[]{file.getAbsolutePath()}));
return file;
}
可以直接进行文件写入,漏洞触发。整个过程没有对参数进行任何限制。
总结一下流程。
CommonServletDispatcher#doGet
ServiceDispatcher#execCall
ServiceDispatcher#invokeBeanMethod
HRMultiServicesInvorkerImpl#HRMultiServicesInvorkerImpl
PFxxFileServiceImpl#writeDocToXMLFile
FileUtils#writeBytesToFile
poc测试


补丁分析

修了FileUtils这个类
public static File writeBytesToFile(byte[] filedata, String filename) throws IOException {
File file = new File(filename);
String ctxPath = RuntimeEnv.getInstance().getCanonicalNCHome();
String realPath = ctxPath + File.separator + "webapps" + File.separator + "u8c_web" + File.separator;
if (file.getCanonicalPath().startsWith((new File(realPath)).getCanonicalPath())) {
throw new IOException("Illegal File Path");
} else {
File pFile = file.getParentFile();
if (!pFile.exists()) {
pFile.mkdirs();
}
Debug.debug(NCLangResOnserver.getInstance().getStrByID(
"uffactory_hyeaa",
"UPPuffactory_hyeaa-000521",
(String)null,
new String[]{file.getAbsolutePath()}
));
try (FileOutputStream outStream = new FileOutputStream(file)) {
outStream.write(filedata);
}
Debug.debug(NCLangResOnserver.getInstance().getStrByID(
"uffactory_hyeaa",
"UPPuffactory_hyeaa-000522",
(String)null,
new String[]{file.getAbsolutePath()}
));
return file;
}
}

获取了U8 Cloud的安装根目录并且拼接出来并且使用getCanonicalPath获取webapps/u8c_web/的真实路径,禁止上传到web根目录,但可信服务的问题没有修改,仍然可以任意文件上传到其他目录,,所以可能还有其他利用手法。