Serialization must die!

When it comes to security issues caused by random object serialization, author Arshan Dabirsiaghi suggests five performance metrics to help us assess   the health of enterprise Java applications. While @frohoff , @gebl ,  and @breenmachine are working  together to address Java security issues (which this article classifies under the term "serial event"), I have come up with a deserialization alternative. Where will our customers go? Do they have a brighter future? This article will find out for you one by one. From the author's point of view: the future is dark, and you can't see your fingers. 
In this article, we'll take a look at Kyro, the "cutting edge" serialization database. As we all know, the Kryo framework has been used in many well-known software , but Kryo is also a database, a database that may be used by many downstream enterprises.  The following will be broken down in detail. This article is compiled and organized by OneAPM , the domestic  ITOM  management platform .

 

How many fish are in the water

The amazing thing about Kryo is that you can use it to serialize or deserialize any Java type, not necessarily just those marked with java.io.Serializable. In many cases, this greatly simplifies development, such as serializing existing or uncontrollable features.

If you use Kryo to deserialize an object, you must declare the expected type. Here is an example:

// serializingKryo kryo = new Kryo ();

AcmeMessage message = buildAcmeMessage(); // some domain objectOutput out = new Output(response.getOutputStream())

kryo.writeObject(out, myObject);// deserializingInput in = new Input(request.getInputStream());

AcmeMessage message = kryo.readObject(in, AcmeMessage.class);
  

This API design is a big step forward, at least for the  Java  Object Serialization Specification. This specification forces the user to deserialize everything the attacker sends, and then the user has to hope that whatever they're looking for later doesn't get caught - which is absurd. This is the first hurdle an attacker encounters: how to trick the user into deserializing an arbitrary type?

 

Stealing the beam and changing the column

This trap is actually easier to avoid. In real life, any domain object that people send over and over has collections - maps, lists, arrays, or similar data. Consider the following example object:

/* AcmeMessage.java */private List<AcmeRecipient> recipients;
  

The point is that this code doesn't exist. The only thing that matters is compile time. I was in the running environment when the attack happened. This means that, for us, the code should be understood this way instead:

/* AcmeMessage.java */private List<Object> recipients;
  

So, if our target application deserializes a certain  AcmeMessage type, we can fill the receiving field with some unexpected arbitrary type, since all types extend from  Object. This way, as is the case with the Sequence Gala attack, the types used by the attacker must still be on the classpath of the victim application, library, or server.

当然,如果应用程序尝试将其中一个对象用作 AcmeMessage,就会引爆问题,但我们的攻击会在那之前取得成果。请参见实际测试用例

 

驾驭类型集市

我们可以发送任意类型。那么,是否有限制条件呢?经典的序列化的规则并不多 - 只需要一个标记为 java.io.Serializable 的类便足矣。细查代码后,我们似乎只发现一个规则:类必须具备一个无参构造函数。现在感受到了吗?

Kryo 在较高的层面上反序列化了这一模型:

*1. 获取指定类型的零参构造函数

1.如果是私有构造函数,将其标为可访问

2.调用此构造函数

3.对于类型中的每一个字段:

反序列化消息中传递的字段(递归)

将字段分配给第 3 步创建的新类型*

有很多方法将这一行为重写为更加滥用的行为,但我们先重点看看默认设置吧。我之前其实已经提到了漏洞,各位发现了吗?

 

几乎已成序列盛会

序列盛会的发起人们希望人们明白这一点:问题并不在于这四五个类。模型的根基已断。我们是怎样在 MD5 完全遭到破坏前便得知 MD5 已损,各位想必是知道的。这里也是一样的原理。

如果你让开发人员在其类型的 Serializable#readObject() 方法中指定任意行为,就可以将这些行为的副作用串联起来,给隐藏的火种火上浇油。这就是事情的真相。

此处也并无甚区别。

