办公软件历史远程命令执行漏洞分析

阅读量256956

|评论1

|

发布时间 : 2020-06-17 15:30:00

 

0x00前言

这个漏洞是 国家电网公司信息与网络安全重点实验室 发现的,并在公众号上写了比较详细的分析,但是没有给出具体的poc。现在微信公众号已经将文章删除,但其他的安全公众号仍留有记录,看了一下分析过程,故想要跟着分析漏洞成因,编写poc,因此才有了这一篇分析文章。

 

0x01分析过程

1.客户端分析

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

//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);,继续跟进:

//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,进去到下面的分支。

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

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

了解java代理的都知道不管用户调用代理对象的任何方法,该方法都会调用处理器(即Proxy.newProxyInstance()的第三个参数)的invoke方法,这里即是nc.bs.framework.rmi.RemoteInvocationHandler#invoke。不懂的可以先看这里

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

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

//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 ,并获取服务端返回的结果反序列化,回显到客户端。

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

2.服务端分析

先来分析jndi注入的形成

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

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

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

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

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

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

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

3.效果演示

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

4.前面的反序列化

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

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

整个调用栈如下:

 

0X02 最后说几句

漏洞过程并不是太复杂,应该不难理解的。因第一次写文章且本人水平有限,故有错误的地方望大佬们扶正,手下留情。

 

0X03参考链接

本文由Drea1v1原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/208726

安全客 - 有思想的安全新媒体

分享到:微信
+15赞
收藏
Drea1v1
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66