NC用友远程命令执行漏洞分析

0x00前言

这个漏洞是 国家电网公司信息与网络安全重点实验室 发现的,微信公众号做了简要分析,但是没有给出具体的poc。现在微信公众号已经将文章删除,但其他的安全公众号仍留有记录,看了一下分析过程,故想要跟着分析漏洞成因,编写poc,因此才有了这一篇分析文章。虽然此次漏洞公告上写的版本为NC6.5,但是实际上像较新版的1909 NCCloud(用友2018年11月发布的最新技术架构软件)等都是存在该漏洞的。

0x01分析过程

1.客户端分析

访问http://ip:port/index.jsp会提示有两种方法登录系统,一种是通过下载客户端、一种是使用浏览器访问。因为,浏览器访问的方式需要依赖不同用户设备上的java版本,IE浏览器,系统配置等环境因素,使用起来不是很方便,所以为了解决这些问题,用友提供了系统专用的UClient浏览器,可直接通过该浏览器访问nc而无需安装配置任意东西。

1592135084333

下载NClient并安装后,进入启动页面,可以选择添加应用。添加完后,在安装目录中可以看到所安装的应用。

1592135683158

1592135763897

点击app.esc发现直接启动了nc客户端,查看文件内容,发现执行了NClogin65.jar文件中的nc.starter.test.JStarter:

1592136037372

反编译NClogin65.jar,查看nc.starter.test.JStarter,调用nc.starter.ui.NCLauncher#main主要是与远程服务端通信,生成uI之类的操作。主要的通信代码并不在该jar包,继续在目录中寻找。

1592136244643

1592136335295

在nc_client_home\NCCACHE\CODE目录的子目录中有很多的jar包,其中external目录上的jar包是客户端通信的逻辑代码。

1592136581135

随便点了一两个包,发现类还不少,如果逐个看的话很耗费时间,效率还不高,看了分析的文章发现用javaagent把调用的类都打印出来的方法可以解决这个问题,具体原理感兴趣的可以去网上搜索相关的文章,具体可以代码可以参考javaagent项目中使用

之前在app.esc文件中可以看到启动jar的jvm配置信息,加上我们的javaagent的jar包,这样才能正常加载自己的javaagent。

1592137794616

启动后可以看到把所有调用的类都输出出来了。

1592137667520

这里可以配合idea的远程调试,添加参数jdwp:

1
-agentlib:jdwp=transport=dt_socket,server=y,address=8000

1592151755174

打开客户端,随便输入账号密码点击登录后,查看log可以看到有login字眼的类:

1592151973097

找到对应的nc.login.ui.LoginUISupport类,这个类方法很多,我一开始想的是通过一般登录都是带有request,response的,所以我就搜了request的关键字,在一个看起来比较像处理登录请求的方法下了断点,点击登录后果然是在这个地方断下来了。该方法主要是将输入的用户名、密码等值,在requestd类的变量赋值。

1592152447890

执行getInstance(),获取NCLocator的实例,并执行实例的lookup方法。

1592203909575

跟进nc.bs.framework.common.NCLocator#getInstance(java.util.Properties)

1592204019467

刚启动的时候locatorMap为空,则会在下面的判断分支中,创建RmiNCLocator实例并将该实例存放到locatorMap中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
locator = (NCLocator)locatorMap.get(key);
if (locator != null) {
return locator;
} else {
if (!isEmpty(locatorProvider)) {
locator = newInstance(locatorProvider);
} else if (!isEmpty(svcDispatchURL)) {
locator = newInstance("nc.bs.framework.rmi.RmiNCLocator");
} else {
locator = getDefaultLocator();
}

locator.init(props);
locatorMap.put(key, locator);
return locator;
}

获取到RmiNCLocator实例后,跟进到lookup方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//nc.bs.framework.rmi.RmiNCLocator#lookup

public Object lookup(String name) throws ComponentException {
Object result = null;

try {
result = this.remoteContext.lookup(name);
return result;
} catch (Throwable var4) {
if (var4 instanceof FrameworkRuntimeException) {
throw (FrameworkRuntimeException)var4;
} else {
throw new ComponentException(name, "Component resolve exception ", var4);
}
}
}

调用了this.remoteContext.lookup(name);,继续跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//nc.bs.framework.rmi.RemoteContextStub#lookup

public Object lookup(String name) {
Object so = this.proxyMap.get(name);
if (so != null) {
return so;
} else {
ComponentMetaVO metaVO = this.getMetaOnDemand(name);
if (metaVO == null) {
throw new ComponentNotFoundException(name, "no remote componnet found from server");
} else {
so = this.proxyMap.get(metaVO.getName());
if (so != null) {
return so;
} else {
RemoteAddressSelector ras = new GroupBasedRemoteAddressSelector(this.getRealTarget(metaVO), this.getServerGroup(metaVO));
so = RemoteProxyFactory.getDefault().createRemoteProxy(RemoteContextStub.class.getClassLoader(), metaVO, ras);
this.proxyMap.put(metaVO.getName(), so);
return so;
}
}
}
}