我可以提交位于你的类路径之上的任意类,你(受害人应用程序)也可以调用其构造函数。开发人员可以在其构造函数中放置任意数据。但这并不能保证避开副作用。

下面来分析一些攻击策略和我们将要用到的相应工具。

 

在构造函数中滥用静态副作用

下面是 ColdFusion 的一个类,能够极大地打击你可以控制的单例。

package coldfusion.syndication;import com.sun.syndication.io.impl.CFDateParser;public class FeedDateParser implements CFDateParser {    private FeedDateParser() {

        DateParser.registerCFDateParser(this);

     }    

    ...}
  

显然这一方法仅可调用一次。如果我给你发送一个经过 Kryo 序列化且拥有空白字段的 FeedDateParser,会有什么后果?答案:超级有效的应用程序 DoS 攻击。我会用自己的恶意单例来重写这一单例,并在其中填入各种空白字段成员。由于所有人都在使用,于是会导致各处触发 NullPointerExceptions 异常。仅仅一个 HTTP 请求便可让你毫无招架之力。

请注意,这里的构造函数是私有的。这对 Kryo 并无影响。但对我来说就有些棘手了。如果我把一个构造函数标为私有,我就希望只能以我允许的方式创建它。这可能是有充分原因的 - 甚至是安全原因!尽管如此,Kryo 用户将“不支持私有构造函数”报告为程序错误,然后函数库维护人员就相应地添加了支持。奇怪的是,发起人在具有重大意义的功能请求中,认为不能以更安全的方式将对象实例化是安全问题。他们可以在看到这一请求后几年内实现更安全的对象实例化,但这仍然不是默认功能。

构造函数还可造成其他多种破坏性影响。尽管我确定有一种影响,但利用我所能找到的策略却未能发现致命远程执行代码。

 

滥用 finalize() 清理工具

构造函数并非我们可用以影响变更的唯一副作用来源。

如果某个类实现了Object#finalize()方法,Java 会在对象被作为垃圾回收之前调用此方法。开发人员利用这一方法来清理一直未得到恰当清理的所有非 JVM 资源。由于是自动调用,这一方法中可能发生的所有副作用都可能遭到滥用,而应用程序根本无需在你的恶意对象上运行!事实上,在利用漏洞的过程中必须将其抛开。

你可以利用一些类型来玩些花样。

 

1 号攻击:删除任意文件 (org.jpedal.io.ObjectStore)

到目前为止,finalize() 中最常被滥用的策略就是扰乱文件。这确实有些用处;类型在某种程度上有模板文件“撑腰”,而 finalize() 是一个表明文件可被清理的明确信号。

我借此发现的第一个类型恰巧也是在 ColdFusion 10 之中:org.jpedal.io.ObjectStore。对类进行分析以确定潜在的 Kryo finalize() 利用工具时,只需查看两件事:零参构造函数和 finalize() 方法。这些是唯一会发生的事情。只要字段也是可以用零参构造函数创建的对象,你就可以控制字段。以下是构造函数:

public ObjectStore() {

  init();

    }
  

下面这张截图表明,在 Kryo 调用 readObject() 期间,默认构造函数被调用: 

其实也没做什么。无论如何,其所做作为都已撤销,因为一旦构造函数执行完毕,Kryo 就在其状态之上复制了我们的状态。然后就是 finalize():

protected void finalize() { ...

 flush(); ...}

protected void flush() { .../**

 * flush any image data serialized as bytes

 */ Iterator filesTodelete = imagesOnDiskAsBytes.keySet().iterator(); 

    while(filesTodelete.hasNext()) { 

     final Object file = filesTodelete.next();  

      if(file! = null){   

        final File delete_file = new File((String)imagesOnDiskAsBytes.get(file));        if(delete_file.exists()) {

         delete_file.delete();

    }

   }
} ...} 
 

参见 testCF10_JPedal() 测试用例,验证这一工具。

 

2 号攻击:内存损坏(多个类型)

