0x01 概述
近期,爆出了 U8cloud ServiceDispatcherServlet 接口的反序列化漏洞。在对该漏洞进行分析时,我们发现 NC 也曾出现过 ServiceDispatcherServlet 接口的反序列化漏洞。经过分析后发现,这两个漏洞的功能代码实现方式并不相同。但二者都实现了自定义的序列化过程(本文简称 NetObjectStream),并且对传输的序列化流进行了合法性检测。因此,该漏洞的利用条件需要的自定义序列化类生成序列化数据进行攻击。
在漏洞分析过程中,我们发现并验证了其他接口也使用了自定义的序列化类(NetObjectStream)进行反序列化解析数据,存在反序列化漏洞(已报送给国家漏洞监管单位和厂商进行修复)。
0x02 补丁分析
官方的修复方式是通过增加序列化对象的限定类来防止反序列化漏洞。具体是在 NetObjectInputStream
和 NCObjectInputStream
类中增加了新的构造方法,在构造方法中增加了 InvocationInfo.class
、ESBContextForNC.class
两个白名单限制类。
未修复
NetObjectInputStream objIn = new NetObjectInputStream(new ByteArrayInputStream(bytes));
修复后
NetObjectInputStream objIn = new NetObjectInputStream(new ByteArrayInputStream(bytes), new Class[]{InvocationInfo.class, ESBContextForNC.class});
在 NetObjectInputStream
和 NCObjectInputStream
类中增加了新的构造方法 [1],将上述的两个限定类传入到私有变量 classes
中用于检测处理。
public NetObjectInputStream(InputStream in, Class[] classes) throws IOException {
this.bufferSize = 1024;
ObjectResolver resolver = new NCObjectResolver();
this.objIn = new NCObjectInputStream(new NCInputStream(in), resolver, classes); // [1]
}
在 NCObjectInputStream
方法中将 classes 传入了 this.classes 变量中,即将 InvocationInfo.class, ESBContextForNC.class
两个限定类传入到 this.classes [2] 中。
由于传入的 resolver 是个对象,所以会调用 this.enableResolveObject(true);
该方法的作用是当调用 enableResolveObject(true)
[3] 时,在反序列化过程中通过调用 resolveObject()
方法来解析特定类型的对象。
public NCObjectInputStream(InputStream in, ObjectResolver resolver, Class[] classes) throws IOException {
super(in);
this.check = false;
this.resolver = resolver;
this.classes = classes; // [2]
if (this.resolver != null) {
this.enableResolveObject(true); // [3]
}
}
由于开启了 enableResolveObject(true)
,当序列化对象在调用 readObject()
方法中,默认调用其 resolveObject()
方法。
方法内部通过 isAssignableFrom()
方法 [3] 判断传入的字节流是否为 InvocationInfo.class
, ESBContextForNC.class
的实现类或子类,如果不是,则会抛出异常,导致字节流不能够被反序列化,从而避免了反序列化漏洞的产生。
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
try {
Class superImpl = super.resolveClass(desc);
if(classes!=null&&classes.length>0){
if(this.check) return superImpl;
boolean legal = false;
for(Class c:classes){
if(c.isAssignableFrom(superImpl)){ // [3]
legal = true; // [4]
break;
}
}
if(legal){
this.check = true;
return superImpl;
}else{
throw new IllegalArgumentException("####### class::" + superImpl);
}
}
return superImpl;
} catch (ClassNotFoundException nfe) {
...
}
}
0x03 路由分析
U8cloud 在 web.xml
文件中配置 ServiceDispatcher 的 servlet 映射,URL路由为 /ServiceDispatcherServlet
,servlet 命名为 CommonServletDispatcher
。
<servlet-mapping>
<servlet-name>CommonServletDispatcher</servlet-name>
<url-pattern>/ServiceDispatcherServlet</url-pattern>
</servlet-mapping>
跟进名为 CommonServletDispatcher
的 servlet ,发现该 servlet 映射到了 nc.bs.framework.comn.serv.CommonServletDispatcher
类中,并在初始化中定义了一个 service
参数 [5] ,该参数映射到了 nc.bs.framework.comn.serv.ServiceDispatcher
类[6]中。
<servlet>
<servlet-name>CommonServletDispatcher</servlet-name>
<servlet-class>nc.bs.framework.comn.serv.CommonServletDispatcher</servlet-class>
<init-param>
<param-name>service</param-name> // [5]
<param-value>nc.bs.framework.comn.serv.ServiceDispatcher</param-value> // [6]
</init-param>
<load-on-startup>10</load-on-startup>
</servlet>
0x04 漏洞分析
CommonServletDispatcher
类继承了 HttpServlet
接口并实现了 init()
、destroy()
、 doGet()
和 doPost()
方法。在 init()
方法中通过 web.xml
文件定义的 service
参数获取对应的 nc.bs.framework.comn.serv.ServiceDispatcher
类 [7] 并进行实例化 [8]。
public class CommonServletDispatcher extends HttpServlet {
private ServiceHandler serviceHandler = null;
public void init() throws ServletException {
String targetname = null;
Throwable cause = null;
this.log.debug("ServletDispatcher.initing......");
if ((targetname = this.getInitParameter("service")) != null) { // [7]
try {
Class handlerClass = Class.forName(targetname);
this.serviceHandler = (ServiceHandler)handlerClass.newInstance();// [8]
}
...
}
...
}
}
在 doGet
方法中定义了 label118 的循环体,循环中通过 this.serviceHandler.execCall(request, response);
调用了上文中已经实例化后的 nc.bs.framework.comn.serv.ServiceDispatcher
类的 execCall
方法并传入 request
和 response
。
this.serviceHandler.execCall(request, response);
跟进 ServiceDispatcher
类的 execCall()
方法,通过调用本类中的静态方法 readObject()
[9] 对传入的 request.getInputStream()
序列化流数据进行反序列化操作。
int[] lsizes = new int[1];
long wBeginTime;
try {
ThreadTracer.getInstance().beginReadFromClient();
wBeginTime = System.currentTimeMillis();
invInfo = (InvocationInfo)readObject(request.getInputStream(), streamRet, lsizes); // [9]
}
跟进 ServiceDispatcher
类的 readObject()
方法,该方法首先使用 BufferedInputStream
读取流数据,通过 NetObjectInputStream
类的 readInt
方法对流数据进行移位运算解析得到流的字节长度,对比 BufferedInputStream
类中读取的长度来判断是否为一个正常的可以被 NetObjectInputStream
解析的反序列化流数据。
如果判断是一个可以被 NetObjectInputStream
[10] 读取的流,则读取该流并进行 readObject()
[11] 反序列化操作,这时就可以通过构造利用链进行反序列化攻击。
public static Object readObject(InputStream in, boolean[] retValue, int[] lsizes) throws IOException, ClassNotFoundException {
BufferedInputStream bin = new BufferedInputStream(in);
int len = NetObjectInputStream.readInt(bin);
byte[] bytes = new byte[len];
int readLen = bin.read(bytes);
int tmpLen;
for(lsizes[0] = readLen; readLen < len; readLen += tmpLen) {
tmpLen = bin.read(bytes, readLen, len - readLen);
if (tmpLen < 0) {
break;
}
}
if (readLen < len) { // [10]
throw new EOFException("ReadObject EOF error readLen: " + readLen + " expected: " + len);
} else {
NetObjectInputStream objIn = new NetObjectInputStream(new ByteArrayInputStream(bytes));
if (retValue != null) {
retValue[0] = objIn.isCompressed();
retValue[1] = objIn.isEncrypted();
}
return objIn.readObject(); // [11]
}
}
0x05 漏洞利用
U8 Cloud 中依赖了 commons-collections-3.2.1
,则可以通过cc链达到反序列化利用的目的。
在利用过 CC 链利用过程中,我们尝试直接通过 CC 链攻击时发现并不能有效地成功利用。经过调试后发现,原因出在该接口的序列化数据中,正如上文提到需要经过 NetObjectInputStream
进行序列化数据检测,而我们则使用的是 ObjectOutputStream
进行生成的序列化数据,导致了序列化数据不合法。
于是我们尝试通过 NetObjectOutputStream
生成序列化序列化数据,这样就通过了 NetObjectInputStream
对序列化流的合法性检测,成功达到了反序列化攻击的目的。
// 序列化对象
public static void serialize(Object obj) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
NetObjectOutputStream noo = new NetObjectOutputStream(bao);
noo.writeObject(obj);
noo.flush();
noo.close();
FileOutputStream fos = new FileOutputStream("./ServiceDispatcherServlet.ser");
NetObjectOutputStream.writeObject(fos, obj);
}
0x06 总结
本文对 U8Cloud ServiceDispatcher 接口反序列化漏洞原理进行跟踪分析,讲述了该漏洞是如何被利用以及官方的修复方式。同时,在漏洞分析过程中,我们发现并验证了其他接口也使用了自定义的序列化类(NetObjectStream)进行反序列化解析数据,存在反序列化漏洞(已报送给国家漏洞监管单位和厂商进行修复)。
Goby 欢迎表哥/表姐们加入我们的社区大家庭,一起交流技术、生活趣事、奇闻八卦,结交无数白帽好友。
也欢迎投稿到 Goby(Goby 介绍/扫描/口令爆破/漏洞利用/插件开发/ PoC 编写/ IP 库使用场景/ Webshell /漏洞分析 等文章均可),审核通过后可奖励 Goby 红队版,快来加入微信群体验吧~~~
文章来自Goby社区成员:Gryffinbit@白帽汇安全研究院,转载请注明出处。
微信群:公众号发暗号“加群”,参与积分商城、抽奖等众多有趣的活动
获取版本:https://gobysec.net/sale