可以看到先从proxyMap中查看是否存在参数name的方法,如果存在则直接返回,不存在则进入另外的分支。因为这里我是刚启动的,所以该方法是不存在的,也可以直接将so赋值为null,进去到下面的分支去看看具体的执行流程。

1592209159419

跟进nc.bs.framework.rmi.RemoteContextStub#getMetaOnDemand,又可以看到调用了this.remoteMetaContext.lookup(name);方法,这个变量remoteMetaContext是nc.bs.framework.server.RemoteMetaContext类,那么这个类是怎么来的呢?

1592209487982

因为当前的类为nc.bs.framework.rmi.RemoteContextStub#RemoteContextStub,这里可以回到类构造函数,第65行创建了一个代理,并赋值到this.remoteMetaContext

1592212049147

了解过java代理的应该知道,不管用户调用代理对象的任何方法,该方法都会调用处理器的invoke方法,这里即是nc.bs.framework.rmi.RemoteInvocationHandler#invoke。不懂的可以先看这里

1592217600559

那么现在回到上面,跟进this.remoteMetaContext.lookup(name);,果然进入到了invoke方法。经过一番判断

执行this.sendRequest(method, args)方法。

1592218223159

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
//nc.bs.framework.rmi.RemoteInvocationHandler#sendRequest(java.lang.reflect.Method, java.lang.Object[])

public Object sendRequest(Method method, Object[] args) throws Throwable {
InvocationInfo ii = this.newInvocationInfo(method, args);
Address old = null;
int retry = 0;
ConnectorFailException error = null;

do {
Address target = this.ras.select();
if (old != null) {
Logger.error("connect to: " + old + " failed, now retry connect to: " + target);
if (old.equals(target)) {
try {
Thread.sleep(this.retryInterval);
} catch (Exception var13) {
}
}
}

this.restoreToken(ii, target);

try {
Object var8 = this.sendRequest(target, ii, method, args);
return var8;
} catch (ConnectorFailException var14) {
++retry;
old = target;
error = var14;
} finally {
this.storeToken(ii, target);
}
} while(retry < this.retryMax);

throw error;
}

继续跟进this.sendRequest(target, ii, method, args);,在第182行将ii序列化输出,发送到http://server:port/ServiceDispatcherServlet,并获取服务端返回的结果反序列化,回显到客户端。

1592219243047

1592219210083

到此客户端的处理流程大致分析完成,看到这里大家可能有会对上面客户端将类序列化发往服务端,那服务端肯定要反序列化呀,会不会有问题?别急,继续往下看。

2.服务端分析

先来分析jndi注入的形成

nc.bs.framework.comn.serv.CommonServletDispatcher#doPost第38行下断点。

1592236432591

跟进this.rmiHandler.handle(new HttpRMIContext(request, response));,跟进后在第85行继续跟进this.doHandle(rmiCtx);

1592236486535

在第153行出现处理客户端提交内容的,继续跟进:

1592236605328

在第282行可以看到直接将输入流的内容反序列化了,代码执行过程中完全没有任何的过滤,确实存在触发反序列化漏洞,这里先不管,继续往下。

1592236746588

这里注意的是第286行将反序列化后的类赋予到抽象类nc.bs.framework.rmi.server.AbstractRMIContext#invInfo的invInfo变量里,这个变量在下面用到。

1592220689757

回到刚才的第二个断点,跟进result.result = this.invokeBeanMethod(rmiCtx);,这里第333行就是上面说到的invinfo变量,实际就是反序列化后的类。

这里有两个分支,不管是哪个都存在jndi注入,因为这里lookup的参数service是可控的,所以必然存在漏洞。

1592320624956

3.效果演示

这里就不给poc了,如果看懂了上面的过程其实也不难,实际就是构造一个InvocationInfo类,并将servicename的值设置为远程恶意类,序列化后发送到服务端触发jndi注入即可。

1592322620367

image-20200617093259154

1592323482077

4.前面的反序列化

上面发现的反序列化根本都没有过滤的,为啥还要这么麻烦要jndi注入呢,直接反序列化不香嘛?看了web的依赖环境,commons-collections3.2,那不是现成的利用嘛。

image-20200617105253981

直接ysoserial生成恶意类发送,弹计算器。

1592236873117

整个调用栈如下:

image-20200617105315331

注:在NCCloud 1909的版本中该依赖包为较新版,此利用链不可用。

0X02 最后说几句

漏洞过程并不是太复杂,应该不难理解的。实际上,用友NC系统多处存在未过滤的反序列化漏洞,不过由于新版NC将依赖版本更新了,yso的大多数利用链都不能使用了,因此需要重新寻找新的利用链。

0X03参考链接