要追寻这一线索,我自然不是最佳人选,但我们可以用下面的方法来创建内存损坏漏洞基元。它们都会在由用户控制的内存地址上调用 free(): 
** com.sun.jna.Memory, 连同 Vert.x

com.sun.medialib.codec.jpeg.Encoder(无可用资源),ColdFusion 10 的一部分

com.sun.medialib.codec.png.Decoder(无可用资源),ColdFusion 10 的一部分

com.xuggle.ferry.AtomicInteger,Liferay (亦即 Xuggle 数据库)的一部分*

除此之外当然还有很多,但以上这些已经可用于众多常见平台。下面我们来看看 com.sun.jna.Memory#finalize() 调用:

 

free() 确实已被转交给 stdlib.h::free() 函数。如果没有相应的初级知识,便极为危险。这个测试用例可证实内存损坏。测试已被禁用,但你可以将其从公共测试更改为私有测试,从而将其激活。激活后,测试用例会使 JVM 崩溃,如下所示:

 

我要在此重申:若读取的 Kryo 对象来自不受信任的资源且具备其中任一工具,就容易受单次激发应用破坏的影响。

 

3 号攻击:关闭所有文件描述符 (java.net.DatagramSocket)

这些攻击与其他攻击类型极为相似,我至今仍未完全摸索出来。尽管如此,其中最明显的就是java.net.PlainDatagramSocketImpl了,连同 JRE。

该类型中并无显式构造函数,也无超类,直至java.lang.Object。尽管如此,一个java.io.FileDescriptor字段会在其祖父字段 java.net.DatagramSocketImpl 中调用 fd。

FileDescriptor类型具有一个零参构造函数,此外便仅包含一个简单的整型,代表 OS 层面的文件描述符。finalize() 可启动函数:

protected void finalize() {

 close();

}/**

 * Close the socket.

  */protected void close() { 

  if (fd != null) {

   datagramSocketClose();

   …}
  

这一方法较为原生,且正如 HotSpot 原生层的这段代码所示,只是来自 unistd.h 极为普通的 close()

int os::close(int fd) { return ::close(fd);}int os::socket_close(int fd) { return ::close(fd);}
  

参见 testDatagramSocket()测试用例,验证这一工具。攻击者可以提交众多工具实例并关闭所有可能的文件描述符。这可以防止与用户产生套接口通信、读取文件或写入以及任何 IPC。

 

总结

可以让 Kryo 调用其他方法,比如 compareTo()、entrySet()、toString(),等等,总之,能找出的小玩意多得很。

底线应当是,任何现代应用程序在其类路径上都可能存在一个能够删除文件、关闭文件描述符或使 JVM 完全崩溃的工具。这意味着,人们应当普遍意识到:允许 Kryo 操作不受信任的数据流是不安全的。

可能有人会极力反驳,如果让默认对象实例化策略不调用类型的构造函数,Kryo 就会安全得多。采用 JVM 技巧便可实现这一目标,我们会在下一篇文章中细细解析序列化程序这一问题。利用这一构造函数较少的实例化,可以防止副作用波及构造函数及其 finalize() 方法。

但如果你确实这么做了,Kryo 再也不需要零参构造函数便可创建新的对象 - 这样攻击者就能够实例化更多类型!这一权衡之道将导致大量工具可为攻击者所用,但在这些工具上执行方法的机会却少了 - 不再有构造函数,也不再有定型化方法。

下次,我们会对一家尝试过相同办法但仍然身处困境的企业进行分析 - XStream!

原文地址:https://dzone.com/articles/serialization-must-die

国内 ITOM 管理平台OneAPM致力于帮助企业用户提供全栈式的性能管理以及 IT 运维管理服务,通过一个探针就能够完成日志分析、安全防护、APM 基础组件监控、集成报警以及大数据分析等功能。想阅读更多优秀文章,请访问 OneAPM 官方技术博客。 
本文转自 OneAPM 官方博客

+

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326224850&siteId=291194637